packages feed

flatcv 0.3.0.1 → 0.3.1.0

raw patch · 8 files changed

+9241/−2752 lines, 8 filesdep +textPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: text

API changes (from Hackage documentation)

+ FlatCV: Connectivity4 :: Connectivity
+ FlatCV: Connectivity8 :: Connectivity
+ FlatCV: data Connectivity
+ FlatCV: floodFill :: Word32 -> Word32 -> [Point2D] -> Word8 -> Word8 -> Connectivity -> Vector Word8 -> IO (Vector Word8)
+ FlatCV: instance GHC.Classes.Eq FlatCV.Connectivity
+ FlatCV: instance GHC.Internal.Show.Show FlatCV.Connectivity
- FlatCV: addBorder :: Word32 -> Word32 -> Word32 -> String -> Word32 -> Vector Word8 -> IO (Word32, Word32, Vector Word8)
+ FlatCV: addBorder :: Word32 -> Word32 -> Word32 -> Text -> Word32 -> Vector Word8 -> IO (Word32, Word32, Vector Word8)
- FlatCV: convertToBinary :: Int32 -> Int32 -> String -> String -> Vector Word8 -> IO (Vector Word8)
+ FlatCV: convertToBinary :: Int32 -> Int32 -> Text -> Text -> Vector Word8 -> IO (Vector Word8)
- FlatCV: drawCircle :: Word32 -> Word32 -> Word32 -> String -> Double -> Double -> Double -> Vector Word8 -> IO (Vector Word8)
+ FlatCV: drawCircle :: Word32 -> Word32 -> Word32 -> Text -> Double -> Double -> Double -> Vector Word8 -> IO (Vector Word8)
- FlatCV: drawDisk :: Word32 -> Word32 -> Word32 -> String -> Double -> Double -> Double -> Vector Word8 -> IO (Vector Word8)
+ FlatCV: drawDisk :: Word32 -> Word32 -> Word32 -> Text -> Double -> Double -> Double -> Vector Word8 -> IO (Vector Word8)
- FlatCV: parseHexColor :: String -> IO (Word8, Word8, Word8)
+ FlatCV: parseHexColor :: Text -> IO (Word8, Word8, Word8)
- FlatCV: prettyShowCorners :: Corners -> String
+ FlatCV: prettyShowCorners :: Corners -> Text
- FlatCV: prettyShowMatrix3x3 :: Matrix3x3 -> String
+ FlatCV: prettyShowMatrix3x3 :: Matrix3x3 -> Text

Files

README.md view
@@ -111,6 +111,7 @@ ### Segmentation  - `watershedSegmentation` - Watershed segmentation+- `floodFill` - Flood fill with multiple seeds and 4- or 8-connectivity   ### Utility
changelog.md view
@@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file.  +## 2026-11-07 - 0.3.1.0++- Fix corner ordering for long, skewed documents to prevent quarter-turns+    during extraction; relabel corners after selecting the final four peaks+- Fix build with GCC 15, which defaults to C23,+    whose `bool` keyword can't be parsed by c2hs+- Add `floodFill` binding++ ## 2025-01-15 - 0.1  - Initial release of Haskell bindings for FlatCV
flatcv.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               flatcv-version:            0.3.0.1+version:            0.3.1.0 synopsis:           Haskell bindings for FlatCV image processing library description:     FlatCV is a lightweight, dependency-free image processing library@@ -43,11 +43,16 @@      build-depends:         base >= 4.12 && < 5,+        text >= 1.2 && < 2.2,         vector >= 0.12 && < 0.14      include-dirs:       .     c-sources:          flatcv_impl.c +    -- c2hs cannot parse C23's bool keyword (the default since GCC 15).+    -- Spell it as the equivalent C99 type using a portable preprocessor flag.+    cpp-options:        -Dbool=_Bool+     ghc-options:        -Wall  test-suite flatcv-test@@ -60,4 +65,5 @@         base >= 4.12 && < 5,         flatcv,         hspec >= 2.7 && < 2.12,+        text >= 1.2 && < 2.2,         vector >= 0.12 && < 0.14
flatcv.h view
@@ -271,9 +271,9 @@ #include <stdint.h>  /**- * Get EXIF orientation from a JPEG file.+ * Get EXIF orientation from a JPEG or PNG file.  *- * @param filename Path to the JPEG file.+ * @param filename Path to the JPEG or PNG file.  * @return Orientation value (1-8), or 1 if not found or error.  */ int32_t fcv_get_exif_orientation(char const *filename);@@ -329,6 +329,24 @@   uint32_t height,   uint8_t const * const data );+// File: include/flood_fill.h+#ifndef FLATCV_AMALGAMATION+#pragma once+#include "1_types.h"+#endif++#include <stdint.h>++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+); // File: include/foerstner_corner.h #ifndef FLATCV_AMALGAMATION #pragma once@@ -406,6 +424,49 @@  Corners fcv_detect_corners(const uint8_t *image, int32_t width, int32_t height); Corners* fcv_detect_corners_ptr(const uint8_t *image, int32_t width, int32_t height);+// File: include/qr_code.h+#ifndef FLATCV_AMALGAMATION+#pragma once+#endif++#include <stddef.h>+#include <stdint.h>++#ifndef FLATCV_AMALGAMATION+#include "1_types.h"+#endif++typedef struct {+  char *text;+  Corners corners;+  Point2D finders[3]; // TL, TR, BL finder pattern centers+  Point2D *alignments;     // Alignment pattern centers (pixel coords)+  size_t alignment_count;  // Number of alignment patterns (0 for v1)+  int version;             // QR version, 1..40+  int qr_size;             // Module count per side: 21 + 4*(version-1)+  double module_size;+} FCVQRCode;++typedef struct {+  FCVQRCode *codes;+  size_t count;+} FCVQRCodeResult;++/**+ * Decode QR codes from a single-channel grayscale image.+ *+ * @param width Image width in pixels.+ * @param height Image height in pixels.+ * @param gray_pixels Grayscale pixel array (0 = black, 255 = white).+ * @return Result struct with decoded codes. Call fcv_free_qr_result to release.+ */+FCVQRCodeResult fcv_decode_qr_codes(+  uint32_t width,+  uint32_t height,+  uint8_t const *const gray_pixels+);++void fcv_free_qr_result(FCVQRCodeResult result); // File: include/rgba_to_grayscale.h #ifndef FLATCV_AMALGAMATION #pragma once
flatcv_impl.c view
@@ -1622,2732 +1622,9077 @@                 .bl_y = sorted_result[3 % peaks->count].y * scale_y};   } -  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]);-}--int32_t fcv_get_exif_orientation(char const *filename) {-  FILE *file = fopen(filename, "rb");-  if (!file) {-    return 1;-  }--  uint8_t header[2];-  if (fread(header, 1, 2, file) != 2 || header[0] != 0xFF ||-      header[1] != 0xD8) {-    fclose(file);-    return 1;-  }--  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;-      }--      long tiff_start = ftell(file);-      uint8_t tiff_header[2];-      if (fread(tiff_header, 1, 2, file) != 2) {-        break;-      }--      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) {-        break;-      }--      uint16_t magic = read_u16(file, is_little_endian);-      if (magic != 42) {-        break;-      }--      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-          uint16_t orientation = read_u16(file, is_little_endian);-          fclose(file);-          return (int32_t)orientation;-        }-        else {-          fseek(file, 4, SEEK_CUR); // Skip value/offset-        }-      }-      break; // IFD0 done-    }-    else {-      fseek(file, size - 2, SEEK_CUR);-    }-  }--  fclose(file);-  return 1;-}-// 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/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/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 the top-left corner (minimum x+y sum)-  uint32_t top_left_idx = 0;-  double min_sum = working_corners[0].x + working_corners[0].y;-  for (uint32_t i = 1; i < corners_to_process; i++) {-    double sum = working_corners[i].x + working_corners[i].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;
src/FlatCV.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}  -- |@@ -33,6 +34,7 @@   , Corners(..)   , Matrix3x3(..)   , Point2D(..)+  , Connectivity(..)      -- * Pretty printing   , prettyShowCorners@@ -96,6 +98,7 @@      -- * Segmentation   , watershedSegmentation+  , floodFill      -- * EXIF   , getExifOrientation@@ -139,6 +142,8 @@  import Control.Monad (when) import Data.Int (Int32)+import Data.Text (Text)+import qualified Data.Text as T import Data.Word (Word8, Word32) import qualified Data.Vector.Storable as V import qualified Data.Vector.Storable.Mutable as MV@@ -146,7 +151,7 @@ import Foreign.C.Types (CUChar) import Foreign.ForeignPtr (newForeignPtr, FinalizerPtr, castForeignPtr) import Foreign.Marshal.Alloc (alloca, free, mallocBytes)-import Foreign.Marshal.Array (pokeArray)+import Foreign.Marshal.Array (pokeArray, withArrayLen) import Foreign.Ptr (Ptr, castPtr, nullPtr) import Foreign.Storable (Storable(..), peek, poke, sizeOf, alignment) import Text.Printf (printf)@@ -178,6 +183,12 @@     pokeByteOff ptr 0 pointX     pokeByteOff ptr 8 pointY +-- | Pixel neighborhood used for region growing+data Connectivity+  = Connectivity4  -- ^ Edge neighbors only+  | Connectivity8  -- ^ Edge and diagonal neighbors+  deriving (Show, Eq)+ -- | Quadrilateral corners (top-left, top-right, bottom-right, bottom-left) data Corners = Corners   { tlX :: {-# UNPACK #-} !Double@@ -214,10 +225,10 @@     pokeByteOff ptr 56 blY  -- | Pretty print corners for debugging-prettyShowCorners :: Corners -> String+prettyShowCorners :: Corners -> Text prettyShowCorners Corners{..} =-  show (tlX, tlY) ++ " " ++ show (trX, trY) ++ "\n" ++-  show (blX, blY) ++ " " ++ show (brX, brY)+  T.pack (show (tlX, tlY)) <> " " <> T.pack (show (trX, trY)) <> "\n" <>+  T.pack (show (blX, blY)) <> " " <> T.pack (show (brX, brY))  -- | 3x3 transformation matrix data Matrix3x3 = Matrix3x3@@ -258,12 +269,12 @@     pokeByteOff ptr 64 m22  -- | Pretty print 3x3 matrix for debugging-prettyShowMatrix3x3 :: Matrix3x3 -> String+prettyShowMatrix3x3 :: Matrix3x3 -> Text prettyShowMatrix3x3 Matrix3x3{..} =-  let fNum n = printf "% .5f" (n :: Double)-  in fNum m00 ++ " " ++ fNum m01 ++ " " ++ fNum m02 ++ "\n" ++-     fNum m10 ++ " " ++ fNum m11 ++ " " ++ fNum m12 ++ "\n" ++-     fNum m20 ++ " " ++ fNum m21 ++ " " ++ fNum m22+  let fNum n = T.pack (printf "% .5f" (n :: Double))+  in fNum m00 <> " " <> fNum m01 <> " " <> fNum m02 <> "\n" <>+     fNum m10 <> " " <> fNum m11 <> " " <> fNum m12 <> "\n" <>+     fNum m20 <> " " <> fNum m21 <> " " <> fNum m22  -- Foreign finalizer for C-allocated memory foreign import ccall "stdlib.h &free" finalizerFree :: FinalizerPtr CUChar@@ -539,31 +550,31 @@ --------------------------------------------------------------------------------  -- | Draw a circle outline on an image (modifies input in place)-drawCircle :: Word32 -> Word32 -> Word32 -> String -> Double -> Double -> Double -> V.Vector Word8 -> IO (V.Vector Word8)+drawCircle :: Word32 -> Word32 -> Word32 -> Text -> Double -> Double -> Double -> V.Vector Word8 -> IO (V.Vector Word8) drawCircle width height channels hexColor radius centerX centerY input = do   output <- V.thaw input   MV.unsafeWith output $ \ptr ->-    withCString hexColor $ \colorPtr ->+    withCString (T.unpack hexColor) $ \colorPtr ->       Raw.fcv_draw_circle width height channels colorPtr radius centerX centerY ptr   V.unsafeFreeze output  -- | Draw a filled disk on an image (modifies input in place)-drawDisk :: Word32 -> Word32 -> Word32 -> String -> Double -> Double -> Double -> V.Vector Word8 -> IO (V.Vector Word8)+drawDisk :: Word32 -> Word32 -> Word32 -> Text -> Double -> Double -> Double -> V.Vector Word8 -> IO (V.Vector Word8) drawDisk width height channels hexColor radius centerX centerY input = do   output <- V.thaw input   MV.unsafeWith output $ \ptr ->-    withCString hexColor $ \colorPtr ->+    withCString (T.unpack hexColor) $ \colorPtr ->       Raw.fcv_draw_disk width height channels colorPtr radius centerX centerY ptr   V.unsafeFreeze output  -- | Add a border around an image -- Returns (output width, output height, output data)-addBorder :: Word32 -> Word32 -> Word32 -> String -> Word32 -> V.Vector Word8 -> IO (Word32, Word32, V.Vector Word8)+addBorder :: Word32 -> Word32 -> Word32 -> Text -> Word32 -> V.Vector Word8 -> IO (Word32, Word32, V.Vector Word8) addBorder width height channels hexColor borderWidth input = do   alloca $ \outWPtr ->     alloca $ \outHPtr -> do       V.unsafeWith input $ \inPtr ->-        withCString hexColor $ \colorPtr -> do+        withCString (T.unpack hexColor) $ \colorPtr -> do           outPtr <- Raw.fcv_add_border width height channels colorPtr borderWidth inPtr outWPtr outHPtr           when (outPtr == nullPtr) $ error "FlatCV: add_border failed"           outW <- peek outWPtr@@ -606,6 +617,25 @@   free markersPtr   return result +-- | Flood fill segmentation on single-channel grayscale data.+-- Starting from each marker, all connected pixels whose value differs+-- by at most @threshold@ from that marker's value are set to @newValue@.+-- Each marker is filled independently, the result is the union of all fills.+-- Fails if no markers are given or a marker lies outside the image.+floodFill+  :: Word32 -> Word32 -> [Point2D] -> Word8 -> Word8 -> Connectivity+  -> V.Vector Word8 -> IO (V.Vector Word8)+floodFill width height markers threshold newValue connectivity input =+  withArrayLen markers $ \numMarkers markersPtr ->+    withImageData input+      (\p -> Raw.fcv_flood_fill width height p (castPtr markersPtr)+        (fromIntegral numMarkers) threshold newValue connectivityNum)+      (width * height)+  where+    connectivityNum = case connectivity of+      Connectivity4 -> 4+      Connectivity8 -> 8+ -------------------------------------------------------------------------------- -- EXIF --------------------------------------------------------------------------------@@ -621,12 +651,12 @@ --------------------------------------------------------------------------------  -- | Parse hex color string (e.g., "#FF0000" or "FF0000") to RGB-parseHexColor :: String -> IO (Word8, Word8, Word8)+parseHexColor :: Text -> IO (Word8, Word8, Word8) parseHexColor hexColor =   alloca $ \rPtr ->     alloca $ \gPtr ->       alloca $ \bPtr ->-        withCString hexColor $ \colorPtr -> do+        withCString (T.unpack hexColor) $ \colorPtr -> do           Raw.fcv_parse_hex_color colorPtr rPtr gPtr bPtr           r <- peek rPtr           g <- peek gPtr@@ -634,10 +664,10 @@           return (r, g, b)  -- | Convert image to binary with custom foreground and background colors-convertToBinary :: Int32 -> Int32 -> String -> String -> V.Vector Word8 -> IO (V.Vector Word8)+convertToBinary :: Int32 -> Int32 -> Text -> Text -> V.Vector Word8 -> IO (V.Vector Word8) convertToBinary width height fgHex bgHex input =-  withCString fgHex $ \fgPtr ->-    withCString bgHex $ \bgPtr ->+  withCString (T.unpack fgHex) $ \fgPtr ->+    withCString (T.unpack bgHex) $ \bgPtr ->       withImageData input (\p -> Raw.fcv_convert_to_binary p width height fgPtr bgPtr)         (fromIntegral $ width * height * 4) 
src/FlatCV/Raw.chs view
@@ -513,3 +513,20 @@   , `Bool'                        -- ^ create_boundaries   } -> `Ptr Word8' toWord8Ptr #}++--------------------------------------------------------------------------------+-- Flood fill+--------------------------------------------------------------------------------++-- | Flood fill segmentation+{#fun fcv_flood_fill+  { `Word32'                      -- ^ width+  , `Word32'                      -- ^ height+  , fromWord8Ptr `Ptr Word8'      -- ^ grayscale data+  , `Point2DPtr'                  -- ^ markers array+  , `Word32'                      -- ^ num_markers+  , `Word8'                       -- ^ threshold+  , `Word8'                       -- ^ new_value+  , `Word8'                       -- ^ connectivity+  } -> `Ptr Word8' toWord8Ptr+#}
test/Spec.hs view
@@ -2,6 +2,8 @@  module Main where +import Data.Text (Text)+import qualified Data.Text as T import Data.Word (Word8) import qualified Data.Vector.Storable as V import Test.Hspec@@ -184,7 +186,7 @@     describe "prettyShowCorners" $ do       it "formats corners as a readable string" $ do         let corners = Corners 0 0 100 0 100 100 0 100-        prettyShowCorners corners `shouldBe` "(0.0,0.0) (100.0,0.0)\n(0.0,100.0) (100.0,100.0)"+        prettyShowCorners corners `shouldBe` ("(0.0,0.0) (100.0,0.0)\n(0.0,100.0) (100.0,100.0)" :: Text)      describe "Matrix3x3" $ do       it "has correct Storable instance" $ do@@ -198,8 +200,8 @@       it "formats matrix as a readable string" $ do         let mat = Matrix3x3 1 0 0 0 1 0 0 0 1         -- Just check it contains the values and has newlines-        prettyShowMatrix3x3 mat `shouldContain` "1.00000"-        prettyShowMatrix3x3 mat `shouldContain` "\n"+        prettyShowMatrix3x3 mat `shouldSatisfy` T.isInfixOf "1.00000"+        prettyShowMatrix3x3 mat `shouldSatisfy` T.isInfixOf "\n"      describe "calculatePerspectiveTransform" $ do       it "calculates identity-like transform for same corners" $ do@@ -243,6 +245,24 @@         outW `shouldBe` 8  -- 4 + 2*2         outH `shouldBe` 8         V.length result `shouldBe` 256  -- 8x8x4++    describe "floodFill" $ do+      it "fills the connected region within threshold of the seed" $ do+        let input = V.fromList [10, 12, 200, 11]+        result <- floodFill 4 1 [Point2D 0 0] 5 0 Connectivity4 input+        result `shouldBe` V.fromList [0, 0, 200, 11]++      it "fills one region per seed" $ do+        let input = V.fromList [10, 12, 200, 11]+        result <- floodFill 4 1 [Point2D 0 0, Point2D 3 0] 5 0 Connectivity4 input+        result `shouldBe` V.fromList [0, 0, 200, 0]++      it "only crosses diagonal gaps with 8-connectivity" $ do+        let input = V.fromList [0, 255, 0, 255, 0, 255, 0, 255, 0]+        result4 <- floodFill 3 3 [Point2D 1 1] 0 100 Connectivity4 input+        result8 <- floodFill 3 3 [Point2D 1 1] 0 100 Connectivity8 input+        result4 `shouldBe` V.fromList [0, 255, 0, 255, 100, 255, 0, 255, 0]+        result8 `shouldBe` V.fromList [100, 255, 100, 255, 100, 255, 100, 255, 100]      describe "convertToBinary" $ do       it "converts to binary with custom colors" $ do