Files
OmaPhoto/kernels/DitherPixels.c
T

279 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "DitherPixels.h"
#include <math.h>
#include <stdlib.h>
static inline float clamp01(float v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
// Density darkens (positive) or lightens as a gamma, so black and white stay put; contrast pivots on mid gray.
static inline float adjust_tone(float v, float gamma, float contrast) {
v = powf(clamp01(v), gamma);
return clamp01((v - 0.5f) * contrast + 0.5f);
}
// One error-diffusion kernel: neighbors to the right on this row and below, with their weights over `divisor`.
typedef struct { int dx, dy, weight; } Tap;
typedef struct { const Tap *taps; int count; float divisor; } Kernel;
static const Tap atkinson[] = { {1,0,1}, {2,0,1}, {-1,1,1}, {0,1,1}, {1,1,1}, {0,2,1} };
static const Tap floyd[] = { {1,0,7}, {-1,1,3}, {0,1,5}, {1,1,1} };
// Atkinson passes on only six eighths of the error, which is what gives the Mac's crisp, contrasty look.
static Kernel kernel_for(int style) {
return style == DITHER_ATKINSON ? (Kernel){ atkinson, 6, 8 } : (Kernel){ floyd, 4, 16 };
}
static inline float quantize(float v, int levels) {
float steps = (float)(levels - 1);
return roundf(clamp01(v) * steps) / steps;
}
// Diffuses each plane in serpentine order, so the error's drift doesn't streak to one side.
static void diffuse(float *plane, const uint8_t *alpha, size_t width, size_t height, const DitherParams *p) {
Kernel k = kernel_for(p->style);
for (size_t y = 0; y < height; ++y) {
int reverse = (int)(y & 1);
for (size_t i = 0; i < width; ++i) {
size_t x = reverse ? width - 1 - i : i;
size_t at = y * width + x;
if (!alpha[at]) continue;
float old = plane[at], q = quantize(old, p->levels);
plane[at] = q;
float error = (old - q) * p->diffusion / k.divisor;
for (int t = 0; t < k.count; ++t) {
long nx = (long)x + (reverse ? -k.taps[t].dx : k.taps[t].dx), ny = (long)y + k.taps[t].dy;
if (nx < 0 || nx >= (long)width || ny >= (long)height) continue;
plane[(size_t)ny * width + (size_t)nx] += error * (float)k.taps[t].weight;
}
}
}
}
static const uint8_t bayer8[64] = {
0, 32, 8, 40, 2, 34, 10, 42, 48, 16, 56, 24, 50, 18, 58, 26,
12, 44, 4, 36, 14, 46, 6, 38, 60, 28, 52, 20, 62, 30, 54, 22,
3, 35, 11, 43, 1, 33, 9, 41, 51, 19, 59, 27, 49, 17, 57, 25,
15, 47, 7, 39, 13, 45, 5, 37, 63, 31, 55, 23, 61, 29, 53, 21,
};
// The ordered threshold for a pixel, in [0, 1). Smaller Bayer matrices are the top-left corners of the 8 × 8 one,
// rescaled, which is how the recursive construction nests them.
static inline float ordered_threshold(int style, size_t x, size_t y) {
switch (style) {
case DITHER_BAYER_2: { static const uint8_t m[4] = { 0, 2, 3, 1 }; return ((float)m[(y & 1) * 2 + (x & 1)] + 0.5f) / 4; }
case DITHER_BAYER_4: {
static const uint8_t m[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5 };
return ((float)m[(y & 3) * 4 + (x & 3)] + 0.5f) / 16;
}
default: return ((float)bayer8[(y & 7) * 8 + (x & 7)] + 0.5f) / 64;
}
}
static inline float ordered(float v, float threshold, int levels) {
float steps = (float)(levels - 1);
float q = floorf(clamp01(v) * steps + threshold);
return (q > steps ? steps : q) / steps;
}
// How much of a halftone cell a point must be covered by before it's marked, for each screen shape. `u` and `v`
// run from −0.5 to 0.5 across the cell; the shapes grow from its middle as coverage rises.
static inline float spot(int style, float u, float v) {
float au = fabsf(u), av = fabsf(v);
switch (style) {
case DITHER_DOTS: return 3.14159265f * (u * u + v * v);
case DITHER_LINES: return av * 2;
default: return au + av;
}
}
// Old Mac fill patterns, 8 × 8, one byte per row with the leftmost pixel in the top bit, from sparsest to fullest.
static const uint8_t patterns[][8] = {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
{ 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 },
{ 0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00 },
{ 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 },
{ 0x88, 0x22, 0x88, 0x22, 0x88, 0x22, 0x88, 0x22 },
{ 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00 },
{ 0x11, 0x22, 0x44, 0x88, 0x11, 0x22, 0x44, 0x88 },
{ 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0x00 },
{ 0x88, 0x55, 0x22, 0x55, 0x88, 0x55, 0x22, 0x55 },
{ 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x08, 0x08, 0x08 },
{ 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55 },
{ 0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81 },
{ 0x77, 0xAA, 0xDD, 0xAA, 0x77, 0xAA, 0xDD, 0xAA },
{ 0xEE, 0xDD, 0xBB, 0x77, 0xEE, 0xDD, 0xBB, 0x77 },
{ 0x77, 0xFF, 0xDD, 0xFF, 0x77, 0xFF, 0xDD, 0xFF },
{ 0x7F, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF },
{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF },
};
static const int patternCount = (int)(sizeof patterns / sizeof patterns[0]);
static inline void write_pixel(uint8_t *px, float r, float g, float b) {
float a = (float)px[3] / 255.0f;
px[0] = (uint8_t)lroundf(clamp01(r) * a * 255.0f);
px[1] = (uint8_t)lroundf(clamp01(g) * a * 255.0f);
px[2] = (uint8_t)lroundf(clamp01(b) * a * 255.0f);
}
int dither_apply(uint8_t *rgba, size_t width, size_t height, size_t stride, const DitherParams *p) {
size_t count = width * height;
if (!count) return 1;
int planes = p->originalColors ? 3 : 1;
float *tone = malloc(count * sizeof(float) * (size_t)planes);
uint8_t *alpha = malloc(count);
// The image's own colors, unadjusted: halftone dots and glyphs take them in Original mode.
float *source = p->originalColors ? malloc(count * sizeof(float) * 3) : NULL;
if (!tone || !alpha || (p->originalColors && !source)) { free(tone); free(alpha); free(source); return 0; }
float gamma = exp2f(p->density * 1.5f);
float contrast = p->contrast >= 0 ? 1.0f / (1.0f - 0.95f * p->contrast) : 1.0f + p->contrast;
for (size_t y = 0; y < height; ++y) {
const uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
const uint8_t *px = row + x * 4;
size_t at = y * width + x;
alpha[at] = px[3];
float r = 0, g = 0, b = 0;
if (px[3]) {
float scale = 1.0f / (float)px[3];
r = px[0] * scale; g = px[1] * scale; b = px[2] * scale;
}
if (p->originalColors) {
tone[at] = adjust_tone(r, gamma, contrast);
tone[count + at] = adjust_tone(g, gamma, contrast);
tone[2 * count + at] = adjust_tone(b, gamma, contrast);
source[at * 3] = r; source[at * 3 + 1] = g; source[at * 3 + 2] = b;
} else {
tone[at] = adjust_tone(0.2126f * r + 0.7152f * g + 0.0722f * b, gamma, contrast);
}
}
}
float dark[3] = { p->dark[0] / 255.0f, p->dark[1] / 255.0f, p->dark[2] / 255.0f };
float light[3] = { p->light[0] / 255.0f, p->light[1] / 255.0f, p->light[2] / 255.0f };
int style = p->style;
int levels = p->levels < 2 ? 2 : p->levels > 16 ? 16 : p->levels;
if (style <= DITHER_BAYER_8) {
// Diffusion and ordered dithering: each plane is quantized to `levels` tones, then mapped to colors.
if (style <= DITHER_FLOYD_STEINBERG) {
DitherParams local = *p;
local.levels = levels;
for (int c = 0; c < planes; ++c) diffuse(tone + (size_t)c * count, alpha, width, height, &local);
} else {
for (int c = 0; c < planes; ++c) {
float *plane = tone + (size_t)c * count;
for (size_t y = 0; y < height; ++y)
for (size_t x = 0; x < width; ++x) {
size_t at = y * width + x;
if (alpha[at]) plane[at] = ordered(plane[at], ordered_threshold(style, x, y), levels);
}
}
}
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
size_t at = y * width + x;
if (!alpha[at]) continue;
if (p->originalColors) {
write_pixel(row + x * 4, tone[at], tone[count + at], tone[2 * count + at]);
} else {
float t = tone[at];
write_pixel(row + x * 4, dark[0] + (light[0] - dark[0]) * t, dark[1] + (light[1] - dark[1]) * t,
dark[2] + (light[2] - dark[2]) * t);
}
}
}
} else {
// Marks (halftone shapes, patterns, glyphs) cover as much of each spot as the tone calls for. On light, they
// stand for darkness and are drawn in the dark color; light on dark, the reverse.
float *marks = p->originalColors ? malloc(count * sizeof(float)) : tone;
if (!marks) { free(tone); free(alpha); free(source); return 0; }
if (p->originalColors)
for (size_t i = 0; i < count; ++i)
marks[i] = 0.2126f * tone[i] + 0.7152f * tone[count + i] + 0.0722f * tone[2 * count + i];
int cell = p->cell < 2 ? 2 : p->cell;
float cosA = cosf(p->angle), sinA = sinf(p->angle);
float *ink = p->lightOnDark ? light : dark, *paper = p->lightOnDark ? dark : light;
// Glyphs: each cell shares one, picked from the cell's average tone, worked out once per cell.
size_t columns = (width + (size_t)cell - 1) / (size_t)cell, cellRows = (height + (size_t)cell - 1) / (size_t)cell;
int *picked = NULL;
if (style == DITHER_GLYPHS && p->glyphCount > 0) {
picked = malloc(columns * cellRows * sizeof(int));
if (!picked) { if (marks != tone) free(marks); free(tone); free(alpha); free(source); return 0; }
for (size_t row = 0; row < cellRows; ++row)
for (size_t column = 0; column < columns; ++column) {
float sum = 0; int n = 0;
for (size_t yy = row * (size_t)cell; yy < (row + 1) * (size_t)cell && yy < height; ++yy)
for (size_t xx = column * (size_t)cell; xx < (column + 1) * (size_t)cell && xx < width; ++xx) {
size_t i = yy * width + xx;
if (alpha[i]) { sum += marks[i]; ++n; }
}
float t = n ? sum / (float)n : 1;
float wanted = (p->lightOnDark ? t : 1 - t) * p->glyphCoverage[p->glyphCount - 1];
int best = 0;
float bestDistance = 2;
for (int g = 0; g < p->glyphCount; ++g) {
float d = fabsf(p->glyphCoverage[g] - wanted);
if (d < bestDistance) { bestDistance = d; best = g; }
}
picked[row * columns + column] = best;
}
}
// Original colors: marks take the pixel's own color, on black (light on dark) or white.
float paperOriginal = p->lightOnDark ? 0.0f : 1.0f;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
size_t at = y * width + x;
if (!alpha[at]) continue;
float amount;
if (picked) {
int glyph = picked[(y / (size_t)cell) * columns + x / (size_t)cell];
amount = p->glyphs[(size_t)glyph * (size_t)(cell * cell) + (y % (size_t)cell) * (size_t)cell + x % (size_t)cell] / 255.0f;
} else if (style == DITHER_PATTERNS) {
float t = marks[at];
float coverage = p->lightOnDark ? t : 1 - t;
int index = (int)lroundf(coverage * (float)(patternCount - 1));
amount = (patterns[index][y & 7] >> (7 - (x & 7))) & 1;
} else {
float fx = (float)x + 0.5f, fy = (float)y + 0.5f;
float u = (fx * cosA + fy * sinA) / (float)cell, v = (-fx * sinA + fy * cosA) / (float)cell;
u -= floorf(u) + 0.5f; v -= floorf(v) + 0.5f;
float t = marks[at];
amount = (p->lightOnDark ? t : 1 - t) > spot(style, u, v) ? 1 : 0;
}
if (p->originalColors) {
const float *s = source + at * 3;
write_pixel(row + x * 4, paperOriginal + (s[0] - paperOriginal) * amount,
paperOriginal + (s[1] - paperOriginal) * amount, paperOriginal + (s[2] - paperOriginal) * amount);
} else {
write_pixel(row + x * 4, paper[0] + (ink[0] - paper[0]) * amount, paper[1] + (ink[1] - paper[1]) * amount,
paper[2] + (ink[2] - paper[2]) * amount);
}
}
}
free(picked);
if (marks != tone) free(marks);
}
free(tone); free(alpha); free(source);
return 1;
}
void dither_dots(uint8_t *rgba, size_t width, size_t height, size_t stride, int block, const uint8_t *gap) {
if (block < 2) return;
float radius = (float)block * 0.42f, middle = (float)block / 2;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
float dy = (float)(y % (size_t)block) + 0.5f - middle;
for (size_t x = 0; x < width; ++x) {
uint8_t *px = row + x * 4;
if (!px[3]) continue;
float dx = (float)(x % (size_t)block) + 0.5f - middle;
float cover = clamp01(radius - sqrtf(dx * dx + dy * dy) + 0.5f);
if (cover >= 1) continue;
for (int c = 0; c < 3; ++c)
px[c] = (uint8_t)lroundf((float)px[c] * cover + (float)gap[c] * (float)px[3] / 255.0f * (1 - cover));
}
}
}