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+
115// REFERENCES :
216// https://stackoverflow.com/questions/38285229/calculating-aspect-ratio-of-perspective-transform-destination-image/38402378#38402378
317// http://www.corrmap.com/features/homography_transformation.php
418// https://ags.cs.uni-kl.de/fileadmin/inf_ags/3dcv-ws11-12/3DCV_WS11-12_lec04.pdf
519// http://graphics.cs.cmu.edu/courses/15-463/2011_fall/Lectures/morphing.pdf
620
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 ;
835
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+ }
1141
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 ( ) ;
13103
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+ */
14124function order4Points ( pts : Point [ ] ) {
15125 let tl : Point ;
16126 let tr : Point ;
@@ -61,11 +171,21 @@ function order4Points(pts: Point[]) {
61171
62172 return [ tl , tr , br , bl ] ;
63173}
64-
174+ /**
175+ * Calculates distance between points.
176+ * @param p1 - Point1
177+ * @param p2 - Point2
178+ * @returns distance between points.
179+ */
65180function distance2Points ( p1 : Point , p2 : Point ) {
66181 return Math . hypot ( p1 . column - p2 . column , p1 . row - p2 . row ) ;
67182}
68-
183+ /**
184+ * Calculates cross products between two vectors.
185+ * @param u - Vector1.
186+ * @param v - Vector2.
187+ * @returns new calculated vector.
188+ */
69189function crossVect ( u : Vector , v : Vector ) : Vector {
70190 const result = [
71191 u [ 1 ] * v [ 2 ] - u [ 2 ] * v [ 1 ] ,
@@ -74,11 +194,27 @@ function crossVect(u: Vector, v: Vector): Vector {
74194 ] ;
75195 return result as Vector ;
76196}
77-
197+ /**
198+ * Calculates dot product between two vectors.
199+ * @param u - Vector1.
200+ * @param v - Vector2.
201+ * @returns result of the product.
202+ */
78203function dotVect ( u : Vector , v : Vector ) : number {
79204 const result = u [ 0 ] * v [ 0 ] + u [ 1 ] * v [ 1 ] + u [ 2 ] * v [ 2 ] ;
80205 return result ;
81206}
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+ */
82218function computeWidthAndHeigth (
83219 points : { tl : Point ; tr : Point ; br : Point ; bl : Point } ,
84220 widthImage : number ,
@@ -182,106 +318,3 @@ function projectionPoint(
182318 ] ;
183319 return image . getValue ( Math . floor ( newX ) , Math . floor ( newY ) , channel ) ;
184320}
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