Skip to content

Commit

Permalink
common: Implement blue noise dithering
Browse files Browse the repository at this point in the history
This adds the CHAFA_DITHER_MODE_NOISE dithering mode to the API. It
will apply blue noise dithering to quantized outputs (sixels and
symbols modes with <= 256 colors). The CLI tool will apply it by
default in sixel mode if nothing else is specified.

Fixes #238 (GitHub).
  • Loading branch information
hpjansson committed Jan 5, 2025
1 parent 8aebfc8 commit 1a5ec3f
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 49 deletions.
2 changes: 2 additions & 0 deletions chafa/chafa-common.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ ChafaColorSpace;
* @CHAFA_DITHER_MODE_NONE: No dithering.
* @CHAFA_DITHER_MODE_ORDERED: Ordered dithering (Bayer or similar).
* @CHAFA_DITHER_MODE_DIFFUSION: Error diffusion dithering (Floyd-Steinberg or similar).
* @CHAFA_DITHER_MODE_NOISE: Noise pattern dithering (blue noise or similar).
* @CHAFA_DITHER_MODE_MAX: Last supported dither mode plus one.
**/

Expand All @@ -164,6 +165,7 @@ typedef enum
CHAFA_DITHER_MODE_NONE,
CHAFA_DITHER_MODE_ORDERED,
CHAFA_DITHER_MODE_DIFFUSION,
CHAFA_DITHER_MODE_NOISE,

CHAFA_DITHER_MODE_MAX
}
Expand Down
222 changes: 222 additions & 0 deletions chafa/chafa-util.c

Large diffs are not rendered by default.

73 changes: 53 additions & 20 deletions chafa/internal/chafa-dither.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
#include "internal/chafa-dither.h"
#include "internal/chafa-private.h"

#define BAYER_MATRIX_DIM_SHIFT 4
#define BAYER_MATRIX_DIM (1 << (BAYER_MATRIX_DIM_SHIFT))
#define BAYER_MATRIX_SIZE ((BAYER_MATRIX_DIM) * (BAYER_MATRIX_DIM))
#define TEXTURE_DIM_SHIFT 4
#define TEXTURE_DIM (1 << (TEXTURE_DIM_SHIFT))
#define TEXTURE_SIZE ((TEXTURE_DIM) * (TEXTURE_DIM))
#define TEXTURE_NOISE_N_CHANNELS 3

static gint
calc_grain_shift (gint size)
Expand Down Expand Up @@ -61,12 +62,18 @@ chafa_dither_init (ChafaDither *dither, ChafaDitherMode mode,
dither->intensity = intensity;
dither->grain_width_shift = calc_grain_shift (grain_width);
dither->grain_height_shift = calc_grain_shift (grain_height);
dither->bayer_size_shift = BAYER_MATRIX_DIM_SHIFT;
dither->bayer_size_mask = BAYER_MATRIX_DIM - 1;

if (mode == CHAFA_DITHER_MODE_ORDERED)
{
dither->bayer_matrix = chafa_gen_bayer_matrix (BAYER_MATRIX_DIM, intensity);
dither->texture_size_shift = TEXTURE_DIM_SHIFT;
dither->texture_size_mask = TEXTURE_DIM - 1;
dither->texture_data = chafa_gen_bayer_matrix (TEXTURE_DIM, intensity);
}
else if (mode == CHAFA_DITHER_MODE_NOISE)
{
dither->texture_size_shift = 6;
dither->texture_size_mask = (1 << 6) - 1;
dither->texture_data = chafa_gen_noise_matrix (dither->intensity * 0.1);
}
else if (mode == CHAFA_DITHER_MODE_DIFFUSION)
{
Expand All @@ -77,34 +84,60 @@ chafa_dither_init (ChafaDither *dither, ChafaDitherMode mode,
void
chafa_dither_deinit (ChafaDither *dither)
{
g_free (dither->bayer_matrix);
dither->bayer_matrix = NULL;
g_free (dither->texture_data);
dither->texture_data = NULL;
}

void
chafa_dither_copy (const ChafaDither *src, ChafaDither *dest)
{
memcpy (dest, src, sizeof (*dest));
if (dest->bayer_matrix)
dest->bayer_matrix = g_memdup (src->bayer_matrix, BAYER_MATRIX_SIZE * sizeof (gint));
if (dest->texture_data)
{
if (dest->mode == CHAFA_DITHER_MODE_NOISE)
dest->texture_data = g_memdup (src->texture_data,
64 * 64 * TEXTURE_NOISE_N_CHANNELS * sizeof (gint));
else
dest->texture_data = g_memdup (src->texture_data, TEXTURE_SIZE * sizeof (gint));
}
}

ChafaColor
chafa_dither_color_ordered (const ChafaDither *dither, ChafaColor color, gint x, gint y)
chafa_dither_color (const ChafaDither *dither, ChafaColor color, gint x, gint y)
{
gint bayer_index = (((y >> dither->grain_height_shift) & dither->bayer_size_mask)
<< dither->bayer_size_shift)
+ ((x >> dither->grain_width_shift) & dither->bayer_size_mask);
gint16 bayer_mod = dither->bayer_matrix [bayer_index];
gint texture_index = (((y >> dither->grain_height_shift) & dither->texture_size_mask)
<< dither->texture_size_shift)
+ ((x >> dither->grain_width_shift) & dither->texture_size_mask);
gint i;

for (i = 0; i < 3; i++)
if (dither->mode == CHAFA_DITHER_MODE_ORDERED)
{
gint16 c;
gint16 texture_mod = dither->texture_data [texture_index];

for (i = 0; i < 3; i++)
{
gint16 c;

c = (gint16) color.ch [i] + bayer_mod;
c = CLAMP (c, 0, 255);
color.ch [i] = c;
c = (gint16) color.ch [i] + texture_mod;
c = CLAMP (c, 0, 255);
color.ch [i] = c;
}
}
else if (dither->mode == CHAFA_DITHER_MODE_NOISE)
{
for (i = 0; i < 3; i++)
{
gint16 texture_mod = dither->texture_data [texture_index * TEXTURE_NOISE_N_CHANNELS + i];
gint16 c;

c = (gint16) color.ch [i] + texture_mod;
c = CLAMP (c, 0, 255);
color.ch [i] = c;
}
}
else
{
g_assert_not_reached ();
}

return color;
Expand Down
8 changes: 4 additions & 4 deletions chafa/internal/chafa-dither.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ typedef struct
gint grain_width_shift;
gint grain_height_shift;

gint bayer_size_shift;
guint bayer_size_mask;
gint *bayer_matrix;
gint texture_size_shift;
guint texture_size_mask;
gint *texture_data;
}
ChafaDither;

Expand All @@ -43,7 +43,7 @@ void chafa_dither_init (ChafaDither *dither, ChafaDitherMode mode,
void chafa_dither_deinit (ChafaDither *dither);
void chafa_dither_copy (const ChafaDither *src, ChafaDither *dest);

ChafaColor chafa_dither_color_ordered (const ChafaDither *dither, ChafaColor color, gint x, gint y);
ChafaColor chafa_dither_color (const ChafaDither *dither, ChafaColor color, gint x, gint y);

G_END_DECLS

Expand Down
9 changes: 5 additions & 4 deletions chafa/internal/chafa-indexed-image.c
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ draw_pixels_pass_2_nodither (ChafaBatchInfo *batch, const DrawPixelsCtx *ctx,
}

static void
draw_pixels_pass_2_bayer (ChafaBatchInfo *batch, const DrawPixelsCtx *ctx,
ChafaColorHash *chash)
draw_pixels_pass_2_dither (ChafaBatchInfo *batch, const DrawPixelsCtx *ctx,
ChafaColorHash *chash)
{
const guint32 *src_p;
guint8 *dest_p, *dest_end_p;
Expand All @@ -194,7 +194,7 @@ draw_pixels_pass_2_bayer (ChafaBatchInfo *batch, const DrawPixelsCtx *ctx,
gint index;

col = chafa_color8_fetch_from_rgba8 (src_p);
col = chafa_dither_color_ordered (&ctx->indexed_image->dither, col, x, y);
col = chafa_dither_color (&ctx->indexed_image->dither, col, x, y);
index = quantize_pixel (&ctx->indexed_image->palette, ctx->color_space, chash, col);
*dest_p = index;

Expand Down Expand Up @@ -354,7 +354,8 @@ draw_pixels_pass_2_worker (ChafaBatchInfo *batch, const DrawPixelsCtx *ctx)
break;

case CHAFA_DITHER_MODE_ORDERED:
draw_pixels_pass_2_bayer (batch, ctx, &chash);
case CHAFA_DITHER_MODE_NOISE:
draw_pixels_pass_2_dither (batch, ctx, &chash);
break;

case CHAFA_DITHER_MODE_DIFFUSION:
Expand Down
36 changes: 19 additions & 17 deletions chafa/internal/chafa-pixops.c
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ convert_rgb_to_din99d (ChafaPixel *pixels, gint width, gint dest_y, gint n_rows)
}

static void
bayer_dither (const ChafaDither *dither, ChafaPixel *pixels, gint width, gint dest_y, gint n_rows)
simple_dither (const ChafaDither *dither, ChafaPixel *pixels, gint width, gint dest_y, gint n_rows)
{
ChafaPixel *pixel = pixels + dest_y * width;
ChafaPixel *pixel_max = pixel + n_rows * width;
Expand All @@ -303,7 +303,7 @@ bayer_dither (const ChafaDither *dither, ChafaPixel *pixels, gint width, gint de
{
for (x = 0; x < width; x++)
{
pixel->col = chafa_dither_color_ordered (dither, pixel->col, x, y);
pixel->col = chafa_dither_color (dither, pixel->col, x, y);
pixel++;
}
}
Expand Down Expand Up @@ -413,8 +413,8 @@ fs_dither (const ChafaDither *dither, const ChafaPalette *palette,
}

static void
bayer_and_convert_rgb_to_din99d (const ChafaDither *dither,
ChafaPixel *pixels, gint width, gint dest_y, gint n_rows)
dither_and_convert_rgb_to_din99d (const ChafaDither *dither,
ChafaPixel *pixels, gint width, gint dest_y, gint n_rows)
{
ChafaPixel *pixel = pixels + dest_y * width;
ChafaPixel *pixel_max = pixel + n_rows * width;
Expand All @@ -424,7 +424,7 @@ bayer_and_convert_rgb_to_din99d (const ChafaDither *dither,
{
for (x = 0; x < width; x++)
{
pixel->col = chafa_dither_color_ordered (dither, pixel->col, x, y);
pixel->col = chafa_dither_color (dither, pixel->col, x, y);
chafa_color_rgb_to_din99d (&pixel->col, &pixel->col);
pixel++;
}
Expand Down Expand Up @@ -643,13 +643,14 @@ prepare_pixels_2_worker (ChafaBatchInfo *batch, PrepareContext *prep_ctx)

if (prep_ctx->color_space == CHAFA_COLOR_SPACE_DIN99D)
{
if (prep_ctx->dither->mode == CHAFA_DITHER_MODE_ORDERED)
if (prep_ctx->dither->mode == CHAFA_DITHER_MODE_ORDERED
|| prep_ctx->dither->mode == CHAFA_DITHER_MODE_NOISE)
{
bayer_and_convert_rgb_to_din99d (prep_ctx->dither,
prep_ctx->dest_pixels,
prep_ctx->dest_width,
batch->first_row,
batch->n_rows);
dither_and_convert_rgb_to_din99d (prep_ctx->dither,
prep_ctx->dest_pixels,
prep_ctx->dest_width,
batch->first_row,
batch->n_rows);
}
else if (prep_ctx->dither->mode == CHAFA_DITHER_MODE_DIFFUSION)
{
Expand All @@ -668,13 +669,14 @@ prepare_pixels_2_worker (ChafaBatchInfo *batch, PrepareContext *prep_ctx)
batch->n_rows);
}
}
else if (prep_ctx->dither->mode == CHAFA_DITHER_MODE_ORDERED)
else if (prep_ctx->dither->mode == CHAFA_DITHER_MODE_ORDERED
|| prep_ctx->dither->mode == CHAFA_DITHER_MODE_NOISE)
{
bayer_dither (prep_ctx->dither,
prep_ctx->dest_pixels,
prep_ctx->dest_width,
batch->first_row,
batch->n_rows);
simple_dither (prep_ctx->dither,
prep_ctx->dest_pixels,
prep_ctx->dest_width,
batch->first_row,
batch->n_rows);
}
else if (prep_ctx->dither->mode == CHAFA_DITHER_MODE_DIFFUSION)
{
Expand Down
1 change: 1 addition & 0 deletions chafa/internal/chafa-private.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ void chafa_canvas_config_deinit (ChafaCanvasConfig *canvas_config);
void chafa_canvas_config_copy_contents (ChafaCanvasConfig *dest, const ChafaCanvasConfig *src);

gint *chafa_gen_bayer_matrix (gint matrix_size, gdouble magnitude);
gint *chafa_gen_noise_matrix (gdouble magnitude);

/* Math stuff */

Expand Down
5 changes: 3 additions & 2 deletions docs/chafa.xml
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,9 @@ which is faster but less accurate.
<term><option>--dither <replaceable>type</replaceable></option></term>
<listitem><para>
Type of dithering to apply during quantization. One of [none, ordered,
diffusion]. "Bayer" is a synonym for "ordered", and "fs" (Floyd-Steinberg) is
a synonym for "diffusion".
diffusion, noise]. "Bayer" is a synonym for "ordered", and "fs" (Floyd-Steinberg)
is a synonym for "diffusion". Defaults to "noise" in sixel mode, otherwise
"none".
</para></listitem>
</varlistentry>

Expand Down
16 changes: 14 additions & 2 deletions tools/chafa/chafa.c
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ typedef struct

GList *args;
ChafaCanvasMode mode;
gboolean dither_mode_set;
ChafaColorExtractor color_extractor;
ChafaColorSpace color_space;
ChafaDitherMode dither_mode;
Expand Down Expand Up @@ -477,7 +478,8 @@ print_summary (void)
" --color-space=CS Color space used for quantization; one of [rgb, din99d].\n"
" Defaults to rgb, which is faster but less accurate.\n"
" --dither=DITHER Set output dither mode; one of [none, ordered,\n"
" diffusion]. No effect with 24-bit color. Defaults to none.\n"
" diffusion, noise]. No effect with 24-bit color. Defaults to\n"
" noise for sixels, none otherwise.\n"
" --dither-grain=WxH Set dimensions of dither grains in 1/8ths of a\n"
" character cell [1, 2, 4, 8]. Defaults to 4x4.\n"
" --dither-intensity=NUM Multiplier for dither intensity [0.0 - inf].\n"
Expand Down Expand Up @@ -604,6 +606,7 @@ fuzz_options_with_seed (GlobalOptions *opt, gconstpointer seed, gint seed_len)
opt->color_extractor = fuzz_seed_get_uint (seed, seed_len, &ofs, 0, CHAFA_COLOR_EXTRACTOR_MAX);
opt->color_space = fuzz_seed_get_uint (seed, seed_len, &ofs, 0, CHAFA_COLOR_SPACE_MAX);
opt->dither_mode = fuzz_seed_get_uint (seed, seed_len, &ofs, 0, CHAFA_DITHER_MODE_MAX);
opt->dither_mode_set = TRUE;
opt->pixel_mode = fuzz_seed_get_uint (seed, seed_len, &ofs, 0, CHAFA_PIXEL_MODE_MAX);
opt->dither_grain_width = 1 << (fuzz_seed_get_uint (seed, seed_len, &ofs, 0, 4));
opt->dither_grain_height = 1 << (fuzz_seed_get_uint (seed, seed_len, &ofs, 0, 4));
Expand Down Expand Up @@ -802,13 +805,16 @@ parse_dither_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_
else if (!g_ascii_strcasecmp (value, "diffusion")
|| !g_ascii_strcasecmp (value, "fs"))
options.dither_mode = CHAFA_DITHER_MODE_DIFFUSION;
else if (!g_ascii_strcasecmp (value, "noise"))
options.dither_mode = CHAFA_DITHER_MODE_NOISE;
else
{
g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
"Dither must be one of [none, ordered, diffusion].");
"Dither must be one of [none, ordered, diffusion, noise].");
result = FALSE;
}

options.dither_mode_set = TRUE;
return result;
}

Expand Down Expand Up @@ -2352,6 +2358,12 @@ parse_options (int *argc, char **argv [])
options.scale = 1.0;
}

if (options.pixel_mode == CHAFA_PIXEL_MODE_SIXELS)
{
if (!options.dither_mode_set)
options.dither_mode = CHAFA_DITHER_MODE_NOISE;
}

/* Apply detected passthrough if auto */
if (!options.passthrough_set)
{
Expand Down

0 comments on commit 1a5ec3f

Please sign in to comment.