├── large.jpg
├── test_img.jpg
├── scissors
├── util.js
├── scissorsLines.js
├── bucketQueue.js
├── scissorsServer.js
├── pointQueue.js
├── scissorsClient.js
├── maskDrawing.js
├── scissorsWorker.js
└── scissors.js
├── sky_scissors_instructions.html
└── mturkScissors.html
/large.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chronos-Sk/intelligent-scissors-js/HEAD/large.jpg
--------------------------------------------------------------------------------
/test_img.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chronos-Sk/intelligent-scissors-js/HEAD/test_img.jpg
--------------------------------------------------------------------------------
/scissors/util.js:
--------------------------------------------------------------------------------
1 | function Point(x,y) {
2 | this.x = x;
3 | this.y = y;
4 | }
5 |
6 | Point.prototype.equals = function(q) {
7 | if ( !q ) {
8 | return false;
9 | }
10 |
11 | return (this.x == q.x) && (this.y == q.y);
12 | };
13 |
14 | Point.prototype.toString = function() {
15 | return "(" + this.x + ", " + this.y + ")";
16 | };
17 |
18 | Point.prototype.dist = function(p) {
19 | return Math.sqrt(Math.pow(this.x-p.x,2) + Math.pow(this.y-p.y,2));
20 | };
21 |
22 | Point.prototype.index = function(width) {
23 | return this.y*width + this.x;
24 | };
25 |
26 | Point.prototype.translate = function(tx, ty) {
27 | this.x += tx;
28 | this.y += ty;
29 | };
30 |
31 | function index(i, j, width) {
32 | return i*width + j;
33 | }
34 |
35 | function fromIndex(idx, width) {
36 | return new Point(idx % width, Math.floor(idx / width));
37 | }
38 |
39 | function translate(p, tx, ty) {
40 | if ( !p ) {
41 | return p;
42 | }
43 |
44 | return new Point(p.x + tx, p.y + ty);
45 | }
46 |
47 | //Converts absolute coordinates to element coordinates.
48 | function getRelativePoint(element, x, y) {
49 | var p = computeOffset(element);
50 |
51 | p.x = x - p.x;
52 | p.y = y - p.y;
53 |
54 | // Eclipse has a nonsensical type warning here for some reason. Can't figure out why.
55 | return p;
56 | }
57 |
58 | // Computes the absolute offset of an element
59 | function computeOffset(element) {
60 | var x = 0, y = 0;
61 |
62 | while (element) {
63 | x += element.offsetLeft;
64 | y += element.offsetTop;
65 | element = element.offsetParent;
66 | }
67 |
68 | x -= window.pageXOffset;
69 | y -= window.pageYOffset;
70 |
71 | return new Point(x, y);
72 | }
73 |
74 | function wrapHandler(_this, handler) {
75 | // Wraps the supplied handler so that it has an appropriate "this" reference.
76 | return function(event) {
77 | return handler.apply(_this, [event]);
78 | };
79 | }
--------------------------------------------------------------------------------
/scissors/scissorsLines.js:
--------------------------------------------------------------------------------
1 | // Masquerades as the real deal, in order to provide polygonal segmentation.
2 |
3 | function Point(x,y) {
4 | this.x = x;
5 | this.y = y;
6 | }
7 |
8 | Point.prototype.equals = function(q) {
9 | if ( !q ) {
10 | return false;
11 | }
12 |
13 | return (this.x == q.x) && (this.y == q.y);
14 | };
15 |
16 | Point.prototype.toString = function() {
17 | return "(" + this.x + ", " + this.y + ")";
18 | };
19 |
20 | Point.prototype.dist = function(p) {
21 | return Math.sqrt(Math.pow(this.x-p.x,2) + Math.pow(this.y-p.y,2));
22 | }
23 |
24 | function ScissorsWorker(scissorsURL) {
25 | // Nothing to do here.
26 | }
27 |
28 | ScissorsWorker.prototype.setTraining = function(train) {};
29 |
30 | ScissorsWorker.prototype.setImageData = function(imageData) {};
31 |
32 | ScissorsWorker.prototype.setPoint = function(p) {
33 | this.curPoint = p;
34 | };
35 |
36 | ScissorsWorker.prototype.hasPoint = function() {
37 | return this.getPoint() != null;
38 | };
39 |
40 | ScissorsWorker.prototype.getPoint = function() {
41 | return this.curPoint;
42 | };
43 |
44 | ScissorsWorker.prototype.getPathFrom = function(p) {
45 | return this.getLine(p, this.curPoint);
46 | }
47 |
48 | ScissorsWorker.prototype.hasPathFor = function(p) {
49 | return true;
50 | }
51 |
52 | ScissorsWorker.prototype.stop = function() {};
53 |
54 | ScissorsWorker.prototype.resetTraining = function() {};
55 |
56 | ScissorsWorker.prototype.isWorking = function() {
57 | return false;
58 | };
59 |
60 | // Bresenham's algorithm.
61 | // Thank you, Phrogz, from StackOverflow.
62 | ScissorsWorker.prototype.getLine = function(p, q) {
63 | var line = new Array();
64 |
65 | // For faster access
66 | px = p.x; py = p.y;
67 | qx = q.x; qy = q.y;
68 |
69 | var dx = Math.abs(qx-px);
70 | var dy = Math.abs(qy-py);
71 | var sx = (px < qx) ? 1 : -1;
72 | var sy = (py < qy) ? 1 : -1;
73 | var err = dx - dy;
74 |
75 | while( (px != qx) || (py != qy) ) {
76 |
77 | // Do what you need to for this
78 | line.push(new Point(px, py));
79 |
80 | var e2 = 2 * err;
81 |
82 | if ( e2 > -dy ){
83 | err -= dy;
84 | px += sx;
85 | }
86 |
87 | if ( e2 < dx ){
88 | err += dx;
89 | py += sy;
90 | }
91 | }
92 |
93 | line.push(new Point(px, py));
94 | return line;
95 | }
--------------------------------------------------------------------------------
/scissors/bucketQueue.js:
--------------------------------------------------------------------------------
1 |
2 | // Implemented from specification given in:
3 | //
4 | //Eric N. Mortensen, William A. Barrett, Interactive Segmentation with
5 | // Intelligent Scissors, Graphical Models and Image Processing, Volume 60,
6 | // Issue 5, September 1998, Pages 349-384, ISSN 1077-3169,
7 | // DOI: 10.1006/gmip.1998.0480.
8 | //(http://www.sciencedirect.com/science/article/B6WG4-45JB8WN-9/2/6fe59d8089fd1892c2bfb82283065579)
9 |
10 | // Circular Bucket Queue
11 | //
12 | // Returns input'd points in sorted order. All operations run in roughly O(1)
13 | // time (for input with small cost values), but it has a strict requirement:
14 | //
15 | // If the most recent point had a cost of c, any points added should have a cost
16 | // c' in the range c <= c' <= c + (capacity - 1).
17 |
18 | function BucketQueue(bits, cost_functor) {
19 | this.bucketCount = 1 << bits; // # of buckets = 2^bits
20 | this.mask = this.bucketCount - 1; // 2^bits - 1 = index mask
21 | this.size = 0;
22 |
23 | this.loc = 0; // Current index in bucket list
24 |
25 | // Cost defaults to item value
26 | this.cost = (typeof(cost_functor) != 'undefined') ? cost_functor : function(item) {
27 | return item;
28 | };
29 |
30 | this.buckets = this.buildArray(this.bucketCount);
31 | }
32 |
33 | BucketQueue.prototype.push = function(item) {
34 | // Prepend item to the list in the appropriate bucket
35 | var bucket = this.getBucket(item);
36 | item.next = this.buckets[bucket];
37 | this.buckets[bucket] = item;
38 |
39 | this.size++;
40 | };
41 |
42 | BucketQueue.prototype.pop = function() {
43 | if ( this.size == 0 ) {
44 | throw new Error("BucketQueue is empty.");
45 | }
46 |
47 | // Find first empty bucket
48 | while ( this.buckets[this.loc] == null ) this.loc = (this.loc + 1) % this.bucketCount;
49 |
50 | // All items in bucket have same cost, return the first one
51 | var ret = this.buckets[this.loc];
52 | this.buckets[this.loc] = ret.next;
53 | ret.next = null;
54 |
55 | this.size--;
56 | return ret;
57 | };
58 |
59 | BucketQueue.prototype.remove = function(item) {
60 | // Tries to remove item from queue. Returns true on success, false otherwise
61 | if ( !item ) {
62 | return false;
63 | }
64 |
65 | // To find node, go to bucket and search through unsorted list.
66 | var bucket = this.getBucket(item);
67 | var node = this.buckets[bucket];
68 |
69 | while ( node != null && !item.equals(node.next) ) {
70 | node = node.next;
71 | }
72 |
73 | if ( node == null ) {
74 | // Item not in list, ergo item not in queue
75 | return false;
76 | } else {
77 | // Found item, do standard list node deletion
78 | node.next = node.next.next;
79 |
80 | this.size--;
81 | return true;
82 | }
83 | };
84 |
85 | BucketQueue.prototype.isEmpty = function() {
86 | return this.size == 0;
87 | };
88 |
89 | BucketQueue.prototype.getBucket = function(item) {
90 | // Bucket index is the masked cost
91 | return this.cost(item) & this.mask;
92 | };
93 |
94 | BucketQueue.prototype.buildArray = function(newSize) {
95 | // Create array and initialze pointers to null
96 | var buckets = new Array();
97 | buckets.length = newSize;
98 |
99 | for ( var i = 0; i < buckets.length; i++ ) {
100 | buckets[i] = null;
101 | }
102 |
103 | return buckets;
104 | };
--------------------------------------------------------------------------------
/sky_scissors_instructions.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Sky Scissors Instructions{% endblock %}
4 |
5 | {% block include %}
6 |
25 | {% endblock %}
26 |
27 | {% block content %}
28 |
29 |

30 |
31 |
Sky Scissors Instructions
32 |
33 |
Interface:
34 |

35 |
36 |
Legend:
37 |
38 | - Status Bar: Displays what is currently going on behind the scenes.
39 | - Stop Drawing: Stops the drawing process and detach the mouse from the last point clicked.
40 | - Clear Lines: Removes all of the lines so far from the image. This cannot be undone.
41 | - Undo: Undoes the last line segment drawn and stops the drawing process.
42 | - Submit: Submits the HIT to Mechanical Turk for processing.
43 | - Image: The image to draw on. Clicking on this will start the drawing process.
44 |
45 |
46 |
Instructions:
47 |
48 | In this HIT, you will be separating the sky from the rest of the image
49 | by drawing a line along the sky's boundary, where the sky and the rest
50 | of the image meet. In order to do this, you must:
51 |
52 |
53 | - First wait for the status bar (1) to say "Ready". This tells you that the image (6) is ready to be drawn on.
54 | - Click somewhere on the boundary between the sky and the rest of the image. When clicking, click and release, do not click and hold. The status bar (1) should now say "Processing...".
55 | - You can now move the mouse around the image. There should be a line drawn from where you click to where you move the cursor. If a line is not drawn between your mouse and where you last clicked, that means that your browser does not yet know exactly how to draw the line. If you wait, the line should pop in shortly.

56 | - You can now click on another point in the image to fix the line shown in place and continue the line. Make sure you wait for the line to appear before clicking, otherwise nothing will happen, and you will still be attached to the same point.
57 | - If you do not want to continue a line, click on the Stop Drawing (2) button. Everywhere you clicked will be saved, and you can start a new line anywhere you want.
58 | - If you mess up, you can click on the undo button (4) to remove the last line segment, or you can click clear lines (3) to start over again.
59 | - Once you are finished, click on the submit button (5) to submit the HIT.
60 |
61 |
62 |
Notes:
63 |
64 | - You do not need to draw a line around the outer border of the image, and you don't have to draw around clouds or things of that nature.
65 | - If the status bar (1) changes to say "Ready!", that means it has finished processing, and you should be able to move your mouse anywhere in the image and see a line.
66 | -
67 | If you try and go too far with one line, the line might not align with the boundary of the sky:
68 |
69 | By drawing the line is shorter steps, this can be avoided:
70 |
71 |
72 |
73 |
74 |
Example completed HIT:
75 |

76 |
77 | {% endblock %}
78 |
--------------------------------------------------------------------------------
/scissors/scissorsServer.js:
--------------------------------------------------------------------------------
1 |
2 | var PREPROCESSING_STR = "Preprocessing image. Please wait...";
3 | var PROCESSING_STR = "Mapping out edges. Feel free to choose an edge.";
4 | var READY_STR = "Processing complete. Feel free to start or continue your edge.";
5 | var TRAINING_STR = "Training. Please wait...";
6 | var STOPPED_STR = "Processing stopped. Click somewhere to begin a new edge or click submit.";
7 | var TRAINING_TRUE_STR = "Will adapt edge detection.";
8 | var TRAINING_FALSE_STR = "Won't adapt edge detection.";
9 |
10 | function Message(msgType) {
11 | this.msgType = msgType;
12 | }
13 |
14 | Message.GRADIENT = -4;
15 | Message.RESULTS = -3;
16 | Message.WORKING = -2;
17 | Message.STATUS = -1;
18 |
19 | Message.ERROR = 0;
20 |
21 | Message.POINT = 1;
22 | Message.CONTINUE = 2;
23 | Message.STOP = 3;
24 | Message.IMAGE = 4;
25 | Message.RESET = 5;
26 | Message.TRAIN = 6;
27 |
28 | function ScissorsServer(scissors) {
29 | this.scissors = scissors;
30 | this.scissors.server = this;
31 |
32 | this.expectingImage = false;
33 | this.postPartials = true;
34 | this.train = false;
35 | }
36 |
37 | ScissorsServer.prototype.postMessage = function(event) {
38 | var data = event.data;
39 |
40 | switch (data.msgType) {
41 | case Message.CONTINUE:
42 | this._processContinueMessage(data);
43 | break;
44 | case Message.POINT:
45 | this._processPointMessage(data);
46 | break;
47 | case Message.STOP:
48 | this._processStopMessage(data);
49 | break;
50 | case Message.DIMENSION:
51 | this._processDimensionMessage(data);
52 | break;
53 | case Message.IMAGE:
54 | this._processImageMessage(data);
55 | break;
56 | case Message.RESET:
57 | this._processResetMessage(data);
58 | break;
59 | case Message.TRAIN:
60 | this._processTrainMessage(data);
61 | break;
62 | case Message.CONTINUE:
63 | this._processContinueMessage(data);
64 | default:
65 | throw new Error("Uknown message type: '" + data.msgType + "'");
66 | }
67 | };
68 |
69 | ScissorsServer.prototype.status = function(status) {
70 | var msg = new Message(Message.STATUS);
71 | msg.status = status;
72 | postMessage(msg);
73 | };
74 |
75 | ScissorsServer.prototype.postResults = function(data) {
76 | var msg = new Message(Message.RESULTS);
77 | msg.results = data;
78 | postMessage(msg);
79 | };
80 |
81 | ScissorsServer.prototype.setWorking = function(working) {
82 | var msg = new Message(Message.WORKING);
83 | msg.working = working;
84 | postMessage(msg);
85 |
86 | if ( !working ) {
87 | this.status(READY_STR);
88 | }
89 | };
90 |
91 | ScissorsServer.prototype._processContinueMessage = function(data) {
92 | if ( this.scissors.working ) {
93 | this.scissors.doWork();
94 | }
95 | };
96 |
97 | ScissorsServer.prototype._processDimensionMessage = function(data) {
98 | this.scissors.setDimensions(data.width, data.height);
99 | };
100 |
101 | ScissorsServer.prototype._processImageMessage = function(data) {
102 | this._processDimensionMessage(data);
103 | this.status(PREPROCESSING_STR);
104 | this.setWorking(true);
105 | this.scissors.setData(data.imageData, data.mask);
106 | this.setWorking(false);
107 | this._postGradientMessage(this.scissors.gradient);
108 | this.status(READY_STR);
109 | };
110 |
111 | ScissorsServer.prototype._postGradientMessage = function(gradient) {
112 | var msg = new Message(Message.GRADIENT);
113 | msg.gradient = gradient;
114 | postMessage(msg);
115 | };
116 |
117 | ScissorsServer.prototype._processPointMessage = function(data) {
118 | this.setWorking(true);
119 | if ( this.train ) {
120 | this.status(TRAINING_STR);
121 | this.scissors.doTraining(data.point);
122 | }
123 | this.status(PROCESSING_STR);
124 | this.scissors.setPoint(data.point);
125 | this.scissors.doWork();
126 | };
127 |
128 | ScissorsServer.prototype._processResetMessage = function(data) {
129 | this.scissors.resetTraining();
130 | };
131 |
132 | ScissorsServer.prototype._processStopMessage = function(data) {
133 | this.scissors.setWorking(false);
134 | this.status(STOPPED_STR);
135 | };
136 |
137 | ScissorsServer.prototype._processTrainMessage = function(data) {
138 | this.train = data.train;
139 | if ( this.train ) {
140 | this.status(TRAINING_TRUE_STR);
141 | } else {
142 | this.status(TRAINING_FALSE_STR);
143 | }
144 | };
145 |
--------------------------------------------------------------------------------
/mturkScissors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Intelligent Scissors
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
68 |
69 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |

156 |
157 |
158 |
159 |
160 |
161 |
162 |
191 |
192 |
193 |
194 |
195 |
196 |
--------------------------------------------------------------------------------
/scissors/pointQueue.js:
--------------------------------------------------------------------------------
1 |
2 | function Point(x,y) {
3 | this.x = x;
4 | this.y = y;
5 | }
6 |
7 | Point.prototype.equals = function(q) {
8 | return (this.x == q.x) && (this.y == q.y);
9 | };
10 |
11 | Point.prototype.toString = function() {
12 | return "(" + this.x + ", " + this.y + ")";
13 | };
14 |
15 | function testPQ() {
16 | pq = new PointQueue(1000, 1000);
17 |
18 | try {
19 | for ( var i = 0; i < 100; i++ ) {
20 | p = new Point(Math.floor(Math.random()*1000), Math.floor(Math.random()*1000));
21 | }
22 | } catch (err) {
23 | throw new Error("Push: " + err.message);
24 | }
25 |
26 | try {
27 | for ( var i = 1; i < pq.items.length; i++ ) {
28 | if ( pq.less(pq.items[i], pq.items[(i-1) >> 1]) ) {
29 | throw new Error("Iterate: (" + pq.items[(i-1)>>1] + " => " + pq.items[i] + ")");
30 | }
31 | }
32 | } catch (err) {
33 | throw new Error("Iterate: " + err.message);
34 | }
35 |
36 | try {
37 | for ( var i = 0; i < pq.items.length; i++ ) {
38 | fIdx = pq.find(pq.items[i]);
39 | if ( (!fIdx && fIdx != 0) || fIdx < 0 ) {
40 | throw new Error("Find: " + i + " => " + fIdx);
41 | }
42 |
43 | if ( !pq.items[i].equals(pq.items[fIdx]) ) {
44 | throw new Error("Find: " + i + " => " + fIdx);
45 | }
46 | }
47 | } catch (err) {
48 | throw new Error("Find: " + err.message);
49 | }
50 |
51 | try {
52 | var prev = -1;
53 | while ( !pq.isEmpty() ) {
54 | var next = pq.pop();
55 |
56 | if ( pq.less(next, prev) ) {
57 | throw new Error("Pop: (" + prev + " => " + next + ")");
58 | }
59 |
60 | prev = next;
61 |
62 | try {
63 | for ( var i = 0; i < pq.items.length; i++ ) {
64 | fIdx = pq.find(pq.items[i]);
65 | if ( (!fIdx && fIdx != 0) || fIdx < 0 ) {
66 | throw new Error("Find: " + i + " => " + fIdx);
67 | }
68 |
69 | if ( !pq.items[i].equals(pq.items[fIdx]) ) {
70 | throw new Error("Find: " + i + " => " + fIdx);
71 | }
72 | }
73 | } catch (err) {
74 | throw new Error("Find: " + err.message);
75 | }
76 | }
77 | } catch (err) {
78 | throw new Error("Pop: " + err.message);
79 | }
80 |
81 | return true;
82 | }
83 |
84 | function PointQueue(width, height, less_comparator) {
85 | this.less = typeof(less_comparator) != 'undefined' ? less_comparator : function(p,q) {
86 | return (p.x < q.x) || (p.x == q.x && p.y < q.y);
87 | };
88 |
89 | this.items = new Array();
90 |
91 | this.width = width;
92 | this.height = height;
93 | this.map = new Array();
94 |
95 | for ( var y = 0; y < this.height; y++ ) {
96 | this.map[y] = new Array();
97 |
98 | for ( var x = 0; x < this.width; x++ ) {
99 | this.map[y][x] = -1;
100 | }
101 | }
102 |
103 | // this.mapIdx = function(p) {
104 | // return p.y * this.width + p.x;
105 | // }
106 | this.setPos = function(p, idx) {
107 | this.map[p.y][p.x] = idx;
108 | };
109 | this.getPos = function(x, y) {
110 | return this.map[y][x];
111 | };
112 |
113 | this.peek = function() {
114 | return this.items[0];
115 | };
116 |
117 | this.pop = function() {
118 | // if ( this.items.length == 0 ) {
119 | // throw new Error("PointQueue is empty");
120 | // }
121 |
122 | var ret = this.items[0];
123 |
124 | this.swap(0, this.items.length - 1);
125 | this.sink(0);
126 | this.items.length = this.items.length - 1;
127 | this.setPos(ret, -1);
128 |
129 | return ret;
130 | };
131 |
132 | this.push = function(item) {
133 | // if ( this.find(item) != -1 ) {
134 | // throw new Error("Point already in PointQueue.");
135 | // }
136 |
137 | var loc = this.items.length;
138 | this.items[loc] = item;
139 | this.setPos(item, loc);
140 |
141 | this.swim(loc);
142 | };
143 |
144 | this.find = function(x, y) {
145 | return this.getPos(x, y);
146 |
147 | // var id = this.getPos(item);
148 |
149 | // //msg = "Find: (" + item.x + "," + item.y + ") => ";
150 |
151 | // if ( !(id + 1) ) { //Undefined, NaN, or -1
152 | // id = -1;
153 | // //msg += "-1";
154 | // } else {
155 | // //msg += "(" + this.items[id].x + "," + this.items[id].y + ")";
156 | // }
157 |
158 | // //postMessage(msg);
159 |
160 | // return id;
161 | };
162 |
163 | // this.find = function(item) {
164 | // for ( var i = 0; i < this.items.length; i++ ) {
165 | // if ( item.equals(this.items[i]) ) {
166 | // return i;
167 | // }
168 | // }
169 |
170 | // return -1;
171 | // }
172 |
173 | this.decreaseKey = function(item) {
174 | this.sink(this.find(item));
175 | };
176 |
177 | this.isEmpty = function() {
178 | return this.items.length == 0;
179 | };
180 |
181 | this.sink = function(idx) {
182 | var left = (idx << 1) + 1;
183 | var right = left + 1;
184 | var small = idx;
185 | if ( left < this.items.length && this.less(this.items[left], this.items[idx]) ) {
186 | small = left;
187 | }
188 | if ( right < this.items.length && this.less(this.items[right], this.items[small]) ) {
189 | small = right;
190 | }
191 | if ( small != idx ) {
192 | this.swap(idx, small);
193 | this.sink(small);
194 | }
195 | };
196 |
197 | this.swim = function(idx) {
198 | var parent = (idx-1) >> 1;
199 | while ( idx > 0 && this.less(this.items[idx], this.items[parent]) ) {
200 | this.swap(idx, parent);
201 |
202 | idx = parent;
203 | parent = (idx-1) >> 1;
204 | }
205 | };
206 |
207 | this.swap = function(p, q) {
208 | var pt = this.items[p];
209 | var qt = this.items[q];
210 |
211 | this.items[p] = qt;
212 | this.items[q] = pt;
213 |
214 | this.setPos(pt, q);
215 | this.setPos(qt, p);
216 | };
217 | }
--------------------------------------------------------------------------------
/scissors/scissorsClient.js:
--------------------------------------------------------------------------------
1 |
2 | function Message(msgType) {
3 | this.msgType = msgType;
4 | }
5 |
6 | Message.GRADIENT = -4;
7 | Message.RESULTS = -3;
8 | Message.WORKING = -2;
9 | Message.STATUS = -1;
10 |
11 | Message.ERROR = 0;
12 |
13 | Message.POINT = 1;
14 | Message.CONTINUE = 2;
15 | Message.STOP = 3;
16 | Message.IMAGE = 4;
17 | Message.RESET = 5;
18 | Message.TRAIN = 6;
19 | Message.SEARCH = 7;
20 |
21 | // No arguments => only need one instance.
22 | Message.RESET_MESSAGE = new Message(Message.RESET);
23 | Message.STOP_MESSAGE = new Message(Message.STOP);
24 | Message.CONTINUE_MESSAGE = new Message(Message.CONTINUE);
25 |
26 | function ScissorsWorker(scissorsURL) {
27 | this.worker = new Worker(scissorsURL);
28 | this.worker.enclosingScissorsWorker = this; // For onmessage proxy.
29 |
30 | this.width = -1;
31 | this.height = -1;
32 |
33 | this.working = false;
34 | this.processing = false; // Won't accept resultant data when false.
35 |
36 | this.gradient = null;
37 | this.parentPoints = null;
38 |
39 | this.curPoint = null;
40 |
41 | this.onmessage = null;
42 | this.onerror = function(event) {};
43 | this.onstatus = function(msg) {};
44 | this.ondata = function(data) {};
45 |
46 | this.worker.onmessage = function(event) {
47 | this.enclosingScissorsWorker._processMessage(event);
48 | };
49 |
50 | this.worker.onerror = function(event) {
51 | this.enclosingScissorsWorker.onerror(event);
52 | };
53 | }
54 |
55 | ScissorsWorker.prototype.destroy = function() {
56 | this.gradient = null;
57 | this.parentPoints = null;
58 | this.worker.terminate();
59 | };
60 |
61 | ScissorsWorker.prototype.initialProcessingDone = function() {
62 | return this.gradient != null;
63 | };
64 |
65 | ScissorsWorker.prototype.toWorkerSpace = function(p) {
66 | return translate(p, -this.aoi[0], -this.aoi[1]);
67 | };
68 |
69 | ScissorsWorker.prototype.toImageSpace = function(p) {
70 | return translate(p, this.aoi[0], this.aoi[1]);
71 | };
72 |
73 | ScissorsWorker.prototype.setTraining = function(train) {
74 | this._postTrainMessage(train);
75 | };
76 |
77 | ScissorsWorker.prototype.computeGreyscale = function(data) {
78 | // Returns 2D augmented array containing greyscale data
79 | // Greyscale values found by averaging color channels
80 | // Input should be in a flat RGBA array, with values between 0 and 255
81 | var greyscale = new Float32Array(data.length / 4);
82 |
83 | for (var i = 0; i < data.length; i += 4) {
84 | greyscale[i/4] = (data[i] + data[i+1] + data[i+2]) / (3*255);
85 | }
86 |
87 | return greyscale;
88 | };
89 |
90 | ScissorsWorker.prototype.setImageData = function(image, aoi, mask) {
91 | var imageData;
92 | if ( aoi ) {
93 | // AOI is supplied so image should be a 2D Context.
94 | this.aoi = aoi;
95 | imageData = image.getImageData(aoi[0], aoi[1], aoi[2], aoi[3]);
96 | } else {
97 | // AOI is not supplied, so image should be an ImageData.
98 | this.aoi = [0, 0, image.width, image.height];
99 | imageData = image;
100 | }
101 |
102 | var grey = this.computeGreyscale(imageData.data);
103 |
104 | if ( !mask ) {
105 | mask = null;
106 | }
107 |
108 | this.width = aoi[2];
109 | this.height = aoi[3];
110 | this.gradient = null;
111 | this._postImageMessage(grey, mask);
112 | };
113 |
114 | ScissorsWorker.prototype.setPoint = function(p) {
115 | this.curPoint = p;
116 | this._resetParentPoints();
117 | this.processing = true;
118 |
119 | this._postPointMessage(this.toWorkerSpace(p));
120 | };
121 |
122 | ScissorsWorker.prototype.hasPoint = function() {
123 | return this.getPoint() != null;
124 | };
125 |
126 | ScissorsWorker.prototype.getPoint = function() {
127 | return this.curPoint;
128 | };
129 |
130 | ScissorsWorker.prototype.getInvertedGradient = function(p) {
131 | p = this.toWorkerSpace(p);
132 |
133 | if ( !this.gradient ) {
134 | return Infinity;
135 | }
136 |
137 | if ( p.x < 0 || p.x >= this.width ||
138 | p.y < 0 || p.y >= this.height ) {
139 | return Infinity;
140 | }
141 |
142 | return this.gradient[p.index(this.width)];
143 | };
144 |
145 | ScissorsWorker.prototype.getParentPoint = function(p) {
146 | aoi = this.aoi;
147 | p = this.toWorkerSpace(p);
148 | return this.toImageSpace(this.parentPoints[p.index(this.width)]);
149 | };
150 |
151 | ScissorsWorker.prototype.getPathFrom = function(p) {
152 | var subpath = new Array();
153 | var width = this.width;
154 |
155 | p = this.toWorkerSpace(p);
156 | var pi = index(p.y, p.x, width);
157 | while (pi) {
158 | subpath.push(this.toImageSpace(fromIndex(pi, width)));
159 | pi = this.parentPoints[pi];
160 | }
161 |
162 | return subpath;
163 | };
164 |
165 | ScissorsWorker.prototype.hasPathFor = function(p) {
166 | return !!this.getParentPoint(p);
167 | };
168 |
169 | ScissorsWorker.prototype.getParentInfo = function() {
170 | return this.parentPoints;
171 | };
172 |
173 | ScissorsWorker.prototype.stop = function() {
174 | this._postStopMessage();
175 | this.processing = false;
176 | };
177 |
178 | ScissorsWorker.prototype.resetTraining = function() {
179 | this._postResetMessage();
180 | };
181 |
182 | ScissorsWorker.prototype.isWorking = function() {
183 | return working;
184 | };
185 |
186 | ScissorsWorker.prototype.postMessage = function(event) {
187 | this.worker.postMessage(event);
188 | };
189 |
190 | ScissorsWorker.prototype._resetParentPoints = function() {
191 | this.parentPoints = new Uint32Array(this.width * this.height);
192 | };
193 |
194 | ScissorsWorker.prototype._processMessage = function(event) {
195 | var data = event.data;
196 |
197 | switch (data.msgType) {
198 | case Message.RESULTS:
199 | this._processResultsMessage(data);
200 | break;
201 | case Message.STATUS:
202 | this._processStatusMessage(data);
203 | break;
204 | case Message.GRADIENT:
205 | this._processGradientMessage(data);
206 | break;
207 | case Message.WORKING:
208 | this._processWorkingMessage(data);
209 | break;
210 | default:
211 | this._processUnknownMessage(event);
212 | }
213 | };
214 |
215 | ScissorsWorker.prototype._processResultsMessage = function(data) {
216 | if ( !this.processing ) {
217 | return;
218 | }
219 |
220 | this._postContinueMessage(); // Pipe clear for next batch.
221 |
222 | var width = this.width;
223 |
224 | var results = data.results;
225 | for ( var i = 0; i < results.length; i += 2 ) {
226 | var p = results[i];
227 | var q = results[i+1];
228 | this.parentPoints[p] = q;
229 |
230 | results[i] = this.toImageSpace(fromIndex(p, width));
231 | results[i+1] = this.toImageSpace(fromIndex(q, width));
232 | }
233 |
234 | this.ondata(results);
235 | };
236 |
237 | ScissorsWorker.prototype._processGradientMessage = function(data) {
238 | this.gradient = data.gradient;
239 | };
240 |
241 | ScissorsWorker.prototype._processStatusMessage = function(data) {
242 | this.onstatus(data.status);
243 | };
244 |
245 | ScissorsWorker.prototype._processUnknownMessage = function(event) {
246 | if ( this.onmessage != null ) {
247 | this.onmessage(event);
248 | } else {
249 | throw new Error("Unknown message type: '" + event.data.msgType + "'");
250 | }
251 | };
252 |
253 | ScissorsWorker.prototype._processWorkingMessage = function(data) {
254 | this.working = data.working;
255 | };
256 |
257 | ScissorsWorker.prototype._postContinueMessage = function() {
258 | this.worker.postMessage(Message.CONTINUE_MESSAGE);
259 | };
260 |
261 | ScissorsWorker.prototype._postImageMessage = function(data, mask) {
262 | var msg = new Message(Message.IMAGE);
263 | msg.imageData = data;
264 | msg.mask = mask;
265 | msg.width = this.width;
266 | msg.height = this.height;
267 | this.worker.postMessage(msg);
268 | };
269 |
270 | ScissorsWorker.prototype._postPointMessage = function(p) {
271 | var msg = new Message(Message.POINT);
272 | msg.point = p;
273 | this.worker.postMessage(msg);
274 | };
275 |
276 | ScissorsWorker.prototype._postResetMessage = function() {
277 | this.worker.postMessage(Message.RESET_MESSAGE);
278 | };
279 |
280 | ScissorsWorker.prototype._postStopMessage = function() {
281 | this.worker.postMessage(Message.STOP_MESSAGE);
282 | };
283 |
284 | ScissorsWorker.prototype._postTrainMessage = function(train) {
285 | var msg = new Message(Message.TRAIN);
286 | msg.train = train;
287 | this.worker.postMessage(msg);
288 | };
289 |
--------------------------------------------------------------------------------
/scissors/maskDrawing.js:
--------------------------------------------------------------------------------
1 |
2 | function Masker() {
3 | this.id = "masker";
4 |
5 | this.image = null;
6 |
7 | this.canvas = null;
8 | this.ctx = null;
9 | this.maskBuffer = null;
10 | this.maskCtx = null;
11 |
12 | this.maxDimension = 640; // Pixels
13 | this.width = -1;
14 | this.height = -1;
15 |
16 | this.drawing = false;
17 | this.prevPoint = null;
18 | this.points = [];
19 | this.pathStarts = [];
20 | this.radius = 12; // # of pixels in the shrunken image.
21 | this.color = "#F0F";
22 | this.opacity = 0.5;
23 |
24 | return this;
25 | }
26 |
27 | Masker.prototype.setUp = function(container, image) {
28 | this.image = image;
29 |
30 | // Tie global listeners to the document DOM element
31 | this.globalListenerTarget = document;
32 | this.globalListeners = new Array();
33 |
34 | this.calculateSize();
35 | this.constructCanvases(container);
36 | this.registerListeners();
37 | this.paint();
38 | };
39 |
40 | Masker.prototype.tearDown = function() {
41 | this.canvas.parentNode.removeChild(this.canvas);
42 | this.deregisterListeners();
43 |
44 | // Clear references for garbage collector.
45 | this.canvas = null;
46 | this.ctx = null;
47 | this.maskBuffer = null;
48 | this.maskCtx = null;
49 | this.points = [];
50 | };
51 |
52 | Masker.prototype.calculateSize = function() {
53 | var image = this.image;
54 | var maxDimension = this.maxDimension;
55 |
56 | var width = image.width;
57 | var height = image.height;
58 |
59 | scale = Math.min(maxDimension / width, maxDimension / height);
60 | if ( scale < 1 ) {
61 | width = Math.floor(width * scale);
62 | height = Math.floor(height * scale);
63 | }
64 |
65 | this.width = width;
66 | this.height = height;
67 | };
68 |
69 | Masker.prototype.constructCanvases = function(container) {
70 | // Displayed canvas
71 | var canvas = this.newCanvas();
72 | canvas.id = this.id;
73 |
74 | var style = canvas.style;
75 | style.position = "absolute";
76 | style.top = "0px";
77 | style.left = "0px";
78 | style.cursor = "pointer";
79 | container.appendChild(canvas);
80 |
81 | this.canvas = canvas;
82 | this.ctx = canvas.getContext('2d');
83 |
84 | // Canvas drawn on by user
85 | var maskBuffer = this.newCanvas();
86 | this.maskBuffer = maskBuffer;
87 | this.maskCtx = maskBuffer.getContext('2d');
88 | };
89 |
90 | Masker.prototype.newCanvas = function(width, height) {
91 | if ( !width || !height ) {
92 | width = this.width;
93 | height = this.height;
94 | }
95 |
96 | var canvas = document.createElement("canvas");
97 | canvas.width = width;
98 | canvas.height = height;
99 |
100 | return canvas;
101 | };
102 |
103 | Masker.prototype.registerListeners = function() {
104 | this.canvas.addEventListener("mousedown", wrapHandler(this, this.mouseDown));
105 | this.addGlobalListener("mouseup", this.mouseUp);
106 | this.addGlobalListener("mousemove", this.mouseMove);
107 | };
108 |
109 | Masker.prototype.addGlobalListener = function(type, listener, bubble) {
110 | var wrapper = wrapHandler(this, listener);
111 | wrapper.type = type;
112 | wrapper.bubble = bubble;
113 |
114 | this.globalListenerTarget.addEventListener(type, wrapper, bubble);
115 | this.globalListeners.push(wrapper);
116 | };
117 |
118 | Masker.prototype.deregisterListeners = function() {
119 | // Only have to worry about the global ones
120 | var globalListenerTarget = this.globalListenerTarget;
121 | var globalListeners = this.globalListeners;
122 |
123 | for ( var i = 0; i < globalListeners.length; i++ ) {
124 | var listener = globalListeners[i];
125 | globalListenerTarget.removeEventListener(listener.type,listener,listener.bubble);
126 | }
127 | };
128 |
129 | Masker.prototype.paint = function() {
130 | var ctx = this.ctx;
131 | ctx.drawImage(this.image, 0, 0, this.width, this.height);
132 | ctx.drawImage(this.maskBuffer, 0, 0, this.width, this.height);
133 | };
134 |
135 | Masker.prototype.clearMask = function() {
136 | this.maskCtx.clearRect(0, 0, this.width, this.height);
137 | this.points = [];
138 | this.pathStarts = [];
139 | this.paint();
140 | };
141 |
142 | Masker.prototype.getMask = function() {
143 | var points = this.points;
144 | if ( points.length == 0 ) {
145 | // There are no masked pixels
146 | return null;
147 | }
148 |
149 | // First, we need to resize our mask to fit the original image
150 | var fullWidth = this.image.width;
151 | var fullHeight = this.image.height;
152 |
153 | var scaleX = fullWidth / this.width;
154 | var scaleY = fullHeight / this.height;
155 |
156 | // First determine area of interest
157 | var sx = this.width, sy = this.height, ex = 0, ey = 0;
158 |
159 | // Add 1 to radius for margin to avoid edge artifacts in the shortest-paths tree
160 | var margin = this.radius + 1;
161 |
162 | // Iterate over line end points to find bounding box
163 | for ( var i = 0; i < points.length; i++ ) {
164 | p = points[i];
165 | sx = Math.min(p.x - margin, sx);
166 | sy = Math.min(p.y - margin, sy);
167 | ex = Math.max(p.x + margin, ex);
168 | ey = Math.max(p.y + margin, ey);
169 | }
170 |
171 | // Clip bounding box to image
172 | sx = Math.max(sx, 0);
173 | sy = Math.max(sy, 0);
174 | ex = Math.min(ex, this.width);
175 | ey = Math.min(ey, this.height);
176 |
177 | // Scale to find corresponding box in full-size image
178 | var fsx = Math.floor(sx * scaleX);
179 | var fsy = Math.floor(sy * scaleY);
180 | var fex = Math.ceil(ex * scaleX);
181 | var fey = Math.ceil(ey * scaleY);
182 |
183 | // Find final width and height
184 | var maskedWidth = fex - fsx;
185 | var maskedHeight = fey - fsy;
186 |
187 | // Upscale the masking image to full size
188 | var fullSizeCanvas = this.newCanvas(maskedWidth, maskedHeight);
189 | var fullSize = fullSizeCanvas.getContext('2d');
190 | // fullSize.drawImage(this.maskBuffer, sx, sy, ex-sx, ey-sy, 0, 0, maskedWidth, maskedHeight);
191 | fullSize.lineWidth = this.radius * 2 * Math.max(scaleX, scaleY);
192 | fullSize.lineCap = "round";
193 |
194 | var pathIdx = 0;
195 | var pathStarts = this.pathStarts.slice(0); // Copy of pathStarts
196 | pathStarts.push(points.length);
197 | fullSize.stroke();
198 | prevPoint = null;
199 | for ( var i = 0; i < points.length; i++ ) {
200 | var p = points[i];
201 | p.x = p.x * scaleX - fsx;
202 | p.y = p.y * scaleY - fsy;
203 |
204 | if ( pathStarts[pathIdx] != i ) {
205 | // Firefox sometimes ignores lineJoin, so we draw line segments separately to
206 | // ensures mask consistency
207 | fullSize.beginPath();
208 | fullSize.moveTo(prevPoint.x, prevPoint.y);
209 | fullSize.lineTo(p.x, p.y);
210 | fullSize.stroke();
211 | } else {
212 | pathIdx++;
213 | }
214 |
215 | prevPoint = p;
216 | }
217 | fullSize.stroke();
218 |
219 | var maskPixels = fullSize.getImageData(0, 0, maskedWidth, maskedHeight).data;
220 |
221 | // The mask pixels are those with alpha > 0
222 | var mask = new Uint8Array(maskedWidth * maskedHeight);
223 | for ( var y = 0; y < maskedHeight; y++ ) {
224 | for ( var x = 0; x < maskedWidth; x++ ) {
225 | idx = index(y, x, maskedWidth);
226 | mask[idx] = (maskPixels[idx*4 + 3] > 0);
227 | }
228 | }
229 |
230 | return {'points': mask, 'image': fullSizeCanvas, 'aoi': [fsx, fsy, maskedWidth, maskedHeight]};
231 | };
232 |
233 | Masker.prototype.addLine = function(a, b) {
234 | var maskCtx = this.maskCtx;
235 |
236 | maskCtx.save();
237 | maskCtx.lineWidth = this.radius * 2;
238 | maskCtx.lineCap = "round";
239 | maskCtx.strokeStyle = this.color;
240 | maskCtx.globalAlpha = this.opacity;
241 |
242 | // 'copy' is not working in Firefox. 'xor' works because the alpha is exactly 0.5.
243 | maskCtx.globalCompositeOperation = "xor";
244 |
245 | maskCtx.beginPath();
246 | maskCtx.moveTo(a.x, a.y);
247 | maskCtx.lineTo(b.x, b.y);
248 | maskCtx.stroke();
249 | this.paint();
250 |
251 | maskCtx.restore();
252 |
253 | // Record point for when calculating exact mask
254 | this.points.push(b);
255 | };
256 |
257 | Masker.prototype.startDrawing = function(event) {
258 | this.drawing = true;
259 | this.prevPoint = this.getPoint(event);
260 | this.pathStarts.push(this.points.length);
261 | this.addLine(this.prevPoint, this.prevPoint);
262 | };
263 |
264 | Masker.prototype.stopDrawing = function() {
265 | this.drawing = false;
266 | };
267 |
268 | Masker.prototype.mouseDown = function(event) {
269 | event.preventDefault();
270 | this.startDrawing(event);
271 | };
272 |
273 | Masker.prototype.mouseUp = function(event) {
274 | this.stopDrawing();
275 | };
276 |
277 | Masker.prototype.mouseMove = function(event) {
278 | if ( this.drawing ) {
279 | event.preventDefault();
280 |
281 | var point = this.getPoint(event);
282 | this.addLine(this.prevPoint, point);
283 | this.prevPoint = point;
284 | }
285 | };
286 |
287 | Masker.prototype.getPoint = function(event) {
288 | return getRelativePoint(this.canvas, event.clientX, event.clientY);
289 | };
290 |
--------------------------------------------------------------------------------
/scissors/scissorsWorker.js:
--------------------------------------------------------------------------------
1 | //As of Firefox 3.6.4, object allocation is still very slow. (Except when it's not?)
2 | //Avoid allocating objects inside loops.
3 |
4 | //If setTimout starts running things in new threads, work cancellation will need
5 | //to be fixed so that it synchronizes correctly.
6 |
7 | if ( this.importScripts != undefined ) {
8 | // We're running this script in a Web Worker, so set up environment
9 |
10 | importScripts("bucketQueue.js", "scissorsServer.js", "util.js");
11 |
12 | var scissorsServer = new ScissorsServer(new Scissors()); // Protocol object
13 | onmessage = function(event) {
14 | scissorsServer.postMessage(event);
15 | };
16 | }
17 |
18 | if ( !Number.prototype.equals ) {
19 | // Needed for the BucketQueue
20 | Number.prototype.equals = function(other) {
21 | if ( !other ) {
22 | return false;
23 | }
24 |
25 | return this.valueOf() == other.valueOf();
26 | };
27 | }
28 |
29 | // Temporary fix to deal with memory issues.
30 | var MAX_IMAGE_SIZE_FOR_TRAINING = 1000*1000;
31 |
32 | ////Begin Scissors class ////
33 | function Scissors() {
34 | this.server = null;
35 |
36 | this.width = -1;
37 | this.height = -1;
38 | this.mask = null;
39 |
40 | this.curPoint = null; // Corrent point we're searching on.
41 | this.searchGranBits = 8; // Bits of resolution for BucketQueue.
42 | this.searchGran = 1 << this.earchGranBits; //bits.
43 | this.pointsPerPost = 1000;
44 |
45 | // Precomputed image data. All in ranges 0 >= x >= 1 and all inverted (1 - x).
46 | this.greyscale = null; // Greyscale of image
47 | this.laplace = null; // Laplace zero-crossings (either 0 or 1).
48 | this.gradient = null; // Gradient magnitudes.
49 | this.gradX = null; // X-differences.
50 | this.gradY = null; // Y-differences.
51 | // this.gradDir = null; // Precomputed gradient directions.
52 |
53 | this.parents = null; // Matrix mapping point => parent along shortest-path to root.
54 |
55 | this.working = false; // Currently computing shortest paths?
56 |
57 | // Begin Training:
58 | this.trained = false;
59 | this.trainingPoints = null;
60 |
61 | this.edgeWidth = 2;
62 | this.trainingLength = 32;
63 |
64 | this.edgeGran = 256;
65 | this.edgeTraining = null;
66 |
67 | this.gradPointsNeeded = 32;
68 | this.gradGran = 1024;
69 | this.gradTraining = null;
70 |
71 | this.insideGran = 256;
72 | this.insideTraining = null;
73 |
74 | this.outsideGran = 256;
75 | this.outsideTraining = null;
76 | // End Training
77 | }
78 |
79 | Scissors.prototype.dx = function(x,y) {
80 | var width = this.width;
81 | var grey = this.greyscale;
82 |
83 | if ( x+1 == width ) {
84 | // If we're at the end, back up one
85 | x--;
86 | }
87 |
88 | return grey[index(y, x+1, width)] - grey[index(y, x, width)];
89 | };
90 |
91 | Scissors.prototype.dy = function(x,y) {
92 | var width = this.width;
93 | var grey = this.greyscale;
94 |
95 | if ( y+1 == grey.length / width ) {
96 | // If we're at the end, back up one
97 | y--;
98 | }
99 |
100 | return grey[index(y, x, width)] - grey[index(y+1, x, width)];
101 | };
102 |
103 | Scissors.prototype.gradMagnitude = function(x,y) {
104 | var dx = this.dx(x,y); var dy = this.dy(x,y);
105 | return Math.sqrt(dx*dx + dy*dy);
106 | };
107 |
108 | Scissors.prototype.lap = function(x,y) {
109 | // Laplacian of Gaussian
110 | var width = this.width;
111 | var grey = this.greyscale;
112 |
113 | function index(y, x) {
114 | return y*width + x;
115 | }
116 |
117 | var lap = -16 * grey[index(y, x)];
118 | lap += grey[index(y-2, x)];
119 | lap += grey[index(y-1, x-1)] + 2*grey[index(y-1, x)] + grey[index(y-1, x+1)];
120 | lap += grey[index(y, x-2)] + 2*grey[index(y, x-1)] + 2*grey[index(y, x+1)] + grey[index(y, x+2)];
121 | lap += grey[index(y+1, x-1)] + 2*grey[index(y+1, x)] + grey[index(y+1, x+1)];
122 | lap += grey[index(y+2, x)];
123 |
124 | return lap;
125 | };
126 |
127 | Scissors.prototype.computeGradient = function() {
128 | // Returns a 2D array of gradient magnitude values for greyscale. The values
129 | // are scaled between 0 and 1, and then flipped, so that it works as a cost
130 | // function.
131 | var greyscale = this.greyscale;
132 | var mask = this.mask;
133 |
134 | var gradient = new Float32Array(greyscale.length);
135 | var width = this.width;
136 |
137 | var max = 0; // Maximum gradient found, for scaling purposes
138 |
139 | for (var y = 0; y < greyscale.length / width; y++) {
140 | for (var x = 0; x < width; x++) {
141 | var p = index(y, x, width);
142 | if ( mask && !mask[p] ) {
143 | continue;
144 | }
145 |
146 | var grad = this.gradMagnitude(x,y);
147 | gradient[p] = grad;
148 | max = Math.max(grad, max);
149 | }
150 | }
151 |
152 | // gradient[greyscale.length-1] = new Array();
153 | // for (var i = 0; i < gradient[0].length; i++) {
154 | // gradient[greyscale.length-1][i] = gradient[greyscale.length-2][i];
155 | // }
156 |
157 | // Flip and scale.
158 | for (var i = 0; i < gradient.length; i++) {
159 | gradient[i] = 1 - (gradient[i] / max);
160 | }
161 |
162 | return gradient;
163 | };
164 |
165 | Scissors.prototype.computeLaplace = function() {
166 | // Returns a 2D array of Laplacian of Gaussian values
167 | var greyscale = this.greyscale;
168 | var mask = this.mask;
169 |
170 | var laplace = new Float32Array(greyscale.length);
171 | var width = this.width;
172 |
173 | function index(i, j) {
174 | return i*width + j;
175 | }
176 |
177 | // Make the edges low cost here.
178 |
179 | var height = greyscale.length / width;
180 |
181 | for (var i = 1; i < width; i++) {
182 | // Pad top, since we can't compute Laplacian
183 | laplace[index(0, i)] = 1;
184 | laplace[index(1, i)] = 1;
185 | }
186 |
187 | for (var y = 2; y < height-2; y++) {
188 | laplace[y] = new Array();
189 | // Pad left, ditto
190 | laplace[index(y, 0)] = 1;
191 | laplace[index(y, 1)] = 1;
192 |
193 | for (var x = 2; x < width-2; x++) {
194 | p = index(y, x);
195 |
196 | if ( mask && !mask[p] ) {
197 | continue;
198 | }
199 |
200 | // Threshold needed to get rid of clutter.
201 | laplace[p] = (this.lap(x,y) > 0.33) ? 0 : 1;
202 | }
203 |
204 | // Pad right, ditto
205 | laplace[index(y, width-2)] = 1;
206 | laplace[index(y, width-1)] = 1;
207 | }
208 |
209 | for (var i = 1; i < width; i++) {
210 | // Pad bottom, ditto
211 | laplace[index(greyscale.length-2, i)] = 1;
212 | laplace[index(greyscale.length-1, i)] = 1;
213 | }
214 |
215 | return laplace;
216 | };
217 |
218 | Scissors.prototype.computeGradX = function() {
219 | // Returns 2D array of x-gradient values for greyscale
220 | var greyscale = this.greyscale;
221 | var mask = this.mask;
222 |
223 | var gradX = new Float32Array(greyscale.length);
224 | var width = this.width;
225 |
226 | for ( var y = 0; y < greyscale.length / width; y++ ) {
227 | for ( var x = 0; x < width; x++ ) {
228 | p = index(y, x, width);
229 | if ( mask && !mask[p] ) {
230 | continue;
231 | }
232 |
233 | gradX[p] = this.dx(x,y);
234 | }
235 | }
236 |
237 | return gradX;
238 | };
239 |
240 | Scissors.prototype.computeGradY = function() {
241 | // Returns 2D array of x-gradient values for greyscale
242 | var greyscale = this.greyscale;
243 | var mask = this.mask;
244 |
245 | var gradY = new Float32Array(greyscale.length);
246 | var width = this.width;
247 |
248 | for ( var y = 0; y < greyscale.length / width; y++ ) {
249 | for ( var x = 0; x < width; x++ ) {
250 | p = index(y, x, width);
251 | if ( mask && !mask[p] ) {
252 | continue;
253 | }
254 |
255 | gradY[p] = this.dy(x,y);
256 | }
257 | }
258 |
259 | return gradY;
260 | };
261 |
262 | Scissors.prototype.gradUnitVector = function(px, py, out) {
263 | var gradX = this.gradX;
264 | var gradY = this.gradY;
265 | var width = this.width;
266 |
267 | // Returns the gradient vector at (px,py), scaled to a magnitude of 1
268 | var ox = gradX[index(py, px, width)]; var oy = gradY[index(py, px, width)];
269 |
270 | var gvm = Math.sqrt(ox*ox + oy*oy);
271 | gvm = Math.max(gvm, 1e-100); // To avoid possible divide-by-0 errors
272 |
273 | out.x = ox / gvm;
274 | out.y = oy / gvm;
275 | };
276 |
277 | // Pre-created to reduce allocation in inner loops
278 | var __dgpuv = new Point(-1, -1); var __gdquv = new Point(-1, -1);
279 |
280 | Scissors.prototype.gradDirection = function(px, py, qx, qy) {
281 | // Compute the gradiant direction, in radians, between to points
282 | this.gradUnitVector(px, py, __dgpuv);
283 | this.gradUnitVector(qx, qy, __gdquv);
284 |
285 | var dp = __dgpuv.y * (qx - px) - __dgpuv.x * (qy - py);
286 | var dq = __gdquv.y * (qx - px) - __gdquv.x * (qy - py);
287 |
288 | // Make sure dp is positive, to keep things consistant
289 | if (dp < 0) {
290 | dp = -dp; dq = -dq;
291 | }
292 |
293 | if ( px != qx && py != qy ) {
294 | // We're going diagonally between pixels
295 | dp *= Math.SQRT1_2;
296 | dq *= Math.SQRT1_2;
297 | }
298 |
299 | return Scissors._2_3_PI * (Math.acos(dp) + Math.acos(dq));
300 | };
301 | Scissors._2_3_PI = (2 / (3 * Math.PI)); // Precompute'd
302 |
303 | Scissors.prototype.computeSides = function() {
304 | // Returns 2 2D arrays, containing inside and outside greyscale values.
305 | // These greyscale values are the intensity just a little bit along the
306 | // gradient vector, in either direction, from the supplied point. These
307 | // values are used when using active-learning Intelligent Scissors
308 | var greyscale = this.greyscale;
309 | var mask = this.mask;
310 | var gradX = this.gradX;
311 | var gradY = this.gradY;
312 | var dist = this.edgeWidth;
313 |
314 | var sides = new Object();
315 | sides.inside = new Float32Array(greyscale.length);
316 | sides.outside = new Float32Array(greyscale.length);
317 |
318 | var guv = new Point(-1, -1); // Current gradient unit vector
319 |
320 | var width = this.width;
321 | var height = gradX.length / width;
322 |
323 | for ( var y = 0; y < height; y++ ) {
324 | for ( var x = 0; x < width; x++ ) {
325 | p = index(y, x, width);
326 |
327 | if ( mask && !mask[p] ) {
328 | continue;
329 | }
330 |
331 | //console.log(gradX.length + " " + gradY.length + " " + new Point(x,y) + " " + guv);
332 |
333 | this.gradUnitVector(gradX, gradY, x, y, guv);
334 |
335 | //console.log(guv + "= (" + guv.x + ", " + guv.y + ")");
336 |
337 | //(x, y) rotated 90 = (y, -x)
338 |
339 | var ix = Math.round(x + dist*guv.y);
340 | var iy = Math.round(y - dist*guv.x);
341 | var ox = Math.round(x - dist*guv.y);
342 | var oy = Math.round(y + dist*guv.x);
343 |
344 | ix = Math.max(Math.min(ix, width-1), 0);
345 | ox = Math.max(Math.min(ox, width-1), 0);
346 | iy = Math.max(Math.min(iy, height-1), 0);
347 | oy = Math.max(Math.min(oy, height-1), 0);
348 |
349 | sides.inside[p] = greyscale[index(iy, ix, width)];
350 | sides.outside[p] = greyscale[index(oy, ox, width)];
351 | }
352 | }
353 |
354 | return sides;
355 | };
356 |
357 | Scissors.prototype.setWorking = function(working) {
358 | // Sets working flag and informs DOM side
359 | this.working = working;
360 |
361 | if ( this.server ) {
362 | this.server.setWorking(working);
363 | }
364 | };
365 |
366 | // Begin training methods //
367 | Scissors.prototype.getTrainingIdx = function(granularity, value) {
368 | return Math.round((granularity - 1) * value);
369 | };
370 |
371 | Scissors.prototype.getTrainedEdge = function(edge) {
372 | return this.edgeTraining[this.getTrainingIdx(this.edgeGran, edge)];
373 | };
374 |
375 | Scissors.prototype.getTrainedGrad = function(grad) {
376 | return this.gradTraining[this.getTrainingIdx(this.gradGran, grad)];
377 | };
378 |
379 | Scissors.prototype.getTrainedInside = function(inside) {
380 | return this.insideTraining[this.getTrainingIdx(this.insideGran, inside)];
381 | };
382 |
383 | Scissors.prototype.getTrainedOutside = function(outside) {
384 | return this.outsideTraining[this.getTrainingIdx(this.outsideGran, outside)];
385 | };
386 | // End training methods //
387 |
388 | Scissors.prototype.status = function(msg) {
389 | // Update the status message on the DOM side
390 | if ( this.server != null ) {
391 | this.server.status(msg);
392 | }
393 | };
394 |
395 | Scissors.prototype.setDimensions = function(width, height) {
396 | this.width = width;
397 | this.height = height;
398 | };
399 |
400 | Scissors.prototype.setData = function(greyscale, mask) {
401 | if ( this.width == -1 || this.height == -1 ) {
402 | // The width and height should have already been set
403 | throw new Error("Dimensions have not been set.");
404 | }
405 |
406 | this.mask = mask;
407 | this.greyscale = greyscale;
408 |
409 | this.status(PREPROCESSING_STR + " 1/6");
410 | this.laplace = this.computeLaplace();
411 | this.status(PREPROCESSING_STR + " 2/6");
412 | this.gradient = this.computeGradient();
413 | this.status(PREPROCESSING_STR + " 3/6");
414 | this.gradX = this.computeGradX();
415 | this.status(PREPROCESSING_STR + " 4/6");
416 | this.gradY = this.computeGradY();
417 | this.status(PREPROCESSING_STR + " 5/6");
418 |
419 | if ( this.width * this.height <= MAX_IMAGE_SIZE_FOR_TRAINING ) {
420 | var sides = this.computeSides();
421 | this.status(PREPROCESSING_STR + " 6/6");
422 | this.inside = sides.inside;
423 | this.outside = sides.outside;
424 | this.edgeTraining = new Float32Array(this.edgeGran);
425 | this.gradTraining = new Float32Array(this.gradGran);
426 | this.insideTraining = new Float32Array(this.insideGran);
427 | this.outsideTraining = new Float32Array(this.outsideGran);
428 | }
429 | };
430 |
431 | Scissors.prototype.findTrainingPoints = function(p) {
432 | // Grab the last handful of points for training
433 | var points = new Uint32Array();
434 |
435 | if ( this.parents != null ) {
436 | for ( var i = 0; i < this.trainingLength && p; i++ ) {
437 | points.push(p);
438 | p = this.parents[p];
439 | }
440 | }
441 |
442 | return points;
443 | };
444 |
445 | Scissors.prototype.resetTraining = function() {
446 | this.trained = false; // Training is ignored with this flag set
447 | };
448 |
449 | Scissors.prototype.doTraining = function(p) {
450 | if ( this.width * this.height > MAX_IMAGE_SIZE_FOR_TRAINING ) {
451 | return;
452 | }
453 |
454 | // Compute training weights and measures
455 | this.trainingPoints = this.findTrainingPoints(p);
456 |
457 | if ( this.trainingPoints.length < 8 ) {
458 | return; // Not enough points, I think. It might crash if length = 0.
459 | }
460 |
461 | var buffer = new Array();
462 | this.calculateTraining(buffer, this.edgeGran, this.greyscale, this.edgeTraining);
463 | this.calculateTraining(buffer, this.gradGran, this.gradient, this.gradTraining);
464 | this.calculateTraining(buffer, this.insideGran, this.inside, this.insideTraining);
465 | this.calculateTraining(buffer, this.outsideGran, this.outside, this.outsideTraining);
466 |
467 | if ( this.trainingPoints.length < this.gradPointsNeeded ) {
468 | // If we have two few training points, the gradient weight map might not
469 | // be smooth enough, so average with normal weights.
470 | this.addInStaticGrad(this.trainingPoints.length, this.gradPointsNeeded);
471 | }
472 |
473 | this.trained = true;
474 | };
475 |
476 | Scissors.prototype.calculateTraining = function(buffer, granularity, input, output) {
477 | // Build a map of raw-weights to trained-weights by favoring input values
478 | buffer.length = granularity;
479 | for ( var i = 0; i < granularity; i++ ) {
480 | buffer[i] = 0;
481 | }
482 |
483 | var maxVal = 1;
484 | for ( var i = 0; i < this.trainingPoints.length; i++ ) {
485 | var p = this.trainingPoints[i];
486 | var idx = this.getTrainingIdx(granularity, input[p]);
487 | buffer[idx] += 1;
488 |
489 | maxVal = Math.max(maxVal, buffer[idx]);
490 | }
491 |
492 | // Invert and scale.
493 | for ( var i = 0; i < granularity; i++ ) {
494 | buffer[i] = 1 - buffer[i] / maxVal;
495 | }
496 |
497 | // Blur it, as suggested. Gets rid of static.
498 | gaussianBlur(buffer, output);
499 | };
500 |
501 | function gaussianBlur(buffer, out) {
502 | // Smooth values over to fill in gaps in the mapping
503 | out[0] = 0.4*buffer[0] + 0.5*buffer[1] + 0.1*buffer[1];
504 | out[1] = 0.25*buffer[0] + 0.4*buffer[1] + 0.25*buffer[2] + 0.1*buffer[3];
505 |
506 | for ( var i = 2; i < buffer.length-2; i++ ) {
507 | out[i] = 0.05*buffer[i-2] + 0.25*buffer[i-1] + 0.4*buffer[i] + 0.25*buffer[i+1] + 0.05*buffer[i+2];
508 | }
509 |
510 | len = buffer.length;
511 | out[len-2] = 0.25*buffer[len-1] + 0.4*buffer[len-2] + 0.25*buffer[len-3] + 0.1*buffer[len-4];
512 | out[len-1] = 0.4*buffer[len-1] + 0.5*buffer[len-2] + 0.1*buffer[len-3];
513 | }
514 |
515 | Scissors.prototype.addInStaticGrad = function(have, need) {
516 | // Average gradient raw-weights to trained-weights map with standard weight
517 | // map so that we don't end up with something to spiky
518 | for ( var i = 0; i < this.gradGran; i++ ) {
519 | this.gradTraining[i] = Math.min(this.gradTraining[i], 1 - i*(need - have)/(need*this.gradGran));
520 | }
521 | };
522 |
523 | Scissors.prototype.dist = function(p, q) {
524 | // The grand culmunation of most of the code: the weighted distance function
525 | var width = this.width;
526 | var px = p % width; var py = Math.round(p / width);
527 | var qx = q % width; var qy = Math.round(q / width);
528 |
529 | var grad = this.gradient[q];
530 |
531 | if ( px == qx || py == qy ) {
532 | // The distance is Euclidean-ish; non-diagonal edges should be shorter
533 | grad *= Math.SQRT1_2;
534 | }
535 |
536 | var lap = this.laplace[q];
537 | var dir = this.gradDirection(px, py, qx, qy);
538 |
539 | if ( this.trained ) {
540 | // Apply training magic
541 | var gradT = this.getTrainedGrad(grad);
542 | var edgeT = this.getTrainedEdge(this.greyscale[p]);
543 | var insideT = this.getTrainedInside(this.inside[p]);
544 | var outsideT = this.getTrainedOutside(this.outside[p]);
545 |
546 | return 0.3*gradT + 0.3*lap + 0.1*(dir + edgeT + insideT + outsideT);
547 | } else {
548 | // Normal weights
549 | return 0.43*grad + 0.43*lap + 0.11*dir;
550 | }
551 | };
552 |
553 | Scissors.prototype.adj = function(p) {
554 | var list = new Array();
555 |
556 | var width = this.width;
557 | var px = p % width; var py = Math.floor(p / width);
558 |
559 | var sx = Math.max(px-1, 0);
560 | var sy = Math.max(py-1, 0);
561 | var ex = Math.min(px+1, width-1);
562 | var ey = Math.min(py+1, this.height-1);
563 |
564 | var idx = 0;
565 | for ( var y = sy; y <= ey; y++ ) {
566 | for ( var x = sx; x <= ex; x++ ) {
567 | flat = index(y, x, width);
568 | if ( (x != px || y != py) && (!this.mask || this.mask[flat]) ) {
569 | list[idx++] = flat;
570 | }
571 | }
572 | }
573 |
574 | return list;
575 | };
576 |
577 | Scissors.prototype.setPoint = function(sp) {
578 | this.setWorking(true);
579 |
580 | // Can't use sp.index(), since this object was JSON-ified.
581 | this.curPoint = index(sp.y, sp.x, this.width);
582 |
583 | this.visited = new Uint8Array(this.greyscale.length);
584 | this.parents = new Uint32Array(this.greyscale.length);
585 |
586 | this.cost = new Float32Array(this.greyscale.length);
587 | for ( var i = 0; i < this.greyscale.length; i++ ) {
588 | this.cost[i] = Infinity;
589 | }
590 |
591 | this.pq = new BucketQueue(this.searchGranBits, function(p) {
592 | return Math.round(this.searchGran * this.costArr[p]);
593 | });
594 | this.pq.searchGran = this.searchGran;
595 | this.pq.costArr = this.cost;
596 |
597 | this.pq.push(new Number(this.curPoint));
598 | this.cost[this.curPoint] = 0;
599 | };
600 |
601 | Scissors.prototype.doWork = function() {
602 | if ( !this.working ) {
603 | return;
604 | }
605 |
606 | this.timeout = null;
607 |
608 | var pointCount = 0;
609 | var newPoints = new Array();
610 | while ( !this.pq.isEmpty() && pointCount < this.pointsPerPost ) {
611 | var p = this.pq.pop().valueOf();
612 | newPoints.push(p);
613 | newPoints.push(this.parents[p]);
614 |
615 | this.visited[p] = true;
616 |
617 | var adjList = this.adj(p);
618 | for ( var i = 0; i < adjList.length; i++) {
619 | var q = adjList[i];
620 |
621 | var pqCost = this.cost[p] + this.dist(p, q);
622 | if ( pqCost < this.cost[q] ) {
623 | if ( this.cost[q] != Number.Infinity ) {
624 | // Already in PQ, must remove it so we can re-add it.
625 | this.pq.remove(new Number(q));
626 | }
627 |
628 | this.cost[q] = pqCost;
629 | this.parents[q] = p;
630 | this.pq.push(new Number(q));
631 | }
632 | }
633 |
634 | pointCount++;
635 | }
636 |
637 | if ( this.server && this.working ) {
638 | this.server.postResults(newPoints);
639 | }
640 |
641 | if ( this.pq.isEmpty() ) {
642 | this.setWorking(false);
643 | this.status(READY_STR);
644 | }
645 |
646 | return newPoints;
647 | };
648 | //// End Scissors class ////
--------------------------------------------------------------------------------
/scissors/scissors.js:
--------------------------------------------------------------------------------
1 | // Created: A while ago
2 |
3 | function Scissors() {
4 | this.lineColor = "red"; //new Array(255, 0, 0, 255);
5 | this.fadeColor = "black";
6 | this.fadeAlpha = 0.5;
7 |
8 | this.output = null; // Element to stick output text
9 |
10 | this.image_canvas = null; // Canvas for drawing image
11 | this.line_canvas = null; // Canvas for drawing commited lines
12 | this.scratch_canvas = null; // Canvas for drawing preview lines
13 |
14 | this.image_ctx = null;
15 | this.line_ctx = null;
16 | this.scratch_ctx = null;
17 |
18 | this.scissorsWorker = null;
19 | this.trainCheck = null;
20 |
21 | this.mousePoint = new Point(0, 0);
22 | this.exampleLineDrawn = false;
23 |
24 | this.isDrawing = false;
25 |
26 | this.snapSize = 2;
27 | this.startPointSize = 4;
28 | this.start = null;
29 | this.overStart = false;
30 |
31 | this.imageUrl = null;
32 | this.img = null;
33 |
34 | this.dragScrolling = false;
35 | this.dragScrollSpeed = 1.25;
36 |
37 | this.paths = new Array(); // Array of completed paths.
38 | this.currentPath = new Array(); // Array of subpaths (which are arrays of points)
39 | // Note: each subpath goes backwards, from the destination to the source.
40 | }
41 |
42 | // Creates a new canvas element and adds it to the DOM
43 | Scissors.prototype.createCanvas = function(id, zIndex) {
44 | var imageNode = this.img;
45 |
46 | var canvas = document.createElement("canvas");
47 | canvas.id = id;
48 | canvas.width = imageNode.width;
49 | canvas.height = imageNode.height;
50 |
51 | var style = canvas.style;
52 | style.position = "absolute";
53 | style.top = "0px";
54 | style.left = "0px";
55 | style.zIndex = zIndex;
56 |
57 | if ( imageNode.nextSibling ) {
58 | imageNode.parentNode.insertBefore(canvas, imageNode.nextSibling);
59 | } else {
60 | imageNode.parentNode.appendChild(canvas);
61 | }
62 |
63 | return canvas;
64 | };
65 |
66 | // Converts absolute coordinates to canvas coordinates.
67 | Scissors.prototype.getCanvasPoint = function(x, y) {
68 | return getRelativePoint(this.image_canvas, x, y);
69 | };
70 |
71 | // Initializes everything, creates all of the canvases, and starts the Web
72 | // Workers.
73 | Scissors.prototype.init = function(img, mask, visualize) {
74 | this.img = img;
75 |
76 | this.trainCheck = document.getElementById("trainCheck");
77 | this.output = document.getElementById("output");
78 |
79 | this.image_canvas = this.createCanvas("image_canvas", 0);
80 | this.line_canvas = this.createCanvas("line_canvas", 1);
81 | this.scratch_canvas = this.createCanvas("scratch_canvas", 2);
82 | this.image_ctx = this.image_canvas.getContext("2d");
83 | this.line_ctx = this.line_canvas.getContext("2d");
84 | this.scratch_ctx = this.scratch_canvas.getContext("2d");
85 |
86 | this.image_canvas.style.position = 'static'; // So Scissors takes up space.
87 |
88 | this.image_canvas.width = this.img.naturalWidth;
89 | this.image_canvas.height = this.img.naturalHeight;
90 | this.line_canvas.width = this.image_canvas.width;
91 | this.line_canvas.height = this.image_canvas.height;
92 | this.scratch_canvas.width = this.image_canvas.width;
93 | this.scratch_canvas.height = this.image_canvas.height;
94 |
95 | this.image_ctx.drawImage(this.img, 0, 0, this.image_canvas.width, this.image_canvas.height);
96 |
97 | this.scissorsWorker = new ScissorsWorker("scissors/scissorsWorker.js");
98 |
99 | this.drawFromPoint = null;
100 | this.drawData = null;
101 |
102 | this.visualize = visualize;
103 | if ( visualize ) {
104 | this.line_ctx.strokeRect(mask.aoi[0], mask.aoi[1], mask.aoi[2], mask.aoi[3]);
105 | }
106 |
107 | // wrapHandler is a function in util.js that makes sure the handler is called with the
108 | // appropriate "this" reference.
109 | this.scissorsWorker.ondata = wrapHandler(this, this.onData);
110 | this.scissorsWorker.onerror = wrapHandler(this, this.onError);
111 | this.scissorsWorker.onstatus = wrapHandler(this, this.onStatus);
112 |
113 | if ( mask ) {
114 | this.mask = mask.points;
115 | this.aoi = mask.aoi;
116 | }
117 |
118 | this.scissorsWorker.setImageData(this.image_ctx, this.aoi, this.mask);
119 |
120 | if ( mask && mask.image ) {
121 | this.fadeImage(mask.image);
122 | }
123 |
124 | this.scratch_canvas.addEventListener("mousemove", wrapHandler(this, this.mouseMove), false);
125 | this.scratch_canvas.addEventListener("mousedown", wrapHandler(this, this.mouseClick), true);
126 | this.scratch_canvas.addEventListener("mouseup", wrapHandler(this, this.mouseUp), true);
127 | this.scratch_canvas.addEventListener("mouseout", wrapHandler(this, this.endDragScrolling), true);
128 | this.scratch_canvas.addEventListener("contextmenu", function (event) {
129 | event.preventDefault();
130 | });
131 |
132 | var updateCursor = wrapHandler(this, this.updateCursor);
133 | this.updateCursorHandler = updateCursor;
134 | this.scratch_canvas.addEventListener("mouseover", updateCursor, true);
135 |
136 | var body = document.getElementsByTagName('body')[0];
137 | body.addEventListener("keydown", updateCursor, true);
138 | body.addEventListener("keyup", updateCursor, true);
139 | };
140 |
141 | Scissors.prototype.onData = function(data) {
142 | if ( this.isDrawing && !this.exampleLineDrawn && this.mousePoint ) {
143 | // If we haven't drawn the path to the current mouse point...
144 |
145 | // ...and we can draw that path.
146 | if ( this.scissorsWorker.hasPathFor(this.mousePoint) ) {
147 | // Draw it!
148 | this.updatePreview();
149 | }
150 | }
151 |
152 | if ( this.visualize ) {
153 | if (this.drawFromPoint != this.scissorsWorker.curPoint) {
154 | this.drawData = this.line_ctx.getImageData(0,0, this.image_canvas.width, this.image_canvas.height);
155 | this.drawFromPoint = this.scissorsWorker.curPoint;
156 | }
157 |
158 | var drawData = this.drawData;
159 | for ( var i = 0; i < data.length; i += 2 ) {
160 | q = data[i+1];
161 |
162 | if ( !q ) {
163 | continue;
164 | }
165 |
166 | idx = (q.y*drawData.width + q.x) * 4;
167 |
168 | drawData.data[idx] = 255;
169 | drawData.data[idx+1] = 0;
170 | drawData.data[idx+2] = 255;
171 | drawData.data[idx+3] = 255;
172 | }
173 | this.line_ctx.putImageData(drawData, 0, 0);
174 | }
175 | };
176 |
177 | Scissors.prototype.onError = function(event){
178 | this.output.textContent = event.message;
179 | throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
180 | };
181 |
182 | Scissors.prototype.onStatus = function(msg) {
183 | this.output.textContent = msg;
184 | this.updateCursor(msg);
185 | };
186 |
187 | Scissors.prototype.fadeImage = function(image) {
188 | var aoi = this.aoi;
189 | var fade = this.createCanvas("tempFade", -100);
190 | fadeCtx = fade.getContext('2d');
191 |
192 | fadeCtx.globalCompositeOperation = "xor";
193 | fadeCtx.fillStyle = this.fadeColor;
194 | fadeCtx.fillRect(0, 0, fade.width, fade.height);
195 | fadeCtx.drawImage(image, aoi[0], aoi[1], aoi[2], aoi[3]); // Subtract mask
196 |
197 | var image_ctx = this.image_ctx;
198 | image_ctx.save();
199 | image_ctx.globalAlpha = this.fadeAlpha;
200 | this.image_ctx.drawImage(fade, 0, 0, fade.width, fade.height);
201 | image_ctx.restore();
202 |
203 | fade.parentNode.removeChild(fade);
204 | };
205 |
206 | Scissors.prototype.destroy = function() {
207 | var container = this.img.parentNode;
208 | var children = container.childNodes;
209 | var idx = 0;
210 | while ( children.length > 1 ) {
211 | if ( children[idx].id != image_id ) {
212 | container.removeChild(children[idx]);
213 | } else {
214 | idx++;
215 | }
216 | }
217 |
218 | var body = document.getElementsByTagName('body')[0];
219 | body.removeEventListener('keydown', this.updateCursorHandler);
220 | body.removeEventListener('keyup', this.updateCursorHandler);
221 |
222 | this.scissorsWorker.destroy();
223 | };
224 |
225 | // Aborts the current computation and stops showing potential paths
226 | Scissors.prototype.stopDrawing = function() {
227 | this.isDrawing = false;
228 | this.scissorsWorker.stop();
229 | this.scissorsWorker.resetTraining();
230 | this.scratch_ctx.clearRect(0, 0, this.scratch_canvas.width, this.scratch_canvas.height);
231 |
232 | if ( this.currentPath.length > 0 ) {
233 | this.paths.push(this.currentPath);
234 | this.currentPath = new Array();
235 | }
236 |
237 | this.start = null;
238 | };
239 |
240 | // Puts this object in the drawing state
241 | Scissors.prototype.drawing = function(p) {
242 | this.isDrawing = true;
243 | this.start = p;
244 | };
245 |
246 | // Deletes all of the saved lines so far
247 | Scissors.prototype.clearLines = function() {
248 | this.stopDrawing();
249 | this.paths = new Array(); // Clear stored paths
250 | this.line_ctx.clearRect(0, 0, this.line_canvas.width, this.line_canvas.height);
251 |
252 | this.start = null;
253 | };
254 |
255 | // Updates whether the algorithm should do live training, according to the
256 | // trainCheck's value
257 | Scissors.prototype.setTraining = function() {
258 | this.scissorsWorker.setTraining(this.trainCheck.value);
259 | };
260 |
261 | // Returns true if the last path saved is closed (i.e., its last point is
262 | // equal to its first).
263 | Scissors.prototype.isClosed = function() {
264 | // Closed attribute of most recent path, if any
265 | if ( this.isDrawing ) {
266 | return this.isPathClosed(this.currentPath);
267 | } else if ( this.paths.length > 0 ) {
268 | return this.isPathClosed(this.paths[this.paths.length-1]);
269 | } else {
270 | return false;
271 | }
272 | };
273 |
274 | // Returns whether the supplied path is closed
275 | Scissors.prototype.isPathClosed = function(path) {
276 | return path.length > 0
277 | && this.getFirstPoint(path).equals(this.getLastPoint(path));
278 | };
279 |
280 | // Set to true, and the algorithm will not allow the user to submit without
281 | // drawing a closed path, or add a new path once one is closed
282 | Scissors.prototype.setRequiresClosed = function(req) {
283 | this.reqClosed = req;
284 | };
285 |
286 | Scissors.prototype.requiresClosed = function() {
287 | return this.reqClosed;
288 | };
289 |
290 | // Returns true if the supplied point is considered to be over the start point
291 | // of the current path
292 | Scissors.prototype.isOverStart = function(p) {
293 | return this.start && this.start.dist(p) < this.startPointSize;
294 | };
295 |
296 | // Returns the last point in the supplied path (array of subpaths)
297 | Scissors.prototype.getLastPoint = function(path) {
298 | return path[path.length-1][0];
299 | };
300 |
301 | // Returns the first point in the supplied path (array of subpaths)
302 | Scissors.prototype.getFirstPoint = function(path) {
303 | return path[0][path[0].length-1];
304 | };
305 |
306 | // Attempts to snap the supplied point to either the starting point or a point
307 | // with high gradient magnitude.
308 | Scissors.prototype.snapPoint = function(p) {
309 | if ( this.requiresClosed() && this.isOverStart(p) ) {
310 | return this.start; // We're close enough to snap to start
311 | }
312 |
313 | var sx = p.x-this.snapSize;
314 | var sy = p.y-this.snapSize;
315 | var ex = p.x+this.snapSize;
316 | var ey = p.y+this.snapSize;
317 |
318 | var maxGrad = this.scissorsWorker.getInvertedGradient(p);
319 | var maxPoint = p;
320 | var testPoint = new Point();
321 | for ( var y = sy; y <= ey; y++ ) {
322 | testPoint.y = y;
323 | for ( var x = sx; x <= ex; x++ ) {
324 | testPoint.x = x;
325 |
326 | grad = this.scissorsWorker.getInvertedGradient(testPoint);
327 | if ( grad < maxGrad ) {
328 | maxGrad = grad;
329 | maxPoint.x = testPoint.x; maxPoint.y = testPoint.y;
330 | }
331 | }
332 | }
333 |
334 | return maxPoint;
335 | };
336 |
337 | Scissors.prototype.inAoi = function(p) {
338 | var aoi = this.aoi;
339 | var mask = this.mask;
340 | return !aoi || (p.x >= aoi[0] && p.x - aoi[0] <= aoi[2]
341 | && p.y >= aoi[1] && p.y - aoi[1] <= aoi[3]
342 | && mask[index(p.y-aoi[1], p.x-aoi[0], aoi[2])]);
343 | };
344 |
345 | // Captures mouse clicks and either updates the path, starts a new one, and/or
346 | // finishes the current one.
347 | Scissors.prototype.mouseClick = function(event) {
348 | var p = this.getCanvasPoint(event.clientX, event.clientY);
349 |
350 | if ( event.button == 2 ) { // Right mouse button
351 | this.rightClick(event);
352 | } else if ( event.button == 0 ) { // Left mouse button
353 | this.leftClick(event, p);
354 | }
355 |
356 | this.updateCursor(event);
357 | event.preventDefault();
358 | };
359 |
360 | Scissors.prototype.rightClick = function(event) {
361 | if ( this.requiresClosed() && this.isDrawing ) {
362 | // close path.
363 | this.currentPath.push(_this.getLine(this.start, this.getLastPoint(this.currentPath)));
364 | this.stopDrawing();
365 | this.redrawPaths();
366 | } else if ( !this.requiresClosed() ) {
367 | this.startDragScrolling(event);
368 | }
369 | };
370 |
371 | Scissors.prototype.leftClick = function(event, p) {
372 | if ( event.ctrlKey ) {
373 | this.startDragScrolling(event);
374 | return;
375 | }
376 |
377 | if ( !this.inAoi(p) ) {
378 | return;
379 | }
380 |
381 | if ( !event.altKey ) {
382 | p = this.snapPoint(p);
383 | }
384 |
385 | if ( this.isDrawing && this.scissorsWorker.hasPathFor(p) ) {
386 | // If we're drawing, and the chosen point has it's path calculated
387 | // add path to point and continue
388 | this.appendPath(p, this.currentPath);
389 | this.redrawPaths();
390 |
391 | }
392 |
393 | // Stop drawing if the user requests it (and we can), or when the path is
394 | // finished
395 | if ( (event.shiftKey && this.isDrawing && !this.requiresClosed())
396 | || (this.requiresClosed() && this.isClosed()) ) {
397 | this.stopDrawing();
398 | this.redrawPaths();
399 | } else if ( !this.isDrawing ) {
400 | if ( this.requiresClosed() && this.isClosed() ) {
401 | window.alert('Path is already closed. Click "Undo" or "Clear Lines" to change the path.');
402 | }
403 |
404 | // Start drawing new segment
405 | this.drawing(p);
406 | this.drawStart();
407 | this.scissorsWorker.setPoint(p);
408 | } else {
409 | // We're continuing a edge as normal.
410 | this.scissorsWorker.setPoint(p);
411 | }
412 | };
413 |
414 | Scissors.prototype.mouseUp = function(event) {
415 | this.endDragScrolling();
416 | this.updateCursor(event);
417 | };
418 |
419 | // Captures mouse movement and updates preview paths accordingly
420 | Scissors.prototype.mouseMove = function(event) {
421 | var p = this.getCanvasPoint(event.clientX, event.clientY);
422 |
423 | if ( this.dragScrolling ) {
424 | this.updateDragScrolling(event);
425 | } else if ( this.isDrawing && this.inAoi(p) ) {
426 | if ( !event.ctrlKey ) {
427 | p = this.snapPoint(p);
428 | }
429 |
430 | this.updatePreview();
431 | }
432 |
433 | this.mousePoint = p;
434 | this.updateCursor(event);
435 | };
436 |
437 | Scissors.prototype.endDragScrolling = function() {
438 | this.dragScrolling = false;
439 | };
440 |
441 | Scissors.prototype.startDragScrolling = function(event) {
442 | this.prevDragPoint = new Point(event.screenX, event.screenY);
443 | this.dragScrolling = true;
444 | };
445 |
446 | Scissors.prototype.updateDragScrolling = function(event) {
447 | var tx = this.prevDragPoint.x - event.screenX;
448 | var ty = this.prevDragPoint.y - event.screenY;
449 |
450 | // Prefer axis-aligned movement to reduce apparent jitteriness
451 | txa = Math.abs(tx);
452 | tya = Math.abs(ty);
453 | if ( (txa < 3 && tya > 5) || (txa * 9 < tya) ) {
454 | tx = 0;
455 | } else if ( (tya < 3 && txa > 5) || (tya * 9 < txa) ) {
456 | ty = 0;
457 | }
458 |
459 | var speed = this.dragScrollSpeed;
460 | window.scrollBy(tx * speed, ty * speed);
461 | this.prevDragPoint = new Point(event.screenX, event.screenY);
462 | };
463 |
464 | Scissors.prototype.updatePreview = function() {
465 | this.exampleLineDrawn = this.scissorsWorker.hasPathFor(this.mousePoint);
466 |
467 | this.scratch_ctx.clearRect(0, 0, this.scratch_canvas.width, this.scratch_canvas.height);
468 | this.drawPathFrom(this.mousePoint, this.scratch_ctx);
469 |
470 | this.overStart = this.isOverStart(this.mousePoint);
471 | this.drawStart();
472 | };
473 |
474 | //Draws a line from the supplied point to the start point onto the supplied
475 | //context.
476 | Scissors.prototype.drawPathFrom = function(p, imageCtx) {
477 | var subpath = this.scissorsWorker.getPathFrom(p);
478 |
479 | if (subpath.length < 2) {
480 | return;
481 | }
482 |
483 | imageCtx.strokeStyle = this.lineColor;
484 | imageCtx.beginPath();
485 | imageCtx.moveTo(subpath[0].x, subpath[1].y);
486 | for ( var i = 1; i < subpath.length; i++ ) {
487 | imageCtx.lineTo(subpath[i].x, subpath[i].y);
488 | }
489 | imageCtx.stroke();
490 | };
491 |
492 | // Draws the supplied path onto the context.
493 | Scissors.prototype.drawPath = function(path, imageCtx) {
494 | imageCtx.strokeStyle = this.lineColor;
495 |
496 | for ( var i = 0; i < path.length; i++ ) { // Iterate over subpaths
497 | var subpath = path[i];
498 | imageCtx.beginPath();
499 | imageCtx.moveTo(subpath[0].x, subpath[0].y);
500 | for ( var j = 0; j < subpath.length; j++ ) { // and points.
501 | imageCtx.lineTo(subpath[j].x, subpath[j].y);
502 | }
503 | imageCtx.stroke();
504 | }
505 | };
506 |
507 | // Draws a circle representing the starting point of the current path.
508 | Scissors.prototype.drawStart = function() {
509 | if ( this.start && this.requiresClosed() ) {
510 | this.line_ctx.beginPath();
511 | this.line_ctx.arc(this.start.x, this.start.y, this.startPointSize, 0, 2*Math.PI);
512 | this.line_ctx.fill();
513 | this.line_ctx.stroke();
514 | }
515 | };
516 |
517 | // Appends the subpath from the supplied point to the previous clicked point to
518 | // the supplied path array
519 | Scissors.prototype.appendPath = function(p, path) {
520 | subpath = this.scissorsWorker.getPathFrom(p);
521 | path.push(subpath);
522 | };
523 |
524 | // Bresenham's algorithm for constructing a straight line between two points.
525 | // Thank you, Phrogz, from StackOverflow.
526 | Scissors.prototype.getLine = function(p, q) {
527 | var line = new Array();
528 |
529 | // For faster access
530 | px = p.x; py = p.y;
531 | qx = q.x; qy = q.y;
532 |
533 | var dx = Math.abs(qx-px);
534 | var dy = Math.abs(qy-py);
535 | var sx = (px < qx) ? 1 : -1;
536 | var sy = (py < qy) ? 1 : -1;
537 | var err = dx - dy;
538 |
539 | while( (px != qx) || (py != qy) ) {
540 |
541 | // Do what you need to for this
542 | line.push(new Point(px, py));
543 |
544 | var e2 = 2 * err;
545 |
546 | if ( e2 > -dy ){
547 | err -= dy;
548 | px += sx;
549 | }
550 |
551 | if ( e2 < dx ){
552 | err += dx;
553 | py += sy;
554 | }
555 | }
556 |
557 | line.push(new Point(px, py));
558 | return line;
559 | };
560 |
561 | // Undoes the previously commited line
562 | Scissors.prototype.undo = function() {
563 | // Remove last path component and redraw
564 | if ( this.isDrawing && this.currentPath.length == 0 ) {
565 | this.stopDrawing();
566 | } else {
567 | this.stopDrawing();
568 | if ( this.paths.length > 0 ) {
569 | var path = this.paths[this.paths.length - 1]; // Last element
570 | var removed = path.pop();
571 |
572 | // Start drawing from the start of the removed path
573 | this.scissorsWorker.setPoint(removed[removed.length-1]);
574 |
575 | this.currentPath = this.paths.pop(); // currentPath = path
576 | if ( this.currentPath.length > 0 ) {
577 | this.drawing(this.getFirstPoint(this.currentPath));
578 | } else {
579 | this.drawing(removed[removed.length-1]);
580 | }
581 | }
582 | }
583 |
584 | this.redrawPaths();
585 | };
586 |
587 | // Redraws everything except the image canvas
588 | Scissors.prototype.redrawPaths = function() {
589 | // Clear canvas
590 | var line_ctx = this.line_ctx;
591 | line_ctx.clearRect(0, 0, this.line_canvas.width, this.line_canvas.height);
592 |
593 | for ( var i = 0; i < this.paths.length; i++ ) { // Iterate over paths...
594 | // and draw
595 | this.drawPath(this.paths[i], line_ctx);
596 | }
597 |
598 | // Redraw start point and current path
599 | if ( this.currentPath && this.currentPath.length > 0 ) {
600 | this.drawPath(this.currentPath, line_ctx);
601 | }
602 |
603 | this.drawStart(); // Must draw straight to canvas
604 | };
605 |
606 | // Completely replaces the paths array
607 | Scissors.prototype.setPaths = function(paths) {
608 | this.stopDrawing();
609 | this.paths = paths;
610 | this.redrawPaths();
611 | };
612 |
613 | Scissors.prototype.updateCursor = function(event) {
614 | var target = this.scratch_canvas.style;
615 |
616 | if ( this.dragScrolling || event.ctrlKey ) {
617 | target.cursor = 'move'; // Drag scrolling
618 | } else if ( !this.scissorsWorker.initialProcessingDone() ) {
619 | target.cursor = 'wait'; // Processing
620 | } else if ( this.inAoi(this.mousePoint)) {
621 | if ( this.isDrawing && event.shiftKey ) {
622 | target.cursor = 'pointer'; // End path
623 | } else {
624 | target.cursor = 'crosshair'; // Normal point picking
625 | }
626 | } else {
627 | target.cursor = 'default';
628 | }
629 | };
630 |
631 | // Attempts to encode the current paths array and add it to the scissors_form
632 | // form object.
633 | Scissors.prototype.submitScissors = function() {
634 | if ( this.requiresClosed() && !this.isClosed() ) {
635 | window.alert("Outline must form a complete loop, which it currently doesn't.");
636 | return false; // Cancel submission
637 | }
638 |
639 | var form = document.getElementById('scissors_form');
640 |
641 | // Create hidden form element for path
642 | var pathInput = document.createElement('input');
643 | pathInput.setAttribute('type', 'hidden');
644 | pathInput.setAttribute('name', 'paths');
645 | pathInput.setAttribute('value', JSON.stringify(paths));
646 |
647 | form.appendChild(pathInput);
648 | return true;
649 | };
650 |
651 |
652 |
--------------------------------------------------------------------------------