image-js
Version:
Image processing and manipulation in JavaScript
219 lines (200 loc) • 5.86 kB
text/typescript
import { Matrix, inverse } from 'ml-matrix';
import { Image } from '../Image.js';
import { getClamp } from '../utils/clamp.js';
import { getDefaultColor } from '../utils/getDefaultColor.ts';
import type { BorderType } from '../utils/interpolateBorder.js';
import { getBorderInterpolation } from '../utils/interpolateBorder.js';
import type { InterpolationType } from '../utils/interpolatePixel.js';
import { getInterpolationFunction } from '../utils/interpolatePixel.js';
import { validateColor } from '../utils/validators/validators.ts';
export interface TransformOptions {
/**
* Width of the output image.
*/
width?: number;
/**
* Height of the output image.
*/
height?: number;
/**
* Method to use to interpolate the new pixels.
* @default `'bilinear'`
*/
interpolationType?: InterpolationType;
/**
* Specify how the borders should be handled.
* @default `'constant'`
*/
borderType?: BorderType;
/**
* Value of the border if BorderType is 'constant'.
* @default `0`
*/
borderValue?: number | number[];
/**
* Whether the transform matrix should be inverted.
*/
inverse?: boolean;
/**
* Bypasses width and height options to include
* every pixel of the original image inside the transformed image.
*/
fullImage?: boolean;
}
/**
* Transforms an image using a matrix.
* @param image - Original image.
* @param transformMatrix - 2×3 transform matrix.
* @param options - Transform options.
* @returns The new image.
*/
export function transform(
image: Image,
transformMatrix: number[][],
options: TransformOptions = {},
): Image {
const {
borderValue = getDefaultColor(image),
borderType = 'constant',
interpolationType = 'bilinear',
fullImage,
} = options;
let { width = image.width, height = image.height } = options;
if (Array.isArray(borderValue)) {
validateColor(borderValue, image);
}
if (!isValidMatrix(transformMatrix)) {
throw new TypeError(
`transformation matrix must be 2x3 or 3x3. Received ${transformMatrix.length}x${transformMatrix[1].length}`,
);
}
if (transformMatrix.length === 2) {
transformMatrix.push([0, 0, 1]);
}
if (fullImage) {
transformMatrix = transformMatrix.map((row) => row.slice());
transformMatrix[0][2] = 0;
transformMatrix[1][2] = 0;
const corners = [
image.getCoordinates('top-left'),
image.getCoordinates('top-right'),
image.getCoordinates('bottom-right'),
image.getCoordinates('bottom-left'),
];
corners[1].column += 1;
corners[2].column += 1;
corners[2].row += 1;
corners[3].row += 1;
const transformedCorners = corners.map((corner) => {
return [
transformPoint(
transformMatrix[0],
transformMatrix[2],
corner.column,
corner.row,
),
transformPoint(
transformMatrix[1],
transformMatrix[2],
corner.column,
corner.row,
),
];
});
const xCoordinates = transformedCorners.map((c) => c[0]);
const yCoordinates = transformedCorners.map((c) => c[1]);
const maxX = Math.max(...xCoordinates);
const maxY = Math.max(...yCoordinates);
const minX = Math.min(...xCoordinates);
const minY = Math.min(...yCoordinates);
const center = [(image.width - 1) / 2, (image.height - 1) / 2];
width = maxX - minX;
height = maxY - minY;
const centerX = transformPoint(
transformMatrix[0],
transformMatrix[2],
center[0],
center[1],
);
const centerY = transformPoint(
transformMatrix[1],
transformMatrix[2],
center[0],
center[1],
);
const a = (width - 1) / 2 - centerX;
const b = (height - 1) / 2 - centerY;
transformMatrix[0][2] = a;
transformMatrix[1][2] = b;
width = Math.round(width);
height = Math.round(height);
}
if (!options.inverse) {
transformMatrix = inverse(new Matrix(transformMatrix)).to2DArray();
}
const newImage = Image.createFrom(image, {
width,
height,
});
const interpolateBorder = getBorderInterpolation(borderType, borderValue);
const clamp = getClamp(newImage);
const interpolate = getInterpolationFunction(interpolationType);
for (let row = 0; row < newImage.height; row++) {
for (let column = 0; column < newImage.width; column++) {
const nx = transformPoint(
transformMatrix[0],
transformMatrix[2],
column,
row,
);
const ny = transformPoint(
transformMatrix[1],
transformMatrix[2],
column,
row,
);
for (let channel = 0; channel < newImage.channels; channel++) {
const newValue = interpolate(
image,
nx,
ny,
channel,
interpolateBorder,
clamp,
);
newImage.setValue(column, row, channel, newValue);
}
}
}
return newImage;
}
/**
* Apply a transformation to a point.
* @param transform - Transformation matrix.
* @param perspective - Perspective matrix.
* @param column - Column of the point.
* @param row - Row of the point.
* @returns New value.
*/
function transformPoint(
transform: number[],
perspective: number[],
column: number,
row: number,
): number {
return (
(transform[0] * column + transform[1] * row + transform[2]) /
(perspective[0] * column + perspective[1] * row + perspective[2])
);
}
function isValidMatrix(transformationMatrix: number[][]) {
return (
(transformationMatrix.length === 3 &&
transformationMatrix[0].length === 3 &&
transformationMatrix[1].length === 3 &&
transformationMatrix[2].length === 3) ||
(transformationMatrix.length === 2 &&
transformationMatrix[0].length === 3 &&
transformationMatrix[1].length === 3)
);
}