How e-ink displays more detail when only black and white are available

Eleven Ways to Restore Detail When Grayscale Is Limited to Black and White

Monochrome screens and thermal printers only recognize black and white. The tal_image component in TuyaOpen contains 11 “dithering” algorithms, specifically designed to fit a grayscale image into these two colors in the least unsightly way possible. This article runs these algorithms on real images to show exactly where they differ and how to choose the right one for your project.

Why Dithering Is Needed

Many inexpensive displays (small monochrome LCDs, electronic shelf labels, some e-ink screens) and virtually all thermal printers can only have each point either “on” or “off,” “inked” or “uninked”—there is no intermediate gray. However, images captured by cameras or photos to be printed clearly contain grayscale with light-to-dark transitions.

The simplest approach is a direct cut-off: if a pixel is brighter than a certain value, render it white; if darker, render it black (this is the “fixed threshold” method below). But the results are usually poor—large gradients like skies or shadows on faces will turn into solid blocks of black or white, losing all detail.

Dithering algorithms solve this problem by: using only black and white colors, arranging the density and patterns of these dots so that the human eye perceives grayscale layers when viewed from a slight distance. Black-and-white photos in newspapers and patterns from old dot-matrix printers use this same principle. The 11 algorithms in TuyaOpen are 11 different implementations of this approach.

Location in TuyaOpen

All 11 algorithms are implemented centrally in tal_image_dither_core.c, serving as a “shared core” that anyone can adjust—whether the image comes from a camera or a JPEG file, it follows the same codebase rather than each module copying and modifying its own version.

Camera Preview (YUV422 Frame) ──┐
                               ├─→ tal_image_dither_core (11 dithering methods) ─→ Monochrome Screen (1bpp bitmap)
JPEG Image (Photo/Received)  ──┘                                        └─→ Thermal Printer (1bpp bitmap)

Both paths ultimately convert the image into a 1-bit bitmap (each pixel occupies only 1 bit, where 1 represents “inked/lit” and 0 represents “uninked/unlit,” arranged from most significant bit to least significant bit within each byte)—the only difference lies in the format of the incoming raw data and the target hardware.

Eleven Algorithms, Four Approaches

Threshold Methods (Threshold)

The most straightforward approach: set a “passing line” threshold. Pixels brighter than this line are rendered white, darker ones black. Each pixel is judged independently without considering neighbors or retaining errors, resulting in the lowest computational cost and fastest speed. However, large gradient areas are basically unsalvageable, turning into solid black or white blocks.

  • TAL_IMAGE_MONO_MTH_FIXED Fixed Threshold —— Define your own threshold value (default 128); brighter renders white, darker renders black. Suitable for: debugging / baseline comparison.
  • TAL_IMAGE_MONO_MTH_ADAPTIVE Adaptive Threshold —— The threshold is set to the average brightness of the entire image. It is slightly smarter than the fixed threshold when the overall image is too dark or too bright. Suitable for: unevenly lit scenes.
  • TAL_IMAGE_MONO_MTH_OTSU Otsu’s Method —— Automatically finds a dividing line using a histogram that “best splits the image into two halves.” It is more accurate than simply averaging but has a higher computational cost. Suitable for: images with strong light-dark contrast.

Bayer Ordered Dithering (Ordered Dither)

Instead of a single line, this method compares pixel brightness against a fixed “dot pattern” (matrix)—the principle is similar to halftone printing in old newspapers. Larger patterns (2×2 → 3×3 → 4×4) can represent more grayscale levels and produce finer dot textures, but the regular pattern remains visible. Its advantages are speed, lack of trailing, and no “smearing” artifacts associated with error diffusion.

  • TAL_IMAGE_MONO_MTH_BAYER4_DITHER Bayer 2×2 (4 levels) —— Coarsest dot pattern. Suitable for: extremely low-resolution screens.
  • TAL_IMAGE_MONO_MTH_BAYER8_DITHER Bayer 3×3 (8 levels) —— The most commonly used option. Suitable for: real-time camera preview.
  • TAL_IMAGE_MONO_MTH_BAYER16_DITHER Bayer 4×4 (16 levels) —— Finest dot pattern, richest detail. Suitable for: static images where detail is prioritized.

Error Diffusion (Error Diffusion)

Core concept: If a pixel should represent “half gray” but can only be pure black or pure white, the error from the “rounding” doesn’t disappear—it is “booked” and distributed to neighboring unprocessed pixels, conserving overall brightness. Visually, this is usually the closest to the original image among the 11 methods. The cost is that it must calculate pixel by pixel and maintain several rows of “error ledgers.” The differences between the three algorithms mainly lie in how far and to how many neighbors the error is distributed.

  • TAL_IMAGE_MONO_MTH_FLOYD_STEINBERG Floyd–Steinberg —— The most classic method. Error is distributed to the 3 adjacent pixels to the right and below. Suitable for: general default choice.
  • TAL_IMAGE_MONO_MTH_STUCKI Stucki —— Error is distributed to 12 subsequent pixels over a wider area, providing smoother transitions but requiring more computation. Suitable for: printing photos where image quality is paramount.
  • TAL_IMAGE_MONO_MTH_JARVIS Jarvis–Judice–Ninke —— Also distributes error to 12 pixels, but with different weight distribution. It is the smoothest and slowest of the three. Suitable for: high-quality printing where a few milliseconds don’t matter.

Advanced Variants (Advanced)

These add a specific “twist” on top of error diffusion:

  • TAL_IMAGE_MONO_MTH_EDGE_ATKINSON Edge-Atkinson (Edge Locking) —— First applies gamma correction, then detects whether each pixel is on an “edge.” If it is, it is directly judged as black, and error diffusion is skipped. This results in sharper, less blurry outlines. Suitable for: portraits, line art, and text-based images.
  • TAL_IMAGE_MONO_MTH_GAMMA_SERPENTINE Gamma-Serpentine FS —— Gamma correction combined with a serpentine (snake-like) scan order (left-to-right for odd rows, right-to-left for even rows) to avoid directional patterns left by unidirectional scanning. Suitable for: images sensitive to pattern direction.

Effect Comparison

Examine details in close-ups and overall effects in full images—large gradient areas like skies and clouds are where the difference between threshold methods and error diffusion is most pronounced.

Sample 1 · Landscape Illustration (Mountains/Forest / Steam / Arch Bridge)

Sample 2 · Animal Close-up (Fur / Metal Gears / High Contrast)

Remaining 2 sample images


How to Use in TuyaOpen

The method enumeration TAL_IMAGE_MONO_METHOD_E is shared; choosing an algorithm is simply a matter of changing one parameter.

Path 1 · Camera YUV422 Preview → Monochrome Screen

#include "tal_image_yuv422_to_binary.h"

TAL_IMAGE_YUV422_TO_BINARY_T conv_cfg = {
    .method          = TAL_IMAGE_MONO_MTH_FLOYD_STEINBERG,
    .fixed_threshold = 128,   // Only used by MTH_FIXED
    .invert_colors   = 1,     // 1: bit=1→White (LVGL); 0: bit=1→Black (Printer)
    .in_buf          = yuv422_frame,
    .in_width        = 320,
    .in_height       = 240,
    .out_buf         = mono_buf,
    .out_width       = 96,
    .out_height      = 96,
    .rotate          = TAL_IMAGE_ROTATE_0,
};

tal_image_format_yuv422_to_binary(&conv_cfg);

Path 2 · JPEG Image → Thermal Printer

#include "tal_image_jpeg_codec.h"

TAL_IMAGE_JPEG_OUTPUT_T out = {
    .out_buf      = printer_buf,
    .out_buf_size = sizeof(printer_buf),
    .out_width    = 384,   // Common width for 58mm thermal printers (in dots)
    .out_height   = 480,
};

tal_image_jpeg_decode_bitmap(jpeg_data, jpeg_size, &out,
                              TAL_IMAGE_MONO_MTH_STUCKI, 128);

Parameter Description:

Parameter Description
method Enumeration value for the 11 algorithms; both paths share the same TAL_IMAGE_MONO_METHOD_E.
fixed_threshold / threshold Only MTH_FIXED uses this as the dividing line; other methods calculate their own. However, in the JPEG path, error diffusion methods still use it to set the center of “denoising clamping,” indirectly affecting results.
invert_colors 1: bit=1 renders white (common convention for LVGL monochrome screens); 0: bit=1 renders black (common convention for printers)—do not select incorrectly, or the image will appear as a “negative.”
rotate Only available in the YUV422 path: rotates clockwise before conversion to compensate for installation angle differences between the camera module and the screen.

Path 3 · Static Image Resource → e-ink / Monochrome LCD

#include "tal_image_jpeg_codec.h"
#include "tdl_display_manage.h"

#define IMG_W 200
#define IMG_H 200

// disp_hdl: Screen device opened previously via tdl_disp_find_dev() + tdl_disp_dev_open()
// static_jpeg_data/size: JPEG packaged into firmware (e.g., boot welcome screen), not from a camera
TDL_DISP_FRAME_BUFF_T *fb = tdl_disp_create_frame_buff(DISP_FB_TP_SRAM, (IMG_W + 7) / 8 * IMG_H);
fb->fmt    = TUYA_PIXEL_FMT_MONOCHROME;
fb->width  = IMG_W;
fb->height = IMG_H;

TAL_IMAGE_JPEG_OUTPUT_T out = {
    .out_buf      = fb->frame,
    .out_buf_size = fb->len,
    .out_width    = IMG_W,
    .out_height   = IMG_H,
};

tal_image_jpeg_decode_bitmap(static_jpeg_data, static_jpeg_size, &out,
                              TAL_IMAGE_MONO_MTH_BAYER8_DITHER, 128);

tdl_disp_dev_flush(disp_hdl, fb);
tdl_disp_free_frame_buff(fb);

This shares the same tal_image_jpeg_decode_bitmap() function as Path 2. The difference lies only in the final step: the output is directly pushed into TDL_DISP_FRAME_BUFF_T to tdl_disp_dev_flush(), rather than being sent to the printer driver.

Parameter Description:

Parameter Description
method Enumeration value for the 11 algorithms; all three paths share the same TAL_IMAGE_MONO_METHOD_E.
fixed_threshold / threshold Only MTH_FIXED uses this as the dividing line; other methods calculate their own. However, in the JPEG path (Paths 2 and 3), error diffusion methods still use it to set the center of “denoising clamping,” indirectly affecting results.
invert_colors Only available in Path 1 (YUV422): 1: bit=1 renders white (common convention for LVGL monochrome screens); 0: bit=1 renders black (common convention for printers)—do not select incorrectly, or the image will appear as a “negative.” Paths 2 and 3 do not have this parameter; tal_image_jpeg_decode_bitmap() always outputs with “bit=1 renders black.” If the screen driver convention is opposite, you need to flip it manually before pushing to the screen.
rotate Only available in Path 1 (YUV422): rotates clockwise before conversion to compensate for installation angle differences between the camera module and the screen.

Quick Selection Guide

Scenario Recommended Why
Real-time camera preview, needs speed and power efficiency BAYER8_DITHER No need to maintain error ledgers; calculates pixel by pixel independently, resulting in the lowest CPU usage.
Printing a photo, prioritizing aesthetics STUCKI / FLOYD_STEINBERG Wide error diffusion range and most natural gradient transitions; FS is faster, Stucki is smoother.
Printing portraits, line art, or text-based images EDGE_ATKINSON Edge locking judges pixels as black, preventing blurry outlines; sharper than pure error diffusion.
Images with uneven lighting (backlit, locally dark) ADAPTIVE / OTSU The dividing line is calculated, not a fixed value guessed arbitrarily, fitting the current scene better.
No special requirements, just want it to work FLOYD_STEINBERG Most balanced combination of effect and speed; also the most commonly used default in TuyaOpen.

Real-World Shots

The above results were generated on a computer. Finally, here is a real-device photo shot—under the same algorithms, real-world paper reflections and screen pixel gaps cause subtle differences in the effect.

2 Likes