Dithering - Experimentos em Programação Criativa - 8

Essa é mais uma tentativa de reproduzir uma imagem utilizando computação criativa. Como nas últimas vezes, o objetivo dessa tentativa é de utilizar uma inspiração para aprender técnicas e conceitos novos. Esses artigos são principalmente criados como uma forma de catalogar o processo de implementação para referencia futura quando eu estiver desenvolvendo novas coisas.

Inspiração

Desta vez, uma das imagens que eu tinha salvo como inspiração é a seguinte:

Gradiente pixelado

Por mais que pareça usar um algoritmo diferente, decidi usar ela como inspiração para aprender a implementar algoritmos de dithering. Dithering é uma forma de ruído, usado para “melhorar” a qualidade de imagens quando utilizando uma quantidade limitada de cores. Em nosso exemplo, usaremos apenas preto e branco.

Implementação

Usarei novamento o editor do p5 para essa implementação, usando como inspiração esse artigo e essa implementação, também em Javascript.

O artigo consegue explicar muito melhor como cada artigo funciona, então aqui descreverei apenas como fiz para representar o mesmo usando o editor do p5 e também usar como referência caso eu pretenda usálo em outros projetos.

Passo 1

Ao invés de usar um simples gradiente, camos usar a própria câmera do laptop como input para o nosso algoritmo. Isso pode ser feito facilmente com p5 da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
12
let capture;

function setup() {
createCanvas(600, 450);
capture = createCapture(VIDEO);
capture.hide()
}

function draw() {
background(220);
image(capture, 0, 0, width, height);
}

Com esse código exebimos no canvas a imagem da câmera

Imagem da câmera

Fazendo um loop nos pixels da imagem podemos calcular qual o valor médio de cor em cada pixel e atribuir 0 (preto) ou 255 (branco) para o pixel, fazendo assim a imagem monocromática.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function draw() {
...
applyMonochrome();
}

function applyMonochrome() {
loadPixels();
for (let i = 0; i < pixels.length; i += 4) {
const blackAndWhite = Math.floor((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3);
const c = blackAndWhite < 129 ? 0 : 255
pixels[i] = c;
pixels[i + 1] = c;
pixels[i + 2] = c;
}
updatePixels();
}

Imagem monocromática

Interessante notar que a lista the pixels gerada pelo p5 contem 4 itens para cada pixel, respectivamente os canais RGBA. Por isso precisamos atualizar os três primeiros para que a cor seja definida.

Para testarmos diferentes níveis de limear, podemos usar também um slider.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let slider;


function setup() {
...

slider = createSlider(0, 255, 129);
}

...


function applyMonochrome() {
...
const c = blackAndWhite < slider.value() ? 0 : 255
...
}

Passo 2

O segundo algoritmo que implementaremos será o de Floyd-Steinberg, que utiliza uma matriz de difusão para distribuição de erro aos pixels vizinhos.

O algoritmo final fica desta forma

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function applyFloydSteiberg() {
loadPixels();
for (let i = 0; i < pixels.length; i += 4) {
const blackAndWhite = Math.floor((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3);
const c = blackAndWhite < slider.value() ? 0 : 255;

const err = Math.floor((blackAndWhite - c) / 16);

for (let j = 0; j < 3; j++) {
pixels[i + j] = c;
pixels[i + j + 4] += err * 7;
pixels[i + j + 4 * width - 4] += err * 3;
pixels[i + j + 4 * width] += err * 5;
pixels[i + j + 4 * width + 4] += err * 1;
}
}
updatePixels();
}

Imagem com filtro Floyd-Steiberg

A imagem aparenta até ter tons de cinza, mas olhando próximo é possível perceber que são pixeis pretos e brancos intercalados.

Para facilitar o teste, podemos adicionar radio buttons para alternar entre os métodos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let radio;

function setup() {
...
radio = createRadio();
radio.option('monochrome');
radio.option('Floyd-Steinberg');
...
}

function draw() {
...

switch(radio.value()) {
case 'monochrome': {
applyMonochrome();
break;
}
case 'Floyd-Steinberg': {
applyFloydSteiberg();
break;
}
}
}

Passo 3

Sem muitas explicações, os seguintes algoritmos são o de Bill Atkinson e Bayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function applyBillAttkinson() {
loadPixels();
for (let i = 0; i < pixels.length; i += 4) {
const blackAndWhite = Math.floor((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3);
const c = blackAndWhite < slider.value() ? 0 : 255

const err = Math.floor((blackAndWhite - c) / 8);

for (let j = 0; j < 3; j++) {
pixels[i + j]= c;
pixels[i + j + 4] += err;
pixels[i + j + 8] += err;
pixels[i + j + 4 * width - 4] += err;
pixels[i + j + 4 * width] += err;
pixels[i + j + 4 * width + 4] += err;
pixels[i + j + 8 * width] += err;
}
}
updatePixels();
}

function applyBayer() {
const bayerThresholdMap = [
[ 15, 135, 45, 165 ],
[ 195, 75, 225, 105 ],
[ 60, 180, 30, 150 ],
[ 240, 120, 210, 90 ]
];

loadPixels();
for (let i = 0; i < pixels.length; i += 4) {
const blackAndWhite = Math.floor((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3);

const x = i/4 % width;
const y = Math.floor(i/4 / width);
const mapped = Math.floor( (blackAndWhite + bayerThresholdMap[x%4][y%4]) / 2 );
const c = (mapped < slider.value()) ? 0 : 255;

pixels[i] = c;
pixels[i + 1] = c;
pixels[i + 2] = c;
}
updatePixels();
}

Imagem com filtro de Bill Atkinson

Imagem com filtro Bayer

Próximos Passos

Esses não são os únicos. Outros algoritmos de dithering também podem ser aplicados. A ideia de próximo passos é implementar novos algoritmos e talvez pensar em quais peojetos seria divertido aplicá-los.

O código completo está abaixo e é uma forma rápida de começar a testar e brincar com esses filtros.

Código completo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
let capture;
let slider;
let radio;

function setup() {
createCanvas(600, 450);
capture = createCapture(VIDEO);
capture.hide();

radio = createRadio();
radio.option('monochrome');
radio.option('Floyd-Steinberg');
radio.option('Bill Atkinson');
radio.option('Bayer');

slider = createSlider(0, 255, 129);
}

function draw() {
background(220);
image(capture, 0, 0, width, height);


switch(radio.value()) {
case 'monochrome': {
applyMonochrome();
break;
}
case 'Floyd-Steinberg': {
applyFloydSteiberg();
break;
}
case 'Bill Atkinson': {
applyBillAttkinson();
break;
}
case 'Bayer': {
applyBayer();
break;
}
}
}

function applyMonochrome() {
loadPixels();
for (let i = 0; i < pixels.length; i += 4) {
const blackAndWhite = Math.floor((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3);
const c = blackAndWhite < slider.value() ? 0 : 255
pixels[i] = c;
pixels[i + 1] = c;
pixels[i + 2] = c;
}
updatePixels();
}

function applyFloydSteiberg() {
loadPixels();
for (let i = 0; i < pixels.length; i += 4) {
const blackAndWhite = Math.floor((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3);
const c = blackAndWhite < slider.value() ? 0 : 255;

const err = Math.floor((blackAndWhite - c) / 16);

for (let j = 0; j < 3; j++) {
pixels[i + j] = c;
pixels[i + j + 4] += err * 7;
pixels[i + j + 4 * width - 4] += err * 3;
pixels[i + j + 4 * width] += err * 5;
pixels[i + j + 4 * width + 4] += err * 1;
}
}
updatePixels();
}

function applyBillAttkinson() {
loadPixels();
for (let i = 0; i < pixels.length; i += 4) {
const blackAndWhite = Math.floor((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3);
const c = blackAndWhite < slider.value() ? 0 : 255

const err = Math.floor((blackAndWhite - c) / 8);

for (let j = 0; j < 3; j++) {
pixels[i + j]= c;
pixels[i + j + 4] += err;
pixels[i + j + 8] += err;
pixels[i + j + 4 * width - 4] += err;
pixels[i + j + 4 * width] += err;
pixels[i + j + 4 * width + 4] += err;
pixels[i + j + 8 * width] += err;
}
}
updatePixels();
}

function applyBayer() {
const bayerThresholdMap = [
[ 15, 135, 45, 165 ],
[ 195, 75, 225, 105 ],
[ 60, 180, 30, 150 ],
[ 240, 120, 210, 90 ]
];

loadPixels();
for (let i = 0; i < pixels.length; i += 4) {
const blackAndWhite = Math.floor((pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3);

const x = i/4 % width;
const y = Math.floor(i/4 / width);
const mapped = Math.floor( (blackAndWhite + bayerThresholdMap[x%4][y%4]) / 2 );
const c = (mapped < slider.value()) ? 0 : 255;

pixels[i] = c;
pixels[i + 1] = c;
pixels[i + 2] = c;
}
updatePixels();
}