flatcv-0.3.1.0: flatcv_impl.c
/* FlatCV - Amalgamated implementation (auto-generated) */
/*
* ISC License
*
* Copyright (c) 2025 Adrian Sieber
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
#define FLATCV_AMALGAMATION
#include "flatcv.h"
// File: src/binary_closing_disk.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef FLATCV_AMALGAMATION
#include "conversion.h"
#include "perspectivetransform.h"
#else
#include "flatcv.h"
#endif
uint8_t *fcv_binary_dilation_disk(
uint8_t const *image_data,
int32_t width,
int32_t height,
int32_t radius
) {
if (!image_data || width <= 0 || height <= 0 || radius < 0) {
return NULL;
}
// Check for overflow: width * height (width and height already validated > 0)
if ((size_t)width > SIZE_MAX / (size_t)height) {
return NULL;
}
size_t num_pixels = (size_t)width * (size_t)height;
uint8_t *result = malloc(num_pixels);
if (!result) {
return NULL;
}
// Initialize result to 0 (black)
memset(result, 0, num_pixels);
// For each pixel in the image
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
int32_t idx = y * width + x;
// If current pixel is white (255), dilate it
if (image_data[idx] == 255) {
// Apply disk-shaped structuring element
for (int32_t dy = -radius; dy <= radius; dy++) {
for (int32_t dx = -radius; dx <= radius; dx++) {
// Check if point32_t is within disk (use slightly larger radius for
// better connectivity)
double r_eff = radius + 0.5;
if (dx * dx + dy * dy <= r_eff * r_eff) {
int32_t ny = y + dy;
int32_t nx = x + dx;
// Check bounds
if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
int32_t nidx = ny * width + nx;
result[nidx] = 255;
}
}
}
}
}
}
}
return result;
}
static uint8_t *binary_erosion_disk_internal(
uint8_t const *image_data,
int32_t width,
int32_t height,
int32_t radius,
bool replicate_border
) {
if (!image_data || width <= 0 || height <= 0 || radius < 0) {
return NULL;
}
// Check for overflow: width * height (width and height already validated > 0)
if ((size_t)width > SIZE_MAX / (size_t)height) {
return NULL;
}
size_t num_pixels = (size_t)width * (size_t)height;
uint8_t *result = malloc(num_pixels);
if (!result) {
return NULL;
}
// Initialize result to 0 (black)
memset(result, 0, num_pixels);
// For each pixel in the image
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
int32_t idx = y * width + x;
// Check if structuring element fits entirely within white pixels
bool can_erode = true;
for (int32_t dy = -radius; dy <= radius && can_erode; dy++) {
for (int32_t dx = -radius; dx <= radius && can_erode; dx++) {
// Check if point32_t is within disk (use slightly larger radius for
// better connectivity)
double r_eff = radius + 0.5;
if (dx * dx + dy * dy <= r_eff * r_eff) {
int32_t ny = y + dy;
int32_t nx = x + dx;
uint8_t neighbor_value;
// Check bounds
if (ny < 0 || ny >= height || nx < 0 || nx >= width) {
if (replicate_border) {
// Replicate border: clamp coordinates to valid range
int32_t clamped_y =
ny < 0 ? 0 : (ny >= height ? height - 1 : ny);
int32_t clamped_x = nx < 0 ? 0 : (nx >= width ? width - 1 : nx);
neighbor_value = image_data[clamped_y * width + clamped_x];
}
else {
// Default: treat out-of-bounds as black
neighbor_value = 0;
}
}
else {
neighbor_value = image_data[ny * width + nx];
}
if (neighbor_value != 255) {
can_erode = false;
}
}
}
}
if (can_erode) {
result[idx] = 255;
}
}
}
return result;
}
uint8_t *fcv_binary_erosion_disk(
uint8_t const *image_data,
int32_t width,
int32_t height,
int32_t radius
) {
// Public API uses default behavior: treat out-of-bounds as black
return binary_erosion_disk_internal(image_data, width, height, radius, false);
}
uint8_t *fcv_binary_closing_disk(
uint8_t const *image_data,
int32_t width,
int32_t height,
int32_t radius
) {
if (!image_data || width <= 0 || height <= 0 || radius < 0) {
return NULL;
}
// Step 1: Dilation
uint8_t *dilated =
fcv_binary_dilation_disk(image_data, width, height, radius);
if (!dilated) {
return NULL;
}
// Step 2: Erosion of the dilated image
// Use replicate_border=true to preserve original white pixels at borders
// In a closing operation, out-of-bounds pixels use "replicate" border mode
// which clamps coordinates to valid range, preventing border pixels from
// being erroneously eroded due to artificial black boundary
uint8_t *result =
binary_erosion_disk_internal(dilated, width, height, radius, true);
// Free intermediate result
free(dilated);
return result;
}
uint8_t *fcv_binary_opening_disk(
uint8_t const *image_data,
int32_t width,
int32_t height,
int32_t radius
) {
if (!image_data || width <= 0 || height <= 0 || radius < 0) {
return NULL;
}
// Step 1: Erosion
uint8_t *eroded = fcv_binary_erosion_disk(image_data, width, height, radius);
if (!eroded) {
return NULL;
}
// Step 2: Dilation of the eroded image
uint8_t *result = fcv_binary_dilation_disk(eroded, width, height, radius);
// Free intermediate result
free(eroded);
return result;
}
// File: src/conversion.c
#include <assert.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "conversion.h"
#include "draw.h"
#include "parse_hex_color.h"
#include "perspectivetransform.h"
#include "rgba_to_grayscale.h"
#include "single_to_multichannel.h"
#include "sobel_edge_detection.h"
#else
#include "flatcv.h"
#endif
/**
* Convert raw RGBA row-major top-to-bottom image data
* to RGBA row-major top-to-bottom grayscale image data.
*
* @param width Width of the image.
* @param height Height of the image.
* @param data Pointer to the pixel data.
* @return Pointer to the grayscale image data.
*/
uint8_t *
fcv_grayscale(uint32_t width, uint32_t height, uint8_t const *const data) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t num_pixels = (size_t)width * height;
if (num_pixels > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = num_pixels * 4;
uint8_t *grayscale_data = malloc(img_length_byte);
if (!grayscale_data) { // Memory allocation failed
return NULL;
}
// Process each pixel row by row
for (uint32_t i = 0; i < width * height; i++) {
uint32_t rgba_index = i * 4;
uint8_t r = data[rgba_index];
uint8_t g = data[rgba_index + 1];
uint8_t b = data[rgba_index + 2];
uint8_t gray = (r * R_WEIGHT + g * G_WEIGHT + b * B_WEIGHT) >> 8;
grayscale_data[rgba_index] = gray;
grayscale_data[rgba_index + 1] = gray;
grayscale_data[rgba_index + 2] = gray;
grayscale_data[rgba_index + 3] = 255;
}
return grayscale_data;
}
/**
* Convert raw RGBA row-major top-to-bottom image data
* to RGBA row-major top-to-bottom grayscale image data
* with a stretched contrast range.
* Set the 1.5625 % darkest pixels to 0 and the 1.5625 % brightest to 255.
* Uses this specific value for speed: x * 1.5625 % = x >> 6
* The rest of the pixel values are linearly scaled to the range [0, 255].
* @param width Width of the image.
* @param height Height of the image.
* @param data Pointer to the pixel data.
* @return Pointer to the grayscale image data.
*/
uint8_t *fcv_grayscale_stretch(
uint32_t width,
uint32_t height,
uint8_t const *const data
) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t img_length_px = (size_t)width * height;
if (img_length_px > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = img_length_px * 4;
uint8_t *grayscale_data = malloc(img_length_byte);
if (!grayscale_data) { // Memory allocation failed
return NULL;
}
// Ignore 1.5625 % of the pixels
uint32_t num_pixels_to_ignore = img_length_px >> 6;
uint8_t *gray_values = malloc(img_length_px);
if (!gray_values) { // Memory allocation failed
free(grayscale_data);
return NULL;
}
// Process each pixel row by row to get grayscale values
for (uint32_t i = 0; i < img_length_px; i++) {
uint32_t rgba_index = i * 4;
uint8_t r = data[rgba_index];
uint8_t g = data[rgba_index + 1];
uint8_t b = data[rgba_index + 2];
gray_values[i] = (r * R_WEIGHT + g * G_WEIGHT + b * B_WEIGHT) >> 8;
}
// Use counting sort to find the 1.5625% darkest and brightest pixels
uint32_t histogram[256] = {0};
for (uint32_t i = 0; i < img_length_px; i++) {
histogram[gray_values[i]]++;
}
uint32_t cumulative_count = 0;
uint8_t min_val = 0;
for (uint32_t i = 0; i < 256; i++) {
cumulative_count += histogram[i];
if (cumulative_count > num_pixels_to_ignore) {
min_val = i;
break;
}
}
cumulative_count = 0;
uint8_t max_val = 255;
for (int32_t i = 255; i >= 0; i--) {
cumulative_count += histogram[i];
if (cumulative_count > num_pixels_to_ignore) {
max_val = i;
break;
}
}
free(gray_values);
uint8_t range = max_val - min_val;
// Process each pixel row by row
for (size_t i = 0; i < img_length_px; i++) {
uint32_t rgba_index = i * 4;
uint8_t r = data[rgba_index];
uint8_t g = data[rgba_index + 1];
uint8_t b = data[rgba_index + 2];
uint8_t gray = (r * R_WEIGHT + g * G_WEIGHT + b * B_WEIGHT) >> 8;
if (range == 0) {
// All pixels have same value, preserve as-is
// (no stretching possible)
}
else if (gray < min_val) {
gray = 0;
}
else if (gray > max_val) {
gray = 255;
}
else {
gray = (gray - min_val) * 255 / range;
}
grayscale_data[rgba_index] = gray;
grayscale_data[rgba_index + 1] = gray;
grayscale_data[rgba_index + 2] = gray;
grayscale_data[rgba_index + 3] = 255;
}
return grayscale_data;
}
/**
* Apply a global threshold to the image data.
*
* @param img_length_px Length of the image data in pixels.
* @param data Pointer to the image data.
* @param threshold Threshold value.
*
*/
void fcv_apply_global_threshold(
uint32_t img_length_px,
uint8_t *data,
uint8_t threshold
) {
for (uint32_t i = 0; i < img_length_px; i++) {
data[i] = data[i] > threshold ? 255 : 0;
}
}
/**
* Applies two thresholds to the image data by blackening pixels
* below the lower threshold and whitening pixels above the upper threshold.
* Pixels between the two thresholds are scaled to the range [0, 255].
*
* @param img_length_px Length of the image data in pixels.
* @param data Pointer to the image data.
* @param lower_threshold Every pixel below this value will be blackened.
* @param upper_threshold Every pixel above this value will be whitened.
*
*/
void apply_double_threshold(
uint32_t img_length_px,
uint8_t *data,
uint8_t lower_threshold,
uint8_t upper_threshold
) {
for (uint32_t i = 0; i < img_length_px; i++) {
if (data[i] < lower_threshold) {
data[i] = 0;
}
else if (data[i] > upper_threshold) {
data[i] = 255;
}
else {
data[i] =
(data[i] - lower_threshold) * 255 / (upper_threshold - lower_threshold);
}
}
}
/**
* Apply Otsu's thresholding algorithm to the image data.
*
* @param width Width of the image.
* @param height Height of the image.
* @param use_double_threshold Whether to use double thresholding.
* @param data Pointer to the pixel data.
* @return Pointer to the monochrome image data.
*/
uint8_t *fcv_otsu_threshold_rgba(
uint32_t width,
uint32_t height,
bool use_double_threshold,
uint8_t const *const data
) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height
if (width > SIZE_MAX / height) {
return NULL;
}
uint8_t *grayscale_img = fcv_rgba_to_grayscale(width, height, data);
if (!grayscale_img) {
return NULL;
}
size_t img_length_px = (size_t)width * height;
uint32_t histogram[256] = {0};
for (uint32_t i = 0; i < img_length_px; i++) {
histogram[grayscale_img[i]]++;
}
float histogram_norm[256] = {0};
for (uint32_t i = 0; i < 256; i++) {
histogram_norm[i] = (float)histogram[i] / img_length_px;
}
float global_mean = 0.0;
for (uint32_t i = 0; i < 256; i++) {
global_mean += i * histogram_norm[i];
}
float cumulative_sum = 0.0;
float cumulative_mean = 0.0;
float max_variance = 0.0;
int32_t optimal_threshold = 0;
for (uint32_t i = 0; i < 256; i++) {
cumulative_sum += histogram_norm[i];
cumulative_mean += i * histogram_norm[i];
if (cumulative_sum == 0 || cumulative_sum == 1) {
continue;
}
float mean1 = cumulative_mean / cumulative_sum;
float mean2 = (global_mean - cumulative_mean) / (1 - cumulative_sum);
float class_variance =
cumulative_sum * (1 - cumulative_sum) * (mean1 - mean2) * (mean1 - mean2);
if (class_variance > max_variance) {
max_variance = class_variance;
optimal_threshold = i;
}
}
const int32_t threshold_range_offset = 16;
if (use_double_threshold) {
apply_double_threshold(
img_length_px,
grayscale_img,
optimal_threshold - threshold_range_offset,
optimal_threshold + threshold_range_offset
);
}
else {
fcv_apply_global_threshold(img_length_px, grayscale_img, optimal_threshold);
}
uint8_t *monochrome_data =
fcv_single_to_multichannel(width, height, grayscale_img);
free(grayscale_img);
return monochrome_data;
}
/**
* Apply gaussian blur to the image data.
*
* @param width Width of the image.
* @param height Height of the image.
* @param data Pointer to the pixel data.
* @return Pointer to the blurred image data.
*/
uint8_t *fcv_apply_gaussian_blur(
uint32_t width,
uint32_t height,
double radius,
uint8_t const *const data
) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Validate radius
if (radius < 0 || !isfinite(radius)) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t img_length_px = (size_t)width * height;
if (img_length_px > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = img_length_px * 4;
if (radius == 0) {
uint8_t *copy = malloc(img_length_byte);
if (!copy) {
return NULL;
}
return memcpy(copy, data, img_length_byte);
}
// Reject excessive radius to prevent excessive memory allocation
if (radius > 1000) {
return NULL;
}
uint8_t *blurred_data = malloc(img_length_byte);
if (!blurred_data) { // Memory allocation failed
return NULL;
}
uint32_t kernel_size = (uint32_t)(2 * radius + 1);
float *kernel = malloc(kernel_size * sizeof(float));
if (!kernel) { // Memory allocation failed
free(blurred_data);
return NULL;
}
float sigma = radius / 3.0;
float sigma_sq = sigma * sigma;
float two_sigma_sq = 2 * sigma_sq;
float sqrt_two_pi_sigma = sqrt(2 * M_PI) * sigma;
for (uint32_t i = 0; i < kernel_size; i++) {
int32_t x = i - radius;
kernel[i] = exp(-(x * x) / two_sigma_sq) / sqrt_two_pi_sigma;
}
// Apply the kernel in the horizontal direction
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
float r_sum = 0.0;
float g_sum = 0.0;
float b_sum = 0.0;
float weight_sum = 0.0;
for (int32_t k = -radius; k <= radius; k++) {
int32_t x_offset = x + k;
if (x_offset < 0 || (uint32_t)x_offset >= width) {
continue;
}
uint32_t img_index = y * width + x_offset;
uint32_t img_rgba_index = img_index * 4;
// Bounds check for array access
if (img_rgba_index + 2 >= img_length_px * 4) {
continue;
}
float weight = kernel[k + (int32_t)radius];
weight_sum += weight;
r_sum += data[img_rgba_index] * weight;
g_sum += data[img_rgba_index + 1] * weight;
b_sum += data[img_rgba_index + 2] * weight;
}
uint32_t rgba_index = (y * width + x) * 4;
blurred_data[rgba_index] = r_sum / weight_sum;
blurred_data[rgba_index + 1] = g_sum / weight_sum;
blurred_data[rgba_index + 2] = b_sum / weight_sum;
blurred_data[rgba_index + 3] = 255;
}
}
// Create temporary buffer for vertical pass to avoid reading from buffer
// being written to
uint8_t *temp_data = malloc(img_length_px * 4);
if (!temp_data) {
free(blurred_data);
free(kernel);
return NULL;
}
// Copy horizontal blur result to temp buffer
memcpy(temp_data, blurred_data, img_length_px * 4);
// Apply the kernel in the vertical direction
for (uint32_t x = 0; x < width; x++) {
for (uint32_t y = 0; y < height; y++) {
float r_sum = 0.0;
float g_sum = 0.0;
float b_sum = 0.0;
float weight_sum = 0.0;
for (int32_t k = -radius; k <= radius; k++) {
int32_t y_offset = y + k;
if (y_offset < 0 || (uint32_t)y_offset >= height) {
continue;
}
uint32_t img_index = y_offset * width + x;
uint32_t img_rgba_index = img_index * 4;
float weight = kernel[k + (int32_t)radius];
weight_sum += weight;
r_sum += temp_data[img_rgba_index] * weight;
g_sum += temp_data[img_rgba_index + 1] * weight;
b_sum += temp_data[img_rgba_index + 2] * weight;
}
uint32_t rgba_index = (y * width + x) * 4;
blurred_data[rgba_index] = r_sum / weight_sum;
blurred_data[rgba_index + 1] = g_sum / weight_sum;
blurred_data[rgba_index + 2] = b_sum / weight_sum;
blurred_data[rgba_index + 3] = 255;
}
}
free(temp_data);
free(kernel);
return blurred_data;
}
#include <time.h>
/**
* Convert image to anti-aliased black and white.
* 1. Convert the image to grayscale.
* 2. Subtract blurred image from the original image to get the high
* frequencies.
* 3. Apply OTSU's threshold to get the optimal threshold.
* 4. Apply the threshold + offset to get the anti-aliased image.
*
* @param width Width of the image.
* @param height Height of the image.
* @param use_double_threshold Whether to use double thresholding.
* @param data Pointer to the pixel data.
* @return Pointer to the blurred image data.
*/
uint8_t *fcv_bw_smart(
uint32_t width,
uint32_t height,
bool use_double_threshold,
uint8_t const *const data
) {
if (!data || width == 0 || height == 0) {
return NULL;
}
uint8_t *grayscale_data = fcv_grayscale(width, height, data);
if (!grayscale_data) {
return NULL;
}
// Calculate blur radius dependent on image size
// (Empirical formula after testing)
double blurRadius = (sqrt((double)width * (double)height)) * 0.1;
uint8_t *blurred_data =
fcv_apply_gaussian_blur(width, height, blurRadius, grayscale_data);
if (!blurred_data) {
free(grayscale_data);
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
free(grayscale_data);
free(blurred_data);
return NULL;
}
size_t img_length_px = (size_t)width * height;
if (img_length_px > SIZE_MAX / 4) {
free(grayscale_data);
free(blurred_data);
return NULL;
}
uint8_t *high_freq_data = malloc(img_length_px * 4);
if (!high_freq_data) { // Memory allocation failed
free((void *)grayscale_data);
free((void *)blurred_data);
return NULL;
}
// Subtract blurred image from the original image to get the high frequencies
// and invert the high frequencies to get a white background.
for (uint32_t i = 0; i < img_length_px; i++) {
uint32_t rgba_idx = i * 4;
int32_t high_freq_val =
127 + grayscale_data[rgba_idx] - blurred_data[rgba_idx];
// Clamp the value to [0, 255] to prevent overflow
if (high_freq_val < 0) {
high_freq_val = 0;
}
else if (high_freq_val > 255) {
high_freq_val = 255;
}
high_freq_data[rgba_idx] = high_freq_val; // R
high_freq_data[rgba_idx + 1] = high_freq_val; // G
high_freq_data[rgba_idx + 2] = high_freq_val; // B
high_freq_data[rgba_idx + 3] = 255; // A
}
free((void *)grayscale_data);
free((void *)blurred_data);
uint8_t *final_data = fcv_otsu_threshold_rgba(
width,
height,
use_double_threshold,
high_freq_data
);
free(high_freq_data);
return final_data;
}
/**
* Resize an image by given resize factors using bilinear interpolation.
*
* @param width Width of the input image.
* @param height Height of the input image.
* @param resize_x Horizontal resize factor (e.g., 2.0 for 2x, 0.5 for half).
* @param resize_y Vertical resize factor (e.g., 2.0 for 2x, 0.5 for half).
* @param out_width Pointer to store the output image width.
* @param out_height Pointer to store the output image height.
* @param data Pointer to the input pixel data.
* @return Pointer to the resized image data.
*/
uint8_t *fcv_resize(
uint32_t width,
uint32_t height,
double resize_x,
double resize_y,
uint32_t *out_width,
uint32_t *out_height,
uint8_t const *const data
) {
if (!data || !out_width || !out_height) {
return NULL;
}
if (width == 0 || height == 0) {
return NULL;
}
if (resize_x <= 0.0 || resize_y <= 0.0 || !isfinite(resize_x) ||
!isfinite(resize_y)) {
return NULL;
}
// Check for overflow in output dimensions
double new_width_d = (double)width * resize_x;
double new_height_d = (double)height * resize_y;
if (new_width_d > UINT32_MAX || new_height_d > UINT32_MAX) {
return NULL;
}
*out_width = (uint32_t)new_width_d;
*out_height = (uint32_t)new_height_d;
if (*out_width == 0 || *out_height == 0) {
return NULL;
}
// Check for overflow: out_width * out_height * 4
if (*out_width > SIZE_MAX / *out_height) {
return NULL;
}
size_t out_img_pixels = (size_t)(*out_width) * (*out_height);
if (out_img_pixels > SIZE_MAX / 4) {
return NULL;
}
size_t out_img_length = out_img_pixels * 4;
uint8_t *resized_data = malloc(out_img_length);
if (!resized_data) {
return NULL;
}
for (uint32_t out_y = 0; out_y < *out_height; out_y++) {
for (uint32_t out_x = 0; out_x < *out_width; out_x++) {
if (resize_x < 1.0 || resize_y < 1.0) {
double src_x = (out_x + 0.5) / resize_x - 0.5;
double src_y = (out_y + 0.5) / resize_y - 0.5;
double filter_size_x = 1.0 / resize_x;
double filter_size_y = 1.0 / resize_y;
double x_start = src_x - filter_size_x * 0.5;
double y_start = src_y - filter_size_y * 0.5;
double x_end = src_x + filter_size_x * 0.5;
double y_end = src_y + filter_size_y * 0.5;
int32_t ix_start = (int32_t)floor(x_start);
int32_t iy_start = (int32_t)floor(y_start);
int32_t ix_end = (int32_t)ceil(x_end);
int32_t iy_end = (int32_t)ceil(y_end);
if (ix_start < 0) {
ix_start = 0;
}
if (iy_start < 0) {
iy_start = 0;
}
if (ix_end > (int32_t)width) {
ix_end = width;
}
if (iy_end > (int32_t)height) {
iy_end = height;
}
double r_sum = 0.0, g_sum = 0.0, b_sum = 0.0;
double total_weight = 0.0;
for (int32_t sy = iy_start; sy < iy_end; sy++) {
for (int32_t sx = ix_start; sx < ix_end; sx++) {
double left = sx;
double right = sx + 1;
double top = sy;
double bottom = sy + 1;
double overlap_left = left > x_start ? left : x_start;
double overlap_right = right < x_end ? right : x_end;
double overlap_top = top > y_start ? top : y_start;
double overlap_bottom = bottom < y_end ? bottom : y_end;
if (overlap_right > overlap_left && overlap_bottom > overlap_top) {
double weight =
(overlap_right - overlap_left) * (overlap_bottom - overlap_top);
total_weight += weight;
uint32_t src_idx = (sy * width + sx) * 4;
r_sum += data[src_idx] * weight;
g_sum += data[src_idx + 1] * weight;
b_sum += data[src_idx + 2] * weight;
}
}
}
if (total_weight > 0.0) {
resized_data[(out_y * *out_width + out_x) * 4] =
(uint8_t)(r_sum / total_weight + 0.5);
resized_data[(out_y * *out_width + out_x) * 4 + 1] =
(uint8_t)(g_sum / total_weight + 0.5);
resized_data[(out_y * *out_width + out_x) * 4 + 2] =
(uint8_t)(b_sum / total_weight + 0.5);
}
else {
resized_data[(out_y * *out_width + out_x) * 4] = 0;
resized_data[(out_y * *out_width + out_x) * 4 + 1] = 0;
resized_data[(out_y * *out_width + out_x) * 4 + 2] = 0;
}
resized_data[(out_y * *out_width + out_x) * 4 + 3] = 255;
}
else {
double src_x = (out_x + 0.5) / resize_x - 0.5;
double src_y = (out_y + 0.5) / resize_y - 0.5;
int32_t x0 = (int32_t)floor(src_x);
int32_t y0 = (int32_t)floor(src_y);
int32_t x1 = x0 + 1;
int32_t y1 = y0 + 1;
if (x0 < 0) {
x0 = 0;
}
if (y0 < 0) {
y0 = 0;
}
if (x1 >= (int32_t)width) {
x1 = width - 1;
}
if (y1 >= (int32_t)height) {
y1 = height - 1;
}
double dx = src_x - x0;
double dy = src_y - y0;
if (dx < 0) {
dx = 0;
}
if (dy < 0) {
dy = 0;
}
if (dx > 1) {
dx = 1;
}
if (dy > 1) {
dy = 1;
}
for (int32_t c = 0; c < 3; c++) {
uint8_t p00 = data[(y0 * width + x0) * 4 + c];
uint8_t p01 = data[(y0 * width + x1) * 4 + c];
uint8_t p10 = data[(y1 * width + x0) * 4 + c];
uint8_t p11 = data[(y1 * width + x1) * 4 + c];
double interpolated = p00 * (1 - dx) * (1 - dy) +
p01 * dx * (1 - dy) + p10 * (1 - dx) * dy +
p11 * dx * dy;
resized_data[(out_y * *out_width + out_x) * 4 + c] =
(uint8_t)(interpolated + 0.5);
}
resized_data[(out_y * *out_width + out_x) * 4 + 3] = 255;
}
}
}
return resized_data;
}
// File: src/convert_to_binary.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef FLATCV_AMALGAMATION
#include "convert_to_binary.h"
#include "parse_hex_color.h"
#else
#include "flatcv.h"
#endif
/**
* Convert an image to binary format based on foreground and background colors.
* Return a grayscale image where the foreground color is white and the
* background color is black.
*
* @param image_data Pointer to the input image data (RGBA format).
* @param width Width of the image.
* @param height Height of the image.
* @param foreground_hex Hex color code for the foreground color (e.g.,
* "FF0000").
* @param background_hex Hex color code for the background color (e.g.,
* "00FF00").
*/
uint8_t *fcv_convert_to_binary(
const uint8_t *image_data,
int32_t width,
int32_t height,
const char *foreground_hex,
const char *background_hex
) {
if (!image_data || !foreground_hex || !background_hex) {
return NULL;
}
if (width <= 0 || height <= 0) {
return NULL;
}
// Check for overflow: width * height (width and height already validated > 0)
if ((size_t)width > SIZE_MAX / (size_t)height) {
return NULL;
}
size_t num_pixels = (size_t)width * (size_t)height;
uint8_t *result = malloc(num_pixels);
if (!result) {
return NULL;
}
uint8_t r_fg, g_fg, b_fg;
fcv_parse_hex_color(foreground_hex, &r_fg, &g_fg, &b_fg);
uint8_t r_bg, g_bg, b_bg;
fcv_parse_hex_color(background_hex, &r_bg, &g_bg, &b_bg);
for (size_t i = 0; i < num_pixels; i++) {
size_t rgba_index = i * 4;
uint8_t r = image_data[rgba_index];
uint8_t g = image_data[rgba_index + 1];
uint8_t b = image_data[rgba_index + 2];
if (r == r_fg && g == g_fg && b == b_fg) {
// Convert foreground color to white
result[i] = 255;
}
else if (r == r_bg && g == g_bg && b == b_bg) {
// Convert background color to black
result[i] = 0;
}
else {
// Default to black for unmatched colors
result[i] = 0;
}
}
return result;
}
// File: src/corner_detection.c
#include <assert.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "binary_closing_disk.h"
#include "conversion.h"
#include "convert_to_binary.h"
#include "corner_peaks.h"
#include "draw.h"
#include "foerstner_corner.h"
#include "parse_hex_color.h"
#include "perspectivetransform.h"
#include "sobel_edge_detection.h"
#include "sort_corners.h"
#include "watershed_segmentation.h"
#ifdef DEBUG_LOGGING
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#endif
#else
#include "flatcv.h"
#endif
/** Count number of distinct colors in RGBA image
* 1. Convert to grayscale
* 2. Scale to 256x256 (save scale ratio for x and y)
* 3. Blur image
* 4. Create elevation map with Sobel
* 5. Flatten elevation map at center seed to avoid being trapped in a local
* minimum
* 6. Add black border to flood from all directions at once
* 7. `watershed(image=elevation_map, markers=markers)`
* Set center as marker for foreground basin and border for background basin
* 8. Check region count equals 2
* 9. Smooth result with binary closing
* 10. Use Foerstner corner detector
* (Harris detector corners are shifted inwards)
* 11. Sort corners
* 12. Select 4 corners with the largest angle while maintaining their order
* 13. Normalize corners based on scale ratio
*/
int32_t count_colors(const uint8_t *image, int32_t width, int32_t height) {
assert(image != NULL);
assert(width > 0);
assert(height > 0);
int32_t color_count = 0;
int32_t *color_map = (int32_t *)calloc(256 * 256 * 256, sizeof(int32_t));
if (!color_map) {
fprintf(stderr, "Error: Failed to allocate memory for color map\n");
return -1;
}
for (int32_t i = 0; i < width * height * 4; i += 4) {
uint8_t r = image[i];
uint8_t g = image[i + 1];
uint8_t b = image[i + 2];
uint8_t a = image[i + 3];
// Skip fully transparent pixels
if (a == 0) {
continue;
}
// Create a unique color key
int32_t color_key = (r << 16) | (g << 8) | b;
// Increment the count for this color
if (color_map[color_key] == 0) {
color_count++;
}
color_map[color_key]++;
}
free(color_map);
return color_count;
}
typedef struct {
int32_t width;
int32_t height;
int32_t channels;
const uint8_t *data;
} Image;
#ifdef DEBUG_LOGGING
void write_debug_img(Image img, const char *filename) {
int32_t result = stbi_write_png(
filename,
img.width,
img.height,
img.channels,
img.data,
img.width * img.channels
);
if (result == 0) {
fprintf(stderr, "Error: Failed to write debug image %s\n", filename);
}
}
#endif
/**
* Detect corners in the input image.
*/
Corners
fcv_detect_corners(const uint8_t *image, int32_t width, int32_t height) {
assert(image != NULL);
assert(width > 0);
assert(height > 0);
Corners default_corners = {
.tl_x = 0,
.tl_y = 0,
.tr_x = width - 1,
.tr_y = 0,
.br_x = width - 1,
.br_y = height - 1,
.bl_x = 0,
.bl_y = height - 1
};
// 1. Convert to grayscale
uint8_t const *grayscale_image = fcv_grayscale(width, height, image);
if (!grayscale_image) {
fprintf(stderr, "Error: Failed to convert image to grayscale\n");
return default_corners;
}
// 2. Resize image to 256x256
uint32_t out_width = 256;
uint32_t out_height = 256;
uint8_t const *resized_image = fcv_resize(
width,
height,
(double)out_width / width,
(double)out_height / height,
&out_width,
&out_height,
grayscale_image
);
free((void *)grayscale_image);
if (!resized_image) {
fprintf(stderr, "Error: Failed to resize image\n");
exit(EXIT_FAILURE);
}
#ifdef DEBUG_LOGGING
Image out_img = {
.width = out_width,
.height = out_height,
.channels = 4,
.data = resized_image
};
write_debug_img(out_img, "temp_1_resized_image.png");
#endif
// 3. Apply Gaussian blur
uint8_t const *blurred_image =
fcv_apply_gaussian_blur(out_width, out_height, 3.0, resized_image);
#ifndef DEBUG_LOGGING
free((void *)resized_image);
#endif
if (!blurred_image) {
fprintf(stderr, "Error: Failed to apply Gaussian blur\n");
exit(EXIT_FAILURE);
}
// 4. Create elevation map with Sobel edge detection
uint8_t *elevation_map = (uint8_t *)
fcv_sobel_edge_detection(out_width, out_height, 4, blurred_image);
free((void *)blurred_image);
if (!elevation_map) {
fprintf(stderr, "Error: Failed to create elevation map with Sobel\n");
exit(EXIT_FAILURE);
}
// 5. Flatten elevation map at center to not get trapped in a local minimum
fcv_draw_disk(
out_width,
out_height,
1, // Single channel grayscale
"000000",
24,
out_width / 2.0,
out_height / 2.0,
elevation_map
);
// 6. Add black border to flood from all directions at once
uint32_t bordered_width, bordered_height;
uint8_t *bordered_elevation_map = fcv_add_border(
out_width,
out_height,
1, // Single channel grayscale
"000000", // Black border
1, // Border width of 1 pixel
elevation_map,
&bordered_width,
&bordered_height
);
free(elevation_map);
if (!bordered_elevation_map) {
fprintf(stderr, "Error: Failed to add black border to elevation map\n");
exit(EXIT_FAILURE);
}
#ifdef DEBUG_LOGGING
out_img.data = bordered_elevation_map;
out_img.channels = 1; // Grayscale
out_img.width = bordered_width;
out_img.height = bordered_height;
write_debug_img(out_img, "temp_2_elevation_map.png");
#endif
// 7. Perform watershed segmentation
int32_t num_markers = 2;
Point2D *markers = malloc(num_markers * sizeof(Point2D));
if (!markers) {
fprintf(stderr, "Error: Failed to allocate memory for markers\n");
free((void *)bordered_elevation_map);
exit(EXIT_FAILURE);
}
// Set center as foreground marker and upper left corner as background marker
// Adjust for the added border (border width = 1)
markers[0] = (Point2D){.x = bordered_width / 2.0, .y = bordered_height / 2.0};
markers[1] = (Point2D){.x = 0, .y = 0};
uint8_t *segmented_image_wide = fcv_watershed_segmentation(
bordered_width,
bordered_height,
bordered_elevation_map,
markers,
num_markers,
false // No boundaries
);
free((void *)bordered_elevation_map);
free((void *)markers);
// Remove 1 pixel border from segmented image
uint8_t *segmented_image = malloc(out_width * out_height * 4);
if (!segmented_image) {
fprintf(stderr, "Error: Failed to allocate memory for segmented image\n");
free((void *)segmented_image_wide);
exit(EXIT_FAILURE);
}
for (uint32_t y = 1; y < bordered_height - 1; y++) {
memcpy(
&segmented_image[(y - 1) * out_width * 4],
&segmented_image_wide[(y * bordered_width + 1) * 4],
out_width * 4
);
}
#ifdef DEBUG_LOGGING
out_img.width = out_width;
out_img.height = out_height;
out_img.channels = 4;
out_img.data = segmented_image;
write_debug_img(out_img, "temp_3_watershed.png");
#endif
// 8. Check if the image has exactly 2 regions (foreground and background)
int32_t region_count = count_colors(segmented_image, out_width, out_height);
if (region_count != 2) {
fprintf(stderr, "Error: Expected 2 regions, found %d\n", region_count);
free((void *)segmented_image);
exit(EXIT_FAILURE);
}
// Convert red pixel (marker 1 - foreground) to white
// and green pixel (marker 2 - background) to black
uint8_t *segmented_binary = fcv_convert_to_binary(
segmented_image,
out_width,
out_height,
"FF0000", // red -> white
"00FF00" // green -> black
);
free((void *)segmented_image);
#ifdef DEBUG_LOGGING
out_img.data = segmented_binary;
out_img.channels = 1;
write_debug_img(out_img, "temp_4_watershed_binary.png");
#endif
// 9. Smooth the result
uint8_t *segmented_closed = fcv_binary_closing_disk(
segmented_binary,
out_width,
out_height,
12 // Closing radius
);
free((void *)segmented_binary);
if (!segmented_closed) {
fprintf(stderr, "Error: Failed to perform binary closing\n");
exit(EXIT_FAILURE);
}
#ifdef DEBUG_LOGGING
out_img.data = segmented_closed;
write_debug_img(out_img, "temp_5_segmented_closed.png");
#endif
// 10. Find corners in the closed image
uint8_t *corner_response = fcv_foerstner_corner(
out_width,
out_height,
segmented_closed,
1.5 // Sigma for Gaussian smoothing
);
free((void *)segmented_closed);
if (!corner_response) {
fprintf(stderr, "Error: Failed to compute corner response\n");
exit(EXIT_FAILURE);
}
// Extract `w` channel (error ellipse size) for visualization
uint8_t *w_channel = malloc(out_width * out_height);
if (!w_channel) {
fprintf(stderr, "Error: Failed to allocate memory for w channel\n");
free((void *)corner_response);
exit(EXIT_FAILURE);
}
for (uint32_t i = 0; i < out_width * out_height; i++) {
w_channel[i] = corner_response[i * 2]; // Extract `w` channel
}
#ifdef DEBUG_LOGGING
out_img.data = w_channel;
write_debug_img(out_img, "temp_6_corner_response.png");
#endif
free(w_channel);
// 10. Find corner peaks using thresholds
// Gradually decrease thresholds until it finds at least 4 corners
CornerPeaks *peaks = NULL;
double accuracy_thresh = 0.4;
double roundness_thresh = 0.2;
const double thresh_decrement = 0.05;
const double min_thresh = 0.01;
do {
if (peaks) {
free(peaks->points);
free(peaks);
}
peaks = fcv_corner_peaks(
out_width,
out_height,
corner_response,
16, // Minimum distance between peaks
accuracy_thresh,
roundness_thresh
);
if (!peaks || peaks->count < 4) {
accuracy_thresh -= thresh_decrement;
roundness_thresh -= thresh_decrement;
// Prevent thresholds from going negative
if (accuracy_thresh < min_thresh) {
accuracy_thresh = min_thresh;
}
if (roundness_thresh < min_thresh) {
roundness_thresh = min_thresh;
}
}
} while (peaks && peaks->count < 4 &&
(accuracy_thresh > min_thresh || roundness_thresh > min_thresh));
free((void *)corner_response);
if (!peaks) {
fprintf(stderr, "Error: Failed to find corner peaks\n");
exit(EXIT_FAILURE);
}
// First, sort all corners to get them in clockwise order
Point2D *sorted_result = malloc(peaks->count * sizeof(Point2D));
sort_corners(
width,
height,
out_width,
out_height,
peaks->points,
peaks->count,
sorted_result
);
// Scale corners back to original image dimensions
double scale_x = (double)width / out_width;
double scale_y = (double)height / out_height;
Corners sorted_corners;
if (peaks->count > 4) {
// Calculate angles for each corner using cross product method
double *angles = malloc(peaks->count * sizeof(double));
typedef struct {
uint32_t index;
double angle_abs;
} AngleIndex;
AngleIndex *angle_indices = malloc(peaks->count * sizeof(AngleIndex));
for (uint32_t i = 0; i < peaks->count; i++) {
// Get the three consecutive points for angle calculation
Point2D prev = sorted_result[(i - 1 + peaks->count) % peaks->count];
Point2D curr = sorted_result[i];
Point2D next = sorted_result[(i + 1) % peaks->count];
// Calculate vectors from current point
double a_x = prev.x - curr.x;
double a_y = prev.y - curr.y;
double b_x = next.x - curr.x;
double b_y = next.y - curr.y;
// Calculate lengths of vectors
double a_len = sqrt(a_x * a_x + a_y * a_y);
double b_len = sqrt(b_x * b_x + b_y * b_y);
if (a_len == 0 || b_len == 0) {
angles[i] = 0;
}
else {
// Calculate cross product (z-component in 2D)
double cross_product = (a_x * b_y - a_y * b_x) / (a_len * b_len);
// Clamp to [-1, 1] to avoid numerical errors
if (cross_product > 1.0) {
cross_product = 1.0;
}
if (cross_product < -1.0) {
cross_product = -1.0;
}
// Calculate angle in degrees
angles[i] = asin(cross_product) * 180.0 / M_PI;
}
angle_indices[i].index = i;
angle_indices[i].angle_abs = fabs(angles[i]);
}
// Sort indices by descending angle magnitude
for (uint32_t i = 0; i < peaks->count - 1; i++) {
for (uint32_t j = 0; j < peaks->count - 1 - i; j++) {
if (angle_indices[j].angle_abs < angle_indices[j + 1].angle_abs) {
AngleIndex temp = angle_indices[j];
angle_indices[j] = angle_indices[j + 1];
angle_indices[j + 1] = temp;
}
}
}
// Take top 4 indices and sort them to maintain original clockwise order
uint32_t top_4_indices[4];
for (uint32_t i = 0; i < 4; i++) {
top_4_indices[i] = angle_indices[i].index;
}
// Sort the top 4 indices to maintain original clockwise order
for (uint32_t i = 0; i < 3; i++) {
for (uint32_t j = 0; j < 3 - i; j++) {
if (top_4_indices[j] > top_4_indices[j + 1]) {
uint32_t temp = top_4_indices[j];
top_4_indices[j] = top_4_indices[j + 1];
top_4_indices[j + 1] = temp;
}
}
}
// Create final corners struct directly from selected corners in clockwise
// order The first sorted corner becomes top-left, and we maintain clockwise
// sequence
sorted_corners =
(Corners){.tl_x = sorted_result[top_4_indices[0]].x * scale_x,
.tl_y = sorted_result[top_4_indices[0]].y * scale_y,
.tr_x = sorted_result[top_4_indices[1]].x * scale_x,
.tr_y = sorted_result[top_4_indices[1]].y * scale_y,
.br_x = sorted_result[top_4_indices[2]].x * scale_x,
.br_y = sorted_result[top_4_indices[2]].y * scale_y,
.bl_x = sorted_result[top_4_indices[3]].x * scale_x,
.bl_y = sorted_result[top_4_indices[3]].y * scale_y};
free(angles);
free(angle_indices);
}
else {
// Use all available corners if we have 4 or fewer, already in clockwise
// order
sorted_corners =
(Corners){.tl_x = sorted_result[0].x * scale_x,
.tl_y = sorted_result[0].y * scale_y,
.tr_x = sorted_result[1 % peaks->count].x * scale_x,
.tr_y = sorted_result[1 % peaks->count].y * scale_y,
.br_x = sorted_result[2 % peaks->count].x * scale_x,
.br_y = sorted_result[2 % peaks->count].y * scale_y,
.bl_x = sorted_result[3 % peaks->count].x * scale_x,
.bl_y = sorted_result[3 % peaks->count].y * scale_y};
}
// Selecting the strongest four peaks can discard the starting corner and
// change the document bounds. Label the final quadrilateral again rather
// than inheriting the first surviving peak as top-left.
Point2D selected[4] = {
{sorted_corners.tl_x, sorted_corners.tl_y},
{sorted_corners.tr_x, sorted_corners.tr_y},
{sorted_corners.br_x, sorted_corners.br_y},
{sorted_corners.bl_x, sorted_corners.bl_y}
};
Point2D ordered[4];
sorted_corners =
sort_corners(width, height, width, height, selected, 4, ordered);
free(sorted_result);
#ifdef DEBUG_LOGGING
// Print peaks for debugging
fprintf(stderr, "Peaks:\n");
fprintf(stderr, "%.0f,%.0f\n", sorted_corners.tl_x, sorted_corners.tl_y);
fprintf(stderr, "%.0f,%.0f\n", sorted_corners.tr_x, sorted_corners.tr_y);
fprintf(stderr, "%.0f,%.0f\n", sorted_corners.br_x, sorted_corners.br_y);
fprintf(stderr, "%.0f,%.0f\n", sorted_corners.bl_x, sorted_corners.bl_y);
// Draw corner peaks on the grayscale and resized image
for (uint32_t i = 0; i < peaks->count; i++) {
// Print peak coordinates for debugging
fprintf(
stderr,
"Peak %u: (%.1f, %.1f)\n",
i,
peaks->points[i].x,
peaks->points[i].y
);
fcv_draw_circle(
out_width,
out_height,
4, // GRAY as RGBA (each value is the same)
"FFFF00", // Yellow color for corners
2, // Radius
peaks->points[i].x,
peaks->points[i].y,
(uint8_t *)resized_image
);
}
// Draw sorted corners with different colors to distinguish from peaks.
// Reuse the scale variables from above.
// (Inverted for drawing on resized image.)
double draw_scale_x = (double)out_width / width;
double draw_scale_y = (double)out_height / height;
// Draw top-left corner in red
fcv_draw_circle(
out_width,
out_height,
4,
"FF0000", // Red color for top-left
3, // Slightly larger radius to distinguish from peaks
sorted_corners.tl_x * draw_scale_x,
sorted_corners.tl_y * draw_scale_y,
(uint8_t *)resized_image
);
// Draw top-right corner in green
fcv_draw_circle(
out_width,
out_height,
4,
"00FF00", // Green color for top-right
3,
sorted_corners.tr_x * draw_scale_x,
sorted_corners.tr_y * draw_scale_y,
(uint8_t *)resized_image
);
// Draw bottom-right corner in blue
fcv_draw_circle(
out_width,
out_height,
4,
"0000FF", // Blue color for bottom-right
3,
sorted_corners.br_x * draw_scale_x,
sorted_corners.br_y * draw_scale_y,
(uint8_t *)resized_image
);
// Draw bottom-left corner in magenta
fcv_draw_circle(
out_width,
out_height,
4,
"FF00FF", // Magenta color for bottom-left
3,
sorted_corners.bl_x * draw_scale_x,
sorted_corners.bl_y * draw_scale_y,
(uint8_t *)resized_image
);
Image corners_img = {
.width = out_width,
.height = out_height,
.channels = 4,
.data = resized_image
};
write_debug_img(corners_img, "temp_7_corners.png");
free((void *)resized_image);
#endif
free(peaks);
return sorted_corners;
}
/**
* Wrapper function that allocates corners on heap for FFI.
*/
Corners *
fcv_detect_corners_ptr(const uint8_t *image, int32_t width, int32_t height) {
Corners corners = fcv_detect_corners(image, width, height);
Corners *result = malloc(sizeof(Corners));
if (result == NULL) {
return NULL;
}
*result = corners;
return result;
}
// File: src/corner_peaks.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef FLATCV_AMALGAMATION
#include "1_types.h"
#include "conversion.h"
#include "corner_peaks.h"
#else
#include "flatcv.h"
#endif
static bool is_local_maximum(
uint8_t const *data,
uint32_t width,
uint32_t height,
uint32_t x,
uint32_t y,
uint32_t channel
) {
if (x == 0 || y == 0 || x >= width - 1 || y >= height - 1) {
return false;
}
uint32_t center_idx = (y * width + x) * 2 + channel;
uint8_t center_val = data[center_idx];
if (center_val == 0) {
return false;
}
for (int32_t dy = -1; dy <= 1; dy++) {
for (int32_t dx = -1; dx <= 1; dx++) {
if (dx == 0 && dy == 0) {
continue;
}
uint32_t neighbor_idx = ((y + dy) * width + (x + dx)) * 2 + channel;
if (data[neighbor_idx] > center_val) {
return false;
}
}
}
return true;
}
static double euclidean_distance(Point2D a, Point2D b) {
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
/** Detect corner peaks in the image.
* This function finds local maxima in the corner response image
* and filters them based on specified thresholds.
*
* @param width Width of the image.
* @param height Height of the image.
* @param data Pointer to the 2 channel (w and q) corner response data.
* @param min_distance Minimum distance between peaks.
* @param accuracy_thresh Threshold for accuracy measure (w).
* @param roundness_thresh Threshold for roundness measure (q).
* @return Pointer to CornerPeaks structure containing detected peaks.
*/
CornerPeaks *fcv_corner_peaks(
uint32_t width,
uint32_t height,
uint8_t const *data,
uint32_t min_distance,
double accuracy_thresh,
double roundness_thresh
) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Validate thresholds
if (!isfinite(accuracy_thresh) || !isfinite(roundness_thresh)) {
return NULL;
}
// Check for overflow: width * height
if (width > SIZE_MAX / height) {
return NULL;
}
size_t num_pixels = (size_t)width * height;
// Check for overflow in candidates allocation
if (num_pixels > SIZE_MAX / sizeof(Point2D)) {
return NULL;
}
Point2D *candidates = malloc(sizeof(Point2D) * num_pixels);
if (!candidates) {
return NULL;
}
uint32_t candidate_count = 0;
for (uint32_t y = 1; y < height - 1; y++) {
for (uint32_t x = 1; x < width - 1; x++) {
uint32_t idx = (y * width + x) * 2;
// Convert normalized values (0-255) back to 0-1 range
double w = data[idx] / 255.0; // accuracy measure
double q = data[idx + 1] / 255.0; // roundness measure
if (q > roundness_thresh && w > accuracy_thresh &&
is_local_maximum(data, width, height, x, y, 0)) {
candidates[candidate_count].x = (double)x;
candidates[candidate_count].y = (double)y;
candidate_count++;
}
}
}
if (candidate_count == 0) {
free(candidates);
CornerPeaks *result = malloc(sizeof(CornerPeaks));
if (result) {
result->points = NULL;
result->count = 0;
}
return result;
}
bool *rejected = calloc(candidate_count, sizeof(bool));
if (!rejected) {
free(candidates);
return NULL;
}
for (uint32_t i = 0; i < candidate_count; i++) {
if (rejected[i]) {
continue;
}
for (uint32_t j = i + 1; j < candidate_count; j++) {
if (rejected[j]) {
continue;
}
if (euclidean_distance(candidates[i], candidates[j]) < min_distance) {
uint32_t i_idx =
((uint32_t)candidates[i].y * width + (uint32_t)candidates[i].x) * 2;
uint32_t j_idx =
((uint32_t)candidates[j].y * width + (uint32_t)candidates[j].x) * 2;
if (data[i_idx] >= data[j_idx]) {
rejected[j] = true;
}
else {
rejected[i] = true;
break;
}
}
}
}
uint32_t final_count = 0;
for (uint32_t i = 0; i < candidate_count; i++) {
if (!rejected[i]) {
final_count++;
}
}
Point2D *final_points = malloc(sizeof(Point2D) * final_count);
if (!final_points) {
free(candidates);
free(rejected);
return NULL;
}
uint32_t idx = 0;
for (uint32_t i = 0; i < candidate_count; i++) {
if (!rejected[i]) {
final_points[idx] = candidates[i];
idx++;
}
}
free(candidates);
free(rejected);
CornerPeaks *result = malloc(sizeof(CornerPeaks));
if (!result) {
free(final_points);
return NULL;
}
result->points = final_points;
result->count = final_count;
return result;
}
// File: src/crop.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "crop.h"
#else
#include "flatcv.h"
#endif
/**
* Crop an image.
*
* @param width Width of the original image.
* @param height Height of the original image.
* @param channels Number of channels in the image.
* @param data Pointer to the pixel data.
* @param x The x-coordinate of the top-left corner of the crop area.
* @param y The y-coordinate of the top-left corner of the crop area.
* @param new_width The width of the crop area.
* @param new_height The height of the crop area.
* @return Pointer to the new cropped image data.
*/
uint8_t *fcv_crop(
uint32_t width,
uint32_t height,
uint32_t channels,
uint8_t const *const data,
uint32_t x,
uint32_t y,
uint32_t new_width,
uint32_t new_height
) {
if (!data) {
return NULL;
}
if (width == 0 || height == 0 || channels == 0 || new_width == 0 ||
new_height == 0) {
return NULL;
}
// Check for overflow in x + new_width and y + new_height
if (x > UINT32_MAX - new_width || y > UINT32_MAX - new_height) {
return NULL;
}
if (x + new_width > width || y + new_height > height) {
fprintf(stderr, "Crop area is outside the original image bounds.\n");
return NULL;
}
// Check for overflow in memory allocation: new_width * new_height * channels
if (new_width > SIZE_MAX / new_height) {
return NULL;
}
size_t num_pixels = (size_t)new_width * new_height;
if (num_pixels > SIZE_MAX / channels) {
return NULL;
}
size_t alloc_size = num_pixels * channels;
uint8_t *cropped_data = malloc(alloc_size);
if (!cropped_data) {
fprintf(stderr, "Memory allocation failed for cropped image.\n");
return NULL;
}
size_t row_bytes = (size_t)new_width * channels;
for (uint32_t i = 0; i < new_height; ++i) {
size_t src_index = ((size_t)(y + i) * width + x) * channels;
size_t dst_index = (size_t)i * row_bytes;
memcpy(cropped_data + dst_index, data + src_index, row_bytes);
}
return cropped_data;
}
// File: src/draw.c
#include <assert.h>
#include <limits.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "draw.h"
#include "parse_hex_color.h"
#else
#include "flatcv.h"
#endif
/**
* Helper function to set a pixel to specified color (for circle drawing)
*/
void fcv_set_circle_pixel(
uint8_t *data,
uint32_t width,
uint32_t height,
uint32_t channels,
int32_t px,
int32_t py,
uint8_t r,
uint8_t g,
uint8_t b
) {
if (!data || width == 0 || height == 0 || channels == 0) {
return;
}
if (px >= 0 && px < (int32_t)width && py >= 0 && py < (int32_t)height) {
size_t pixel_index = ((size_t)py * width + px) * channels;
if (channels == 1) {
// Grayscale: use luminance formula
data[pixel_index] = (uint8_t)(0.299 * r + 0.587 * g + 0.114 * b);
}
else if (channels >= 3) {
data[pixel_index] = r; // R
data[pixel_index + 1] = g; // G
data[pixel_index + 2] = b; // B
if (channels == 4) {
data[pixel_index + 3] = 255; // A
}
}
}
}
/**
* Helper function to draw 8 symmetric points of a circle
*/
void fcv_draw_circle_points(
uint8_t *data,
uint32_t width,
uint32_t height,
uint32_t channels,
int32_t cx,
int32_t cy,
int32_t x,
int32_t y,
uint8_t r,
uint8_t g,
uint8_t b
) {
fcv_set_circle_pixel(data, width, height, channels, cx + x, cy + y, r, g, b);
fcv_set_circle_pixel(data, width, height, channels, cx - x, cy + y, r, g, b);
fcv_set_circle_pixel(data, width, height, channels, cx + x, cy - y, r, g, b);
fcv_set_circle_pixel(data, width, height, channels, cx - x, cy - y, r, g, b);
fcv_set_circle_pixel(data, width, height, channels, cx + y, cy + x, r, g, b);
fcv_set_circle_pixel(data, width, height, channels, cx - y, cy + x, r, g, b);
fcv_set_circle_pixel(data, width, height, channels, cx + y, cy - x, r, g, b);
fcv_set_circle_pixel(data, width, height, channels, cx - y, cy - x, r, g, b);
}
/**
* Helper function to fill horizontal lines for a filled circle (disk)
*/
void fcv_fill_disk_lines(
uint8_t *data,
uint32_t width,
uint32_t height,
uint32_t channels,
int32_t cx,
int32_t cy,
int32_t x,
int32_t y,
uint8_t r,
uint8_t g,
uint8_t b
) {
// Fill horizontal lines at y offsets
for (int32_t i = cx - x; i <= cx + x; i++) {
fcv_set_circle_pixel(data, width, height, channels, i, cy + y, r, g, b);
fcv_set_circle_pixel(data, width, height, channels, i, cy - y, r, g, b);
}
// Fill horizontal lines at x offsets
// (avoid duplicating center line when x == y)
if (x != y) {
for (int32_t i = cx - y; i <= cx + y; i++) {
fcv_set_circle_pixel(data, width, height, channels, i, cy + x, r, g, b);
fcv_set_circle_pixel(data, width, height, channels, i, cy - x, r, g, b);
}
}
}
/**
* Draw a circle on an image using Bresenham's circle algorithm
* (modifies in-place).
*
* @param width Width of the image.
* @param height Height of the image.
* @param channels Number of channels in the image (1, 3, or 4).
* @param hex_color Hex color code (e.g., "FF0000" for red, "#00FF00" for
* green).
* @param radius Radius of the circle.
* @param center_x X coordinate of the circle center.
* @param center_y Y coordinate of the circle center.
* @param data Pointer to the pixel data (modified in-place).
*/
void fcv_draw_circle(
uint32_t width,
uint32_t height,
uint32_t channels,
const char *hex_color,
double radius,
double center_x,
double center_y,
uint8_t *data
) {
if (!data || !hex_color || width == 0 || height == 0 || channels == 0) {
return;
}
// Validate radius and center coordinates
if (radius < 0 || !isfinite(radius) || !isfinite(center_x) ||
!isfinite(center_y)) {
return;
}
// Reject excessive radius to prevent integer overflow in Bresenham algorithm
if (radius > INT32_MAX / 2) {
return;
}
// Parse color
uint8_t r, g, b;
fcv_parse_hex_color(hex_color, &r, &g, &b);
int32_t radius_int32_t = (int32_t)radius;
int32_t cx = (int32_t)center_x;
int32_t cy = (int32_t)center_y;
// Bresenham's circle algorithm
int32_t x = 0;
int32_t y = radius_int32_t;
int32_t d = 3 - 2 * radius_int32_t;
// Initial points
fcv_draw_circle_points(data, width, height, channels, cx, cy, x, y, r, g, b);
while (y >= x) {
x++;
if (d > 0) {
y--;
d = d + 4 * (x - y) + 10;
}
else {
d = d + 4 * x + 6;
}
fcv_draw_circle_points(
data,
width,
height,
channels,
cx,
cy,
x,
y,
r,
g,
b
);
}
}
/**
* Draw a filled circle (disk) on an image using Bresenham's circle
* algorithm (modifies in-place).
*
* @param width Width of the image.
* @param height Height of the image.
* @param channels Number of channels in the image (1, 3, or 4).
* @param hex_color Hex color code (e.g., "FF0000" for red, "#00FF00" for
* green).
* @param radius Radius of the disk.
* @param center_x X coordinate of the disk center.
* @param center_y Y coordinate of the disk center.
* @param data Pointer to the pixel data (modified in-place).
*/
void fcv_draw_disk(
uint32_t width,
uint32_t height,
uint32_t channels,
const char *hex_color,
double radius,
double center_x,
double center_y,
uint8_t *data
) {
if (!data || !hex_color || width == 0 || height == 0 || channels == 0) {
return;
}
// Validate radius and center coordinates
if (radius < 0 || !isfinite(radius) || !isfinite(center_x) ||
!isfinite(center_y)) {
return;
}
// Reject excessive radius to prevent integer overflow in Bresenham algorithm
if (radius > INT32_MAX / 2) {
return;
}
// Parse color
uint8_t r, g, b;
fcv_parse_hex_color(hex_color, &r, &g, &b);
int32_t radius_int32_t = (int32_t)radius;
int32_t cx = (int32_t)center_x;
int32_t cy = (int32_t)center_y;
// Bresenham's circle algorithm for filled disk
int32_t x = 0;
int32_t y = radius_int32_t;
int32_t d = 3 - 2 * radius_int32_t;
// Initial filled lines
fcv_fill_disk_lines(data, width, height, channels, cx, cy, x, y, r, g, b);
while (y >= x) {
x++;
if (d > 0) {
y--;
d = d + 4 * (x - y) + 10;
}
else {
d = d + 4 * x + 6;
}
fcv_fill_disk_lines(data, width, height, channels, cx, cy, x, y, r, g, b);
}
}
/**
* Add a colored border around an image.
* Returns a new image with the border added.
*
* @param width Width of the input image.
* @param height Height of the input image.
* @param channels Number of channels in the image (1, 3, or 4).
* @param hex_color Hex color code for the border (e.g., "FF0000" for red).
* @param border_width Width of the border in pixels.
* @param input_data Pointer to the input pixel data.
* @param output_width Pointer to store the output image width.
* @param output_height Pointer to store the output image height.
* @return Pointer to the new image data with border, or NULL on failure.
*/
uint8_t *fcv_add_border(
uint32_t width,
uint32_t height,
uint32_t channels,
const char *hex_color,
uint32_t border_width,
uint8_t *input_data,
uint32_t *output_width,
uint32_t *output_height
) {
if (!input_data || !output_width || !output_height || !hex_color ||
border_width == 0 || channels == 0) {
return NULL;
}
if (width == 0 || height == 0) {
return NULL;
}
// Check for overflow in output dimensions
if (border_width > (UINT32_MAX - width) / 2 ||
border_width > (UINT32_MAX - height) / 2) {
return NULL;
}
// Parse border color
uint8_t r, g, b;
fcv_parse_hex_color(hex_color, &r, &g, &b);
// Calculate output dimensions
*output_width = width + 2 * border_width;
*output_height = height + 2 * border_width;
// Check for overflow in memory allocation: output_width * output_height *
// channels
if (*output_width > SIZE_MAX / *output_height) {
return NULL;
}
size_t num_pixels = (size_t)(*output_width) * (*output_height);
if (num_pixels > SIZE_MAX / channels) {
return NULL;
}
size_t output_size = num_pixels * channels;
uint8_t *output_data = malloc(output_size);
if (!output_data) {
return NULL;
}
// Fill the entire output image with border color
for (uint32_t y = 0; y < *output_height; y++) {
for (uint32_t x = 0; x < *output_width; x++) {
uint32_t pixel_index = (y * *output_width + x) * channels;
if (channels == 1) {
// Grayscale: use luminance formula
output_data[pixel_index] = (uint8_t)(0.299 * r + 0.587 * g + 0.114 * b);
}
else if (channels >= 3) {
output_data[pixel_index] = r; // R
output_data[pixel_index + 1] = g; // G
output_data[pixel_index + 2] = b; // B
if (channels == 4) {
output_data[pixel_index + 3] = 255; // A
}
}
}
}
// Copy the original image into the center
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
uint32_t src_index = (y * width + x) * channels;
uint32_t dst_index =
((y + border_width) * *output_width + (x + border_width)) * channels;
for (uint32_t c = 0; c < channels; c++) {
output_data[dst_index + c] = input_data[src_index + c];
}
}
}
return output_data;
}
// File: src/exif.c
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#ifndef FLATCV_AMALGAMATION
#include "exif.h"
#endif
static uint16_t read_u16(FILE *file, int is_little_endian) {
uint8_t buf[2];
if (fread(buf, 1, 2, file) != 2) {
return 0;
}
if (is_little_endian) {
return (uint16_t)(buf[0] | (buf[1] << 8));
}
return (uint16_t)((buf[0] << 8) | buf[1]);
}
static uint32_t read_u32(FILE *file, int is_little_endian) {
uint8_t buf[4];
if (fread(buf, 1, 4, file) != 4) {
return 0;
}
if (is_little_endian) {
return (uint32_t)(buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24));
}
return (uint32_t)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]);
}
/**
* Parse the orientation tag from a TIFF/EXIF block. `tiff_start` must point
* to the first byte of the TIFF header (the "II" / "MM" byte-order marker).
* Returns the orientation value (1-8) or 1 if not found.
*/
static int32_t read_orientation_from_tiff(FILE *file, long tiff_start) {
fseek(file, tiff_start, SEEK_SET);
uint8_t tiff_header[2];
if (fread(tiff_header, 1, 2, file) != 2) {
return 1;
}
int is_little_endian = (tiff_header[0] == 'I' && tiff_header[1] == 'I');
int is_big_endian = (tiff_header[0] == 'M' && tiff_header[1] == 'M');
if (!is_little_endian && !is_big_endian) {
return 1;
}
uint16_t magic = read_u16(file, is_little_endian);
if (magic != 42) {
return 1;
}
uint32_t ifd_offset = read_u32(file, is_little_endian);
fseek(file, tiff_start + ifd_offset, SEEK_SET);
uint16_t num_entries = read_u16(file, is_little_endian);
for (uint16_t i = 0; i < num_entries; i++) {
uint16_t tag = read_u16(file, is_little_endian);
(void)read_u16(file, is_little_endian); // Skip type
(void)read_u32(file, is_little_endian); // Skip count
if (tag == 0x0112) { // Orientation
return (int32_t)read_u16(file, is_little_endian);
}
else {
fseek(file, 4, SEEK_CUR); // Skip value/offset
}
}
return 1;
}
/**
* Read the EXIF orientation from a JPEG file. `file` must be positioned just
* after the two-byte SOI marker (0xFFD8).
*/
static int32_t get_jpeg_orientation(FILE *file) {
uint8_t marker[2];
while (fread(marker, 1, 2, file) == 2) {
if (marker[0] != 0xFF) {
break;
}
if (marker[1] == 0xD9 || marker[1] == 0xDA) { // EOI or SOS
break;
}
uint16_t size = read_u16(file, 0); // Markers are always big-endian
if (marker[1] == 0xE1) { // APP1 - EXIF
uint8_t exif_header[6];
if (fread(exif_header, 1, 6, file) != 6) {
break;
}
if (memcmp(exif_header, "Exif\0\0", 6) != 0) {
fseek(file, size - 2 - 6, SEEK_CUR);
continue;
}
return read_orientation_from_tiff(file, ftell(file));
}
else {
fseek(file, size - 2, SEEK_CUR);
}
}
return 1;
}
/**
* Read the EXIF orientation from a PNG file. `file` must be positioned just
* after the eight-byte PNG signature.
*
* EXIF data is stored in an `eXIf` chunk whose payload is a raw TIFF block
* (no leading "Exif\0\0" prefix, unlike JPEG's APP1 segment).
*/
static int32_t get_png_orientation(FILE *file) {
uint8_t type[4];
while (1) {
uint32_t length = read_u32(file, 0); // Chunk fields are big-endian
if (fread(type, 1, 4, file) != 4) {
break;
}
if (memcmp(type, "eXIf", 4) == 0) {
return read_orientation_from_tiff(file, ftell(file));
}
if (memcmp(type, "IEND", 4) == 0) {
break;
}
fseek(file, (long)length + 4, SEEK_CUR); // Skip chunk data + CRC
}
return 1;
}
int32_t fcv_get_exif_orientation(char const *filename) {
FILE *file = fopen(filename, "rb");
if (!file) {
return 1;
}
uint8_t signature[8];
size_t read_count = fread(signature, 1, 8, file);
int32_t orientation = 1;
static uint8_t const png_signature[8] =
{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
if (read_count >= 2 && signature[0] == 0xFF && signature[1] == 0xD8) {
fseek(file, 2, SEEK_SET);
orientation = get_jpeg_orientation(file);
}
else if (read_count == 8 && memcmp(signature, png_signature, 8) == 0) {
orientation = get_png_orientation(file);
}
fclose(file);
return orientation;
}
// File: src/extract_document.c
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef FLATCV_AMALGAMATION
#include "perspectivetransform.h"
#else
#include "flatcv.h"
#endif
uint8_t *fcv_extract_document(
uint32_t width,
uint32_t height,
uint8_t const *const data,
uint32_t output_width,
uint32_t output_height
) {
if (!data) {
fprintf(stderr, "Error: NULL image data\n");
return NULL;
}
// Step 1: Detect corners of the document
Corners detected_corners = fcv_detect_corners(data, width, height);
// Step 2: Define destination corners (rectangular output)
Corners dst_corners = {
.tl_x = 0.0,
.tl_y = 0.0,
.tr_x = (double)(output_width - 1),
.tr_y = 0.0,
.br_x = (double)(output_width - 1),
.br_y = (double)(output_height - 1),
.bl_x = 0.0,
.bl_y = (double)(output_height - 1)
};
// Step 3: Calculate perspective transformation matrix
// Note: We need to map FROM destination TO source for the inverse transform
Matrix3x3 *transform_matrix =
fcv_calculate_perspective_transform(&dst_corners, &detected_corners);
if (!transform_matrix) {
fprintf(stderr, "Error: Failed to calculate perspective transform\n");
return NULL;
}
// Step 4: Apply the transformation
uint8_t *result = fcv_apply_matrix_3x3(
width,
height,
(uint8_t *)data,
output_width,
output_height,
transform_matrix
);
// Free the transformation matrix if it's not the identity matrix
if (transform_matrix &&
!(transform_matrix->m00 == 1.0 && transform_matrix->m01 == 0.0 &&
transform_matrix->m02 == 0.0 && transform_matrix->m10 == 0.0 &&
transform_matrix->m11 == 1.0 && transform_matrix->m12 == 0.0 &&
transform_matrix->m20 == 0.0 && transform_matrix->m21 == 0.0 &&
transform_matrix->m22 == 1.0)) {
free(transform_matrix);
}
return result;
}
uint8_t *fcv_extract_document_auto(
uint32_t width,
uint32_t height,
uint8_t const *const data,
uint32_t *output_width,
uint32_t *output_height
) {
if (!data || !output_width || !output_height) {
fprintf(stderr, "Error: NULL parameters\n");
return NULL;
}
// Step 1: Detect corners of the document
Corners detected_corners = fcv_detect_corners(data, width, height);
// Step 2: Calculate optimal output dimensions based on detected corners
// Calculate distances between corners to determine aspect ratio
double top_width = sqrt(
pow(detected_corners.tr_x - detected_corners.tl_x, 2) +
pow(detected_corners.tr_y - detected_corners.tl_y, 2)
);
double bottom_width = sqrt(
pow(detected_corners.br_x - detected_corners.bl_x, 2) +
pow(detected_corners.br_y - detected_corners.bl_y, 2)
);
double left_height = sqrt(
pow(detected_corners.bl_x - detected_corners.tl_x, 2) +
pow(detected_corners.bl_y - detected_corners.tl_y, 2)
);
double right_height = sqrt(
pow(detected_corners.br_x - detected_corners.tr_x, 2) +
pow(detected_corners.br_y - detected_corners.tr_y, 2)
);
// Use maximum dimensions to preserve detail
double max_width = fmax(top_width, bottom_width);
double max_height = fmax(left_height, right_height);
// Round to reasonable dimensions
*output_width = (uint32_t)(max_width + 0.5);
*output_height = (uint32_t)(max_height + 0.5);
// Ensure minimum reasonable size
if (*output_width < 100) {
*output_width = 100;
}
if (*output_height < 100) {
*output_height = 100;
}
// Step 3: Define destination corners (rectangular output)
Corners dst_corners = {
.tl_x = 0.0,
.tl_y = 0.0,
.tr_x = (double)(*output_width - 1),
.tr_y = 0.0,
.br_x = (double)(*output_width - 1),
.br_y = (double)(*output_height - 1),
.bl_x = 0.0,
.bl_y = (double)(*output_height - 1)
};
// Step 4: Calculate perspective transformation matrix
// Note: We need to map FROM destination TO source for the inverse transform
Matrix3x3 *transform_matrix =
fcv_calculate_perspective_transform(&dst_corners, &detected_corners);
if (!transform_matrix) {
fprintf(stderr, "Error: Failed to calculate perspective transform\n");
return NULL;
}
// Step 5: Apply the transformation
uint8_t *result = fcv_apply_matrix_3x3(
width,
height,
(uint8_t *)data,
*output_width,
*output_height,
transform_matrix
);
// Free the transformation matrix if it's not the identity matrix
if (transform_matrix &&
!(transform_matrix->m00 == 1.0 && transform_matrix->m01 == 0.0 &&
transform_matrix->m02 == 0.0 && transform_matrix->m10 == 0.0 &&
transform_matrix->m11 == 1.0 && transform_matrix->m12 == 0.0 &&
transform_matrix->m20 == 0.0 && transform_matrix->m21 == 0.0 &&
transform_matrix->m22 == 1.0)) {
free(transform_matrix);
}
return result;
}
// File: src/flip.c
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifndef FLATCV_AMALGAMATION
#include "flip.h"
#else
#include "flatcv.h"
#endif
/**
* Flip an image horizontally (mirror along vertical axis).
*
* @param width Width of the image.
* @param height Height of the image.
* @param data Pointer to the pixel data.
* @return Pointer to the flipped image data.
*/
uint8_t *
fcv_flip_x(uint32_t width, uint32_t height, uint8_t const *const data) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t num_pixels = (size_t)width * height;
if (num_pixels > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = num_pixels * 4;
uint8_t *flipped_data = malloc(img_length_byte);
if (!flipped_data) { // Memory allocation failed
return NULL;
}
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
size_t src_index = ((size_t)y * width + x) * 4;
size_t dst_index = ((size_t)y * width + (width - 1 - x)) * 4;
flipped_data[dst_index] = data[src_index]; // R
flipped_data[dst_index + 1] = data[src_index + 1]; // G
flipped_data[dst_index + 2] = data[src_index + 2]; // B
flipped_data[dst_index + 3] = data[src_index + 3]; // A
}
}
return flipped_data;
}
/**
* Flip an image vertically (mirror along horizontal axis).
*
* @param width Width of the image.
* @param height Height of the image.
* @param data Pointer to the pixel data.
* @return Pointer to the flipped image data.
*/
uint8_t *
fcv_flip_y(uint32_t width, uint32_t height, uint8_t const *const data) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t num_pixels = (size_t)width * height;
if (num_pixels > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = num_pixels * 4;
uint8_t *flipped_data = malloc(img_length_byte);
if (!flipped_data) { // Memory allocation failed
return NULL;
}
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
size_t src_index = ((size_t)y * width + x) * 4;
size_t dst_index = ((size_t)(height - 1 - y) * width + x) * 4;
flipped_data[dst_index] = data[src_index]; // R
flipped_data[dst_index + 1] = data[src_index + 1]; // G
flipped_data[dst_index + 2] = data[src_index + 2]; // B
flipped_data[dst_index + 3] = data[src_index + 3]; // A
}
}
return flipped_data;
}
/**
* Transpose an image (flip along main diagonal).
*/
uint8_t *
fcv_transpose(uint32_t width, uint32_t height, uint8_t const *const data) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t num_pixels = (size_t)width * height;
if (num_pixels > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = num_pixels * 4;
uint8_t *transposed_data = malloc(img_length_byte);
if (!transposed_data) {
return NULL;
}
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
size_t src_index = ((size_t)y * width + x) * 4;
size_t dst_index = ((size_t)x * height + y) * 4;
transposed_data[dst_index] = data[src_index];
transposed_data[dst_index + 1] = data[src_index + 1];
transposed_data[dst_index + 2] = data[src_index + 2];
transposed_data[dst_index + 3] = data[src_index + 3];
}
}
return transposed_data;
}
/**
* Transverse an image (flip along anti-diagonal).
*/
uint8_t *
fcv_transverse(uint32_t width, uint32_t height, uint8_t const *const data) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t num_pixels = (size_t)width * height;
if (num_pixels > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = num_pixels * 4;
uint8_t *transposed_data = malloc(img_length_byte);
if (!transposed_data) {
return NULL;
}
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
size_t src_index = ((size_t)y * width + x) * 4;
size_t dst_index =
((size_t)(width - 1 - x) * height + (height - 1 - y)) * 4;
transposed_data[dst_index] = data[src_index];
transposed_data[dst_index + 1] = data[src_index + 1];
transposed_data[dst_index + 2] = data[src_index + 2];
transposed_data[dst_index + 3] = data[src_index + 3];
}
}
return transposed_data;
}
// File: src/flood_fill.c
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifndef FLATCV_AMALGAMATION
#include "1_types.h"
#include "flood_fill.h"
#else
#include "flatcv.h"
#endif
/**
* Flood fill segmentation
* using a seeded breadth-first traversal with threshold-based inclusion
*
* Starting from each marker, expands across neighboring pixels whose
* grayscale values stay within the given threshold of that marker's value.
* Every reached pixel is set to `new_value`.
* Each marker is filled independently (relative to its own seed value),
* so the result is the union of all individual fills.
*
* @param width Width of the image.
* @param height Height of the image.
* @param grayscale_data Pointer to the input grayscale image data.
* @param markers Seeds from which to start the flood fill.
* @param num_markers Number of seeds.
* @param threshold Maximum absolute grayscale difference from the seed value.
* @param new_value Replacement value written to filled pixels.
* @param connectivity 4 (edge neighbors) or 8 (edge and diagonal neighbors).
* @return Pointer to the filled image data,
* or NULL on invalid input or allocation failure.
*/
uint8_t *fcv_flood_fill(
uint32_t width,
uint32_t height,
uint8_t const *const grayscale_data,
Point2D const *markers,
uint32_t num_markers,
uint8_t threshold,
uint8_t new_value,
uint8_t connectivity
) {
if (!grayscale_data || !markers || num_markers == 0 || width == 0 ||
height == 0 || width > INT32_MAX || height > INT32_MAX ||
(connectivity != 4 && connectivity != 8)) {
return NULL;
}
// Range-check as double before casting: out-of-range float-to-int
// conversion is undefined behavior, and truncation would map e.g. -0.5 to 0.
for (uint32_t m = 0; m < num_markers; m++) {
double x = markers[m].x;
double y = markers[m].y;
if (!isfinite(x) || !isfinite(y) || x < 0 || y < 0 || x >= (double)width ||
y >= (double)height) {
return NULL;
}
}
if (width > SIZE_MAX / height) {
return NULL;
}
size_t img_length_px = (size_t)width * height;
if (img_length_px > SIZE_MAX / sizeof(size_t)) {
return NULL;
}
uint8_t *output_data = malloc(img_length_px);
bool *visited = calloc(img_length_px, sizeof(bool));
// Each pixel is enqueued at most once per seed,
// so a linear (non-wrapping) queue of image size suffices.
// After a seed's traversal, it also holds exactly the visited pixels.
size_t *queue = malloc(img_length_px * sizeof(size_t));
if (!output_data || !visited || !queue) {
free(output_data);
free(visited);
free(queue);
return NULL;
}
memcpy(output_data, grayscale_data, img_length_px);
// First 4 entries are edge neighbors, last 4 are diagonals
int32_t const dx[] = {-1, 1, 0, 0, -1, 1, -1, 1};
int32_t const dy[] = {0, 0, -1, 1, -1, -1, 1, 1};
for (uint32_t m = 0; m < num_markers; m++) {
uint32_t seed_x = (uint32_t)markers[m].x;
uint32_t seed_y = (uint32_t)markers[m].y;
size_t seed_idx = (size_t)seed_y * width + seed_x;
uint8_t target_value = grayscale_data[seed_idx];
size_t head = 0;
size_t tail = 0;
queue[tail++] = seed_idx;
visited[seed_idx] = true;
while (head < tail) {
size_t idx = queue[head++];
output_data[idx] = new_value;
int32_t x = (int32_t)(idx % width);
int32_t y = (int32_t)(idx / width);
for (uint8_t d = 0; d < connectivity; d++) {
int32_t nx = x + dx[d];
int32_t ny = y + dy[d];
if (nx < 0 || nx >= (int32_t)width || ny < 0 || ny >= (int32_t)height) {
continue;
}
size_t neighbor_idx = (size_t)ny * width + (size_t)nx;
if (visited[neighbor_idx]) {
continue;
}
int32_t diff = (int32_t)grayscale_data[neighbor_idx] - target_value;
if (diff < 0) {
diff = -diff;
}
if (diff > threshold) {
continue;
}
visited[neighbor_idx] = true;
queue[tail++] = neighbor_idx;
}
}
// Reset only the pixels visited by this seed,
// so the next seed can traverse them relative to its own value
for (size_t i = 0; i < tail; i++) {
visited[queue[i]] = false;
}
}
free(queue);
free(visited);
return output_data;
}
// File: src/foerstner_corner.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "conversion.h"
#include "perspectivetransform.h"
#else
#include "flatcv.h"
#endif
/** Implementation of the Foerstner corner measure response image.
* Expected input is a grayscale image.
*/
uint8_t *fcv_foerstner_corner(
uint32_t width,
uint32_t height,
uint8_t const *const gray_data,
double sigma
) {
if (!gray_data || width == 0 || height == 0) {
return NULL;
}
// Allocate memory for gradient images
double *grad_x = calloc(width * height, sizeof(double));
double *grad_y = calloc(width * height, sizeof(double));
if (!grad_x || !grad_y) {
free(grad_x);
free(grad_y);
return NULL;
}
// Compute image gradients using Sobel-like operators
for (uint32_t y = 1; y < height - 1; y++) {
for (uint32_t x = 1; x < width - 1; x++) {
uint32_t idx = y * width + x;
// Sobel X kernel: [-1 0 1; -2 0 2; -1 0 1]
double gx = 0.0;
gx += -1.0 * gray_data[(y - 1) * width + (x - 1)];
gx += 1.0 * gray_data[(y - 1) * width + (x + 1)];
gx += -2.0 * gray_data[y * width + (x - 1)];
gx += 2.0 * gray_data[y * width + (x + 1)];
gx += -1.0 * gray_data[(y + 1) * width + (x - 1)];
gx += 1.0 * gray_data[(y + 1) * width + (x + 1)];
// Sobel Y kernel: [-1 -2 -1; 0 0 0; 1 2 1]
double gy = 0.0;
gy += -1.0 * gray_data[(y - 1) * width + (x - 1)];
gy += -2.0 * gray_data[(y - 1) * width + x];
gy += -1.0 * gray_data[(y - 1) * width + (x + 1)];
gy += 1.0 * gray_data[(y + 1) * width + (x - 1)];
gy += 2.0 * gray_data[(y + 1) * width + x];
gy += 1.0 * gray_data[(y + 1) * width + (x + 1)];
grad_x[idx] = gx / 8.0; // Normalize
grad_y[idx] = gy / 8.0;
}
}
// Compute gradient products for structure tensor
double *Axx = malloc(width * height * sizeof(double));
double *Axy = malloc(width * height * sizeof(double));
double *Ayy = malloc(width * height * sizeof(double));
if (!Axx || !Axy || !Ayy) {
free(grad_x);
free(grad_y);
free(Axx);
free(Axy);
free(Ayy);
return NULL;
}
for (uint32_t i = 0; i < width * height; i++) {
Axx[i] = grad_x[i] * grad_x[i];
Axy[i] = grad_x[i] * grad_y[i];
Ayy[i] = grad_y[i] * grad_y[i];
}
// Apply Gaussian smoothing to structure tensor elements
// For simplicity, use a basic box filter approximation when sigma is small
int32_t kernel_size = (int32_t)(3 * sigma) | 1; // Ensure odd size
if (kernel_size < 3) {
kernel_size = 3;
}
int32_t half_kernel = kernel_size / 2;
double *Axx_smooth = calloc(width * height, sizeof(double));
double *Axy_smooth = calloc(width * height, sizeof(double));
double *Ayy_smooth = calloc(width * height, sizeof(double));
if (!Axx_smooth || !Axy_smooth || !Ayy_smooth) {
free(grad_x);
free(grad_y);
free(Axx);
free(Axy);
free(Ayy);
free(Axx_smooth);
free(Axy_smooth);
free(Ayy_smooth);
return NULL;
}
// Simple box filter smoothing
for (uint32_t y = half_kernel; y < height - half_kernel; y++) {
for (uint32_t x = half_kernel; x < width - half_kernel; x++) {
uint32_t idx = y * width + x;
double sum_xx = 0.0, sum_xy = 0.0, sum_yy = 0.0;
int32_t count = 0;
for (int32_t ky = -half_kernel; ky <= half_kernel; ky++) {
for (int32_t kx = -half_kernel; kx <= half_kernel; kx++) {
int32_t sample_idx = (y + ky) * width + (x + kx);
sum_xx += Axx[sample_idx];
sum_xy += Axy[sample_idx];
sum_yy += Ayy[sample_idx];
count++;
}
}
Axx_smooth[idx] = sum_xx / count;
Axy_smooth[idx] = sum_xy / count;
Ayy_smooth[idx] = sum_yy / count;
}
}
// Compute Foerstner measures w and q
uint8_t *result = malloc(width * height * 2); // 2 channels: w, q
if (!result) {
free(grad_x);
free(grad_y);
free(Axx);
free(Axy);
free(Ayy);
free(Axx_smooth);
free(Axy_smooth);
free(Ayy_smooth);
return NULL;
}
double max_w = 0.0, max_q = 0.0;
double *w_values = malloc(width * height * sizeof(double));
double *q_values = malloc(width * height * sizeof(double));
if (!w_values || !q_values) {
free(result);
free(w_values);
free(q_values);
free(grad_x);
free(grad_y);
free(Axx);
free(Axy);
free(Ayy);
free(Axx_smooth);
free(Axy_smooth);
free(Ayy_smooth);
return NULL;
}
// First pass: compute w and q values and find maximum for normalization
for (uint32_t i = 0; i < width * height; i++) {
double det_A =
Axx_smooth[i] * Ayy_smooth[i] - Axy_smooth[i] * Axy_smooth[i];
double trace_A = Axx_smooth[i] + Ayy_smooth[i];
double w = 0.0, q = 0.0;
if (fabs(trace_A) > 1e-10) { // Avoid division by zero
w = det_A / trace_A;
q = 4.0 * det_A / (trace_A * trace_A);
}
w_values[i] = fmax(0.0, w); // Ensure non-negative
q_values[i] = fmax(0.0, q);
if (w_values[i] > max_w) {
max_w = w_values[i];
}
if (q_values[i] > max_q) {
max_q = q_values[i];
}
}
// Second pass: normalize and convert to bytes
for (uint32_t i = 0; i < width * height; i++) {
uint8_t w_byte = 0, q_byte = 0;
if (max_w > 0.0) {
w_byte = (uint8_t)(255.0 * w_values[i] / max_w);
}
if (max_q > 0.0) {
q_byte = (uint8_t)(255.0 * q_values[i] / max_q);
}
result[i * 2] = w_byte; // w measure
result[i * 2 + 1] = q_byte; // q measure
}
free(w_values);
free(q_values);
// Cleanup
free(grad_x);
free(grad_y);
free(Axx);
free(Axy);
free(Ayy);
free(Axx_smooth);
free(Axy_smooth);
free(Ayy_smooth);
return result;
}
// File: src/histogram.c
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef FLATCV_AMALGAMATION
#include "histogram.h"
#else
#include "flatcv.h"
#endif
/**
* Generate a histogram visualization image from input image data.
* For grayscale images, creates a single histogram.
* For RGB(A) images, creates overlapping histograms for each channel.
*
* @param width Width of the input image.
* @param height Height of the input image.
* @param channels Number of channels in the input image (1, 3, or 4).
* @param data Pointer to the input pixel data.
* @param out_width Pointer to store the output histogram width.
* @param out_height Pointer to store the output histogram height.
* @return Pointer to the histogram image data (RGBA format).
*/
uint8_t *fcv_generate_histogram(
uint32_t width,
uint32_t height,
uint32_t channels,
uint8_t const *const data,
uint32_t *out_width,
uint32_t *out_height
) {
if (!data || !out_width || !out_height) {
return NULL;
}
if (width == 0 || height == 0 || channels == 0) {
return NULL;
}
// Check for overflow in img_size calculation
if (width > SIZE_MAX / height) {
return NULL;
}
size_t img_size = (size_t)width * height;
// Validate that we can access all pixels
if (img_size > SIZE_MAX / channels) {
return NULL;
}
// Output histogram dimensions
const uint32_t hist_width = 256; // One pixel per brightness value
const uint32_t hist_height = 200; // Fixed height for visualization
*out_width = hist_width;
*out_height = hist_height;
// Output size is fixed: 256 * 200 * 4 = 204800 bytes (no overflow possible)
size_t output_size = (size_t)hist_width * hist_height * 4;
// Allocate output image (initialized to black background)
uint8_t *output = calloc(output_size, sizeof(uint8_t));
if (!output) {
return NULL;
}
// Initialize with black background and full alpha
for (uint32_t i = 0; i < (*out_width) * (*out_height); i++) {
output[i * 4 + 3] = 255; // Alpha channel
}
// Count histograms for each channel
uint32_t hist_r[256] = {0};
uint32_t hist_g[256] = {0};
uint32_t hist_b[256] = {0};
uint32_t hist_gray[256] = {0};
bool is_grayscale = (channels == 1);
if (channels == 4) {
// Check if it's actually grayscale (all RGB channels equal)
is_grayscale = true;
for (size_t i = 0; i < img_size && is_grayscale; i++) {
size_t idx = i * 4;
if (data[idx] != data[idx + 1] || data[idx] != data[idx + 2]) {
is_grayscale = false;
}
}
}
if (is_grayscale) {
// Single channel or grayscale image
for (size_t i = 0; i < img_size; i++) {
uint8_t value;
if (channels == 1) {
value = data[i];
}
else {
// Use the red channel for grayscale RGBA
value = data[i * 4];
}
hist_gray[value]++;
}
}
else {
// RGB or RGBA image - count each channel
for (size_t i = 0; i < img_size; i++) {
size_t idx = i * channels;
hist_r[data[idx]]++;
if (channels >= 3) {
hist_g[data[idx + 1]]++;
hist_b[data[idx + 2]]++;
}
}
}
// Find maximum count for scaling
uint32_t max_count = 0;
if (is_grayscale) {
for (int i = 0; i < 256; i++) {
if (hist_gray[i] > max_count) {
max_count = hist_gray[i];
}
}
}
else {
for (int i = 0; i < 256; i++) {
if (hist_r[i] > max_count) {
max_count = hist_r[i];
}
if (channels >= 3) {
if (hist_g[i] > max_count) {
max_count = hist_g[i];
}
if (hist_b[i] > max_count) {
max_count = hist_b[i];
}
}
}
}
if (max_count == 0) {
return output; // Return black image if no data
}
// Draw histogram bars
for (int x = 0; x < 256; x++) {
if (is_grayscale) {
// Draw white histogram for grayscale
uint32_t bar_height = (hist_gray[x] * hist_height) / max_count;
for (uint32_t y = 0; y < bar_height; y++) {
uint32_t output_x = x;
uint32_t output_y = hist_height - 1 - y;
uint32_t output_idx = (output_y * (*out_width) + output_x) * 4;
output[output_idx] = 255; // R
output[output_idx + 1] = 255; // G
output[output_idx + 2] = 255; // B
output[output_idx + 3] = 255; // A
}
}
else {
// Draw overlapping colored histograms for RGB
uint32_t bar_height_r = (hist_r[x] * hist_height) / max_count;
uint32_t bar_height_g =
(channels >= 3) ? (hist_g[x] * hist_height) / max_count : 0;
uint32_t bar_height_b =
(channels >= 3) ? (hist_b[x] * hist_height) / max_count : 0;
uint32_t max_bar_height = bar_height_r;
if (bar_height_g > max_bar_height) {
max_bar_height = bar_height_g;
}
if (bar_height_b > max_bar_height) {
max_bar_height = bar_height_b;
}
for (uint32_t y = 0; y < max_bar_height; y++) {
uint32_t output_x = x;
uint32_t output_y = hist_height - 1 - y;
uint32_t output_idx = (output_y * (*out_width) + output_x) * 4;
// Initialize to black
uint8_t r = 0, g = 0, b = 0;
// Add red channel contribution
if (y < bar_height_r) {
r = 255;
}
// Add green channel contribution
if (channels >= 3 && y < bar_height_g) {
g = 255;
}
// Add blue channel contribution
if (channels >= 3 && y < bar_height_b) {
b = 255;
}
output[output_idx] = r; // R
output[output_idx + 1] = g; // G
output[output_idx + 2] = b; // B
output[output_idx + 3] = 255; // A
}
}
}
return output;
}
// File: src/parse_hex_color.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "parse_hex_color.h"
#else
#include "flatcv.h"
#endif
// Helper function to parse hex color code to RGB values
void fcv_parse_hex_color(
const char *hex_color,
uint8_t *r,
uint8_t *g,
uint8_t *b
) {
const char *hex = hex_color;
// Skip '#' if present
if (hex[0] == '#') {
hex++;
}
// Validate hex string length (should be 6 characters)
if (strlen(hex) != 6) {
// Default to white for invalid hex codes
*r = 255;
*g = 255;
*b = 255;
return;
}
// Validate that all characters are valid hex digits
for (int32_t i = 0; i < 6; i++) {
if (!((hex[i] >= '0' && hex[i] <= '9') ||
(hex[i] >= 'A' && hex[i] <= 'F') ||
(hex[i] >= 'a' && hex[i] <= 'f'))) {
// Default to white for invalid hex codes
*r = 255;
*g = 255;
*b = 255;
return;
}
}
// Parse hex values
uint32_t rgb_value;
sscanf(hex, "%x", &rgb_value);
*r = (rgb_value >> 16) & 0xFF;
*g = (rgb_value >> 8) & 0xFF;
*b = rgb_value & 0xFF;
}
// File: src/perspectivetransform.c
#include <assert.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "perspectivetransform.h"
#else
#include "flatcv.h"
#endif
// #define DEBUG_LOGGING
#ifdef DEBUG_LOGGING
#define log(msg) printf("DEBUG: %s\n", msg)
#else
#define log(msg) // No operation
#endif
/**
* Helper function to solve 8x8 linear system using Gaussian elimination
* Returns 1 on success, 0 on failure
*/
int32_t solve_linear_system(double A[8][8], double b[8], double x[8]) {
const int32_t n = 8;
const double epsilon = 1e-10;
int32_t i, j, k;
// Create augmented matrix [A|b] with extra safety margin
double aug[8][10]; // One extra column for safety
// Initialize augmented matrix
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
aug[i][j] = A[i][j];
}
aug[i][n] = b[i];
}
// Gaussian elimination with partial pivoting
for (i = 0; i < n; i++) {
// Find pivot
int32_t max_row = i;
double max_val = fabs(aug[i][i]);
for (k = i + 1; k < n; k++) {
if (fabs(aug[k][i]) > max_val) {
max_val = fabs(aug[k][i]);
max_row = k;
}
}
// Check for singularity
if (max_val < epsilon) {
log("Warning: Matrix is nearly singular\n");
return 0;
}
// Swap maximum row with current row
if (max_row != i) {
for (j = 0; j <= n; j++) {
double temp = aug[i][j];
aug[i][j] = aug[max_row][j];
aug[max_row][j] = temp;
}
}
// Eliminate column i
for (j = i + 1; j < n; j++) {
double factor = aug[j][i] / aug[i][i];
for (k = i; k <= n; k++) {
aug[j][k] -= factor * aug[i][k];
}
}
}
// Back substitution
for (i = n - 1; i >= 0; i--) {
if (fabs(aug[i][i]) < epsilon) {
log("Warning: Zero pivot encountered\n");
return 0;
}
x[i] = aug[i][n];
for (j = i + 1; j < n; j++) {
x[i] -= aug[i][j] * x[j];
}
x[i] /= aug[i][i];
// Check for invalid results
if (isnan(x[i]) || isinf(x[i])) {
log("Warning: Invalid result detected\n");
return 0;
}
}
return 1;
}
/**
* Calculate the perspective transformation matrix
* from the source and destination corner coordinates.
*/
Matrix3x3 *fcv_calculate_perspective_transform(
Corners *src_corners,
Corners *dst_corners
) {
// Initialize matrices with zeros
double A[8][8] = {{0}};
double b[8] = {0};
double x[8] = {0};
// Identity matrix as fallback
static Matrix3x3 identity = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0};
if (!src_corners || !dst_corners) {
log("Error: NULL pointer passed to calculate_perspective_transform\n");
return &identity;
}
#ifdef DEBUG_LOGGING
fprintf(stderr, "[C] Calculating perspective transform:\n");
fprintf(
stderr,
"src_corners:\ntl(%f, %f)\ntr(%f, %f)\nbr(%f, %f)\nbl(%f, %f)\n\n",
src_corners->tl_x,
src_corners->tl_y,
src_corners->tr_x,
src_corners->tr_y,
src_corners->br_x,
src_corners->br_y,
src_corners->bl_x,
src_corners->bl_y
);
fprintf(
stderr,
"dst_corners:\ntl(%f, %f)\ntr(%f, %f)\nbr(%f, %f)\nbl(%f, %f)\n\n",
dst_corners->tl_x,
dst_corners->tl_y,
dst_corners->tr_x,
dst_corners->tr_y,
dst_corners->br_x,
dst_corners->br_y,
dst_corners->bl_x,
dst_corners->bl_y
);
#endif
// Validate input coordinates
if (isnan(src_corners->tl_x) || isnan(src_corners->tl_y) ||
isnan(src_corners->tr_x) || isnan(src_corners->tr_y) ||
isnan(src_corners->br_x) || isnan(src_corners->br_y) ||
isnan(src_corners->bl_x) || isnan(src_corners->bl_y) ||
isnan(dst_corners->tl_x) || isnan(dst_corners->tl_y) ||
isnan(dst_corners->tr_x) || isnan(dst_corners->tr_y) ||
isnan(dst_corners->br_x) || isnan(dst_corners->br_y) ||
isnan(dst_corners->bl_x) || isnan(dst_corners->bl_y)) {
log("Error: Invalid coordinates (NaN) detected\n");
return &identity;
}
// Set up the system of equations
for (int32_t i = 0; i < 4; i++) {
double srcX = 0.0, srcY = 0.0, dstX = 0.0, dstY = 0.0;
// Safely extract coordinates
switch (i) {
case 0: // Top-left
srcX = src_corners->tl_x;
srcY = src_corners->tl_y;
dstX = dst_corners->tl_x;
dstY = dst_corners->tl_y;
break;
case 1: // Top-right
srcX = src_corners->tr_x;
srcY = src_corners->tr_y;
dstX = dst_corners->tr_x;
dstY = dst_corners->tr_y;
break;
case 2: // Bottom-right
srcX = src_corners->br_x;
srcY = src_corners->br_y;
dstX = dst_corners->br_x;
dstY = dst_corners->br_y;
break;
case 3: // Bottom-left
srcX = src_corners->bl_x;
srcY = src_corners->bl_y;
dstX = dst_corners->bl_x;
dstY = dst_corners->bl_y;
break;
}
// Validate extracted coordinates
if (isinf(srcX) || isinf(srcY) || isinf(dstX) || isinf(dstY)) {
log("Error: Invalid coordinates (Inf) detected\n");
return &identity;
}
// First four equations for x coordinates
A[i][0] = srcX;
A[i][1] = srcY;
A[i][2] = 1.0;
A[i][6] = -srcX * dstX;
A[i][7] = -srcY * dstX;
b[i] = dstX;
// Last four equations for y coordinates
A[i + 4][3] = srcX;
A[i + 4][4] = srcY;
A[i + 4][5] = 1.0;
A[i + 4][6] = -srcX * dstY;
A[i + 4][7] = -srcY * dstY;
b[i + 4] = dstY;
}
log("Solve the system of equations …\n");
if (!solve_linear_system(A, b, x)) {
log("Failed to solve system, returning identity matrix\n");
return &identity;
}
// Validate solution
for (int32_t i = 0; i < 8; i++) {
if (isnan(x[i]) || isinf(x[i]) || fabs(x[i]) > 1e6) {
log("Error: Invalid solution values detected\n");
return &identity;
}
}
Matrix3x3 *result = malloc(sizeof(Matrix3x3));
*result = (Matrix3x3){x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], 1.0};
#ifdef DEBUG_LOGGING
fprintf(stderr, "Result matrix:\n");
fprintf(stderr, "%f, %f, %f\n", result->m00, result->m01, result->m02);
fprintf(stderr, "%f, %f, %f\n", result->m10, result->m11, result->m12);
fprintf(stderr, "%f, %f, %f\n", result->m20, result->m21, result->m22);
#endif
// Final validation of the result matrix
if (isnan(result->m00) || isnan(result->m01) || isnan(result->m02) ||
isnan(result->m10) || isnan(result->m11) || isnan(result->m12) ||
isnan(result->m20) || isnan(result->m21) || isnan(result->m22) ||
isinf(result->m00) || isinf(result->m01) || isinf(result->m02) ||
isinf(result->m10) || isinf(result->m11) || isinf(result->m12) ||
isinf(result->m20) || isinf(result->m21) || isinf(result->m22)) {
log("Error: Invalid values in result matrix\n");
return &identity;
}
return result;
}
/**
* Apply the transformation matrix to the input image
* and store the result in the output image.
* Use bilinear interpolation to calculate final pixel values.
*/
uint8_t *fcv_apply_matrix_3x3(
int32_t in_width,
int32_t in_height,
uint8_t *in_data,
int32_t out_width,
int32_t out_height,
Matrix3x3 *tmat
) {
if (!in_data || !tmat) {
return NULL;
}
if (in_width <= 0 || in_height <= 0 || out_width <= 0 || out_height <= 0) {
return NULL;
}
// Check for overflow: out_width * out_height * 4 (dimensions already
// validated > 0)
if ((size_t)out_width > SIZE_MAX / (size_t)out_height) {
return NULL;
}
size_t out_pixels = (size_t)out_width * (size_t)out_height;
if (out_pixels > SIZE_MAX / 4) {
return NULL;
}
// Patch flip matrix if needed
if (fabs(tmat->m00 + 1.0) < 1e-9 && fabs(tmat->m11 + 1.0) < 1e-9 &&
tmat->m02 == 0.0 && tmat->m12 == 0.0) {
tmat->m02 = in_width - 1;
tmat->m12 = in_height - 1;
}
uint8_t *out_data = calloc(out_pixels * 4, sizeof(uint8_t));
if (!out_data) { // Memory allocation failed
return NULL;
}
// Iterate through every pixel in the output image
for (int32_t out_y = 0; out_y < out_height; ++out_y) {
for (int32_t out_x = 0; out_x < out_width; ++out_x) {
// Apply the inverse transformation to find the corresponding source pixel
double w = tmat->m20 * out_x + tmat->m21 * out_y + tmat->m22;
if (fabs(w) < 1e-10) {
continue; // Skip if w is too close to zero
}
double srcX = (tmat->m00 * out_x + tmat->m01 * out_y + tmat->m02) / w;
double srcY = (tmat->m10 * out_x + tmat->m11 * out_y + tmat->m12) / w;
// Convert source coordinates to integers
int32_t x0 = (int32_t)floor(srcX);
int32_t y0 = (int32_t)floor(srcY);
int32_t x1 = x0 + 1;
int32_t y1 = y0 + 1;
// Verify that the anchor pixel is inside the source image
if (x0 >= 0 && x0 < in_width && y0 >= 0 && y0 < in_height) {
// Clamp the neighbor coordinates so that a (degenerated)
// bilinear interpolation can be applied at the image borders.
int32_t x1c = (x1 < in_width) ? x1 : x0;
int32_t y1c = (y1 < in_height) ? y1 : y0;
double dx = srcX - x0;
double dy = srcY - y0;
// If a neighbour got clamped we force the corresponding weight to 0
if (x1c == x0) {
dx = 0.0;
}
if (y1c == y0) {
dy = 0.0;
}
uint8_t *p00 = &in_data[(y0 * in_width + x0) * 4];
uint8_t *p01 = &in_data[(y0 * in_width + x1c) * 4];
uint8_t *p10 = &in_data[(y1c * in_width + x0) * 4];
uint8_t *p11 = &in_data[(y1c * in_width + x1c) * 4];
for (int32_t c = 0; c < 4; ++c) {
out_data[(out_y * out_width + out_x) * 4 + c] =
(uint8_t)(p00[c] * (1 - dx) * (1 - dy) + p01[c] * dx * (1 - dy) +
p10[c] * (1 - dx) * dy + p11[c] * dx * dy);
}
}
}
}
return out_data;
}
// File: src/qr_code.c
#include <math.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef FLATCV_AMALGAMATION
#include "qr_code.h"
#else
#include "flatcv.h"
#endif
#define MAX_QR_CODES 10
#define MAX_CANDIDATES 200
#define MAX_TRIPLES 10
/* Max alignment patterns per QR. Version 40 has 7×7 = 49 grid positions,
of which 3 (top-left, top-right, bottom-left) overlap the finder patterns
and are skipped — leaving at most 46. */
#define QR_MAX_ALIGNMENTS 46
/* ---- Types ---- */
typedef struct {
float x, y, module_size;
} FinderPattern;
/* ---- Adaptive binarization ----
Hybrid thresholding: combines a global Otsu threshold with a local-variance
check via a mean/square integral image. In high-contrast windows we
threshold against the local mean (handles uneven illumination on e-ink
displays and perspective-lit photos); in low-contrast windows we fall back
to the global Otsu threshold (preserves large uniform dark/light regions
that a pure local mean would invert).
Output buffer: 0 for dark, 255 for light. */
static int compute_otsu_threshold(uint8_t const *gray, int w, int h) {
size_t n = (size_t)w * (size_t)h;
uint32_t hist[256] = {0};
for (size_t i = 0; i < n; i++) {
hist[gray[i]]++;
}
double total = (double)n;
double sum_all = 0.0;
for (int i = 0; i < 256; i++) {
sum_all += (double)i * hist[i];
}
double w_bg = 0.0, sum_bg = 0.0;
double max_var = 0.0;
int best_t = 128;
for (int t = 0; t < 256; t++) {
w_bg += hist[t];
if (w_bg == 0.0 || w_bg == total) {
continue;
}
sum_bg += (double)t * hist[t];
double w_fg = total - w_bg;
double mean_bg = sum_bg / w_bg;
double mean_fg = (sum_all - sum_bg) / w_fg;
double diff = mean_bg - mean_fg;
double var_between = w_bg * w_fg * diff * diff;
if (var_between > max_var) {
max_var = var_between;
best_t = t;
}
}
return best_t;
}
static uint8_t *binarize_global_otsu(uint8_t const *gray, int w, int h) {
if (w <= 0 || h <= 0) {
return NULL;
}
size_t n = (size_t)w * (size_t)h;
uint8_t *bin = malloc(n);
if (!bin) {
return NULL;
}
int t = compute_otsu_threshold(gray, w, h);
for (size_t i = 0; i < n; i++) {
bin[i] = (gray[i] < t) ? 0 : 255;
}
return bin;
}
static uint8_t *binarize_adaptive(uint8_t const *gray, int w, int h) {
if (w <= 0 || h <= 0) {
return NULL;
}
size_t n = (size_t)w * (size_t)h;
uint8_t *bin = malloc(n);
if (!bin) {
return NULL;
}
size_t iw = (size_t)w + 1;
uint64_t *ii_sum = calloc(iw * (size_t)(h + 1), sizeof(uint64_t));
uint64_t *ii_sq = calloc(iw * (size_t)(h + 1), sizeof(uint64_t));
if (!ii_sum || !ii_sq) {
free(bin);
free(ii_sum);
free(ii_sq);
return NULL;
}
for (int y = 0; y < h; y++) {
uint64_t row_sum = 0;
uint64_t row_sq = 0;
for (int x = 0; x < w; x++) {
uint8_t v = gray[y * w + x];
row_sum += v;
row_sq += (uint64_t)v * v;
size_t idx = (size_t)(y + 1) * iw + (size_t)(x + 1);
ii_sum[idx] = ii_sum[idx - iw] + row_sum;
ii_sq[idx] = ii_sq[idx - iw] + row_sq;
}
}
int global_t = compute_otsu_threshold(gray, w, h);
int block = (w < h ? w : h) / 8;
if (block < 15) {
block = 15;
}
if (block > 101) {
block = 101;
}
if ((block & 1) == 0) {
block++;
}
int half = block / 2;
/* Below min_std the window is treated as uniform and the global threshold
decides the pixel class. This keeps large uniform dark regions (e.g.
finder-pattern centers, dark backgrounds) from being inverted by a local
mean that sits right on the pixel value. */
const double min_std = 24.0;
for (int y = 0; y < h; y++) {
int y0 = y - half;
if (y0 < 0) {
y0 = 0;
}
int y1 = y + half;
if (y1 >= h) {
y1 = h - 1;
}
for (int x = 0; x < w; x++) {
int x0 = x - half;
if (x0 < 0) {
x0 = 0;
}
int x1 = x + half;
if (x1 >= w) {
x1 = w - 1;
}
double count = (double)((y1 - y0 + 1) * (x1 - x0 + 1));
size_t a = (size_t)(y1 + 1) * iw + (size_t)(x1 + 1);
size_t b = (size_t)y0 * iw + (size_t)(x1 + 1);
size_t c = (size_t)(y1 + 1) * iw + (size_t)x0;
size_t d = (size_t)y0 * iw + (size_t)x0;
double sum = (double)(ii_sum[a] - ii_sum[b] - ii_sum[c] + ii_sum[d]);
double sqsum = (double)(ii_sq[a] - ii_sq[b] - ii_sq[c] + ii_sq[d]);
double mean = sum / count;
double var = sqsum / count - mean * mean;
if (var < 0.0) {
var = 0.0;
}
double std = sqrt(var);
uint8_t gv = gray[y * w + x];
int dark;
if (std < min_std) {
dark = (gv < global_t);
}
else {
dark = ((double)gv < mean);
}
bin[y * w + x] = dark ? 0 : 255;
}
}
free(ii_sum);
free(ii_sq);
return bin;
}
/* ---- Sauvola binarization ----
T = mean * (1 + k * (std / R - 1)), k=0.34, R=128. Scales the threshold
by local std, which catches faded/low-contrast modules that the fixed
min_std fallback in binarize_adaptive misclassifies. Block deliberately
smaller than binarize_adaptive's so it stays responsive when modules are
only 2-3 px wide. */
static uint8_t *binarize_sauvola(uint8_t const *gray, int w, int h) {
if (w <= 0 || h <= 0) {
return NULL;
}
size_t n = (size_t)w * (size_t)h;
uint8_t *bin = malloc(n);
if (!bin) {
return NULL;
}
size_t iw = (size_t)w + 1;
uint64_t *ii_sum = calloc(iw * (size_t)(h + 1), sizeof(uint64_t));
uint64_t *ii_sq = calloc(iw * (size_t)(h + 1), sizeof(uint64_t));
if (!ii_sum || !ii_sq) {
free(bin);
free(ii_sum);
free(ii_sq);
return NULL;
}
for (int y = 0; y < h; y++) {
uint64_t row_sum = 0;
uint64_t row_sq = 0;
for (int x = 0; x < w; x++) {
uint8_t v = gray[y * w + x];
row_sum += v;
row_sq += (uint64_t)v * v;
size_t idx = (size_t)(y + 1) * iw + (size_t)(x + 1);
ii_sum[idx] = ii_sum[idx - iw] + row_sum;
ii_sq[idx] = ii_sq[idx - iw] + row_sq;
}
}
int block = (w < h ? w : h) / 12;
if (block < 11) {
block = 11;
}
if (block > 81) {
block = 81;
}
if ((block & 1) == 0) {
block++;
}
int half = block / 2;
const double k = 0.34;
const double R = 128.0;
for (int y = 0; y < h; y++) {
int y0 = y - half;
if (y0 < 0) {
y0 = 0;
}
int y1 = y + half;
if (y1 >= h) {
y1 = h - 1;
}
for (int x = 0; x < w; x++) {
int x0 = x - half;
if (x0 < 0) {
x0 = 0;
}
int x1 = x + half;
if (x1 >= w) {
x1 = w - 1;
}
double count = (double)((y1 - y0 + 1) * (x1 - x0 + 1));
size_t a = (size_t)(y1 + 1) * iw + (size_t)(x1 + 1);
size_t b = (size_t)y0 * iw + (size_t)(x1 + 1);
size_t c = (size_t)(y1 + 1) * iw + (size_t)x0;
size_t d = (size_t)y0 * iw + (size_t)x0;
double sum = (double)(ii_sum[a] - ii_sum[b] - ii_sum[c] + ii_sum[d]);
double sqsum = (double)(ii_sq[a] - ii_sq[b] - ii_sq[c] + ii_sq[d]);
double mean = sum / count;
double var = sqsum / count - mean * mean;
if (var < 0.0) {
var = 0.0;
}
double std = sqrt(var);
double thresh = mean * (1.0 + k * (std / R - 1.0));
uint8_t gv = gray[y * w + x];
bin[y * w + x] = ((double)gv < thresh) ? 0 : 255;
}
}
free(ii_sum);
free(ii_sq);
return bin;
}
/* ---- Hybrid binarization (zxing-cpp port) ----
Port of zxing-cpp's HybridBinarizer (core/src/HybridBinarizer.cpp, NEW
algorithm). Works in 8x8 blocks and computes a per-block threshold of
(min+max)/2 only when the dynamic range exceeds 24; low-contrast blocks
are marked "unknown" and inherit their threshold from a 5x5 smoothing
over the *known* neighbours, followed by raster flood-fill of any
surviving zeros. This is substantially more robust than binarize_adaptive
on low-contrast pastel palettes: flatcv's adaptive falls back to the
global Otsu threshold whenever local std<24, which misclassifies entire
interior regions; zxing's scheme instead propagates an edge-of-QR
threshold inward.
Output: 0 = dark, 255 = light. Returns NULL if image smaller than the
5x5 window (40 px) — caller should fall back to global Otsu. */
#define HB_BLOCK_SIZE 8
#define HB_WINDOW_BLOCKS 5
#define HB_WINDOW_SIZE (HB_BLOCK_SIZE * HB_WINDOW_BLOCKS)
#define HB_MIN_DYNAMIC_RANGE 24
static uint8_t *binarize_hybrid(uint8_t const *gray, int w, int h) {
if (w < HB_WINDOW_SIZE || h < HB_WINDOW_SIZE) {
return NULL;
}
int sub_w = (w + HB_BLOCK_SIZE - 1) / HB_BLOCK_SIZE;
int sub_h = (h + HB_BLOCK_SIZE - 1) / HB_BLOCK_SIZE;
size_t sub_n = (size_t)sub_w * (size_t)sub_h;
uint8_t *raw = malloc(sub_n);
uint8_t *smoothed = malloc(sub_n);
uint8_t *bin = malloc((size_t)w * (size_t)h);
if (!raw || !smoothed || !bin) {
free(raw);
free(smoothed);
free(bin);
return NULL;
}
/* Per-block threshold: (max+min)/2 if range exceeds min_dynamic_range,
else 0 (unknown — filled in by smoothing). Last row/col of blocks is
clamped so the 8x8 window stays fully inside the image. */
for (int by = 0; by < sub_h; by++) {
int y0 = by * HB_BLOCK_SIZE;
if (y0 > h - HB_BLOCK_SIZE) {
y0 = h - HB_BLOCK_SIZE;
}
for (int bx = 0; bx < sub_w; bx++) {
int x0 = bx * HB_BLOCK_SIZE;
if (x0 > w - HB_BLOCK_SIZE) {
x0 = w - HB_BLOCK_SIZE;
}
uint8_t mn = 255, mx = 0;
for (int yy = 0; yy < HB_BLOCK_SIZE; yy++) {
uint8_t const *row = gray + (size_t)(y0 + yy) * (size_t)w + x0;
for (int xx = 0; xx < HB_BLOCK_SIZE; xx++) {
uint8_t v = row[xx];
if (v < mn) {
mn = v;
}
if (v > mx) {
mx = v;
}
}
}
raw[(size_t)by * sub_w + bx] =
(mx - mn > HB_MIN_DYNAMIC_RANGE) ? (uint8_t)(((int)mx + mn) / 2) : 0;
}
}
/* 5x5 gaussian-like smoothing over non-zero thresholds (center counted
twice). Low-contrast blocks with no contrasty neighbour remain 0. */
const int R = HB_WINDOW_BLOCKS / 2;
for (int by = 0; by < sub_h; by++) {
for (int bx = 0; bx < sub_w; bx++) {
int left = bx;
if (left < R) {
left = R;
}
else if (left > sub_w - R - 1) {
left = sub_w - R - 1;
}
int top = by;
if (top < R) {
top = R;
}
else if (top > sub_h - R - 1) {
top = sub_h - R - 1;
}
int center = raw[(size_t)by * sub_w + bx];
int sum = center * 2;
int n = (center > 0) ? 2 : 0;
for (int dy = -R; dy <= R; dy++) {
for (int dx = -R; dx <= R; dx++) {
int t = raw[(size_t)(top + dy) * sub_w + (left + dx)];
sum += t;
if (t > 0) {
n++;
}
}
}
smoothed[(size_t)by * sub_w + bx] = (n > 0) ? (uint8_t)(sum / n) : 0;
}
}
free(raw);
/* Raster flood-fill of any remaining zero-threshold blocks. Propagates
the nearest-preceding non-zero threshold forward; runs of trailing
zeros inherit from the last non-zero value seen. */
int last = -1;
for (size_t i = 0; i < sub_n; i++) {
if (smoothed[i]) {
if ((int)i != last + 1) {
uint8_t fill = smoothed[i];
for (int j = last + 1; j < (int)i; j++) {
smoothed[j] = fill;
}
}
last = (int)i;
}
}
if (last < (int)sub_n - 1) {
uint8_t fill = (last >= 0) ? smoothed[last] : 128;
for (int j = last + 1; j < (int)sub_n; j++) {
smoothed[j] = fill;
}
}
/* Apply per-block threshold to every pixel. Pixels in the rightmost/
bottom partial block simply use that block's threshold — block-index
lookup via integer division is fine even when (w % BLOCK_SIZE) != 0
because sub_w/sub_h were computed with ceil(). */
for (int y = 0; y < h; y++) {
int by = y / HB_BLOCK_SIZE;
if (by >= sub_h) {
by = sub_h - 1;
}
uint8_t const *row = gray + (size_t)y * w;
uint8_t *out = bin + (size_t)y * w;
for (int x = 0; x < w; x++) {
int bx = x / HB_BLOCK_SIZE;
if (bx >= sub_w) {
bx = sub_w - 1;
}
uint8_t t = smoothed[(size_t)by * sub_w + bx];
out[x] = (row[x] <= t) ? 0 : 255;
}
}
free(smoothed);
return bin;
}
/* ---- Unsharp mask on grayscale ----
3-tap separable Gaussian blur, then sharp = clamp(2*g - blurred). The
small kernel matches level_4/5 blur radii (0.8-1.8 px). Returns a freshly
allocated grayscale buffer. */
static uint8_t *unsharp_mask_gray(uint8_t const *gray, int w, int h) {
if (w <= 2 || h <= 2) {
return NULL;
}
size_t n = (size_t)w * (size_t)h;
uint8_t *blurred = malloc(n);
uint8_t *tmp = malloc(n);
uint8_t *sharp = malloc(n);
if (!blurred || !tmp || !sharp) {
free(blurred);
free(tmp);
free(sharp);
return NULL;
}
/* Horizontal pass [1 2 1] / 4 into tmp. */
for (int y = 0; y < h; y++) {
uint8_t const *row = gray + (size_t)y * w;
uint8_t *out_row = tmp + (size_t)y * w;
out_row[0] = row[0];
out_row[w - 1] = row[w - 1];
for (int x = 1; x < w - 1; x++) {
int v = row[x - 1] + 2 * row[x] + row[x + 1];
out_row[x] = (uint8_t)(v >> 2);
}
}
/* Vertical pass [1 2 1] / 4 from tmp into blurred. */
for (int x = 0; x < w; x++) {
blurred[x] = tmp[x];
blurred[(size_t)(h - 1) * w + x] = tmp[(size_t)(h - 1) * w + x];
}
for (int y = 1; y < h - 1; y++) {
for (int x = 0; x < w; x++) {
int v = tmp[(size_t)(y - 1) * w + x] + 2 * tmp[(size_t)y * w + x] +
tmp[(size_t)(y + 1) * w + x];
blurred[(size_t)y * w + x] = (uint8_t)(v >> 2);
}
}
/* sharp = clamp(2*gray - blurred). */
for (size_t i = 0; i < n; i++) {
int v = 2 * (int)gray[i] - (int)blurred[i];
if (v < 0) {
v = 0;
}
if (v > 255) {
v = 255;
}
sharp[i] = (uint8_t)v;
}
free(blurred);
free(tmp);
return sharp;
}
/* ---- N-x bilinear upsample on grayscale ----
Returns a freshly allocated (N*w x N*h) buffer. Larger N gives bigger
effective module sizes (helping finder ratio checks and subpixel
refinement) at quadratic memory + time cost. */
static uint8_t *upsample_nx_bilinear(uint8_t const *gray, int w, int h, int n) {
if (w <= 0 || h <= 0 || n < 2) {
return NULL;
}
int w2 = w * n;
int h2 = h * n;
double inv = 1.0 / (double)n;
uint8_t *out = malloc((size_t)w2 * (size_t)h2);
if (!out) {
return NULL;
}
for (int y = 0; y < h2; y++) {
double sy = ((double)y + 0.5) * inv - 0.5;
int y0 = (int)floor(sy);
int y1 = y0 + 1;
double fy = sy - y0;
if (y0 < 0) {
y0 = 0;
}
if (y1 < 0) {
y1 = 0;
}
if (y0 > h - 1) {
y0 = h - 1;
}
if (y1 > h - 1) {
y1 = h - 1;
}
for (int x = 0; x < w2; x++) {
double sx = ((double)x + 0.5) * inv - 0.5;
int x0 = (int)floor(sx);
int x1 = x0 + 1;
double fx = sx - x0;
if (x0 < 0) {
x0 = 0;
}
if (x1 < 0) {
x1 = 0;
}
if (x0 > w - 1) {
x0 = w - 1;
}
if (x1 > w - 1) {
x1 = w - 1;
}
double v00 = gray[y0 * w + x0];
double v01 = gray[y0 * w + x1];
double v10 = gray[y1 * w + x0];
double v11 = gray[y1 * w + x1];
double v0 = v00 * (1.0 - fx) + v01 * fx;
double v1 = v10 * (1.0 - fx) + v11 * fx;
double v = v0 * (1.0 - fy) + v1 * fy;
if (v < 0.0) {
v = 0.0;
}
if (v > 255.0) {
v = 255.0;
}
out[y * w2 + x] = (uint8_t)(v + 0.5);
}
}
return out;
}
/* ---- 2x grayscale downsample (box average) ----
Each output pixel is the mean of a 2x2 input block. Mirrors zxing-cpp's
LumImagePyramid step. Anti-aliased QR-edge pixels get averaged with
interior pixels, producing new intermediate luma values within an 8x8
block that raise its dynamic range above the hybrid binariser's
MIN_DYNAMIC_RANGE=24 threshold. Essential for pastel palettes where
module-vs-background contrast at full resolution is below that gate
everywhere in the image. */
static uint8_t *downsample_2x_box(uint8_t const *gray, int w, int h) {
if (w < 2 || h < 2) {
return NULL;
}
int nw = w / 2;
int nh = h / 2;
uint8_t *out = malloc((size_t)nw * (size_t)nh);
if (!out) {
return NULL;
}
for (int y = 0; y < nh; y++) {
int sy = y * 2;
uint8_t const *r0 = gray + (size_t)sy * w;
uint8_t const *r1 = gray + (size_t)(sy + 1) * w;
uint8_t *dst = out + (size_t)y * nw;
for (int x = 0; x < nw; x++) {
int sx = x * 2;
int sum = r0[sx] + r0[sx + 1] + r1[sx] + r1[sx + 1];
dst[x] = (uint8_t)(sum >> 2);
}
}
return out;
}
/* ---- 3x3 grayscale median filter ----
Removes salt-and-pepper noise without softening edges (unlike Gaussian
blur). Targets level_5's heavy random noise (sigma 4-9) that injects
isolated black/white pixels into otherwise-uniform module interiors. */
static uint8_t *median_filter_3x3(uint8_t const *gray, int w, int h) {
if (w <= 2 || h <= 2) {
return NULL;
}
uint8_t *out = malloc((size_t)w * h);
if (!out) {
return NULL;
}
/* Copy the 1-pixel border unchanged. */
for (int x = 0; x < w; x++) {
out[x] = gray[x];
out[(size_t)(h - 1) * w + x] = gray[(size_t)(h - 1) * w + x];
}
for (int y = 1; y < h - 1; y++) {
out[y * w] = gray[y * w];
out[y * w + (w - 1)] = gray[y * w + (w - 1)];
}
for (int y = 1; y < h - 1; y++) {
for (int x = 1; x < w - 1; x++) {
uint8_t v[9];
v[0] = gray[(y - 1) * w + (x - 1)];
v[1] = gray[(y - 1) * w + x];
v[2] = gray[(y - 1) * w + (x + 1)];
v[3] = gray[y * w + (x - 1)];
v[4] = gray[y * w + x];
v[5] = gray[y * w + (x + 1)];
v[6] = gray[(y + 1) * w + (x - 1)];
v[7] = gray[(y + 1) * w + x];
v[8] = gray[(y + 1) * w + (x + 1)];
/* Insertion sort to find the median (5th element of 9). */
for (int i = 1; i < 9; i++) {
uint8_t tmp = v[i];
int j = i - 1;
while (j >= 0 && v[j] > tmp) {
v[j + 1] = v[j];
j--;
}
v[j + 1] = tmp;
}
out[y * w + x] = v[4];
}
}
return out;
}
/* ---- Inverted polarity ----
QR spec allows light-on-dark modules (level_13 corpus); flatcv's finder
and sampling code assume dark-on-light. Retrying with 255 - v lets the
same pipeline decode both polarities without any per-stage branching. */
static uint8_t *invert_gray(uint8_t const *gray, int w, int h) {
if (w <= 0 || h <= 0) {
return NULL;
}
size_t n = (size_t)w * (size_t)h;
uint8_t *out = malloc(n);
if (!out) {
return NULL;
}
for (size_t i = 0; i < n; i++) {
out[i] = (uint8_t)(255 - gray[i]);
}
return out;
}
/* ---- Pixel access ---- */
static int is_dark(uint8_t const *px, int w, int h, int x, int y) {
if (x < 0 || x >= w || y < 0 || y >= h) {
return 0;
}
return px[y * w + x] < 128;
}
/* ---- Finder pattern ratio check (1:1:3:1:1) ---- */
static int check_ratio(const int s[5]) {
int total = 0;
for (int i = 0; i < 5; i++) {
if (s[i] == 0) {
return 0;
}
total += s[i];
}
if (total < 7) {
return 0;
}
float m = (float)total / 7.0f;
float v = m * 0.7f;
return (
fabsf(s[0] - m) <= v && fabsf(s[1] - m) <= v &&
fabsf(s[2] - 3 * m) <= 3 * v && fabsf(s[3] - m) <= v && fabsf(s[4] - m) <= v
);
}
/* ---- Cross-check along an arbitrary angle ----
Walk along direction (cos_a, sin_a) through (cx, cy), counting the
1:1:3:1:1 dark-light-dark-light-dark run pattern.
Returns module_size if valid, -1 otherwise. Writes refined center. */
static float cross_check_angle(
uint8_t const *px,
int w,
int h,
float cx,
float cy,
float cos_a,
float sin_a,
int exp_total,
float *out_cx,
float *out_cy
) {
int max_walk = (w > h ? w : h);
/* Walk negative: expect dark(center), light, dark at boundary */
int neg_dark_center = 0;
int neg_light = 0;
int neg_dark_outer = 0;
int phase = 0;
for (int step = 1; step <= max_walk && phase < 3; step++) {
float fx = cx - step * cos_a;
float fy = cy - step * sin_a;
int ix = (int)(fx + 0.5f), iy = (int)(fy + 0.5f);
if (ix < 0 || ix >= w || iy < 0 || iy >= h) {
break;
}
int dark = is_dark(px, w, h, ix, iy);
if (phase == 0) {
if (dark) {
neg_dark_center++;
}
else {
neg_light = 1;
phase = 1;
}
}
else if (phase == 1) {
if (!dark) {
neg_light++;
}
else {
neg_dark_outer = 1;
phase = 2;
}
}
else {
if (dark) {
neg_dark_outer++;
}
else {
phase = 3;
}
}
}
/* Walk positive: expect dark(center), light, dark at boundary */
int pos_dark_center = 0;
int pos_light = 0;
int pos_dark_outer = 0;
phase = 0;
for (int step = 1; step <= max_walk && phase < 3; step++) {
float fx = cx + step * cos_a;
float fy = cy + step * sin_a;
int ix = (int)(fx + 0.5f), iy = (int)(fy + 0.5f);
if (ix < 0 || ix >= w || iy < 0 || iy >= h) {
break;
}
int dark = is_dark(px, w, h, ix, iy);
if (phase == 0) {
if (dark) {
pos_dark_center++;
}
else {
pos_light = 1;
phase = 1;
}
}
else if (phase == 1) {
if (!dark) {
pos_light++;
}
else {
pos_dark_outer = 1;
phase = 2;
}
}
else {
if (dark) {
pos_dark_outer++;
}
else {
phase = 3;
}
}
}
int s[5];
s[0] = neg_dark_outer;
s[1] = neg_light;
s[2] = neg_dark_center + 1 + pos_dark_center;
s[3] = pos_light;
s[4] = pos_dark_outer;
int total = s[0] + s[1] + s[2] + s[3] + s[4];
if (exp_total > 0 && abs(total - exp_total) > exp_total) {
return -1;
}
if (!check_ratio(s)) {
return -1;
}
float offset = (float)(pos_dark_center - neg_dark_center) / 2.0f;
*out_cx = cx + offset * cos_a;
*out_cy = cy + offset * sin_a;
return (float)total / 7.0f;
}
/* ---- Radial cross-check at multiple angles ---- */
static int cross_check_radial(
uint8_t const *px,
int w,
int h,
float cx,
float cy,
float exp_ms,
float *out_cx,
float *out_cy,
float *out_ms
) {
/* 8 angles: 0, 22.5, 45, 67.5, 90, 112.5, 135, 157.5 degrees */
static const float angles[8] = {
0.0f,
0.3927f,
0.7854f,
1.1781f,
1.5708f,
1.9635f,
2.3562f,
2.7489f,
};
int exp_total = (int)(exp_ms * 7.0f + 0.5f);
float rcx_arr[8], rcy_arr[8], rms_arr[8];
int passed[8];
int npass = 0;
for (int a = 0; a < 8; a++) {
float ca = cosf(angles[a]), sa = sinf(angles[a]);
rms_arr[a] = cross_check_angle(
px,
w,
h,
cx,
cy,
ca,
sa,
exp_total,
&rcx_arr[a],
&rcy_arr[a]
);
passed[a] = (rms_arr[a] > 0);
if (passed[a]) {
npass++;
}
}
if (npass < 4) {
return 0;
}
int best_pair = -1;
float best_pair_ms_match = 1e9f;
for (int a = 0; a < 4; a++) {
int b = a + 4;
if (passed[a] && passed[b]) {
float ms_err = fabsf(rms_arr[a] - exp_ms) + fabsf(rms_arr[b] - exp_ms);
if (best_pair < 0 || ms_err < best_pair_ms_match) {
best_pair = a;
best_pair_ms_match = ms_err;
}
}
}
if (best_pair >= 0) {
int a = best_pair, b = best_pair + 4;
float ca = cosf(angles[a]), sa = sinf(angles[a]);
float off_a = (rcx_arr[a] - cx) * ca + (rcy_arr[a] - cy) * sa;
float cb = cosf(angles[b]), sb = sinf(angles[b]);
float off_b = (rcx_arr[b] - cx) * cb + (rcy_arr[b] - cy) * sb;
*out_cx = cx + off_a * ca + off_b * cb;
*out_cy = cy + off_a * sa + off_b * sb;
*out_ms = (rms_arr[a] + rms_arr[b]) / 2.0f;
}
else {
float sum_cx = 0, sum_cy = 0, sum_ms = 0;
for (int a = 0; a < 8; a++) {
if (passed[a]) {
sum_cx += rcx_arr[a];
sum_cy += rcy_arr[a];
sum_ms += rms_arr[a];
}
}
*out_cx = sum_cx / npass;
*out_cy = sum_cy / npass;
*out_ms = sum_ms / npass;
}
return 1;
}
/* ---- Merge a candidate into the list ---- */
static int merge_candidate(
FinderPattern *cands,
int nc,
int max_cands,
float cx,
float cy,
float ms
) {
float merge_r = ms * 1.5f;
for (int c = 0; c < nc; c++) {
float dx = cands[c].x - cx;
float dy = cands[c].y - cy;
if (sqrtf(dx * dx + dy * dy) < merge_r) {
cands[c].x = (cands[c].x + cx) / 2;
cands[c].y = (cands[c].y + cy) / 2;
cands[c].module_size = (cands[c].module_size + ms) / 2;
return nc;
}
}
if (nc < max_cands) {
cands[nc].x = cx;
cands[nc].y = cy;
cands[nc].module_size = ms;
return nc + 1;
}
return nc;
}
/* ---- Scan a single line of pixels for finder patterns ---- */
typedef struct {
uint8_t const *px;
int w, h;
int x0, y0, dx, dy, len;
} ScanLine;
static int get_dark_at(const ScanLine *sl, int step) {
int x = sl->x0 + step * sl->dx;
int y = sl->y0 + step * sl->dy;
return is_dark(sl->px, sl->w, sl->h, x, y);
}
static void scan_one_line(
const ScanLine *sl,
FinderPattern *cands,
int *nc,
int max_cands
) {
int s[5] = {0};
int st = 0;
for (int i = 0; i < sl->len; i++) {
int dark = get_dark_at(sl, i);
if (dark) {
if (st == 1 || st == 3) {
st++;
}
s[st]++;
}
else {
if (st == 0 || st == 2) {
st++;
s[st]++;
}
else if (st == 4) {
if (check_ratio(s)) {
int total = s[0] + s[1] + s[2] + s[3] + s[4];
float center_step = (float)(i - s[4] - s[3]) - (float)s[2] / 2.0f;
float cx = sl->x0 + center_step * sl->dx;
float cy = sl->y0 + center_step * sl->dy;
float ms = (float)total / 7.0f;
float rcx, rcy, rms;
if (cross_check_radial(
sl->px,
sl->w,
sl->h,
cx,
cy,
ms,
&rcx,
&rcy,
&rms
)) {
*nc = merge_candidate(cands, *nc, max_cands, rcx, rcy, rms);
}
}
s[0] = s[2];
s[1] = s[3];
s[2] = s[4];
s[3] = 1;
s[4] = 0;
st = 3;
}
else {
s[st]++;
}
}
}
}
/* ---- Find finder patterns with multi-direction scanning ---- */
static int
find_finders(uint8_t const *px, int w, int h, FinderPattern *out, int max_out) {
FinderPattern cands[MAX_CANDIDATES];
int nc = 0;
ScanLine sl;
sl.px = px;
sl.w = w;
sl.h = h;
/* Direction 1: Horizontal */
sl.dx = 1;
sl.dy = 0;
for (int y = 0; y < h && nc < MAX_CANDIDATES; y++) {
sl.x0 = 0;
sl.y0 = y;
sl.len = w;
scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
}
/* Direction 2: Vertical */
sl.dx = 0;
sl.dy = 1;
for (int x = 0; x < w && nc < MAX_CANDIDATES; x++) {
sl.x0 = x;
sl.y0 = 0;
sl.len = h;
scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
}
/* Direction 3: Diagonal 45° */
sl.dx = 1;
sl.dy = 1;
for (int y = 0; y < h && nc < MAX_CANDIDATES; y++) {
sl.x0 = 0;
sl.y0 = y;
sl.len = (w < h - y) ? w : h - y;
scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
}
for (int x = 1; x < w && nc < MAX_CANDIDATES; x++) {
sl.x0 = x;
sl.y0 = 0;
sl.len = (w - x < h) ? w - x : h;
scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
}
/* Direction 4: Diagonal 135° */
sl.dx = 1;
sl.dy = -1;
for (int y = 0; y < h && nc < MAX_CANDIDATES; y++) {
sl.x0 = 0;
sl.y0 = y;
sl.len = (w < y + 1) ? w : y + 1;
scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
}
for (int x = 1; x < w && nc < MAX_CANDIDATES; x++) {
sl.x0 = x;
sl.y0 = h - 1;
sl.len = (w - x < h) ? w - x : h;
scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
}
int n = (nc < max_out) ? nc : max_out;
for (int i = 0; i < n; i++) {
out[i] = cands[i];
}
return n;
}
/* ---- Subpixel finder center refinement ----
Once a candidate center is detected, refine it by computing the weighted
centroid of dark pixels in the inner 3x3-module dark square. This fixes
the ±2-3 pixel systematic bias from integer-pixel 1D scans, which
otherwise throws off grid sampling under perspective distortion. */
static void
refine_finder_center(uint8_t const *gray, int w, int h, FinderPattern *fp) {
float ms = fp->module_size;
if (ms < 1.0f) {
return;
}
/* Search in a 3-module window centered on the current estimate so the
centroid is dominated by the inner 3x3 dark core and not the outer
7x7 dark ring (which would pull the result off-center). */
int r = (int)(ms * 1.5f);
int x0 = (int)(fp->x + 0.5f) - r;
int x1 = (int)(fp->x + 0.5f) + r;
int y0 = (int)(fp->y + 0.5f) - r;
int y1 = (int)(fp->y + 0.5f) + r;
if (x0 < 0) {
x0 = 0;
}
if (y0 < 0) {
y0 = 0;
}
if (x1 >= w) {
x1 = w - 1;
}
if (y1 >= h) {
y1 = h - 1;
}
if (x1 <= x0 || y1 <= y0) {
return;
}
/* Compute local min/max to threshold adaptively within this tiny window. */
uint8_t lo = 255, hi = 0;
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
uint8_t v = gray[y * w + x];
if (v < lo) {
lo = v;
}
if (v > hi) {
hi = v;
}
}
}
if (hi <= lo + 10) {
return; /* no contrast — leave center alone */
}
int t = (lo + hi) / 2;
double sx = 0, sy = 0, sw = 0;
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
uint8_t v = gray[y * w + x];
if (v < t) {
double weight = (double)(t - v);
sx += (double)x * weight;
sy += (double)y * weight;
sw += weight;
}
}
}
if (sw > 0) {
fp->x = (float)(sx / sw);
fp->y = (float)(sy / sw);
}
}
/* ---- Identify TL, TR, BL ---- */
static float fp_dist(FinderPattern a, FinderPattern b) {
float dx = a.x - b.x, dy = a.y - b.y;
return sqrtf(dx * dx + dy * dy);
}
static void identify_triple(
FinderPattern a,
FinderPattern b,
FinderPattern c,
FinderPattern *tl,
FinderPattern *tr,
FinderPattern *bl
) {
*tl = a;
float cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
if (cross > 0) {
*tr = b;
*bl = c;
}
else {
*tr = c;
*bl = b;
}
}
/* ---- Timing pattern verification ----
Walk the candidate timing row between two finder centers, offset 3
modules perpendicular toward the third finder (finder center is at
module row 3, timing pattern at module row 6). A real QR timing row
shows alternating dark/light modules at module-size pitch; incidental
1:1:3:1:1 "finder" triples in random data do not. Returns transitions
per step in [0,1]; ≥ 0.75 reliably indicates a timing pattern (random
data averages 0.5 with 95% CI up to ~0.67). */
static float timing_score_pair(
uint8_t const *px,
int w,
int h,
FinderPattern a,
FinderPattern b,
FinderPattern c_toward
) {
float ms = (a.module_size + b.module_size) / 2.0f;
if (ms < 1.0f) {
return 0;
}
float ux = b.x - a.x, uy = b.y - a.y;
float u_len = sqrtf(ux * ux + uy * uy);
if (u_len < 10.0f * ms) {
return 0;
}
ux /= u_len;
uy /= u_len;
float perp_x = -uy, perp_y = ux;
float to_c_x = c_toward.x - a.x, to_c_y = c_toward.y - a.y;
if (perp_x * to_c_x + perp_y * to_c_y < 0) {
perp_x = -perp_x;
perp_y = -perp_y;
}
/* Finder center is the center of module (3,3); timing pattern runs along
the center of module (·,6). Offset from finder-center line to timing
line is 6 − 3 = 3 modules. */
float offset_x = 3.0f * ms * perp_x;
float offset_y = 3.0f * ms * perp_y;
float sx = a.x + 4.0f * ms * ux + offset_x;
float sy = a.y + 4.0f * ms * uy + offset_y;
float ex = b.x - 4.0f * ms * ux + offset_x;
float ey = b.y - 4.0f * ms * uy + offset_y;
float walk_len = sqrtf((ex - sx) * (ex - sx) + (ey - sy) * (ey - sy));
int n_steps = (int)(walk_len / ms + 0.5f);
if (n_steps < 6 || n_steps > 165) {
return 0;
}
int prev_dark = -1;
int transitions = 0;
int samples = 0;
for (int i = 0; i <= n_steps; i++) {
float t = (float)i / (float)n_steps;
float fx = sx + t * (ex - sx);
float fy = sy + t * (ey - sy);
int ix = (int)(fx + 0.5f), iy = (int)(fy + 0.5f);
if (ix < 0 || ix >= w || iy < 0 || iy >= h) {
continue;
}
int dark = is_dark(px, w, h, ix, iy);
samples++;
if (prev_dark != -1 && dark != prev_dark) {
transitions++;
}
prev_dark = dark;
}
if (samples < 5) {
return 0;
}
return (float)transitions / (float)(samples - 1);
}
static int select_and_rank(
uint8_t const *px,
int w,
int h,
FinderPattern *fps,
int nfp,
FinderPattern triples[][3],
int max_triples
) {
if (nfp < 3) {
return 0;
}
/* Count ms-cluster partners for each candidate: how many other candidates
have module_size within 15% of mine. Real finder triples are three
near-identical-ms blobs; scoring-by-ms alone lets wide-run false
positives in dense data regions push the real triple out of the
search window on cluttered images (e.g. receipt QR next to a
barcode). Clustered candidates get promoted so the triple survives. */
int cluster_count[MAX_CANDIDATES];
for (int i = 0; i < nfp; i++) {
int count = 0;
for (int j = 0; j < nfp; j++) {
if (j == i) {
continue;
}
float m = (fps[i].module_size > 0.001f) ? fps[i].module_size : 0.001f;
float ratio = fabsf(fps[i].module_size - fps[j].module_size) / m;
if (ratio < 0.15f) {
count++;
}
}
cluster_count[i] = count;
}
/* Selection sort by (clustered desc, module_size desc). Candidates with
≥ 2 ms-partners come first; within each group, larger ms wins. */
for (int i = 0; i < nfp - 1; i++) {
int best = i;
for (int j = i + 1; j < nfp; j++) {
int j_cl = cluster_count[j] >= 2;
int b_cl = cluster_count[best] >= 2;
int replace = 0;
if (j_cl && !b_cl) {
replace = 1;
}
else if (j_cl == b_cl && fps[j].module_size > fps[best].module_size) {
replace = 1;
}
if (replace) {
best = j;
}
}
if (best != i) {
FinderPattern tmp = fps[i];
fps[i] = fps[best];
fps[best] = tmp;
int tc = cluster_count[i];
cluster_count[i] = cluster_count[best];
cluster_count[best] = tc;
}
}
typedef struct {
float score;
int i, j, k;
} Scored;
int max_scored = max_triples * 4;
if (max_scored > 200) {
max_scored = 200;
}
Scored *scored = malloc(max_scored * sizeof(Scored));
if (!scored) {
return 0;
}
int ns = 0;
/* Consider all clustered candidates (capped at 80 for O(n³) budget), or at
least 30 if few candidates cluster — preserves original behaviour on
noise-only scenes while accommodating crowded ones. */
int num_clustered = 0;
for (int i = 0; i < nfp; i++) {
if (cluster_count[i] >= 2) {
num_clustered++;
}
}
int limit = num_clustered;
if (limit < 30) {
limit = 30;
}
if (limit > nfp) {
limit = nfp;
}
if (limit > 80) {
limit = 80;
}
for (int i = 0; i < limit; i++) {
for (int j = i + 1; j < limit; j++) {
for (int k = j + 1; k < limit; k++) {
float ms_avg =
(fps[i].module_size + fps[j].module_size + fps[k].module_size) / 3.0f;
float ms_var = (fabsf(fps[i].module_size - ms_avg) +
fabsf(fps[j].module_size - ms_avg) +
fabsf(fps[k].module_size - ms_avg)) /
ms_avg;
float d01 = fp_dist(fps[i], fps[j]);
float d02 = fp_dist(fps[i], fps[k]);
float d12 = fp_dist(fps[j], fps[k]);
float sides[3] = {d01, d02, d12};
for (int a = 0; a < 2; a++) {
for (int b = a + 1; b < 3; b++) {
if (sides[a] > sides[b]) {
float t = sides[a];
sides[a] = sides[b];
sides[b] = t;
}
}
}
float side_ratio = (sides[1] > 0) ? sides[0] / sides[1] : 0;
float expected_hyp = sqrtf(sides[0] * sides[0] + sides[1] * sides[1]);
float hyp_err = (expected_hyp > 0)
? fabsf(sides[2] - expected_hyp) / expected_hyp
: 1;
/* Real QR finder centers sit at module (3.5, 3.5), (sz-3.5, 3.5),
(3.5, sz-3.5) — so the short side divided by module size should be
qr_size - 7, at least 14 for v1. Triples formed from incidental
1:1:3:1:1 patterns in adjacent data modules have small
sides/ms ratios; saturate the bonus at 14 so real finders outrank
them without giving oversized triples unbounded advantage. */
float modules_between = (ms_avg > 0) ? sides[0] / ms_avg : 0;
/* QR spec: finder-center distance = (qr_size − 7) × module_size, with
qr_size ∈ [21, 177]. So the longer leg cannot plausibly exceed
~170 modules; leave ~10% slack for perspective and scan noise. A
triple spanning an entire receipt/page at some spurious module size
blows past this even when its module sizes happen to cluster. */
float modules_between_long = (ms_avg > 0) ? sides[1] / ms_avg : 0;
if (modules_between_long > 190.0f) {
continue;
}
float scale_bonus = modules_between / 14.0f;
if (scale_bonus > 1.0f) {
scale_bonus = 1.0f;
}
/* ms_var dominates: real QR finders have near-identical module sizes;
triples whose "finders" are false positives at different scales
get rejected hard. Discount hyp_err so moderate perspective
distortion doesn't kill the true triple. */
float score = side_ratio - ms_var * 5.0f - hyp_err * 1.0f + scale_bonus;
/* Timing-pattern bonus: real finder triples have two pairs whose
inter-finder line runs along a QR timing row of alternating
dark/light modules. False-positive triples in dense document/
receipt text rarely hit this, so the bonus of +1 per timing
pair lets perspective-distorted real triples outrank clean but
incidental right-triangle triples. Gate on threshold 0.75 —
random data ratio is ≤ 0.67 with 95% confidence. */
int timing_pairs = 0;
float t_ij = timing_score_pair(px, w, h, fps[i], fps[j], fps[k]);
float t_ik = timing_score_pair(px, w, h, fps[i], fps[k], fps[j]);
float t_jk = timing_score_pair(px, w, h, fps[j], fps[k], fps[i]);
if (t_ij >= 0.75f) {
timing_pairs++;
}
if (t_ik >= 0.75f) {
timing_pairs++;
}
if (t_jk >= 0.75f) {
timing_pairs++;
}
score += (float)timing_pairs;
if (ns < max_scored) {
scored[ns++] = (Scored){score, i, j, k};
}
else {
int worst = 0;
for (int s = 1; s < ns; s++) {
if (scored[s].score < scored[worst].score) {
worst = s;
}
}
if (score > scored[worst].score) {
scored[worst] = (Scored){score, i, j, k};
}
}
}
}
}
for (int i = 0; i < ns - 1; i++) {
for (int j = i + 1; j < ns; j++) {
if (scored[j].score > scored[i].score) {
Scored t = scored[i];
scored[i] = scored[j];
scored[j] = t;
}
}
}
int out_count = (ns < max_triples) ? ns : max_triples;
for (int t = 0; t < out_count; t++) {
FinderPattern triple[3] =
{fps[scored[t].i], fps[scored[t].j], fps[scored[t].k]};
float d01 = fp_dist(triple[0], triple[1]);
float d02 = fp_dist(triple[0], triple[2]);
float d12 = fp_dist(triple[1], triple[2]);
FinderPattern a, b, c;
if (d01 >= d02 && d01 >= d12) {
a = triple[2];
b = triple[0];
c = triple[1];
}
else if (d02 >= d01 && d02 >= d12) {
a = triple[1];
b = triple[0];
c = triple[2];
}
else {
a = triple[0];
b = triple[1];
c = triple[2];
}
identify_triple(a, b, c, &triples[t][0], &triples[t][1], &triples[t][2]);
}
free(scored);
return out_count;
}
/* ---- Homography (perspective) ---- */
/* Solve an 8x8 linear system for a perspective transform that maps 4 source
points to 4 destination points. Returns 1 on success, 0 if singular. */
static int compute_homography(
const double src[4][2],
const double dst[4][2],
double H[9]
) {
double A[8][9];
memset(A, 0, sizeof(A));
for (int i = 0; i < 4; i++) {
double sx = src[i][0], sy = src[i][1];
double dx = dst[i][0], dy = dst[i][1];
A[2 * i][0] = sx;
A[2 * i][1] = sy;
A[2 * i][2] = 1.0;
A[2 * i][6] = -sx * dx;
A[2 * i][7] = -sy * dx;
A[2 * i][8] = dx;
A[2 * i + 1][3] = sx;
A[2 * i + 1][4] = sy;
A[2 * i + 1][5] = 1.0;
A[2 * i + 1][6] = -sx * dy;
A[2 * i + 1][7] = -sy * dy;
A[2 * i + 1][8] = dy;
}
for (int i = 0; i < 8; i++) {
int max_row = i;
double max_val = fabs(A[i][i]);
for (int k = i + 1; k < 8; k++) {
if (fabs(A[k][i]) > max_val) {
max_val = fabs(A[k][i]);
max_row = k;
}
}
if (max_val < 1e-10) {
return 0;
}
if (max_row != i) {
for (int j = 0; j <= 8; j++) {
double t = A[i][j];
A[i][j] = A[max_row][j];
A[max_row][j] = t;
}
}
for (int j = i + 1; j < 8; j++) {
double f = A[j][i] / A[i][i];
for (int k = i; k <= 8; k++) {
A[j][k] -= f * A[i][k];
}
}
}
for (int i = 7; i >= 0; i--) {
if (fabs(A[i][i]) < 1e-10) {
return 0;
}
double s = A[i][8];
for (int j = i + 1; j < 8; j++) {
s -= A[i][j] * H[j];
}
H[i] = s / A[i][i];
if (isnan(H[i]) || isinf(H[i])) {
return 0;
}
}
H[8] = 1.0;
return 1;
}
static void apply_homography(
const double H[9],
double mx,
double my,
float *out_x,
float *out_y
) {
double w = H[6] * mx + H[7] * my + H[8];
if (fabs(w) < 1e-10) {
*out_x = 0;
*out_y = 0;
return;
}
double x = (H[0] * mx + H[1] * my + H[2]) / w;
double y = (H[3] * mx + H[4] * my + H[5]) / w;
/* Clamp to a range safely castable to int by callers. Values outside this
range come from ill-conditioned homographies and would be off-image
anyway; unclamped they trigger UB when cast via (int)fx. */
const double lim = 1.0e7;
if (!(x == x) || x < -lim) {
x = -lim;
}
else if (x > lim) {
x = lim;
}
if (!(y == y) || y < -lim) {
y = -lim;
}
else if (y > lim) {
y = lim;
}
*out_x = (float)x;
*out_y = (float)y;
}
/* Solve the linear system Mh = v where M is 8x8 symmetric (A^T A) and v is
8x1 (A^T b), by Gaussian elimination with partial pivoting. Writes the
result into the first 8 slots of H and sets H[8] = 1. Returns 1 on
success, 0 on singular / ill-conditioned input. */
static int solve_8x8(double M[8][8], const double v[8], double H[9]) {
double A[8][9];
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
A[i][j] = M[i][j];
}
A[i][8] = v[i];
}
for (int i = 0; i < 8; i++) {
int max_row = i;
double max_val = fabs(A[i][i]);
for (int k = i + 1; k < 8; k++) {
if (fabs(A[k][i]) > max_val) {
max_val = fabs(A[k][i]);
max_row = k;
}
}
if (max_val < 1e-12) {
return 0;
}
if (max_row != i) {
for (int j = 0; j <= 8; j++) {
double t = A[i][j];
A[i][j] = A[max_row][j];
A[max_row][j] = t;
}
}
for (int k = i + 1; k < 8; k++) {
double f = A[k][i] / A[i][i];
for (int j = i; j <= 8; j++) {
A[k][j] -= f * A[i][j];
}
}
}
for (int i = 7; i >= 0; i--) {
if (fabs(A[i][i]) < 1e-12) {
return 0;
}
double s = A[i][8];
for (int j = i + 1; j < 8; j++) {
s -= A[i][j] * H[j];
}
H[i] = s / A[i][i];
if (isnan(H[i]) || isinf(H[i])) {
return 0;
}
}
H[8] = 1.0;
return 1;
}
/* Solve an over-determined homography system from N≥4 correspondences via
normal equations: the 2N×8 constraint matrix A is folded into an 8×8
symmetric M = A^T A and an 8×1 vector v = A^T b, and the 8×8 system is
solved directly. No Hartley normalisation — QR decoding stays within a
few hundred pixels so the coordinate magnitudes don't blow out double
precision here. Returns 1 on success, 0 on singular input. */
static int compute_homography_ls(
const double src[][2],
const double dst[][2],
int n,
double H[9]
) {
if (n < 4) {
return 0;
}
double M[8][8];
double v[8];
memset(M, 0, sizeof(M));
memset(v, 0, sizeof(v));
for (int i = 0; i < n; i++) {
double X = src[i][0], Y = src[i][1];
double x = dst[i][0], y = dst[i][1];
/* x-equation row: [X, Y, 1, 0, 0, 0, -x*X, -x*Y] with RHS = x */
double rx[8] = {X, Y, 1, 0, 0, 0, -x * X, -x * Y};
for (int a = 0; a < 8; a++) {
for (int b = 0; b < 8; b++) {
M[a][b] += rx[a] * rx[b];
}
v[a] += rx[a] * x;
}
/* y-equation row: [0, 0, 0, X, Y, 1, -y*X, -y*Y] with RHS = y */
double ry[8] = {0, 0, 0, X, Y, 1, -y * X, -y * Y};
for (int a = 0; a < 8; a++) {
for (int b = 0; b < 8; b++) {
M[a][b] += ry[a] * ry[b];
}
v[a] += ry[a] * y;
}
}
return solve_8x8(M, v, H);
}
/* Solve an NxN linear system M x = v by Gaussian elimination with partial
pivoting. M is destroyed. Result written into x. Returns 1 on success,
0 on singular matrix. */
static int solve_nxn(double *M, double *v, int n, double *x) {
/* Augment [M | v] into A: A is n × (n+1), row-major. */
int stride = n + 1;
double *A = (double *)malloc((size_t)n * (size_t)stride * sizeof(double));
if (!A) {
return 0;
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
A[i * stride + j] = M[i * n + j];
}
A[i * stride + n] = v[i];
}
for (int i = 0; i < n; i++) {
int max_row = i;
double max_val = fabs(A[i * stride + i]);
for (int k = i + 1; k < n; k++) {
double av = fabs(A[k * stride + i]);
if (av > max_val) {
max_val = av;
max_row = k;
}
}
if (max_val < 1e-12) {
free(A);
return 0;
}
if (max_row != i) {
for (int j = 0; j <= n; j++) {
double t = A[i * stride + j];
A[i * stride + j] = A[max_row * stride + j];
A[max_row * stride + j] = t;
}
}
for (int k = i + 1; k < n; k++) {
double f = A[k * stride + i] / A[i * stride + i];
for (int j = i; j <= n; j++) {
A[k * stride + j] -= f * A[i * stride + j];
}
}
}
for (int i = n - 1; i >= 0; i--) {
if (fabs(A[i * stride + i]) < 1e-12) {
free(A);
return 0;
}
double s = A[i * stride + n];
for (int j = i + 1; j < n; j++) {
s -= A[i * stride + j] * x[j];
}
x[i] = s / A[i * stride + i];
if (isnan(x[i]) || isinf(x[i])) {
free(A);
return 0;
}
}
free(A);
return 1;
}
/* Fit a quadratic warp (u, v) -> (x, y) using N correspondences. The warp
is a bivariate polynomial of degree 2 per output coordinate, i.e.
x(u,v) = a0 + a1·u + a2·v + a3·u·v + a4·u² + a5·v²
y(u,v) = b0 + b1·u + b2·v + b3·u·v + b4·u² + b5·v²
— 12 parameters fit via independent 6-unknown normal equations per
coordinate. Needs ≥ 6 observations and they must span a 2-D region
(all points on one line gives a singular matrix). Writes 12 floats
into coeffs: indices 0..5 are the x-coefficients in the order above,
6..11 are the y-coefficients. Returns 1 on success, 0 on failure.
A quadratic warp can model paper curl and moderate lens/fisheye
distortion that a single 8-parameter homography can't. It's not
perspective-correct for truly flat surfaces, but on real-world
photographs of QR codes on non-planar surfaces (receipts, folded
paper, curved displays) it typically gives sub-pixel accuracy where
the homography drifts by 1–2 modules across the data area. */
static int fit_quadratic_warp(
const double src[][2],
const double dst[][2],
int n,
double coeffs[12]
) {
if (n < 6) {
return 0;
}
double Mx[36], vx[6];
double My[36], vy[6];
memset(Mx, 0, sizeof(Mx));
memset(My, 0, sizeof(My));
memset(vx, 0, sizeof(vx));
memset(vy, 0, sizeof(vy));
for (int i = 0; i < n; i++) {
double u = src[i][0], v = src[i][1];
double x = dst[i][0], y = dst[i][1];
double row[6] = {1.0, u, v, u * v, u * u, v * v};
for (int a = 0; a < 6; a++) {
for (int b = 0; b < 6; b++) {
Mx[a * 6 + b] += row[a] * row[b];
My[a * 6 + b] += row[a] * row[b];
}
vx[a] += row[a] * x;
vy[a] += row[a] * y;
}
}
double cx[6] = {0}, cy[6] = {0};
if (!solve_nxn(Mx, vx, 6, cx) || !solve_nxn(My, vy, 6, cy)) {
return 0;
}
for (int i = 0; i < 6; i++) {
coeffs[i] = cx[i];
coeffs[i + 6] = cy[i];
}
return 1;
}
/* Apply a quadratic warp to map a (u, v) module-space point to a (px, py)
pixel-space point. Matches the coefficient layout of fit_quadratic_warp. */
static void apply_quadratic_warp(
const double coeffs[12],
double u,
double v,
double *px,
double *py
) {
*px = coeffs[0] + coeffs[1] * u + coeffs[2] * v + coeffs[3] * u * v +
coeffs[4] * u * u + coeffs[5] * v * v;
*py = coeffs[6] + coeffs[7] * u + coeffs[8] * v + coeffs[9] * u * v +
coeffs[10] * u * u + coeffs[11] * v * v;
}
/* Light-pixel analogue of find_dark_centroid_local — centroid of *light*
pixels in a local window. Used for known-light QR cells (e.g. the light
timing modules, the light 3x3 interior of the finder pattern) to add
anchor points between the dark modules the primary observer finds. */
static int find_light_centroid_local(
uint8_t const *gray,
int w,
int h,
double px_pred,
double py_pred,
float search_r,
double *px_obs,
double *py_obs
) {
int r = (int)(search_r + 0.5f);
if (r < 2) {
r = 2;
}
int cx = (int)(px_pred + 0.5);
int cy = (int)(py_pred + 0.5);
int x0 = cx - r;
int y0 = cy - r;
int x1 = cx + r;
int y1 = cy + r;
if (x0 < 0) {
x0 = 0;
}
if (y0 < 0) {
y0 = 0;
}
if (x1 >= w) {
x1 = w - 1;
}
if (y1 >= h) {
y1 = h - 1;
}
if (x1 - x0 < 2 || y1 - y0 < 2) {
return 0;
}
uint8_t lo = 255, hi = 0;
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
uint8_t g = gray[y * w + x];
if (g < lo) {
lo = g;
}
if (g > hi) {
hi = g;
}
}
}
if (hi - lo < 20) {
return 0;
}
int t = (lo + hi) / 2;
double sx = 0, sy = 0, sw = 0;
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
uint8_t g = gray[y * w + x];
if (g >= t) {
double weight = (double)(g - t);
sx += (double)x * weight;
sy += (double)y * weight;
sw += weight;
}
}
}
if (sw <= 0) {
return 0;
}
*px_obs = sx / sw;
*py_obs = sy / sw;
return 1;
}
/* Observe the pixel-space centroid of dark pixels in a local window around
a predicted position, using an adaptive local threshold. Used to ground-
truth the position of a known-dark QR module (timing pattern dark
module, alignment center, etc.) against the pixel buffer so we can
refine a homography that was built from far-apart control points.
Returns 1 on success; 0 if the window is uniform (low contrast ⇒
unreliable). */
static int find_dark_centroid_local(
uint8_t const *gray,
int w,
int h,
double px_pred,
double py_pred,
float search_r,
double *px_obs,
double *py_obs
) {
int r = (int)(search_r + 0.5f);
if (r < 2) {
r = 2;
}
int cx = (int)(px_pred + 0.5);
int cy = (int)(py_pred + 0.5);
int x0 = cx - r;
int y0 = cy - r;
int x1 = cx + r;
int y1 = cy + r;
if (x0 < 0) {
x0 = 0;
}
if (y0 < 0) {
y0 = 0;
}
if (x1 >= w) {
x1 = w - 1;
}
if (y1 >= h) {
y1 = h - 1;
}
if (x1 - x0 < 2 || y1 - y0 < 2) {
return 0;
}
uint8_t lo = 255, hi = 0;
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
uint8_t g = gray[y * w + x];
if (g < lo) {
lo = g;
}
if (g > hi) {
hi = g;
}
}
}
if (hi - lo < 20) {
return 0;
}
int t = (lo + hi) / 2;
double sx = 0, sy = 0, sw = 0;
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
uint8_t g = gray[y * w + x];
if (g < t) {
double weight = (double)(t - g);
sx += (double)x * weight;
sy += (double)y * weight;
sw += weight;
}
}
}
if (sw <= 0) {
return 0;
}
*px_obs = sx / sw;
*py_obs = sy / sw;
return 1;
}
/* Forward declarations — defined later alongside the homography decode path. */
static char *decode_grid(
uint8_t *grid,
uint8_t const *conf,
int qr_size,
int version,
int *out_fmt_dist,
int *out_rs_cost
);
static float timing_error_rate(uint8_t const *grid, int qr_size);
static float finder_error_rate(uint8_t const *grid, int qr_size);
/* Sample the QR module grid using a quadratic warp instead of a homography.
Behaves like sample_grid_homography_offset_mode's non-bilinear path but
with a polynomial mapping that can bend to follow paper curl. The 5-
point sub-sample voting is preserved for tolerance against small
within-module drift. Returns a heap buffer of size qr_size*qr_size;
caller frees. */
static uint8_t *sample_grid_quadratic(
uint8_t const *px,
int w,
int h,
const double coeffs[12],
int qr_size
) {
int total = qr_size * qr_size;
uint8_t *grid = calloc(total, 1);
if (!grid) {
return NULL;
}
static const double offs[5][2] = {
{0.5, 0.5},
{0.35, 0.35},
{0.65, 0.35},
{0.35, 0.65},
{0.65, 0.65},
};
for (int r = 0; r < qr_size; r++) {
for (int c = 0; c < qr_size; c++) {
int votes = 0;
for (int o = 0; o < 5; o++) {
double px_d, py_d;
apply_quadratic_warp(
coeffs,
c + offs[o][0],
r + offs[o][1],
&px_d,
&py_d
);
int ix = (int)(px_d + 0.5);
int iy = (int)(py_d + 0.5);
if (is_dark(px, w, h, ix, iy)) {
votes++;
}
}
grid[r * qr_size + c] = (votes >= 3) ? 1 : 0;
}
}
return grid;
}
/* Fit a quadratic warp from 3 finder centers + alignment + all observable
timing-module centroids, then sample the grid with it and try to
decode. Returns a heap-allocated decoded string (caller frees) or
NULL if sampling/decode fails. */
static char *try_decode_quadratic(
uint8_t const *bin,
uint8_t const *gray,
int w,
int h,
int qr_size,
int version,
FinderPattern tl,
FinderPattern tr,
FinderPattern bl,
int have_alignment,
double alignment_x,
double alignment_y,
const double H_init[9],
float ms,
int *out_fmt_dist,
int *out_rs_cost
) {
enum { MAX_CORR = 80 };
double src[MAX_CORR][2];
double dst[MAX_CORR][2];
int n = 0;
src[n][0] = 3.5;
src[n][1] = 3.5;
dst[n][0] = tl.x;
dst[n][1] = tl.y;
n++;
src[n][0] = qr_size - 3.5;
src[n][1] = 3.5;
dst[n][0] = tr.x;
dst[n][1] = tr.y;
n++;
src[n][0] = 3.5;
src[n][1] = qr_size - 3.5;
dst[n][0] = bl.x;
dst[n][1] = bl.y;
n++;
if (have_alignment) {
src[n][0] = qr_size - 6.5;
src[n][1] = qr_size - 6.5;
dst[n][0] = alignment_x;
dst[n][1] = alignment_y;
n++;
}
float search_r = ms * 0.5f;
/* Horizontal timing pattern: dark at even cols, light at odd cols. Sample
both so the quadratic warp has fine-grained constraints along row 6. */
for (int col = 7; col <= qr_size - 8; col++) {
if (n >= MAX_CORR) {
break;
}
double mx = (double)col + 0.5;
double my = 6.5;
double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
if (fabs(pw) < 1e-10) {
continue;
}
double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
double px_obs, py_obs;
int ok_c;
if ((col % 2) == 0) {
ok_c = find_dark_centroid_local(
gray,
w,
h,
px_pred,
py_pred,
search_r,
&px_obs,
&py_obs
);
}
else {
ok_c = find_light_centroid_local(
gray,
w,
h,
px_pred,
py_pred,
search_r,
&px_obs,
&py_obs
);
}
if (ok_c) {
src[n][0] = mx;
src[n][1] = my;
dst[n][0] = px_obs;
dst[n][1] = py_obs;
n++;
}
}
for (int row = 7; row <= qr_size - 8; row++) {
if (n >= MAX_CORR) {
break;
}
double mx = 6.5;
double my = (double)row + 0.5;
double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
if (fabs(pw) < 1e-10) {
continue;
}
double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
double px_obs, py_obs;
int ok_c;
if ((row % 2) == 0) {
ok_c = find_dark_centroid_local(
gray,
w,
h,
px_pred,
py_pred,
search_r,
&px_obs,
&py_obs
);
}
else {
ok_c = find_light_centroid_local(
gray,
w,
h,
px_pred,
py_pred,
search_r,
&px_obs,
&py_obs
);
}
if (ok_c) {
src[n][0] = mx;
src[n][1] = my;
dst[n][0] = px_obs;
dst[n][1] = py_obs;
n++;
}
}
/* Alignment pattern anchors: sample the 8 known-light inner-ring cells
around the BR alignment center to pin the quadratic warp's BR quadrant
— without this the warp is only constrained by the single alignment
center in that region and extrapolates wildly into the data area. */
if (have_alignment) {
double ax = (double)qr_size - 6.5; /* alignment center in module coords */
double ay = (double)qr_size - 6.5;
static const double ring1[8][2] = {
{-1.0, -1.0},
{0.0, -1.0},
{1.0, -1.0},
{-1.0, 0.0},
{1.0, 0.0},
{-1.0, 1.0},
{0.0, 1.0},
{1.0, 1.0},
};
for (int i = 0; i < 8; i++) {
if (n >= MAX_CORR) {
break;
}
double mx = ax + ring1[i][0];
double my = ay + ring1[i][1];
double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
if (fabs(pw) < 1e-10) {
continue;
}
double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
double px_obs, py_obs;
if (find_light_centroid_local(
gray,
w,
h,
px_pred,
py_pred,
search_r,
&px_obs,
&py_obs
)) {
src[n][0] = mx;
src[n][1] = my;
dst[n][0] = px_obs;
dst[n][1] = py_obs;
n++;
}
}
}
/* Need at least 8 well-spread observations for a stable quadratic fit
(6 unknowns per axis + a bit of over-determination). */
if (n < 8) {
return NULL;
}
double coeffs[12];
if (!fit_quadratic_warp(src, dst, n, coeffs)) {
return NULL;
}
uint8_t *grid = sample_grid_quadratic(bin, w, h, coeffs, qr_size);
if (!grid) {
return NULL;
}
int fd = 16, rc = 100000;
char *d = decode_grid(grid, NULL, qr_size, version, &fd, &rc);
free(grid);
if (d) {
if (out_fmt_dist) {
*out_fmt_dist = fd;
}
if (out_rs_cost) {
*out_rs_cost = rc;
}
}
return d;
}
/* Refine a homography by observing pixel-space centroids of every dark
module in the two QR timing patterns and solving a least-squares fit
over the combined set of control points and timing observations. The
3 finder centers (and optional BR alignment) give the coarse shape;
the timing observations, which thread the QR's interior, constrain
the transform through the data area and correct the sub-module drift
that a 4-point homography can't resolve on curled or lens-distorted
captures (e.g. a receipt photographed at an angle). Returns 1 if the
refined H is valid and used ≥ 6 points total. Leaves H_out untouched
on failure. */
static int refine_h_with_timing(
uint8_t const *gray,
int w,
int h,
const double H_init[9],
int qr_size,
float ms,
FinderPattern tl,
FinderPattern tr,
FinderPattern bl,
int have_alignment,
double alignment_x,
double alignment_y,
double H_out[9]
) {
if (!gray || qr_size < 21) {
return 0;
}
enum { MAX_CORR = 80 };
double src[MAX_CORR][2];
double dst[MAX_CORR][2];
int n = 0;
/* 3 finder centers as anchoring control points. */
src[n][0] = 3.5;
src[n][1] = 3.5;
dst[n][0] = tl.x;
dst[n][1] = tl.y;
n++;
src[n][0] = qr_size - 3.5;
src[n][1] = 3.5;
dst[n][0] = tr.x;
dst[n][1] = tr.y;
n++;
src[n][0] = 3.5;
src[n][1] = qr_size - 3.5;
dst[n][0] = bl.x;
dst[n][1] = bl.y;
n++;
/* Optional alignment-pattern center for the BR corner. */
if (have_alignment) {
src[n][0] = qr_size - 6.5;
src[n][1] = qr_size - 6.5;
dst[n][0] = alignment_x;
dst[n][1] = alignment_y;
n++;
}
/* Timing pattern: module row 6 columns 8..qr_size-9, dark at even cols;
module col 6 rows 8..qr_size-9, dark at even rows. Sample the dark
modules' pixel-space centroids using the initial H as a guide and a
half-module search radius — the initial guess is within a module's
width of truth on realistic captures. */
float search_r = ms * 0.5f;
for (int col = 8; col <= qr_size - 9; col += 2) {
if (n >= MAX_CORR) {
break;
}
double mx = (double)col + 0.5;
double my = 6.5;
double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
if (fabs(pw) < 1e-10) {
continue;
}
double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
double px_obs, py_obs;
if (find_dark_centroid_local(
gray,
w,
h,
px_pred,
py_pred,
search_r,
&px_obs,
&py_obs
)) {
src[n][0] = mx;
src[n][1] = my;
dst[n][0] = px_obs;
dst[n][1] = py_obs;
n++;
}
}
for (int row = 8; row <= qr_size - 9; row += 2) {
if (n >= MAX_CORR) {
break;
}
double mx = 6.5;
double my = (double)row + 0.5;
double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
if (fabs(pw) < 1e-10) {
continue;
}
double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
double px_obs, py_obs;
if (find_dark_centroid_local(
gray,
w,
h,
px_pred,
py_pred,
search_r,
&px_obs,
&py_obs
)) {
src[n][0] = mx;
src[n][1] = my;
dst[n][0] = px_obs;
dst[n][1] = py_obs;
n++;
}
}
/* Need meaningfully more than the 4-point minimum for the least-squares
to actually do anything; if only the control points were observed
(all timing centroid calls failed — likely a low-contrast or obscured
QR) the refined H would equal the initial one. */
if (n < 6) {
return 0;
}
return compute_homography_ls(src, dst, n, H_out);
}
/* ---- Alignment pattern search ---- */
/* Check if (cx, cy) looks like the center of a QR alignment pattern:
dark center with light ring 1 module out and dark ring 2 modules out.
Samples along 8 radial directions at 1 and 2 module offsets, and
requires at least 6 of 8 each to match. */
static int
is_alignment_center(uint8_t const *px, int w, int h, int cx, int cy, float ms) {
if (cx < 0 || cx >= w || cy < 0 || cy >= h) {
return 0;
}
if (!is_dark(px, w, h, cx, cy)) {
return 0;
}
/* 8 radial directions */
static const float dirs[8][2] = {
{1.0f, 0.0f},
{0.707f, 0.707f},
{0.0f, 1.0f},
{-0.707f, 0.707f},
{-1.0f, 0.0f},
{-0.707f, -0.707f},
{0.0f, -1.0f},
{0.707f, -0.707f},
};
int light_ok = 0;
int dark_ring_ok = 0;
int beyond_light_ok = 0;
int beyond_checked = 0;
for (int d = 0; d < 8; d++) {
int lx = (int)(cx + ms * dirs[d][0] + 0.5f * (dirs[d][0] > 0 ? 1 : -1));
int ly = (int)(cy + ms * dirs[d][1] + 0.5f * (dirs[d][1] > 0 ? 1 : -1));
int rx =
(int)(cx + 2.0f * ms * dirs[d][0] + 0.5f * (dirs[d][0] > 0 ? 1 : -1));
int ry =
(int)(cy + 2.0f * ms * dirs[d][1] + 0.5f * (dirs[d][1] > 0 ? 1 : -1));
/* A real alignment pattern is exactly 5 modules wide: the outer dark
ring sits at radius 2·ms, so just beyond (2.8·ms) we must be out of
the pattern again. Data-module clusters that happen to expose a
dark center + light 1·ms + dark 2·ms often keep going dark past
radius 2, because they're embedded in a larger dense region —
require "light past the dark ring" in the majority of directions
to rule those out. */
int bx =
(int)(cx + 2.8f * ms * dirs[d][0] + 0.5f * (dirs[d][0] > 0 ? 1 : -1));
int by =
(int)(cy + 2.8f * ms * dirs[d][1] + 0.5f * (dirs[d][1] > 0 ? 1 : -1));
if (lx < 0 || lx >= w || ly < 0 || ly >= h) {
continue;
}
if (rx < 0 || rx >= w || ry < 0 || ry >= h) {
continue;
}
if (!is_dark(px, w, h, lx, ly)) {
light_ok++;
}
if (is_dark(px, w, h, rx, ry)) {
dark_ring_ok++;
}
if (bx >= 0 && bx < w && by >= 0 && by < h) {
beyond_checked++;
if (!is_dark(px, w, h, bx, by)) {
beyond_light_ok++;
}
}
}
if (light_ok < 6 || dark_ring_ok < 6) {
return 0;
}
/* Require ≥ 5 of 8 outer directions to be light beyond the dark ring.
5 (of 8) keeps a small margin for alignment patterns adjacent to a
single dark data module while still killing patterns embedded in
dark clusters. If we couldn't check enough directions (pattern near
image edge), pass through — the existing 2-ring check has to carry. */
if (beyond_checked >= 5 && beyond_light_ok < 5) {
return 0;
}
return 1;
}
static int find_alignment_pattern(
uint8_t const *px,
int w,
int h,
float cx_pred,
float cy_pred,
float expected_ms,
float search_radius,
float *out_cx,
float *out_cy
) {
int x_min = (int)(cx_pred - search_radius);
int x_max = (int)(cx_pred + search_radius);
int y_min = (int)(cy_pred - search_radius);
int y_max = (int)(cy_pred + search_radius);
if (x_min < 0) {
x_min = 0;
}
if (y_min < 0) {
y_min = 0;
}
if (x_max >= w) {
x_max = w - 1;
}
if (y_max >= h) {
y_max = h - 1;
}
if (x_max <= x_min || y_max <= y_min) {
return 0;
}
/* Scan every dark pixel; the one with the smallest distance to the
predicted center that also passes the template check wins. */
float best_cx = -1, best_cy = -1;
float best_dist = 1e30f;
for (int y = y_min; y <= y_max; y++) {
for (int x = x_min; x <= x_max; x++) {
if (!is_dark(px, w, h, x, y)) {
continue;
}
if (!is_alignment_center(px, w, h, x, y, expected_ms)) {
continue;
}
float dx = (float)x - cx_pred;
float dy = (float)y - cy_pred;
float dist = dx * dx + dy * dy;
if (dist < best_dist) {
best_dist = dist;
best_cx = (float)x;
best_cy = (float)y;
}
}
}
if (best_cx < 0) {
return 0;
}
/* Refine by averaging all near-center dark pixels (the center module is
~1 module wide, so there will be a cluster of passing pixels). */
int r_refine = (int)(expected_ms * 0.5f);
if (r_refine < 1) {
r_refine = 1;
}
double sum_x = 0, sum_y = 0;
int count = 0;
for (int dy = -r_refine; dy <= r_refine; dy++) {
for (int dx = -r_refine; dx <= r_refine; dx++) {
int x = (int)best_cx + dx;
int y = (int)best_cy + dy;
if (x < 0 || x >= w || y < 0 || y >= h) {
continue;
}
if (is_dark(px, w, h, x, y) &&
is_alignment_center(px, w, h, x, y, expected_ms)) {
sum_x += x;
sum_y += y;
count++;
}
}
}
if (count > 0) {
*out_cx = (float)(sum_x / count);
*out_cy = (float)(sum_y / count);
}
else {
*out_cx = best_cx;
*out_cy = best_cy;
}
return 1;
}
/* ---- Alignment pattern grid positions (ISO/IEC 18004 Annex E) ----
Each row lists the grid coordinates (in modules, 0-indexed) where
alignment pattern centres lie for that version. Alignment patterns
exist at every combination (row, col) of these values EXCEPT the
three that overlap the finder patterns: (first, first), (first, last),
(last, first). Index = version. Count is stored in [0]; up to 7 values
follow in [1..7]. v1 has no alignment patterns. */
static const int QR_ALIGN_GRID[41][8] = {
{0}, /* v0 unused */
{0}, /* v1 */
{2, 6, 18},
{2, 6, 22},
{2, 6, 26},
{2, 6, 30},
{2, 6, 34},
{3, 6, 22, 38},
{3, 6, 24, 42},
{3, 6, 26, 46},
{3, 6, 28, 50},
{3, 6, 30, 54},
{3, 6, 32, 58},
{3, 6, 34, 62},
{4, 6, 26, 46, 66},
{4, 6, 26, 48, 70},
{4, 6, 26, 50, 74},
{4, 6, 30, 54, 78},
{4, 6, 30, 56, 82},
{4, 6, 30, 58, 86},
{4, 6, 34, 62, 90},
{5, 6, 28, 50, 72, 94},
{5, 6, 26, 50, 74, 98},
{5, 6, 30, 54, 78, 102},
{5, 6, 28, 54, 80, 106},
{5, 6, 32, 58, 84, 110},
{5, 6, 30, 58, 86, 114},
{5, 6, 34, 62, 90, 118},
{6, 6, 26, 50, 74, 98, 122},
{6, 6, 30, 54, 78, 102, 126},
{6, 6, 26, 52, 78, 104, 130},
{6, 6, 30, 56, 82, 108, 134},
{6, 6, 34, 60, 86, 112, 138},
{6, 6, 30, 58, 86, 114, 142},
{6, 6, 34, 62, 90, 118, 146},
{7, 6, 30, 54, 78, 102, 126, 150},
{7, 6, 24, 50, 76, 102, 128, 154},
{7, 6, 28, 54, 80, 106, 132, 158},
{7, 6, 32, 58, 84, 110, 136, 162},
{7, 6, 26, 54, 82, 110, 138, 166},
{7, 6, 30, 58, 86, 114, 142, 170},
};
/* Apply a 3x3 homography H (row-major) to module coordinates (mx, my),
returning pixel coordinates. Shared with the quadratic-warp path. */
static int project_homography(
const double H[9],
double mx,
double my,
double *out_px,
double *out_py
) {
double w_h = H[6] * mx + H[7] * my + H[8];
if (fabs(w_h) < 1e-10) {
return 0;
}
*out_px = (H[0] * mx + H[1] * my + H[2]) / w_h;
*out_py = (H[3] * mx + H[4] * my + H[5]) / w_h;
return 1;
}
/* Find every alignment pattern predicted by H for this version, skipping
the three that overlap the finders. Writes (module-coord, pixel-coord)
pairs for each one found, and returns the count. `search_r_modules`
controls how far find_alignment_pattern looks from each predicted
centre — widen it when H is an unrefined affine extrapolation from
three finders (the residual under strong perspective can be 3-4
modules), tighten it when H already folds in the BR alignment. */
static int find_all_alignment_patterns(
uint8_t const *px,
int w,
int h,
int version,
const double H[9],
float avg_m,
float search_r_modules,
double out_src[][2],
double out_dst[][2],
int max_out
) {
if (version < 2 || version > 40) {
return 0;
}
int nvals = QR_ALIGN_GRID[version][0];
if (nvals < 2) {
return 0;
}
int last = QR_ALIGN_GRID[version][nvals];
int first = QR_ALIGN_GRID[version][1];
float search_r = avg_m * search_r_modules;
int found = 0;
for (int ri = 0; ri < nvals; ri++) {
int row = QR_ALIGN_GRID[version][1 + ri];
for (int ci = 0; ci < nvals; ci++) {
int col = QR_ALIGN_GRID[version][1 + ci];
/* Skip the three finder-overlapping positions. */
if ((row == first && col == first) || (row == first && col == last) ||
(row == last && col == first)) {
continue;
}
if (found >= max_out) {
return found;
}
double mx = (double)col + 0.5;
double my = (double)row + 0.5;
double pred_x, pred_y;
if (!project_homography(H, mx, my, &pred_x, &pred_y)) {
continue;
}
float found_x, found_y;
if (find_alignment_pattern(
px,
w,
h,
(float)pred_x,
(float)pred_y,
avg_m,
search_r,
&found_x,
&found_y
)) {
out_src[found][0] = mx;
out_src[found][1] = my;
out_dst[found][0] = found_x;
out_dst[found][1] = found_y;
found++;
}
}
}
return found;
}
/* ---- Grid sampling ----
Homography-only: uses a 4-point perspective transform and samples a small
cluster of pixels near each module center, taking the majority value to
tolerate minor grid misalignment. The affine-equivalent case (no
perspective) is expressible as a homography where the 4th point is the
parallelogram completion, so a separate affine path isn't needed. */
/* Bilinear sample of the binary image at fractional pixel (fx, fy). Returns
the 0..255 weighted blend of the four neighbouring pixels' dark-ness. */
static int bilinear_dark(uint8_t const *px, int w, int h, float fx, float fy) {
int x0 = (int)floorf(fx);
int y0 = (int)floorf(fy);
int x1 = x0 + 1;
int y1 = y0 + 1;
float dx = fx - x0;
float dy = fy - y0;
if (x0 < 0) {
x0 = 0;
}
if (y0 < 0) {
y0 = 0;
}
if (x1 < 0) {
x1 = 0;
}
if (y1 < 0) {
y1 = 0;
}
if (x0 >= w) {
x0 = w - 1;
}
if (y0 >= h) {
y0 = h - 1;
}
if (x1 >= w) {
x1 = w - 1;
}
if (y1 >= h) {
y1 = h - 1;
}
int v00 = px[y0 * w + x0] < 128 ? 1 : 0;
int v01 = px[y0 * w + x1] < 128 ? 1 : 0;
int v10 = px[y1 * w + x0] < 128 ? 1 : 0;
int v11 = px[y1 * w + x1] < 128 ? 1 : 0;
float v0 = v00 * (1.0f - dx) + v01 * dx;
float v1 = v10 * (1.0f - dx) + v11 * dx;
return (v0 * (1.0f - dy) + v1 * dy) >= 0.5f;
}
/* Sample a single grayscale value via bilinear interpolation, with edge
clamping. Returns 0 if input gray is NULL (signals "no gray available"). */
static int
sample_gray_bilinear(uint8_t const *gray, int w, int h, float fx, float fy) {
if (!gray) {
return -1;
}
int x0 = (int)floorf(fx);
int y0 = (int)floorf(fy);
int x1 = x0 + 1;
int y1 = y0 + 1;
float dx = fx - x0;
float dy = fy - y0;
if (x0 < 0) {
x0 = 0;
}
if (y0 < 0) {
y0 = 0;
}
if (x1 < 0) {
x1 = 0;
}
if (y1 < 0) {
y1 = 0;
}
if (x0 >= w) {
x0 = w - 1;
}
if (y0 >= h) {
y0 = h - 1;
}
if (x1 >= w) {
x1 = w - 1;
}
if (y1 >= h) {
y1 = h - 1;
}
float v00 = gray[y0 * w + x0];
float v01 = gray[y0 * w + x1];
float v10 = gray[y1 * w + x0];
float v11 = gray[y1 * w + x1];
float v0 = v00 * (1.0f - dx) + v01 * dx;
float v1 = v10 * (1.0f - dx) + v11 * dx;
float v = v0 * (1.0f - dy) + v1 * dy;
if (v < 0.0f) {
v = 0.0f;
}
if (v > 255.0f) {
v = 255.0f;
}
return (int)(v + 0.5f);
}
/* Sample the QR module grid given a homography. If `gray_for_conf` is
non-NULL and `out_conf` is non-NULL, also writes a per-module ambiguity
buffer (1 = low-confidence module, 0 = high-confidence) of the same size
as the grid. The caller is responsible for free()ing both buffers.
Confidence is derived adaptively: after sampling all modules and their
per-module mean grayscale, compute the median grayscale of the modules
classified as light vs dark. A module is flagged ambiguous when its
mean gray sits a long way from BOTH expected values (signals an
occlusion patch whose color belongs to neither QR cluster) or when the
5 sub-sample votes were a near-tie (signals the patch boundary crosses
the module's sample area). */
static uint8_t *sample_grid_homography_offset_mode(
uint8_t const *px,
uint8_t const *gray_for_conf,
int w,
int h,
const double H[9],
int qr_size,
double off_x,
double off_y,
int use_bilinear,
uint8_t **out_conf
) {
int total = qr_size * qr_size;
uint8_t *grid = calloc(total, 1);
if (!grid) {
return NULL;
}
uint8_t *conf = NULL;
uint8_t *gmean_buf = NULL;
uint8_t *vote_tie = NULL;
if (out_conf && gray_for_conf) {
conf = calloc(total, 1);
gmean_buf = malloc(total);
vote_tie = calloc(total, 1);
if (!conf || !gmean_buf || !vote_tie) {
free(grid);
free(conf);
free(gmean_buf);
free(vote_tie);
return NULL;
}
}
static const double offs[5][2] = {
{0.5, 0.5},
{0.35, 0.35},
{0.65, 0.35},
{0.35, 0.65},
{0.65, 0.65},
};
for (int r = 0; r < qr_size; r++) {
for (int c = 0; c < qr_size; c++) {
int votes = 0;
int gray_sum = 0;
int gray_n = 0;
for (int o = 0; o < 5; o++) {
float fx, fy;
apply_homography(
H,
c + offs[o][0] + off_x,
r + offs[o][1] + off_y,
&fx,
&fy
);
int dark;
if (use_bilinear) {
dark = bilinear_dark(px, w, h, fx, fy);
}
else {
int ix = (int)(fx + 0.5f);
int iy = (int)(fy + 0.5f);
dark = is_dark(px, w, h, ix, iy);
}
if (dark) {
votes++;
}
if (gmean_buf) {
int g = sample_gray_bilinear(gray_for_conf, w, h, fx, fy);
if (g >= 0) {
gray_sum += g;
gray_n++;
}
}
}
grid[r * qr_size + c] = (votes >= 3) ? 1 : 0;
if (gmean_buf) {
int gmean = (gray_n > 0) ? (gray_sum / gray_n) : 128;
if (gmean < 0) {
gmean = 0;
}
if (gmean > 255) {
gmean = 255;
}
gmean_buf[r * qr_size + c] = (uint8_t)gmean;
if (vote_tie) {
vote_tie[r * qr_size + c] =
(uint8_t)((votes == 2 || votes == 3) ? 1 : 0);
}
}
}
}
if (conf && gmean_buf) {
/* Compute medians of dark / light cluster grays via histogram. */
int hist_dark[256] = {0};
int hist_light[256] = {0};
int n_dark = 0, n_light = 0;
for (int i = 0; i < total; i++) {
if (grid[i]) {
hist_dark[gmean_buf[i]]++;
n_dark++;
}
else {
hist_light[gmean_buf[i]]++;
n_light++;
}
}
int dark_med = 0, light_med = 255;
if (n_dark > 0) {
int target = n_dark / 2;
int acc = 0;
for (int v = 0; v < 256; v++) {
acc += hist_dark[v];
if (acc > target) {
dark_med = v;
break;
}
}
}
if (n_light > 0) {
int target = n_light / 2;
int acc = 0;
for (int v = 0; v < 256; v++) {
acc += hist_light[v];
if (acc > target) {
light_med = v;
break;
}
}
}
/* Tolerance: a chunky fraction of the cluster gap. A module is flagged
only when its mean gray is FAR from BOTH the dark and the light
cluster medians — that is the strong signal that the pixel belongs
to a foreign region (occlusion patch). We also keep a floor so the
rule still bites when the clusters are weakly separated. */
int gap = light_med - dark_med;
if (gap < 0) {
gap = -gap;
}
int tol = gap / 3;
if (tol < 35) {
tol = 35;
}
if (tol > 80) {
tol = 80;
}
for (int i = 0; i < total; i++) {
int g = gmean_buf[i];
int dd = g - dark_med;
if (dd < 0) {
dd = -dd;
}
int dl = g - light_med;
if (dl < 0) {
dl = -dl;
}
int ambig = 0;
if (dd > tol && dl > tol) {
ambig = 1;
}
conf[i] = (uint8_t)ambig;
}
}
free(gmean_buf);
free(vote_tie);
if (out_conf) {
*out_conf = conf;
}
return grid;
}
/* ---- Timing pattern check ----
The timing patterns on row 6 and column 6 alternate dark/light between the
three finder patterns. Starting at module (6, 8) = dark, (6, 9) = light,
(6, 10) = dark, … — dark whenever col or row is even. Real QRs have a
near-perfect match; a grid sampled from three incidental 1:1:3:1:1 runs in
noise produces essentially random bits here (~50% error). Returns the
fraction of timing modules whose value disagrees with the spec, 0.0 for a
perfect match and up to 1.0 for all wrong. */
static float timing_error_rate(uint8_t const *grid, int qr_size) {
if (qr_size < 15) {
return 0.0f;
}
int errors = 0;
int total = 0;
for (int c = 8; c <= qr_size - 9; c++) {
int expected = ((c % 2) == 0) ? 1 : 0;
int actual = grid[6 * qr_size + c] ? 1 : 0;
if (actual != expected) {
errors++;
}
total++;
}
for (int r = 8; r <= qr_size - 9; r++) {
int expected = ((r % 2) == 0) ? 1 : 0;
int actual = grid[r * qr_size + 6] ? 1 : 0;
if (actual != expected) {
errors++;
}
total++;
}
if (total == 0) {
return 1.0f;
}
return (float)errors / (float)total;
}
/* ---- Finder pattern structural check ----
After sampling, each of the three 7x7 finder regions should match the
canonical dark/light pattern (outer ring dark, 1-module light inset, 3x3
dark core). Plus the 1-module separator (row/col between finder and data)
should be light. Incidental 1:1:3:1:1 runs in noise almost never project
into grids whose TL/TR/BL 7x7 windows all match the finder pattern.
Returns the fraction of the 48 checked cells (16 per finder) that
disagree — callers reject above ~0.10. */
static const uint8_t FINDER_PATTERN[7][7] = {
{1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 1, 1, 0, 1},
{1, 0, 1, 1, 1, 0, 1},
{1, 0, 1, 1, 1, 0, 1},
{1, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1},
};
static float finder_error_rate(uint8_t const *grid, int qr_size) {
if (qr_size < 21) {
return 1.0f;
}
int errors = 0;
int total = 0;
/* TL at (0,0); TR at (0, qr_size-7); BL at (qr_size-7, 0). */
const int origins[3][2] = {
{0, 0},
{0, qr_size - 7},
{qr_size - 7, 0},
};
for (int f = 0; f < 3; f++) {
int r0 = origins[f][0];
int c0 = origins[f][1];
for (int dr = 0; dr < 7; dr++) {
for (int dc = 0; dc < 7; dc++) {
int expected = FINDER_PATTERN[dr][dc];
int actual = grid[(r0 + dr) * qr_size + (c0 + dc)] ? 1 : 0;
if (actual != expected) {
errors++;
}
total++;
}
}
}
if (total == 0) {
return 1.0f;
}
return (float)errors / (float)total;
}
/* ---- Format information ---- */
static const int FMT_COORDS1[15][2] = {
{8, 0},
{8, 1},
{8, 2},
{8, 3},
{8, 4},
{8, 5},
{8, 7},
{8, 8},
{7, 8},
{5, 8},
{4, 8},
{3, 8},
{2, 8},
{1, 8},
{0, 8},
};
static const uint32_t VALID_FMTS[32] = {
0x5412, 0x5125, 0x5E7C, 0x5B4B, 0x45F9, 0x40CE, 0x4F97, 0x4AA0,
0x77C4, 0x72F3, 0x7DAA, 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976,
0x1689, 0x13BE, 0x1CE7, 0x19D0, 0x0762, 0x0255, 0x0D0C, 0x083B,
0x355F, 0x3068, 0x3F31, 0x3A06, 0x24B4, 0x2183, 0x2EDA, 0x2BED,
};
static int popcount_u(uint32_t v) {
int c = 0;
while (v) {
c += v & 1;
v >>= 1;
}
return c;
}
static int decode_format(
uint8_t const *grid,
int qr_size,
int *ec_level,
int *mask_pattern,
int *out_dist
) {
uint32_t bits1 = 0;
for (int i = 0; i < 15; i++) {
int col = FMT_COORDS1[i][0], row = FMT_COORDS1[i][1];
if (row < qr_size && col < qr_size && grid[row * qr_size + col]) {
bits1 |= (1u << i);
}
}
uint32_t bits2 = 0;
for (int i = 0; i < 7; i++) {
int row = qr_size - 1 - i;
if (grid[row * qr_size + 8]) {
bits2 |= (1u << i);
}
}
for (int i = 0; i < 8; i++) {
int col = qr_size - 8 + i;
if (grid[8 * qr_size + col]) {
bits2 |= (1u << (7 + i));
}
}
int best_d = 16, best_f = -1;
for (int copy = 0; copy < 2; copy++) {
uint32_t bits = (copy == 0) ? bits1 : bits2;
for (int f = 0; f < 32; f++) {
int d = popcount_u(bits ^ VALID_FMTS[f]);
if (d < best_d) {
best_d = d;
best_f = f;
}
}
}
if (best_f < 0 || best_d > 3) {
return -1;
}
int ec_ind = best_f >> 3;
*mask_pattern = best_f & 7;
static const int ec_map[4] = {1, 0, 3, 2};
*ec_level = ec_map[ec_ind];
if (out_dist) {
*out_dist = best_d;
}
return 0;
}
/* ---- Masking ---- */
static int apply_mask(int mask, int row, int col) {
switch (mask) {
case 0:
return (row + col) % 2 == 0;
case 1:
return row % 2 == 0;
case 2:
return col % 3 == 0;
case 3:
return (row + col) % 3 == 0;
case 4:
return (row / 2 + col / 3) % 2 == 0;
case 5:
return (row * col) % 2 + (row * col) % 3 == 0;
case 6:
return ((row * col) % 2 + (row * col) % 3) % 2 == 0;
case 7:
return ((row + col) % 2 + (row * col) % 3) % 2 == 0;
default:
return 0;
}
}
/* ---- Function pattern check ---- */
static const int ALIGN_POS[41][8] = {
{0},
{0},
{6, 18, 0},
{6, 22, 0},
{6, 26, 0},
{6, 30, 0},
{6, 34, 0},
{6, 22, 38, 0},
{6, 24, 42, 0},
{6, 26, 46, 0},
{6, 28, 50, 0},
{6, 30, 54, 0},
{6, 32, 58, 0},
{6, 34, 62, 0},
{6, 26, 46, 66, 0},
{6, 26, 48, 70, 0},
{6, 26, 50, 74, 0},
{6, 30, 54, 78, 0},
{6, 30, 56, 82, 0},
{6, 30, 58, 86, 0},
{6, 34, 62, 90, 0},
{6, 28, 50, 72, 94, 0},
{6, 26, 50, 74, 98, 0},
{6, 30, 54, 78, 102, 0},
{6, 28, 54, 80, 106, 0},
{6, 32, 58, 84, 110, 0},
{6, 30, 58, 86, 114, 0},
{6, 34, 62, 90, 118, 0},
{6, 26, 50, 74, 98, 122, 0},
{6, 30, 54, 78, 102, 126, 0},
{6, 26, 52, 78, 104, 130, 0},
{6, 30, 56, 82, 108, 134, 0},
{6, 34, 60, 86, 112, 138, 0},
{6, 30, 58, 86, 114, 142, 0},
{6, 34, 62, 90, 118, 146, 0},
{6, 30, 54, 78, 102, 126, 150, 0},
{6, 24, 50, 76, 102, 128, 154, 0},
{6, 28, 54, 80, 106, 132, 158, 0},
{6, 32, 58, 84, 110, 136, 162, 0},
{6, 26, 54, 82, 110, 138, 166, 0},
{6, 30, 58, 86, 114, 142, 170, 0},
};
static int is_function(int row, int col, int sz, int ver) {
if (row <= 8 && col <= 8) {
return 1;
}
if (row <= 8 && col >= sz - 8) {
return 1;
}
if (row >= sz - 8 && col <= 8) {
return 1;
}
if (row == 6 || col == 6) {
return 1;
}
if (ver >= 7) {
if (col >= sz - 11 && col <= sz - 9 && row <= 5) {
return 1;
}
if (row >= sz - 11 && row <= sz - 9 && col <= 5) {
return 1;
}
}
if (ver >= 2 && ver <= 40) {
const int *pos = ALIGN_POS[ver];
int np = 0;
while (pos[np] != 0 && np < 7) {
np++;
}
for (int i = 0; i < np; i++) {
for (int j = 0; j < np; j++) {
int ay = pos[i], ax = pos[j];
if (ay <= 8 && ax <= 8) {
continue;
}
if (ay <= 8 && ax >= sz - 8) {
continue;
}
if (ay >= sz - 8 && ax <= 8) {
continue;
}
if (abs(row - ay) <= 2 && abs(col - ax) <= 2) {
return 1;
}
}
}
}
return 0;
}
/* ---- Data extraction in zigzag order ---- */
static int extract_bits(
uint8_t const *grid,
uint8_t const *conf,
int sz,
int ver,
int mask,
uint8_t *bits,
uint8_t *bit_conf,
int max_bits
) {
int n = 0, up = 1;
for (int right = sz - 1; right >= 1; right -= 2) {
if (right == 6) {
right = 5;
}
for (int i = 0; i < sz; i++) {
int row = up ? (sz - 1 - i) : i;
for (int c = 0; c < 2; c++) {
int col = right - c;
if (col < 0) {
continue;
}
if (!is_function(row, col, sz, ver) && n < max_bits) {
int val = grid[row * sz + col];
if (apply_mask(mask, row, col)) {
val ^= 1;
}
bits[n] = (uint8_t)val;
if (bit_conf) {
bit_conf[n] = conf ? conf[row * sz + col] : 0;
}
n++;
}
}
}
up = !up;
}
return n;
}
/* ---- Bits to bytes ---- */
static int bits_to_bytes(
uint8_t const *bits,
uint8_t const *bit_conf,
int nbits,
uint8_t *bytes,
uint8_t *byte_ambig
) {
int nb = nbits / 8;
for (int i = 0; i < nb; i++) {
uint8_t b = 0;
int ambig = 0;
for (int j = 0; j < 8; j++) {
b = (b << 1) | bits[i * 8 + j];
if (bit_conf && bit_conf[i * 8 + j]) {
ambig = 1;
}
}
bytes[i] = b;
if (byte_ambig) {
byte_ambig[i] = (uint8_t)ambig;
}
}
return nb;
}
/* ---- Read N bits from byte array ---- */
static int read_bits(uint8_t const *data, int len, int *pos, int n) {
int val = 0;
for (int i = 0; i < n; i++) {
int bi = *pos + i;
int byte_idx = bi / 8, bit_idx = 7 - (bi % 8);
if (byte_idx < len) {
val = (val << 1) | ((data[byte_idx] >> bit_idx) & 1);
}
else {
val <<= 1;
}
}
*pos += n;
return val;
}
/* ---- EC table ---- */
typedef struct {
int total_cw, ec_cw_per_block;
int g1_blocks, g1_data_cw;
int g2_blocks, g2_data_cw;
} ECInfo;
static const ECInfo EC_TABLE[40][4] = {
/* L M Q H */
/* v1 */ {
{26, 7, 1, 19, 0, 0},
{26, 10, 1, 16, 0, 0},
{26, 13, 1, 13, 0, 0},
{26, 17, 1, 9, 0, 0}
},
/* v2 */
{{44, 10, 1, 34, 0, 0},
{44, 16, 1, 28, 0, 0},
{44, 22, 1, 22, 0, 0},
{44, 28, 1, 16, 0, 0}},
/* v3 */
{{70, 15, 1, 55, 0, 0},
{70, 26, 1, 44, 0, 0},
{70, 18, 2, 17, 0, 0},
{70, 22, 2, 13, 0, 0}},
/* v4 */
{{100, 20, 1, 80, 0, 0},
{100, 18, 2, 32, 0, 0},
{100, 26, 2, 24, 0, 0},
{100, 16, 4, 9, 0, 0}},
/* v5 */
{{134, 26, 1, 108, 0, 0},
{134, 24, 2, 43, 0, 0},
{134, 18, 2, 15, 2, 16},
{134, 22, 2, 11, 2, 12}},
/* v6 */
{{172, 18, 2, 68, 0, 0},
{172, 16, 4, 27, 0, 0},
{172, 24, 4, 19, 0, 0},
{172, 28, 4, 15, 0, 0}},
/* v7 */
{{196, 20, 2, 78, 0, 0},
{196, 18, 4, 31, 0, 0},
{196, 18, 2, 14, 4, 15},
{196, 26, 4, 13, 1, 14}},
/* v8 */
{{242, 24, 2, 97, 0, 0},
{242, 22, 2, 38, 2, 39},
{242, 22, 4, 18, 2, 19},
{242, 26, 4, 14, 2, 15}},
/* v9 */
{{292, 30, 2, 116, 0, 0},
{292, 22, 3, 36, 2, 37},
{292, 20, 4, 16, 4, 17},
{292, 24, 4, 12, 4, 13}},
/* v10 */
{{346, 18, 2, 68, 2, 69},
{346, 26, 4, 43, 1, 44},
{346, 24, 6, 19, 2, 20},
{346, 28, 6, 15, 2, 16}},
/* v11 */
{{404, 20, 4, 81, 0, 0},
{404, 30, 1, 50, 4, 51},
{404, 28, 4, 22, 4, 23},
{404, 24, 3, 12, 8, 13}},
/* v12 */
{{466, 24, 2, 92, 2, 93},
{466, 22, 6, 36, 2, 37},
{466, 26, 4, 20, 6, 21},
{466, 28, 7, 14, 4, 15}},
/* v13 */
{{532, 26, 4, 107, 0, 0},
{532, 22, 8, 37, 1, 38},
{532, 24, 8, 20, 4, 21},
{532, 22, 12, 11, 4, 12}},
/* v14 */
{{581, 30, 3, 115, 1, 116},
{581, 24, 4, 40, 5, 41},
{581, 20, 11, 16, 5, 17},
{581, 24, 11, 12, 5, 13}},
/* v15 */
{{655, 22, 5, 87, 1, 88},
{655, 24, 5, 41, 5, 42},
{655, 30, 5, 24, 7, 25},
{655, 24, 11, 12, 7, 13}},
/* v16 */
{{733, 24, 5, 98, 1, 99},
{733, 28, 7, 45, 3, 46},
{733, 24, 15, 19, 2, 20},
{733, 30, 3, 15, 13, 16}},
/* v17 */
{{815, 28, 1, 107, 5, 108},
{815, 28, 10, 46, 1, 47},
{815, 28, 1, 22, 15, 23},
{815, 28, 2, 14, 17, 15}},
/* v18 */
{{901, 30, 5, 120, 1, 121},
{901, 26, 9, 43, 4, 44},
{901, 28, 17, 22, 1, 23},
{901, 28, 2, 14, 19, 15}},
/* v19 */
{{991, 28, 3, 113, 4, 114},
{991, 26, 3, 44, 11, 45},
{991, 26, 17, 21, 4, 22},
{991, 26, 9, 13, 16, 14}},
/* v20 */
{{1085, 28, 3, 107, 5, 108},
{1085, 26, 3, 41, 13, 42},
{1085, 30, 15, 24, 5, 25},
{1085, 28, 15, 15, 10, 16}},
/* v21 */
{{1156, 28, 4, 116, 4, 117},
{1156, 26, 17, 42, 0, 0},
{1156, 28, 17, 22, 6, 23},
{1156, 30, 19, 16, 6, 17}},
/* v22 */
{{1258, 28, 2, 111, 7, 112},
{1258, 28, 17, 46, 0, 0},
{1258, 30, 7, 24, 16, 25},
{1258, 24, 34, 13, 0, 0}},
/* v23 */
{{1364, 30, 4, 121, 5, 122},
{1364, 28, 4, 47, 14, 48},
{1364, 30, 11, 24, 14, 25},
{1364, 30, 16, 15, 14, 16}},
/* v24 */
{{1474, 30, 6, 117, 4, 118},
{1474, 28, 6, 45, 14, 46},
{1474, 30, 11, 24, 16, 25},
{1474, 30, 30, 16, 2, 17}},
/* v25 */
{{1588, 26, 8, 106, 4, 107},
{1588, 28, 8, 47, 13, 48},
{1588, 30, 7, 24, 22, 25},
{1588, 30, 22, 15, 13, 16}},
/* v26 */
{{1706, 28, 10, 114, 2, 115},
{1706, 28, 19, 46, 4, 47},
{1706, 28, 28, 22, 6, 23},
{1706, 30, 33, 16, 4, 17}},
/* v27 */
{{1828, 30, 8, 122, 4, 123},
{1828, 28, 22, 45, 3, 46},
{1828, 30, 8, 23, 26, 24},
{1828, 30, 12, 15, 28, 16}},
/* v28 */
{{1921, 30, 3, 117, 10, 118},
{1921, 28, 3, 45, 23, 46},
{1921, 30, 4, 24, 31, 25},
{1921, 30, 11, 15, 31, 16}},
/* v29 */
{{2051, 30, 7, 116, 7, 117},
{2051, 28, 21, 45, 7, 46},
{2051, 30, 1, 23, 37, 24},
{2051, 30, 19, 15, 26, 16}},
/* v30 */
{{2185, 30, 5, 115, 10, 116},
{2185, 28, 19, 47, 10, 48},
{2185, 30, 15, 24, 25, 25},
{2185, 30, 23, 15, 25, 16}},
/* v31 */
{{2323, 30, 13, 115, 3, 116},
{2323, 28, 2, 46, 29, 47},
{2323, 30, 42, 24, 1, 25},
{2323, 30, 23, 15, 28, 16}},
/* v32 */
{{2465, 30, 17, 115, 0, 0},
{2465, 28, 10, 46, 23, 47},
{2465, 30, 10, 24, 35, 25},
{2465, 30, 19, 15, 35, 16}},
/* v33 */
{{2611, 30, 17, 115, 1, 116},
{2611, 28, 14, 46, 21, 47},
{2611, 30, 29, 24, 19, 25},
{2611, 30, 11, 15, 46, 16}},
/* v34 */
{{2761, 30, 13, 115, 6, 116},
{2761, 28, 14, 46, 23, 47},
{2761, 30, 44, 24, 7, 25},
{2761, 30, 59, 16, 1, 17}},
/* v35 */
{{2876, 30, 12, 121, 7, 122},
{2876, 28, 12, 47, 26, 48},
{2876, 30, 39, 24, 14, 25},
{2876, 30, 22, 15, 41, 16}},
/* v36 */
{{3034, 30, 6, 121, 14, 122},
{3034, 28, 6, 47, 34, 48},
{3034, 30, 46, 24, 10, 25},
{3034, 30, 2, 15, 64, 16}},
/* v37 */
{{3196, 30, 17, 122, 4, 123},
{3196, 28, 29, 46, 14, 47},
{3196, 30, 49, 24, 10, 25},
{3196, 30, 24, 15, 46, 16}},
/* v38 */
{{3362, 30, 4, 122, 18, 123},
{3362, 28, 13, 46, 32, 47},
{3362, 30, 48, 24, 14, 25},
{3362, 30, 42, 15, 32, 16}},
/* v39 */
{{3532, 30, 20, 117, 4, 118},
{3532, 28, 40, 47, 7, 48},
{3532, 30, 43, 24, 22, 25},
{3532, 30, 10, 15, 67, 16}},
/* v40 */
{{3706, 30, 19, 118, 6, 119},
{3706, 28, 18, 47, 31, 48},
{3706, 30, 34, 24, 34, 25},
{3706, 30, 20, 15, 61, 16}},
};
/* ---- Reed-Solomon error correction over GF(256), primitive poly 0x11d ---- */
static uint8_t gf_exp[512];
static uint8_t gf_log[256];
static int gf_initialized = 0;
static void gf_init(void) {
if (gf_initialized) {
return;
}
int x = 1;
for (int i = 0; i < 255; i++) {
gf_exp[i] = (uint8_t)x;
gf_log[x] = (uint8_t)i;
x <<= 1;
if (x & 0x100) {
x ^= 0x11d;
}
}
for (int i = 255; i < 512; i++) {
gf_exp[i] = gf_exp[i - 255];
}
gf_initialized = 1;
}
static uint8_t gf_mul(uint8_t a, uint8_t b) {
if (a == 0 || b == 0) {
return 0;
}
return gf_exp[gf_log[a] + gf_log[b]];
}
static uint8_t gf_inv(uint8_t a) { return gf_exp[255 - gf_log[a]]; }
/* Decode an RS block of nk data codewords followed by nroots EC codewords,
in standard polynomial order (msg[0] is highest-degree coefficient).
Optional erasure positions (eras_pos[k] is the msg index of an erasure)
double the per-block correction budget: 2*errors + erasures <= nroots.
Pass eras_pos=NULL/n_eras=0 for plain errors-only decoding (corrects up
to nroots/2 errors). Returns number of corrections (errors+erasures) made
in place, or -1 if the block is uncorrectable. */
static int rs_decode_block_eras(
uint8_t *msg,
int nk,
int nroots,
const int *eras_pos,
int n_eras
) {
gf_init();
int n = nk + nroots;
if (nroots <= 0 || nroots > 64 || nk <= 0 || n > 300) {
return -1;
}
if (n_eras < 0 || n_eras > nroots) {
return -1;
}
/* Syndromes: S[i] = msg(α^i) for i in [0, nroots). */
uint8_t syn[64];
int nonzero = 0;
for (int i = 0; i < nroots; i++) {
uint8_t x = gf_exp[i];
uint8_t s = 0;
for (int j = 0; j < n; j++) {
s = gf_mul(s, x) ^ msg[j];
}
syn[i] = s;
if (s) {
nonzero = 1;
}
}
if (!nonzero) {
return 0;
}
/* Erasure locator Λ_E(x) = ∏ (1 - α^{n-1-eras_pos[k]} x), ascending degree.
Roots at x = α^{-(n-1-eras_pos[k])}, matching the same x = α^{-i}
convention used by the Chien search below. */
uint8_t lambda_e[65] = {0};
lambda_e[0] = 1;
int le_deg = 0;
uint8_t seen_eras[300] = {0};
for (int k = 0; k < n_eras; k++) {
int e = eras_pos[k];
if (e < 0 || e >= n) {
return -1;
}
if (seen_eras[e]) {
continue; /* dedupe duplicate erasure positions */
}
seen_eras[e] = 1;
int p = (n - 1 - e) % 255;
if (p < 0) {
p += 255;
}
uint8_t a = gf_exp[p];
if (le_deg + 1 >= 65) {
return -1;
}
le_deg++;
for (int i = le_deg; i > 0; i--) {
lambda_e[i] ^= gf_mul(a, lambda_e[i - 1]);
}
}
/* Forney syndromes T(x) = (Λ_E(x) * S(x)) mod x^nroots. */
uint8_t Tsyn[64] = {0};
for (int i = 0; i < nroots; i++) {
uint8_t s = 0;
for (int j = 0; j <= le_deg && j <= i; j++) {
s ^= gf_mul(lambda_e[j], syn[i - j]);
}
Tsyn[i] = s;
}
/* Berlekamp-Massey on the trailing (nroots - le_deg) Forney syndromes to
find the error locator Σ(x) for unknown-position errors. The first
le_deg Forney coefficients are absorbed by the erasure locator. */
uint8_t C[65] = {0};
uint8_t B[65] = {0};
C[0] = 1;
B[0] = 1;
int L = 0;
int m = 1;
uint8_t b_last = 1;
int n_iter = nroots - le_deg;
for (int k = 0; k < n_iter; k++) {
int kk = k + le_deg;
uint8_t delta = Tsyn[kk];
for (int i = 1; i <= L; i++) {
if (kk - i >= 0) {
delta ^= gf_mul(C[i], Tsyn[kk - i]);
}
}
if (delta != 0) {
uint8_t coef = gf_mul(delta, gf_inv(b_last));
uint8_t Tc[65];
memcpy(Tc, C, sizeof(Tc));
for (int i = 0; i + m < 65; i++) {
C[i + m] ^= gf_mul(coef, B[i]);
}
if (2 * L <= k) {
L = k + 1 - L;
memcpy(B, Tc, sizeof(B));
b_last = delta;
m = 1;
}
else {
m++;
}
}
else {
m++;
}
}
if (2 * L + le_deg > nroots) {
return -1;
}
/* Combined locator Ψ(x) = Λ_E(x) * Σ(x). Coefficients in [0..130). */
uint8_t Psi[130] = {0};
for (int i = 0; i <= le_deg; i++) {
if (!lambda_e[i]) {
continue;
}
for (int j = 0; j <= L; j++) {
if (!C[j]) {
continue;
}
Psi[i + j] ^= gf_mul(lambda_e[i], C[j]);
}
}
int psi_deg = le_deg + L;
if (psi_deg <= 0 || psi_deg > nroots) {
return -1;
}
/* Chien search: combined locator roots are at x = α^{-i}. */
int err_pos[64];
int nerr = 0;
for (int i = 0; i < n; i++) {
int xi = (255 - (i % 255)) % 255;
uint8_t v = 0;
for (int j = 0; j <= psi_deg; j++) {
if (Psi[j]) {
int e = (gf_log[Psi[j]] + xi * j) % 255;
v ^= gf_exp[e];
}
}
if (v == 0) {
if (nerr >= 64) {
return -1;
}
err_pos[nerr++] = i;
}
}
if (nerr != psi_deg) {
return -1;
}
/* Forney: Ω(x) = (S(x) * Ψ(x)) mod x^nroots, then magnitude at position i
is Ω(α^{-i}) / Ψ'(α^{-i}). Ψ has degree up to nroots, so omega and Ψ'
buffers need to span that range. */
uint8_t omega[130] = {0};
for (int i = 0; i < nroots; i++) {
for (int j = 0; j <= psi_deg && i + j < 130; j++) {
if (i + j < nroots) {
omega[i + j] ^= gf_mul(syn[i], Psi[j]);
}
}
}
uint8_t Psip[130] = {0};
for (int i = 1; i <= psi_deg; i += 2) {
Psip[i - 1] = Psi[i];
}
for (int k = 0; k < nerr; k++) {
int i = err_pos[k];
int xi = (255 - (i % 255)) % 255;
uint8_t o = 0;
for (int j = 0; j < nroots; j++) {
if (omega[j]) {
int e = (gf_log[omega[j]] + xi * j) % 255;
o ^= gf_exp[e];
}
}
uint8_t cp = 0;
for (int j = 0; j < 130; j++) {
if (Psip[j]) {
int e = (gf_log[Psip[j]] + xi * j) % 255;
cp ^= gf_exp[e];
}
}
if (cp == 0) {
return -1;
}
/* Forney for syndromes starting at b=0: e_j = X_j * Ω(X_j^{-1}) /
Ψ'(X_j^{-1}). The X_j factor is essential — without it the recovered
magnitude is e_j / X_j, the verify check fails, and RS decoding silently
bails out on every block with at least one real error. */
int xpow = i % 255;
uint8_t correction = gf_mul(o, gf_inv(cp));
correction = gf_mul(correction, gf_exp[xpow]);
msg[n - 1 - i] ^= correction;
}
/* Verify: all syndromes zero now. */
for (int i = 0; i < nroots; i++) {
uint8_t x = gf_exp[i];
uint8_t s = 0;
for (int j = 0; j < n; j++) {
s = gf_mul(s, x) ^ msg[j];
}
if (s != 0) {
return -1;
}
}
return nerr;
}
static int rs_decode_block(uint8_t *msg, int nk, int nroots) {
return rs_decode_block_eras(msg, nk, nroots, NULL, 0);
}
/* ---- De-interleave codewords + Reed-Solomon error correction ----
Splits the raw interleaved codeword stream into per-block [data | EC]
buffers, runs RS decode on each block, and writes the corrected data
codewords to `out`. Returns the total number of data codewords, or 0 on
structural failure. Individual block RS failures are tolerated: the
(uncorrected) bytes are still written so the payload decoder can try.
If out_rs_cost is non-NULL, writes the total RS error-correction cost:
sum of bits corrected across all blocks, plus a heavy penalty per
uncorrectable block. Lower values mean a more confident decode. */
static int deinterleave(
uint8_t const *raw,
uint8_t const *raw_ambig,
int raw_len,
const ECInfo *info,
uint8_t *out,
int *out_rs_cost
) {
int total_blocks = info->g1_blocks + info->g2_blocks;
if (total_blocks == 0 || total_blocks > 100) {
return 0;
}
int ec_cw = info->ec_cw_per_block;
int total_data =
info->g1_blocks * info->g1_data_cw + info->g2_blocks * info->g2_data_cw;
int total_needed = total_data + total_blocks * ec_cw;
if (total_needed > raw_len) {
/* Not enough bytes to run RS. Fall back to copying available data only. */
int n = total_data < raw_len ? total_data : raw_len;
memcpy(out, raw, n);
return n;
}
uint8_t **blocks = calloc(total_blocks, sizeof(uint8_t *));
uint8_t **block_ambig = NULL;
int *dlen = calloc(total_blocks, sizeof(int));
if (raw_ambig) {
block_ambig = calloc(total_blocks, sizeof(uint8_t *));
}
if (!blocks || !dlen || (raw_ambig && !block_ambig)) {
free(blocks);
free(dlen);
free(block_ambig);
return 0;
}
int alloc_fail = 0;
for (int b = 0; b < total_blocks; b++) {
dlen[b] = (b < info->g1_blocks) ? info->g1_data_cw : info->g2_data_cw;
blocks[b] = calloc(dlen[b] + ec_cw, 1);
if (block_ambig) {
block_ambig[b] = calloc(dlen[b] + ec_cw, 1);
if (!block_ambig[b]) {
alloc_fail = 1;
}
}
if (!blocks[b]) {
alloc_fail = 1;
}
}
if (alloc_fail) {
for (int b = 0; b < total_blocks; b++) {
free(blocks[b]);
if (block_ambig) {
free(block_ambig[b]);
}
}
free(blocks);
free(block_ambig);
free(dlen);
return 0;
}
int max_data_cw = (info->g2_blocks > 0) ? info->g2_data_cw : info->g1_data_cw;
int pos = 0;
/* Data codewords are interleaved round-robin by position. Group-1 blocks
run out first (they're shorter), so later rounds only fill group-2. */
for (int i = 0; i < max_data_cw; i++) {
for (int b = 0; b < total_blocks && pos < raw_len; b++) {
if (i < dlen[b]) {
blocks[b][i] = raw[pos];
if (block_ambig) {
block_ambig[b][i] = raw_ambig[pos];
}
pos++;
}
}
}
/* EC codewords: all blocks have the same EC length, so clean round-robin. */
for (int i = 0; i < ec_cw; i++) {
for (int b = 0; b < total_blocks && pos < raw_len; b++) {
blocks[b][dlen[b] + i] = raw[pos];
if (block_ambig) {
block_ambig[b][dlen[b] + i] = raw_ambig[pos];
}
pos++;
}
}
/* Snapshot the original block bytes BEFORE rs_decode_block mutates them
in place. Used to retry with erasures (and to detect when erasure
decoding flips bytes the errors-only path "corrected" away). */
uint8_t **orig = NULL;
if (block_ambig) {
orig = calloc(total_blocks, sizeof(uint8_t *));
if (orig) {
for (int b = 0; b < total_blocks; b++) {
int blk_n = dlen[b] + ec_cw;
orig[b] = malloc(blk_n);
if (orig[b]) {
memcpy(orig[b], blocks[b], blk_n);
}
}
}
}
int rs_cost = 0;
for (int b = 0; b < total_blocks; b++) {
int blk_n = dlen[b] + ec_cw;
int corr = rs_decode_block(blocks[b], dlen[b], ec_cw);
if (block_ambig && orig && orig[b]) {
/* Collect erasure candidate positions for this block. */
int eras_pos[64];
int n_eras = 0;
for (int k = 0; k < blk_n && n_eras < ec_cw && n_eras < 64; k++) {
if (block_ambig[b][k]) {
eras_pos[n_eras++] = k;
}
}
if (n_eras > 0) {
uint8_t *retry = malloc(blk_n);
if (retry) {
memcpy(retry, orig[b], blk_n);
int corr2 =
rs_decode_block_eras(retry, dlen[b], ec_cw, eras_pos, n_eras);
if (corr2 >= 0) {
int prefer_erasure = 0;
if (corr < 0) {
/* Errors-only failed; erasure version is our only option. */
prefer_erasure = 1;
}
else if (memcmp(retry, blocks[b], blk_n) != 0) {
/* Errors-only "succeeded" but with a different answer than
the erasure-aware decode. The erasure decode had MORE
constraints (specific byte positions known to be unreliable)
so it's more likely to be the true codeword when our
erasure candidates overlap the real errors. The risk: if
most candidates were wrong, this flips a correct decode to
a wrong one — the upper-layer payload validator and
fmt_dist scoring is what saves us when that happens. */
prefer_erasure = 1;
}
if (prefer_erasure) {
memcpy(blocks[b], retry, blk_n);
corr = corr2;
rs_cost += corr2 + n_eras * 2 + 200;
free(retry);
continue;
}
}
free(retry);
}
}
}
if (corr < 0) {
/* Uncorrectable block: every EC symbol effectively spent without
success. Penalise heavily so this decode loses ties to one whose
blocks all decoded cleanly. */
rs_cost += ec_cw * 8 + 1000;
}
else {
rs_cost += corr;
}
}
if (orig) {
for (int b = 0; b < total_blocks; b++) {
free(orig[b]);
}
free(orig);
}
if (out_rs_cost) {
*out_rs_cost = rs_cost;
}
int out_pos = 0;
for (int b = 0; b < total_blocks; b++) {
memcpy(out + out_pos, blocks[b], dlen[b]);
out_pos += dlen[b];
free(blocks[b]);
if (block_ambig) {
free(block_ambig[b]);
}
}
free(blocks);
free(block_ambig);
free(dlen);
return out_pos;
}
/* ---- Payload decoding ---- */
static char *decode_payload(uint8_t const *data, int len, int ver) {
if (len < 1) {
return NULL;
}
int pos = 0;
int mode = read_bits(data, len, &pos, 4);
if (mode == 0) {
return NULL;
}
/* ECI (Extended Channel Interpretation): consume the assignment number
bytes and continue with the next mode indicator. URL QRs commonly
begin with ECI 26 (UTF-8) or ECI 3 (ISO-8859-1) followed by byte
mode. We don't do any character-set re-encoding — all the ECIs we
care about produce payloads that are already valid UTF-8 or ASCII. */
if (mode == 7) {
int first = read_bits(data, len, &pos, 1);
if (first < 0) {
return NULL;
}
if (first == 0) {
/* 1-byte ECI (7 more bits, total 8) */
if (read_bits(data, len, &pos, 7) < 0) {
return NULL;
}
}
else {
int second = read_bits(data, len, &pos, 1);
if (second < 0) {
return NULL;
}
if (second == 0) {
/* 2-byte ECI (14 more bits, total 16) */
if (read_bits(data, len, &pos, 14) < 0) {
return NULL;
}
}
else {
/* 3-byte ECI (21 more bits, total 24) */
if (read_bits(data, len, &pos, 21) < 0) {
return NULL;
}
}
}
mode = read_bits(data, len, &pos, 4);
if (mode == 0) {
return NULL;
}
}
int cc_bits;
if (mode == 1) {
cc_bits = (ver < 10) ? 10 : (ver < 27) ? 12 : 14;
}
else if (mode == 2) {
cc_bits = (ver < 10) ? 9 : (ver < 27) ? 11 : 13;
}
else if (mode == 4) {
cc_bits = (ver < 10) ? 8 : 16;
}
else if (mode == 8) {
cc_bits = (ver < 10) ? 8 : (ver < 27) ? 10 : 12;
}
else {
return NULL;
}
int count = read_bits(data, len, &pos, cc_bits);
/* Reject impossible counts: must be positive and must fit in the data bits
remaining after all header bits consumed so far (mode + optional ECI
header + count). A bit-flip in the high bit of the count field can
otherwise produce massively-inflated counts that consume padding bytes
as fake content (numeric mode is especially prone — corrupted padding
bytes decode as long runs of '0' digits). */
int avail_bits = len * 8 - pos;
if (avail_bits < 0) {
free(NULL);
return NULL;
}
int max_count;
if (mode == 1) { /* Numeric: 10 bits per 3 digits */
max_count = (avail_bits * 3) / 10;
}
else if (mode == 2) { /* Alphanumeric: 11 bits per 2 chars */
max_count = (avail_bits * 2) / 11;
}
else if (mode == 4) { /* Byte: 8 bits per char */
max_count = avail_bits / 8;
}
else { /* Kanji: 13 bits per char */
max_count = avail_bits / 13;
}
if (count <= 0 || count > max_count) {
return NULL;
}
char *result = malloc(count + 1);
if (!result) {
return NULL;
}
if (mode == 4) { /* Byte */
for (int i = 0; i < count; i++) {
result[i] = (char)read_bits(data, len, &pos, 8);
}
result[count] = '\0';
}
else if (mode == 2) { /* Alphanumeric */
static const char tbl[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
int i = 0;
while (i + 1 < count) {
int v = read_bits(data, len, &pos, 11);
int hi = v / 45;
int lo = v % 45;
if (hi >= 45) {
free(result);
return NULL;
}
result[i] = tbl[hi];
result[i + 1] = tbl[lo];
i += 2;
}
if (i < count) {
int v = read_bits(data, len, &pos, 6);
if (v >= 45) {
free(result);
return NULL;
}
result[i] = tbl[v];
}
result[count] = '\0';
}
else if (mode == 1) { /* Numeric */
int i = 0;
while (i + 2 < count) {
int v = read_bits(data, len, &pos, 10);
result[i] = '0' + v / 100;
result[i + 1] = '0' + (v / 10) % 10;
result[i + 2] = '0' + v % 10;
i += 3;
}
if (count - i == 2) {
int v = read_bits(data, len, &pos, 7);
result[i] = '0' + v / 10;
result[i + 1] = '0' + v % 10;
}
else if (count - i == 1) {
result[i] = '0' + read_bits(data, len, &pos, 4);
}
result[count] = '\0';
}
else {
free(result);
return NULL;
}
return result;
}
/* Decode a sampled grid into a payload string, assuming canonical
orientation (finders at TL / TR / BL). Returns NULL on failure. Writes
the format-code Hamming distance to *out_fmt_dist if non-NULL. Writes
the Reed-Solomon correction cost (lower = more confident) to
*out_rs_cost if non-NULL. */
static char *decode_grid_oriented(
uint8_t *grid,
uint8_t const *conf,
int qr_size,
int version,
int *out_fmt_dist,
int *out_rs_cost
) {
/* Structural check on the three finder windows. Real QRs match the
7x7 finder pattern at (0,0) / (0, qr_size-7) / (qr_size-7, 0) almost
perfectly; hallucinated grids only match about half the 147 cells.
The 0.15 cap leaves room for moderate homography drift and sampling
jitter under blur/perspective while killing the small-module numeric
FPs that slipped past the timing alternation. */
float fer = finder_error_rate(grid, qr_size);
if (fer > 0.15f) {
return NULL;
}
/* Gate on the timing pattern before the expensive RS path runs. Triples
lifted from noise almost always flunk this (alternation is random).
Base threshold 0.22 gives real QRs under moderate blur/perspective
plenty of headroom while nearly every hallucinated grid sits around
0.5. When the three finder windows match their 7x7 template almost
perfectly (fer < 0.03) we're confident the triple is real — raise
the timing cap to 0.35 so photographs with paper curl or moderate
perspective drift don't get rejected on what is ultimately a grid-
sampling approximation error, not a false positive. */
float timing_cap = (fer < 0.03f) ? 0.35f : 0.22f;
if (timing_error_rate(grid, qr_size) > timing_cap) {
return NULL;
}
int ec_level, mask_pattern;
int fmt_dist = 16;
if (decode_format(grid, qr_size, &ec_level, &mask_pattern, &fmt_dist) < 0) {
return NULL;
}
if (out_fmt_dist) {
*out_fmt_dist = fmt_dist;
}
if (out_rs_cost) {
*out_rs_cost = 100000;
}
int max_bits = qr_size * qr_size;
uint8_t *data_bits = malloc(max_bits);
uint8_t *bit_conf = NULL;
if (conf) {
bit_conf = malloc(max_bits);
if (!bit_conf) {
free(data_bits);
return NULL;
}
}
if (!data_bits) {
free(bit_conf);
return NULL;
}
int nbits = extract_bits(
grid,
conf,
qr_size,
version,
mask_pattern,
data_bits,
bit_conf,
max_bits
);
int max_bytes = nbits / 8;
uint8_t *raw_bytes = malloc(max_bytes + 1);
uint8_t *byte_ambig = NULL;
if (bit_conf) {
byte_ambig = malloc(max_bytes + 1);
if (!byte_ambig) {
free(data_bits);
free(bit_conf);
free(raw_bytes);
return NULL;
}
}
if (!raw_bytes) {
free(data_bits);
free(bit_conf);
free(byte_ambig);
return NULL;
}
int nraw = bits_to_bytes(data_bits, bit_conf, nbits, raw_bytes, byte_ambig);
free(data_bits);
free(bit_conf);
int data_cw_count = nraw;
uint8_t *data_bytes = raw_bytes;
uint8_t *deint_buf = NULL;
int rs_cost = 100000;
if (version >= 1 && version <= 40 && ec_level >= 0 && ec_level <= 3) {
const ECInfo *info = &EC_TABLE[version - 1][ec_level];
int total_data =
info->g1_blocks * info->g1_data_cw + info->g2_blocks * info->g2_data_cw;
if (total_data > 0 && total_data <= nraw) {
deint_buf = malloc(total_data + 1);
if (deint_buf) {
data_cw_count =
deinterleave(raw_bytes, byte_ambig, nraw, info, deint_buf, &rs_cost);
data_bytes = deint_buf;
}
else {
data_cw_count = total_data;
}
}
}
char *decoded = decode_payload(data_bytes, data_cw_count, version);
free(raw_bytes);
free(byte_ambig);
free(deint_buf);
if (decoded) {
int dlen = (int)strlen(decoded);
for (int i = 0; i < dlen; i++) {
uint8_t ch = (uint8_t)decoded[i];
if (ch < 0x20 || ch > 0x7e) {
free(decoded);
return NULL;
}
}
}
if (decoded && out_rs_cost) {
*out_rs_cost = rs_cost;
}
return decoded;
}
/* Transpose an n*n byte buffer (grid[i*n+j] -> out[j*n+i]). */
static void transpose_grid_u8(uint8_t const *src, uint8_t *dst, int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dst[j * n + i] = src[i * n + j];
}
}
}
/* Decode a sampled grid, retrying with the matrix transposed when the
canonical orientation fails. A "mirrored" QR (ZXing terminology: the
symbol read as its own reflection, e.g. through a mirror or printed
wrong-way-round) appears with rows and columns swapped relative to the
spec. Finder and timing patterns are symmetric under transposition, so
those structural checks pass on both orientations, but decode_format
samples the wrong cells and the decode aborts. Transposing the grid
(and confidence buffer) puts the format, mask, alignment patterns, and
zigzag data reading back into canonical positions. Runs only after the
canonical decode returns NULL, so valid non-mirrored QRs never pay for
the retry and cannot be displaced by a spurious mirror decode. */
static char *decode_grid(
uint8_t *grid,
uint8_t const *conf,
int qr_size,
int version,
int *out_fmt_dist,
int *out_rs_cost
) {
char *d = decode_grid_oriented(
grid,
conf,
qr_size,
version,
out_fmt_dist,
out_rs_cost
);
if (d) {
return d;
}
int n = qr_size * qr_size;
uint8_t *tgrid = malloc((size_t)n);
if (!tgrid) {
return NULL;
}
uint8_t *tconf = NULL;
if (conf) {
tconf = malloc((size_t)n);
if (!tconf) {
free(tgrid);
return NULL;
}
transpose_grid_u8(conf, tconf, qr_size);
}
transpose_grid_u8(grid, tgrid, qr_size);
int fd = 16, rc = 100000;
char *dm = decode_grid_oriented(tgrid, tconf, qr_size, version, &fd, &rc);
free(tgrid);
free(tconf);
if (dm) {
if (out_fmt_dist) {
*out_fmt_dist = fd;
}
if (out_rs_cost) {
*out_rs_cost = rc;
}
}
return dm;
}
/* ---- Decode the grid under a homography, then perturb the sample offset
to hunt for a stronger decode ----
The 5-point voting in sample_grid_homography_offset_mode is robust to small
grid drift inside a single module, but if the WHOLE sampling grid is shifted
by a fraction of a module (typical when finder centers carry sub-module error
under blur or perspective), entire rows or columns get misread the same
way. A 1-bit error in the byte-mode count field is the most damaging
failure mode — it can truncate an 11-character decode to 3 characters
while still passing the format BCH cleanly. Re-sampling at small (dx, dy)
shifts in module space and keeping the best decode catches these
off-by-fraction grid misalignments. */
static char *decode_with_perturbation(
uint8_t const *pixels,
uint8_t const *gray,
int width,
int height,
const double H[9],
int qr_size,
int version,
int *out_fmt_dist,
int *out_rs_cost
) {
/* Sample all 9 offsets (canonical + 8 perturbations) and collect the
decodes with their format-distance scores. We then apply two acceptance
paths: (a) if canonical succeeds, allow a strictly-longer perturbation
that has the canonical result as a prefix to replace it (the byte-mode
count-truncation recovery — "c3Q" -> "c3QrgLROiE5"). (b) if canonical
fails, only accept a perturbation result that two or more perturbations
agree on (a consensus decode), because lone perturbation decodes from
triples whose canonical homography fails are almost always wrong same-
length samples that other triples would decode correctly. */
#define PERT_N 10
/* Sample plan: 8 NN perturbations + 2 canonical samples (NN + bilinear).
Bilinear is only used at the canonical (0,0) offset — its purpose is
specifically to recover from the byte-mode count-truncation pattern
where NN nearest-pixel rounding clips the high bit of the count field.
Off-canonical bilinear samples introduce too many same-length wrong
decodes that distort the consensus vote in path (b). */
static const double offs[PERT_N][2] = {
{0.00, 0.00},
{0.00, 0.00}, /* p=0 NN canonical, p=1 bilinear canonical */
{0.30, 0.00},
{-0.30, 0.00},
{0.00, 0.30},
{0.00, -0.30},
{0.30, 0.30},
{-0.30, 0.30},
{0.30, -0.30},
{-0.30, -0.30},
};
char *decodes[PERT_N] = {0};
int fds[PERT_N];
int rs_costs[PERT_N];
size_t lens[PERT_N] = {0};
for (int i = 0; i < PERT_N; i++) {
fds[i] = 16;
rs_costs[i] = 100000;
}
for (int p = 0; p < PERT_N; p++) {
int use_bilinear = (p == 1);
uint8_t *conf = NULL;
uint8_t *g = sample_grid_homography_offset_mode(
pixels,
gray,
width,
height,
H,
qr_size,
offs[p][0],
offs[p][1],
use_bilinear,
gray ? &conf : NULL
);
if (!g) {
free(conf);
continue;
}
char *d = decode_grid(g, conf, qr_size, version, &fds[p], &rs_costs[p]);
free(g);
free(conf);
if (d) {
decodes[p] = d;
lens[p] = strlen(d);
}
/* Early exit if the NN canonical (p=0) is already a long, perfect
decode with no RS corrections — nothing can credibly improve on it. */
if (p == 0 && d && fds[0] == 0 && lens[0] >= 8 && rs_costs[0] == 0) {
break;
}
}
char *best = NULL;
int best_fd = 16;
int best_rs_out = 100000;
if (decodes[0]) {
/* Path (a): canonical succeeded. Replace either with (i) a strictly
longer decode that has the canonical as a prefix (the byte-mode
count-truncation recovery), or (ii) a same-length decode whose
Reed-Solomon correction cost is strictly lower (RS needed fewer
fixes, which is a strong signal that the perturbation hit cleaner
data bits even when format BCH ties at fmt_dist=0). */
best = decodes[0];
best_fd = fds[0];
int best_rs = rs_costs[0];
decodes[0] = NULL;
for (int p = 1; p < PERT_N; p++) {
if (!decodes[p]) {
continue;
}
int replace = 0;
if (lens[p] > strlen(best) &&
strncmp(decodes[p], best, strlen(best)) == 0) {
replace = 1;
}
else if (lens[p] == strlen(best) && rs_costs[p] < best_rs) {
replace = 1;
}
if (replace) {
free(best);
best = decodes[p];
best_fd = fds[p];
best_rs = rs_costs[p];
decodes[p] = NULL;
}
}
best_rs_out = best_rs;
}
else {
/* Path (b): canonical failed. Pick the perturbation decode that the
most other perturbations agree on, requiring at least two
agreements total. Tie-break: longer string, then lower fmt_dist.
Without consensus, return NULL so other triples can be tried. */
int best_idx = -1;
int best_count = 0;
for (int i = 1; i < PERT_N; i++) {
if (!decodes[i]) {
continue;
}
int count = 0;
for (int j = 1; j < PERT_N; j++) {
if (decodes[j] && strcmp(decodes[i], decodes[j]) == 0) {
count++;
}
}
int better = 0;
if (count > best_count) {
better = 1;
}
else if (count == best_count && best_idx >= 0) {
if (lens[i] > lens[best_idx]) {
better = 1;
}
else if (lens[i] == lens[best_idx] && fds[i] < fds[best_idx]) {
better = 1;
}
}
if (better) {
best_count = count;
best_idx = i;
}
}
if (best_idx >= 0 && best_count >= 2) {
best = decodes[best_idx];
best_fd = fds[best_idx];
best_rs_out = rs_costs[best_idx];
decodes[best_idx] = NULL;
}
}
for (int p = 0; p < PERT_N; p++) {
free(decodes[p]);
}
#undef PERT_N
if (best) {
if (out_fmt_dist) {
*out_fmt_dist = best_fd;
}
if (out_rs_cost) {
*out_rs_cost = best_rs_out;
}
}
return best;
}
/* ---- Try to decode a single QR code from a triple ---- */
static char *try_decode_triple(
uint8_t const *pixels,
uint8_t const *gray,
int width,
int height,
FinderPattern tl,
FinderPattern tr,
FinderPattern bl,
int *out_fmt_dist,
int *out_rs_cost,
Corners *out_corners,
Point2D out_finders[3],
double *out_module_size,
int *out_version,
int *out_qr_size,
Point2D out_alignments[QR_MAX_ALIGNMENTS],
int *out_alignment_count
) {
float dist_t = fp_dist(tl, tr), dist_l = fp_dist(tl, bl);
float avg_d = (dist_t + dist_l) / 2.0f;
float avg_m = (tl.module_size + tr.module_size + bl.module_size) / 3.0f;
if (avg_m < 0.5f) {
return NULL;
}
float qr_sz_f = avg_d / avg_m + 7.0f;
int version = (int)((qr_sz_f - 21.0f + 2.0f) / 4.0f) + 1;
if (version < 1) {
version = 1;
}
if (version > 40) {
version = 40;
}
int qr_size = 21 + (version - 1) * 4;
/* Build a perspective transform for grid sampling. For v≥2 QRs we search
for the BR alignment pattern and use its pixel location as the 4th
homography point; under perspective distortion this beats the
parallelogram-BR assumption that affine sampling relies on. For v1 (no
alignment) we fall back to the parallelogram BR. */
int fmt_dist = 16;
int rs_cost = 100000;
char *decoded = NULL;
/* Track the best initial homography we have so timing-pattern refinement
has a starting point to predict module-centroid positions from. */
double H_primary[9];
int have_primary_H = 0;
int have_alignment = 0;
double alignment_x = 0, alignment_y = 0;
/* Track the homography (if any) associated with the winning decode so
callers can enumerate every alignment pattern the image exposes. Not
populated for the quadratic-warp fallback, which has no single H. */
double winning_H[9];
int have_winning_H = 0;
if (version >= 2) {
float sx0 = 3.5f, sy0 = 3.5f;
float sx1 = qr_size - 3.5f, sy1 = 3.5f;
float sx2 = 3.5f, sy2 = qr_size - 3.5f;
float denom = (sx0 - sx2) * (sy1 - sy2) - (sx1 - sx2) * (sy0 - sy2);
if (fabsf(denom) >= 1e-6f) {
float a11 =
((tl.x - bl.x) * (sy1 - sy2) - (tr.x - bl.x) * (sy0 - sy2)) / denom;
float a12 =
((tr.x - bl.x) * (sx0 - sx2) - (tl.x - bl.x) * (sx1 - sx2)) / denom;
float a21 =
((tl.y - bl.y) * (sy1 - sy2) - (tr.y - bl.y) * (sy0 - sy2)) / denom;
float a22 =
((tr.y - bl.y) * (sx0 - sx2) - (tl.y - bl.y) * (sx1 - sx2)) / denom;
float b1 = tl.x - a11 * sx0 - a12 * sy0;
float b2 = tl.y - a21 * sx0 - a22 * sy0;
float amx = qr_size - 6.5f, amy = qr_size - 6.5f;
float predicted_ax = a11 * amx + a12 * amy + b1;
float predicted_ay = a21 * amx + a22 * amy + b2;
/* Search radius scales with QR size: under severe perspective, the
affine-predicted BR drift grows with finder separation — a v8
receipt photo can have the real BR alignment 10+ modules from
the affine prediction. Clamp so the search area stays bounded. */
float search_r = avg_m * 10.0f;
if (version >= 7) {
float wide = avg_m * (float)(qr_size / 4);
if (wide > search_r) {
search_r = wide;
}
if (search_r > avg_m * 20.0f) {
search_r = avg_m * 20.0f;
}
}
float found_ax, found_ay;
if (find_alignment_pattern(
pixels,
width,
height,
predicted_ax,
predicted_ay,
avg_m,
search_r,
&found_ax,
&found_ay
)) {
have_alignment = 1;
alignment_x = found_ax;
alignment_y = found_ay;
double src[4][2] = {{sx0, sy0}, {sx1, sy1}, {amx, amy}, {sx2, sy2}};
double dst[4][2] =
{{tl.x, tl.y}, {tr.x, tr.y}, {found_ax, found_ay}, {bl.x, bl.y}};
double H[9];
if (compute_homography(src, dst, H)) {
/* Try up to two homography variants for v7+ QRs: the exact
4-point fit from the BR snap (accurate when the snap is clean)
and a least-squares fit over all found alignment patterns
(accurate under paper curl or severe perspective, which
leaves the 4-point fit drifting inside the grid). Decode with
the BR-snap H first, then fall back to the LS-refined H if
the primary fails. */
double H_variants[2][9];
memcpy(H_variants[0], H, sizeof(H));
int n_variants = 1;
if (version >= 7) {
enum { MAX_AP = 45 };
double ap_src[MAX_AP][2], ap_dst[MAX_AP][2];
int n_ap = find_all_alignment_patterns(
pixels,
width,
height,
version,
H,
avg_m,
2.0f,
ap_src,
ap_dst,
MAX_AP
);
double ap_src_a[MAX_AP][2], ap_dst_a[MAX_AP][2];
double H_affine[9] = {a11, a12, b1, a21, a22, b2, 0.0, 0.0, 1.0};
int n_ap_a = find_all_alignment_patterns(
pixels,
width,
height,
version,
H_affine,
avg_m,
4.0f,
ap_src_a,
ap_dst_a,
MAX_AP
);
if (n_ap_a > n_ap) {
n_ap = n_ap_a;
memcpy(ap_src, ap_src_a, sizeof(ap_src_a));
memcpy(ap_dst, ap_dst_a, sizeof(ap_dst_a));
}
double H_refined[9];
memcpy(H_refined, H, sizeof(H_refined));
int refined_ok = 0;
for (int iter = 0; iter < 3 && n_ap >= 2; iter++) {
double pts_src[MAX_AP + 4][2];
double pts_dst[MAX_AP + 4][2];
int np = 0;
pts_src[np][0] = sx0;
pts_src[np][1] = sy0;
pts_dst[np][0] = tl.x;
pts_dst[np][1] = tl.y;
np++;
pts_src[np][0] = sx1;
pts_src[np][1] = sy1;
pts_dst[np][0] = tr.x;
pts_dst[np][1] = tr.y;
np++;
pts_src[np][0] = sx2;
pts_src[np][1] = sy2;
pts_dst[np][0] = bl.x;
pts_dst[np][1] = bl.y;
np++;
for (int i = 0; i < n_ap; i++) {
pts_src[np][0] = ap_src[i][0];
pts_src[np][1] = ap_src[i][1];
pts_dst[np][0] = ap_dst[i][0];
pts_dst[np][1] = ap_dst[i][1];
np++;
}
double H_ls[9];
if (!compute_homography_ls(pts_src, pts_dst, np, H_ls)) {
break;
}
/* Outlier rejection: discard any AP whose reprojection is
more than 1.5 modules off the fit — those are wrong snaps
(random dark clusters that match the 5x5 template). Real
APs sit within 0.5 modules of the fit. Finders anchor the
three known corners and are kept unconditionally. */
double pts_src_in[MAX_AP + 4][2];
double pts_dst_in[MAX_AP + 4][2];
int np_in = 3;
for (int i = 0; i < 3; i++) {
pts_src_in[i][0] = pts_src[i][0];
pts_src_in[i][1] = pts_src[i][1];
pts_dst_in[i][0] = pts_dst[i][0];
pts_dst_in[i][1] = pts_dst[i][1];
}
float thresh = avg_m * 1.5f;
for (int i = 3; i < np; i++) {
double rx, ry;
if (!project_homography(
H_ls,
pts_src[i][0],
pts_src[i][1],
&rx,
&ry
)) {
continue;
}
double dx = rx - pts_dst[i][0];
double dy = ry - pts_dst[i][1];
if (dx * dx + dy * dy <= thresh * thresh) {
pts_src_in[np_in][0] = pts_src[i][0];
pts_src_in[np_in][1] = pts_src[i][1];
pts_dst_in[np_in][0] = pts_dst[i][0];
pts_dst_in[np_in][1] = pts_dst[i][1];
np_in++;
}
}
if (np_in >= 4 && np_in < np) {
double H_ls2[9];
if (compute_homography_ls(
pts_src_in,
pts_dst_in,
np_in,
H_ls2
)) {
memcpy(H_ls, H_ls2, sizeof(H_ls2));
}
}
memcpy(H_refined, H_ls, sizeof(H_refined));
refined_ok = 1;
n_ap = find_all_alignment_patterns(
pixels,
width,
height,
version,
H_refined,
avg_m,
2.0f,
ap_src,
ap_dst,
MAX_AP
);
}
if (refined_ok) {
memcpy(H_variants[1], H_refined, sizeof(H_refined));
n_variants = 2;
}
}
memcpy(H_primary, H, sizeof(H));
have_primary_H = 1;
int fd = 16;
int rc = 100000;
char *d = NULL;
for (int v_idx = 0; v_idx < n_variants && !d; v_idx++) {
fd = 16;
rc = 100000;
d = decode_with_perturbation(
pixels,
gray,
width,
height,
H_variants[v_idx],
qr_size,
version,
&fd,
&rc
);
if (d) {
memcpy(winning_H, H_variants[v_idx], sizeof(winning_H));
have_winning_H = 1;
}
}
/* If neither variant decoded, try each found alignment pattern
as the 4th homography point on its own (exact 3-finders + 1-AP
fit). Covers severe perspective where one individual AP is
well-located but the ensemble fit gets pulled off by its
outliers. */
if (!d && version >= 7) {
enum { MAX_AP = 45 };
double ap_src[MAX_AP][2], ap_dst[MAX_AP][2];
double H_affine[9] = {a11, a12, b1, a21, a22, b2, 0.0, 0.0, 1.0};
int n_ap = find_all_alignment_patterns(
pixels,
width,
height,
version,
H_affine,
avg_m,
8.0f,
ap_src,
ap_dst,
MAX_AP
);
for (int ai = 0; ai < n_ap && !d; ai++) {
double src_i[4][2] = {
{sx0, sy0},
{sx1, sy1},
{ap_src[ai][0], ap_src[ai][1]},
{sx2, sy2}
};
double dst_i[4][2] = {
{tl.x, tl.y},
{tr.x, tr.y},
{ap_dst[ai][0], ap_dst[ai][1]},
{bl.x, bl.y}
};
double H_i[9];
if (!compute_homography(src_i, dst_i, H_i)) {
continue;
}
fd = 16;
rc = 100000;
d = decode_with_perturbation(
pixels,
gray,
width,
height,
H_i,
qr_size,
version,
&fd,
&rc
);
if (d) {
memcpy(winning_H, H_i, sizeof(winning_H));
have_winning_H = 1;
}
}
}
if (d) {
decoded = d;
fmt_dist = fd;
rs_cost = rc;
}
}
/* If the canonical alignment-based homography failed, sweep a
small grid of alignment-point perturbations and retry. Paper curl
and perspective foreshortening mean the real alignment pattern
can sit several pixels from where find_alignment_pattern snaps
to, while the homography is extremely sensitive to this 4th
point — a few pixels at the BR alignment translate to sub-module
drift across the data area, tripping the timing and finder
gates even when the three finder positions are pin-accurate.
Sweep radius scales with module size (the residual alignment
uncertainty is always a fraction of a module); 2x2 module
radius at half-module steps is cheap (25 samples) and covers
the drift seen on moderately curled photos. */
if (!decoded) {
float step = avg_m * 0.5f;
float radius = avg_m * 1.5f;
int n_steps = (int)(radius / step);
char *best_sweep = NULL;
int best_sweep_fd = 16;
int best_sweep_rc = 100000;
double best_sweep_H[9];
int have_best_sweep_H = 0;
for (int dy = -n_steps; dy <= n_steps; dy++) {
for (int dx = -n_steps; dx <= n_steps; dx++) {
if (dx == 0 && dy == 0) {
continue; /* already tried */
}
double pert_dst[4][2] = {
{tl.x, tl.y},
{tr.x, tr.y},
{found_ax + dx * step, found_ay + dy * step},
{bl.x, bl.y}
};
double Hp[9];
if (!compute_homography(src, pert_dst, Hp)) {
continue;
}
int fd = 16;
int rc = 100000;
char *d = decode_with_perturbation(
pixels,
gray,
width,
height,
Hp,
qr_size,
version,
&fd,
&rc
);
if (!d) {
continue;
}
/* Take the lowest-cost decode across the whole sweep rather
than stopping on the first hit — some perturbed alignments
yield a consensus decode that is still garbage (random
alphanumeric strings with fd=0 and rc=0 do occur when
sampling happens to line up with a false-but-self-
consistent grid). Prefer longer strings and lower RS
cost so the best geometric fit wins. */
int replace = 0;
if (!best_sweep) {
replace = 1;
}
else if (fd < best_sweep_fd) {
replace = 1;
}
else if (fd == best_sweep_fd &&
(int)strlen(d) > (int)strlen(best_sweep)) {
replace = 1;
}
else if (fd == best_sweep_fd &&
(int)strlen(d) == (int)strlen(best_sweep) &&
rc < best_sweep_rc) {
replace = 1;
}
if (replace) {
free(best_sweep);
best_sweep = d;
best_sweep_fd = fd;
best_sweep_rc = rc;
memcpy(best_sweep_H, Hp, sizeof(best_sweep_H));
have_best_sweep_H = 1;
}
else {
free(d);
}
}
}
/* Only accept sweep results with a strong confidence signal:
perfect format BCH (fd = 0) and near-zero RS correction cost.
Random perturbations in a QR-free region can consensus-
decode as alphanumeric garbage with fd=0 but substantial RS
cost (≥ 100), so locking the rc threshold low rejects those.
Real decodes with the right alignment typically need zero
corrections; a small tolerance (≤ 5) lets through slightly
noisy captures without admitting hallucinated decodes. */
if (best_sweep && best_sweep_fd == 0 && best_sweep_rc <= 5) {
decoded = best_sweep;
fmt_dist = best_sweep_fd;
rs_cost = best_sweep_rc;
if (have_best_sweep_H) {
memcpy(winning_H, best_sweep_H, sizeof(winning_H));
have_winning_H = 1;
}
}
else {
free(best_sweep);
}
}
}
}
}
/* If no homography-based decode worked yet, try the plain parallelogram
BR (affine-equivalent). */
if (!decoded) {
float br_x = tr.x + bl.x - tl.x;
float br_y = tr.y + bl.y - tl.y;
double src[4][2] = {
{3.5, 3.5},
{qr_size - 3.5, 3.5},
{qr_size - 3.5, qr_size - 3.5},
{3.5, qr_size - 3.5}
};
double dst[4][2] = {{tl.x, tl.y}, {tr.x, tr.y}, {br_x, br_y}, {bl.x, bl.y}};
double H[9];
if (compute_homography(src, dst, H)) {
if (!have_primary_H) {
memcpy(H_primary, H, sizeof(H));
have_primary_H = 1;
}
int fd = 16;
int rc = 100000;
char *d = decode_with_perturbation(
pixels,
gray,
width,
height,
H,
qr_size,
version,
&fd,
&rc
);
if (d) {
decoded = d;
fmt_dist = fd;
rs_cost = rc;
memcpy(winning_H, H, sizeof(winning_H));
have_winning_H = 1;
}
}
}
/* Timing-pattern homography refinement. When a 4-point homography (from
alignment pattern or parallelogram BR) can't produce a clean decode,
the usual cause on photos with any perspective or paper curl is
sub-module drift in the QR's interior — the 4 control points anchor
the corners but nothing constrains the transform *between* them.
Observing pixel-space centroids of every known-dark timing module
gives us ≈ 2·(qr_size − 16) extra correspondences threading the
code's interior, and solving an over-determined DLT over the union
of control points + timing observations averages out the distortion
the 4-point fit can't model. Run it only after the canonical paths
have failed (it adds real cost: ~2·qr_size centroid searches + an
8×8 normal-equation solve + another 10-perturbation decode pass). */
if (!decoded && gray && have_primary_H && version >= 2) {
double H_cur[9];
memcpy(H_cur, H_primary, sizeof(H_cur));
/* Iterate refinement a few times — each pass re-predicts timing-
module positions through the currently best H, re-observes
centroids in the pixel buffer, and re-fits. Three rounds is
usually enough to converge; diminishing returns beyond that. */
for (int iter = 0; iter < 3 && !decoded; iter++) {
double H_ref[9];
if (!refine_h_with_timing(
gray,
width,
height,
H_cur,
qr_size,
avg_m,
tl,
tr,
bl,
have_alignment,
alignment_x,
alignment_y,
H_ref
)) {
break;
}
memcpy(H_cur, H_ref, sizeof(H_cur));
int fd = 16;
int rc = 100000;
char *d = decode_with_perturbation(
pixels,
gray,
width,
height,
H_ref,
qr_size,
version,
&fd,
&rc
);
if (d) {
decoded = d;
fmt_dist = fd;
rs_cost = rc;
memcpy(winning_H, H_ref, sizeof(winning_H));
have_winning_H = 1;
}
}
/* Last-resort attempt: quadratic (polynomial) warp sampling. When
every homography we can build still fails to decode, the surface
itself isn't planar — typical case is a QR printed on a curled
receipt. Fit a bivariate degree-2 polynomial to 3 finders +
alignment + timing observations and sample the grid with it. The
quadratic warp can follow a cylindrical bend that no single
homography can model. Only used when the refined H's decode
failed, so the added cost is bounded to the hardest images. */
if (!decoded) {
int fd = 16;
int rc = 100000;
char *d = try_decode_quadratic(
pixels,
gray,
width,
height,
qr_size,
version,
tl,
tr,
bl,
have_alignment,
alignment_x,
alignment_y,
H_cur,
avg_m,
&fd,
&rc
);
if (d) {
decoded = d;
fmt_dist = fd;
rs_cost = rc;
/* Quadratic warp has no homography; H_cur from the last timing
refinement is the closest planar approximation. Good enough to
seed alignment-pattern search — find_all_alignment_patterns
snaps to the pixel-space pattern anyway. */
memcpy(winning_H, H_cur, sizeof(winning_H));
have_winning_H = 1;
}
}
}
if (!decoded) {
return NULL;
}
if (out_fmt_dist) {
*out_fmt_dist = fmt_dist;
}
if (out_rs_cost) {
*out_rs_cost = rs_cost;
}
{
/* Compute QR code outer corners from finder pattern centers.
Finder centers sit at module (3.5, 3.5) from their corners. */
if (out_corners) {
float ux = (tr.x - tl.x) / (qr_size - 7.0f);
float uy = (tr.y - tl.y) / (qr_size - 7.0f);
float vx = (bl.x - tl.x) / (qr_size - 7.0f);
float vy = (bl.y - tl.y) / (qr_size - 7.0f);
out_corners->tl_x = tl.x - 3.5f * (ux + vx);
out_corners->tl_y = tl.y - 3.5f * (uy + vy);
out_corners->tr_x = tr.x + 3.5f * (ux - vx);
out_corners->tr_y = tr.y + 3.5f * (uy - vy);
float br_center_x = tr.x + bl.x - tl.x;
float br_center_y = tr.y + bl.y - tl.y;
out_corners->br_x = br_center_x + 3.5f * (ux + vx);
out_corners->br_y = br_center_y + 3.5f * (uy + vy);
out_corners->bl_x = bl.x - 3.5f * (ux - vx);
out_corners->bl_y = bl.y - 3.5f * (uy - vy);
}
if (out_finders) {
out_finders[0].x = tl.x;
out_finders[0].y = tl.y;
out_finders[1].x = tr.x;
out_finders[1].y = tr.y;
out_finders[2].x = bl.x;
out_finders[2].y = bl.y;
}
if (out_module_size) {
float ux = (tr.x - tl.x) / (qr_size - 7.0f);
float uy = (tr.y - tl.y) / (qr_size - 7.0f);
float vx = (bl.x - tl.x) / (qr_size - 7.0f);
float vy = (bl.y - tl.y) / (qr_size - 7.0f);
float mu = sqrtf(ux * ux + uy * uy);
float mv = sqrtf(vx * vx + vy * vy);
*out_module_size = (mu + mv) / 2.0f;
}
if (out_version) {
*out_version = version;
}
if (out_qr_size) {
*out_qr_size = qr_size;
}
if (out_alignment_count) {
*out_alignment_count = 0;
}
/* Enumerate every alignment pattern the decoder could locate in the
binarized buffer so callers (e.g. qr_draw) can visualise them.
v1 has none; v2–6 have 1 (BR); v7+ have a spec-defined grid. Uses
the outer-corners homography as the prediction frame — find_all_*
snaps to the actual dark 5x5 pattern in pixel space, so prediction
accuracy only has to put us within search_r of the true center. */
if (out_alignments && out_alignment_count && version >= 2 && out_corners) {
double src[4][2] = {
{0.0, 0.0},
{(double)qr_size, 0.0},
{(double)qr_size, (double)qr_size},
{0.0, (double)qr_size}
};
double dst[4][2] = {
{out_corners->tl_x, out_corners->tl_y},
{out_corners->tr_x, out_corners->tr_y},
{out_corners->br_x, out_corners->br_y},
{out_corners->bl_x, out_corners->bl_y}
};
double H[9];
if (have_winning_H) {
memcpy(H, winning_H, sizeof(H));
}
if (have_winning_H || compute_homography(src, dst, H)) {
/* Enumerate every spec-defined alignment-pattern grid position
for this version, projecting each module-space centre through
H to predict its pixel location. Then refine each prediction
against the binarized buffer if find_alignment_pattern can
snap to a real 5x5 template — otherwise keep the prediction
(useful for qr_draw even when the binarization degraded the
pattern enough that the snap fails). */
int nvals = QR_ALIGN_GRID[version][0];
int first = (nvals >= 1) ? QR_ALIGN_GRID[version][1] : 0;
int last = (nvals >= 1) ? QR_ALIGN_GRID[version][nvals] : 0;
int n_out = 0;
for (int ri = 0; ri < nvals && n_out < QR_MAX_ALIGNMENTS; ri++) {
int row = QR_ALIGN_GRID[version][1 + ri];
for (int ci = 0; ci < nvals && n_out < QR_MAX_ALIGNMENTS; ci++) {
int col = QR_ALIGN_GRID[version][1 + ci];
if ((row == first && col == first) ||
(row == first && col == last) ||
(row == last && col == first)) {
continue;
}
double mx = (double)col + 0.5;
double my = (double)row + 0.5;
double pred_x, pred_y;
if (!project_homography(H, mx, my, &pred_x, &pred_y)) {
continue;
}
float snap_x = (float)pred_x, snap_y = (float)pred_y;
/* 5-module search radius is the same tolerance used elsewhere
in the decoder for paper-curled captures where the
module-size estimate can drift up to ±30% locally. */
find_alignment_pattern(
pixels,
width,
height,
(float)pred_x,
(float)pred_y,
(float)(*out_module_size),
(float)(*out_module_size) * 5.0f,
&snap_x,
&snap_y
);
out_alignments[n_out].x = snap_x;
out_alignments[n_out].y = snap_y;
n_out++;
}
}
*out_alignment_count = n_out;
}
}
}
return decoded;
}
/* ---- Main API ---- */
/* Run the finder + decode pipeline on a single binary buffer.
best_* are initialized by the caller and refined across multiple attempts
(e.g. one attempt per binarization strategy). gray is passed for
subpixel finder-center refinement. */
static void try_pipeline(
uint8_t const *bin,
uint8_t const *gray,
int w,
int h,
char **best_decoded,
int *best_fmt_dist,
int *best_rs_cost,
Corners *best_corners,
Point2D best_finders[3],
double *best_module_size,
int *best_version,
int *best_qr_size,
Point2D best_alignments[QR_MAX_ALIGNMENTS],
int *best_alignment_count
) {
FinderPattern fps[MAX_CANDIDATES];
int nfp = find_finders(bin, w, h, fps, MAX_CANDIDATES);
if (nfp < 3) {
return;
}
for (int i = 0; i < nfp; i++) {
refine_finder_center(gray, w, h, &fps[i]);
}
FinderPattern triples[MAX_TRIPLES][3];
int n_triples = select_and_rank(bin, w, h, fps, nfp, triples, MAX_TRIPLES);
for (int t = 0; t < n_triples; t++) {
int fmt_dist = 16;
int rs_cost = 100000;
Corners corners = {0};
Point2D finders[3];
double module_size = 0;
int version = 0;
int qr_size = 0;
Point2D alignments[QR_MAX_ALIGNMENTS];
int alignment_count = 0;
char *decoded = try_decode_triple(
bin,
gray,
w,
h,
triples[t][0],
triples[t][1],
triples[t][2],
&fmt_dist,
&rs_cost,
&corners,
finders,
&module_size,
&version,
&qr_size,
alignments,
&alignment_count
);
if (decoded) {
/* Composite score: lower fmt_dist wins, tie-break first on decoded
length (longer decodes mean a larger byte-mode count field
survived), then on Reed-Solomon cost (lower = the data block
decoded with fewer error corrections, which discriminates between
two "Mz4Y9X" candidates where one has clean RS and the other is
RS-uncorrectable but happens to share the same fmt_dist). */
int dlen = (int)strlen(decoded);
int replace = 0;
if (fmt_dist < *best_fmt_dist) {
replace = 1;
}
else if (fmt_dist == *best_fmt_dist && *best_decoded != NULL) {
int prev_len = (int)strlen(*best_decoded);
if (dlen > prev_len) {
replace = 1;
}
else if (dlen == prev_len && rs_cost < *best_rs_cost) {
replace = 1;
}
}
if (replace) {
free(*best_decoded);
*best_decoded = decoded;
*best_fmt_dist = fmt_dist;
*best_rs_cost = rs_cost;
*best_corners = corners;
for (int i = 0; i < 3; i++) {
best_finders[i] = finders[i];
}
*best_module_size = module_size;
*best_version = version;
*best_qr_size = qr_size;
*best_alignment_count = alignment_count;
for (int i = 0; i < alignment_count; i++) {
best_alignments[i] = alignments[i];
}
}
else {
free(decoded);
}
}
}
}
/* Runs the full binarizer/preprocessor cascade against `gray_pixels`. The
sequence is cheapest-first; each attempt short-circuits once a
high-confidence decode (fmt_dist <= 1, length >= 5) is in hand, so the
work only expands as far as the image demands. Called by the public
decoder at each pyramid level and once again on the inverted buffer —
which lets light-on-dark QRs (level_13) ride the same Sauvola/upsample/
median passes the normal-polarity pass has. `is_finest_level` gates the
native-resolution-only passes (2x/3x upsample), which exist to rescue
tiny QRs at native scale; upsampling from a downsampled pyramid level
is strictly worse than running those passes at level 0. */
static void run_all_attempts(
uint8_t const *gray_pixels,
int w,
int h,
int is_finest_level,
char **best_decoded,
int *best_fmt_dist,
int *best_rs_cost,
Corners *best_corners,
Point2D best_finders[3],
double *best_module_size,
int *best_version,
int *best_qr_size,
Point2D best_alignments[QR_MAX_ALIGNMENTS],
int *best_alignment_count
) {
/* Attempt 1: Otsu on original. */
uint8_t *bin = binarize_global_otsu(gray_pixels, w, h);
if (bin) {
try_pipeline(
bin,
gray_pixels,
w,
h,
best_decoded,
best_fmt_dist,
best_rs_cost,
best_corners,
best_finders,
best_module_size,
best_version,
best_qr_size,
best_alignments,
best_alignment_count
);
free(bin);
}
/* Attempt 2: zxing-cpp hybrid on original. Sparse-threshold per 8x8 block
plus 5x5 neighbour smoothing — low-contrast interior blocks inherit a
threshold from contrasty edge blocks instead of falling back to a
whole-image Otsu. Primary recovery path for faded / pastel palettes. */
if (!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5)) {
bin = binarize_hybrid(gray_pixels, w, h);
if (bin) {
try_pipeline(
bin,
gray_pixels,
w,
h,
best_decoded,
best_fmt_dist,
best_rs_cost,
best_corners,
best_finders,
best_module_size,
best_version,
best_qr_size,
best_alignments,
best_alignment_count
);
free(bin);
}
}
/* Attempt 3: adaptive on original. */
if (!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5)) {
bin = binarize_adaptive(gray_pixels, w, h);
if (bin) {
try_pipeline(
bin,
gray_pixels,
w,
h,
best_decoded,
best_fmt_dist,
best_rs_cost,
best_corners,
best_finders,
best_module_size,
best_version,
best_qr_size,
best_alignments,
best_alignment_count
);
free(bin);
}
}
/* The remaining attempts (Sauvola, unsharp, upsample) target large,
visually-degraded photographs. On tiny synthetic buffers they are more
likely to hallucinate a decode from noise than to recover a real QR
— anything smaller than ~100 px on the short side cannot plausibly
hold a decodable v1 QR (≥ ~5 px/module × 21 modules + quiet zone). */
int small_image = (w < 100 || h < 100);
/* Attempt 3: Sauvola on original. Targets faded / low-contrast modules
where adaptive's fixed min_std fallback misclassifies. */
if (!small_image &&
!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5)) {
bin = binarize_sauvola(gray_pixels, w, h);
if (bin) {
try_pipeline(
bin,
gray_pixels,
w,
h,
best_decoded,
best_fmt_dist,
best_rs_cost,
best_corners,
best_finders,
best_module_size,
best_version,
best_qr_size,
best_alignments,
best_alignment_count
);
free(bin);
}
}
/* Attempt 4: unsharp mask + Otsu, then unsharp mask + adaptive. Targets
blurred QRs (level_4/5 generators add Gaussian blur radius up to 1.8). */
if (!small_image &&
!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5)) {
uint8_t *sharp = unsharp_mask_gray(gray_pixels, w, h);
if (sharp) {
bin = binarize_global_otsu(sharp, w, h);
if (bin) {
try_pipeline(
bin,
sharp,
w,
h,
best_decoded,
best_fmt_dist,
best_rs_cost,
best_corners,
best_finders,
best_module_size,
best_version,
best_qr_size,
best_alignments,
best_alignment_count
);
free(bin);
}
if (!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5
)) {
bin = binarize_adaptive(sharp, w, h);
if (bin) {
try_pipeline(
bin,
sharp,
w,
h,
best_decoded,
best_fmt_dist,
best_rs_cost,
best_corners,
best_finders,
best_module_size,
best_version,
best_qr_size,
best_alignments,
best_alignment_count
);
free(bin);
}
}
free(sharp);
}
}
/* Attempt 5: 2x bilinear upsample + Otsu, then upsample + adaptive.
Targets tiny QRs whose modules are too small for finder ratio checks
at native resolution. Coordinates from any winning upsampled decode are
scaled back by 1/n before returning. Attempt 5b (3x) follows when 2x
still doesn't yield a confident decode; helpful for QRs with mod < 3 px
where even doubled modules remain on the edge of the ratio tolerance.
Skipped at coarser pyramid levels: upsampling a downsampled buffer is
strictly worse than running this at level 0 on the original data. */
for (int n_try = 2; n_try <= 3; n_try++) {
if (small_image || !is_finest_level) {
break;
}
/* Skip the upscaled passes when we already have a long, perfect-format
decode at the native scale; otherwise always try, because a tiny QR
(modules < 4 px) cannot reliably decode at native resolution and a
length-5 wrong decode (e.g. "Mz4Y9c" with fd=0 + uncorrectable RS)
must not block 3x exploration that may surface "Mz4Y9a". */
if (*best_decoded && *best_fmt_dist == 0 && strlen(*best_decoded) >= 10) {
break;
}
int wn = w * n_try;
int hn = h * n_try;
uint8_t *grayn = upsample_nx_bilinear(gray_pixels, w, h, n_try);
if (!grayn) {
continue;
}
char *best_decoded_up = NULL;
Corners best_corners_up = {0};
Point2D best_finders_up[3] = {{0, 0}, {0, 0}, {0, 0}};
double best_module_size_up = 0;
int best_fmt_dist_up = 16;
int best_rs_cost_up = 100000;
int best_version_up = 0;
int best_qr_size_up = 0;
Point2D best_alignments_up[QR_MAX_ALIGNMENTS];
int best_alignment_count_up = 0;
bin = binarize_global_otsu(grayn, wn, hn);
if (bin) {
try_pipeline(
bin,
grayn,
wn,
hn,
&best_decoded_up,
&best_fmt_dist_up,
&best_rs_cost_up,
&best_corners_up,
best_finders_up,
&best_module_size_up,
&best_version_up,
&best_qr_size_up,
best_alignments_up,
&best_alignment_count_up
);
free(bin);
}
if (!(best_decoded_up && best_fmt_dist_up <= 1)) {
bin = binarize_adaptive(grayn, wn, hn);
if (bin) {
try_pipeline(
bin,
grayn,
wn,
hn,
&best_decoded_up,
&best_fmt_dist_up,
&best_rs_cost_up,
&best_corners_up,
best_finders_up,
&best_module_size_up,
&best_version_up,
&best_qr_size_up,
best_alignments_up,
&best_alignment_count_up
);
free(bin);
}
}
free(grayn);
/* Promote the upsampled result if it improves on what we already have:
lower fmt_dist wins, tie-break on longer decode length, then on
lower RS cost. Mirrors the scoring inside try_pipeline. Scale
spatial outputs back to native coordinates by 1/n. */
if (best_decoded_up) {
int replace = 0;
int prev_len = (*best_decoded) ? (int)strlen(*best_decoded) : -1;
int new_len = (int)strlen(best_decoded_up);
if (!*best_decoded) {
replace = 1;
}
else if (best_fmt_dist_up < *best_fmt_dist) {
replace = 1;
}
else if (best_fmt_dist_up == *best_fmt_dist && new_len > prev_len) {
replace = 1;
}
else if (best_fmt_dist_up == *best_fmt_dist && new_len == prev_len &&
best_rs_cost_up < *best_rs_cost) {
replace = 1;
}
if (replace) {
double inv = 1.0 / (double)n_try;
free(*best_decoded);
*best_decoded = best_decoded_up;
*best_fmt_dist = best_fmt_dist_up;
*best_rs_cost = best_rs_cost_up;
best_corners->tl_x = best_corners_up.tl_x * inv;
best_corners->tl_y = best_corners_up.tl_y * inv;
best_corners->tr_x = best_corners_up.tr_x * inv;
best_corners->tr_y = best_corners_up.tr_y * inv;
best_corners->br_x = best_corners_up.br_x * inv;
best_corners->br_y = best_corners_up.br_y * inv;
best_corners->bl_x = best_corners_up.bl_x * inv;
best_corners->bl_y = best_corners_up.bl_y * inv;
for (int i = 0; i < 3; i++) {
best_finders[i].x = best_finders_up[i].x * inv;
best_finders[i].y = best_finders_up[i].y * inv;
}
*best_module_size = best_module_size_up * inv;
*best_version = best_version_up;
*best_qr_size = best_qr_size_up;
*best_alignment_count = best_alignment_count_up;
for (int i = 0; i < best_alignment_count_up; i++) {
best_alignments[i].x = best_alignments_up[i].x * inv;
best_alignments[i].y = best_alignments_up[i].y * inv;
}
}
else {
free(best_decoded_up);
}
}
}
/* Attempt 6: 3x3 median filter + Otsu, then median + adaptive. Median
denoising preserves edges while removing salt-and-pepper noise that
defeats both finder-ratio checks and module-center voting. Especially
helpful for level_5 images (sigma 4-9 noise + low contrast). Runs
when nothing decoded OR the existing decode is suspiciously short
(a count-field truncation of byte mode); a long decode from earlier
is trusted and we don't risk displacing it. */
int existing_short = *best_decoded && strlen(*best_decoded) < 8;
if (!small_image && (!*best_decoded || existing_short)) {
uint8_t *med = median_filter_3x3(gray_pixels, w, h);
if (med) {
bin = binarize_global_otsu(med, w, h);
if (bin) {
try_pipeline(
bin,
med,
w,
h,
best_decoded,
best_fmt_dist,
best_rs_cost,
best_corners,
best_finders,
best_module_size,
best_version,
best_qr_size,
best_alignments,
best_alignment_count
);
free(bin);
}
if (!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5
)) {
bin = binarize_adaptive(med, w, h);
if (bin) {
try_pipeline(
bin,
med,
w,
h,
best_decoded,
best_fmt_dist,
best_rs_cost,
best_corners,
best_finders,
best_module_size,
best_version,
best_qr_size,
best_alignments,
best_alignment_count
);
free(bin);
}
}
free(med);
}
}
}
/* ---- Pyramid-level decode helpers ---- */
/* Scales all spatial fields of a decode state by `scale`. Used to convert
coordinates from a downsampled pyramid level back into original-image
pixel space. */
static void scale_decode_state(
Corners *corners,
Point2D finders[3],
double *module_size,
Point2D alignments[QR_MAX_ALIGNMENTS],
int alignment_count,
double scale
) {
corners->tl_x *= scale;
corners->tl_y *= scale;
corners->tr_x *= scale;
corners->tr_y *= scale;
corners->br_x *= scale;
corners->br_y *= scale;
corners->bl_x *= scale;
corners->bl_y *= scale;
for (int i = 0; i < 3; i++) {
finders[i].x *= scale;
finders[i].y *= scale;
}
for (int i = 0; i < alignment_count; i++) {
alignments[i].x *= scale;
alignments[i].y *= scale;
}
*module_size *= scale;
}
/* Returns 1 if (cand_*) should replace (cur_*). Ranking is: lower fmt_dist
first, then longer decoded string, then lower RS cost. Matches the
promotion logic previously inlined in the upsample / downsample blocks. */
static int decode_is_better(
char const *cur,
int cur_fmt,
int cur_rs,
char const *cand,
int cand_fmt,
int cand_rs
) {
if (!cand) {
return 0;
}
if (!cur) {
return 1;
}
if (cand_fmt != cur_fmt) {
return cand_fmt < cur_fmt;
}
int cur_len = (int)strlen(cur);
int cand_len = (int)strlen(cand);
if (cand_len != cur_len) {
return cand_len > cur_len;
}
return cand_rs < cur_rs;
}
FCVQRCodeResult fcv_decode_qr_codes(
uint32_t width,
uint32_t height,
uint8_t const *const gray_pixels
) {
FCVQRCodeResult result;
result.codes = NULL;
result.count = 0;
if (!gray_pixels || width == 0 || height == 0) {
return result;
}
if (width > INT32_MAX || height > INT32_MAX) {
return result;
}
result.codes = malloc(MAX_QR_CODES * sizeof(FCVQRCode));
if (!result.codes) {
return result;
}
int w = (int)width;
int h = (int)height;
/* ---- Build a box-averaged pyramid ----
Level 0 is the original; level k+1 is a 2x2 box-average of level k.
We stop halving once the short side would drop below MIN_SHORT_SIDE,
below which even a version-1 QR (21 modules) can't plausibly survive
binarization (<6 px/module after quiet zone). */
#define QR_PYRAMID_MAX 6
#define QR_PYRAMID_MIN_SHORT 120
#define QR_PYRAMID_TARGET_SHORT 640
uint8_t const *lv_px[QR_PYRAMID_MAX];
int lv_w[QR_PYRAMID_MAX], lv_h[QR_PYRAMID_MAX];
int lv_owned[QR_PYRAMID_MAX];
lv_px[0] = gray_pixels;
lv_w[0] = w;
lv_h[0] = h;
lv_owned[0] = 0;
int n_levels = 1;
while (n_levels < QR_PYRAMID_MAX) {
int nw = lv_w[n_levels - 1] / 2;
int nh = lv_h[n_levels - 1] / 2;
int short_side = nw < nh ? nw : nh;
if (short_side < QR_PYRAMID_MIN_SHORT) {
break;
}
uint8_t *down = downsample_2x_box(
lv_px[n_levels - 1],
lv_w[n_levels - 1],
lv_h[n_levels - 1]
);
if (!down) {
break;
}
lv_px[n_levels] = down;
lv_w[n_levels] = nw;
lv_h[n_levels] = nh;
lv_owned[n_levels] = 1;
n_levels++;
}
/* Target level: the coarsest (highest-index) level whose short side is
still >= TARGET. Decouples first-pass work from sensor resolution:
the common case of "one QR filling a large fraction of a phone photo"
gets scanned at ~640 px regardless of whether the source is 1 MP or
48 MP. If no level meets the target (small input), target stays 0. */
int target = 0;
for (int i = 1; i < n_levels; i++) {
int short_side = lv_w[i] < lv_h[i] ? lv_w[i] : lv_h[i];
if (short_side >= QR_PYRAMID_TARGET_SHORT) {
target = i;
}
else {
break;
}
}
char *best_decoded = NULL;
Corners best_corners = {0};
Point2D best_finders[3] = {{0, 0}, {0, 0}, {0, 0}};
double best_module_size = 0;
int best_fmt_dist = 16;
int best_rs_cost = 100000;
int best_version = 0;
int best_qr_size = 0;
Point2D best_alignments[QR_MAX_ALIGNMENTS];
int best_alignment_count = 0;
/* Visit order: target first (best balance of detail vs. noise for the
typical "one QR filling most of a phone photo" case), then coarser
levels (cheap, different binarizer response — catches level_13's
low-contrast pastel-on-checkered case the old inline 2x-downsample
attempt used to handle), then finer levels (expensive, last resort
for tiny QRs). Stop as soon as a confident decode (fmt_dist <= 1,
length >= 5) is in hand. Non-confident decodes are kept as best-so-
far and may be displaced by a cleaner result from another level. */
int visit_order[QR_PYRAMID_MAX];
int n_visits = 0;
visit_order[n_visits++] = target;
for (int lvl = target + 1; lvl < n_levels; lvl++) {
visit_order[n_visits++] = lvl;
}
for (int lvl = target - 1; lvl >= 0; lvl--) {
visit_order[n_visits++] = lvl;
}
for (int vi = 0; vi < n_visits; vi++) {
int lvl = visit_order[vi];
double scale = (double)(1 << lvl);
int is_finest = (lvl == 0);
int lw = lv_w[lvl];
int lh = lv_h[lvl];
uint8_t const *lpx = lv_px[lvl];
char *decoded = NULL;
Corners corners = {0};
Point2D finders[3] = {{0, 0}, {0, 0}, {0, 0}};
double module_size = 0;
int fmt_dist = 16;
int rs_cost = 100000;
int version = 0;
int qr_size = 0;
Point2D alignments[QR_MAX_ALIGNMENTS];
int alignment_count = 0;
run_all_attempts(
lpx,
lw,
lh,
is_finest,
&decoded,
&fmt_dist,
&rs_cost,
&corners,
finders,
&module_size,
&version,
&qr_size,
alignments,
&alignment_count
);
/* Inverted-polarity retry at this level — handles light-on-dark QRs.
Only fires if nothing at this level has decoded confidently.
Confidence = fmt_dist <= 1 (format info BCH can correct 1-bit errors
in its 15-bit code, so a 0-or-1-bit fmt_dist reliably indicates a
real QR). Length is not a trust signal: legitimate v1 QRs routinely
encode 2-4 characters. */
if (!(decoded && fmt_dist <= 1)) {
uint8_t *inv = invert_gray(lpx, lw, lh);
if (inv) {
run_all_attempts(
inv,
lw,
lh,
is_finest,
&decoded,
&fmt_dist,
&rs_cost,
&corners,
finders,
&module_size,
&version,
&qr_size,
alignments,
&alignment_count
);
free(inv);
}
}
if (decoded) {
scale_decode_state(
&corners,
finders,
&module_size,
alignments,
alignment_count,
scale
);
if (decode_is_better(
best_decoded,
best_fmt_dist,
best_rs_cost,
decoded,
fmt_dist,
rs_cost
)) {
free(best_decoded);
best_decoded = decoded;
best_fmt_dist = fmt_dist;
best_rs_cost = rs_cost;
best_corners = corners;
for (int i = 0; i < 3; i++) {
best_finders[i] = finders[i];
}
best_module_size = module_size;
best_version = version;
best_qr_size = qr_size;
best_alignment_count = alignment_count;
for (int i = 0; i < alignment_count; i++) {
best_alignments[i] = alignments[i];
}
}
else {
free(decoded);
}
}
if (best_decoded && best_fmt_dist <= 1) {
break;
}
}
for (int i = 1; i < n_levels; i++) {
if (lv_owned[i]) {
free((uint8_t *)lv_px[i]);
}
}
#undef QR_PYRAMID_MAX
#undef QR_PYRAMID_MIN_SHORT
#undef QR_PYRAMID_TARGET_SHORT
if (best_decoded) {
FCVQRCode *qr = &result.codes[result.count++];
qr->text = best_decoded;
qr->corners = best_corners;
for (int i = 0; i < 3; i++) {
qr->finders[i] = best_finders[i];
}
qr->module_size = best_module_size;
qr->version = best_version;
qr->qr_size = best_qr_size;
qr->alignment_count = (size_t)best_alignment_count;
if (best_alignment_count > 0) {
qr->alignments = malloc((size_t)best_alignment_count * sizeof(Point2D));
if (qr->alignments) {
for (int i = 0; i < best_alignment_count; i++) {
qr->alignments[i] = best_alignments[i];
}
}
else {
qr->alignment_count = 0;
}
}
else {
qr->alignments = NULL;
}
}
return result;
}
void fcv_free_qr_result(FCVQRCodeResult result) {
if (result.codes) {
for (size_t i = 0; i < result.count; i++) {
free(result.codes[i].text);
free(result.codes[i].alignments);
}
free(result.codes);
}
}
// File: src/rgba_to_grayscale.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "rgba_to_grayscale.h"
#else
#include "flatcv.h"
#endif
/**
* Convert raw RGBA row-major top-to-bottom image data
* to a single channel grayscale image data.
*
* @param width Width of the image.
* @param height Height of the image.
* @param data Pointer to the pixel data.
* @return Pointer to the single channel grayscale image data.
*/
uint8_t *fcv_rgba_to_grayscale(
uint32_t width,
uint32_t height,
uint8_t const *const data
) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height
if (width > SIZE_MAX / height) {
return NULL;
}
size_t img_length_px = (size_t)width * height;
uint8_t *grayscale_data = malloc(img_length_px);
if (!grayscale_data) { // Memory allocation failed
return NULL;
}
// Process each pixel row by row
for (size_t i = 0; i < img_length_px; i++) {
size_t rgba_index = i * 4;
uint8_t r = data[rgba_index];
uint8_t g = data[rgba_index + 1];
uint8_t b = data[rgba_index + 2];
uint8_t gray = (r * R_WEIGHT + g * G_WEIGHT + b * B_WEIGHT) >> 8;
grayscale_data[i] = gray;
}
return grayscale_data;
}
// File: src/rotate.c
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifndef FLATCV_AMALGAMATION
#include "rotate.h"
#else
#include "flatcv.h"
#endif
/**
* Rotate an image 90 degrees clockwise.
*/
uint8_t *
fcv_rotate_90_cw(uint32_t width, uint32_t height, uint8_t const *const data) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t num_pixels = (size_t)width * height;
if (num_pixels > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = num_pixels * 4;
uint8_t *rotated_data = malloc(img_length_byte);
if (!rotated_data) {
return NULL;
}
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
size_t src_index = ((size_t)y * width + x) * 4;
size_t dst_index = ((size_t)x * height + (height - 1 - y)) * 4;
rotated_data[dst_index] = data[src_index];
rotated_data[dst_index + 1] = data[src_index + 1];
rotated_data[dst_index + 2] = data[src_index + 2];
rotated_data[dst_index + 3] = data[src_index + 3];
}
}
return rotated_data;
}
/**
* Rotate an image 180 degrees.
*/
uint8_t *
fcv_rotate_180(uint32_t width, uint32_t height, uint8_t const *const data) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t num_pixels = (size_t)width * height;
if (num_pixels > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = num_pixels * 4;
uint8_t *rotated_data = malloc(img_length_byte);
if (!rotated_data) {
return NULL;
}
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
size_t src_index = ((size_t)y * width + x) * 4;
size_t dst_index =
((size_t)(height - 1 - y) * width + (width - 1 - x)) * 4;
rotated_data[dst_index] = data[src_index];
rotated_data[dst_index + 1] = data[src_index + 1];
rotated_data[dst_index + 2] = data[src_index + 2];
rotated_data[dst_index + 3] = data[src_index + 3];
}
}
return rotated_data;
}
/**
* Rotate an image 270 degrees clockwise (90 degrees counter-clockwise).
*/
uint8_t *
fcv_rotate_270_cw(uint32_t width, uint32_t height, uint8_t const *const data) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t num_pixels = (size_t)width * height;
if (num_pixels > SIZE_MAX / 4) {
return NULL;
}
size_t img_length_byte = num_pixels * 4;
uint8_t *rotated_data = malloc(img_length_byte);
if (!rotated_data) {
return NULL;
}
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
size_t src_index = ((size_t)y * width + x) * 4;
size_t dst_index = ((size_t)(width - 1 - x) * height + y) * 4;
rotated_data[dst_index] = data[src_index];
rotated_data[dst_index + 1] = data[src_index + 1];
rotated_data[dst_index + 2] = data[src_index + 2];
rotated_data[dst_index + 3] = data[src_index + 3];
}
}
return rotated_data;
}
// File: src/single_to_multichannel.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "single_to_multichannel.h"
#else
#include "flatcv.h"
#endif
/**
* Convert single channel grayscale image data to
* RGBA row-major top-to-bottom image data.
*
* @param width Width of the image.
* @param height Height of the image.
* @param data Pointer to the pixel data.
*/
uint8_t *fcv_single_to_multichannel(
uint32_t width,
uint32_t height,
uint8_t const *const data
) {
if (!data || width == 0 || height == 0) {
return NULL;
}
// Check for overflow: width * height * 4
if (width > SIZE_MAX / height) {
return NULL;
}
size_t img_length_px = (size_t)width * height;
if (img_length_px > SIZE_MAX / 4) {
return NULL;
}
uint8_t *multichannel_data = malloc(img_length_px * 4);
if (!multichannel_data) { // Memory allocation failed
return NULL;
}
for (size_t i = 0; i < img_length_px; i++) {
size_t rgba_index = i * 4;
multichannel_data[rgba_index] = data[i];
multichannel_data[rgba_index + 1] = data[i];
multichannel_data[rgba_index + 2] = data[i];
multichannel_data[rgba_index + 3] = 255;
}
return multichannel_data;
}
// File: src/sobel_edge_detection.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "conversion.h"
#include "perspectivetransform.h"
#include "rgba_to_grayscale.h"
#include "sobel_edge_detection.h"
#else
#include "flatcv.h"
#endif
/**
* Apply Sobel edge detection to the image data and return single-channel
* grayscale data. Uses Sobel kernels to detect edges in horizontal and vertical
* directions, then combines them to get the edge magnitude.
*
* @param width Width of the image.
* @param height Height of the image.
* @param channels Number of channels in the input image.
* (1: Grayscale, 3: RGB, 4: RGBA)
* @param data Pointer to the input pixel data.
* @return Pointer to the single-channel grayscale edge-detected image data.
*/
uint8_t *fcv_sobel_edge_detection(
uint32_t width,
uint32_t height,
uint32_t channels,
uint8_t const *const data
) {
if (!data || width == 0 || height == 0 || channels == 0) {
return NULL;
}
// Check for overflow: width * height
if (width > SIZE_MAX / height) {
return NULL;
}
size_t img_length_px = (size_t)width * height;
// Check for overflow in magnitudes allocation
if (img_length_px > SIZE_MAX / sizeof(double)) {
return NULL;
}
uint8_t *grayscale_data;
bool allocated_grayscale = false;
if (channels == 1) {
// Single-channel input, use data directly
grayscale_data = (uint8_t *)data;
allocated_grayscale = false;
}
else {
// Multi-channel input, convert to grayscale
grayscale_data = fcv_rgba_to_grayscale(width, height, data);
if (!grayscale_data) {
return NULL;
}
allocated_grayscale = true;
}
uint8_t *sobel_data = malloc(img_length_px);
if (!sobel_data) {
if (allocated_grayscale) {
free(grayscale_data);
}
return NULL;
}
// Sobel kernels
int32_t sobel_x[3][3] = {{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}};
int32_t sobel_y[3][3] = {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}};
// Temporary buffer to store magnitudes for normalization
double *magnitudes = malloc(img_length_px * sizeof(double));
if (!magnitudes) {
if (allocated_grayscale) {
free(grayscale_data);
}
free(sobel_data);
return NULL;
}
double min_magnitude = INFINITY;
double max_magnitude = 0.0;
// First pass: calculate all magnitudes and find min/max
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
int32_t gx = 0;
int32_t gy = 0;
// Apply Sobel kernels
for (int32_t ky = -1; ky <= 1; ky++) {
for (int32_t kx = -1; kx <= 1; kx++) {
int32_t px = x + kx;
int32_t py = y + ky;
// Handle boundaries by using nearest pixel values
if (px < 0) {
px = 0;
}
if ((uint32_t)px >= width) {
px = width - 1;
}
if (py < 0) {
py = 0;
}
if ((uint32_t)py >= height) {
py = height - 1;
}
uint8_t pixel = grayscale_data[(size_t)py * width + px];
gx += pixel * sobel_x[ky + 1][kx + 1];
gy += pixel * sobel_y[ky + 1][kx + 1];
}
}
double magnitude = sqrt((double)gx * gx + (double)gy * gy);
magnitudes[(size_t)y * width + x] = magnitude;
if (magnitude < min_magnitude) {
min_magnitude = magnitude;
}
if (magnitude > max_magnitude) {
max_magnitude = magnitude;
}
}
}
// Second pass: normalize and assign to output
double range = max_magnitude - min_magnitude;
if (range == 0.0) {
range = 1.0; // Avoid division by zero
}
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
double magnitude = magnitudes[(size_t)y * width + x];
// Normalize to 0-255 range based on actual min/max
int32_t final_magnitude =
(int32_t)(((magnitude - min_magnitude) / range) * 255.0);
// Clamp to valid range
if (final_magnitude < 0) {
final_magnitude = 0;
}
if (final_magnitude > 255) {
final_magnitude = 255;
}
sobel_data[(size_t)y * width + x] = (uint8_t)final_magnitude;
}
}
free(magnitudes);
if (allocated_grayscale) {
free(grayscale_data);
}
return sobel_data;
}
// File: src/sort_corners.c
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef FLATCV_AMALGAMATION
#include "1_types.h"
#else
#include "flatcv.h"
#endif
typedef struct {
double r; // radius
double theta; // angle
} PolarPoint;
// Helper function to convert Cartesian to polar coordinates
PolarPoint cartesian_to_polar(Point2D cart) {
PolarPoint polar;
polar.r = sqrt(cart.x * cart.x + cart.y * cart.y);
double theta = atan2(cart.y, cart.x);
// Normalize theta: Convert to degrees and apply normalization
double theta_deg = theta * 180.0 / M_PI;
if (theta_deg < 0) {
polar.theta = -theta_deg;
}
else {
polar.theta = -(theta_deg - 360.0);
}
return polar;
}
// Comparison function for qsort (sorting by angle in descending order)
int compare_polar_angles(const void *a, const void *b) {
const PolarPoint *pa = (const PolarPoint *)a;
const PolarPoint *pb = (const PolarPoint *)b;
if (pa->theta < pb->theta) {
return 1; // Descending order
}
if (pa->theta > pb->theta) {
return -1;
}
return 0;
}
// Helper structure to keep track of original indices during sorting
typedef struct {
PolarPoint polar;
uint32_t original_index;
} IndexedPolarPoint;
int compare_indexed_polar_angles(const void *a, const void *b) {
const IndexedPolarPoint *pa = (const IndexedPolarPoint *)a;
const IndexedPolarPoint *pb = (const IndexedPolarPoint *)b;
if (pa->polar.theta < pb->polar.theta) {
return 1; // Descending order
}
if (pa->polar.theta > pb->polar.theta) {
return -1;
}
return 0;
}
Corners sort_corners(
uint32_t width,
uint32_t height,
uint32_t out_width,
uint32_t out_height,
Point2D *corners,
uint32_t num_corners,
Point2D *result
) {
if (num_corners < 3) {
// Not enough corners, return zeros
Corners empty_corners = {0, 0, 0, 0, 0, 0, 0, 0};
return empty_corners;
}
// Allocate memory for working corners (max of num_corners or 4 for 3-corner
// case)
uint32_t max_corners = (num_corners >= 4) ? num_corners : 4;
Point2D *working_corners = malloc(max_corners * sizeof(Point2D));
if (!working_corners) {
Corners empty_corners = {0, 0, 0, 0, 0, 0, 0, 0};
return empty_corners;
}
uint32_t corners_to_process;
if (num_corners == 3) {
// Copy the 3 corners we have
working_corners[0] = corners[0];
working_corners[1] = corners[1];
working_corners[2] = corners[2];
// Calculate the fourth corner to complete a parallelogram
// For 3 corners A, B, C, we need to determine which corner is missing
// and calculate it using vector addition
// First, sort the 3 corners by position to understand the layout
Point2D sorted[3] = {corners[0], corners[1], corners[2]};
// Find bounding box of the 3 corners
double min_x = sorted[0].x, max_x = sorted[0].x;
double min_y = sorted[0].y, max_y = sorted[0].y;
for (int i = 1; i < 3; i++) {
if (sorted[i].x < min_x) {
min_x = sorted[i].x;
}
if (sorted[i].x > max_x) {
max_x = sorted[i].x;
}
if (sorted[i].y < min_y) {
min_y = sorted[i].y;
}
if (sorted[i].y > max_y) {
max_y = sorted[i].y;
}
}
// Determine which corner is missing by finding which position in the
// rectangle is empty
bool has_tl = false, has_tr = false, has_bl = false, has_br = false;
for (int i = 0; i < 3; i++) {
double x = sorted[i].x;
double y = sorted[i].y;
// Use small tolerance for floating point comparison
double tolerance = 1.0;
if (fabs(x - min_x) < tolerance && fabs(y - min_y) < tolerance) {
has_tl = true;
}
else if (fabs(x - max_x) < tolerance && fabs(y - min_y) < tolerance) {
has_tr = true;
}
else if (fabs(x - min_x) < tolerance && fabs(y - max_y) < tolerance) {
has_bl = true;
}
else if (fabs(x - max_x) < tolerance && fabs(y - max_y) < tolerance) {
has_br = true;
}
}
// Calculate the missing corner
Point2D missing_corner;
if (!has_tl) {
missing_corner.x = min_x;
missing_corner.y = min_y;
}
else if (!has_tr) {
missing_corner.x = max_x;
missing_corner.y = min_y;
}
else if (!has_bl) {
missing_corner.x = min_x;
missing_corner.y = max_y;
}
else if (!has_br) {
missing_corner.x = max_x;
missing_corner.y = max_y;
}
else {
// Fallback: use parallelogram completion
// For a parallelogram ABCD where we have A, B, C and need D:
// D = A + C - B (assuming B and D are diagonal)
// Try all combinations and pick the best one
Point2D candidates[3];
candidates[0].x = corners[0].x + corners[1].x - corners[2].x;
candidates[0].y = corners[0].y + corners[1].y - corners[2].y;
candidates[1].x = corners[0].x + corners[2].x - corners[1].x;
candidates[1].y = corners[0].y + corners[2].y - corners[1].y;
candidates[2].x = corners[1].x + corners[2].x - corners[0].x;
candidates[2].y = corners[1].y + corners[2].y - corners[0].y;
// Choose the candidate that gives the most reasonable result
missing_corner = candidates[2]; // Default to the last one
double best_score = -1;
for (int i = 0; i < 3; i++) {
double score = 0;
if (candidates[i].x >= 0) {
score += 1;
}
if (candidates[i].y >= 0) {
score += 1;
}
// Check if it's not a duplicate
bool is_duplicate = false;
for (int j = 0; j < 3; j++) {
if (fabs(candidates[i].x - corners[j].x) < 0.1 &&
fabs(candidates[i].y - corners[j].y) < 0.1) {
is_duplicate = true;
break;
}
}
if (!is_duplicate) {
score += 5;
}
if (score > best_score) {
best_score = score;
missing_corner = candidates[i];
}
}
}
working_corners[3] = missing_corner;
corners_to_process = 4;
}
else {
// Copy all existing corners
for (uint32_t i = 0; i < num_corners; i++) {
working_corners[i] = corners[i];
}
corners_to_process = num_corners;
}
// Clockwise corner sorting starting from top-left corner
// Works with any number of corners >= 3
// Step 1: Find the center point (centroid)
double center_x = 0.0, center_y = 0.0;
for (uint32_t i = 0; i < corners_to_process; i++) {
center_x += working_corners[i].x;
center_y += working_corners[i].y;
}
center_x /= corners_to_process;
center_y /= corners_to_process;
// Step 2: Find top-left in document-relative coordinates. A plain x+y
// score lets the long axis dominate on skewed receipts, choosing bottom-left
// (or top-right) instead. Normalize by the document bounds, not image bounds,
// so the labels also survive the detector's non-uniform resize to 256x256.
double min_x = working_corners[0].x, max_x = min_x;
double min_y = working_corners[0].y, max_y = min_y;
for (uint32_t i = 1; i < corners_to_process; i++) {
min_x = fmin(min_x, working_corners[i].x);
max_x = fmax(max_x, working_corners[i].x);
min_y = fmin(min_y, working_corners[i].y);
max_y = fmax(max_y, working_corners[i].y);
}
double span_x = max_x > min_x ? max_x - min_x : 1.0;
double span_y = max_y > min_y ? max_y - min_y : 1.0;
uint32_t top_left_idx = 0;
double min_sum = (working_corners[0].x - min_x) / span_x +
(working_corners[0].y - min_y) / span_y;
for (uint32_t i = 1; i < corners_to_process; i++) {
double sum = (working_corners[i].x - min_x) / span_x +
(working_corners[i].y - min_y) / span_y;
if (sum < min_sum) {
min_sum = sum;
top_left_idx = i;
}
}
// Step 3: Create array with indices and calculate angles relative to center
typedef struct {
uint32_t index;
double angle;
double distance;
} CornerInfo;
CornerInfo *corner_info = malloc(corners_to_process * sizeof(CornerInfo));
if (!corner_info) {
free(working_corners);
Corners empty_corners = {0, 0, 0, 0, 0, 0, 0, 0};
return empty_corners;
}
for (uint32_t i = 0; i < corners_to_process; i++) {
corner_info[i].index = i;
corner_info[i].distance = sqrt(
(working_corners[i].x - center_x) * (working_corners[i].x - center_x) +
(working_corners[i].y - center_y) * (working_corners[i].y - center_y)
);
// Calculate angle from center to corner
double dx = working_corners[i].x - center_x;
double dy = working_corners[i].y - center_y;
corner_info[i].angle = atan2(dy, dx);
// Normalize angle to [0, 2π) range
if (corner_info[i].angle < 0) {
corner_info[i].angle += 2.0 * M_PI;
}
}
// Step 4: Calculate reference angle from center to top-left corner
double top_left_angle = corner_info[top_left_idx].angle;
// Step 5: Adjust all angles relative to top-left corner and sort clockwise
for (uint32_t i = 0; i < corners_to_process; i++) {
// Adjust angle relative to top-left corner
corner_info[i].angle = corner_info[i].angle - top_left_angle;
// Normalize to [0, 2π) range
while (corner_info[i].angle < 0) {
corner_info[i].angle += 2.0 * M_PI;
}
while (corner_info[i].angle >= 2.0 * M_PI) {
corner_info[i].angle -= 2.0 * M_PI;
}
}
// Step 6: Sort corners by angle (clockwise from top-left)
for (uint32_t i = 0; i < corners_to_process - 1; i++) {
for (uint32_t j = 0; j < corners_to_process - 1 - i; j++) {
bool should_swap = false;
if (corner_info[j].angle > corner_info[j + 1].angle) {
should_swap = true;
}
else if (fabs(corner_info[j].angle - corner_info[j + 1].angle) < 1e-6) {
// If angles are very close, sort by distance from center (closer first)
if (corner_info[j].distance > corner_info[j + 1].distance) {
should_swap = true;
}
}
if (should_swap) {
CornerInfo temp = corner_info[j];
corner_info[j] = corner_info[j + 1];
corner_info[j + 1] = temp;
}
}
}
// Step 7: Copy sorted corners to result
for (uint32_t i = 0; i < corners_to_process; i++) {
result[i] = working_corners[corner_info[i].index];
}
// Scale corners back to original image dimensions
double scale_x = (double)width / out_width;
double scale_y = (double)height / out_height;
// Return corners struct using the sorted result
Corners sorted_corners_result = {
.tl_x = result[0].x * scale_x,
.tl_y = result[0].y * scale_y,
.tr_x = result[1].x * scale_x,
.tr_y = result[1].y * scale_y,
.br_x = result[2].x * scale_x,
.br_y = result[2].y * scale_y,
.bl_x = result[3].x * scale_x,
.bl_y = result[3].y * scale_y
};
// Clean up allocated memory
free(corner_info);
free(working_corners);
return sorted_corners_result;
}
// File: src/trim.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "crop.h"
#include "trim.h"
#else
#include "flatcv.h"
#endif
static bool
pixels_match(const uint8_t *pixel1, const uint8_t *pixel2, uint32_t channels) {
for (uint32_t c = 0; c < channels; c++) {
if (pixel1[c] != pixel2[c]) {
return false;
}
}
return true;
}
/**
* Check if two pixels match within a given tolerance.
*
* @param pixel1 First pixel data.
* @param pixel2 Second pixel data.
* @param channels Number of channels.
* @param tolerance Maximum allowed difference per channel (0-255).
* @return true if all channels are within tolerance, false otherwise.
*/
static bool pixels_match_threshold(
const uint8_t *pixel1,
const uint8_t *pixel2,
uint32_t channels,
uint8_t tolerance
) {
for (uint32_t c = 0; c < channels; c++) {
int32_t diff = (int32_t)pixel1[c] - (int32_t)pixel2[c];
if (diff < 0) {
diff = -diff;
}
if ((uint8_t)diff > tolerance) {
return false;
}
}
return true;
}
/**
* Trim border pixels that have the same color.
*
* @param width Pointer to width of the image (will be updated).
* @param height Pointer to height of the image (will be updated).
* @param channels Number of channels in the image.
* @param data Pointer to the pixel data.
* @return Pointer to the new trimmed image data.
*/
uint8_t *fcv_trim(
int32_t *width,
int32_t *height,
uint32_t channels,
uint8_t const *const data
) {
if (!data || !width || !height || *width <= 0 || *height <= 0 ||
channels == 0) {
return NULL;
}
uint32_t w = (uint32_t)*width;
uint32_t h = (uint32_t)*height;
// Check for potential overflow in pixel index calculations
if (w > SIZE_MAX / h || (size_t)w * h > SIZE_MAX / channels) {
return NULL;
}
uint32_t left = 0;
uint32_t top = 0;
uint32_t right = w;
uint32_t bottom = h;
// Trim from the left
while (left < right) {
const uint8_t *ref_pixel = &data[(top * w + left) * channels];
bool can_trim = true;
// Check if the entire left column has the same color as the reference pixel
for (uint32_t y = top; y < bottom; y++) {
const uint8_t *pixel = &data[(y * w + left) * channels];
if (!pixels_match(pixel, ref_pixel, channels)) {
can_trim = false;
break;
}
}
if (can_trim && right - left > 1) {
left++;
}
else {
break;
}
}
// Trim from the right
while (left < right) {
const uint8_t *ref_pixel = &data[(top * w + (right - 1)) * channels];
bool can_trim = true;
// Check if the entire right column has the same color as the reference
// pixel
for (uint32_t y = top; y < bottom; y++) {
const uint8_t *pixel = &data[(y * w + (right - 1)) * channels];
if (!pixels_match(pixel, ref_pixel, channels)) {
can_trim = false;
break;
}
}
if (can_trim && right - left > 1) {
right--;
}
else {
break;
}
}
// Trim from the top
while (top < bottom && bottom - top > 1) {
const uint8_t *ref_pixel = &data[(top * w + left) * channels];
bool can_trim = true;
// Check if the entire top row has the same color as the reference pixel
for (uint32_t x = left; x < right; x++) {
const uint8_t *pixel = &data[(top * w + x) * channels];
if (!pixels_match(pixel, ref_pixel, channels)) {
can_trim = false;
break;
}
}
if (can_trim) {
top++;
}
else {
break;
}
}
// Trim from the bottom
while (top < bottom && bottom - top > 1) {
const uint8_t *ref_pixel = &data[((bottom - 1) * w + left) * channels];
bool can_trim = true;
// Check if the entire bottom row has the same color as the reference pixel
for (uint32_t x = left; x < right; x++) {
const uint8_t *pixel = &data[((bottom - 1) * w + x) * channels];
if (!pixels_match(pixel, ref_pixel, channels)) {
can_trim = false;
break;
}
}
if (can_trim) {
bottom--;
}
else {
break;
}
}
// Calculate new dimensions
uint32_t new_width = right - left;
uint32_t new_height = bottom - top;
// If no trimming was done, return a copy
if (left == 0 && top == 0 && right == w && bottom == h) {
size_t alloc_size = (size_t)w * h * channels;
uint8_t *result = malloc(alloc_size);
if (result) {
memcpy(result, data, alloc_size);
}
return result;
}
// Use existing crop function to extract the trimmed region
uint8_t *trimmed_data =
fcv_crop(w, h, channels, data, left, top, new_width, new_height);
// Update dimensions
*width = (int32_t)new_width;
*height = (int32_t)new_height;
return trimmed_data;
}
/**
* Trim border pixels that are within a threshold of the reference color.
* This is useful for images with JPEG artifacts or slight vignetting.
*
* @param width Pointer to width of the image (will be updated).
* @param height Pointer to height of the image (will be updated).
* @param channels Number of channels in the image.
* @param data Pointer to the pixel data.
* @param threshold_percent Tolerance percentage (0-100). A value of 2 means
* pixels within 2% (≈5 units) of the reference color will be trimmed.
* @return Pointer to the new trimmed image data.
*/
uint8_t *fcv_trim_threshold(
int32_t *width,
int32_t *height,
uint32_t channels,
uint8_t const *const data,
double threshold_percent
) {
if (!data || !width || !height || *width <= 0 || *height <= 0 ||
channels == 0) {
return NULL;
}
// Validate threshold_percent - reject invalid values
if (!isfinite(threshold_percent)) {
return NULL;
}
// Clamp threshold to valid range and convert to absolute value (0-255)
if (threshold_percent < 0.0) {
threshold_percent = 0.0;
}
if (threshold_percent > 100.0) {
threshold_percent = 100.0;
}
uint8_t tolerance = (uint8_t)(threshold_percent * 255.0 / 100.0);
// If tolerance is 0, use the exact match version
if (tolerance == 0) {
return fcv_trim(width, height, channels, data);
}
uint32_t w = (uint32_t)*width;
uint32_t h = (uint32_t)*height;
// Check for potential overflow in pixel index calculations
if (w > SIZE_MAX / h || (size_t)w * h > SIZE_MAX / channels) {
return NULL;
}
uint32_t left = 0;
uint32_t top = 0;
uint32_t right = w;
uint32_t bottom = h;
// Trim from the left
while (left < right) {
const uint8_t *ref_pixel = &data[(top * w + left) * channels];
bool can_trim = true;
// Check if the entire left column matches within threshold
for (uint32_t y = top; y < bottom; y++) {
const uint8_t *pixel = &data[(y * w + left) * channels];
if (!pixels_match_threshold(pixel, ref_pixel, channels, tolerance)) {
can_trim = false;
break;
}
}
if (can_trim && right - left > 1) {
left++;
}
else {
break;
}
}
// Trim from the right
while (left < right) {
const uint8_t *ref_pixel = &data[(top * w + (right - 1)) * channels];
bool can_trim = true;
// Check if the entire right column matches within threshold
for (uint32_t y = top; y < bottom; y++) {
const uint8_t *pixel = &data[(y * w + (right - 1)) * channels];
if (!pixels_match_threshold(pixel, ref_pixel, channels, tolerance)) {
can_trim = false;
break;
}
}
if (can_trim && right - left > 1) {
right--;
}
else {
break;
}
}
// Trim from the top
while (top < bottom && bottom - top > 1) {
const uint8_t *ref_pixel = &data[(top * w + left) * channels];
bool can_trim = true;
// Check if the entire top row matches within threshold
for (uint32_t x = left; x < right; x++) {
const uint8_t *pixel = &data[(top * w + x) * channels];
if (!pixels_match_threshold(pixel, ref_pixel, channels, tolerance)) {
can_trim = false;
break;
}
}
if (can_trim) {
top++;
}
else {
break;
}
}
// Trim from the bottom
while (top < bottom && bottom - top > 1) {
const uint8_t *ref_pixel = &data[((bottom - 1) * w + left) * channels];
bool can_trim = true;
// Check if the entire bottom row matches within threshold
for (uint32_t x = left; x < right; x++) {
const uint8_t *pixel = &data[((bottom - 1) * w + x) * channels];
if (!pixels_match_threshold(pixel, ref_pixel, channels, tolerance)) {
can_trim = false;
break;
}
}
if (can_trim) {
bottom--;
}
else {
break;
}
}
// Calculate new dimensions
uint32_t new_width = right - left;
uint32_t new_height = bottom - top;
// If no trimming was done, return a copy
if (left == 0 && top == 0 && right == w && bottom == h) {
size_t alloc_size = (size_t)w * h * channels;
uint8_t *result = malloc(alloc_size);
if (result) {
memcpy(result, data, alloc_size);
}
return result;
}
// Use existing crop function to extract the trimmed region
uint8_t *trimmed_data =
fcv_crop(w, h, channels, data, left, top, new_width, new_height);
// Update dimensions
*width = (int32_t)new_width;
*height = (int32_t)new_height;
return trimmed_data;
}
// File: src/watershed_segmentation.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef FLATCV_AMALGAMATION
#include "1_types.h"
#include "watershed_segmentation.h"
#else
#include "flatcv.h"
#endif
typedef struct {
int32_t x, y;
int32_t label;
} QueueItem;
typedef struct {
QueueItem *items;
int32_t front;
int32_t rear;
int32_t size;
int32_t capacity;
} Queue;
static Queue *create_queue(size_t capacity) {
if (capacity == 0 || capacity > SIZE_MAX / sizeof(QueueItem) ||
capacity > INT32_MAX) {
return NULL;
}
Queue *q = malloc(sizeof(Queue));
if (!q) {
return NULL;
}
q->items = malloc(capacity * sizeof(QueueItem));
if (!q->items) {
free(q);
return NULL;
}
q->front = 0;
q->rear = -1;
q->size = 0;
q->capacity = (int32_t)capacity;
return q;
}
static void destroy_queue(Queue *q) {
if (q) {
free(q->items);
free(q);
}
}
static int32_t is_queue_empty(Queue *q) { return q->size == 0; }
static int32_t enqueue(Queue *q, int32_t x, int32_t y, int32_t label) {
if (q->size >= q->capacity) {
return 0;
}
q->rear = (q->rear + 1) % q->capacity;
q->items[q->rear].x = x;
q->items[q->rear].y = y;
q->items[q->rear].label = label;
q->size++;
return 1;
}
static QueueItem dequeue(Queue *q) {
QueueItem item = {-1, -1, -1};
if (q->size == 0) {
return item;
}
item = q->items[q->front];
q->front = (q->front + 1) % q->capacity;
q->size--;
return item;
}
/**
* Watershed segmentation
* using (x, y) coordinate markers with elevation-based flooding
*
* Implements the watershed transform for image segmentation by treating the
* grayscale image as an elevation map. Water floods from the marker points,
* and watershed lines form where different regions would meet. Lower
* intensity values represent valleys where water accumulates, and higher
* values represent hills/ridges.
*
* @param width Width of the image.
* @param height Height of the image.
* @param grayscale_data Pointer to the input grayscale image data.
* @param markers Array of Point2D markers with x,y coordinates.
* @param num_markers Number of markers in the array.
* @return Pointer to the segmented image data.
*/
uint8_t *fcv_watershed_segmentation(
uint32_t width,
uint32_t height,
uint8_t const *const grayscale_data,
Point2D *markers,
uint32_t num_markers,
bool create_boundaries
) {
if (!grayscale_data || !markers || num_markers == 0) {
return NULL;
}
if (width == 0 || height == 0) {
return NULL;
}
// Validate all marker positions and coordinates
for (uint32_t m = 0; m < num_markers; m++) {
// Check for NaN/Inf
if (!isfinite(markers[m].x) || !isfinite(markers[m].y)) {
return NULL;
}
int32_t marker_x = (int32_t)markers[m].x;
int32_t marker_y = (int32_t)markers[m].y;
// Check bounds - if any marker is invalid, fail the entire operation
if (marker_x < 0 || marker_x >= (int32_t)width || marker_y < 0 ||
marker_y >= (int32_t)height) {
return NULL;
}
}
// Check for overflow: width * height
if (width > SIZE_MAX / height) {
return NULL;
}
size_t img_length_px = (size_t)width * height;
// Check for overflow in output_data allocation (pixels * 4)
if (img_length_px > SIZE_MAX / 4) {
return NULL;
}
// Check for overflow in labels allocation
if (img_length_px > SIZE_MAX / sizeof(int32_t)) {
return NULL;
}
// Create output data
uint8_t *output_data = malloc(img_length_px * 4);
if (!output_data) {
return NULL;
}
// Initialize labels array (-1 = unvisited, 0+ = region labels)
int32_t *labels = malloc(img_length_px * sizeof(int32_t));
if (!labels) {
free(output_data);
return NULL;
}
// Initialize all pixels as unvisited
for (uint32_t i = 0; i < img_length_px; i++) {
labels[i] = -1;
}
// Create queue for flood fill
Queue *queue = create_queue(img_length_px);
if (!queue) {
free(output_data);
free(labels);
return NULL;
}
// Initialize markers and add to queue
for (uint32_t m = 0; m < num_markers; m++) {
int32_t marker_x = (int32_t)markers[m].x;
int32_t marker_y = (int32_t)markers[m].y;
int32_t idx = marker_y * width + marker_x;
int32_t label = (int32_t)m; // Label starts from 0
labels[idx] = label;
enqueue(queue, marker_x, marker_y, label);
}
// Level-wise watershed algorithm - process pixels in elevation order
int32_t dx[] = {-1, 1, 0, 0};
int32_t dy[] = {0, 0, -1, 1};
// Process each elevation level from 0 to 255
for (int32_t current_level = 0; current_level <= 255; current_level++) {
// Single-step expansion: each region grows by exactly one pixel layer per
// iteration
bool changed = true;
while (changed) {
changed = false;
// Find all pixels at current level that should be added in this step
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
int32_t idx = y * width + x;
// Skip if already labeled
if (labels[idx] != -1) {
continue;
}
// Allow water to flow downhill:
// Process pixels at or below current level.
// This enables proper watershed behavior
// where water flows from high to low elevation.
bool can_flood = (grayscale_data[idx] <= current_level);
if (!can_flood) {
continue;
}
// Check if this pixel neighbors any labeled pixel
int32_t neighbor_label = -1;
bool multiple_labels = false;
for (int32_t d = 0; d < 4; d++) {
int32_t nx = x + dx[d];
int32_t ny = y + dy[d];
if (nx >= 0 && nx < (int32_t)width && ny >= 0 &&
ny < (int32_t)height) {
int32_t neighbor_idx = ny * width + nx;
if (labels[neighbor_idx] != -1) {
if (neighbor_label == -1) {
neighbor_label = labels[neighbor_idx];
}
else if (neighbor_label != labels[neighbor_idx]) {
multiple_labels = true;
break;
}
}
}
}
// If found exactly one labeled neighbor, mark for expansion
// If multiple labels and boundaries disabled, use first found label
if (neighbor_label != -1 &&
(!multiple_labels || !create_boundaries)) {
enqueue(queue, x, y, neighbor_label);
changed = true;
}
}
}
// Apply all expansions from this step simultaneously
while (!is_queue_empty(queue)) {
QueueItem item = dequeue(queue);
int32_t idx = item.y * width + item.x;
labels[idx] = item.label;
}
}
}
destroy_queue(queue);
// Generate distinct colors for each region
uint8_t colors[10][3] = {
{255, 0, 0}, // red
{0, 255, 0}, // green
{0, 0, 255}, // blue
{255, 255, 0}, // yellow
{255, 0, 255}, // magenta
{0, 255, 255}, // cyan
{255, 128, 0}, // orange
{128, 0, 255}, // purple
{255, 192, 203}, // pink
{128, 128, 128} // gray
};
// Create output image
for (uint32_t i = 0; i < img_length_px; i++) {
uint32_t rgba_idx = i * 4;
int32_t label = labels[i];
if (label == -1) {
// Unassigned pixels -> background (black)
output_data[rgba_idx] = 0; // R
output_data[rgba_idx + 1] = 0; // G
output_data[rgba_idx + 2] = 0; // B
}
else if (label >= 0 && label < 10) {
// Assigned to a region
output_data[rgba_idx] = colors[label][0]; // R
output_data[rgba_idx + 1] = colors[label][1]; // G
output_data[rgba_idx + 2] = colors[label][2]; // B
}
else {
// Fallback for more than 10 regions
output_data[rgba_idx] = 128; // R
output_data[rgba_idx + 1] = 128; // G
output_data[rgba_idx + 2] = 128; // B
}
output_data[rgba_idx + 3] = 255; // A
}
free(labels);
return output_data;
}
/* End of FlatCV amalgamation */