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:
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 | let capture; |
Com esse código exebimos no canvas a 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 | function draw() { |
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 | let slider; |
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 | function applyFloydSteiberg() { |
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 | let radio; |
Passo 3
Sem muitas explicações, os seguintes algoritmos são o de Bill Atkinson e Bayer
1 | function applyBillAttkinson() { |
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
118let 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();
}