const hasard = require('hasard');
const AbstractAugmenter = require('./abstract');
/**
* @typedef {Number | Hasard.<Number>} NumberArgument
*/
/**
* @typedef {Boolean | Hasard.<Boolean>} BooleanArgument
*/
/**
* A color (3 channels or 4 channels) to use in the augmentation step
* @typedef {Array.<Number> | Hasard.<Array.<Number>>} ColorArgument
*/
/**
* @typedef {String | Hasard.<String>} BorderTypeArgument
* describe how to handle the new pixel created by the augmentation step
* * "constant" : use a constant variable
* * "transparent" : use transparent pixels
* * "replicate" : replicate the border's colors
*/
/**
* Applies affine transformations to images.
* @constructor
* @param {Object} opts options
* @param {ArrayXY} [opts.translatePercent = [0,0]] translates
* @param {ArrayXY} [opts.scale = [1,1]] scale percent
* @param {NumberArgument} [opts.rotate=0] degrees of rotation
* @param {NumberArgument} [opts.shear=0] degrees of shear
* @param {BooleanArgument} [keepSize=false]
* If true, the output image plane size will be fitted
* to the distorted image size, i.e. images rotated by 45deg
* will not be partially outside of the image plane.
* WARNING : not implemented yet
* @param {ColorArgument} [opts.borderValue=[0,0,0]] if borderType is "constant" this is used as border pixel values
* @param {BorderTypeArgument} [opts.borderType="constant"] "constant", "replicate", "transparent"
* @example
// Simple usage, Affine transform with scale change
ia.affine({scale: 1.2});
* @example
// Affine transform with rotation of 90° and random scale between 100% and 150%
ia.affine({scale: h.number(1, 1.5), rotate: 90});
*/
class AffineTransformAugmenter extends AbstractAugmenter {
constructor(opts, ia) {
super(opts, ia);
const {scale = [1, 1], translatePercent = [0, 0], rotate = 0, shear = 0, keepSize = false, borderValue = [0, 0, 0], borderType = 'constant'} = opts;
this.scale = this.toSize2(scale);
this.translatePercent = this.toSize2(translatePercent);
this.rotate = rotate;
this.shear = shear;
this.keepSize = keepSize;
this.borderValue = borderValue;
this.borderType = borderType;
}
buildParams() {
const {scale, translatePercent, rotate, shear, borderValue, borderType} = this;
return new hasard.Object({
scale,
translatePercent,
rotate,
shear,
borderValue,
borderType
});
}
getMatrix({
scale,
translatePercent,
rotate,
shear,
width,
height
}) {
const centerX = width / 2;
const centerY = height / 2;
// From http://scikit-image.org/docs/dev/api/skimage.transform.html#skimage.transform.AffineTransform
const rot = rotate / 180 * Math.PI;
const shr = shear / 180 * Math.PI;
const a0 = scale[0] * Math.cos(rot);
const a1 = -1 * scale[1] * Math.sin(rot + shr);
const a2Base = (translatePercent[0] * width);
const a2 = a2Base - (a0 * centerX) - (a1 * centerY) + centerX;
const b0 = scale[0] * Math.sin(rot);
const b1 = scale[1] * Math.cos(rot + shr);
const b2Base = (translatePercent[1] * height);
const b2 = b2Base - (b0 * centerY) - (b1 * centerX) + centerY;
return this.backend.floatMatrix([[a0, a1, a2], [b0, b1, b2]]);
}
augmentImage({width, height, image}, {
scale,
translatePercent,
rotate,
shear,
borderValue,
borderType
}) {
const affineMatrix = this.getMatrix({
scale,
translatePercent,
rotate,
shear,
width,
height
});
const res = this.backend.affine(image, {
affineMatrix,
size: [width, height],
borderType,
borderValue
});
this.backend.dispose(affineMatrix);
return res;
}
augmentPoints({points, width, height}, attr) {
const affineMatrix = this.getMatrix(Object.assign({}, attr, {width, height}));
const transformPoint = point => {
const p3 = this.backend.point3(point.x, point.y, 1);
const res = this.backend.matMul(affineMatrix, p3, affineMatrix.type);
const p2 = this.backend.pointFromMat(res);
const res2 = this.backend.pointToArray(p2);
this.backend.dispose(res);
return res2;
};
const res = points.map(transformPoint);
this.backend.dispose(affineMatrix);
return res;
}
}
module.exports = AffineTransformAugmenter;