Comparing version 0.3.1 to 0.4.0
@@ -0,1 +1,6 @@ | ||
/* | ||
A demonstration app for nudged | ||
*/ | ||
var Hammer = require('hammerjs'); | ||
@@ -13,3 +18,3 @@ var loadimages = require('loadimages'); | ||
var drawPoint = function (ctx, px, py, label, color, ghost) { | ||
var radius = 10; | ||
var radius = 14; | ||
ctx.font = '14px bold serif'; | ||
@@ -45,15 +50,35 @@ ctx.fillStyle = color; | ||
// Improve usability by tweaking the recognizers | ||
hammerRange.get('pan').set({ threshold: 5 }); | ||
hammerDomain.get('swipe').set({ enable: false }); | ||
hammerDomain.get('pan').set({ | ||
threshold: 5, | ||
direction: Hammer.DIRECTION_ALL | ||
}); | ||
hammerRange.get('swipe').set({ enable: false }); | ||
hammerRange.get('pan').set({ | ||
threshold: 5, | ||
direction: Hammer.DIRECTION_ALL | ||
}); | ||
// Input; point creation | ||
hammerDomain.on('tap', function (ev) { | ||
// Transform to canvas coordinates | ||
var cr = canvasDomain.getBoundingClientRect(); | ||
var x = ev.center.x - cr.left; | ||
var y = ev.center.y - cr.top; | ||
(function defineHowToCreateDomainPoints() { | ||
// Input; point creation | ||
hammerDomain.on('tap', function (ev) { | ||
// Transform to canvas coordinates | ||
var cr = canvasDomain.getBoundingClientRect(); | ||
var x = ev.center.x - cr.left; | ||
var y = ev.center.y - cr.top; | ||
model.addToDomain(x, y); | ||
}); | ||
model.addToDomain(x, y); | ||
}); | ||
(function defineHowToPanDomainPoints() { | ||
hammerDomain.on('press', function (ev) { | ||
// Transform to canvas coordinates | ||
var cr = canvasDomain.getBoundingClientRect(); | ||
var x = ev.center.x - cr.left; | ||
var y = ev.center.y - cr.top; | ||
model.addFixedPoint(x, y); | ||
}); | ||
}()); | ||
(function defineHowToPanDomainAndFixedPoints() { | ||
var movingPoint = null; | ||
@@ -69,3 +94,3 @@ var x0 = 0; | ||
var np = model.findNearestDomainPoint(x, y); | ||
var np = model.findNearestDomainOrFixedPoint(x, y); | ||
if (np !== null) { | ||
@@ -128,2 +153,3 @@ // Found | ||
var ran = model.getRange(); | ||
var piv = model.getFixedPoint(); | ||
var tra = model.getTransform(); | ||
@@ -136,5 +162,6 @@ var invtra = tra.getInverse(); | ||
// Domain image: always still | ||
ctxDomain.drawImage(img, dx, dy, iw, ih); | ||
// Apply transform | ||
// Range image: apply transform to it | ||
ctxRange.setTransform(tra.s, tra.r, -tra.r, tra.s, tra.tx, tra.ty); | ||
@@ -144,3 +171,8 @@ ctxRange.drawImage(img, dx, dy, iw, ih); | ||
// Draw points to canvases | ||
// Draw points to the canvases | ||
if (piv !== null) { | ||
drawPoint(ctxDomain, piv.x, piv.y, piv.label, 'black', false); | ||
drawPoint(ctxRange, piv.x, piv.y, piv.label, 'black', true); | ||
} | ||
dom.forEach(function (dp) { | ||
@@ -167,5 +199,10 @@ // Transform the domain points to the range | ||
var html = 'var domain = ' + JSON.stringify(domparam) + ';<br>' + | ||
'var range = ' + JSON.stringify(ranparam) + ';<br>' + | ||
'var trans = nudged.estimate(domain, range);<br>' + | ||
'trans.getMatrix();<br>' + | ||
'var range = ' + JSON.stringify(ranparam) + ';<br>'; | ||
if (piv !== null) { | ||
html += 'var pivot = ' + JSON.stringify(pointToArray(piv)) + ';<br>'; | ||
html += 'var trans = nudged.estimateFixed(domain, range, pivot);<br>'; | ||
} else { | ||
html += 'var trans = nudged.estimate(domain, range);<br>'; | ||
} | ||
html += 'trans.getMatrix();<br>' + | ||
'-> [[' + m[0][0] + ', ' + m[0][1] + ', ' + m[0][2] + '],<br>' + | ||
@@ -172,0 +209,0 @@ ' [' + m[1][0] + ', ' + m[1][1] + ', ' + m[1][2] + '],<br>' + |
@@ -11,2 +11,6 @@ var Emitter = require('component-emitter'); | ||
// If fixed point is set, pivot !== null | ||
// If so, we use fixed point transformation. | ||
this.pivot = null; | ||
// Init with identity transform | ||
@@ -18,3 +22,9 @@ this.transform = nudged.estimate([], []); | ||
var ran = this.range.map(function (p) { return [p.x, p.y]; }); | ||
this.transform = nudged.estimate(dom, ran); | ||
var piv; | ||
if (this.pivot === null) { | ||
this.transform = nudged.estimate(dom, ran); | ||
} else { | ||
piv = [this.pivot.x, this.pivot.y]; | ||
this.transform = nudged.estimateFixed(dom, ran, piv); | ||
} | ||
}; | ||
@@ -45,2 +55,14 @@ | ||
this.addFixedPoint = function (x, y) { | ||
this.pivot = new Point(x, y, 'X'); | ||
var model = this; | ||
this.pivot.on('update', function () { | ||
model._updateTransform(); | ||
model.emit('update'); | ||
}); | ||
this._updateTransform(); | ||
this.emit('update'); | ||
}; | ||
this._findNearestPoint = function (list, x, y) { | ||
@@ -72,2 +94,12 @@ // Time complexity O(n) ≈ sloooow | ||
this.findNearestDomainOrFixedPoint = function (x, y) { | ||
var points; | ||
if (this.pivot === null) { | ||
points = this.domain; | ||
} else { | ||
points = this.domain.concat([this.pivot]); | ||
} | ||
return this._findNearestPoint(points, x, y); | ||
}; | ||
this.getDomain = function () { | ||
@@ -81,2 +113,6 @@ return this.domain; | ||
this.getFixedPoint = function () { | ||
return this.pivot; | ||
}; | ||
this.getTransform = function () { | ||
@@ -83,0 +119,0 @@ return this.transform; |
@@ -25,11 +25,2 @@ var Transform = require('./Transform'); | ||
// If length is one, the denominator becomes zero and estimates can not be | ||
// computed. However, for this special case we can choose the translation | ||
// be the best quess. | ||
if (N === 1) { | ||
tx = Y[0][0] - X[0][0]; | ||
ty = Y[0][1] - X[0][1]; | ||
return new Transform(1, 0, tx, ty); | ||
} // else | ||
var i, a, b, c, d; | ||
@@ -65,3 +56,3 @@ var a1 = 0; | ||
// It is zero iff X[i] = X[j] for every i and j in [0, n). | ||
// In other words, iff all the domain points are the same. | ||
// In other words, iff all the domain points are the same or there is only one domain point. | ||
var den = N * a2 + N * b2 - a1 * a1 - b1 * b1; | ||
@@ -68,0 +59,0 @@ |
@@ -1,2 +0,2 @@ | ||
var estimate = require('./estimate'); | ||
var Transform = require('./Transform'); | ||
@@ -17,18 +17,55 @@ module.exports = function estimateFixed(domain, range, pivot) { | ||
// | ||
var X, Y, px, py, N, X_hat, Y_hat, i; | ||
var X, Y, N, s, r, tx, ty; | ||
// Alias | ||
X = domain; | ||
Y = range; | ||
// Allow arrays of different length but | ||
// ignore the extra points. | ||
N = Math.min(X.length, Y.length); | ||
X_hat = []; | ||
Y_hat = []; | ||
px = pivot[0]; | ||
py = pivot[1]; | ||
var v = pivot[0]; | ||
var w = pivot[1]; | ||
var i, a, b, c, d; | ||
var a2, b2; | ||
a2 = b2 = 0; | ||
var ac, bd, bc, ad; | ||
ac = bd = bc = ad = 0; | ||
for (i = 0; i < N; i += 1) { | ||
X_hat.push([X[i][0] - px, X[i][1] - py]); | ||
Y_hat.push([Y[i][0] - px, Y[i][1] - py]); | ||
a = X[i][0] - v; | ||
b = X[i][1] - w; | ||
c = Y[i][0] - v; | ||
d = Y[i][1] - w; | ||
a2 += a * a; | ||
b2 += b * b; | ||
ac += a * c; | ||
bd += b * d; | ||
bc += b * c; | ||
ad += a * d; | ||
} | ||
return estimate(X_hat, Y_hat); | ||
// Denominator = determinant. | ||
// It becomes zero iff N = 0 or X[i] = [v, w] for every i in [0, n). | ||
// In other words, iff all the domain points are under the fixed point or | ||
// there is no domain points. | ||
var den = a2 + b2; | ||
var eps = 0.00000001; | ||
if (Math.abs(den) < eps) { | ||
// The domain points are under the pivot or there is no domain points. | ||
// We assume identity transform be the simplest guess. It keeps | ||
// the domain points under the pivot if there is some. | ||
return new Transform(1, 0, 0, 0); | ||
} | ||
// Estimators | ||
s = (ac + bd) / den; | ||
r = (-bc + ad) / den; | ||
tx = w * r - v * s + v; | ||
ty = -v * r - w * s + w; | ||
return new Transform(s, r, tx, ty); | ||
}; |
@@ -1,1 +0,1 @@ | ||
module.exports = '0.3.1'; | ||
module.exports = '0.4.0'; |
{ | ||
"name": "nudged", | ||
"version": "0.3.1", | ||
"version": "0.4.0", | ||
"description": "Similarity transformation estimator", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -1,11 +0,15 @@ | ||
# nudged<sup>0.3.1</sup> | ||
# nudged<sup>0.4.0</sup> | ||
A JavaScript lib to estimate scale, rotation, and translation between two sets of 2D points. Applicable for example in cases where one wants to move objects by multiple fingers or where a large number of points from an eye tracker device are wanted to be corrected based on a few calibration points. In general, you can apply *nudged* in any situation where you want to move a number of points based on a few sample points. | ||
A JavaScript lib to estimate scale, rotation, and translation between two sets of 2D points. Applicable for example in cases where one wants to move objects by multiple fingers or where a large number of points from an eye tracker device are wanted to be corrected based on a few calibration points. In general, you can apply *nudged* in any situation where you want to move a number of points based on a few sample points and optionally a fixed pivot point. | ||
<img src="https://rawgit.com/axelpale/nudged/master/doc/nudged-logo.png" alt="Example transformation" width="300"/> | ||
Mathematically speaking, *nudged* is an optimal least squares estimator for [affine transformation matrices](https://en.wikipedia.org/wiki/Affine_transformation) with uniform scaling, rotation, and translation and without reflection or shearing. The estimation has time complexity of O(*n*) that consists of *6n+22* multiplications and *11n+19* additions, where *n* is the cardinality (size) of the point sets. In other words, *nudged* solves an affine 2D to 2D point set registration problem in linear time. | ||
Mathematically speaking, *nudged* is an optimal least squares estimator for [affine transformation matrices](https://en.wikipedia.org/wiki/Affine_transformation) with uniform scaling, rotation, and translation and without reflection or shearing. The estimation has time complexity of O(*n*) that consists of *6n+22* multiplications and *11n+19* additions, where *n* is the cardinality (size) of the point sets. Under the constraint of a fixed pivot point, the number of operations is even smaller. In other words, *nudged* solves an affine 2D to 2D point set registration problem in linear time. | ||
The development of *nudged* has been supported by [Infant Cognition Laboratory](http://www.uta.fi/med/icl/index.html) at [University of Tampere](http://www.uta.fi/en/) where it is used to correct eye tracking data. | ||
Available also [in Python](https://pypi.python.org/pypi/nudged). | ||
## Example app | ||
@@ -27,33 +31,42 @@ | ||
// Points before and after transformation i.e. the training data | ||
Let `domain` and `range` be point sets before and after transformation i.e. the training data: | ||
var domain = [[0,0], [2,0], [ 1,2]]; | ||
var range = [[1,1], [1,3], [-1,2]]; | ||
// Compute an optimal tranformation based on the points | ||
Compute an optimal transformation based on the points: | ||
var trans = nudged.estimate(domain, range); | ||
// Display the transformation matrix | ||
Alternatively, set a fixed pivot point that should not be altered in the transformation. You can think it as a pin or anchor: | ||
var pivot = [3,3]; | ||
var pivotedTrans = nudged.estimateFixed(domain, range, pivot); | ||
Examine the transformation matrix: | ||
trans.getMatrix() | ||
// [[0,-1, 1], | ||
// [1, 0, 1], | ||
// [0, 0, 1]] | ||
-> [[0,-1, 1], | ||
[1, 0, 1], | ||
[0, 0, 1]] | ||
trans.getRotation() | ||
-> 1.5707... = π / 2 | ||
trans.getScale() | ||
-> 1.0 | ||
trans.getTranslation() | ||
-> [1, 1] | ||
// Apply the transformation to other points | ||
Apply the transformation to other points: | ||
trans.transform([2,2]) | ||
// [-1,3] | ||
-> [-1,3] | ||
// Get rotation in radians | ||
trans.getRotation() | ||
// 1.5707... = π / 2 | ||
Inverse the transformation: | ||
// Get scaling multiplier | ||
trans.getScale() | ||
// 1.0 | ||
var inv = trans.getInverse(); | ||
inv.transform([-1,3]) | ||
-> [2,2] | ||
// Get horizontal and vertical movement | ||
trans.getTranslation() | ||
// [1, 1] | ||
## API | ||
@@ -64,5 +77,7 @@ | ||
Compute an optimal affine transformation from the domain to range points. | ||
**Parameters** | ||
- *domain*, array of [x,y] points | ||
- *range*, array of [x,y] points | ||
- *domain*: array of [x,y] points | ||
- *range*: array of [x,y] points | ||
@@ -74,2 +89,16 @@ The *domain* and *range* should have equal length. Different lengths are allowed but additional points in the longer array are ignored in the estimation. | ||
### nudged.estimateFixed(domain, range, pivot) | ||
Often one point, e.g. a corner of a picture, needs to stay put regardless of the domain and range. | ||
**Parameters** | ||
- *domain*: array of [x,y] points | ||
- *range*: array of [x,y] points | ||
- *pivot*: [x,y] point | ||
The *domain* and *range* should have equal length. Different lengths are allowed but additional points in the longer array are ignored in the estimation. | ||
**Return** new *nudged.Transform(...)* instance. | ||
### nudged.version | ||
@@ -92,2 +121,4 @@ | ||
Apply the transform to a point or an array of points. | ||
**Return** an array of transformed points or single point if only a point was given. For example: | ||
@@ -109,2 +140,4 @@ | ||
Get clockwise rotation from the positive x-axis. | ||
**Return** rotation in radians. | ||
@@ -111,0 +144,0 @@ |
@@ -109,2 +109,11 @@ var should = require('should'); | ||
describe('.estimateFixed', function () { | ||
it('should allow domain under pivot', function () { | ||
var t = nudged.estimateFixed([[1,1], [1,1]], [[2,2], [2,2]], [1,1]); | ||
// Identity transform | ||
t.transform([5,6]).should.deepEqual([5,6]); | ||
}); | ||
}); | ||
describe('.Transform', function () { | ||
@@ -111,0 +120,0 @@ var t; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
266639
3455
177
0