image-js
Version:
Image processing and manipulation in JavaScript
131 lines • 4.62 kB
JavaScript
import { Matrix, SingularValueDecomposition } from 'ml-matrix';
// REFERENCES :
// http://graphics.cs.cmu.edu/courses/15-463/2011_fall/Lectures/morphing.pdf
/**
* Computes a perspective transformation matrix to rectify a quadrilateral region into a rectangle.
*
* This function takes four corner points of a quadrilateral (e.g., a document photographed at an angle) and calculates the transformation matrix needed to "unwarp" it into a rectangular image.
* The output dimensions can be specified or calculated automatically based on the input geometry.
* @param pts - 4 reference corners.
* @param options - PerspectiveWarpOptions.
* @returns - Matrix from 4 points.
*/
export function getPerspectiveWarp(pts, options = {}) {
if (pts.length !== 4) {
throw new Error(`The array pts must have four elements, which are the four corners. Currently, pts have ${pts.length} elements`);
}
const { width, height } = options;
const [tl, tr, br, bl] = order4Points(pts);
let widthRect;
let heightRect;
if (height && width) {
widthRect = width;
heightRect = height;
}
else if (!height && !width) {
widthRect = Math.ceil(Math.max(distance2Points(tl, tr), distance2Points(bl, br)));
heightRect = Math.ceil(Math.max(distance2Points(tl, bl), distance2Points(tr, br)));
}
else {
throw new Error(`Invalid dimensions: ${width ? '`height`' : '`width`'} is missing. ` +
`Either provide both width and height, or omit both to auto-calculate dimensions.`);
}
const [x1, y1] = [0, 0];
const [x2, y2] = [widthRect - 1, 0];
const [x3, y3] = [widthRect - 1, heightRect - 1];
const [x4, y4] = [0, heightRect - 1];
const S = new Matrix([
[x1, y1, 1, 0, 0, 0, -x1 * tl.column, -y1 * tl.column],
[x2, y2, 1, 0, 0, 0, -x2 * tr.column, -y2 * tr.column],
[x3, y3, 1, 0, 0, 0, -x3 * br.column, -y3 * br.column],
[x4, y4, 1, 0, 0, 0, -x4 * bl.column, -y4 * bl.column],
[0, 0, 0, x1, y1, 1, -x1 * tl.row, -y1 * tl.row],
[0, 0, 0, x2, y2, 1, -x2 * tr.row, -y2 * tr.row],
[0, 0, 0, x3, y3, 1, -x3 * br.row, -y3 * br.row],
[0, 0, 0, x4, y4, 1, -x4 * bl.row, -y4 * bl.row],
]);
const D = Matrix.columnVector([
tl.column,
tr.column,
br.column,
bl.column,
tl.row,
tr.row,
br.row,
bl.row,
]);
const svd = new SingularValueDecomposition(S);
const T = svd.solve(D).to1DArray(); // solve S*T = D
T.push(1);
const M = [];
for (let i = 0; i < 3; i++) {
const row = [];
for (let j = 0; j < 3; j++) {
row.push(T[i * 3 + j]);
}
M.push(row);
}
return { matrix: M, width: widthRect, height: heightRect };
}
/**
* Sorts 4 points in order =>[top-left,top-right,bottom-right,bottom-left]. Input points must be in clockwise or counter-clockwise order.
* @param pts - Array of 4 points.
* @returns Sorted array of 4 points.
*/
export function order4Points(pts) {
let tl;
let tr;
let br;
let bl;
let minX = pts[0].column;
let indexMinX = 0;
for (let i = 1; i < pts.length; i++) {
if (pts[i].column < minX) {
minX = pts[i].column;
indexMinX = i;
}
}
let minX2 = pts[(indexMinX + 1) % pts.length].column;
let indexMinX2 = (indexMinX + 1) % pts.length;
for (let i = 0; i < pts.length; i++) {
if (pts[i].column < minX2 && i !== indexMinX) {
minX2 = pts[i].column;
indexMinX2 = i;
}
}
if (pts[indexMinX2].row < pts[indexMinX].row) {
tl = pts[indexMinX2];
bl = pts[indexMinX];
if (indexMinX !== (indexMinX2 + 1) % 4) {
tr = pts[(indexMinX2 + 1) % 4];
br = pts[(indexMinX2 + 2) % 4];
}
else {
tr = pts[(indexMinX2 + 2) % 4];
br = pts[(indexMinX2 + 3) % 4];
}
}
else {
bl = pts[indexMinX2];
tl = pts[indexMinX];
if (indexMinX2 !== (indexMinX + 1) % 4) {
tr = pts[(indexMinX + 1) % 4];
br = pts[(indexMinX + 2) % 4];
}
else {
tr = pts[(indexMinX + 2) % 4];
br = pts[(indexMinX + 3) % 4];
}
}
return [tl, tr, br, bl];
}
/**
* Calculates distance between points.
* @param p1 - Point1
* @param p2 - Point2
* @returns distance between points.
*/
function distance2Points(p1, p2) {
return Math.hypot(p1.column - p2.column, p1.row - p2.row);
}
//# sourceMappingURL=getPerspectiveWarp.js.map