Source: perspective-transform.js

const hasard = require('hasard');
const AbstractAugmenter = require('./abstract');
/**
* Applies a random four-point perspective transform to the image (kinda like an advanced form of cropping).
* Each point has a random distance from the image corner, derived from a normal distribution with sigma `sigma`.
*
* Warning : this is only working with opencv4nodejs backend
*
* @param {Object} sigma options
* @param {NumberArgument} sigma.sigma the sigma of the normal distribution
* @param {BooleanArgument} sigma.keepSize If `keepSize` is set to True (default), each image will be resized back to its original size.
* @param {ColorArgument} [sigma.borderValue=[0,0,0]] if borderType is "constant" this is used as border pixel values
* @param {BorderTypeArgument} [sigma.borderType="constant"] can be "constant", "replicate", "transparent"
* @param {Array.<Array.<Number>>} [sigma.cornersVariation=null] if set, sigma is not used. For more deterministic behavior, use this to set manually the percent (x,y) variation of each corner
* @example
// Simple usage, perspective transform on a sigma=10% random perspective tarnsform
ia.perspectiveTransform(0.1);
* @example
// Now replicate the borders
ia.perspectiveTransform({
	sigma: 0.1,
	borderType: "replicate"
});
*/

class PerspectiveTransformAugmenter extends AbstractAugmenter {
	constructor(opts, ia) {
		let o;
		if (typeof (opts) === 'number' || hasard.isHasard(opts)) {
			o = {sigma: opts};
		} else {
			o = opts;
		}

		super(o, ia);
		const {sigma, keepSize, borderValue = [0, 0, 0], borderType = 'constant', cornersVariation} = o;
		this.sigma = this.toSize2(sigma) || [0, 0];
		this.keepSize = keepSize;
		this.borderValue = borderValue;
		this.borderType = borderType;
		this.cornersVariation = cornersVariation;
	}

	buildParams() {
		return new hasard.Object({
			cornersVariation: this.cornersVariation || new hasard.Array({
				size: 4,
				value: new hasard.Array([
					new hasard.Number({
						type: 'normal',
						std: hasard.getProperty(0, this.sigma)
					}),
					new hasard.Number({
						type: 'normal',
						std: hasard.getProperty(1, this.sigma)
					})
				])
			}),
			borderValue: this.borderValue,
			borderType: this.borderType
		});
	}

	getTransformationMatrix({cornersVariation, width, height}) {
		const cornersSrc = [[0, 0], [1, 0], [0, 1], [1, 1]];
		const srcPoints = cornersSrc.map(c => this.backend.point(c[0] * width, c[1] * height));

		const destPoints = cornersSrc.map((c, index) => this.backend.point((c[0] + cornersVariation[index][0]) * width, (c[1] + cornersVariation[index][1]) * height));
		return this.backend.getPerspectiveTransform(srcPoints, destPoints);
	}

	augmentImage({image, width, height}, {cornersVariation, borderValue, borderType}) {
		const transformationMatrix = this.getTransformationMatrix({width, height, cornersVariation});
		const res = this.backend.perspective(image, {transformationMatrix, size: [width, height], borderType, borderValue});
		this.backend.dispose(transformationMatrix);
		return res;
	}

	augmentPoints({points, width, height}, {cornersVariation}) {
		const transformationMatrix = this.getTransformationMatrix({width, height, cornersVariation});

		const transformPoint = point => {
			const res = this.backend.matMul(transformationMatrix, this.backend.point3(point.x, point.y, 1), transformationMatrix.type);
			const p2 = this.backend.pointFromMat(res);
			const res2 = this.backend.pointToArray(p2);
			this.backend.dispose(res);
			return [res2[0] / res2[2], res2[1] / res2[2]];
		};

		const res = points.map(transformPoint);
		this.backend.dispose(transformationMatrix);

		return res;
	}
}

module.exports = PerspectiveTransformAugmenter;