├── 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 |
  1. Status Bar: Displays what is currently going on behind the scenes.
  2. 39 |
  3. Stop Drawing: Stops the drawing process and detach the mouse from the last point clicked.
  4. 40 |
  5. Clear Lines: Removes all of the lines so far from the image. This cannot be undone.
  6. 41 |
  7. Undo: Undoes the last line segment drawn and stops the drawing process.
  8. 42 |
  9. Submit: Submits the HIT to Mechanical Turk for processing.
  10. 43 |
  11. Image: The image to draw on. Clicking on this will start the drawing process.
  12. 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 |
  1. First wait for the status bar (1) to say "Ready". This tells you that the image (6) is ready to be drawn on.
  2. 54 |
  3. 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...".
  4. 55 |
  5. 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.
  6. 56 |
  7. 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.
  8. 57 |
  9. 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.
  10. 58 |
  11. 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.
  12. 59 |
  13. Once you are finished, click on the submit button (5) to submit the HIT.
  14. 60 |
61 | 62 |

Notes:

63 | 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 |
163 |
164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | — 173 | — 174 | 179 |
180 | 181 |
182 | 183 |
184 | 185 | 186 | 187 | 188 |

Intelligent Scissors

189 |
190 |
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 | --------------------------------------------------------------------------------