Comparing methods for warping a quadrilateral based on a beginning and ending set of corners. Based on Projective Mappings for Image Warping and Four Corner Image Warping.
See also: Warp speed
Comparing methods for warping a quadrilateral based on a beginning and ending set of corners. Based on Projective Mappings for Image Warping and Four Corner Image Warping.
See also: Warp speed
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<style> | |
.quad { | |
stroke-linejoin: round; | |
stroke-width: 3px; | |
stroke: #000; | |
fill: #fff; | |
} | |
.grid { | |
stroke: #0eb8ba; | |
stroke-width: 1px; | |
fill: none; | |
} | |
text { | |
fill: #000; | |
text-transform: uppercase; | |
text-anchor: middle; | |
font: 600 36px sans-serif; | |
} | |
</style> | |
</head> | |
<body> | |
<div></div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> | |
<script src="warper.js"></script> | |
<script> | |
var width = 480, | |
height = 480; | |
var svg = d3.select("body").append("svg") | |
.attr("width", 960) | |
.attr("height", 500); | |
var quads = svg.selectAll("g") | |
.data(["bilinear", "projective"]) | |
.enter() | |
.append("g") | |
.attr("transform", function(d, i){ | |
return "translate(" + (i * width) + " 10)"; | |
}); | |
quads.append("text") | |
.text(function(d){ | |
return d; | |
}) | |
.attr("x", width / 2) | |
.attr("y", height) | |
.attr("dy", "-0.5em"); | |
var square = getCorners(true), | |
grid = getGrid(12); | |
quads.append("path") | |
.datum(square) | |
.attr("class", "quad") | |
.call(updateQuad); | |
quads.append("path") | |
.datum(grid) | |
.attr("class", "grid") | |
.call(updateGrid); | |
warp(square); | |
function warp(current) { | |
var next = current === square ? getCorners() : square; | |
quads.each(function(type, i){ | |
var lines = grid, | |
fn = warper[type](current, next); | |
if (current === square) { | |
lines = grid.map(function(linestring){ | |
return linestring.map(fn); | |
}); | |
} | |
var quad = d3.select(this); | |
quad.select(".grid") | |
.datum(lines) | |
.transition() | |
.delay(500) | |
.duration(600) | |
.call(updateGrid); | |
quad.select(".quad") | |
.datum(next) | |
.transition() | |
.delay(500) | |
.duration(600) | |
.call(updateQuad) | |
.each("end",i && warp); | |
}); | |
} | |
function updateQuad(sel) { | |
sel.attr("d",function(d){ | |
return "M" + d.join("L") + "Z"; | |
}); | |
} | |
function updateGrid(sel) { | |
sel.attr("d",function(d){ | |
return d.map(function(linestring){ | |
return "M" + linestring.join("L"); | |
}); | |
}); | |
} | |
function getGrid(dim) { | |
var cols = d3.range(dim).map(function(i){ | |
var x = width / 4 + (width / 2) * (1 + i) / (dim + 1); | |
return d3.range(100).map(function(j){ | |
return [x, height / 4 + (j / 99) * height / 2]; | |
}) | |
}); | |
var rows = cols.map(function(col){ | |
return col.map(function(point){ | |
return [point[1], point[0]]; | |
}); | |
}); | |
return rows.concat(cols); | |
} | |
function getCorners(sq) { | |
return d3.range(4).map(function(i){ | |
return [ | |
((i % 3 ? 1 : 0) + (sq ? 0.5 : 0.1 + Math.random() * 0.8)) * width / 2, | |
(Math.floor(i / 2) + (sq ? 0.5 : 0.1 + Math.random() * 0.8)) * height / 2 | |
]; | |
}); | |
} | |
</script> | |
</body> | |
</html> |
var warper = {}; | |
warper.bilinear = function(start,end) { | |
var u0 = start[0][0], | |
v0 = start[0][1], | |
u1 = start[1][0], | |
v1 = start[1][1], | |
u2 = start[2][0], | |
v2 = start[2][1], | |
u3 = start[3][0], | |
v3 = start[3][1], | |
x0 = end[0][0], | |
y0 = end[0][1], | |
x1 = end[1][0], | |
y1 = end[1][1], | |
x2 = end[2][0], | |
y2 = end[2][1], | |
x3 = end[3][0], | |
y3 = end[3][1]; | |
var square = [ | |
[1,u0,v0,u0 * v0,0,0,0,0], | |
[1,u1,v1,u1 * v1,0,0,0,0], | |
[1,u2,v2,u2 * v2,0,0,0,0], | |
[1,u3,v3,u3 * v3,0,0,0,0], | |
[0,0,0,0,1,u0,v0,u0 * v0], | |
[0,0,0,0,1,u1,v1,u1 * v1], | |
[0,0,0,0,1,u2,v2,u2 * v2], | |
[0,0,0,0,1,u3,v3,u3 * v3] | |
]; | |
var inverted = invert(square); | |
var s = multiply(inverted,[x0,x1,x2,x3,y0,y1,y2,y3]); | |
return function(p) { | |
return [ | |
s[0] + s[1] * p[0] + s[2] * p[1] + s[3] * p[0] * p[1], | |
s[4] + s[5] * p[0] + s[6] * p[1] + s[7] * p[0] * p[1], | |
]; | |
}; | |
}; | |
warper.projective = function(start,end) { | |
var u0 = start[0][0], | |
v0 = start[0][1], | |
u1 = start[1][0], | |
v1 = start[1][1], | |
u2 = start[2][0], | |
v2 = start[2][1], | |
u3 = start[3][0], | |
v3 = start[3][1], | |
x0 = end[0][0], | |
y0 = end[0][1], | |
x1 = end[1][0], | |
y1 = end[1][1], | |
x2 = end[2][0], | |
y2 = end[2][1], | |
x3 = end[3][0], | |
y3 = end[3][1]; | |
var square = [ | |
[u0,v0,1,0,0,0,-u0 * x0,-v0 * x0], | |
[u1,v1,1,0,0,0,-u1 * x1,-v1 * x1], | |
[u2,v2,1,0,0,0,-u2 * x2,-v2 * x2], | |
[u3,v3,1,0,0,0,-u3 * x3,-v3 * x3], | |
[0,0,0,u0,v0,1,-u0 * y0,-v0 * y0], | |
[0,0,0,u1,v1,1,-u1 * y1,-v1 * y1], | |
[0,0,0,u2,v2,1,-u2 * y2,-v2 * y2], | |
[0,0,0,u3,v3,1,-u3 * y3,-v3 * y3] | |
]; | |
var inverted = invert(square); | |
var s = multiply(inverted,[x0,x1,x2,x3,y0,y1,y2,y3]); | |
return function(p) { | |
return [ | |
(s[0] * p[0] + s[1] * p[1] + s[2]) / (s[6] * p[0] + s[7] * p[1] + 1), | |
(s[3] * p[0] + s[4] * p[1] + s[5]) / (s[6] * p[0] + s[7] * p[1] + 1) | |
]; | |
}; | |
} | |
function multiply(matrix,vector) { | |
return matrix.map(function(row){ | |
var sum = 0; | |
row.forEach(function(c,i){ | |
sum += c * vector[i]; | |
}); | |
return sum; | |
}); | |
} | |
function invert(matrix) { | |
var size = matrix.length, | |
base, | |
swap, | |
augmented; | |
// Augment w/ identity matrix | |
augmented = matrix.map(function(row,i){ | |
return row.slice(0).concat(row.slice(0).map(function(d,j){ | |
return j === i ? 1 : 0; | |
})); | |
}); | |
// Process each row | |
for (var r = 0; r < size; r++) { | |
base = augmented[r][r]; | |
// Zero on diagonal, swap with a lower row | |
if (!base) { | |
for (var rr = r + 1; rr < size; rr++) { | |
if (augmented[rr][r]) { | |
// swap | |
swap = augmented[rr]; | |
augmented[rr] = augmented[r]; | |
augmented[r] = swap; | |
base = augmented[r][r]; | |
break; | |
} | |
} | |
if (!base) { | |
throw new Error("Not invertable :("); | |
} | |
} | |
// 1 on the diagonal | |
for (var c = 0; c < size * 2; c++) { | |
augmented[r][c] = augmented[r][c] / base; | |
} | |
// Zeroes elsewhere | |
for (var q = 0; q < size; q++) { | |
if (q !== r) { | |
base = augmented[q][r]; | |
for (var p = 0; p < size * 2; p++) { | |
augmented[q][p] -= base * augmented[r][p]; | |
} | |
} | |
} | |
} | |
return augmented.map(function(row){ | |
return row.slice(size); | |
}); | |
} |