image-js
Version:
Image processing and manipulation in JavaScript
132 lines (117 loc) • 3.63 kB
text/typescript
import type { Image } from '../Image.js';
import { getClamp } from '../utils/clamp.js';
import { getOutputImage } from '../utils/getOutputImage.js';
import checkProcessable from '../utils/validators/checkProcessable.js';
import { validateChannels } from '../utils/validators/validators.js';
export interface LevelOptions {
/**
* Specify which channels should be processed. To process the alpha as well,
* specify the channels as [0,1,2,3] for RGBA images and [0,1] for GREYA.
* @default All components of the image the image (no alpha).
*/
channels?: number[];
/**
* Input lower bound.
* @default `image.minMax().min`
*/
inputMin?: number | number[];
/**
* Input upper bound.
* @default `image.minMax().min`
*/
inputMax?: number | number[];
/**
* output lower bound.
* @default `0`
*/
outputMin?: number | number[];
/**
* Output upper bound.
* @default `image.maxValue`
*/
outputMax?: number | number[];
/**
* Specifies the shape of the curve connecting the two points.
* @default `1`
*/
gamma?: number | number[];
/**
* Image to which to output.
*/
out?: Image;
}
/**
* Level the image using the optional input and output value. The default options allow to increase the image's contrast.
* @param image - Image to process.
* @param options - Level options.
* @returns The levelled image.
*/
export function level(image: Image, options: LevelOptions = {}) {
const minMax = image.minMax();
let {
inputMin = minMax.min,
inputMax = minMax.max,
outputMin = 0,
outputMax = image.maxValue,
gamma = 1,
} = options;
const {
channels = new Array(image.components).fill(0).map((value, index) => index),
} = options;
validateChannels(channels, image);
checkProcessable(image, {
bitDepth: [8, 16],
});
const newImage = getOutputImage(image, options, { clone: true });
const clamp = getClamp(image);
inputMin = getValueArray(inputMin, image.channels);
inputMax = getValueArray(inputMax, image.channels);
outputMin = getValueArray(outputMin, image.channels);
outputMax = getValueArray(outputMax, image.channels);
gamma = getValueArray(gamma, image.channels);
for (let row = 0; row < image.height; row++) {
for (let column = 0; column < image.width; column++) {
for (const channel of channels) {
const currentValue = image.getValue(column, row, channel);
const clamped = Math.max(
Math.min(currentValue, inputMax[channel]),
inputMin[channel],
);
let ratio = clamp(
(clamped - inputMin[channel]) /
(inputMax[channel] - inputMin[channel]),
);
if (Number.isNaN(ratio)) ratio = 0;
const result = clamp(
ratio ** (1 / gamma[channel]) *
(outputMax[channel] - outputMin[channel]) +
outputMin[channel],
);
newImage.setValue(column, row, channel, result);
}
}
}
return newImage;
}
/**
* Get an array with correct values for each channel to process.
* @param value - Number or array to transform to the final array.
* @param imageChannels - Number of channels processed in the level function.
* @returns Array of values for each channel.
*/
function getValueArray(
value: number | number[],
imageChannels: number,
): number[] {
if (Array.isArray(value)) {
if (value.length === imageChannels) {
return value;
} else {
throw new RangeError(
'array length is not compatible with channel option',
);
}
} else {
return new Array(imageChannels).fill(value);
}
}