├── README.md ├── cdt.js ├── cloner.js ├── index.html ├── mvc.js ├── quad.js ├── source.jpg ├── target.jpg ├── ui.js └── underscore-min.js /README.md: -------------------------------------------------------------------------------- 1 | Instant Image Cloning 2 | ======= 3 | Yijie 4 | 5 | ## Demo 6 | 7 | - Use a static server to host the files to overcome the cross-origin restriction. 8 | - Open the demo web page, select a region on the source (upper) image, then drag the cloned region on the target image. 9 | - There are four moded for comparing: 10 | 11 | 1. Composite: simply copy the source image pixel by pixel; 12 | 2. Average: take the average pixel value of the two images (equivalent to alpha = 50%); 13 | 3. Mean-value Coordinates: approximate the solution of Poisson equation by calculating the MVC; 14 | 4. Memberane: just the difference imposed on the source image to obtain the MVC result (i.e., the difference of MVC mode and Composite mode). 15 | 16 | 17 | ## Files 18 | 19 | - ui.js: read/write canvas image data, mouse controls; 20 | - mvc.js: calculate hierarchical boundary, mean-value coordinates; 21 | - quad.js: construct quadratree, conjugate gradient descent(bug still not fixed); 22 | - cloner.js: get the boundary difference, produce image data to put on source/target canvas subject to the boundary conditions; 23 | 24 | + [lib]cdt.js: for Constrained Delaunay Triangulation, a few Node.js library; 25 | + [lib]underscore-min.js [lib]: some commonly used Js utils. 26 | 27 | 28 | 29 | ## Reference 30 | 31 | 1. Coordinates for Instant Image Cloning 32 | 2. Efficient Gradient-Domain Compositing Using Quadtrees 33 | -------------------------------------------------------------------------------- /cdt.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Npm Lib binary-search-bound 3 | */ 4 | 5 | var bsearch = (function(){ 6 | function compileSearch(funcName, predicate, reversed, extraArgs, earlyOut) { 7 | var code = [ 8 | "function ", funcName, "(a,l,h,", extraArgs.join(","), "){", 9 | earlyOut ? "" : "var i=", (reversed ? "l-1" : "h+1"), 10 | ";while(l<=h){\ 11 | var m=(l+h)>>>1,x=a[m]"] 12 | if(earlyOut) { 13 | if(predicate.indexOf("c") < 0) { 14 | code.push(";if(x===y){return m}else if(x<=y){") 15 | } else { 16 | code.push(";var p=c(x,y);if(p===0){return m}else if(p<=0){") 17 | } 18 | } else { 19 | code.push(";if(", predicate, "){i=m;") 20 | } 21 | if(reversed) { 22 | code.push("l=m+1}else{h=m-1}") 23 | } else { 24 | code.push("h=m-1}else{l=m+1}") 25 | } 26 | code.push("}") 27 | if(earlyOut) { 28 | code.push("return -1};") 29 | } else { 30 | code.push("return i};") 31 | } 32 | return code.join("") 33 | } 34 | 35 | function compileBoundsSearch(predicate, reversed, suffix, earlyOut) { 36 | var result = new Function([ 37 | compileSearch("A", "x" + predicate + "y", reversed, ["y"], earlyOut), 38 | compileSearch("P", "c(x,y)" + predicate + "0", reversed, ["y", "c"], earlyOut), 39 | "function dispatchBsearch", suffix, "(a,y,c,l,h){\ 40 | if(typeof(c)==='function'){\ 41 | return P(a,(l===void 0)?0:l|0,(h===void 0)?a.length-1:h|0,y,c)\ 42 | }else{\ 43 | return A(a,(c===void 0)?0:c|0,(l===void 0)?a.length-1:l|0,y)\ 44 | }}\ 45 | return dispatchBsearch", suffix].join("")) 46 | return result() 47 | } 48 | 49 | return { 50 | ge: compileBoundsSearch(">=", false, "GE"), 51 | gt: compileBoundsSearch(">", false, "GT"), 52 | lt: compileBoundsSearch("<", true, "LT"), 53 | le: compileBoundsSearch("<=", true, "LE"), 54 | eq: compileBoundsSearch("-", true, "EQ", true) 55 | } 56 | 57 | })(); 58 | 59 | 60 | /* 61 | * Npm Lib cdt2d 62 | */ 63 | 64 | var createTriangulation = (function(){ 65 | 66 | 67 | function Triangulation(stars, edges) { 68 | this.stars = stars 69 | this.edges = edges 70 | } 71 | 72 | var proto = Triangulation.prototype 73 | 74 | function removePair(list, j, k) { 75 | for(var i=1, n=list.length; i= 0 96 | } 97 | })() 98 | 99 | proto.removeTriangle = function(i, j, k) { 100 | var stars = this.stars 101 | removePair(stars[i], j, k) 102 | removePair(stars[j], k, i) 103 | removePair(stars[k], i, j) 104 | } 105 | 106 | proto.addTriangle = function(i, j, k) { 107 | var stars = this.stars; 108 | stars[i].push(j, k) 109 | stars[j].push(k, i) 110 | stars[k].push(i, j) 111 | } 112 | 113 | 114 | // the starting point in the half edge toward j in stars[i], ReadOnly 115 | proto.opposite = function(j, i) { 116 | var list = this.stars[i] 117 | for(var k=1, n=list.length; k 0 177 | return (b[0]-c[0])*(a[1]-c[1]) - (b[1]-c[1])*(a[0]-c[0]); 178 | } 179 | 180 | var EVENT_POINT = 0 181 | var EVENT_END = 1 182 | var EVENT_START = 2 183 | 184 | //A partial convex hull fragment, made of two unimonotone polygons 185 | function PartialHull(a, b, idx, lowerIds, upperIds) { 186 | this.a = a 187 | this.b = b 188 | this.idx = idx 189 | this.lowerIds = lowerIds 190 | this.upperIds = upperIds 191 | } 192 | 193 | //An event in the sweep line procedure 194 | // a, b: points 195 | // type: EVENT_POINT, EVENT_END, EVENT_START 196 | // idx: edge index 197 | function Event(a, b, type, idx) { 198 | this.a = a 199 | this.b = b 200 | this.type = type 201 | this.idx = idx 202 | } 203 | 204 | //This is used to compare events for the sweep line procedure 205 | // Points are: 206 | // 1. sorted lexicographically 207 | // 2. sorted by type (point < end < start) 208 | // 3. segments sorted by winding order 209 | // 4. sorted by index 210 | function compareEvent(a, b) { 211 | var d = 212 | (a.a[0] - b.a[0]) || 213 | (a.a[1] - b.a[1]) || 214 | (a.type - b.type) 215 | if(d) { return d } 216 | // handle equal case, a.a == b.a 217 | if(a.type !== EVENT_POINT) { 218 | d = orient(a.a, a.b, b.b) // <0: CCW 219 | if(d) { return d } 220 | } 221 | return a.idx - b.idx 222 | } 223 | 224 | function testPoint(hull, p) { 225 | return orient(hull.a, hull.b, p) 226 | } 227 | 228 | function addPoint(cells, hulls, points, p, idx) { 229 | var lo = bsearch.lt(hulls, p, testPoint) 230 | var hi = bsearch.gt(hulls, p, testPoint) 231 | for(var i=lo; i 1 && orient( 238 | points[lowerIds[m-2]], 239 | points[lowerIds[m-1]], 240 | p) > 0) { 241 | cells.push( 242 | [lowerIds[m-1], 243 | lowerIds[m-2], 244 | idx]) 245 | m -= 1 246 | } 247 | lowerIds.length = m 248 | lowerIds.push(idx) 249 | 250 | //Insert p into upper hull 251 | var upperIds = hull.upperIds 252 | var m = upperIds.length 253 | while(m > 1 && orient( 254 | points[upperIds[m-2]], 255 | points[upperIds[m-1]], 256 | p) < 0) { 257 | cells.push( 258 | [upperIds[m-2], 259 | upperIds[m-1], 260 | idx]) 261 | m -= 1 262 | } 263 | upperIds.length = m 264 | upperIds.push(idx) 265 | } 266 | } 267 | 268 | function findSplit(hull, edge) { 269 | var d 270 | if(hull.a[0] < edge.a[0]) { 271 | d = orient(hull.a, hull.b, edge.a) 272 | } else { 273 | d = orient(edge.b, edge.a, hull.a) 274 | } 275 | if(d) { return d } 276 | if(edge.b[0] < hull.b[0]) { 277 | d = orient(hull.a, hull.b, edge.b) 278 | } else { 279 | d = orient(edge.b, edge.a, hull.b) 280 | } 281 | return d || hull.idx - edge.idx 282 | } 283 | 284 | function splitHulls(hulls, points, event) { 285 | var splitIdx = bsearch.le(hulls, event, findSplit) 286 | var hull = hulls[splitIdx] 287 | var upperIds = hull.upperIds 288 | var x = upperIds[upperIds.length-1] 289 | hull.upperIds = [x] 290 | 291 | // insert after 292 | hulls.splice(splitIdx+1, 0, 293 | new PartialHull(event.a, event.b, event.idx, [x], upperIds)) 294 | } 295 | 296 | 297 | function mergeHulls(hulls, points, event) { 298 | //Swap pointers for merge search 299 | var tmp = event.a 300 | event.a = event.b 301 | event.b = tmp 302 | var mergeIdx = bsearch.eq(hulls, event, findSplit) 303 | var upper = hulls[mergeIdx] 304 | var lower = hulls[mergeIdx-1] 305 | lower.upperIds = upper.upperIds 306 | hulls.splice(mergeIdx, 1) 307 | } 308 | 309 | 310 | var monotone = function(points, edges){ 311 | 312 | var numPoints = points.length; 313 | var numEdges = edges.length; 314 | 315 | //Create point events 316 | var events = points.map(function(e,i){ 317 | return new Event( 318 | e, 319 | null, 320 | EVENT_POINT, 321 | i); 322 | }); 323 | 324 | 325 | //Create edge events 326 | edges.forEach(function(e, i){ 327 | var a = points[e[0]] 328 | var b = points[e[1]] 329 | if(a[0] < b[0]) { 330 | events.push( 331 | new Event(a, b, EVENT_START, i), 332 | new Event(b, a, EVENT_END, i)) 333 | } else if(a[0] > b[0]) { 334 | events.push( 335 | new Event(b, a, EVENT_START, i), 336 | new Event(a, b, EVENT_END, i)) 337 | } 338 | //? a[0] == b[0] 339 | }); 340 | 341 | //Sort events 342 | events.sort(compareEvent) 343 | 344 | //Initialize hull 345 | var minX = events[0].a[0] - (1 + Math.abs(events[0].a[0])) * Math.pow(2, -52) 346 | var hull = [ new PartialHull([minX, 1], [minX, 0], -1, [], [], [], []) ] 347 | 348 | //Process events in order 349 | var cells = [] 350 | for(var i=0, numEvents=events.length; iarr[i]){ 391 | min = arr[i]; 392 | i_min = i; 393 | } 394 | } 395 | return i_min; 396 | } 397 | // >0 : inside circle 398 | function inCircle(p1, p2, p3, p4){ 399 | var pts = [p1, p2, p3].map(function(p){ 400 | var ret = {x:p[0]-p4[0], y:p[1]-p4[1]}; 401 | ret.r2 = ret.x*ret.x + ret.y*ret.y; 402 | return ret; 403 | }); 404 | return pts[0].r2 * det2(pts[1], pts[2]) + 405 | pts[1].r2 * det2(pts[2], pts[0]) + 406 | pts[2].r2 * det2(pts[0], pts[1]); 407 | 408 | } 409 | function smallestCos(p0, p1, p2, info){ 410 | var el = [0,0,0]; 411 | el[0] = abdis(p1, p2); 412 | el[1] = abdis(p2, p0); 413 | el[2] = abdis(p0, p1); 414 | var i_min = min_index(el); 415 | var a=el[i_min], b = el[(i_min+1)%3], c = el[(i_min+2)%3]; 416 | 417 | if(info != null){ 418 | info['i_min'] = i_min; 419 | info['i_max'] = b 0) { 509 | var b = stack.pop() 510 | var a = stack.pop() 511 | 512 | //Find opposite pairs 513 | var x = -1, y = -1 514 | var star = stars[a] 515 | for(var i=1; i= 0) { 532 | continue 533 | } 534 | 535 | //Flip the edge 536 | triangulation.flip(a, b) 537 | 538 | //Test flipping neighboring edges 539 | testFlip(points, triangulation, stack, x, a, y) 540 | testFlip(points, triangulation, stack, a, y, x) 541 | testFlip(points, triangulation, stack, y, b, x) 542 | testFlip(points, triangulation, stack, b, x, y) 543 | } 544 | 545 | } 546 | 547 | function badShape(cell, cri){ 548 | var pts = cell.map(function(id){ 549 | return points[id]; 550 | }); 551 | //console.log('badShape', pts, cell); 552 | var info = {}; 553 | var tmp = smallestCos(pts[0], pts[1], pts[2], info); 554 | if(tmp > cri){ 555 | if(tmp >= 1-1e-8){ 556 | 557 | var a = cell[info.i_max], b = cell[(info.i_max+1)%3], c = cell[(info.i_max+2)%3]; 558 | var ops = triangulation.opposite(c, b); 559 | if(ops == a){ 560 | console.log('reversed!'); 561 | ops = triangulation.opposite(b, c); 562 | } 563 | 564 | 565 | triangulation.removeTriangle(a, b, c); 566 | if(ops < 0){ 567 | console.log('fall on boundary'); 568 | return false; 569 | } 570 | triangulation.removeTriangle(c, b, ops); 571 | triangulation.addTriangle(a, b, ops); 572 | triangulation.addTriangle(a, ops,c); 573 | 574 | testFlip(points, triangulation, stack, ops, c, a); 575 | testFlip(points, triangulation, stack, b, ops, a); 576 | //console.log('colinear!', stack.length); 577 | return false; 578 | } 579 | var new_point = [0,1].map(function(i){ 580 | var pp = pts.map(function(e){ 581 | return e[i]; 582 | }); 583 | return Math.round( 584 | dotp(info.el, pp, 3)/(info.el_sum*1.5) +dotp([1,1,1], pp, 3)/9 585 | ); 586 | }); 587 | //console.log('bad triangle', new_point, tmp); 588 | return new_point; 589 | }else{ 590 | return false; 591 | } 592 | } 593 | function checkCriteria(cell){ 594 | var tmp = badShape(cell, criteria); 595 | if(!tmp){// good 596 | return true; 597 | }else{ 598 | triangulation.removeTriangle(cell[0], cell[1], cell[2]); 599 | console.log('add points'); 600 | points.push(tmp); 601 | triangulation.stars.push([]); 602 | tmp = points.length - 1; 603 | triangulation.addTriangle(cell[0], cell[1], tmp); 604 | triangulation.addTriangle(cell[1], cell[2], tmp); 605 | triangulation.addTriangle(cell[2], cell[0], tmp); 606 | 607 | testFlip(points, triangulation, stack, cell[0], cell[1], tmp); 608 | testFlip(points, triangulation, stack, cell[1], cell[2], tmp); 609 | testFlip(points, triangulation, stack, cell[2], cell[0], tmp); 610 | 611 | return false; 612 | 613 | 614 | } 615 | 616 | } 617 | runStack(); 618 | // check once 619 | //var cells = triangulation.cells(); 620 | 621 | function pickCell(){ 622 | var p0 = Math.floor(Math.random()*(points.length - 1)); 623 | var star = triangulation.stars[p0]; 624 | 625 | var p1 = Math.floor(Math.random() * star.length/2); 626 | var pt1 = star[p1*2], pt2 = star[p1*2+1]; 627 | return [p0, pt1, pt2]; 628 | 629 | } 630 | 631 | function optPick(N){ 632 | var cells = triangulation.cells(); 633 | function compareCri(a, b){ 634 | return (b[1] - a[1]) || (b[2]-a[2]) || (a[0] - b[0]); 635 | } 636 | var scores = cells.map(function(cell, i){ 637 | var pts = cell.map(function(id){ 638 | return points[id]; 639 | }); 640 | var amin = smallestCos(pts[0], pts[1], pts[2]); 641 | var lmax = Math.max(abdis(pts[0], pts[1]), abdis(pts[1], pts[2]), abdis(pts[2], pts[0])); 642 | return [i, amin, lmax]; 643 | }); 644 | 645 | 646 | var tot = 0; 647 | scores.forEach(function(e){ 648 | tot += e[1]; 649 | }); 650 | tot /= scores.length; 651 | 652 | scores.sort(compareCri); 653 | 654 | var bench = [criteria, 1, N]; 655 | var bench_i = bsearch.lt(scores, bench, compareCri); 656 | console.log('bench', bench_i, scores.length); 657 | 658 | var len = Math.min(N, cells.length); 659 | var npt = 0; 660 | for(var i=0;i0){ 680 | console.log('depth reduced, points len ', points.length); 681 | delaunayRefine(depth-1, points, triangulation, options) 682 | } 683 | 684 | 685 | } 686 | 687 | return function(points, triangulation, options){ 688 | return delaunayRefine(0, points, triangulation, options); 689 | } 690 | 691 | })(); 692 | 693 | var classifyFaces = (function(){ 694 | 695 | function FaceIndex(cells, neighbor, constraint, flags, active, next, boundary) { 696 | this.cells = cells 697 | this.neighbor = neighbor 698 | this.flags = flags 699 | this.constraint = constraint 700 | this.active = active 701 | this.next = next 702 | this.boundary = boundary 703 | } 704 | 705 | var proto = FaceIndex.prototype 706 | 707 | function compareCell(a, b) { 708 | return a[0] - b[0] || 709 | a[1] - b[1] || 710 | a[2] - b[2] 711 | } 712 | 713 | proto.locate = (function() { 714 | var key = [0,0,0] 715 | return function(a, b, c) { 716 | var x = a, y = b, z = c 717 | if(b < c) { 718 | if(b < a) { 719 | x = b 720 | y = c 721 | z = a 722 | } 723 | } else if(c < a) { 724 | x = c 725 | y = a 726 | z = b 727 | } 728 | if(x < 0) { 729 | return -1 730 | } 731 | key[0] = x 732 | key[1] = y 733 | key[2] = z 734 | return bsearch.eq(this.cells, key, compareCell) 735 | } 736 | })(); 737 | 738 | function indexCells(triangulation, infinity) { 739 | //First get cells and canonicalize 740 | var cells = triangulation.cells() 741 | var nc = cells.length 742 | for(var i=0; i 0 || next.length > 0) { 835 | while(active.length > 0) { 836 | var t = active.pop() 837 | if(flags[t] === -side) { 838 | continue 839 | } 840 | flags[t] = side 841 | var c = cells[t] 842 | for(var j=0; j<3; ++j) { 843 | var f = neighbor[3*t+j] 844 | if(f >= 0 && flags[f] === 0) { 845 | if(constraint[3*t+j]) { 846 | next.push(f) 847 | } else { 848 | active.push(f) 849 | flags[f] = side 850 | } 851 | } 852 | } 853 | } 854 | 855 | //Swap arrays and loop 856 | var tmp = next; 857 | next = active 858 | active = tmp 859 | next.length = 0 860 | side = -side 861 | } 862 | 863 | var result = filterCells(cells, flags, target); 864 | if(infinity) { 865 | return result.concat(index.boundary) 866 | } 867 | return result 868 | } 869 | 870 | return classifyFaces; 871 | 872 | })(); 873 | 874 | var cdt2d = (function(){ 875 | function canonicalizeEdge(e) { 876 | return [Math.min(e[0], e[1]), Math.max(e[0], e[1])] 877 | } 878 | 879 | function compareEdge(a, b) { 880 | return a[0]-b[0] || a[1]-b[1] 881 | } 882 | 883 | function canonicalizeEdges(edges) { 884 | // copy and sort 885 | return edges.map(canonicalizeEdge).sort(compareEdge) 886 | } 887 | function cdt2d(points, edges, options) { 888 | var monotoneTriangulate = monotone; 889 | var makeIndex = createTriangulation; 890 | var delaunayFlip = delaunayRefine; 891 | var filterTriangulation = classifyFaces; 892 | 893 | if(!Array.isArray(edges)) {// omit edges 894 | options = edges || {} 895 | edges = [] 896 | } else { 897 | options = options || {} 898 | edges = edges || [] 899 | } 900 | 901 | 902 | 903 | //Handle trivial case 904 | if(points.length === 0) { 905 | return [] 906 | } 907 | 908 | //Construct initial triangulation 909 | var cells = monotoneTriangulate(points, edges); 910 | console.log('monotone', cells); 911 | 912 | //If delaunay refinement needed, then improve quality by edge flipping 913 | var triangulation = makeIndex(points.length, canonicalizeEdges(edges)) 914 | for(var i=0; i>1, y:(img.height/2)>>1}, this.boundary); 145 | var boundary = this.boundary; 146 | hb.traverse(function(data){ 147 | var pos = boundary[data.index]; 148 | drawPixel(img, {x:pos.x - 1, y:pos.y - 1}, [1, 0, 0], 1); 149 | neighbor8.forEach(function(e){ 150 | drawPixel(img, {x:pos.x - 1 + e[0], y:pos.y - 1 + e[1]}, [.9, .1, .1], 1); 151 | }); 152 | }); 153 | 154 | 155 | var tri; 156 | try{ 157 | tri = this.quadMesh(); 158 | console.log('quadMesh'); 159 | }catch(e){ 160 | console.warn(e); 161 | var simpBoundary = hb.map(function(data){ 162 | return boundary[data.index]; 163 | }); 164 | tri = adaptiveMesh(simpBoundary) 165 | 166 | } 167 | 168 | 169 | var paint = function(ctx, refpos){ 170 | //console.log('paint mesh', tri); 171 | drawTriangulation(ctx, tri.points, tri.edges, {pos:refpos, pColor:'white', pWidth:.4}); 172 | } 173 | 174 | //console.log('hierarchy boundary: ', hbsize, hb); 175 | 176 | 177 | var width = this.mask[0].length - 2; 178 | // draw interpolation points 179 | for(var node in this.rMaps.dmap.reverse){ 180 | var py = Math.floor(node/width); 181 | var px = node % width; 182 | drawPixel(img, {x:px, y:py}, [0,0,.9],1); 183 | } 184 | return {img:img, post_paint:paint}; 185 | } 186 | 187 | ImageCloner.prototype.interpolate = function(bval){ 188 | // y = Syb * b 189 | // if mvc 190 | var errCount = 0; 191 | var tot =0; 192 | 193 | var yval = this.Syb.map(function(yy){ 194 | var ret = 0; 195 | yy.forEach(function(ele){ 196 | tot++; 197 | var bid = ele[0]; 198 | var b = bval[bid]; 199 | //if(b == null || b!=b || ele[1] != ele[1]){ 200 | //console.warn('miss boundary', ele); 201 | //errCount++; 202 | //return; 203 | //} 204 | ret += b * ele[1]; 205 | 206 | }); 207 | return ret; 208 | }); 209 | 210 | //console.log('inter err', errCount, tot, yval.length); 211 | 212 | 213 | // x = S * [b, y] 214 | bval = this.vecs.b.map(function(id){ 215 | return bval[id] || 0; 216 | }); 217 | var b_y = _.object(this.vecs.Y.concat(this.vecs.b), yval.concat(bval)); 218 | 219 | 220 | var b_x = this.rMaps.dmap.rmul(b_y, this.vecs.b.concat(this.vecs.X)); 221 | 222 | return b_x; 223 | 224 | } 225 | 226 | 227 | var Mode = { 228 | composite: 0, 229 | mvc: 3, 230 | cgd: 4, 231 | avg: 2, 232 | membrane: 1 233 | } 234 | var selectedMode = Mode.mvc; 235 | 236 | 237 | function floatToRGB(rgb){ 238 | return [ 239 | Math.floor(rgb[0]*256), 240 | Math.floor(rgb[1]*256), 241 | Math.floor(rgb[2]*256) 242 | ] 243 | } 244 | 245 | ImageCloner.prototype.paintTgt = function(img, bg_img){ 246 | 247 | //var img = new ImageData(this.fg_img); 248 | 249 | // local refs 250 | var mask = this.mask; 251 | var fg_img = this.fg_img; 252 | var rmap = this.rMaps; 253 | var height = this.mask.length - 2, width = this.mask[0].length-2; 254 | var vecs = this.vecs; 255 | function convert_back(id){ 256 | return {x:id%width, y:Math.floor(id/width)}; 257 | } 258 | 259 | //console.log('bvals', bvals_arr);// len 3 260 | var b_x = vecs.b.concat(vecs.X); 261 | 262 | if(selectedMode == Mode.composite){ 263 | b_x.forEach(function(id){ 264 | var pos = convert_back(id); 265 | //var rgb = floatToRGB(rgbval[i]); 266 | var rgb = getRGB(fg_img, pos); 267 | setRGB(img, pos, rgb, 1); 268 | }); 269 | return img; 270 | } 271 | if(selectedMode == Mode.avg){ 272 | 273 | b_x.forEach(function(id){ 274 | var pos = convert_back(id); 275 | var rgb0 = getRGB(bg_img, pos); 276 | var rgb = getRGB(fg_img, pos).map(function(e,i){ 277 | return Math.floor((e+rgb0[i])/2); 278 | }); 279 | setRGB(img, pos, rgb, 1); 280 | }); 281 | return img; 282 | 283 | 284 | } 285 | // bg_img used to calculate the difference 286 | 287 | var diffs = vecs.b.map(function(id){ 288 | var pos = convert_back(id); 289 | return Vec.diff(getRGB(bg_img, pos), getRGB(fg_img, pos),3); 290 | 291 | }); 292 | 293 | var bvals_arr = _.unzip(diffs).map(function(b){ 294 | return _.object(vecs.b, b); 295 | }); 296 | 297 | var rgb_arr = Array(3); 298 | if(selectedMode == Mode.cgd){ 299 | try{ 300 | rgb_arr = run_CGD(mask, this.rMaps.dmap, bvals_arr); 301 | 302 | }catch(e){ 303 | console.warn(e); 304 | rgb_arr = Array(3) 305 | } 306 | 307 | }else{ 308 | console.time('interpolate') 309 | for(var i=0;i<3;++i){ 310 | rgb_arr[i] = this.interpolate(bvals_arr[i]); 311 | } 312 | var rgbval = _.unzip(rgb_arr); 313 | console.timeEnd('interpolate') 314 | 315 | } 316 | 317 | 318 | if(selectedMode == Mode.membrane){ 319 | 320 | for(var i=b_x.length-1;i>=0;i--){ 321 | var pos = convert_back(b_x[i]); 322 | //var rgb = floatToRGB(rgbval[i]); 323 | var rgb = rgbval[i].map(function(e,j){ 324 | return Math.round(e); 325 | }); 326 | setRGB(img, pos, rgb, 1); 327 | 328 | } 329 | return img; 330 | 331 | } 332 | 333 | // else MVC 334 | 335 | for(var i=b_x.length-1;i>=0;i--){ 336 | var pos = convert_back(b_x[i]); 337 | //var rgb = floatToRGB(rgbval[i]); 338 | var rgb0 = getRGB(fg_img, pos); 339 | var rgb = rgbval[i].map(function(e,j){ 340 | return Math.round(rgb0[j]+e); 341 | }); 342 | setRGB(img, pos, rgb, 1); 343 | } 344 | 345 | 346 | 347 | return img; 348 | } 349 | 350 | // video cloning 351 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Instant Image Cloning Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
19 |

Instant Image Cloning Demo (Using MVC and Quadratree)

20 |

Select one region in the upper figure, then drag in the lower figure.

21 |

You need a local server to serve the images, so that the cross-origin access is allowed.

22 | 23 |
24 | 25 | 32 |
33 | 34 |
35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 62 | 63 | -------------------------------------------------------------------------------- /mvc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * List class for boundary node 3 | */ 4 | function List(data){ 5 | var data = data || []; 6 | if(data.length == 0){ 7 | this.head = null; 8 | return; 9 | } 10 | this.head = {data:data[0], pre:null, next:null}; 11 | 12 | var tmp = this.head; 13 | for(var i=1;i=3 68 | var level = 3; 69 | var step = 1 << (maxLevel - 2); 70 | var indices = []; 71 | var bpsize = boundary.length; 72 | for(var i=0;i e_dis * Math.pow(.4, level)){ 109 | 110 | var c = cosdis({x:p1.x-pos.x, y:p1.y-pos.y}, {x:p2.x-pos.x, y:p2.y-pos.y}); 111 | if(Math.acos(c)< e_ang * Math.pow(.8, level)){ 112 | var t = cur.data.t2 = Math.sqrt((1-c)/(1+c));// for MVC calculation 113 | if(t!=t){ 114 | console.warn('t2', cur) 115 | } 116 | cur = cur.next; 117 | cur.data.t1 = t; 118 | continue; 119 | } 120 | } 121 | 122 | 123 | // need finer sampling 124 | step = 1<<(maxLevel - level); // increase level 125 | insertAfter({index:cur.data.index+step}, cur); 126 | } 127 | cur.data.t2 = hb.head.data.t2; 128 | cur.data.dis = hb.head.data.dis; 129 | 130 | 131 | return hb; 132 | 133 | } 134 | 135 | 136 | /* 137 | * Triangulation 138 | */ 139 | 140 | 141 | function Vec2d(x, y){ 142 | this.x = x || 0; 143 | this.y = y || 0; 144 | } 145 | Vec2d.prototype.plus = function(vec, a){ 146 | var a = a || 1; 147 | this.x += vec.x * a; 148 | this.y += vec.y * a; 149 | } 150 | Vec2d.prototype.pos = function(){ 151 | return { 152 | x: this.x, 153 | y: this.y 154 | } 155 | } 156 | Vec2d.prototype.norm = function(){ 157 | return Math.sqrt(this.x*this.x + this.y*this.y); 158 | } 159 | 160 | 161 | 162 | 163 | /* 164 | * Mean-value coordinate 165 | */ 166 | 167 | function barycentric(nodes, weights){ 168 | var ret = new Vec2d(); 169 | weights.forEach(function(e,i){ 170 | ret.plus(nodes[i], e) 171 | }); 172 | return ret.pos(); 173 | } 174 | function barycentricSparse(nodes, weights){ 175 | var ret = new Vec2d(); 176 | for(var i in weights){ 177 | var e = weights[i]; 178 | if(e > 0){ 179 | ret.plus(nodes[i], e); 180 | } 181 | } 182 | return ret.pos(); 183 | } 184 | 185 | function powdiv(x, maxlevel){ 186 | var ret = 0; 187 | while( x & 0x1 == 0 && ret < maxlevel){ 188 | x >> 1; 189 | ret++; 190 | } 191 | return ret; 192 | } 193 | 194 | function Vec(){ 195 | 196 | } 197 | Vec.diff = function(a, b, l){ 198 | var len = l || Math.min(a.length, b.length); 199 | var ret = new Array(len); 200 | for(var i=0;i=img.width || pos.y<0 || pos.y>=img.height){ 10 | return; // invalid pos 11 | } 12 | var ref = 4*(pos.y*img.width+pos.x); 13 | for(var i=0;i<3;++i){ 14 | // clamped array will handle the cases tmp<0 and tmp>255 15 | img.data[ref+i] = Math.floor(rgb[i]*256);; 16 | } 17 | if(typeof a != 'undefined'){ 18 | img.data[ref+3] = Math.floor(a*256); 19 | } 20 | } 21 | 22 | 23 | 24 | function drawRect(img, pos1, pos2, rgb, a){ 25 | // TODO: check 26 | var ref1 = 4*(pos1.y*img.width+pos1.x); 27 | var ref2 = 4*(pos2.y*img.width+pos2.x); 28 | var step = img.width * 4; 29 | var a = a || 1; 30 | for(var ref=ref1;ref= 0;xref-=4){ 32 | for(var i=0;i<3;++i){ 33 | // clamped array will handle the cases tmp<0 and tmp>255 34 | img.data[ref+xref+i] = Math.floor(rgb[i]*256); 35 | } 36 | img.data[ref+xref+3] = Math.floor(a*256); 37 | 38 | } 39 | } 40 | } 41 | function setRGB(img, pos, rgb, a){ 42 | var ref = 4 * (img.width * pos.y + pos.x); 43 | img.data[ref] = rgb[0]; 44 | img.data[ref+1] = rgb[1]; 45 | img.data[ref+2] = rgb[2]; 46 | var a = a || 1; 47 | img.data[ref+3] = Math.floor(a * 256); 48 | } 49 | function getRGB(img, pos){ 50 | var ref = 4 * (img.width * pos.y + pos.x); 51 | return [ 52 | img.data[ref], 53 | img.data[ref+1], 54 | img.data[ref+2] 55 | ]; 56 | } 57 | function diffImg(img1, img2){ 58 | var ret = { 59 | width: Math.min(img1.width, img2.width), 60 | height: Math.min(img1.height, img2.height), 61 | data: [] 62 | }; 63 | for(var y=0;y> 1; 118 | ret++; 119 | } 120 | return ret; 121 | } 122 | 123 | function rnd_vec(N){ 124 | return new Array(N).fill(null).map(function(){ 125 | return Math.random(); 126 | }); 127 | } 128 | 129 | 130 | // vector algebra 131 | function l2norm(arr){ 132 | var s = 0; 133 | arr.forEach(function(e){s += e*e;}); 134 | return arr; 135 | 136 | } 137 | function dotprod(a, b){ 138 | var len = Math.min(a.length, b.length); 139 | var s = 0; 140 | for(var i=0;i 1e-10){ 194 | if(Math.sign(x) == 0){ 195 | th = y > 0? Math.PI/2: -Math.PI/2; 196 | }else{ 197 | th = Math.atan(y/x); 198 | if(x<0){ 199 | th += Math.PI; 200 | } 201 | } 202 | }else{ 203 | r = 0; 204 | } 205 | return { 206 | r: r, 207 | th: th 208 | } 209 | 210 | } 211 | function rectCoordinates(r, th){ 212 | return { 213 | x: r * Math.cos(th), 214 | y: r * Math.sin(th) 215 | } 216 | } 217 | 218 | 219 | /* 220 | * Quadradtree 221 | */ 222 | 223 | 224 | 225 | var NodeType = { 226 | point: 0, 227 | leaf: 1, 228 | empty: 2, // outer leaf, in fact 229 | mixed: 3, // 230 | inner: 4 231 | } 232 | 233 | function QTreeNode(pos, type){ 234 | //this.level = level | 1; 235 | this.type = type || NodeType.leaf; 236 | this.children = Array(4).fill(null); // 0:++, 1:+-, 2:--, 3:-+ 237 | this.pos = pos; // key 238 | } 239 | QTreeNode.prototype.childAt = function(pos){ 240 | if(pos.x >= this.pos.x){ 241 | if(pos.y >= this.pos.y){ 242 | return this.children[0]; 243 | }else{ 244 | return this.children[1]; 245 | } 246 | }else{ 247 | if(pos.y < this.pos.y){ 248 | return this.children[2]; 249 | }else{ 250 | return this.children[3]; 251 | } 252 | } 253 | } 254 | 255 | QTreeNode.prototype.traverse = function(func){ 256 | if(this.type == NodeType.leaf){ 257 | func(this.children); 258 | }else if(this.type == NodeType.mixed){ 259 | this.children.forEach(function(e){ 260 | if(e!=null){ 261 | func(e); 262 | } 263 | }); 264 | }else if(this.type == NodeType.empty){ 265 | console.warn('wrong traverse', this); 266 | }else if(this.type == NodeType.inner){ 267 | for(var i=0;i<4;++i){ 268 | if(this.children[i] != null){ 269 | this.children[i].traverse(func); 270 | } 271 | } 272 | } 273 | } 274 | 275 | QTreeNode.prototype.search = function(pos){ 276 | if(pos.x<0 || pos.y<0 || pos.x >= this.pos.x*2 || pos.y >= this.pos.y*2){ 277 | return null; 278 | } 279 | if(this.type == NodeType.inner){ 280 | var child = this.childAt(pos); 281 | if(child == null){ 282 | return null; 283 | }else{ 284 | return child.search(pos); 285 | } 286 | }else if(this.type == NodeType.mixed){ 287 | var child = this.childAt(pos); 288 | 289 | if(child == null){ 290 | return null; 291 | }else{ 292 | if(child.x != pos.x || child.y != pos.y){ 293 | console.warn('rong search', this); 294 | } 295 | return [[child, 1]]; 296 | } 297 | }else if(this.type == NodeType.empty){ 298 | console.warn('search wrong!', this); 299 | return null; 300 | }else{ 301 | // leaf 302 | if(this.type != NodeType.leaf){ 303 | console.log('wrong search!'); 304 | } 305 | // TODO: handdle T points 306 | var step = this.children[0].y - this.children[2].y; 307 | // should be children[2] 308 | return dlint(this.children[2], pos, step); 309 | } 310 | } 311 | 312 | QTreeNode.prototype.mosaic = function(img){ 313 | this.traverse(function(arr){ 314 | if(Array.isArray(arr)){ 315 | var s = arr[0].x-arr[2].x; 316 | if(s<4) { 317 | drawPixel(img, { 318 | x: Math.floor((arr[2].x + arr[0].x) / 2), 319 | y: Math.floor((arr[0].y + arr[2].y) / 2) 320 | }, [.1, .3, .3], 1); 321 | }else{ 322 | var rgb = rnd_vec(3); 323 | drawRect(img, arr[2], arr[0], linadd([.1,0,.1], rgb, .2 * Math.log2(s)), .8); 324 | } 325 | 326 | }else{ 327 | var rgb = [.1,.2,.1]; 328 | drawPixel(img, arr, rgb, .9); 329 | } 330 | 331 | }); 332 | } 333 | 334 | // building Quadtree bottom-up 335 | function QTree(mask){ 336 | //console.log('mask', mask); 337 | var height = mask.length - 2, width = mask[0].length - 2; 338 | var maxLevel = Math.ceil(Math.log2(Math.max(height, width))); 339 | var len = 1 << (maxLevel - 1); 340 | var pre = new Array(len); 341 | var mixedCount = 0; 342 | for(var y=0;y= height || pos.x >= width){ 352 | s[0]++; // outter 353 | }else{ 354 | var val = mask[pos.y+1][pos.x+1]; 355 | s[val]++; 356 | cv[i] = val; 357 | } 358 | }); 359 | 360 | if(s[0] == 4){ 361 | // empty, will not store at the next level 362 | ele = new QTreeNode(elepos, NodeType.empty); 363 | }else{ 364 | if(s[1] < 4){// mixed 365 | ele = new QTreeNode(elepos, NodeType.mixed); 366 | mixedCount++; 367 | 368 | ele.children = [ 369 | cv[0]>0? {x:2*x+1, y:2*y+1}: null, 370 | cv[1]>0? {x:2*x+1, y:2*y}: null, 371 | cv[2]>0? {x:2*x, y:2*y}: null, 372 | cv[3]>0? {x:2*x, y:2*y+1}: null 373 | ]; 374 | }else if(s[1]==4){//otherwise leaf, directly merge 375 | ele = new QTreeNode(elepos, NodeType.leaf); 376 | ele.children = [ 377 | {x:2*(x+1), y:2*(y+1)}, 378 | {x:2*(x+1), y:2*y}, 379 | {x:2*x, y:2*y}, 380 | {x:2*x, y:2*(y+1)} 381 | ]; 382 | } 383 | } 384 | pre[y][x] = ele; 385 | } 386 | } 387 | // bottom-up 388 | while((len>>=1)>1){ 389 | var step = (1<=0 && pp.y>=0 && pp.x<2*len && pp.y<2*len){ 428 | var type = pre[pp.y][pp.x].type; 429 | if(type != NodeType.leaf){// not leaf 1 or empty 2 430 | //console.log('not merge', type, pp); 431 | merge = false; 432 | break; 433 | } 434 | } 435 | } 436 | if(merge){ 437 | ele.children = [[1,1],[1,0],[0,0],[0,1]].map(function(p, i){ 438 | return {x:step*2*(x+p[0]), y:step*2*(y+p[1])}; 439 | }); 440 | ele.type = NodeType.leaf; 441 | }else{ 442 | ele.type = NodeType.inner; 443 | } 444 | } 445 | } 446 | 447 | } 448 | 449 | pre = next; 450 | } 451 | // root, now pre is 2x2 452 | var root = new QTreeNode({x:1<<(maxLevel-1), y:1<<(maxLevel-1)}, NodeType.inner); 453 | root.children[0] = pre[1][1]; 454 | root.children[1] = pre[0][1]; 455 | root.children[2] = pre[0][0]; 456 | root.children[3] = pre[1][0]; 457 | root.children.forEach(function(e,i,arr){ 458 | if(e.type == NodeType.empty){ 459 | arr[i] = null; 460 | } 461 | }); 462 | 463 | return root; 464 | } 465 | 466 | 467 | 468 | // ========== 469 | 470 | /* 471 | * maps from dense vector to sparse representation 472 | */ 473 | function DoubleMap(N){ 474 | this.forward = new Array(N);// dense dim 475 | for(var i=0;i=height) return true; 589 | if(mask[p.y+1][p.x+1] == 0){ 590 | return true; 591 | }else{ 592 | console.log('left', p.x, p.y, mask[p.y+1][p.x+1]); 593 | //console.log('left', p, tree.search(p)); 594 | return false; 595 | } 596 | } 597 | for(var id in rev){ 598 | var ws = rev[id]; 599 | 600 | 601 | if(ws.length == 0){ 602 | if(!checkEmpty(id)) { 603 | flag = false; 604 | errCount++; 605 | } 606 | }else if(ws.length > 4){ 607 | console.warn('exceed', convert_back(id), ws); 608 | }else{ 609 | var s = {x:0,y:0, i:0}; 610 | ws.forEach(function(w){ 611 | var p = convert_back(w[0]); 612 | s.x += p.x * w[1]; 613 | s.y += p.y * w[1]; 614 | s.i += w[1]; 615 | if(w[1]<0 || w[1]>1) console.warn('invalid', w[1]); 616 | }); 617 | var p = convert_back(id); 618 | if(Math.sign(s.x - p.x) != 0 || Math.sign(s.y - p.y) != 0 || Math.sign(s.i - 1) != 0){ 619 | flag = false; 620 | errCount++; 621 | console.log(s, p, ws); 622 | } 623 | 624 | } 625 | 626 | } 627 | console.warn('errCount', errCount); 628 | return flag; 629 | } 630 | // construct quadratree to achieve sparseness 631 | 632 | function reduceMap(mask){ 633 | var tree = QTree(mask); 634 | var mm = [[],[],[]]; 635 | var height = mask.length - 2, width = mask[0].length - 2; 636 | for(var j=1;j<=height;j++){ 637 | for(var i=1;i<=width;i++){ 638 | var val = mask[j][i]; 639 | if(val>2 || val<0){ 640 | console.warn('wrong mask', val, i, j); 641 | } 642 | mm[val].push(width*(j-1)+(i-1)); 643 | } 644 | } 645 | console.log('outter vec', mm[0].length); 646 | console.log('boundary', mm[2].length); 647 | var inner_count = 0, bcount = 0; 648 | var dmap = new DoubleMap(height*width); 649 | function convert(pos){ 650 | return pos.y * width + pos.x; 651 | } 652 | var bound = {}; 653 | 654 | tree.traverse(function(arr){ 655 | if(Array.isArray(arr)){ 656 | var step = arr[0].x - arr[2].x; 657 | var a = convert(arr[2]), b = convert(arr[1]); 658 | var d = convert(arr[0]), c = convert(arr[3]); 659 | var i_vec = [d, b, a, c]; 660 | if(step == 1){ 661 | // mixed type 662 | console.log('step 1, mixed'); 663 | arr.forEach(function(p, i){ 664 | var val = mask[p.y+1][p.x+1]; 665 | if (val >= 2){ 666 | bound[i_vec[i]] = 1; 667 | dmap.add(i_vec[i], i_vec[i], 1); 668 | bcount++; 669 | }else if(val>0){ 670 | dmap.add(i_vec[i], i_vec[i], 1); 671 | inner_count++; 672 | } 673 | }); 674 | return; 675 | } 676 | 677 | inner_count += step*step; 678 | 679 | // step >= 2, must be leaf 680 | var index = a; 681 | if(mask[arr[2].y+1][arr[2].x+1] != 1){ 682 | console.warn('err leaf', arr[2], step); 683 | } 684 | dmap.add(index, index, 1); 685 | for(var j=1;j 0){ 709 | var index = convert(arr); 710 | dmap.add(index, index, 1); 711 | if(dmap.forward[index].length>1){ 712 | console.warn('duplicate', arr, dmap.forward[index]); 713 | } 714 | inner_count++; 715 | if (val >= 2){ 716 | bound[index] = 1; 717 | bcount++; 718 | } 719 | } 720 | 721 | } 722 | }); 723 | 724 | 725 | 726 | 727 | 728 | return {dmap:dmap, tree:tree, bmap:bound, hash:convert}; 729 | } 730 | 731 | function vecType(mask, rev){ 732 | var width = mask[0].length - 2, height = mask.length - 2; 733 | var ret = {}; 734 | var b_len = 0, X_len = 0, Y_len = 0; 735 | function convert(x,y){ 736 | return x + y * width; 737 | } 738 | for(var y=0;y 0){ 742 | var id = convert(x, y); 743 | if(val >= 2){ 744 | ret[id] = 2; 745 | b_len++; 746 | }else{ 747 | //check whether it is interpolation point 748 | ret[id] = 0; 749 | X_len++; 750 | if(rev[id] != null){ 751 | ret[id] = 1; 752 | Y_len++; 753 | } 754 | } 755 | } 756 | } 757 | } 758 | console.log('vec len: ', b_len, X_len, Y_len); 759 | return ret; 760 | } 761 | function getTypeVec(vmap){ 762 | var bvec = [], Xvec = [], Yvec = []; 763 | var i=0, vecId = {}; 764 | for(var id in vmap){ 765 | vecId[id] = i++; 766 | switch(vmap[id]){ 767 | case 2: 768 | bvec.push(id); 769 | break; 770 | case 1: 771 | Yvec.push(id); 772 | // no break! 773 | case 0: 774 | Xvec.push(id); 775 | break; 776 | default: 777 | console.warn('wrong', vmap); 778 | } 779 | } 780 | console.log('summary:', bvec.length, Xvec.length, Yvec.length, i); 781 | return {b:bvec, X:Xvec, Y:Yvec, vid:vecId}; 782 | } 783 | 784 | 785 | 786 | function Laplacian(mask, dmap){ 787 | this.height = mask.length - 2; 788 | this.width = mask[0].length - 2; 789 | this.mask = mask; 790 | this.vmap = vecType(mask, dmap.reverse); 791 | this.dmap = dmap; 792 | } 793 | 794 | Laplacian.prototype.convert = function(x, y){ 795 | if(x<0 || y<0) { 796 | console.warn('negative coordinates'); 797 | return null; 798 | } 799 | return y * this.width + x; 800 | } 801 | Laplacian.prototype.convert_back = function(i){ 802 | return {x: i%this.width, y: Math.floor(i/this.width)}; 803 | } 804 | 805 | function visitNeighbor4(pos0, func, _this){ 806 | neighbor4.map(function(p, i){ 807 | var pos = {x:pos0.x+p[0], y:pos0.y+p[1]}; 808 | if(_this != null){ 809 | return func.call(_this, pos); 810 | }else{ 811 | return func(pos); 812 | } 813 | 814 | 815 | }); 816 | } 817 | 818 | Laplacian.prototype.mul = function(sid, sval, tid){ 819 | // by spreading from source 820 | var tmp = {}; 821 | for(var i=0;i= 0; i--) { 876 | var ss = svals[i]; 877 | var id = ss[0], sval = ss[1]; 878 | var pos0 = this.convert_back(id); 879 | if (tmp[id] == null) { 880 | tmp[id] = 0; 881 | } 882 | tmp[id] -= 4 * sval;// 883 | visitNeighbor4(pos, function (pos) { 884 | var new_id = this.convert(pos); 885 | if (new_id != null && this.vmap[new_id] != null) { 886 | if (tmp[new_id] == null) { 887 | tmp[new_id] = 0; 888 | } 889 | tmp[new_id] += sval;// 890 | } 891 | }, this); 892 | } 893 | 894 | if (tid == null) { 895 | // return zip vector 896 | var ret = []; 897 | for (var id in tmp) { 898 | var val = tmp[id]; 899 | if (val != 0) { 900 | ret.push([id, val]); 901 | } 902 | } 903 | return ret; 904 | }else if(Array.isArray(tid)){ 905 | // filtering 906 | var ret = []; 907 | tid.forEach(function (id) { 908 | var val = tmp[id]; 909 | if (val != null && val != 0) { 910 | ret.push([id, val]) 911 | } 912 | }); 913 | return ret; 914 | }else{ 915 | // multi filtering 916 | return Object.keys(tid).map(function(fid){ 917 | var ret = []; 918 | tid[fid].forEach(function (id) { 919 | var val = tmp[id]; 920 | if (val != null && val != 0) { 921 | ret.push([id, val]) 922 | } 923 | }); 924 | return ret; 925 | }); 926 | } 927 | }; 928 | 929 | 930 | /* 931 | * Conjugate GD 932 | */ 933 | 934 | function run_CGD(mask, dmap, bvals_array){ 935 | // TODO: optimize by saving intermediate result 936 | var lapl = new Laplacian(mask, dmap.reverse); 937 | var vecs = getTypeVec(lapl.vmap); 938 | var params = bvals_array.map(function(bvals){ 939 | return getGDParams(lapl, dmap, vecs, bvals); 940 | }); 941 | 942 | y0 = Array(vecs.Y.length).fill(0); 943 | var res = params.map(function(p, i){ 944 | return bvals_array[i].concat(conjGD(p.A, p.b, y0, 1e-4, 100)); 945 | }); 946 | 947 | return res; 948 | 949 | 950 | } 951 | 952 | function getGDParams(lapl, dmap, vecs, bvals){ 953 | // bvals is the id:val map of boundary 954 | var bval = vecs.b.map(function(id){ 955 | var val = bvals[id]; 956 | if(val==null){ 957 | console.warn('miss boundary value'); 958 | val = 0; 959 | } 960 | return val; 961 | }); 962 | // S_xb * b 963 | var b1 = dmap.rmul(bvals,vecs.X); 964 | // D_xx * S_xb *b 965 | var b2 = lapl.rmul(vecs.X,b1,vecs.X); 966 | // D^T_xx * b 967 | var b3 = lapl.reverse(vecs.b, bval, vecs.X); 968 | // b2 - b3 969 | var b4 = Vec.diff(b2, b3); 970 | 971 | // S^T_yx * b4 972 | var m = _.object(vecs.b, b4); 973 | var b5 = dmap.rmul(m,vecs.Y); 974 | 975 | // var b5; 976 | 977 | // D_xx * S_xy 978 | var S = lapl.rmul(dmap.reverse); 979 | var S2 = dmap.back(S,vecs.Y,vecs.Y,true); 980 | 981 | 982 | return {A:S2, b:b5}; 983 | 984 | // 985 | 986 | } 987 | 988 | 989 | function conjGD(A, b, x0, ep, Nmax){ 990 | var x = x0, r = b; 991 | var rho =l2norm(r); 992 | var thres = ep * Math.sqrt(rho); 993 | var p = 0, rho_pre = rho; 994 | 995 | for(var k=0;k=2, inner:1 35 | function findMask(boundary, boxP1, boxP2){ 36 | var ret = []; 37 | var width = boxP2.x - boxP1.x + 1; 38 | var height = boxP2.y - boxP1.y + 1; 39 | for(var i=0;i 0){ 68 | var pix = frontier.shift(); 69 | neighbor4.forEach(function(p){ 70 | spread(pix[0]+p[0], pix[1]+p[1]); 71 | }); 72 | } 73 | 74 | // remove spurious edge, order boundary 75 | var reducedBoundary = []; 76 | boundary.forEach(function(e){ 77 | var x = e.x-boxP1.x+1; 78 | var y = e.y-boxP1.y+1; 79 | var s = [0,0,0]; 80 | neighbor4.forEach(function(p){ 81 | var val = ret[y+p[1]][x+p[0]]; 82 | s[val] += 1; 83 | }); 84 | if(s[0] >= 2){ 85 | if(s[1]>0){ 86 | reducedBoundary.push({x:x,y:y}); 87 | }else{ 88 | ret[y][x] = 0; 89 | } 90 | }else if(s[0] > 0){ 91 | reducedBoundary.push({x:x,y:y}); 92 | }else{ 93 | neighbor4p.forEach(function(p){ 94 | var val = ret[y+p[1]][x+p[0]]; 95 | s[val] += 1; 96 | }); 97 | if(s[0]>1){ 98 | reducedBoundary.push({x:x,y:y}); 99 | }else{ 100 | ret[y][x] = 1; 101 | 102 | } 103 | } 104 | 105 | }); 106 | for(var i=1;i=width || r.y<0 || r.y>=height){ 151 | return false; 152 | }else if(mask[r.y+1][r.x+1]>0){ 153 | console.log('hit'); 154 | return r; 155 | }else{ 156 | return false; 157 | } 158 | } 159 | function pressing(e){ 160 | if(pressed){ 161 | return false; 162 | }else if(enabled){ 163 | // decide if within mask area 164 | q = hit(e.offsetX, e.offsetY); 165 | if(q == false){ 166 | q = {}; 167 | return false; 168 | }else{ 169 | hitPos = {x: e.offsetX, y: e.offsetY}; 170 | lastPos = {x: e.offsetX, y: e.offsetY}; 171 | pressed = true; 172 | return true; 173 | } 174 | return true; 175 | }else{ 176 | return false; 177 | } 178 | } 179 | function moving(e){ 180 | if(!pressed){ 181 | return false; 182 | } 183 | var dx = e.offsetX - lastPos.x; 184 | var dy = e.offsetY - lastPos.y; 185 | if(dx == 0 && dy == 0){ 186 | return false; // no change 187 | }else{ 188 | lastPos.x = e.offsetX; 189 | lastPos.y = e.offsetY; 190 | g.x = lastPos.x - q.x + c.x; 191 | g.y = lastPos.y - q.y + c.y; 192 | return { 193 | x: lastPos.x - q.x, 194 | y: lastPos.y - q.y 195 | } 196 | } 197 | } 198 | function updateMask(_mask, parentPos){ 199 | if(typeof _mask != 'undefined'){ 200 | mask = _mask.map(function(e){return e.slice();}); 201 | enabled = true; 202 | width = mask[0].length - 2; 203 | height = mask.length - 2; 204 | // mask center 205 | c = {x: (width>>1),y: (height>>1)}; 206 | // global center 207 | g = {x: parentPos.x + c.x, y: parentPos.y + c.y}; 208 | //console.log('mask updated'); 209 | }else{ 210 | mask = null; 211 | enabled = false; 212 | width = null; 213 | height = null; 214 | c = {}; 215 | g = {}; 216 | } 217 | } 218 | 219 | function release(e){ 220 | if(!pressed){ 221 | return false; 222 | } 223 | pressed = false; 224 | hitPos = {}; 225 | lastPos = {}; 226 | q = {}; 227 | 228 | } 229 | 230 | return { 231 | pressing: pressing, 232 | release: release, 233 | moving: moving, 234 | updateMask: updateMask 235 | } 236 | 237 | } 238 | 239 | 240 | 241 | function selector(){ 242 | var pressed = false; 243 | var boundary = []; 244 | var boxP1 = {}; 245 | var boxP2 = {}; 246 | var lastPos = {}; 247 | 248 | function updateBoundary(x,y){ 249 | var dx = x - lastPos.x; 250 | var dy = y - lastPos.y; 251 | if(dx == 0 && dy == 0){ 252 | return; 253 | } 254 | 255 | // connect 256 | if(dx*dx > 1 && dy*dy<=dx*dx){ 257 | var step = dx>0? 1: -1; 258 | var r = dy/dx; 259 | 260 | for(var j=1;j 1 && dy*dy>=dx*dx){ 264 | var step = dy>0? 1: -1; 265 | var r = dx/dy; 266 | for(var j=1;j boxP2.x){ 277 | boxP2.x = x; 278 | } 279 | if(y < boxP1.y){ 280 | boxP1.y = y; 281 | }else if(y > boxP2.y){ 282 | boxP2.y = y; 283 | } 284 | 285 | } 286 | 287 | function pressing(e){ 288 | if(pressed){ 289 | return false; 290 | } 291 | boundary = []; 292 | lastPos = {x:e.offsetX, y:e.offsetY}; 293 | boundary.push(lastPos); 294 | boxP1 = {x:e.offsetX, y:e.offsetY}; 295 | boxP2 = {x:e.offsetX, y:e.offsetY}; 296 | pressed = true; 297 | return true; 298 | } 299 | 300 | function release(e){ 301 | if(!pressed){ 302 | return false; 303 | } 304 | updateBoundary(boundary[0].x, boundary[0].y); 305 | pressed = false; 306 | 307 | return { 308 | boundary: boundary, 309 | boxP1: boxP1, 310 | boxP2: boxP2 311 | } 312 | 313 | } 314 | return { 315 | pressing: pressing, 316 | pressed: function(){return pressed}, 317 | release: release, 318 | updateBoundary: updateBoundary 319 | } 320 | } 321 | 322 | 323 | // handle mouse event 324 | function registerEvents(src,tgt){ 325 | function register(el, evts){ 326 | for(var type in evts){ 327 | el.addEventListener(type, evts[type]); 328 | } 329 | } 330 | 331 | 332 | var sel = selector(); 333 | var drag = draggor(); 334 | var fg_img, bg_img;// the two image parts we concerned 335 | var img = null; // apply poisson on fg_img; 336 | var cloner; 337 | var pre_pos = null; 338 | 339 | var ctx = mvc.src.ctx; 340 | ctx.strokeStyle = 'red'; 341 | 342 | 343 | // target image mouse control 344 | register(mvc.tgt.aid,{ 345 | 'mousedown': function(e){ 346 | if(drag.pressing(e)){ 347 | // make changes 348 | } 349 | }, 350 | 'mousemove': function(e){ 351 | var pos = drag.moving(e); 352 | if(pos == false){ 353 | // no changes 354 | }else{ 355 | if(img == null){ 356 | return; 357 | } 358 | mvc.tgt.ctx.clearRect(0, 0, mvc.tgt.fg.width, mvc.tgt.fg.height); 359 | // get new background and pass back difference on the boundary 360 | bg_img = mvc.tgt.bg.getContext('2d').getImageData(pos.x, pos.y, img.width, img.height); 361 | img = ctx.createImageData(bg_img.width, bg_img.height) 362 | img = cloner.paintTgt(img, bg_img); 363 | mvc.tgt.ctx.putImageData(img, pos.x, pos.y); 364 | 365 | } 366 | }, 367 | 'mouseup': function(e){ 368 | drag.release(e); 369 | 370 | }, 371 | 'mouseout': function(e){ 372 | drag.release(e); 373 | } 374 | }); 375 | 376 | // source image mouse control 377 | register(mvc.src.aid, { 378 | 'mousedown': function(e){ 379 | if(sel.pressing(e)){ 380 | ctx.beginPath(); 381 | ctx.moveTo(e.offsetX, e.offsetY); 382 | ctx.clearRect(0,0, mvc.src.fg.width, mvc.src.fg.height); 383 | } 384 | 385 | }, 386 | 'mousemove': function(e){ 387 | if(sel.pressed()){ 388 | sel.updateBoundary(e.offsetX, e.offsetY); 389 | 390 | ctx.lineTo(e.offsetX, e.offsetY); 391 | ctx.stroke(); 392 | 393 | } 394 | }, 395 | 'mouseup': release, 396 | 'mouseout': release, 397 | 'dblclick': function(e){ 398 | //console.log(e.offsetX, e.offsetY); 399 | ctx.clearRect(0, 0, mvc.src.fg.width, mvc.src.fg.height); 400 | } 401 | }); 402 | 403 | 404 | // at source image 405 | function release(e){ 406 | var res = sel.release(e); 407 | if(res == false){ 408 | return; 409 | } 410 | ctx.lineTo(res.boundary[0].x, res.boundary[0].y); 411 | ctx.stroke(); 412 | ctx.closePath(); 413 | 414 | var width = res.boxP2.x - res.boxP1.x + 1; 415 | var height = res.boxP2.y - res.boxP1.y + 1; 416 | var data = findMask(res.boundary, res.boxP1, res.boxP2); 417 | drag.updateMask(data.mask, res.boxP1);// for mouse drag hit 418 | fg_img = mvc.src.bg.getContext('2d').getImageData(res.boxP1.x, res.boxP1.y, width, height); 419 | cloner = new ImageCloner(data.mask, data.boundary, fg_img); 420 | 421 | 422 | // triangulation 423 | 424 | 425 | 426 | // painting source image 427 | var meshImg = mvc.src.ctx.createImageData(width+1, height+1); 428 | var src_paint = cloner.paintSrc(meshImg); 429 | mvc.src.ctx.clearRect(res.boxP1.x-1,res.boxP1.y-1, width+2,height+2); 430 | mvc.src.ctx.putImageData(src_paint.img, res.boxP1.x, res.boxP1.y); 431 | // post paint 432 | src_paint.post_paint(ctx, res.boxP1); 433 | 434 | 435 | 436 | 437 | 438 | 439 | // painting target image 440 | 441 | bg_img = mvc.tgt.bg.getContext('2d').getImageData(res.boxP1.x, res.boxP1.y, width, height); 442 | img = mvc.tgt.ctx.createImageData(width+1, height+1) 443 | img = cloner.paintTgt(img, bg_img); 444 | mvc.tgt.ctx.clearRect(0,0, mvc.tgt.fg.width, mvc.tgt.fg.height); 445 | mvc.tgt.ctx.putImageData(img, res.boxP1.x, res.boxP1.y); 446 | 447 | } 448 | // select 449 | document.getElementById('mode').addEventListener('change', function(e){ 450 | selectedMode = Number.parseInt(e.target.selectedIndex); 451 | //mvc.tgt.ctx.clearRect(0, 0, mvc.tgt.fg.width, mvc.tgt.fg.height); 452 | // get new background and pass back difference on the boundary 453 | //bg_img = mvc.tgt.bg.getContext('2d').getImageData(pos.x, pos.y, img.width, img.height); 454 | //img = ctx.createImageData(bg_img.width, bg_img.height) 455 | //img = cloner.paintTgt(img, bg_img); 456 | //mvc.tgt.ctx.putImageData(img, pos.x, pos.y); 457 | }); 458 | 459 | } 460 | 461 | 462 | -------------------------------------------------------------------------------- /underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.8.3 2 | // http://underscorejs.org 3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); 6 | //# sourceMappingURL=underscore-min.map --------------------------------------------------------------------------------