/* * SPDX-License-Identifier: MIT * * Copyright (C) 2013-2024 OpenMV, LLC. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * Blob detection code. */ #include "imlib.h" typedef struct xylr { int16_t x, y, l, r, t_l, b_l; } xylr_t; static float sign(float x) { return x / fabsf(x); } static int sum_m_to_n(int m, int n) { return ((n * (n + 1)) - (m * (m - 1))) / 2; } static int sum_2_m_to_n(int m, int n) { return ((n * (n + 1) * ((2 * n) + 1)) - (m * (m - 1) * ((2 * m) - 1))) / 6; } static int cumulative_moving_average(int avg, int x, int n) { return (x + (n * avg)) / (n + 1); } static void bin_up(uint16_t *hist, uint16_t size, unsigned int max_size, uint16_t **new_hist, uint16_t *new_size) { int start = -1; for (int i = 0; i < size; i++) { if (hist[i]) { start = i; break; } } if (start != -1) { int end = start; for (int i = start + 1; i < size; i++) { if (!hist[i]) { break; } end = i; } uint16_t bin_count = end - start + 1; // >= 1 *new_size = IM_MIN(max_size, bin_count); *new_hist = m_malloc0((*new_size) * sizeof(uint16_t)); float div_value = (*new_size) / ((float) bin_count); // Reversed so we can multiply below. for (int i = 0; i < bin_count; i++) { (*new_hist)[fast_floorf(i * div_value)] += hist[start + i]; } } } static void merge_bins(int b_dst_start, int b_dst_end, uint16_t **b_dst_hist, uint16_t *b_dst_hist_len, int b_src_start, int b_src_end, uint16_t **b_src_hist, uint16_t *b_src_hist_len, unsigned int max_size) { int start = IM_MIN(b_dst_start, b_src_start); int end = IM_MAX(b_dst_end, b_src_end); uint16_t bin_count = end - start + 1; // >= 1 uint16_t new_size = IM_MIN(max_size, bin_count); uint16_t *new_hist = m_malloc0(new_size * sizeof(uint16_t)); float div_value = new_size / ((float) bin_count); // Reversed so we can multiply below. int b_dst_bin_count = b_dst_end - b_dst_start + 1; // >= 1 uint16_t b_dst_new_size = IM_MIN((*b_dst_hist_len), b_dst_bin_count); float b_dst_div_value = b_dst_new_size / ((float) b_dst_bin_count); // Reversed so we can multiply below. int b_src_bin_count = b_src_end - b_src_start + 1; // >= 1 uint16_t b_src_new_size = IM_MIN((*b_src_hist_len), b_src_bin_count); float b_src_div_value = b_src_new_size / ((float) b_src_bin_count); // Reversed so we can multiply below. for (int i = 0; i < bin_count; i++) { if ((b_dst_start <= (i + start)) && ((i + start) <= b_dst_end)) { int index = fast_floorf((i + start - b_dst_start) * b_dst_div_value); new_hist[fast_floorf(i * div_value)] += (*b_dst_hist)[index]; (*b_dst_hist)[index] = 0; // prevent from adding again... } if ((b_src_start <= (i + start)) && ((i + start) <= b_src_end)) { int index = fast_floorf((i + start - b_src_start) * b_src_div_value); new_hist[fast_floorf(i * div_value)] += (*b_src_hist)[index]; (*b_src_hist)[index] = 0; // prevent from adding again... } } m_free(*b_dst_hist); m_free(*b_src_hist); *b_dst_hist_len = new_size; (*b_dst_hist) = new_hist; *b_src_hist_len = 0; (*b_src_hist) = NULL; } static float calc_roundness(float blob_a, float blob_b, float blob_c) { float roundness_div = fast_sqrtf((blob_b * blob_b) + ((blob_a - blob_c) * (blob_a - blob_c))); float roundness_sin = IM_DIV(blob_b, roundness_div); float roundness_cos = IM_DIV(blob_a - blob_c, roundness_div); float roundness_add = (blob_a + blob_c) / 2; float roundness_cos_mul = (blob_a - blob_c) / 2; float roundness_sin_mul = blob_b / 2; float roundness_0 = roundness_add + (roundness_cos * roundness_cos_mul) + (roundness_sin * roundness_sin_mul); float roundness_1 = roundness_add + (roundness_cos * roundness_cos_mul) - (roundness_sin * roundness_sin_mul); float roundness_2 = roundness_add - (roundness_cos * roundness_cos_mul) + (roundness_sin * roundness_sin_mul); float roundness_3 = roundness_add - (roundness_cos * roundness_cos_mul) - (roundness_sin * roundness_sin_mul); float roundness_max = IM_MAX(roundness_0, IM_MAX(roundness_1, IM_MAX(roundness_2, roundness_3))); float roundness_min = IM_MIN(roundness_0, IM_MIN(roundness_1, IM_MIN(roundness_2, roundness_3))); return IM_DIV(roundness_min, roundness_max); } void imlib_find_blobs(list_t *out, image_t *ptr, rectangle_t *roi, unsigned int x_stride, unsigned int y_stride, list_t *thresholds, bool invert, unsigned int area_threshold, unsigned int pixels_threshold, bool merge, int margin, bool (*threshold_cb) (void *, find_blobs_list_lnk_data_t *), void *threshold_cb_arg, bool (*merge_cb) (void *, find_blobs_list_lnk_data_t *, find_blobs_list_lnk_data_t *), void *merge_cb_arg, unsigned int x_hist_bins_max, unsigned int y_hist_bins_max) { // Same size as the image so we don't have to translate. image_t bmp; bmp.w = ptr->w; bmp.h = ptr->h; bmp.pixfmt = PIXFORMAT_BINARY; bmp.data = fb_alloc0(image_size(&bmp), FB_ALLOC_NO_HINT); uint16_t *x_hist_bins = NULL; if (x_hist_bins_max) { x_hist_bins = fb_alloc(ptr->w * sizeof(uint16_t), FB_ALLOC_NO_HINT); } uint16_t *y_hist_bins = NULL; if (y_hist_bins_max) { y_hist_bins = fb_alloc(ptr->h * sizeof(uint16_t), FB_ALLOC_NO_HINT); } lifo_t lifo; size_t lifo_len; lifo_alloc_all(&lifo, &lifo_len, sizeof(xylr_t)); list_init(out, sizeof(find_blobs_list_lnk_data_t)); size_t code = 0; list_for_each(it, thresholds) { color_thresholds_list_lnk_data_t *lnk_data = list_get_data(it); switch (ptr->pixfmt) { case PIXFORMAT_BINARY: { for (int y = roi->y, yy = roi->y + roi->h, y_max = yy - 1; y < yy; y += y_stride) { uint32_t *row_ptr = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(ptr, y); uint32_t *bmp_row_ptr = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y); for (int x = roi->x + (y % x_stride), xx = roi->x + roi->w, x_max = xx - 1; x < xx; x += x_stride) { if ((!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row_ptr, x)) && COLOR_THRESHOLD_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row_ptr, x), lnk_data, invert)) { int old_x = x; int old_y = y; float corners_acc[FIND_BLOBS_CORNERS_RESOLUTION]; point_t corners[FIND_BLOBS_CORNERS_RESOLUTION]; int corners_n[FIND_BLOBS_CORNERS_RESOLUTION]; // These values are initialized to their maximum before we minimize. for (int i = 0; i < FIND_BLOBS_CORNERS_RESOLUTION; i++) { corners[i].x = IM_CLAMP(x_max * sign(cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]), 0, x_max); corners[i].y = IM_CLAMP(y_max * sign(sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]), 0, y_max); corners_acc[i] = (corners[i].x * cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]) + (corners[i].y * sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]); corners_n[i] = 1; } int blob_pixels = 0; int blob_perimeter = 0; int blob_cx = 0; int blob_cy = 0; long long blob_a = 0; long long blob_b = 0; long long blob_c = 0; if (x_hist_bins) { memset(x_hist_bins, 0, ptr->w * sizeof(uint16_t)); } if (y_hist_bins) { memset(y_hist_bins, 0, ptr->h * sizeof(uint16_t)); } // Scanline Flood Fill Algorithm // for (;;) { int left = x, right = x; uint32_t *row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(ptr, y); uint32_t *bmp_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y); while ((left > roi->x) && (!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, left - 1)) && COLOR_THRESHOLD_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, left - 1), lnk_data, invert)) { left--; } while ((right < (roi->x + roi->w - 1)) && (!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, right + 1)) && COLOR_THRESHOLD_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, right + 1), lnk_data, invert)) { right++; } for (int i = left; i <= right; i++) { IMAGE_SET_BINARY_PIXEL_FAST(bmp_row, i); } int sum = sum_m_to_n(left, right); int sum_2 = sum_2_m_to_n(left, right); int cnt = right - left + 1; int avg = sum / cnt; for (int i = 0; i < FIND_BLOBS_CORNERS_RESOLUTION; i++) { int x_new = (cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i] > 0) ? left : ((cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i] == 0) ? avg : right); float z = (x_new * cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]) + (y * sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]); if (z < corners_acc[i]) { corners_acc[i] = z; corners[i].x = x_new; corners[i].y = y; corners_n[i] = 1; } else if (z == corners_acc[i]) { corners[i].x = cumulative_moving_average(corners[i].x, x_new, corners_n[i]); corners[i].y = cumulative_moving_average(corners[i].y, y, corners_n[i]); corners_n[i] += 1; } } blob_pixels += cnt; blob_perimeter += 2; blob_cx += sum; blob_cy += y * cnt; blob_a += sum_2; blob_b += y * sum; blob_c += y * y * cnt; if (y_hist_bins) { y_hist_bins[y] += cnt; } if (x_hist_bins) { for (int i = left; i <= right; i++) { x_hist_bins[i] += 1; } } int top_left = left; int bot_left = left; bool break_out = false; for (;;) { if (lifo_size(&lifo) < lifo_len) { if (y > roi->y) { row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(ptr, y - 1); bmp_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y - 1); bool recurse = false; for (int i = top_left; i <= right; i++) { bool ok = true; // Does nothing if thresholding is skipped. if ((!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, i)) && (ok = COLOR_THRESHOLD_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, i), lnk_data, invert))) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = i + 1; // Don't test the same pixel again... context.b_l = bot_left; lifo_enqueue(&lifo, &context); x = i; y = y - 1; recurse = true; break; } blob_perimeter += (!ok) && (i != left) && (i != right); } if (recurse) { break; } } else { blob_perimeter += right - left + 1; } if (y < (roi->y + roi->h - 1)) { row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(ptr, y + 1); bmp_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y + 1); bool recurse = false; for (int i = bot_left; i <= right; i++) { bool ok = true; // Does nothing if thresholding is skipped. if ((!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, i)) && (ok = COLOR_THRESHOLD_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, i), lnk_data, invert))) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = top_left; context.b_l = i + 1; // Don't test the same pixel again... lifo_enqueue(&lifo, &context); x = i; y = y + 1; recurse = true; break; } blob_perimeter += (!ok) && (i != left) && (i != right); } if (recurse) { break; } } else { blob_perimeter += right - left + 1; } } else { blob_perimeter += (right - left + 1) * 2; } if (!lifo_size(&lifo)) { break_out = true; break; } xylr_t context; lifo_dequeue(&lifo, &context); x = context.x; y = context.y; left = context.l; right = context.r; top_left = context.t_l; bot_left = context.b_l; } if (break_out) { break; } } rectangle_t rect; rect.x = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 0) / 4].x; // l rect.y = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 1) / 4].y; // t rect.w = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 2) / 4].x - corners[(FIND_BLOBS_CORNERS_RESOLUTION * 0) / 4].x + 1; // r - l + 1 rect.h = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 3) / 4].y - corners[(FIND_BLOBS_CORNERS_RESOLUTION * 1) / 4].y + 1; // b - t + 1 if (((rect.w * rect.h) >= area_threshold) && (blob_pixels >= pixels_threshold)) { // http://www.cse.usf.edu/~r1k/MachineVisionBook/MachineVision.files/MachineVision_Chapter2.pdf // https://www.strchr.com/standard_deviation_in_one_pass // // a = sigma(x*x) + (mx*sigma(x)) + (mx*sigma(x)) + (sigma()*mx*mx) // b = sigma(x*y) + (mx*sigma(y)) + (my*sigma(x)) + (sigma()*mx*my) // c = sigma(y*y) + (my*sigma(y)) + (my*sigma(y)) + (sigma()*my*my) // // blob_a = sigma(x*x) // blob_b = sigma(x*y) // blob_c = sigma(y*y) // blob_cx = sigma(x) // blob_cy = sigma(y) // blob_pixels = sigma() float b_mx = blob_cx / ((float) blob_pixels); float b_my = blob_cy / ((float) blob_pixels); int mx = fast_roundf(b_mx); // x centroid int my = fast_roundf(b_my); // y centroid int small_blob_a = blob_a - ((mx * blob_cx) + (mx * blob_cx)) + (blob_pixels * mx * mx); int small_blob_b = blob_b - ((mx * blob_cy) + (my * blob_cx)) + (blob_pixels * mx * my); int small_blob_c = blob_c - ((my * blob_cy) + (my * blob_cy)) + (blob_pixels * my * my); find_blobs_list_lnk_data_t lnk_blob; memcpy(lnk_blob.corners, corners, FIND_BLOBS_CORNERS_RESOLUTION * sizeof(point_t)); memcpy(&lnk_blob.rect, &rect, sizeof(rectangle_t)); lnk_blob.pixels = blob_pixels; lnk_blob.perimeter = blob_perimeter; lnk_blob.code = 1 << code; lnk_blob.count = 1; lnk_blob.centroid_x = b_mx; lnk_blob.centroid_y = b_my; lnk_blob.rotation = (small_blob_a != small_blob_c) ? (fast_atan2f(2 * small_blob_b, small_blob_a - small_blob_c) / 2.0f) : 0.0f; lnk_blob.roundness = calc_roundness(small_blob_a, small_blob_b, small_blob_c); lnk_blob.x_hist_bins_count = 0; lnk_blob.x_hist_bins = NULL; lnk_blob.y_hist_bins_count = 0; lnk_blob.y_hist_bins = NULL; // These store the current average accumulation. lnk_blob.centroid_x_acc = lnk_blob.centroid_x * lnk_blob.pixels; lnk_blob.centroid_y_acc = lnk_blob.centroid_y * lnk_blob.pixels; lnk_blob.rotation_acc_x = cosf(lnk_blob.rotation) * lnk_blob.pixels; lnk_blob.rotation_acc_y = sinf(lnk_blob.rotation) * lnk_blob.pixels; lnk_blob.roundness_acc = lnk_blob.roundness * lnk_blob.pixels; if (x_hist_bins) { bin_up(x_hist_bins, ptr->w, x_hist_bins_max, &lnk_blob.x_hist_bins, &lnk_blob.x_hist_bins_count); } if (y_hist_bins) { bin_up(y_hist_bins, ptr->h, y_hist_bins_max, &lnk_blob.y_hist_bins, &lnk_blob.y_hist_bins_count); } bool add_to_list = threshold_cb_arg == NULL; if (!add_to_list) { // Protect ourselves from caught exceptions in the callback // code from freeing our fb_alloc() stack. fb_alloc_mark(); fb_alloc_mark_permanent(); add_to_list = threshold_cb(threshold_cb_arg, &lnk_blob); fb_alloc_free_till_mark_past_mark_permanent(); } if (add_to_list) { list_push_back(out, &lnk_blob); } else { if (lnk_blob.x_hist_bins) { m_free(lnk_blob.x_hist_bins); } if (lnk_blob.y_hist_bins) { m_free(lnk_blob.y_hist_bins); } } } x = old_x; y = old_y; } } } break; } case PIXFORMAT_GRAYSCALE: { for (int y = roi->y, yy = roi->y + roi->h, y_max = yy - 1; y < yy; y += y_stride) { uint8_t *row_ptr = IMAGE_COMPUTE_GRAYSCALE_PIXEL_ROW_PTR(ptr, y); uint32_t *bmp_row_ptr = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y); for (int x = roi->x + (y % x_stride), xx = roi->x + roi->w, x_max = xx - 1; x < xx; x += x_stride) { if ((!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row_ptr, x)) && COLOR_THRESHOLD_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row_ptr, x), lnk_data, invert)) { int old_x = x; int old_y = y; float corners_acc[FIND_BLOBS_CORNERS_RESOLUTION]; point_t corners[FIND_BLOBS_CORNERS_RESOLUTION]; int corners_n[FIND_BLOBS_CORNERS_RESOLUTION]; // These values are initialized to their maximum before we minimize. for (int i = 0; i < FIND_BLOBS_CORNERS_RESOLUTION; i++) { corners[i].x = IM_CLAMP(x_max * sign(cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]), 0, x_max); corners[i].y = IM_CLAMP(y_max * sign(sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]), 0, y_max); corners_acc[i] = (corners[i].x * cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]) + (corners[i].y * sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]); corners_n[i] = 1; } int blob_pixels = 0; int blob_perimeter = 0; int blob_cx = 0; int blob_cy = 0; long long blob_a = 0; long long blob_b = 0; long long blob_c = 0; if (x_hist_bins) { memset(x_hist_bins, 0, ptr->w * sizeof(uint16_t)); } if (y_hist_bins) { memset(y_hist_bins, 0, ptr->h * sizeof(uint16_t)); } // Scanline Flood Fill Algorithm // for (;;) { int left = x, right = x; uint8_t *row = IMAGE_COMPUTE_GRAYSCALE_PIXEL_ROW_PTR(ptr, y); uint32_t *bmp_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y); while ((left > roi->x) && (!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, left - 1)) && COLOR_THRESHOLD_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, left - 1), lnk_data, invert)) { left--; } while ((right < (roi->x + roi->w - 1)) && (!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, right + 1)) && COLOR_THRESHOLD_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, right + 1), lnk_data, invert)) { right++; } for (int i = left; i <= right; i++) { IMAGE_SET_BINARY_PIXEL_FAST(bmp_row, i); } int sum = sum_m_to_n(left, right); int sum_2 = sum_2_m_to_n(left, right); int cnt = right - left + 1; int avg = sum / cnt; for (int i = 0; i < FIND_BLOBS_CORNERS_RESOLUTION; i++) { int x_new = (cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i] > 0) ? left : ((cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i] == 0) ? avg : right); float z = (x_new * cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]) + (y * sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]); if (z < corners_acc[i]) { corners_acc[i] = z; corners[i].x = x_new; corners[i].y = y; corners_n[i] = 1; } else if (z == corners_acc[i]) { corners[i].x = cumulative_moving_average(corners[i].x, x_new, corners_n[i]); corners[i].y = cumulative_moving_average(corners[i].y, y, corners_n[i]); corners_n[i] += 1; } } blob_pixels += cnt; blob_perimeter += 2; blob_cx += sum; blob_cy += y * cnt; blob_a += sum_2; blob_b += y * sum; blob_c += y * y * cnt; if (y_hist_bins) { y_hist_bins[y] += cnt; } if (x_hist_bins) { for (int i = left; i <= right; i++) { x_hist_bins[i] += 1; } } int top_left = left; int bot_left = left; bool break_out = false; for (;;) { if (lifo_size(&lifo) < lifo_len) { if (y > roi->y) { row = IMAGE_COMPUTE_GRAYSCALE_PIXEL_ROW_PTR(ptr, y - 1); bmp_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y - 1); bool recurse = false; for (int i = top_left; i <= right; i++) { bool ok = true; // Does nothing if thresholding is skipped. if ((!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, i)) && (ok = COLOR_THRESHOLD_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, i), lnk_data, invert))) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = i + 1; // Don't test the same pixel again... context.b_l = bot_left; lifo_enqueue(&lifo, &context); x = i; y = y - 1; recurse = true; break; } blob_perimeter += (!ok) && (i != left) && (i != right); } if (recurse) { break; } } else { blob_perimeter += right - left + 1; } if (y < (roi->y + roi->h - 1)) { row = IMAGE_COMPUTE_GRAYSCALE_PIXEL_ROW_PTR(ptr, y + 1); bmp_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y + 1); bool recurse = false; for (int i = bot_left; i <= right; i++) { bool ok = true; // Does nothing if thresholding is skipped. if ((!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, i)) && (ok = COLOR_THRESHOLD_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, i), lnk_data, invert))) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = top_left; context.b_l = i + 1; // Don't test the same pixel again... lifo_enqueue(&lifo, &context); x = i; y = y + 1; recurse = true; break; } blob_perimeter += (!ok) && (i != left) && (i != right); } if (recurse) { break; } } else { blob_perimeter += right - left + 1; } } else { blob_perimeter += (right - left + 1) * 2; } if (!lifo_size(&lifo)) { break_out = true; break; } xylr_t context; lifo_dequeue(&lifo, &context); x = context.x; y = context.y; left = context.l; right = context.r; top_left = context.t_l; bot_left = context.b_l; } if (break_out) { break; } } rectangle_t rect; rect.x = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 0) / 4].x; // l rect.y = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 1) / 4].y; // t rect.w = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 2) / 4].x - corners[(FIND_BLOBS_CORNERS_RESOLUTION * 0) / 4].x + 1; // r - l + 1 rect.h = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 3) / 4].y - corners[(FIND_BLOBS_CORNERS_RESOLUTION * 1) / 4].y + 1; // b - t + 1 if (((rect.w * rect.h) >= area_threshold) && (blob_pixels >= pixels_threshold)) { // http://www.cse.usf.edu/~r1k/MachineVisionBook/MachineVision.files/MachineVision_Chapter2.pdf // https://www.strchr.com/standard_deviation_in_one_pass // // a = sigma(x*x) + (mx*sigma(x)) + (mx*sigma(x)) + (sigma()*mx*mx) // b = sigma(x*y) + (mx*sigma(y)) + (my*sigma(x)) + (sigma()*mx*my) // c = sigma(y*y) + (my*sigma(y)) + (my*sigma(y)) + (sigma()*my*my) // // blob_a = sigma(x*x) // blob_b = sigma(x*y) // blob_c = sigma(y*y) // blob_cx = sigma(x) // blob_cy = sigma(y) // blob_pixels = sigma() float b_mx = blob_cx / ((float) blob_pixels); float b_my = blob_cy / ((float) blob_pixels); int mx = fast_roundf(b_mx); // x centroid int my = fast_roundf(b_my); // y centroid int small_blob_a = blob_a - ((mx * blob_cx) + (mx * blob_cx)) + (blob_pixels * mx * mx); int small_blob_b = blob_b - ((mx * blob_cy) + (my * blob_cx)) + (blob_pixels * mx * my); int small_blob_c = blob_c - ((my * blob_cy) + (my * blob_cy)) + (blob_pixels * my * my); find_blobs_list_lnk_data_t lnk_blob; memcpy(lnk_blob.corners, corners, FIND_BLOBS_CORNERS_RESOLUTION * sizeof(point_t)); memcpy(&lnk_blob.rect, &rect, sizeof(rectangle_t)); lnk_blob.pixels = blob_pixels; lnk_blob.perimeter = blob_perimeter; lnk_blob.code = 1 << code; lnk_blob.count = 1; lnk_blob.centroid_x = b_mx; lnk_blob.centroid_y = b_my; lnk_blob.rotation = (small_blob_a != small_blob_c) ? (fast_atan2f(2 * small_blob_b, small_blob_a - small_blob_c) / 2.0f) : 0.0f; lnk_blob.roundness = calc_roundness(small_blob_a, small_blob_b, small_blob_c); lnk_blob.x_hist_bins_count = 0; lnk_blob.x_hist_bins = NULL; lnk_blob.y_hist_bins_count = 0; lnk_blob.y_hist_bins = NULL; // These store the current average accumulation. lnk_blob.centroid_x_acc = lnk_blob.centroid_x * lnk_blob.pixels; lnk_blob.centroid_y_acc = lnk_blob.centroid_y * lnk_blob.pixels; lnk_blob.rotation_acc_x = cosf(lnk_blob.rotation) * lnk_blob.pixels; lnk_blob.rotation_acc_y = sinf(lnk_blob.rotation) * lnk_blob.pixels; lnk_blob.roundness_acc = lnk_blob.roundness * lnk_blob.pixels; if (x_hist_bins) { bin_up(x_hist_bins, ptr->w, x_hist_bins_max, &lnk_blob.x_hist_bins, &lnk_blob.x_hist_bins_count); } if (y_hist_bins) { bin_up(y_hist_bins, ptr->h, y_hist_bins_max, &lnk_blob.y_hist_bins, &lnk_blob.y_hist_bins_count); } bool add_to_list = threshold_cb_arg == NULL; if (!add_to_list) { // Protect ourselves from caught exceptions in the callback // code from freeing our fb_alloc() stack. fb_alloc_mark(); fb_alloc_mark_permanent(); add_to_list = threshold_cb(threshold_cb_arg, &lnk_blob); fb_alloc_free_till_mark_past_mark_permanent(); } if (add_to_list) { list_push_back(out, &lnk_blob); } else { if (lnk_blob.x_hist_bins) { m_free(lnk_blob.x_hist_bins); } if (lnk_blob.y_hist_bins) { m_free(lnk_blob.y_hist_bins); } } } x = old_x; y = old_y; } } } break; } case PIXFORMAT_RGB565: { for (int y = roi->y, yy = roi->y + roi->h, y_max = yy - 1; y < yy; y += y_stride) { uint16_t *row_ptr = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(ptr, y); uint32_t *bmp_row_ptr = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y); for (int x = roi->x + (y % x_stride), xx = roi->x + roi->w, x_max = xx - 1; x < xx; x += x_stride) { if ((!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row_ptr, x)) && COLOR_THRESHOLD_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row_ptr, x), lnk_data, invert)) { int old_x = x; int old_y = y; float corners_acc[FIND_BLOBS_CORNERS_RESOLUTION]; point_t corners[FIND_BLOBS_CORNERS_RESOLUTION]; int corners_n[FIND_BLOBS_CORNERS_RESOLUTION]; // Ensures that maximum goes all the way to the edge of the image. for (int i = 0; i < FIND_BLOBS_CORNERS_RESOLUTION; i++) { corners[i].x = IM_CLAMP(x_max * sign(cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]), 0, x_max); corners[i].y = IM_CLAMP(y_max * sign(sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]), 0, y_max); corners_acc[i] = (corners[i].x * cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]) + (corners[i].y * sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]); corners_n[i] = 1; } int blob_pixels = 0; int blob_perimeter = 0; int blob_cx = 0; int blob_cy = 0; long long blob_a = 0; long long blob_b = 0; long long blob_c = 0; if (x_hist_bins) { memset(x_hist_bins, 0, ptr->w * sizeof(uint16_t)); } if (y_hist_bins) { memset(y_hist_bins, 0, ptr->h * sizeof(uint16_t)); } // Scanline Flood Fill Algorithm // for (;;) { int left = x, right = x; uint16_t *row = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(ptr, y); uint32_t *bmp_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y); while ((left > roi->x) && (!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, left - 1)) && COLOR_THRESHOLD_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, left - 1), lnk_data, invert)) { left--; } while ((right < (roi->x + roi->w - 1)) && (!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, right + 1)) && COLOR_THRESHOLD_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, right + 1), lnk_data, invert)) { right++; } for (int i = left; i <= right; i++) { IMAGE_SET_BINARY_PIXEL_FAST(bmp_row, i); } int sum = sum_m_to_n(left, right); int sum_2 = sum_2_m_to_n(left, right); int cnt = right - left + 1; int avg = sum / cnt; for (int i = 0; i < FIND_BLOBS_CORNERS_RESOLUTION; i++) { int x_new = (cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i] > 0) ? left : ((cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i] == 0) ? avg : right); float z = (x_new * cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]) + (y * sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]); if (z < corners_acc[i]) { corners_acc[i] = z; corners[i].x = x_new; corners[i].y = y; corners_n[i] = 1; } else if (z == corners_acc[i]) { corners[i].x = cumulative_moving_average(corners[i].x, x_new, corners_n[i]); corners[i].y = cumulative_moving_average(corners[i].y, y, corners_n[i]); corners_n[i] += 1; } } blob_pixels += cnt; blob_perimeter += 2; blob_cx += sum; blob_cy += y * cnt; blob_a += sum_2; blob_b += y * sum; blob_c += y * y * cnt; if (y_hist_bins) { y_hist_bins[y] += cnt; } if (x_hist_bins) { for (int i = left; i <= right; i++) { x_hist_bins[i] += 1; } } int top_left = left; int bot_left = left; bool break_out = false; for (;;) { if (lifo_size(&lifo) < lifo_len) { if (y > roi->y) { row = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(ptr, y - 1); bmp_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y - 1); bool recurse = false; for (int i = top_left; i <= right; i++) { bool ok = true; // Does nothing if thresholding is skipped. if ((!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, i)) && (ok = COLOR_THRESHOLD_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, i), lnk_data, invert))) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = i + 1; // Don't test the same pixel again... context.b_l = bot_left; lifo_enqueue(&lifo, &context); x = i; y = y - 1; recurse = true; break; } blob_perimeter += (!ok) && (i != left) && (i != right); } if (recurse) { break; } } else { blob_perimeter += right - left + 1; } if (y < (roi->y + roi->h - 1)) { row = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(ptr, y + 1); bmp_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(&bmp, y + 1); bool recurse = false; for (int i = bot_left; i <= right; i++) { bool ok = true; // Does nothing if thresholding is skipped. if ((!IMAGE_GET_BINARY_PIXEL_FAST(bmp_row, i)) && (ok = COLOR_THRESHOLD_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, i), lnk_data, invert))) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = top_left; context.b_l = i + 1; // Don't test the same pixel again... lifo_enqueue(&lifo, &context); x = i; y = y + 1; recurse = true; break; } blob_perimeter += (!ok) && (i != left) && (i != right); } if (recurse) { break; } } else { blob_perimeter += right - left + 1; } } else { blob_perimeter += (right - left + 1) * 2; } if (!lifo_size(&lifo)) { break_out = true; break; } xylr_t context; lifo_dequeue(&lifo, &context); x = context.x; y = context.y; left = context.l; right = context.r; top_left = context.t_l; bot_left = context.b_l; } if (break_out) { break; } } rectangle_t rect; rect.x = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 0) / 4].x; // l rect.y = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 1) / 4].y; // t rect.w = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 2) / 4].x - corners[(FIND_BLOBS_CORNERS_RESOLUTION * 0) / 4].x + 1; // r - l + 1 rect.h = corners[(FIND_BLOBS_CORNERS_RESOLUTION * 3) / 4].y - corners[(FIND_BLOBS_CORNERS_RESOLUTION * 1) / 4].y + 1; // b - t + 1 if (((rect.w * rect.h) >= area_threshold) && (blob_pixels >= pixels_threshold)) { // http://www.cse.usf.edu/~r1k/MachineVisionBook/MachineVision.files/MachineVision_Chapter2.pdf // https://www.strchr.com/standard_deviation_in_one_pass // // a = sigma(x*x) + (mx*sigma(x)) + (mx*sigma(x)) + (sigma()*mx*mx) // b = sigma(x*y) + (mx*sigma(y)) + (my*sigma(x)) + (sigma()*mx*my) // c = sigma(y*y) + (my*sigma(y)) + (my*sigma(y)) + (sigma()*my*my) // // blob_a = sigma(x*x) // blob_b = sigma(x*y) // blob_c = sigma(y*y) // blob_cx = sigma(x) // blob_cy = sigma(y) // blob_pixels = sigma() float b_mx = blob_cx / ((float) blob_pixels); float b_my = blob_cy / ((float) blob_pixels); int mx = fast_roundf(b_mx); // x centroid int my = fast_roundf(b_my); // y centroid int small_blob_a = blob_a - ((mx * blob_cx) + (mx * blob_cx)) + (blob_pixels * mx * mx); int small_blob_b = blob_b - ((mx * blob_cy) + (my * blob_cx)) + (blob_pixels * mx * my); int small_blob_c = blob_c - ((my * blob_cy) + (my * blob_cy)) + (blob_pixels * my * my); find_blobs_list_lnk_data_t lnk_blob; memcpy(lnk_blob.corners, corners, FIND_BLOBS_CORNERS_RESOLUTION * sizeof(point_t)); memcpy(&lnk_blob.rect, &rect, sizeof(rectangle_t)); lnk_blob.pixels = blob_pixels; lnk_blob.perimeter = blob_perimeter; lnk_blob.code = 1 << code; lnk_blob.count = 1; lnk_blob.centroid_x = b_mx; lnk_blob.centroid_y = b_my; lnk_blob.rotation = (small_blob_a != small_blob_c) ? (fast_atan2f(2 * small_blob_b, small_blob_a - small_blob_c) / 2.0f) : 0.0f; lnk_blob.roundness = calc_roundness(small_blob_a, small_blob_b, small_blob_c); lnk_blob.x_hist_bins_count = 0; lnk_blob.x_hist_bins = NULL; lnk_blob.y_hist_bins_count = 0; lnk_blob.y_hist_bins = NULL; // These store the current average accumulation. lnk_blob.centroid_x_acc = lnk_blob.centroid_x * lnk_blob.pixels; lnk_blob.centroid_y_acc = lnk_blob.centroid_y * lnk_blob.pixels; lnk_blob.rotation_acc_x = cosf(lnk_blob.rotation) * lnk_blob.pixels; lnk_blob.rotation_acc_y = sinf(lnk_blob.rotation) * lnk_blob.pixels; lnk_blob.roundness_acc = lnk_blob.roundness * lnk_blob.pixels; if (x_hist_bins) { bin_up(x_hist_bins, ptr->w, x_hist_bins_max, &lnk_blob.x_hist_bins, &lnk_blob.x_hist_bins_count); } if (y_hist_bins) { bin_up(y_hist_bins, ptr->h, y_hist_bins_max, &lnk_blob.y_hist_bins, &lnk_blob.y_hist_bins_count); } bool add_to_list = threshold_cb_arg == NULL; if (!add_to_list) { // Protect ourselves from caught exceptions in the callback // code from freeing our fb_alloc() stack. fb_alloc_mark(); fb_alloc_mark_permanent(); add_to_list = threshold_cb(threshold_cb_arg, &lnk_blob); fb_alloc_free_till_mark_past_mark_permanent(); } if (add_to_list) { list_push_back(out, &lnk_blob); } else { if (lnk_blob.x_hist_bins) { m_free(lnk_blob.x_hist_bins); } if (lnk_blob.y_hist_bins) { m_free(lnk_blob.y_hist_bins); } } } x = old_x; y = old_y; } } } break; } default: { break; } } code += 1; } lifo_free(&lifo); if (y_hist_bins) { fb_free(); } if (x_hist_bins) { fb_free(); } fb_free(); // bitmap if (merge) { for (;;) { bool merge_occured = false; list_t out_temp; list_init(&out_temp, sizeof(find_blobs_list_lnk_data_t)); while (list_size(out)) { find_blobs_list_lnk_data_t lnk_blob; list_pop_front(out, &lnk_blob); for (size_t k = 0, l = list_size(out); k < l; k++) { find_blobs_list_lnk_data_t tmp_blob; list_pop_front(out, &tmp_blob); rectangle_t temp; temp.x = __SSAT(tmp_blob.rect.x - margin, 16); temp.y = __SSAT(tmp_blob.rect.y - margin, 16); temp.w = __USAT(tmp_blob.rect.w + (margin * 2), 15); temp.h = __USAT(tmp_blob.rect.h + (margin * 2), 15); if (rectangle_overlap(&(lnk_blob.rect), &temp) && ((merge_cb_arg == NULL) || merge_cb(merge_cb_arg, &lnk_blob, &tmp_blob))) { // Have to merge these first before merging rects. if (x_hist_bins_max) { merge_bins(lnk_blob.rect.x, lnk_blob.rect.x + lnk_blob.rect.w - 1, &lnk_blob.x_hist_bins, &lnk_blob.x_hist_bins_count, tmp_blob.rect.x, tmp_blob.rect.x + tmp_blob.rect.w - 1, &tmp_blob.x_hist_bins, &tmp_blob.x_hist_bins_count, x_hist_bins_max); } if (y_hist_bins_max) { merge_bins(lnk_blob.rect.y, lnk_blob.rect.y + lnk_blob.rect.h - 1, &lnk_blob.y_hist_bins, &lnk_blob.y_hist_bins_count, tmp_blob.rect.y, tmp_blob.rect.y + tmp_blob.rect.h - 1, &tmp_blob.y_hist_bins, &tmp_blob.y_hist_bins_count, y_hist_bins_max); } // Merge corners... for (int i = 0; i < FIND_BLOBS_CORNERS_RESOLUTION; i++) { float z_dst = (lnk_blob.corners[i].x * cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]) + (lnk_blob.corners[i].y * sin_table[FIND_BLOBS_ANGLE_RESOLUTION * i]); float z_src = (tmp_blob.corners[i].x * cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]) + (tmp_blob.corners[i].y * cos_table[FIND_BLOBS_ANGLE_RESOLUTION * i]); if (z_src < z_dst) { lnk_blob.corners[i].x = tmp_blob.corners[i].x; lnk_blob.corners[i].y = tmp_blob.corners[i].y; } } // Merge rects... rectangle_united(&(lnk_blob.rect), &(tmp_blob.rect)); // Merge counters... lnk_blob.pixels += tmp_blob.pixels; // won't overflow lnk_blob.perimeter += tmp_blob.perimeter; // won't overflow lnk_blob.code |= tmp_blob.code; // won't overflow lnk_blob.count += tmp_blob.count; // won't overflow // Merge accumulators... lnk_blob.centroid_x_acc += tmp_blob.centroid_x_acc; lnk_blob.centroid_y_acc += tmp_blob.centroid_y_acc; lnk_blob.rotation_acc_x += tmp_blob.rotation_acc_x; lnk_blob.rotation_acc_y += tmp_blob.rotation_acc_y; lnk_blob.roundness_acc += tmp_blob.roundness_acc; // Compute current values... lnk_blob.centroid_x = lnk_blob.centroid_x_acc / lnk_blob.pixels; lnk_blob.centroid_y = lnk_blob.centroid_y_acc / lnk_blob.pixels; lnk_blob.rotation = fast_atan2f(lnk_blob.rotation_acc_y / lnk_blob.pixels, lnk_blob.rotation_acc_x / lnk_blob.pixels); lnk_blob.roundness = lnk_blob.roundness_acc / lnk_blob.pixels; merge_occured = true; } else { list_push_back(out, &tmp_blob); } } list_push_back(&out_temp, &lnk_blob); } list_copy(out, &out_temp); if (!merge_occured) { break; } } } } void imlib_flood_fill_int(image_t *out, image_t *img, int x, int y, int seed_threshold, int floating_threshold, flood_fill_call_back_t cb, void *data) { lifo_t lifo; size_t lifo_len; lifo_alloc_all(&lifo, &lifo_len, sizeof(xylr_t)); switch (img->pixfmt) { case PIXFORMAT_BINARY: { for (int seed_pixel = IMAGE_GET_BINARY_PIXEL(img, x, y);;) { int left = x, right = x; uint32_t *row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(img, y); uint32_t *out_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(out, y); while ((left > 0) && (!IMAGE_GET_BINARY_PIXEL_FAST(out_row, left - 1)) && COLOR_BOUND_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, left - 1), seed_pixel, seed_threshold) && COLOR_BOUND_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, left - 1), IMAGE_GET_BINARY_PIXEL_FAST(row, left), floating_threshold)) { left--; } while ((right < (img->w - 1)) && (!IMAGE_GET_BINARY_PIXEL_FAST(out_row, right + 1)) && COLOR_BOUND_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, right + 1), seed_pixel, seed_threshold) && COLOR_BOUND_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, right + 1), IMAGE_GET_BINARY_PIXEL_FAST(row, right), floating_threshold)) { right++; } for (int i = left; i <= right; i++) { IMAGE_SET_BINARY_PIXEL_FAST(out_row, i); } int top_left = left; int bot_left = left; bool break_out = false; for (;;) { if (lifo_size(&lifo) < lifo_len) { uint32_t *old_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(img, y); if (y > 0) { row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(img, y - 1); out_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(out, y - 1); bool recurse = false; for (int i = top_left; i <= right; i++) { if ((!IMAGE_GET_BINARY_PIXEL_FAST(out_row, i)) && COLOR_BOUND_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, i), seed_pixel, seed_threshold) && COLOR_BOUND_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, i), IMAGE_GET_BINARY_PIXEL_FAST(old_row, i), floating_threshold)) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = i + 1; // Don't test the same pixel again... context.b_l = bot_left; lifo_enqueue(&lifo, &context); x = i; y = y - 1; recurse = true; break; } } if (recurse) { break; } } if (y < (img->h - 1)) { row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(img, y + 1); out_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(out, y + 1); bool recurse = false; for (int i = bot_left; i <= right; i++) { if ((!IMAGE_GET_BINARY_PIXEL_FAST(out_row, i)) && COLOR_BOUND_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, i), seed_pixel, seed_threshold) && COLOR_BOUND_BINARY(IMAGE_GET_BINARY_PIXEL_FAST(row, i), IMAGE_GET_BINARY_PIXEL_FAST(old_row, i), floating_threshold)) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = top_left; context.b_l = i + 1; // Don't test the same pixel again... lifo_enqueue(&lifo, &context); x = i; y = y + 1; recurse = true; break; } } if (recurse) { break; } } } if (cb) { cb(img, y, left, right, data); } if (!lifo_size(&lifo)) { break_out = true; break; } xylr_t context; lifo_dequeue(&lifo, &context); x = context.x; y = context.y; left = context.l; right = context.r; top_left = context.t_l; bot_left = context.b_l; } if (break_out) { break; } } break; } case PIXFORMAT_GRAYSCALE: { for (int seed_pixel = IMAGE_GET_GRAYSCALE_PIXEL(img, x, y);;) { int left = x, right = x; uint8_t *row = IMAGE_COMPUTE_GRAYSCALE_PIXEL_ROW_PTR(img, y); uint32_t *out_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(out, y); while ((left > 0) && (!IMAGE_GET_BINARY_PIXEL_FAST(out_row, left - 1)) && COLOR_BOUND_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, left - 1), seed_pixel, seed_threshold) && COLOR_BOUND_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, left - 1), IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, left), floating_threshold)) { left--; } while ((right < (img->w - 1)) && (!IMAGE_GET_BINARY_PIXEL_FAST(out_row, right + 1)) && COLOR_BOUND_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, right + 1), seed_pixel, seed_threshold) && COLOR_BOUND_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, right + 1), IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, right), floating_threshold)) { right++; } for (int i = left; i <= right; i++) { IMAGE_SET_BINARY_PIXEL_FAST(out_row, i); } int top_left = left; int bot_left = left; bool break_out = false; for (;;) { if (lifo_size(&lifo) < lifo_len) { uint8_t *old_row = IMAGE_COMPUTE_GRAYSCALE_PIXEL_ROW_PTR(img, y); if (y > 0) { row = IMAGE_COMPUTE_GRAYSCALE_PIXEL_ROW_PTR(img, y - 1); out_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(out, y - 1); bool recurse = false; for (int i = top_left; i <= right; i++) { if ((!IMAGE_GET_BINARY_PIXEL_FAST(out_row, i)) && COLOR_BOUND_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, i), seed_pixel, seed_threshold) && COLOR_BOUND_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, i), IMAGE_GET_GRAYSCALE_PIXEL_FAST(old_row, i), floating_threshold)) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = i + 1; // Don't test the same pixel again... context.b_l = bot_left; lifo_enqueue(&lifo, &context); x = i; y = y - 1; recurse = true; break; } } if (recurse) { break; } } if (y < (img->h - 1)) { row = IMAGE_COMPUTE_GRAYSCALE_PIXEL_ROW_PTR(img, y + 1); out_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(out, y + 1); bool recurse = false; for (int i = bot_left; i <= right; i++) { if ((!IMAGE_GET_BINARY_PIXEL_FAST(out_row, i)) && COLOR_BOUND_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, i), seed_pixel, seed_threshold) && COLOR_BOUND_GRAYSCALE(IMAGE_GET_GRAYSCALE_PIXEL_FAST(row, i), IMAGE_GET_GRAYSCALE_PIXEL_FAST(old_row, i), floating_threshold)) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = top_left; context.b_l = i + 1; // Don't test the same pixel again... lifo_enqueue(&lifo, &context); x = i; y = y + 1; recurse = true; break; } } if (recurse) { break; } } } if (cb) { cb(img, y, left, right, data); } if (!lifo_size(&lifo)) { break_out = true; break; } xylr_t context; lifo_dequeue(&lifo, &context); x = context.x; y = context.y; left = context.l; right = context.r; top_left = context.t_l; bot_left = context.b_l; } if (break_out) { break; } } break; } case PIXFORMAT_RGB565: { for (int seed_pixel = IMAGE_GET_RGB565_PIXEL(img, x, y);;) { int left = x, right = x; uint16_t *row = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(img, y); uint32_t *out_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(out, y); while ((left > 0) && (!IMAGE_GET_BINARY_PIXEL_FAST(out_row, left - 1)) && COLOR_BOUND_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, left - 1), seed_pixel, seed_threshold) && COLOR_BOUND_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, left - 1), IMAGE_GET_RGB565_PIXEL_FAST(row, left), floating_threshold)) { left--; } while ((right < (img->w - 1)) && (!IMAGE_GET_BINARY_PIXEL_FAST(out_row, right + 1)) && COLOR_BOUND_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, right + 1), seed_pixel, seed_threshold) && COLOR_BOUND_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, right + 1), IMAGE_GET_RGB565_PIXEL_FAST(row, right), floating_threshold)) { right++; } for (int i = left; i <= right; i++) { IMAGE_SET_BINARY_PIXEL_FAST(out_row, i); } int top_left = left; int bot_left = left; bool break_out = false; for (;;) { if (lifo_size(&lifo) < lifo_len) { uint16_t *old_row = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(img, y); if (y > 0) { row = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(img, y - 1); out_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(out, y - 1); bool recurse = false; for (int i = top_left; i <= right; i++) { if ((!IMAGE_GET_BINARY_PIXEL_FAST(out_row, i)) && COLOR_BOUND_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, i), seed_pixel, seed_threshold) && COLOR_BOUND_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, i), IMAGE_GET_RGB565_PIXEL_FAST(old_row, i), floating_threshold)) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = i + 1; // Don't test the same pixel again... context.b_l = bot_left; lifo_enqueue(&lifo, &context); x = i; y = y - 1; recurse = true; break; } } if (recurse) { break; } } if (y < (img->h - 1)) { row = IMAGE_COMPUTE_RGB565_PIXEL_ROW_PTR(img, y + 1); out_row = IMAGE_COMPUTE_BINARY_PIXEL_ROW_PTR(out, y + 1); bool recurse = false; for (int i = bot_left; i <= right; i++) { if ((!IMAGE_GET_BINARY_PIXEL_FAST(out_row, i)) && COLOR_BOUND_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, i), seed_pixel, seed_threshold) && COLOR_BOUND_RGB565(IMAGE_GET_RGB565_PIXEL_FAST(row, i), IMAGE_GET_RGB565_PIXEL_FAST(old_row, i), floating_threshold)) { xylr_t context; context.x = x; context.y = y; context.l = left; context.r = right; context.t_l = top_left; context.b_l = i + 1; // Don't test the same pixel again... lifo_enqueue(&lifo, &context); x = i; y = y + 1; recurse = true; break; } } if (recurse) { break; } } } if (cb) { cb(img, y, left, right, data); } if (!lifo_size(&lifo)) { break_out = true; break; } xylr_t context; lifo_dequeue(&lifo, &context); x = context.x; y = context.y; left = context.l; right = context.r; top_left = context.t_l; bot_left = context.b_l; } if (break_out) { break; } } break; } default: { break; } } lifo_free(&lifo); }