Artificial Intelligence 19 min read

Image Binarization for Receipt Printing: Algorithms and Implementation

The article outlines a receipt‑printer image binarization pipeline that converts color logos and QR codes to black‑white bitmaps by first reviewing a C‑based OTSU implementation, then introducing a parallel trio of OTSU, average‑gray, and double‑peak algorithms whose outputs are compared via hash fingerprints, cached for reuse, and optimized for speed on mobile devices, with plans for adaptive thresholds and block‑wise QR processing.

Youzan Coder
Youzan Coder
Youzan Coder
Image Binarization for Receipt Printing: Algorithms and Implementation

Background: Retail receipt printers only support black‑white bitmap images, while merchants often upload color logos and QR codes. To print these correctly, the images must be converted to binary (0/255) format.

The article describes the overall binarization workflow, emphasizing the importance of the threshold T that separates foreground from background. It first reviews the previous solution that used the OTSU (maximum inter‑class variance) algorithm implemented in C.

Previous OTSU implementation (key parts): /** * Get gray image */ int *gray_image(int *bit_map, int width, int height) { double pixel_total = width * height; if (pixel_total == 0) return NULL; int *gray_pixels = (int *)malloc(pixel_total * sizeof(int)); memset(gray_pixels, 0, pixel_total * sizeof(int)); int *p = bit_map; for (u_int i = 0; i < pixel_total; i++, p++) { u_char alpha = ((*p & 0xFF000000) >> 24); u_char red = ((*p & 0xFF0000) >> 16); u_char green = ((*p & 0x00FF00) >> 8); u_char blue = (*p & 0x0000FF); u_char gray = (red*38 + green*75 + blue*15) >> 7; if (alpha == 0 && gray == 0) gray = 0xFF; gray_pixels[i] = gray; } return gray_pixels; } int *binary_image_with_otsu_threshold_alg(int *bit_map, int width, int height, int *T) { double pixel_total = width * height; if (pixel_total == 0) return NULL; unsigned long sum1 = 0; unsigned long sumB = 0; double wB = 0.0, wF = 0.0, mB = 0.0, mF = 0.0, max_g = 0.0, g = 0.0; u_char threshold = 0; double histogram[256] = {0}; int *gray_pixels = (int *)malloc(pixel_total * sizeof(int)); memset(gray_pixels, 0, pixel_total * sizeof(int)); int *p = bit_map; for (u_int i = 0; i < pixel_total; i++, p++) { // extract RGB and compute gray as above, store in gray_pixels[i] // also fill histogram[gray]++ and sum1 += gray; } for (int i = 0; i < 256; i++) { wB += histogram[i]; wF = pixel_total - wB; if (wB == 0 || wF == 0) continue; sumB += i * histogram[i]; mB = sumB / wB; mF = (sum1 - sumB) / wF; g = wB * wF * (mB - mF) * (mB - mF); if (g >= max_g) { threshold = i; max_g = g; } } for (u_int i = 0; i < pixel_total; i++) { gray_pixels[i] = gray_pixels[i] <= threshold ? 0xFF000000 : 0xFFFFFFFF; } if (T) *T = threshold; return gray_pixels; }

Performance tests on iPhone 6 show OTSU processing times ranging from 0.005 s for 260×260 images to 0.200 s for 2560×1440 images, confirming millisecond‑level performance.

Identified problems: (1) a single algorithm may not produce satisfactory results for all logos (e.g., yellow logos become white after binarization); (2) each print triggers image processing without caching.

New solution: run three algorithms in parallel (OTSU, average‑gray, double‑peak), compute a hash fingerprint for each binary result, compare it with the original gray image hash, and select the binary image with the highest similarity. The workflow also introduces a cache for pre‑processed logo and QR‑code images.

Average‑gray algorithm (key part): int *binary_image_with_average_gray_threshold_alg(int *bit_map, int width, int height, int *T) { double pixel_total = width * height; if (pixel_total == 0) return NULL; unsigned long sum = 0; u_char threshold = 0; int *gray_pixels = (int *)malloc(pixel_total * sizeof(int)); memset(gray_pixels, 0, pixel_total * sizeof(int)); int *p = bit_map; for (u_int i = 0; i < pixel_total; i++, p++) { // extract RGB, compute gray as before // store gray in gray_pixels[i] and accumulate sum += gray; } threshold = sum / pixel_total; // average gray for (u_int i = 0; i < pixel_total; i++) { gray_pixels[i] = gray_pixels[i] <= threshold ? 0xFF000000 : 0xFFFFFFFF; } if (T) *T = threshold; return gray_pixels; }

Double‑peak algorithm (key parts): int is_double_peak(double *histogram) { int peak_count = 0; for (int i = 1; i < 255; i++) { if (histogram[i-1] < histogram[i] && histogram[i+1] < histogram[i]) { peak_count++; if (peak_count > 2) return 0; } } return peak_count == 2; } int *binary_image_with_average_peak_threshold_alg(int *bit_map, int width, int height, int *T) { double pixel_total = width * height; if (pixel_total == 0) return NULL; double histogram1[256] = {0}, histogram2[256] = {0}; int *gray_pixels = (int *)malloc(pixel_total * sizeof(int)); memset(gray_pixels, 0, pixel_total * sizeof(int)); int *p = bit_map; for (u_int i = 0; i < pixel_total; i++, p++) { // compute gray, store in gray_pixels[i] // histogram1[gray]++; } // copy histogram1 to histogram2 memcpy(histogram2, histogram1, 256 * sizeof(double)); int times = 0; while (!is_double_peak(histogram2)) { if (++times > 1000) return NULL; // give up // smooth histogram2 using three‑point averaging // copy back to histogram1 for next iteration } // find the two peaks and compute threshold as their average int peak[2] = {0, 0}, y = 0; for (int i = 1; i < 255; i++) { if (histogram2[i-1] < histogram2[i] && histogram2[i+1] < histogram2[i]) { peak[y++] = i; } } int threshold = (peak[0] + peak[1]) / 2; for (u_int i = 0; i < pixel_total; i++) { gray_pixels[i] = gray_pixels[i] <= threshold ? 0xFF000000 : 0xFFFFFFFF; } if (T) *T = threshold; return gray_pixels; }

Hash fingerprint generation: resize image to at least 8×8, convert to gray, compute average gray, encode each pixel as 0/1 based on the average, and pack bits into 64‑bit integers (or an array of such integers for larger images). Hamming distance between hash arrays measures similarity.

Cache strategy: pre‑process fixed logo and QR‑code images, store the resulting print commands in NSUserDefaults , and reuse them for subsequent prints, falling back to the old processing path only when the cache is missing.

Future work includes collecting statistics on the best algorithm per image, allowing merchants to manually adjust the threshold T , improving cache invalidation when backend images change, and applying block‑wise processing for QR codes.

performance optimizationimage-processingC languagebinarizationOTSU algorithmreceipt printing
Youzan Coder
Written by

Youzan Coder

Official Youzan tech channel, delivering technical insights and occasional daily updates from the Youzan tech team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.