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
JavaScript
/**
* @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)