1
+ import { Matrix , inverse , SingularValueDecomposition } from 'ml-matrix' ;
2
+
3
+ import { Image } from '../Image.js' ;
4
+ import type { Point } from '../utils/geometry/points.js' ;
5
+
6
+ type Vector = [ number , number , number ] ;
7
+ interface PerspectiveWarpOptionsWithDimensions {
8
+ width ?: number ;
9
+ height ?: number ;
10
+ }
11
+ interface PerspectiveWarpOptionsWithRatios {
12
+ calculateRatio ?: boolean ;
13
+ }
14
+
1
15
// REFERENCES :
2
16
// https://stackoverflow.com/questions/38285229/calculating-aspect-ratio-of-perspective-transform-destination-image/38402378#38402378
3
17
// http://www.corrmap.com/features/homography_transformation.php
4
18
// https://ags.cs.uni-kl.de/fileadmin/inf_ags/3dcv-ws11-12/3DCV_WS11-12_lec04.pdf
5
19
// http://graphics.cs.cmu.edu/courses/15-463/2011_fall/Lectures/morphing.pdf
6
20
7
- import { Matrix , inverse , SingularValueDecomposition } from 'ml-matrix' ;
21
+ /**
22
+ * Applies perspective warp on an image from 4 points.
23
+ * @param image - Image to apply the algorithm on.
24
+ * @param pts - 4 reference corners of the new image.
25
+ * @param options - PerspectiveWarpOptions
26
+ * @returns - New image after warp.
27
+ */
28
+ export default function getPerspectiveWarp (
29
+ image : Image ,
30
+ pts : Point [ ] ,
31
+ options : PerspectiveWarpOptionsWithDimensions &
32
+ PerspectiveWarpOptionsWithRatios = { } ,
33
+ ) {
34
+ const { width, height, calculateRatio } = options ;
8
35
9
- import { Image } from '../Image.js' ;
10
- import type { Point } from '../utils/geometry/points.js' ;
36
+ if ( pts . length !== 4 ) {
37
+ throw new Error (
38
+ `The array pts must have four elements, which are the four corners. Currently, pts have ${ pts . length } elements` ,
39
+ ) ;
40
+ }
11
41
12
- type Vector = [ number , number , number ] ;
42
+ const [ tl , tr , br , bl ] = order4Points ( pts ) ;
43
+
44
+ let widthRect ;
45
+ let heightRect ;
46
+ if ( calculateRatio ) {
47
+ [ widthRect , heightRect ] = computeWidthAndHeigth (
48
+ {
49
+ tl,
50
+ tr,
51
+ br,
52
+ bl,
53
+ } ,
54
+ image . width ,
55
+ image . height ,
56
+ ) ;
57
+ } else if ( height && width ) {
58
+ widthRect = width ;
59
+ heightRect = height ;
60
+ } else {
61
+ widthRect = Math . ceil (
62
+ Math . max ( distance2Points ( tl , tr ) , distance2Points ( bl , br ) ) ,
63
+ ) ;
64
+ heightRect = Math . ceil (
65
+ Math . max ( distance2Points ( tl , bl ) , distance2Points ( tr , br ) ) ,
66
+ ) ;
67
+ }
68
+
69
+ const newImage = Image . createFrom ( image , {
70
+ width : widthRect ,
71
+ height : heightRect ,
72
+ } ) ;
73
+ const [ x1 , y1 ] = [ 0 , 0 ] ;
74
+ const [ x2 , y2 ] = [ 0 , widthRect - 1 ] ;
75
+ const [ x3 , y3 ] = [ heightRect - 1 , widthRect - 1 ] ;
76
+ const [ x4 , y4 ] = [ heightRect - 1 , 0 ] ;
77
+
78
+ const S = new Matrix ( [
79
+ [ x1 , y1 , 1 , 0 , 0 , 0 , - x1 * tl . column , - y1 * tl . column ] ,
80
+ [ x2 , y2 , 1 , 0 , 0 , 0 , - x2 * tr . column , - y2 * tr . column ] ,
81
+ [ x3 , y3 , 1 , 0 , 0 , 0 , - x3 * br . column , - y3 * br . column ] ,
82
+ [ x4 , y4 , 1 , 0 , 0 , 0 , - x4 * bl . column , - y4 * bl . column ] ,
83
+ [ 0 , 0 , 0 , x1 , y1 , 1 , - x1 * tl . row , - y1 * tl . row ] ,
84
+ [ 0 , 0 , 0 , x2 , y2 , 1 , - x2 * tr . row , - y2 * tr . row ] ,
85
+ [ 0 , 0 , 0 , x3 , y3 , 1 , - x3 * br . row , - y3 * br . row ] ,
86
+ [ 0 , 0 , 0 , x4 , y4 , 1 , - x4 * bl . row , - y4 * bl . row ] ,
87
+ ] ) ;
88
+
89
+ const D = Matrix . columnVector ( [
90
+ tl . column ,
91
+ tr . column ,
92
+ br . column ,
93
+ bl . column ,
94
+ tl . row ,
95
+ tr . row ,
96
+ br . row ,
97
+ bl . row ,
98
+ ] ) ;
99
+
100
+ const svd = new SingularValueDecomposition ( S ) ;
101
+ const T = svd . solve ( D ) ; // solve S*T = D
102
+ const [ a , b , c , d , e , f , g , h ] = T . to1DArray ( ) ;
13
103
104
+ for ( let i = 0 ; i < heightRect ; i ++ ) {
105
+ for ( let j = 0 ; j < widthRect ; j ++ ) {
106
+ for ( let channel = 0 ; channel < image . channels ; channel ++ ) {
107
+ newImage . setValue (
108
+ j ,
109
+ i ,
110
+ channel ,
111
+ projectionPoint ( i , j , a , b , c , d , e , f , g , h , image , channel ) ,
112
+ ) ;
113
+ }
114
+ }
115
+ }
116
+
117
+ return newImage ;
118
+ }
119
+ /**
120
+ * Sorts 4 points in order =>[top-left,top-right,bottom-right,bottom-left].
121
+ * @param pts - Array of 4 points.
122
+ * @returns Sorted array of 4 points.
123
+ */
14
124
function order4Points ( pts : Point [ ] ) {
15
125
let tl : Point ;
16
126
let tr : Point ;
@@ -61,11 +171,21 @@ function order4Points(pts: Point[]) {
61
171
62
172
return [ tl , tr , br , bl ] ;
63
173
}
64
-
174
+ /**
175
+ * Calculates distance between points.
176
+ * @param p1 - Point1
177
+ * @param p2 - Point2
178
+ * @returns distance between points.
179
+ */
65
180
function distance2Points ( p1 : Point , p2 : Point ) {
66
181
return Math . hypot ( p1 . column - p2 . column , p1 . row - p2 . row ) ;
67
182
}
68
-
183
+ /**
184
+ * Calculates cross products between two vectors.
185
+ * @param u - Vector1.
186
+ * @param v - Vector2.
187
+ * @returns new calculated vector.
188
+ */
69
189
function crossVect ( u : Vector , v : Vector ) : Vector {
70
190
const result = [
71
191
u [ 1 ] * v [ 2 ] - u [ 2 ] * v [ 1 ] ,
@@ -74,11 +194,27 @@ function crossVect(u: Vector, v: Vector): Vector {
74
194
] ;
75
195
return result as Vector ;
76
196
}
77
-
197
+ /**
198
+ * Calculates dot product between two vectors.
199
+ * @param u - Vector1.
200
+ * @param v - Vector2.
201
+ * @returns result of the product.
202
+ */
78
203
function dotVect ( u : Vector , v : Vector ) : number {
79
204
const result = u [ 0 ] * v [ 0 ] + u [ 1 ] * v [ 1 ] + u [ 2 ] * v [ 2 ] ;
80
205
return result ;
81
206
}
207
+ /**
208
+ * Calculates width and height of the new image for perspective warp.
209
+ * @param points - 4 reference corners.
210
+ * @param points.tl - top-left corner.
211
+ * @param points.tr - top-right corner.
212
+ * @param points.br - bottom-right corner.
213
+ * @param points.bl - bottom-left corner.
214
+ * @param widthImage - image width.
215
+ * @param heightImage - image height.
216
+ * @returns new width and height values.
217
+ */
82
218
function computeWidthAndHeigth (
83
219
points : { tl : Point ; tr : Point ; br : Point ; bl : Point } ,
84
220
widthImage : number ,
@@ -182,106 +318,3 @@ function projectionPoint(
182
318
] ;
183
319
return image . getValue ( Math . floor ( newX ) , Math . floor ( newY ) , channel ) ;
184
320
}
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