image-js
Version:
Image processing and manipulation in JavaScript
1,254 lines (1,145 loc) • 34.3 kB
text/typescript
import type { RgbColor } from 'colord';
import { match } from 'ts-pattern';
import type { Mask } from './Mask.js';
import type { DivideOptions } from './compare/divide.js';
import { divide } from './compare/divide.js';
import type { SubtractImageOptions } from './compare/index.js';
import { add, subtract } from './compare/index.js';
import type { MultiplyOptions } from './compare/multiply.js';
import { multiply } from './compare/multiply.js';
import type {
HistogramOptions,
MeanOptions,
MedianOptions,
VarianceOptions,
} from './compute/index.js';
import { histogram, mean, median, variance } from './compute/index.js';
import { correctColor } from './correctColor/index.js';
import type {
DrawCircleOnImageOptions,
DrawLineOnImageOptions,
DrawMarkerOptions,
DrawPointsOptions,
DrawPolygonOnImageOptions,
DrawPolylineOnImageOptions,
DrawRectangleOptions,
} from './draw/index.js';
import {
drawCircleOnImage,
drawLineOnImage,
drawMarker,
drawMarkers,
drawPoints,
drawPolygonOnImage,
drawPolylineOnImage,
drawRectangle,
} from './draw/index.js';
import type {
BlurOptions,
ConvolutionOptions,
DerivativeFilterOptions,
FlipOptions,
GaussianBlurOptions,
GradientFilterOptions,
HypotenuseOptions,
IncreaseContrastOptions,
InvertOptions,
LevelOptions,
MedianFilterOptions,
PixelateOptions,
} from './filters/index.js';
import {
blur,
derivativeFilter,
directConvolution,
flip,
gaussianBlur,
gradientFilter,
hypotenuse,
increaseContrast,
invert,
level,
medianFilter,
pixelate,
rawDirectConvolution,
separableConvolution,
} from './filters/index.js';
import type {
Point,
ResizeOptions,
RotateAngle,
TransformOptions,
TransformRotateOptions,
} from './geometry/index.js';
import {
resize,
rotate,
transform,
transformRotate,
} from './geometry/index.js';
import type { ImageMetadata, Resolution } from './load/load.types.js';
import type {
BottomHatOptions,
CannyEdgeOptions,
CloseOptions,
DilateOptions,
ErodeOptions,
MorphologicalGradientOptions,
OpenOptions,
TopHatOptions,
} from './morphology/index.js';
import {
bottomHat,
cannyEdgeDetector,
close,
dilate,
erode,
morphologicalGradient,
open,
topHat,
} from './morphology/index.js';
import type {
ConvertBitDepthOptions,
ConvertColorOptions,
CopyToOptions,
CropAlphaOptions,
CropOptions,
CropRectangleOptions,
ExtractOptions,
GreyOptions,
PaintMaskOnImageOptions,
ThresholdOptions,
} from './operations/index.js';
import {
convertBitDepth,
convertColor,
copyTo,
crop,
cropAlpha,
cropRectangle,
extract,
grey,
paintMaskOnImage,
split,
threshold,
} from './operations/index.js';
import type { ImageColorModel } from './utils/constants/colorModels.js';
import { colorModels } from './utils/constants/colorModels.js';
import { getMinMax } from './utils/getMinMax.js';
import {
validateChannel,
validateValue,
} from './utils/validators/validators.js';
export type ImageDataArray = Uint8Array | Uint16Array | Uint8ClampedArray;
/**
* Bit depth of the image (nb of bits that encode each value in the image).
*/
export type BitDepth = 1 | 8 | 16;
export const ImageCoordinates = {
CENTER: 'center',
TOP_LEFT: 'top-left',
TOP_RIGHT: 'top-right',
BOTTOM_LEFT: 'bottom-left',
BOTTOM_RIGHT: 'bottom-right',
} as const;
export type ImageCoordinates =
(typeof ImageCoordinates)[keyof typeof ImageCoordinates];
export interface ImageOptions {
/**
* Number of bits per value in each channel.
* @default `8`.
*/
bitDepth?: BitDepth;
/**
* Typed array holding the image data.
*/
data?: ImageDataArray;
/**
* Color model of the created image.
* @default `'RGB'`.
*/
colorModel?: ImageColorModel;
/**
* Origin of the image relative to a parent image (top-left corner).
* @default `{row: 0, column: 0}`
*/
origin?: Point;
/**
* Original resolution decoded from the image.
*/
resolution?: Resolution;
meta?: ImageMetadata;
}
export interface CreateFromOptions extends ImageOptions {
width?: number;
height?: number;
}
export type ImageValues = [number, number, number, number];
export class Image {
/**
* The number of columns of the image.
*/
public readonly width: number;
/**
* The number of rows of the image.
*/
public readonly height: number;
/**
* The total number of pixels in the image (width × height).
*/
public readonly size: number;
/**
* The number of bits per value in each channel.
*/
public readonly bitDepth: BitDepth;
/**
* The color model of the image.
*/
public readonly colorModel: ImageColorModel;
/**
* The number of color channels in the image, excluding the alpha channel.
* A GREY image has 1 component. An RGB image has 3 components.
*/
public readonly components: number;
/**
* The total number of channels in the image, including the alpha channel.
*/
public readonly channels: number;
/**
* Whether the image has an alpha channel or not.
*/
public readonly alpha: boolean;
/**
* The maximum value that a pixel channel can have.
*/
public readonly maxValue: number;
/**
* Origin of the image relative to a the parent image.
*/
public readonly origin: Point;
/**
* Original image resolution.
*/
public readonly originalResolution: Resolution | undefined;
public readonly meta?: ImageMetadata;
/**
* Typed array holding the image data.
*/
private readonly data: ImageDataArray;
/**
* Construct a new Image knowing its dimensions.
* @param width - Image width.
* @param height - Image height.
* @param options - Image options.
*/
public constructor(
width: number,
height: number,
options: ImageOptions = {},
) {
const {
bitDepth = 8,
data,
colorModel = 'RGB',
origin = { row: 0, column: 0 },
meta,
resolution,
} = options;
if (width < 1 || !Number.isInteger(width)) {
throw new RangeError(
`width must be an integer and at least 1. Received ${width}`,
);
}
if (height < 1 || !Number.isInteger(height)) {
throw new RangeError(
`height must be an integer and at least 1. Received ${height}`,
);
}
this.width = width;
this.height = height;
this.size = width * height;
this.bitDepth = bitDepth;
this.colorModel = colorModel;
this.origin = origin;
this.meta = meta;
this.originalResolution = resolution;
const colorModelDef = colorModels[colorModel];
this.components = colorModelDef.components;
this.alpha = colorModelDef.alpha;
this.channels = colorModelDef.channels;
this.maxValue = 2 ** bitDepth - 1;
if (data === undefined) {
this.data = createPixelArray(
this.size,
this.channels,
this.alpha,
this.bitDepth,
this.maxValue,
);
} else {
if (bitDepth === 8 && data instanceof Uint16Array) {
throw new RangeError(`bitDepth is ${bitDepth} but data is Uint16Array`);
} else if (bitDepth === 16 && data instanceof Uint8Array) {
throw new RangeError(`bitDepth is ${bitDepth} but data is Uint8Array`);
}
const expectedLength = this.size * this.channels;
if (data.length !== expectedLength) {
throw new RangeError(
`incorrect data size: ${data.length}. Expected ${expectedLength}`,
);
}
this.data = data;
}
}
/**
* Returns normalized resolution in pixels per centimeter. If resolution unit is unknown, return null.
* @returns Object with x and y resolutions in pixel/cm.
*/
get normalizedResolution() {
if (!this.originalResolution) {
return undefined;
}
const centimetersPerInch = 2.54;
const centimetersPerMeter = 100;
switch (this.originalResolution.unit) {
case 'inch':
return {
x: this.originalResolution.x / centimetersPerInch,
y: this.originalResolution.y / centimetersPerInch,
};
case 'centimeter':
return {
x: this.originalResolution.x,
y: this.originalResolution.y,
};
case 'meter':
return {
x: this.originalResolution.x / centimetersPerMeter,
y: this.originalResolution.y / centimetersPerMeter,
};
case 'unknown':
return null;
default:
throw new Error('Unknown resolution unit.');
}
}
/**
* Create a new Image based on the properties of an existing one.
* @param other - Reference image.
* @param options - Image options.
* @returns New image.
*/
public static createFrom(
other: Image | Mask,
options: CreateFromOptions = {},
): Image {
const { width = other.width, height = other.height } = options;
let bitDepth: BitDepth;
if (other instanceof Image) {
bitDepth = other.bitDepth;
} else {
bitDepth = 8;
}
return new Image(width, height, {
bitDepth,
colorModel: other.colorModel,
origin: other.origin,
...options,
});
}
/**
* Get all the channels of a pixel.
* @param column - Column index.
* @param row - Row index.
* @returns Channels of the pixel.
*/
public getPixel(column: number, row: number): number[] {
const result = [];
const start = (row * this.width + column) * this.channels;
for (let i = 0; i < this.channels; i++) {
result.push(this.data[start + i]);
}
return result;
}
public getColumn(column: number): number[][] {
const columnValues = [];
for (let i = 0; i < this.channels; i++) {
const channelValues = [];
for (let j = 0; j < this.height; j++) {
channelValues.push(this.getValue(column, j, i));
}
columnValues.push(channelValues);
}
return columnValues;
}
public getRow(row: number): number[][] {
const rowValues = [];
for (let i = 0; i < this.channels; i++) {
const channelValues = [];
for (let j = 0; j < this.width; j++) {
channelValues.push(this.getValue(j, row, i));
}
rowValues.push(channelValues);
}
return rowValues;
}
/**
* Set all the channels of a pixel.
* @param column - Column index.
* @param row - Row index.
* @param value - New color of the pixel to set.
*/
public setPixel(column: number, row: number, value: number[]): void {
const start = (row * this.width + column) * this.channels;
for (let i = 0; i < this.channels; i++) {
this.data[start + i] = value[i];
}
}
/**
* Set all the channels of a pixel if the coordinates are inside the image.
* @param column - Column index.
* @param row - Row index.
* @param value - New color of the pixel to set.
*/
public setVisiblePixel(column: number, row: number, value: number[]): void {
if (column >= 0 && column < this.width && row >= 0 && row < this.height) {
this.setPixel(column, row, value);
}
}
/**
* Get all the channels of a pixel using its index.
* @param index - Index of the pixel.
* @returns Channels of the pixel.
*/
public getPixelByIndex(index: number): number[] {
const result = [];
const start = index * this.channels;
for (let i = 0; i < this.channels; i++) {
result.push(this.data[start + i]);
}
return result;
}
/**
* Set all the channels of a pixel using its index.
* @param index - Index of the pixel.
* @param value - New channel values of the pixel to set.
*/
public setPixelByIndex(index: number, value: number[]): void {
const start = index * this.channels;
for (let i = 0; i < this.channels; i++) {
this.data[start + i] = value[i];
}
}
/**
* Get the value of a specific pixel channel. Select pixel using coordinates.
* @param column - Column index.
* @param row - Row index.
* @param channel - Channel index.
* @returns Value of the specified channel of one pixel.
*/
public getValue(column: number, row: number, channel: number): number {
return this.data[(row * this.width + column) * this.channels + channel];
}
/**
* Set the value of a specific pixel channel. Select pixel using coordinates.
* @param column - Column index.
* @param row - Row index.
* @param channel - Channel index.
* @param value - Value to set.
*/
public setValue(
column: number,
row: number,
channel: number,
value: number,
): void {
this.data[(row * this.width + column) * this.channels + channel] = value;
}
/**
* Set the value of a specific pixel channel. Select pixel using coordinates.
* If the value is out of range it is set to the closest extremety.
* @param column - Column index.
* @param row - Row index.
* @param channel - Channel index.
* @param value - Value to set.
*/
public setClampedValue(
column: number,
row: number,
channel: number,
value: number,
): void {
if (value < 0) value = 0;
else if (value > this.maxValue) value = this.maxValue;
this.data[(row * this.width + column) * this.channels + channel] = value;
}
/**
* Get the value of a specific pixel channel. Select pixel using index.
* @param index - Index of the pixel.
* @param channel - Channel index.
* @returns Value of the channel of the pixel.
*/
public getValueByIndex(index: number, channel: number): number {
return this.data[index * this.channels + channel];
}
/**
* Set the value of a specific pixel channel. Select pixel using index.
* @param index - Index of the pixel.
* @param channel - Channel index.
* @param value - Value to set.
*/
public setValueByIndex(index: number, channel: number, value: number): void {
this.data[index * this.channels + channel] = value;
}
/**
* Set the value of a specific pixel channel. Select pixel using index.
* If the value is out of range it is set to the closest extremety.
* @param index - Index of the pixel.
* @param channel - Channel index.
* @param value - Value to set.
*/
public setClampedValueByIndex(
index: number,
channel: number,
value: number,
): void {
if (value < 0) value = 0;
else if (value > this.maxValue) value = this.maxValue;
this.data[index * this.channels + channel] = value;
}
/**
* Get the value of a specific pixel channel. Select pixel using a point.
* @param point - Coordinates of the desired pixel.
* @param channel - Channel index.
* @returns Value of the channel of the pixel.
*/
public getValueByPoint(point: Point, channel: number): number {
return this.getValue(point.column, point.row, channel);
}
/**
* Set the value of a specific pixel channel. Select pixel using a point.
* @param point - Coordinates of the pixel.
* @param channel - Channel index.
* @param value - Value to set.
*/
public setValueByPoint(point: Point, channel: number, value: number): void {
this.setValue(point.column, point.row, channel, value);
}
/**
* Find the min and max values of each channel of the image.
* @returns An object with arrays of the min and max values.
*/
public minMax(): { min: number[]; max: number[] } {
return getMinMax(this);
}
/**
* Return the raw image data.
* @returns The raw data.
*/
public getRawImage() {
return {
width: this.width,
height: this.height,
data: this.data,
channels: this.channels,
bitDepth: this.bitDepth,
};
}
public [Symbol.for('nodejs.util.inspect.custom')](): string {
let dataString;
if (this.height > 20 || this.width > 20) {
dataString = '[...]';
} else {
dataString = printData(this);
}
return `Image {
width: ${this.width}
height: ${this.height}
bitDepth: ${this.bitDepth}
colorModel: ${this.colorModel}
channels: ${this.channels}
data: ${dataString}
}`;
}
/**
* Fill the image with a value or a color.
* @param value - Value or color.
* @returns The image instance.
*/
public fill(value: number | number[]): this {
if (typeof value === 'number') {
validateValue(value, this);
this.data.fill(value);
return this;
} else {
if (value.length !== this.channels) {
throw new RangeError(
`the size of value must match the number of channels (${this.channels}). Received ${value.length}`,
);
}
for (const val of value) validateValue(val, this);
for (let i = 0; i < this.data.length; i += this.channels) {
for (let j = 0; j <= this.channels; j++) {
this.data[i + j] = value[j];
}
}
return this;
}
}
/**
* Fill one channel with a value.
* @param channel - The channel to fill.
* @param value - The new value.
* @returns The image instance.
*/
public fillChannel(channel: number, value: number): this {
validateChannel(channel, this);
validateValue(value, this);
for (let i = channel; i < this.data.length; i += this.channels) {
this.data[i] = value;
}
return this;
}
/**
* Get one channel of the image as an array.
* @param channel - The channel to fill.
* @returns Array with the channel values.
*/
public getChannel(channel: number): number[] {
validateChannel(channel, this);
const result = new Array(this.size);
for (let i = 0; i < this.size; i++) {
result[i] = this.data[channel + i * this.channels];
}
return result;
}
/**
* Fill the alpha channel with the specified value.
* @param value - New channel value.
* @returns The image instance.
*/
public fillAlpha(value: number): this {
validateValue(value, this);
if (!this.alpha) {
throw new TypeError(
'fillAlpha can only be called if the image has an alpha channel',
);
}
const alphaIndex = this.channels - 1;
return this.fillChannel(alphaIndex, value);
}
/**
* Create a copy of this image.
* @returns The image clone.
*/
public clone(): Image {
return Image.createFrom(this, { data: this.data.slice() });
}
/**
* Modify all the values of the image using the given callback.
* @param cb - Callback that modifies a given value.
*/
public changeEach(cb: (value: number) => number): void {
for (let i = 0; i < this.data.length; i++) {
this.data[i] = cb(this.data[i]);
}
}
/**
* Get the coordinates of a point in the image. The reference is the top-left corner.
* @param coordinates - The point for which you want the coordinates.
* @param round - Whether the coordinates should be rounded. This is useful when you want the center of the image.
* @returns Coordinates of the point in the format [column, row].
*/
public getCoordinates(coordinates: ImageCoordinates, round = false): Point {
return match(coordinates)
.with('center', () => {
const centerX = (this.width - 1) / 2;
const centerY = (this.height - 1) / 2;
if (round) {
return { column: Math.round(centerX), row: Math.round(centerY) };
} else {
return { column: centerX, row: centerY };
}
})
.with('top-left', () => ({ column: 0, row: 0 }))
.with('top-right', () => ({ column: this.width - 1, row: 0 }))
.with('bottom-left', () => ({ column: 0, row: this.height - 1 }))
.with('bottom-right', () => ({
column: this.width - 1,
row: this.height - 1,
}))
.exhaustive();
}
// COMPARE
/**
* Subtract other from an image.
* @param other - Image to subtract.
* @param options - Inversion options.
* @returns The subtracted image.
*/
public subtract(other: Image, options: SubtractImageOptions = {}): Image {
return subtract(this, other, options);
}
public add(other: Image): Image {
return add(this, other);
}
/**
* Multiply image pixels by a constant.
* @param value - Value which pixels will be multiplied to.
* @param options - Multiply options.
* @returns Multiplied image.
*/
public multiply(value: number, options: MultiplyOptions = {}): Image {
return multiply(this, value, options);
}
/**
* Divide image pixels by a constant.
* @param value - Value which pixels will be divided to.
* @param options - Divide options.
* @returns Divided image.
*/
public divide(value: number, options: DivideOptions = {}): Image {
return divide(this, value, options);
}
// COMPUTE
public histogram(options?: HistogramOptions): Uint32Array {
return histogram(this, options);
}
/**
* Compute the mean pixel of an image.
* @param options - Mean options.
* @returns The mean pixel.
*/
public mean(options?: MeanOptions): number[] {
return mean(this, options);
}
/**
* Compute the median pixel of an image.
* @param options - Median options.
* @returns The median pixel.
*/
public median(options?: MedianOptions): number[] {
return median(this, options);
}
/**
* Compute the variance of each channel of an image.
* @param options - Variance options.
* @returns The variance of the channels of the image.
*/
public variance(options?: VarianceOptions): number[] {
return variance(this, options);
}
// DRAW
/**
* Draw a set of points on an image.
* @param points - Array of points.
* @param options - Draw points on Image options.
* @returns New mask.
*/
public drawPoints(points: Point[], options: DrawPointsOptions = {}): Image {
return drawPoints(this, points, options);
}
/**
* Draw a line defined by two points onto an image.
* @param from - Line starting point.
* @param to - Line ending point.
* @param options - Draw Line options.
* @returns The mask with the line drawing.
*/
public drawLine(
from: Point,
to: Point,
options: DrawLineOnImageOptions = {},
): Image {
return drawLineOnImage(this, from, to, options);
}
/**
* Draw a rectangle defined by position of the top-left corner, width and height.
* @param options - Draw rectangle options.
* @returns The image with the rectangle drawing.
*/
public drawRectangle(options: DrawRectangleOptions<Image> = {}): Image {
return drawRectangle(this, options);
}
/**
* Draw a polyline defined by an array of points on an image.
* @param points - Polyline array of points.
* @param options - Draw polyline options.
* @returns The image with the polyline drawing.
*/
public drawPolyline(
points: Point[],
options: DrawPolylineOnImageOptions = {},
): Image {
return drawPolylineOnImage(this, points, options);
}
/**
* Draw a polygon defined by an array of points onto an image.
* @param points - Polygon vertices.
* @param options - Draw Line options.
* @returns The image with the polygon drawing.
*/
public drawPolygon(
points: Point[],
options: DrawPolygonOnImageOptions = {},
): Image {
return drawPolygonOnImage(this, points, options);
}
/**
* Draw a circle defined by center and radius onto an image.
* @param center - Circle center.
* @param radius - Circle radius.
* @param options - Draw circle options.
* @returns The image with the circle drawing.
*/
public drawCircle(
center: Point,
radius: number,
options: DrawCircleOnImageOptions = {},
): Image {
return drawCircleOnImage(this, center, radius, options);
}
/**
* Draw a marker on the image.
* @param point - Marker center point.
* @param options - Draw marker options.
* @returns The image with the marker drawing.
*/
public drawMarker(point: Point, options: DrawMarkerOptions = {}): Image {
return drawMarker(this, point, options);
}
/**
* Draw markers on the image.
* @param points - Markers center points.
* @param options - Draw marker options.
* @returns The image with the markers drawing.
*/
public drawMarkers(points: Point[], options: DrawMarkerOptions = {}): Image {
return drawMarkers(this, points, options);
}
// OPERATIONS
public split(): Image[] {
return split(this);
}
public convertColor(
colorModel: ImageColorModel,
options?: ConvertColorOptions,
): Image {
return convertColor(this, colorModel, options);
}
public convertBitDepth(
newDepth: BitDepth,
options?: ConvertBitDepthOptions,
): Image {
return convertBitDepth(this, newDepth, options);
}
public grey(options?: GreyOptions): Image {
return grey(this, options);
}
public copyTo(target: Image, options: CopyToOptions<Image> = {}): Image {
return copyTo(this, target, options);
}
public threshold(options: ThresholdOptions = {}): Mask {
return threshold(this, options);
}
/**
* Crop the input image to a desired size.
* @param [options] - Crop options.
* @returns The new cropped image.
*/
public crop(options?: CropOptions): Image {
return crop(this, options);
}
/**
* Crop an oriented rectangle from the image.
* If the rectangle's length or width are not an integers, its dimension is expanded in both directions such as the length and width are integers.
* @param points - The points of the rectangle. Points must be circling around the rectangle (clockwise or anti-clockwise)
* @param options - Crop options, see {@link CropRectangleOptions}
* @returns The cropped image. The orientation of the image is the one closest to the rectangle passed as input.
*/
public cropRectangle(points: Point[], options?: CropRectangleOptions) {
return cropRectangle(this, points, options);
}
/**
* Crops the image based on the alpha channel
* This removes lines and columns where the alpha channel is lower than a threshold value.
* @param options - Crop alpha options.
* @returns The cropped image.
*/
public cropAlpha(options: CropAlphaOptions = {}): Image {
return cropAlpha(this, options);
}
/**
* Extract the pixels of an image, as specified in a mask.
* @param mask - The mask defining which pixels to keep.
* @param options - Extract options.
* @returns The extracted image.
*/
public extract(mask: Mask, options?: ExtractOptions): Image {
return extract(this, mask, options);
}
/**
* Paint a mask onto an image and the given position and with the given color.
* @param mask - Mask to paint on the image.
* @param options - Paint mask options.
* @returns The painted image.
*/
public paintMask(mask: Mask, options?: PaintMaskOnImageOptions): Image {
return paintMaskOnImage(this, mask, options);
}
// FILTERS
public blur(options: BlurOptions): Image {
return blur(this, options);
}
public pixelate(options: PixelateOptions): Image {
return pixelate(this, options);
}
public directConvolution(
kernel: number[][],
options?: ConvolutionOptions,
): Image {
return directConvolution(this, kernel, options);
}
/**
* Compute direct convolution of an image and return an array with the raw values.
* @param kernel - Kernel used for the convolution.
* @param options - Convolution options.
* @returns Array with the raw convoluted values.
*/
public rawDirectConvolution(
kernel: number[][],
options?: ConvolutionOptions,
): Float64Array {
return rawDirectConvolution(this, kernel, options);
}
public separableConvolution(
kernelX: number[],
kernelY: number[],
options?: ConvolutionOptions,
): Image {
return separableConvolution(this, kernelX, kernelY, options);
}
/**
* Apply a gaussian filter to an image.
* @param options - Gaussian blur options.
* @returns The blurred image.
*/
public gaussianBlur(options: GaussianBlurOptions): Image {
return gaussianBlur(this, options);
}
/**
* Flip the image.
* @param options - Flip options.
* @returns The flipped image.
*/
public flip(options?: FlipOptions): Image {
return flip(this, options);
}
/**
* Invert the colors of the image.
* @param options - Inversion options.
* @returns The inverted image.
*/
public invert(options?: InvertOptions): Image {
return invert(this, options);
}
/**
* Calculate a new image that is the hypotenuse between the current image and the other.
* @param other - Other image.
* @param options - Hypotenuse options.
* @returns Hypotenuse of the two images.
*/
public hypotenuse(other: Image, options?: HypotenuseOptions): Image {
return hypotenuse(this, other, options);
}
/**
* Apply a gradient filter to an image.
* @param options - Gradient filter options.
* @returns The gradient image.
*/
public gradientFilter(options: GradientFilterOptions): Image {
return gradientFilter(this, options);
}
/**
* Apply a derivative filter to an image.
* @param options - Derivative filter options.
* @returns The processed image.
*/
public derivativeFilter(options?: DerivativeFilterOptions): Image {
return derivativeFilter(this, options);
}
/**
* Level the image using the optional input and output value. This function allows you to enhance the image's contrast.
* @param options - Level options.
* @returns The levelled image.
*/
public level(options?: LevelOptions): Image {
return level(this, options);
}
/**
* Increase the contrast of an image by spanning each channel on the range [0, image.maxValue].
* @param options - Increase contrast options.
* @returns The enhanced image.
*/
public increaseContrast(options: IncreaseContrastOptions = {}): Image {
return increaseContrast(this, options);
}
/**
* Correct the colors in an image using the reference colors.
* @param measuredColors - Colors from the image, which will be compared to the reference.
* @param referenceColors - Reference colors.
* @returns Image with the colors corrected.
*/
public correctColor(
measuredColors: RgbColor[],
referenceColors: RgbColor[],
): Image {
return correctColor(this, measuredColors, referenceColors);
}
/**
* Apply a median filter to the image.
* @param options - Options to apply for median filter.
* @returns Image after median filter.
*/
public medianFilter(options?: MedianFilterOptions) {
return medianFilter(this, options);
}
// GEOMETRY
public resize(options: ResizeOptions): Image {
return resize(this, options);
}
public rotate(angle: RotateAngle): Image {
return rotate(this, angle);
}
public transform(
transformMatrix: number[][],
options?: TransformOptions,
): Image {
return transform(this, transformMatrix, options);
}
public transformRotate(
angle: number,
options?: TransformRotateOptions,
): Image {
return transformRotate(this, angle, options);
}
// MORPHOLOGY
/**
* Erode an image.
* @param options - Erode options.
* @returns The eroded image.
*/
public erode(options?: ErodeOptions): Image {
return erode(this, options);
}
/**
* Dilate an image.
* @param options - Dilate options.
* @returns The dilated image.
*/
public dilate(options?: DilateOptions): Image {
return dilate(this, options);
}
/**
* Open an image.
* @param options - Open options.
* @returns The opened image.
*/
public open(options?: OpenOptions): Image {
return open(this, options);
}
/**
* Close an image.
* @param options - Close options.
* @returns The closed image.
*/
public close(options?: CloseOptions): Image {
return close(this, options);
}
/**
* Top hat of an image.
* @param options - Top hat options.
* @returns The top-hatted image.
*/
public topHat(options?: TopHatOptions): Image {
return topHat(this, options);
}
/**
* Bottom hat of an image.
* @param options - Bottom hat options.
* @returns The bottom-hatted image.
*/
public bottomHat(options?: BottomHatOptions): Image {
return bottomHat(this, options);
}
/**
* Apply morphological gradient to an image.
* @param options - Morphological gradient options.
* @returns The processed image.
*/
public morphologicalGradient(options?: MorphologicalGradientOptions): Image {
return morphologicalGradient(this, options);
}
/**
* Apply Canny edge detection to an image.
* @param options - Canny edge detection options.
* @returns The processed image.
*/
public cannyEdgeDetector(options?: CannyEdgeOptions): Mask {
return cannyEdgeDetector(this, options);
}
}
/**
* Create data array and set alpha channel to max value if applicable.
* @param size - Number of pixels.
* @param channels - Number of channels.
* @param alpha - Specify if there is alpha channel.
* @param bitDepth - Number of bits per channel.
* @param maxValue - Maximal acceptable value for the channels.
* @returns The new pixel array.
*/
function createPixelArray(
size: number,
channels: number,
alpha: boolean,
bitDepth: BitDepth,
maxValue: number,
): ImageDataArray {
const length = channels * size;
const arr = match(bitDepth)
.with(8, () => new Uint8Array(length))
.with(16, () => new Uint16Array(length))
.otherwise(() => {
throw new RangeError(`invalid bitDepth: ${bitDepth}`);
});
// Alpha channel is 100% by default.
if (alpha) {
for (let i = channels - 1; i < length; i += channels) {
arr[i] = maxValue;
}
}
return arr;
}
/**
* Returns the image data as a formatted string.
* @param img - The image instance.
* @returns Formatted string containing the image data.
*/
function printData(img: Image): string {
const result = [];
const padding = img.bitDepth === 8 ? 3 : 5;
for (let row = 0; row < img.height; row++) {
const currentRow = [];
for (let column = 0; column < img.width; column++) {
for (let channel = 0; channel < img.channels; channel++) {
currentRow.push(
String(img.getValue(column, row, channel)).padStart(padding, ' '),
);
}
}
result.push(`[${currentRow.join(' ')}]`);
}
return `{
[\n ${result.join('\n ')}\n ]
}`;
}