Skip to content
This repository was archived by the owner on Jul 26, 2025. It is now read-only.

Commit cc4d5b6

Browse files
committed
feat: implement warping from image-js
1 parent bdfff4e commit cc4d5b6

File tree

1 file changed

+287
-0
lines changed

1 file changed

+287
-0
lines changed

src/geometry/getPerspectiveWarp.ts

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
// REFERENCES :
2+
// https://stackoverflow.com/questions/38285229/calculating-aspect-ratio-of-perspective-transform-destination-image/38402378#38402378
3+
// http://www.corrmap.com/features/homography_transformation.php
4+
// https://ags.cs.uni-kl.de/fileadmin/inf_ags/3dcv-ws11-12/3DCV_WS11-12_lec04.pdf
5+
// http://graphics.cs.cmu.edu/courses/15-463/2011_fall/Lectures/morphing.pdf
6+
7+
import { Matrix, inverse, SingularValueDecomposition } from 'ml-matrix';
8+
9+
import { Image } from '../Image.js';
10+
import type { Point } from '../utils/geometry/points.js';
11+
12+
type Vector = [number, number, number];
13+
14+
function order4Points(pts: Point[]) {
15+
let tl: Point;
16+
let tr: Point;
17+
let br: Point;
18+
let bl: Point;
19+
20+
let minX = pts[0].column;
21+
let indexMinX = 0;
22+
23+
for (let i = 1; i < pts.length; i++) {
24+
if (pts[i].column < minX) {
25+
minX = pts[i].column;
26+
indexMinX = i;
27+
}
28+
}
29+
30+
let minX2 = pts[(indexMinX + 1) % pts.length].column;
31+
let indexMinX2 = (indexMinX + 1) % pts.length;
32+
33+
for (let i = 1; i < pts.length; i++) {
34+
if (pts[i].column < minX2 && i !== indexMinX) {
35+
minX2 = pts[i].column;
36+
indexMinX2 = i;
37+
}
38+
}
39+
40+
if (pts[indexMinX2].row < pts[indexMinX].row) {
41+
tl = pts[indexMinX2];
42+
bl = pts[indexMinX];
43+
if (indexMinX !== (indexMinX2 + 1) % 4) {
44+
tr = pts[(indexMinX2 + 1) % 4];
45+
br = pts[(indexMinX2 + 2) % 4];
46+
} else {
47+
tr = pts[(indexMinX2 + 2) % 4];
48+
br = pts[(indexMinX2 + 3) % 4];
49+
}
50+
} else {
51+
bl = pts[indexMinX2];
52+
tl = pts[indexMinX];
53+
if (indexMinX2 !== (indexMinX + 1) % 4) {
54+
tr = pts[(indexMinX + 1) % 4];
55+
br = pts[(indexMinX + 2) % 4];
56+
} else {
57+
tr = pts[(indexMinX + 2) % 4];
58+
br = pts[(indexMinX + 3) % 4];
59+
}
60+
}
61+
62+
return [tl, tr, br, bl];
63+
}
64+
65+
function distance2Points(p1: Point, p2: Point) {
66+
return Math.hypot(p1.column - p2.column, p1.row - p2.row);
67+
}
68+
69+
function crossVect(u: Vector, v: Vector): Vector {
70+
const result = [
71+
u[1] * v[2] - u[2] * v[1],
72+
u[2] * v[0] - u[0] * v[2],
73+
u[0] * v[1] - u[1] * v[0],
74+
];
75+
return result as Vector;
76+
}
77+
78+
function dotVect(u: Vector, v: Vector): number {
79+
const result = u[0] * v[0] + u[1] * v[1] + u[2] * v[2];
80+
return result;
81+
}
82+
function computeWidthAndHeigth(
83+
points: { tl: Point; tr: Point; br: Point; bl: Point },
84+
widthImage: number,
85+
heightImage: number,
86+
) {
87+
const { tl, tr, br, bl } = points;
88+
const w = Math.max(distance2Points(tl, tr), distance2Points(bl, br));
89+
const h = Math.max(distance2Points(tl, bl), distance2Points(tr, br));
90+
let finalW = 0;
91+
let finalH = 0;
92+
93+
const u0 = Math.ceil(widthImage / 2);
94+
const v0 = Math.ceil(heightImage / 2);
95+
const arVis = w / h;
96+
97+
const m1: Vector = [tl.column, tl.row, 1];
98+
const m2: Vector = [tr.column, tr.row, 1];
99+
const m3: Vector = [bl.column, bl.row, 1];
100+
const m4: Vector = [br.column, br.row, 1];
101+
const k2 = dotVect(crossVect(m1, m4), m3) / dotVect(crossVect(m2, m4), m3);
102+
const k3 = dotVect(crossVect(m1, m4), m2) / dotVect(crossVect(m3, m4), m2);
103+
104+
const n2: Vector = [
105+
k2 * m2[0] - m1[0],
106+
k2 * m2[1] - m1[1],
107+
k2 * m2[2] - m1[2],
108+
];
109+
const n3: Vector = [
110+
k3 * m3[0] - m1[0],
111+
k3 * m3[1] - m1[1],
112+
k3 * m3[2] - m1[2],
113+
];
114+
115+
const n21 = n2[0];
116+
const n22 = n2[1];
117+
const n23 = n2[2];
118+
119+
const n31 = n3[0];
120+
const n32 = n3[1];
121+
const n33 = n3[2];
122+
123+
let f =
124+
(1 / (n23 * n33)) *
125+
(n21 * n31 -
126+
(n21 * n33 + n23 * n31) * u0 +
127+
n23 * n33 * u0 * u0 +
128+
(n22 * n32 - (n22 * n33 + n23 * n32) * v0 + n23 * n33 * v0 * v0));
129+
if (f >= 0) {
130+
f = Math.sqrt(f);
131+
} else {
132+
f = Math.sqrt(-f);
133+
}
134+
135+
const A = new Matrix([
136+
[f, 0, u0],
137+
[0, f, v0],
138+
[0, 0, 1],
139+
]);
140+
const At = A.transpose();
141+
const Ati = inverse(At);
142+
const Ai = inverse(A);
143+
144+
const n2R = Matrix.rowVector(n2);
145+
const n3R = Matrix.rowVector(n3);
146+
147+
const arReal = Math.sqrt(
148+
dotVect(n2R.mmul(Ati).mmul(Ai).to1DArray() as Vector, n2) /
149+
dotVect(n3R.mmul(Ati).mmul(Ai).to1DArray() as Vector, n3),
150+
);
151+
152+
if (arReal === 0 || arVis === 0) {
153+
finalW = Math.ceil(w);
154+
finalH = Math.ceil(h);
155+
} else if (arReal < arVis) {
156+
finalW = Math.ceil(w);
157+
finalH = Math.ceil(finalW / arReal);
158+
} else {
159+
finalH = Math.ceil(h);
160+
finalW = Math.ceil(arReal * finalH);
161+
}
162+
return [finalW, finalH];
163+
}
164+
165+
function projectionPoint(
166+
x: number,
167+
y: number,
168+
a: number,
169+
b: number,
170+
c: number,
171+
d: number,
172+
e: number,
173+
f: number,
174+
g: number,
175+
h: number,
176+
image: Image,
177+
channel: number,
178+
) {
179+
const [newX, newY] = [
180+
(a * x + b * y + c) / (g * x + h * y + 1),
181+
(d * x + e * y + f) / (g * x + h * y + 1),
182+
];
183+
return image.getValue(Math.floor(newX), Math.floor(newY), channel);
184+
}
185+
186+
/**
187+
* Transform a quadrilateral into a rectangle
188+
* @memberof Image
189+
* @instance
190+
* @param image
191+
* @param [pts] - Array of the four corners.
192+
* @param [options]
193+
* @param [options.calculateRatio=true] - true if you want to calculate the aspect ratio "width x height" by taking the perspectiv into consideration.
194+
* @returns The new image, which is a rectangle
195+
* @example
196+
* var cropped = image.warpingFourPoints({
197+
* pts: [[0,0], [100, 0], [80, 50], [10, 50]]
198+
* });
199+
*/
200+
201+
export default function getPerspectiveWarp(
202+
image: Image,
203+
pts: Point[],
204+
options: { calculateRatio?: boolean } = {},
205+
) {
206+
const { calculateRatio = true } = options;
207+
208+
if (pts.length !== 4) {
209+
throw new Error(
210+
`The array pts must have four elements, which are the four corners. Currently, pts have ${pts.length} elements`,
211+
);
212+
}
213+
214+
const [tl, tr, br, bl] = order4Points(pts);
215+
216+
let widthRect;
217+
let heightRect;
218+
if (calculateRatio) {
219+
[widthRect, heightRect] = computeWidthAndHeigth(
220+
{
221+
tl,
222+
tr,
223+
br,
224+
bl,
225+
},
226+
image.width,
227+
image.height,
228+
);
229+
} else {
230+
widthRect = Math.ceil(
231+
Math.max(distance2Points(tl, tr), distance2Points(bl, br)),
232+
);
233+
heightRect = Math.ceil(
234+
Math.max(distance2Points(tl, bl), distance2Points(tr, br)),
235+
);
236+
}
237+
238+
const newImage = Image.createFrom(image, {
239+
width: widthRect,
240+
height: heightRect,
241+
});
242+
const [x1, y1] = [0, 0];
243+
const [x2, y2] = [0, widthRect - 1];
244+
const [x3, y3] = [heightRect - 1, widthRect - 1];
245+
const [x4, y4] = [heightRect - 1, 0];
246+
247+
const S = new Matrix([
248+
[x1, y1, 1, 0, 0, 0, -x1 * tl.column, -y1 * tl.column],
249+
[x2, y2, 1, 0, 0, 0, -x2 * tr.column, -y2 * tr.column],
250+
[x3, y3, 1, 0, 0, 0, -x3 * br.column, -y3 * br.column],
251+
[x4, y4, 1, 0, 0, 0, -x4 * bl.column, -y4 * bl.column],
252+
[0, 0, 0, x1, y1, 1, -x1 * tl.row, -y1 * tl.row],
253+
[0, 0, 0, x2, y2, 1, -x2 * tr.row, -y2 * tr.row],
254+
[0, 0, 0, x3, y3, 1, -x3 * br.row, -y3 * br.row],
255+
[0, 0, 0, x4, y4, 1, -x4 * bl.row, -y4 * bl.row],
256+
]);
257+
258+
const D = Matrix.columnVector([
259+
tl.column,
260+
tr.column,
261+
br.column,
262+
bl.column,
263+
tl.row,
264+
tr.row,
265+
br.row,
266+
bl.row,
267+
]);
268+
269+
const svd = new SingularValueDecomposition(S);
270+
const T = svd.solve(D); // solve S*T = D
271+
const [a, b, c, d, e, f, g, h] = T.to1DArray();
272+
273+
for (let i = 0; i < heightRect; i++) {
274+
for (let j = 0; j < widthRect; j++) {
275+
for (let channel = 0; channel < image.channels; channel++) {
276+
newImage.setValue(
277+
j,
278+
i,
279+
channel,
280+
projectionPoint(i, j, a, b, c, d, e, f, g, h, image, channel),
281+
);
282+
}
283+
}
284+
}
285+
286+
return newImage;
287+
}

0 commit comments

Comments
 (0)