Personal custom library used for various image related manipulations. The name
is picture because Image would conflict with the
HTMLImageElement.
git clone git@github.com:Voldemortas/picture.git
and then use deno task's
deno task test - runs tests and creates coverage report in cov_profile/
directory
deno task build - compiles code into javascript, needed for inspecting live
example
deno task server - runs deno's file-server (needs global installation), good
for previewing docs, coverage report, inspecting live example
To add it to your dependencies use one of the following
deno add jsr:@voldemortas/picture
pnpm i jsr:@voldemortas/picture
yarn add jsr:@voldemortas/picture
npx jsr add @voldemortas/picture
bunx jsr add @voldemortas/pictureAnd import with
import Picture, {
AlphaOption,
Comparator,
Kernel,
Pixel,
TRUE_GRAY_RATIO,
} from '@voldemortas/picture'or if using browser:
import Picture, {
AlphaOption,
Comparator,
Kernel,
Pixel,
TRUE_GRAY_RATIO,
} from 'https://esm.sh/jsr/@voldemortas/picture'You can check the functionalit by visiting https://raw.githack.com/Voldemortas/picture/master/example/index.html.
You can get image data using
CanvasRenderingContext2D.getImageData()
or other means if you're doing it serverside. And them simply
//import Picture, prepare context
const imageData = context.getImageData(0, 0, width, height)
let picture = Picture.from(imageData)The Picture object/class works with 4 pixel channels: RGBA, however it
provides functionality to (de)construct it from/to RGB kind of data.
const WIDTH = 1
const HEIGHT = 2
//2 pixels, one green, another red, without alpha
const RGB = new Uint8ClampedArray([0, 255, 0, /*next pixel*/ 255, 0, 0])
//2 pixels, one green, another red, with alpha
const RGBA = new Uint8ClampedArray([0, 255, 0, 255, /*next*/ 255, 0, 0, 255])
const p1 = new Picture(WIDTH, HEIGHT, RGB)
const p2 = new Picture(WIDTH, HEIGHT, RGBA)
const p3 = Picture.From(WIDTH, HEIGHT, RGB)
const p4 = Picture.From(WIDTH, HEIGHT, RGBA)
// all pictures p1 to p4 hold the same data
const p5 = p1.toObject()
// {
// width: 1,
// height: 2,
// data: RGBA
// channels: 4,
// }
const p6 = p1.toNoAlphaObject()
// {
// width: 1,
// height: 2,
// data: RGB
// channels: 3,
// }The library provides a way to manipulate each pixel by providing a callback of type
(channels: number[], index?: number) => [number, number, number, number]
//or
(channels: number[], index?: number) => [number, number, number]In the latter case the alpha becomes 255 (or FF in hex if you may). So you
can do stuff like
//makes the red channel green, green - blue, blue - red
picture.manipulateChannels(([r, g, b]) => [g, b, r])
//makes every 4th channel black
picture.manipulateChannels(([r, g, b], i) => i % 4 !== 0 ? [r, g, b]: [0, 0, 0]))
//picks up blue pixels and make them less red and more green thus making the blue appear more gray
//without altering other pixels
picture.manipulateChannels(([r, g, b]) => (b >= r && b >= g) ? [r * 1/3 + 2/3 * g, g, b] : [r, g, b])
left - original, right - the last manipulation result
You can make the image grayscaled - black - gray - white making all the non-alpha values the same.
//uses default monochromization
picture.monochromize()
//puts more weight for blue and less weight for green channels for monochromization
//note that it's better if the values add up to 1
picture.monochromize([2 / 3, 1 / 6, 3 / 6])
//monochromizes and then makes all darker shades appear more red
picture.monochromize().manipulate(([r]) => [255, r, r])
left - original, right - the monochromized with the default values
makes the channel be capable of carrying either 0 or 255, you can pass an
optional tolerance value to change when the channel becomes black/white
picture.binarize(200) //values higher than 200 become 255, values equal or below 200 become black
left - original, right - the binarized

left - original, right - firsly monochromized, then binarized
Similar to binarization, however it lets you have more than 2 extreme values.
picture.groupColors([1, 1, 1])
//each channel can have one of the possible 3 values (0, 127, 255)
//values between 0-84 become 0
//values between 85-170 become 127
//values between 171-255 become 255
//this because 256/3-1=84
picture.groupColors([1, 2, 1])
//each channel can have one of the possible 3 values (0, 127, 255)
//values between 0-63 become 0
//values between 64-191 become 127
//values between 192-255 become 255
//this because 256/4-1=63 yet the middle ratio is twice as big
//as the other
left - original, right - firsly monochromized, then grouped with [1, 1, 1]
You can convolve the picture using custom or built in kernels.
new Array(40).forEach(() => {
picture = picture.convolve([[0, 0, 0], [1, 0, 0], [0, 0, 0]])
})
what happens if you run the code above
A static method that finds which indices are needed to be used with the Kernel as the image is rectangular yet pixels are encoded linearly.
0 1 2
3 4 5
6 7 8
The direct neighbours of the pixel 4 are [1, 3, 5, 7] and the pixel 2
only has 2 neighbours: [1, 5].
const imageWidth = 3
const imageHeight = 3
//the pixel id is 2 as from the above
const pixelX = 2
const pixelY = 0
const kernelWidth = 3
const kernelHeight = 3
const imagePixelIds = Picture.findCoordsToConvolveKernel(
[imageWidth, imageHeight],
[pixelX, pixelY],
[kernelWidth, kernelHeight],
) //[undefined, 5, 4, undefined, 2, 1, undefined, undefined, undefined]
//undefined - index does not exist0 ➀ ❷
3 ➃ ➄
6 7 8
As the pixel was ❷, the pixels in question needed for convulation are shown in black circles.
merges one picture onto another
const white = [255, 255, 255, 255]
const data = new Uint8ClampedArray(new Array(9).fill(white).flat())
const reddish = [255, 0, 0, 63]
const red = [255, 0, 0, 255]
const a = new Picture(3, 3, data)
const b = new Picture(2, 1, new Uint8ClampedArray(reddish, red).flat())
//replaces center and centre-right pixels
const c = Picture.merge2(a, b, 1, 1)
//c.data is [
//255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
//255, 255, 255, 255, 255, 192, 192, 255, 255, 0, 0, 255
//255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
//]The method divides the picture into block-sized chunks and iterates each chunk
and creates a new picture with the alpha channels only on how each chunk was
similar to the block.
const C = [0, 255, 255]
const R = [255, 0, 0]
//<! -- deno-fmt-ignore-start -->
const bigData = [
R, R, C, R, R,
R, R, C, R, R,
C, C, C, C, C,
R, R, C, R, R,
R, R, C, R, R,
]
const cross = new Picture(5, 5, new Uint8ClampedArray(bigData.flat()))
const block = new Picture(2, 1, new Uint8ClampedArray([R, R].flat()))
const mask = cross.createBlockSimilarityMask(block)
Result of cross.createBlockSimilarityMask(block)
Pixel is a static class full of helpers to manipulate a single pixel made of
[r, g, b, a?] channels.
All of the Pixel methods return a PixelCallback!
type PixelCallback = (
rgb: [number, number, number, number?],
index?: number,
) => [number, number, number, number?]same as Picture.binarizeColors()
const pixel = [0, 127, 255]
Pixel.binarizeColors(pixel, 127) //[0, 0, 255]
//as 255 > 127, 127 <= 127 and 0 <= 127same as Picture.monochromize()
const pixel = [244, 127, 63]
//only considers the red channel when monochromizing data
const rgbRatio = [1, 0, 0]
Pixel.monochromize(rgbRatio)(pixel) //[244, 244, 244]
//because the red channel was 244same as Picture.groupColors()
const pixel = [244, 127, 70]
Pixel.groupColors([1, 1, 1])(pixel) //[255, 127, 0]
Pixel.groupColors([1, 2, 1])(pixel) //[255, 127, 127]Static class with methods to compare pixel (rgba channels)
AlphaOption is an enum used to describe how alpha channels should be treated
export enum AlphaOption {
/** ignores alpha channels */
ignore = 'ignore',
/**
* cares about alpha channels and multiplies pixel's rgb channels by their transparancy
* for comparison sake
* [127, 255, 255, 127] gets treated as if it were [63, 127, 127, 255]
* applies to both pixels in comparison
*/
multiply = 'multiply',
/**
* cares about alpha channels and multiplies pixel's rgb channels by their transparancy
* for comparison sake
* [127, 255, 255, 127] gets treated as if it were [63, 127, 127, 255]
* applies to the `b` pixel only
*/
ignoreFirst = 'ignoreFirst',
/**
* cares about alpha channels, however, instead of multiplying rgb channels by their
* transparency, it just does simple `Math.abs(a[3] - b[3])`
*/
subtract = 'subtract',
}//rgba pixel
const P1 = [255, 0, 0, 63]
//pixel represented with Uint8ClampedArray
const P0 = new Uint8ClampedArray(P1)
const P2 = [0, 255, 0, 127]//255 of red + 255 of green + 0 of blue = 510; alpha channel is ignored
Comparator.comparePixels(P0, P2, AlphaOption.ignore)
//255 of red + 255 of green + 0 of blue = 510; alpha channel is ignored
Comparator.comparePixels(P2, P1, AlphaOption.ignore)
//255 of red + 127 of green + 0 of blue = 382; alpha channel of 2nd is ignored
Comparator.comparePixels(P1, P2, AlphaOption.ignoreFirst)
//63 of red + 255 of green + 0 of blue = 318; alpha channel of 2nd is ignored
Comparator.comparePixels(P2, P1, AlphaOption.ignoreFirst)
//255 of red + 255 of green + 0 of blue + 64 of alpha = 574
Comparator.comparePixels(P1, P2, AlphaOption.subtract)
//255 of red + 255 of green + 0 of blue + 64 of alpha = 574
Comparator.comparePixels(P2, P1, AlphaOption.subtract)
//63 of red + 127 of green = 190
Comparator.comparePixels(P1, P2, AlphaOption.multiply)
//63 of red + 127 of green = 190
Comparator.comparePixels(P2, P1, AlphaOption.multiply)same as simple pixel to pixel comparison but can compare arrays instead, arrays
must be the same length, however, it supports undefined pixels for which you
can set the undefinedScore (by default 255 * 3 - 3 channels)
//510 + 255 * 3 = 1275
Comparator.compareMultiplePixels([P1, undefined], [P2, P1], AlphaOption.ignore)
//510 + 0 = 510
Comparator.compareMultiplePixels(
[P1, undefined],
[P2, P1],
AlphaOption.ignore,
0,
)