#include #include #include #include "nopencv.hpp" #define STB_IMAGE_IMPLEMENTATION #include #define STB_IMAGE_RESIZE_IMPLEMENTATION #include #define IIR_GAUSS_BLUR_IMPLEMENTATION #include "iir_gauss_blur.h" template void iir_gauss_blur(unsigned int width, unsigned int height, unsigned char components, uint8_t* image, float sigma); template void iir_gauss_blur (unsigned int width, unsigned int height, unsigned char components, uint32_t* image, float sigma); template void iir_gauss_blur (unsigned int width, unsigned int height, unsigned char components, float* image, float sigma); using namespace gerbolyze; using namespace gerbolyze::nopencv; static constexpr bool debug = false; /* directions: * 0 * 7 1 * ^ * | * 6 <--- X ---> 2 * | * v * 5 3 * 4 * */ enum Direction { D_N, D_NE, D_E, D_SE, D_S, D_SW, D_W, D_NW }; const char * const dir_str[8] = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" }; static struct { int x; int y; } dir_to_coords[8] = {{0, -1}, {1, -1}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}}; static Direction flip_direction[8] = { D_S, /* 0 */ D_SW, /* 1 */ D_W, /* 2 */ D_NW, /* 3 */ D_N, /* 4 */ D_NE, /* 5 */ D_E, /* 6 */ D_SE /* 7 */ }; static void follow(gerbolyze::nopencv::Image32 &img, int start_x, int start_y, Direction initial_direction, int nbd, int connectivity, Polygon_i &poly) { if (debug) { cerr << "follow " << start_x << " " << start_y << " | dir=" << dir_str[initial_direction] << " nbd=" << nbd << " conn=" << connectivity << endl; } int dir_inc = (connectivity == 4) ? 2 : 1; int probe_x, probe_y; /* homing run: find starting point for algorithm steps below. */ bool found = false; int k; for (k=initial_direction; k= current_direction; k -= dir_inc) { probe_x = center_x + dir_to_coords[k % 8].x; probe_y = center_y + dir_to_coords[k % 8].y; if (k%8 == D_E) flag = true; if (img.at_default(probe_x, probe_y) != 0) { break; } } int set_val = 0; if (flag && img.at_default(center_x+1, center_y) == 0) { img.at(center_x, center_y) = -nbd; set_val = -nbd; } else if (img.at(center_x, center_y) == 1) { img.at(center_x, center_y) = nbd; set_val = nbd; } for (int l = (current_direction + 8 - 2 + 1) / 2 * 2; l > k; l -= dir_inc) { switch (l%8) { case 0: poly.emplace_back(i2p{center_x, center_y}); break; case 2: poly.emplace_back(i2p{center_x+1, center_y}); break; case 4: poly.emplace_back(i2p{center_x+1, center_y+1}); break; case 6: poly.emplace_back(i2p{center_x, center_y+1}); break; } } center_x = probe_x; center_y = probe_y; current_direction = flip_direction[k % 8]; if (debug) { cerr << " " << center_x << " " << center_y << " / " << dir_str[current_direction] << " -> " << set_val << endl; } } while (center_x != start_x || center_y != start_y || current_direction != start_direction); } void gerbolyze::nopencv::find_contours(gerbolyze::nopencv::Image32 &img, gerbolyze::nopencv::ContourCallback cb) { /* Implementation of the hierarchical contour finding algorithm from Suzuki and Abe, 1983: Topological Structural * Analysis of Digitized Binary Images by Border Following * * Written with these two resources as reference: * https://theailearner.com/tag/suzuki-contour-algorithm-opencv/ * https://github.com/FreshJesh5/Suzuki-Algorithm/blob/master/contoursv1/contoursv1.cpp * * WARNING: input image MUST BE BINARIZE: All pixels must have value either 0 or 1. Otherwise, chaos ensues. */ int nbd = 1; Polygon_i poly; for (int y=0; y= 1 && img.at_default(x+1, y) == 0) { /* hole border starting point */ nbd += 1; follow(img, x, y, D_E, nbd, 8, poly); /* FIXME should be 4? */ cb(poly, CP_HOLE); poly.clear(); } } } } static size_t region_of_support(Polygon_i poly, size_t i) { double x0 = poly[i][0], y0 = poly[i][1]; size_t sz = poly.size(); double last_l = 0; double last_r = 0; size_t k; //cerr << "d: "; for (k=1; k<(sz+1)/2; k++) { size_t idx1 = (i + k) % sz; size_t idx2 = (i + sz - k) % sz; double x1 = poly[idx1][0], y1 = poly[idx1][1], x2 = poly[idx2][0], y2 = poly[idx2][1]; double l = sqrt(pow(x2-x1, 2) + pow(y2-y1, 2)); /* https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line * TODO: Check whether distance-to-line is an ok implementation here, the paper asks for distance to chord. */ double d = ((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / sqrt(pow(x2-x1, 2) + pow(y2-y1, 2)); //cerr << d << " "; double r = d/l; bool cond_a = l < last_l; bool cond_b = ((d > 0) && (r < last_r)) || ((d < 0) && (r > last_r)); if (k > 2 && (cond_a || cond_b)) break; last_l = l; last_r = r; } //cerr << endl; k -= 1; return k; } int freeman_angle(const Polygon_i &poly, size_t i) { /* f: * 2 * 3 1 * ^ * | * 4 <--- X ---> 0 * | * v * 5 7 * 6 * */ size_t sz = poly.size(); auto &p_last = poly[(i + sz - 1) % sz]; auto &p_now = poly[i]; auto dx = p_now[0] - p_last[0]; auto dy = p_now[1] - p_last[1]; /* both points must be neighbors */ assert (-1 <= dx && dx <= 1); assert (-1 <= dy && dy <= 1); assert (!(dx == 0 && dy == 0)); int lut[3][3] = {{3, 2, 1}, {4, -1, 0}, {5, 6, 7}}; return lut[dy+1][dx+1]; } double k_curvature(const Polygon_i &poly, size_t i, size_t k) { size_t sz = poly.size(); double acc = 0; for (size_t idx = 0; idx < k; idx++) { acc += freeman_angle(poly, (i + 2*sz - idx) % sz) - freeman_angle(poly, (i+idx + 1) % sz); } return acc / k; } double k_cos(const Polygon_i &poly, size_t i, size_t k) { size_t sz = poly.size(); int64_t x0 = poly[i][0], y0 = poly[i][1]; int64_t x1 = poly[(i + sz + k) % sz][0], y1 = poly[(i + sz + k) % sz][1]; int64_t x2 = poly[(i + sz - k) % sz][0], y2 = poly[(i + sz - k) % sz][1]; auto xa = x0 - x1, ya = y0 - y1; auto xb = x0 - x2, yb = y0 - y2; auto dp = xa*yb + ya*xb; auto sq_a = xa*xa + ya*ya; auto sq_b = xb*xb + yb*yb; return dp / (sqrt(sq_a)*sqrt(sq_b)); } ContourCallback gerbolyze::nopencv::simplify_contours_teh_chin(ContourCallback cb) { return [&cb](Polygon_i &poly, ContourPolarity cpol) { size_t sz = poly.size(); vector ros(sz); vector sig(sz); vector cur(sz); vector retain(sz); for (size_t i=0; i dp_step(Polygon_i &poly, size_t a, size_t b) { double dx = poly[b][0] - poly[a][0]; double dy = poly[b][1] - poly[a][1]; double eps = dp_eps(dx, dy); size_t max_idx = 0; double max_dist = 0; /* https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line */ double dist_ab = sqrt(pow(poly[b][0] - poly[a][0], 2) + pow(poly[b][1] - poly[a][1], 2)); for (size_t i=a+1; i max_dist && dist_i > eps) { max_dist = dist_i; max_idx = i; } } return {a, max_idx, b}; } ContourCallback gerbolyze::nopencv::simplify_contours_douglas_peucker(ContourCallback cb) { return [&cb](Polygon_i &poly, ContourPolarity cpol) { Polygon_i out; out.push_back(poly[0]); stack> indices; indices.push(dp_step(poly, 0, poly.size()-1)); while (!indices.empty()) { auto idx = indices.top(); indices.pop(); /* awesome C++ api let's goooooo */ if (idx[1] > 0) { indices.push(dp_step(poly, idx[0], idx[1])); indices.push(dp_step(poly, idx[1], idx[2])); } else { out.push_back(poly[idx[2]]); } } cb(out, cpol); }; } double gerbolyze::nopencv::polygon_area(Polygon_i &poly) { double acc = 0; size_t prev = poly.size() - 1; for (size_t cur=0; cur gerbolyze::nopencv::Image::Image(int size_x, int size_y, const T *data) { assert(size_x > 0 && size_x < 100000); assert(size_y > 0 && size_y < 100000); m_data = new T[size_x * size_y] { 0 }; m_rows = size_y; m_cols = size_x; if (data != nullptr) { memcpy(m_data, data, sizeof(T) * size_x * size_y); } } template bool gerbolyze::nopencv::Image::load(const char *filename) { return stb_to_internal(stbi_load(filename, &m_cols, &m_rows, nullptr, 1)); } template bool gerbolyze::nopencv::Image::load_memory(const void *buf, size_t len) { return stb_to_internal(stbi_load_from_memory(reinterpret_cast(buf), len, &m_cols, &m_rows, nullptr, 1)); } template void gerbolyze::nopencv::Image::binarize(T threshold) { assert(m_data != nullptr); assert(m_rows > 0 && m_cols > 0); for (int y=0; y= threshold; } } } template bool gerbolyze::nopencv::Image::stb_to_internal(uint8_t *data) { if (data == nullptr) return false; if (m_rows < 0 || m_rows > 100000) return false; if (m_cols < 0 || m_cols > 100000) return false; m_data = new T[size()] { 0 }; for (int y=0; y void gerbolyze::nopencv::Image::blur(int radius) { iir_gauss_blur(m_cols, m_rows, 1, m_data, radius/2.0); } template<> void gerbolyze::nopencv::Image::resize(int new_w, int new_h) { float *old_data = m_data; m_data = new float[new_w * new_h]; stbir_resize_float_linear(old_data, m_cols, m_rows, 0, m_data, new_w, new_h, 0, STBIR_1CHANNEL); m_cols = new_w; m_rows = new_h; delete old_data; } template<> void gerbolyze::nopencv::Image::resize(int new_w, int new_h) { uint8_t *old_data = m_data; m_data = new uint8_t[new_w * new_h]; stbir_resize_uint8_linear(old_data, m_cols, m_rows, 0, m_data, new_w, new_h, 0, STBIR_1CHANNEL); m_cols = new_w; m_rows = new_h; delete old_data; } template gerbolyze::nopencv::Image::Image(int size_x, int size_y, const int32_t *data); template bool gerbolyze::nopencv::Image::load(const char *filename); template bool gerbolyze::nopencv::Image::load_memory(const void *buf, size_t len); template void gerbolyze::nopencv::Image::binarize(int32_t threshold); template bool gerbolyze::nopencv::Image::stb_to_internal(uint8_t *data); template void gerbolyze::nopencv::Image::blur(int radius); template gerbolyze::nopencv::Image::Image(int size_x, int size_y, const uint8_t *data); template bool gerbolyze::nopencv::Image::load(const char *filename); template bool gerbolyze::nopencv::Image::load_memory(const void *buf, size_t len); template void gerbolyze::nopencv::Image::binarize(uint8_t threshold); template bool gerbolyze::nopencv::Image::stb_to_internal(uint8_t *data); template void gerbolyze::nopencv::Image::blur(int radius); template gerbolyze::nopencv::Image::Image(int size_x, int size_y, const float *data); template bool gerbolyze::nopencv::Image::load(const char *filename); template bool gerbolyze::nopencv::Image::load_memory(const void *buf, size_t len); template void gerbolyze::nopencv::Image::binarize(float threshold); template bool gerbolyze::nopencv::Image::stb_to_internal(uint8_t *data); template void gerbolyze::nopencv::Image::blur(int radius);