UNPKG

homography

Version:

Perform Affine, Projective or Piecewise Affine transformations over any Image or HTMLElement from only a set of reference points. High-Performance and easy-to-use.

643 lines (599 loc) 114 kB
/** * @copyright Eric Cañas 2021. * @author Eric Cañas <elcorreodeharu@gmail.com> * @since 1.0.0 * @file Homography.js class. It implements the Homography class, designed for implementing image homographies in Javascript. * It is designed to be easy-to-use (even for developers that are not familiar with Computer Vision) lightweight and fast, * in order to execute in real time applications (even in low-spec devices such as budget smartphones). */ /** * Available types of transforms * @typedef {"auto"|"affine"|"piecewiseaffine"|"projective"} Transform */ /** * Equations of the line of each segment of a Triangle * @typedef {Object} LineEquations * @property {Number} m m parameter of the equation (y = mx+ b). * @property {Number} b b parameter of the equation (y = mx+ b). * @property {Number} minY Minimum value of y within the segment. * @property {Number} maxY Maximum value of y within the segment. */ // In NPM //import Delaunator from 'delaunator'; // In JS import Delaunator from 'https://cdn.skypack.dev/delaunator@5.0.0'; const availableTransforms = ['auto', 'piecewiseaffine', 'affine', 'projective']; const maxCSSDecimal = 5; // It is thought for 2D const dims = 2; // Max allowed width/height in normalized coordinates (just for allowing resizes up to x8) const normalizedMax = 8.0; class Homography { /** * Summary. Class for performing geometrical transformations over images. * * Description. Homography is in charge of performing: Affine, Projective or PiecewiseAffine transformations over images, * in a way that is as transparent and simple to the user as possible. It is lightweight and specially intended * for real-time applications. For this purpose, this class keeps an internal state for avoiding redundant operations * when reused, therefore, critical performance comes when multiple transformations are done over the same image. * * @constructs Homography * @link https://github.com/Eric-Canas/Homography.js * * @param {Transform} [transform = "auto"] String representing the transformation to be done. One of "auto", "affine", "piecewiseaffine" or "projective": * · "auto" : Transformation will be automatically selected depending on the inputs given. Just take "auto" if you * don't know which kind of transform do you need. This is the default value. * * · "affine" : A geometrical transformation that ensures that all parallel lines of the input image will be parallel * in the output image. It will need exactly three source points to be set (and three destiny points). * An affine transformation can only be composed by rotations, scales, shearings and reflections. * * · "piecewiseaffine" : A composition of several affine transforms that allows more complex constructions. This transforms * generates a mesh of triangles with the source points and finds an independent affine transformation * for each one of them. This way, it allows more complex transformation as, for example, sinusoidal forms. * It can take any amount (greater than three) of reference points. When "piecewiseaffine" mode is selected, * only the parts of the input image within a triangle will appear on the output image. If you want to ensure * that the whole image appears in the output, ensure to set include reference point on each corner of the image. * * · "projective" : A transformation that shows how the an image change when the point of view of the observer is modified. * It takes exactly four source points (and four destiny points). This is the transformation that should * be used when looking for perspective modifications. * * @param {Number} [width] Optional width of the input image. If given, it will resize the input image to that width. Lower widths will imply faster * transformations at the cost of lower resolution in the output image, while larger widths will produce higher resolution images * at the cost of processing time. If null, it will use the original image width. * * @param {Number} [height] Optional height of the input image. If given, it will resize the input image to that height. Lower heights will imply faster * transformations at the cost of lower resolution in the output image, while larger heights will produce higher resolution images * at the cost of processing time. If null, it will use the original image height. * */ constructor(transform = 'auto', width=null, height=null){ // Width and Height refers to the input image. If width and height are given it will be resized. if (width !== null) width = Math.round(width); if (height !== null) height = Math.round(height); // Sets the source width and height this._width = width; this._height = height; // Sets the objective width and height to null since it is unkwnown until source and destiny points are set this._objectiveWidth = null; this._objectiveHeight = null; // Set the source and destiny points to null this._srcPoints = null; //Internal type: Float32Array this._dstPoints = null; //Internal type: Float32Array // Set the selected transform this.firstTransformSelected = transform.toLowerCase(); this.transform = transform.toLowerCase(); // Build the hidden canvas that will help to convert HTMLImageElements to flat Uint8 Arrays this._hiddenCanvas = null; this._hiddenCanvas = document.createElement('canvas'); this._hiddenCanvas.style.display = 'hidden'; this._hiddenCanvas.width = width; this._hiddenCanvas.height = height; this._hiddenCanvasContext = this._hiddenCanvas.getContext("2d"); // Sets the internal variables for the current image to null this._HTMLImage = null; this._image = null; // Set the auxiliar variables that are used in piecewiseAffine transforms for minimizing the computation performed this._maxSrcX = null; this._maxSrcY = null; this._minSrcX = null; this._minSrcY = null; // Sets to default (true) the variables that save the current range of the source and destiny arrays this._srcPointsAreNormalized = true; this._dstPointsAreNormalized = true; // Sets the auxiliar variable that will save for the source image, to which triangle of the mesh belongs each coord in "piecewiseaffine" this._trianglesCorrespondencesMatrix = null; this._triangles = null; // Sets the variables that will save the transform matrices this._transformMatrix = null; this._piecewiseMatrices = null; // Allocate some auxiliar memory for avoiding to allocate any new memory during the "piecewiseaffine" matrix calculations this._auxSrcTriangle = new Float32Array(3*dims); this._auxDstTriangle = new Float32Array(3*dims); this._initialTriangles = null; } /** * Summary. Sets the source and destiny reference points ([[x1, y1], [x2, y2], ...]) of the transform and, optionally, * the image that will be transformed. * * Description. Reference points are two sets of 2-D coordinates. Each point [xi, yi], of the source points will be mapped to its correspondent * [xi', yi'] in the output image. The rest of coordinates of the image will be interpolated through the geometrical transform estimated * from these ones. Calling this function will be equivalent to call setSourcePoints(srcPoints) followed by setDstPoints(dstPoints). For * performance reasons, when calling succesive warpings, you should always use one of these two functions if only one set of points is being * modified between frames. * * @param {ArrayBuffer | Array} srcPoints Source points of the transform, given as an ArrayBuffer or Array in the form [x1, y1, x2, y2...] * or [[x1, y1], [x2, y2]...]. These source points should be declared in image coordinates, (x : [0, width], * y : [0, height]) or in normalized coordinates (x : [0.0, 1.0], y : [0.0, 1.0]). To allow rescalings (from x0 to x8), * normalized scale is automatically detected when the points array does not contain any value larger than 8.0. * Coordinates with larger numbers are considered to be in image scale. For avoiding this automatic behaviour use the * srcPointsAreNormalized paremeter. Please note that, if width and height parameters are setted and points are given in * image coordinates, these image coordinates should be declared in terms of the given width and height, (not in terms * of the original image width/height). * * @param {ArrayBuffer | Array} dstPoints Destiny points of the transform, given as a BufferArray or Array in the form [x1, y1, x2, y2...] * or [[x1, y1], [x2, y2]...]. These source destiny should be declared in image coordinates, (x : [0, width], * y : [0, height]) or in normalized coordinates (x : [0.0, 1.0], y : [0.0, 1.0]). * To allow rescalings (from x0 to x8), normalized scale is automatically detected when the points array does not * contain any value larger than 8.0. Coordinates with larger numbers are considered to be in image scale. * For avoiding this automatic behaviour use the dstPointsAreNormalized paremeter. NOTE that these destiny points should match * in size with the given sourcePoints. * * @param {HTMLImageElement} [image] Optional source image, that will be warped later. Setting this element here will help to advance some calculations * improving the later warping performance, specially when it is planned to apply multiple transformations (same source points * different destiny points) to the same image. If width and/or height are given image will be internally rescaled previous * to any transformation. * * @param {Number} [width] Optional width of the input image. If given, it will resize the input image to that width. Lower widths will imply faster * transformations at the cost of lower resolution in the output image, while larger widths will produce higher resolution images * at the cost of processing time. If null, it will use the original image width. * * @param {Number} [height] Optional height of the input image. If given, it will resize the input image to that height. Lower heights will imply faster * transformations at the cost of lower resolution in the output image, while larger heights will produce higher resolution images * at the cost of processing time. If null, it will use the original image height. * * @param {Boolean} [srcPointsAreNormalized] Optional boolean determining if the parameter srcPoints is in normalized or in image coordinates. If not given it will be * automatically inferred from the points array. * * @param {Boolean} [dstPointsAreNormalized] Optional boolean determining if the parameter dstPoints is in normalized or in image coordinates. If not given it will be * automatically inferred from the points array. * */ setReferencePoints(srcPoints, dstPoints, image = null, width = null, height = null, srcPointsAreNormalized = null, dstPointsAreNormalized = null){ if (typeof(srcPoints) === 'undefined' || typeof(dstPoints) === 'undefined'){ throw("Source and Destiny points must be defined when calling setReferencePoints().") } // Set dstPoints as null for avoiding setSourcePoints to calculate a matrix that will turn invalid in the next line this._dstPoints = null; this.setSourcePoints(srcPoints, image, width, height, srcPointsAreNormalized); this.setDestinyPoints(dstPoints, dstPointsAreNormalized) } /** * Summary. Sets the source reference points ([[x1, y1], [x2, y2], ...]) of the transform and, optionally, * the image that will be transformed. * * Description. Source reference points is a set of 2-D coordinates determined in the input image that will exactly go to * the correspondent destiny points coordinates (setted through setDstPoints()) in the output image. The rest * of coordinates of the image will be interpolated through the geometrical transform estimated f these ones. * * @param {ArrayBuffer | Array} points Source points of the transform, given as a ArrayBuffer or Array in the form [x1, y1, x2, y2...] * or [[x1, y1], [x2, y2]...]. These source points should be declared in image coordinates, (x : [0, width], * y : [0, height]) or in normalized coordinates (x : [0.0, 1.0], y : [0.0, 1.0]). To allow rescalings (from x0 to x8), * normalized scale is automatically detected when the points array does not contain any value larger than 8.0. * Coordinates with larger numbers are considered to be in image scale. For avoiding this automatic behaviour use the * pointsAreNormalized paremeter. Please note that, if width and height parameters are setted and points are given in * image coordinates, these image coordinates should be declared in terms of the given width and height, (not in terms * of the original image width/height). * * @param {HTMLImageElement} [image] Optional source image, that will be warped later. Setting this element here will help to advance some calculations * improving the later warping performance, specially when it is planned to apply multiple transformations (same source points * different destiny points) to the same image. If width and/or height are given image will be internally rescaled previous * to any transformation. * * @param {Number} [width] Optional width of the input image. If given, it will resize the input image to that width. Lower widths will imply faster * transformations at the cost of lower resolution in the output image, while larger widths will produce higher resolution images * at the cost of processing time. If null, it will use the original image width. * * @param {Number} [height] Optional height of the input image. If given, it will resize the input image to that height. Lower heights will imply faster * transformations at the cost of lower resolution in the output image, while larger heights will produce higher resolution images * at the cost of processing time. If null, it will use the original image height. * * @param {Boolean} [pointsAreNormalized] Optional boolean determining if the parameter points is in normalized or in image coordinates. If not given it will be * automatically inferred from the points array. * */ setSourcePoints(points, image = null, width = null, height = null, pointsAreNormalized = null){ // If it is given as a list, transform it to an Float32Array for improving performance. if(!ArrayBuffer.isView(points)) points = new Float32Array(points.flat()) // Set the source points property this._srcPoints = points; // Check if it is given in normalized coordinates (if this information is not given by the user). this._srcPointsAreNormalized = pointsAreNormalized === null? !containsValueGreaterThan(this._srcPoints, normalizedMax) : pointsAreNormalized; // Trasform matrtix should be erased as srcPoints have changed, thus it turns invalid. this._transformMatrix = null; // Verifies if the selected transform is coherent with the points array given, or select the best one if 'auto' mode is selected. this.transform = checkAndSelectTransform(this.firstTransformSelected, this._srcPoints); // Unset objective width and height as they can change when source width/height is changed this._objectiveWidth = null; this._objectiveHeight = null; // Set the image property if given. If also given, it will also set the width and height as well as to resize the image. if (image !== null){ this.setImage(image, width, height); // If no image was given but height and width were, set them. } else if (width !== null || height !== null){ this._setSrcWidthHeight(width, height); //It will denormalize the srcPoints array } // Denormalize points if there is enough information for it. if (this._width !== null && this._height !== null && this._srcPointsAreNormalized){ denormalizePoints(this._srcPoints, this._width, this._height); this._srcPointsAreNormalized = false; } // If I have the dstPoints setted, try to recalculate the new transform matrix if possible (except for piecewise). if(this._dstPoints !== null && this.transform !== 'piecewiseaffine'){ this._transformMatrix = calculateTransformMatrix(this.transform, this._srcPoints, this._dstPoints); } // In case that no width or height were given, but points were already in image coordinates, the "piecewiseaffine" correspondence matrix is still calculable. if (this.transform === 'piecewiseaffine' && this._trianglesCorrespondencesMatrix === null){ // Unset any previous information about Piecewise Affine auxiliar matrices, as they are not reutilizable when source points are modified. this._triangles = this._initialTriangles; this._piecewiseMatrices = null; // If there is information for calculating the auxiliar piecewise matrices, calculate them if (!this._srcPointsAreNormalized || (this._width > 0 && this._height > 0)){ // Set all the parameters that can be already set this._setPiecewiseAffineTransformParameters(); // Otherwise calculate only the tringles mesh, that is the unique that can be actually calculated. } else if(this._triangles === null){ this._triangles = Delaunay(this._srcPoints); } } } /** * Summary. Sets the image that will be transformed when warping. * * Description. Setting the image before the destiny points (setDstPoints()) and the warping (call to warp()) will help to advance * calculations as well as to avoid future redundant calculations when successive calls to setDstPoints()->warp() will * occur in the future. It will severally improve the performance of applications that can take profit of that, as for * example those ones that have a static source image that must be continually adapted to different dstPoints detections * coming from a videoStream. This performance improvement will specially highligth for the "piecewiseaffine" transform, * as it is the one that is more computationally expensive. * * @param {HTMLImageElement|ImageData} image Image that will internally saved for future warping (warp()). As an HTMLImageElement or ImageData. * * @param {Number} [width] Optional width. Resizes the input image to the given width. If not provided, original image width will be used * (widths lowers than the original image width will improve speed at cost of resolution). It is not recommended * to set widths below the expected output width, since at this point the speed improvement will dissapear and * only resolution will be worsen. * * @param {Number} [height] Optional height. Resizes the input image to the given height. If not provided, original image height will be used * (heights lowers than the original image height will improve speed at cost of resolution). It is not recommended * to set heights below the expected output height, since at this point the speed improvement will dissapear and * only resolution will be worsen. * */ setImage(image, width = null, height = null){ // Set the current width and height of the input. As the width/height given by the user or the original width/height of the image if not given if ((this._width === null || this._height === null) && !ArrayBuffer.isView(image.data)){ this._setSrcWidthHeight((width === null? image.width : width), (height === null? image.height : height)); } // Sets the image as a flat Uint8ClampedArray, for dealing fast with it. It will also resize the image if needed. // If it is already ImageData save it, else convert it if (ArrayBuffer.isView(image.data)){ this._image = image.data; this._setSrcWidthHeight(image.width, image.height); } else { this._HTMLImage = image; this._image = this._getImageAsRGBAArray(image); } // If source points are already set, now it is possible to calculate the "piecewiseaffine" parameters if needed. if(this._srcPoints !== null && this.transform === 'piecewiseaffine'){ // Calculate all the auxiliar parameters that can be already calculated this._setPiecewiseAffineTransformParameters(); } // If destiny points are already set but objectiveWidth and objectiveHeight are not, set them now. if (this._dstPoints !== null && (this._objectiveWidth <= 0 || this._objectiveHeight <= 0)){ this._induceBestObjectiveWidthAndHeight(); } } /** * Summary. Sets the destiny reference points ([[x1, y1], [x2, y2], ...]) of the transform. * * Description. Destiny reference points is a set of 2-D coordinates determined for the output image. They must match with the * source points, as each source points of the input image will be transformed for going exactly to its correspondent * destiny points in the output image. The rest of coordinates of the image will be interpolated through the geometrical * transform estimated from these correspondences. * * @param {ArrayBuffer | Array} points Destiny points of the transform, given as a BufferArray or Array in the form [x1, y1, x2, y2...] * or [[x1, y1], [x2, y2]...]. These source destiny should be declared in image coordinates, (x : [0, width], * y : [0, height]) or in normalized coordinates (x : [0.0, 1.0], y : [0.0, 1.0]). * To allow rescalings (from x0 to x8), normalized scale is automatically detected when the points array does not * contain any value larger than 8.0. Coordinates with larger numbers are considered to be in image scale. * For avoiding this automatic behaviour use the pointsAreNormalized paremeter. * * @param {Boolean} [pointsAreNormalized] Optional boolean determining if the parameter points is in normalized or in image coordinates. If not given it will be * automatically inferred from the points array. * */ setDestinyPoints(points, pointsAreNormalized = null){ // Transform it to a typed array for perfomance reasons if(!ArrayBuffer.isView(points)) points = new Float32Array(points.flat()); // Verify that these points matches with the source points if(this._srcPoints !== null && points.length !== this._srcPoints.length) throw(`It must be the same amount of destiny points (${points.length/dims}) than source points (${this._srcPoints.length/dims})`); // Set them this._dstPoints = points; this._dstPointsAreNormalized = pointsAreNormalized === null? !containsValueGreaterThan(this._dstPoints, normalizedMax) : pointsAreNormalized; // As both source and destiny points are set now, calculate the transformation matrix for whichever the selected transform is if (this.transform !== 'piecewiseaffine'){ // Denormalize points for the projective case as it needs not normalized ranges if (this._dstPointsAreNormalized && this._width > 0 && this._height > 0 && this.transform === 'projective'){ denormalizePoints(this._dstPoints, this._width, this._height); this._dstPointsAreNormalized = false; } // Ensure that destiny and source points are in the same range this._putSrcAndDstPointsInSameRange(); // Calculate the projective or the affine transform this._transformMatrix = calculateTransformMatrix(this.transform, this._srcPoints, this._dstPoints); } else { // Unset piecewiseMatrices as they turns invalid when dstPoints are changed this._piecewiseMatrices = null; } // If there is enough information for calculating the objective width and height, calculate it if (this._image !== null || (this.transform === 'piecewiseaffine' && this._width > 0 && this._height > 0)){ this._induceBestObjectiveWidthAndHeight(); } // If Piecewise Affine transform was selected and there is enough information, calculate all the auxiliar structures if (this.transform === 'piecewiseaffine' && this._width > 0 && this._height > 0){ // Transform the points to image coordinates if normalized coordinates were given if (this._dstPointsAreNormalized){ denormalizePoints(this._dstPoints, this._width, this._height); this._dstPointsAreNormalized = false; } // Set the parameters piecewise affine parameters that can be already set this._setPiecewiseAffineTransformParameters(); } } /** * Summary. Apply the selected transform to an image. * * Description. Apply the calculated homography to the given image. Output image will have enough width and height for enclosing the whole input image without * any crop or pad. Any void section of the output image will be transparent. If no image is passed to the function and it was setted before the * call of warp (recommended for performance reasons) warps the pre-setted image. In case that an image is given, it will be internally setted, * so any future call to warp() receiving no parameters will apply the transformation over this image again (It will be usually useful when the same * image is being constantly adapted to, for example, detections coming from a video stream). Remember that it will transform the whole input image * for "affine" and "projective" transforms, while for "piecewiseaffine" transforms it will only transform the parts of the image that can be connected * through the given source points. It occurs because "piecewiseaffine" transforms define different Affine transforms for different sections of the input * image, so it can not calculate transforms for undefined sections. If you want the whole output image in a "piecewiseaffine" transform you should set a * source point in each corner of the input image ([[x1, y1], [x2, y2], ..., [0, 0], [0, height], [width, 0], [width, height]]). * * @param {HTMLImageElement} [image] Image that will transformed. If this parameter is not given since image was previously setted through `setImage(img)` or * `setSrcPoints(points, img)`, this previously setted image will be the one that will be warped. If an image is given, * it will be internally setted, so any future call to warp for transforming the same image could avoid to pass this image * parameter again. This reuse of the image, if applicable, would speed up the transformation. * * @param {Boolean} [asHTMLPromise = false] If True, returns a Promise of an HTMLImageElement containing the Image, instead of an ImageData buffer. It could be convenient for some * applications, but try to avoid it on critical performance applications as it would decrease its overall performance. If you need to * draw it on a canvas, it can be directly done through context.putImageData(imgData, x, y). * * @return {ImageData|Promise<HTMLImageElement>} Transformed image in format ImageData or Promise of an HTMLImageElement if asHTMLPromise was set to true. ImageData buffers can be * directly drawn on canvas by using context.putImageData(imgData, x, y). */ warp(image = null, asHTMLPromise = false, applyAlwaysInverse = false){ // If the image was given, sets it internally (It will also recalculate any information that depends of it). if (image !== null){ this.setImage(image); } else if (this._image === null){ throw("warp() must receive an image if it was not setted before through `setImage(img)` or `setSourcePoints(points, img)`"); } let output_img; // Generate an image by applying the selected transform. If output image is larger than input image, apply the Inverse Transform instead in order to avoid holes in it. switch(this.transform){ case 'piecewiseaffine': // If objectiveWidth or objectiveHeight are larger than width or height apply inverse transform, otherwise apply the source to destiny transfrom // Apply also the inverse transform in the reduction case, when the width/height difference is great enough for compensating the overhead of inverse transform output_img = (applyAlwaysInverse || (this._objectiveWidth > this._width || this._objectiveHeight > this._height || this._objectiveWidth*1.2 < this._width || this._objectiveHeight*1.2 < this._height))? this._inversePiecewiseAffineWarp(this._image) : this._piecewiseAffineWarp(this._image); break; case 'affine': // If objectiveWidth or objectiveHeight are larger than width or height apply inverse transform, otherwise apply the source to destiny transfrom output_img = (applyAlwaysInverse || (this._objectiveWidth !== this._width || this._objectiveHeight !== this._height))? this._inverseGeometricWarp(this._image) : this._geometricWarp(this._image); break; case 'projective': //Force inverse, as otherwise projective would produce sparse parts on the image by the perspective properties output_img = this._inverseGeometricWarp(this._image); break; } // Transform it from the Uint8ClampedArray flat form (better performance for calculating) to the ImageData form (more conve for the user). if (this._objectiveWidth*this._objectiveHeight >= 1 && !isNaN(this._objectiveWidth*this._objectiveHeight)){ output_img = new ImageData(output_img, this._objectiveWidth, this._objectiveHeight); } else { //Just avoid to break when the transform produces a 0 shape image. output_img = new ImageData(new Uint8ClampedArray(4), 1,1); } if (asHTMLPromise) return this.HTMLImageElementFromImageData(output_img); else return output_img; } /** * Summary. Transforms an Image from its ImageData respresentation to an HTMLImageElement. NOTE: Remember to await for the promise to be resolved * (if asPromise is true (default)) or to the "onload" event (if asPromise is false). * * Description. In performance critical applications such as, for example, real-time applications based on videoStreams it should be avoided when * possible as this transformation could decrease the overall framerate. Instead, if you need to draw the result image in a canvas, * try to do it directly through context.putImageData(imgData, x, y). * * * @param {ImageData} imgData Image to be transformed to an HTMLImageElement. * * @param {Boolean} [asPromise = true] If true (default), returns a Promise<HTMLImageElement> that ensures that the image is already loaded when resolved. * If false, directly returns the HTMLImageElement. In this case, the user must take care of not using it before the * "onload" event is triggered. * * @return {HTMLImageElement|Promise<HTMLImageElement>} HTMLImageElement (or promise of it) containing the Image inside in the imgData buffer. This HTMLImageElement, * will also have the same width and height than this imgData buffer. */ HTMLImageElementFromImageData(imgData, asPromise = true) { let previousCanvasWidth = null; if (this._objectiveWidth !== this._hiddenCanvas.width){ previousCanvasWidth = this._hiddenCanvas.width; this._hiddenCanvas.width = this._objectiveWidth; } let previousCanvasHeight = null; if (this._objectiveHeight !== this._hiddenCanvas.height){ previousCanvasHeight = this._hiddenCanvas.height; this._hiddenCanvas.height = this._objectiveHeight; } this._hiddenCanvasContext.clearRect(0, 0, this._objectiveWidth, this._objectiveHeight); this._hiddenCanvasContext.putImageData(imgData, 0, 0); let img = document.createElement('img') img.src = this._hiddenCanvas.toDataURL(); img.width = this._objectiveWidth; img.height = this._objectiveHeight; if (previousCanvasWidth !== null) {this._hiddenCanvas.width = previousCanvasWidth;} if (previousCanvasHeight !== null) {this._hiddenCanvas.height = previousCanvasHeight;} if (asPromise){ return new Promise((resolve, reject) => { img.onload = () => {resolve(img);}; img.onerror = reject; }); } else { return img; } } /** * Summary. Transforms an Image from its ImageData respresentation to an HTMLImageElement. NOTE: Remember to await for the promise to be resolved * (if asPromise is true (default)) or to the "onload" event (if asPromise is false). * * Description. In performance critical applications such as, for example, real-time applications based on videoStreams it should be avoided when * possible as this transformation could decrease the overall framerate. Instead, if you need to draw the result image in a canvas, * try to do it directly through context.putImageData(imgData, x, y). * * * @param {ImageData} imgData Image to be transformed to an HTMLImageElement. * * @param {Boolean} [asPromise = true] If true (default), returns a Promise<HTMLImageElement> that ensures that the image is already loaded when resolved. * If false, directly returns the HTMLImageElement. In this case, the user must take care of not using it before the * "onload" event is triggered. * * @return {HTMLImageElement|Promise<HTMLImageElement>} HTMLImageElement (or promise of it) containing the Image inside in the imgData buffer. This HTMLImageElement, * will also have the same width and height than this imgData buffer. */ setTriangles(triangles) { this._triangles = triangles; if ((!this._srcPointsAreNormalized || (this._width > 0 && this._height > 0)) && this._srcPoints !== null ){ // Set all the parameters that can be already set this._setPiecewiseAffineTransformParameters(); } } /** * Summary. Get the current Affine or Projective transform as a string that can be directly applied in CSS. If new Source and/or Destiny Points * are given uses them instead for calculating a new transform. * * Description. Affine and Projective transforms can be applied on each element that accepts the 'transform' CSS property. You can apply this transformation * to an element just by executing `<your_element>.style.transform = getTransformationMatrixAsCSS();`. Take into account, that this function will * not work if transformation selected was "piecewiseaffine" as CSS does not accept Piecewise Affine transforms. * * @param {ArrayBuffer|Array<Number>} [srcPoints] Optional source points for a new transform, given as a ArrayBuffer or Array in the form [x1, y1, x2, y2, ...] * or [[x1, y1], [x2, y2], ...]. These source points should be declared in pixels coordinates, (x : [0, width], * y : [0, height]) or (preferably for simplicity) in normalized coordinates (x : [0.0, 1.0], y : [0.0, 1.0]). * If no points are given, they should have been setted before through `setSrcPoints(points)`. Remember that you * should give three or four reference points if transform selected is "affine" or "projective" respectively. * * @param {ArrayBuffer|Array<Number>} [dstPoints] Optional destiny points for a new transform, given as a ArrayBuffer or Array in the form [x1, y1, x2, y2, ...] * or [[x1, y1], [x2, y2], ...]. These destiny points should be declared, for simplicity, in the same range than * the previously given srcPoints and it must be the same amount of dstPoints than srcPoints (as they match one to one). * If no points are given, they should have been setted before through `setDstPoints(points)`. * * @return {String} String representation of the transformation matrix, that can be directly applied in to the CSS transform property. */ getTransformationMatrixAsCSS(srcPoints = null, dstPoints = null, width = null, height = null){ if (width !== null || height !== null) this._setSrcWidthHeight(width, height); if (srcPoints !== null) this.setSourcePoints(srcPoints, null, width, height); if (dstPoints !== null) this.setDestinyPoints(dstPoints); if (this._srcPoints === null) throw("Impossible to calculate a transform when srcPoints are not set"); else if (this._dstPoints === null) throw("Impossible to calculate a transform when dstPoints are not set"); else if (this._transformMatrix === null) throw("Transform matrix can not be calculated"); let matrix; switch(this.transform){ case "affine": matrix = `matrix(` for (let i = 0; i<this._transformMatrix.length; i++){ matrix += `${this._transformMatrix[i].toFixed(maxCSSDecimal)}`; if (i < this._transformMatrix.length-1) matrix += `, `; else matrix += `)`; } break; case "projective": matrix = `matrix3d(` let i = 0; for (let dy = 0; dy<4; dy++){ for (let dx = 0; dx<4; dx++){ if (dy === 2 && dx === 2 || dy === 3 && dx === 3) matrix += `1`; else if( dy === 2 || dx === 2) matrix += `0`; else matrix += `${this._transformMatrix[((i++)*3)%8].toFixed(maxCSSDecimal)}` if (dy*4+dx < 4*4-1) matrix += `, `; else matrix += `)`; } } break; default: throw (`Only "affine" or "projective" transforms can be applied on the CSS transform property, but ${this.transform} selected`); } return matrix; } /** * Summary. Apply the current Affine or Projective transform over an HTMLElement * * Description. Affine and Projective transforms can be applied on each element that accepts the 'transform' CSS property. Take into account, that this function will * not work if transformation selected was "piecewiseaffine" as CSS does not accept Piecewise Affine transforms. * * @param {HTMLElement} element Element in which to apply the geometric transform. * * @param {ArrayBuffer|Array<Number>} [srcPoints] Optional source points for a the transform, given as a ArrayBuffer or Array in the form [x1, y1, x2, y2, ...] * or [[x1, y1], [x2, y2], ...]. These source points should be declared in pixels coordinates, (x : [0, width], * y : [0, height]) or (preferably for simplicity) in normalized coordinates (x : [0.0, 1.0], y : [0.0, 1.0]). * If no points are given, they should have been setted before through `setSrcPoints(points)` or * setReferencePoints(srcPoints, dstPoints). Remember that you should give three or four reference points if transform * selected is "affine" or "projective" respectively. * * @param {ArrayBuffer|Array<Number>} [dstPoints] Optional destiny points for a the transform, given as a ArrayBuffer or Array in the form [x1, y1, x2, y2, ...] * or [[x1, y1], [x2, y2], ...]. These source points should be declared in pixels coordinates, (x : [0, width], * y : [0, height]) or (preferably for simplicity) in normalized coordinates (x : [0.0, 1.0], y : [0.0, 1.0]). * If no points are given, they should have been setted before through `setDestinyPoints(points)` or * setReferencePoints(srcPoints, dstPoints). Remember th. */ transformHTMLElement(element, srcPoints = null, dstPoints = null){ const elementRect = element.getBoundingClientRect();; element.style.transform = this.getTransformationMatrixAsCSS(srcPoints, dstPoints, elementRect.width, elementRect.height); } /* ----------------------------------------------- PRIVATE FUNCTIONS -------------------------------------------------- */ /* ------------------------------ These functions should never be used by the user ------------------------------------ */ // ----------------- Set Widths and Heights --------------- /** * * Summary. PRIVATE. AVOID TO USE IT. Sets this._width and this._height properties in a consistent way with the rest of the object. * * Description. PRIVATE. AVOID TO USE IT. It sets the source width and source height properties in an efficient and safe way. It ensures * that all modifications to dependant objects are done. It means that the hidden canvas, and the currently setted image are * resized if needed, that this._width and this._height are Integer numbers and that in the case of Piecewise Affine transform * the auxiliar matrices are recalculated. * * @param {Number} width New width of the input. * * @param {Number} height New height of the input. * */ _setSrcWidthHeight(width, height){ const last_width = this._width; const last_height = this._height; this._width = width; this._height = height; // If width and height are the same than before don't do anything. As all the previous structures are already valid if(last_width !== width || last_height !== height)