Skip to content

Commit

Permalink
feat(pixel-dither): add ditherWithKernel() & various presets
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Oct 4, 2021
1 parent 8a7ec9c commit e2ce82a
Show file tree
Hide file tree
Showing 15 changed files with 420 additions and 7 deletions.
111 changes: 111 additions & 0 deletions packages/pixel-dither/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<!-- This file is generated - DO NOT EDIT! -->

# ![pixel-dither](https://media.thi.ng/umbrella/banners/thing-pixel-dither.svg?cd0ecd41)

[![npm version](https://img.shields.io/npm/v/@thi.ng/pixel-dither.svg)](https://www.npmjs.com/package/@thi.ng/pixel-dither)
![npm downloads](https://img.shields.io/npm/dm/@thi.ng/pixel-dither.svg)
[![Twitter Follow](https://img.shields.io/twitter/follow/thing_umbrella.svg?style=flat-square&label=twitter)](https://twitter.com/thing_umbrella)

This project is part of the
[@thi.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo.

- [About](#about)
- [Status](#status)
- [Installation](#installation)
- [Dependencies](#dependencies)
- [API](#api)
- [Authors](#authors)
- [License](#license)

## About

Extensible image dithering w/ various algorithm presets.

The package provides the following dithering algorithm presets (but can also be
very easily extended via definition of custom kernels):

- Atkinson
- Bayes (ordered dithering w/ customizable sizes & levels)
- Burkes
- Diffusion (1D row/column, 2D)
- Floyd-Steinberg
- Jarvis-Judice-Ninke
- Sierra 2-row
- Stucki
- Threshold

### Status

**ALPHA** - bleeding edge / work-in-progress

[Search or submit any issues for this package](https://github.com/thi-ng/umbrella/issues?q=%5Bpixel-dither%5D+in%3Atitle)

## Installation

```bash
yarn add @thi.ng/pixel-dither
```

ES module import:

```html
<script type="module" src="https://cdn.skypack.dev/@thi.ng/pixel-dither"></script>
```

[Skypack documentation](https://docs.skypack.dev/)

For NodeJS (v14.6+):

```text
node --experimental-specifier-resolution=node --experimental-repl-await
> const pixelDither = await import("@thi.ng/pixel-dither");
```

## Dependencies

- [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
- [@thi.ng/math](https://github.com/thi-ng/umbrella/tree/develop/packages/math)
- [@thi.ng/pixel](https://github.com/thi-ng/umbrella/tree/develop/packages/pixel)

## API

[Generated API docs](https://docs.thi.ng/umbrella/pixel-dither/)

```ts
import { packedBufferFromImage, GRAY8 } from "@thi.ng/pixel";
import { ditherWithKernel, ATKINSON } from "@thi.ng/pixel-dither";

const img = packedBufferFromImage("foo.jpg");

// apply dithering to all channels in given pixel buffer
ditherWithKernel(img, ATKINSON);

// first convert to 8-bit gray before dithering
ditherWithKernel(img.as(GRAY8), ATKINSON);

// apply dithering to select channels only
// use custom threshold & error spillage/bleed factor
ditherWithKernel(img, ATKINSON, { channels: [1, 2, 3], threshold: 0.66, bleed: 0.75 });
```

TODO

## Authors

Karsten Schmidt

If this project contributes to an academic publication, please cite it as:

```bibtex
@misc{thing-pixel-dither,
title = "@thi.ng/pixel-dither",
author = "Karsten Schmidt",
note = "https://thi.ng/pixel-dither",
year = 2021
}
```

## License

&copy; 2021 Karsten Schmidt // Apache Software License 2.0
29 changes: 28 additions & 1 deletion packages/pixel-dither/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@thi.ng/pixel-dither",
"version": "0.0.1",
"description": "TODO",
"description": "Extensible image dithering w/ various algorithm presets",
"type": "module",
"module": "./index.js",
"typings": "./index.d.ts",
Expand Down Expand Up @@ -63,8 +63,35 @@
"./api": {
"import": "./api.js"
},
"./atkinson": {
"import": "./atkinson.js"
},
"./burkes": {
"import": "./burkes.js"
},
"./diffusion": {
"import": "./diffusion.js"
},
"./dither": {
"import": "./dither.js"
},
"./floyd-steinberg": {
"import": "./floyd-steinberg.js"
},
"./jarvis": {
"import": "./jarvis.js"
},
"./ordered": {
"import": "./ordered.js"
},
"./sierra2": {
"import": "./sierra2.js"
},
"./stucki": {
"import": "./stucki.js"
},
"./threshold": {
"import": "./threshold.js"
}
},
"thi.ng": {
Expand Down
36 changes: 36 additions & 0 deletions packages/pixel-dither/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
import type { Fn } from "@thi.ng/api";
import type { PackedBuffer } from "@thi.ng/pixel";

export type DitherKernelFactory = Fn<PackedBuffer, DitherKernel>;

export interface DitherKernel {
ox: number[];
oy: number[];
weights: number[];
shift: number;
x1?: number;
x2?: number;
y1?: number;
y2?: number;
}

export interface DitherOpts {
/**
* Normalized threshold
*
* @defaultValue 0.5
*/
threshold: number;
/**
* Error spillage/diffusion factor.
*
* @defaultValue 1.0
*/
bleed: number;
/**
* Channel IDs to limit processing (if omittet, all channels will be
* processed).
*/
channels: number[];
}

export type BayerSize = 1 | 2 | 4 | 8 | 16 | 32 | 64;

export interface BayerMatrix {
Expand Down
16 changes: 16 additions & 0 deletions packages/pixel-dither/src/atkinson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { DitherKernelFactory } from "./api";

/**
* (Bill) Atkinson dither kernel
*
* @remarks
* References:
* - https://beyondloom.com/blog/dither.html
* - https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html
*/
export const ATKINSON: DitherKernelFactory = () => ({
ox: [1, 2, -1, 0, 1, 0],
oy: [0, 0, 1, 1, 1, 2],
weights: [1, 1, 1, 1, 1, 1],
shift: 3,
});
15 changes: 15 additions & 0 deletions packages/pixel-dither/src/burkes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { DitherKernelFactory } from "./api";

/**
* Burkes dither kernel (similar/improved version of {@link STUCKI}).
*
* @remarks
* References:
* - https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html
*/
export const BURKES: DitherKernelFactory = () => ({
ox: [1, 2, -2, -1, 0, 1, 2],
oy: [0, 0, 1, 1, 1, 1, 1],
weights: [8, 4, 2, 4, 8, 4, 2],
shift: 5,
});
33 changes: 33 additions & 0 deletions packages/pixel-dither/src/diffusion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { DitherKernelFactory } from "./api";

/**
* Basic 1D (row-based) error diffusion.
*/
export const DIFFUSION_ROW: DitherKernelFactory = ({ width }) => ({
ox: [1],
oy: [0],
weights: [1],
shift: 0,
x2: width - 1,
});

/**
* Basic 1D (column-based) error diffusion.
*/
export const DIFFUSION_COLUMN: DitherKernelFactory = () => ({
ox: [0],
oy: [1],
weights: [1],
shift: 0,
});

/**
* Basic 2D error diffusion
*/
export const DIFFUSION_2D: DitherKernelFactory = ({ width }) => ({
ox: [1, 0],
oy: [0, 1],
weights: [1, 1],
shift: 1,
x2: width - 1,
});
49 changes: 49 additions & 0 deletions packages/pixel-dither/src/dither.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { PackedBuffer } from "@thi.ng/pixel";
import { range } from "@thi.ng/pixel/range";
import type { DitherKernelFactory, DitherOpts } from "./api";

export const ditherWithKernel = (
img: PackedBuffer,
kernel: DitherKernelFactory,
opts?: Partial<DitherOpts>
) => {
const { channels, bleed, threshold } = {
bleed: 1,
threshold: 0.5,
...opts,
};
const { format, width, height } = img;
for (let cid of channels || range(format.channels.length)) {
const cimg = img.getChannel(cid);
const chan = format.channels[cid];
const $thresh = chan.num * threshold;
const $max = chan.mask0;
const pixels = new Int32Array(cimg.pixels);
const { x1, x2, y1, y2, ox, oy, weights, shift } = {
x1: 0,
x2: width,
y1: 0,
y2: height,
...kernel(cimg),
};
let p: number, err: number;
for (let y = y1; y < y2; y++) {
for (let x = x1, i = x + y * width; x < width; x++, i++) {
p = pixels[i] < $thresh ? 0 : $max;
err = (pixels[i] - p) * bleed;
pixels[i] = p;
if (!err) continue;
for (let j = ox.length; j-- > 0; ) {
const xx = x + ox[j];
const yy = y + oy[j];
if (yy >= 0 && yy < y2 && xx >= 0 && xx < x2) {
pixels[yy * width + xx] += (err * weights[j]) >> shift;
}
}
}
}
cimg.pixels.set(pixels);
img.setChannel(cid, cimg);
}
return img;
};
16 changes: 16 additions & 0 deletions packages/pixel-dither/src/floyd-steinberg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { DitherKernelFactory } from "./api";

/**
* Floyd-Steinberg dither kernel.
*
* @remarks
* References:
* - https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering
* - https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html
*/
export const FLOYD_STEINBERG: DitherKernelFactory = () => ({
ox: [1, -1, 0, 1],
oy: [0, 1, 1, 1],
weights: [7, 3, 5, 1],
shift: 4,
});
10 changes: 10 additions & 0 deletions packages/pixel-dither/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
export * from "./api";
export * from "./dither";
export * from "./ordered";

export * from "./atkinson";
export * from "./burkes";
export * from "./diffusion";
export * from "./floyd-steinberg";
export * from "./jarvis";
export * from "./sierra2";
export * from "./stucki";
export * from "./threshold";
21 changes: 21 additions & 0 deletions packages/pixel-dither/src/jarvis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { DitherKernelFactory } from "./api";

const A = 64 / 48;
const B = 3 * A;
const C = 5 * A;
const D = 7 * A;

/**
* Jarvis-Judice-Ninke dither kernel.
*
* @remarks
* References:
* - https://en.wikipedia.org/wiki/Error_diffusion#minimized_average_error
* - https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html
*/
export const JARVIS_JUDICE_NINKE: DitherKernelFactory = () => ({
ox: [1, 2, -2, -1, 0, 1, 2, -2, -1, 0, 1, 2],
oy: [0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2],
weights: [D, C, B, C, D, C, B, A, B, C, B, A],
shift: 6,
});
Loading

0 comments on commit e2ce82a

Please sign in to comment.