#include #include #include #include #include #include "util.h" #include "nopencv.hpp" #include "geom2d.hpp" #include #include #include "stb_image.h" using namespace gerbolyze; using namespace gerbolyze::nopencv; char msg[512]; class TempfileHack { public: TempfileHack(const string ext) : m_path { filesystem::temp_directory_path() / (std::tmpnam(nullptr) + ext) } {} ~TempfileHack() { remove(m_path); } const char *c_str() { return m_path.c_str(); } private: filesystem::path m_path; }; class SVGPolyRenderer { public: SVGPolyRenderer(const char *fn, int width_px, int height_px) : m_svg(fn) { m_svg << "" << endl; m_svg << "" << endl; } ContourCallback callback() { return [this](Polygon_i &poly, ContourPolarity pol) { mu_assert(poly.size() > 0, "Empty contour returned"); mu_assert(poly.size() > 2, "Contour has less than three points, no area"); mu_assert(pol == CP_CONTOUR || pol == CP_HOLE, "Contour has invalid polarity"); m_svg << "" << endl; }; } void close() { m_svg << "" << endl; m_svg.close(); } private: ofstream m_svg; }; MU_TEST(test_complex_example_from_paper) { int32_t img_data[6*9] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; Image32 test_img(9, 6, static_cast(img_data)); const Polygon_i expected_polys[3] = { { {1,1}, {1,2}, {1,3}, {1,4}, {1,5}, {2,5}, {3,5}, {4,5}, {5,5}, {6,5}, {7,5}, {8,5}, {8,4}, {8,3}, {8,2}, {8,1}, {7,1}, {6,1}, {5,1}, {4,1}, {3,1}, {2,1} }, { {2,2}, {2,3}, {2,4}, {3,4}, {4,4}, {4,3}, {4,2}, {3,2} }, { {5,2}, {5,3}, {5,4}, {6,4}, {7,4}, {7,3}, {7,2}, {6,2} } }; const ContourPolarity expected_polarities[3] = {CP_CONTOUR, CP_HOLE, CP_HOLE}; int invocation_count = 0; gerbolyze::nopencv::find_contours(test_img, [&invocation_count, &expected_polarities, &expected_polys](Polygon_i &poly, ContourPolarity pol) { invocation_count += 1; mu_assert((invocation_count <= 3), "Too many contours returned"); mu_assert(poly.size() > 0, "Empty contour returned"); mu_assert_int_eq(pol, expected_polarities[invocation_count-1]); i2p last; bool first = true; Polygon_i exp = expected_polys[invocation_count-1]; for (auto &p : poly) { if (!first) { mu_assert((fabs(p[0] - last[0]) + fabs(p[1] - last[1]) == 1), "Subsequent contour points have distance other than one"); mu_assert(find(exp.begin(), exp.end(), p) != exp.end(), "Got unexpected contour point"); } last = p; } }); mu_assert_int_eq(3, invocation_count); int32_t tpl[6*9] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2,-2, 0, 0,-3, 0, 0,-4, 0, 0,-2, 0, 0,-3, 0, 0,-4, 0, 0,-2, 0, 0, 2, 2, 2, 2, 2, 2,-2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; for (int y=0; y<6; y++) { for (int x=0; x<9; x++) { int a = test_img.at(x, y), b = tpl[y*9+x]; if (a != b) { cout << "Result:" << endl; cout << " "; for (int x=0; x<9; x++) { cout << x << " "; } cout << endl; cout << " "; for (int x=0; x<9; x++) { cout << "---"; } cout << endl; for (int y=0; y<6; y++) { cout << y << " | "; for (int x=0; x<9; x++) { cout << setfill(' ') << setw(2) << test_img.at(x, y) << " "; } cout << endl; } snprintf(msg, sizeof(msg), "Result does not match template @(%d, %d): %d != %d\n", x, y, a, b); mu_fail(msg); } } } } int render_svg(const char *in_svg, const char *out_png) { vector command_line = {in_svg, out_png}; return run_cargo_command("resvg", command_line, "RESVG"); } static void testdata_roundtrip(const char *fn) { Image32 ref_img; mu_assert(ref_img.load(fn), "Input image failed to load"); ref_img.binarize(128); Image32 ref_img_copy(ref_img); TempfileHack tmp_svg(".svg"); TempfileHack tmp_png(".png"); SVGPolyRenderer ctx(tmp_svg.c_str(), ref_img.cols(), ref_img.rows()); gerbolyze::nopencv::find_contours(ref_img, ctx.callback()); ctx.close(); mu_assert_int_eq(0, render_svg(tmp_svg.c_str(), tmp_png.c_str())); Image32 out_img; mu_assert(out_img.load(tmp_png.c_str()), "Output image failed to load"); out_img.binarize(128); mu_assert_int_eq(ref_img.cols(), out_img.cols()); mu_assert_int_eq(ref_img.rows(), out_img.rows()); for (int y=0; y 0.99, "Polygon smaller than a single pixel"); mu_assert((pol == CP_CONTOUR) == (area >= 0), "Polygon area has incorrect sign"); pos_sum += area; neg_sum -= area; }); mu_assert(pos_sum - white_px_count < 0.01, "Calculated area outside tolerance"); mu_assert(neg_sum - black_px_count < 0.01, "Calculated area outside tolerance"); //cerr << endl << "poly area test " << fn << " done" << endl; } MU_TEST(test_polygon_area_blank) { test_polygon_area("testdata/blank.png"); } MU_TEST(test_polygon_area_white) { test_polygon_area("testdata/white.png"); } MU_TEST(test_polygon_area_blob_border_w) { test_polygon_area("testdata/blob-border-w.png"); } MU_TEST(test_polygon_area_blobs_borders) { test_polygon_area("testdata/blobs-borders.png"); } MU_TEST(test_polygon_area_blobs_corners) { test_polygon_area("testdata/blobs-corners.png"); } MU_TEST(test_polygon_area_blobs_crossing) { test_polygon_area("testdata/blobs-crossing.png"); } MU_TEST(test_polygon_area_cross) { test_polygon_area("testdata/cross.png"); } MU_TEST(test_polygon_area_letter_e) { test_polygon_area("testdata/letter-e.png"); } MU_TEST(test_polygon_area_paper_example) { test_polygon_area("testdata/paper-example.png"); } MU_TEST(test_polygon_area_paper_example_inv) { test_polygon_area("testdata/paper-example-inv.png"); } MU_TEST(test_polygon_area_single_px) { test_polygon_area("testdata/single-px.png"); } MU_TEST(test_polygon_area_single_px_inv) { test_polygon_area("testdata/single-px-inv.png"); } MU_TEST(test_polygon_area_two_blobs) { test_polygon_area("testdata/two-blobs.png"); } MU_TEST(test_polygon_area_two_px) { test_polygon_area("testdata/two-px.png"); } MU_TEST(test_polygon_area_two_px_inv) { test_polygon_area("testdata/two-px-inv.png"); } MU_TEST(test_polygon_area_contour_tracing_demo_input) { test_polygon_area("testdata/contour_tracing_demo_input.png"); } static void chain_approx_test(const char *fn) { //cout << endl << "Testing \"" << fn << "\"" << endl; Image32 ref_img; mu_assert(ref_img.load(fn), "Input image failed to load"); ref_img.binarize(128); Image32 ref_img_copy(ref_img); TempfileHack tmp_svg(".svg"); TempfileHack tmp_png(".png"); SVGPolyRenderer ctx(tmp_svg.c_str(), ref_img.cols(), ref_img.rows()); gerbolyze::nopencv::find_contours(ref_img, simplify_contours_teh_chin(ctx.callback())); ctx.close(); mu_assert_int_eq(0, render_svg(tmp_svg.c_str(), tmp_png.c_str())); Image32 out_img; mu_assert(out_img.load(tmp_png.c_str()), "Output image failed to load"); mu_assert_int_eq(ref_img.rows(), out_img.rows()); mu_assert_int_eq(ref_img.cols(), out_img.cols()); double max_abs_deviation = 0; double rms_sum = 0; double mean_sum = 0; for (int y=0; y 0.5) { snprintf(msg, sizeof(msg), "%s: Chain approximation RMS error is above threshold: %.3f > 0.5\n", fn, rms_sum); mu_fail(msg); } if (mean_sum > 0.1) { snprintf(msg, sizeof(msg), "%s: Chain approximation mean error is above threshold: %.3f > 0.1\n", fn, mean_sum); mu_fail(msg); } //mu_assert(max_abs_deviation < 0.5, "Maximum chain approximation error is above threshold"); } MU_TEST(chain_approx_test_chromosome) { chain_approx_test("testdata/chain-approx-teh-chin-chromosome.png"); } MU_TEST(chain_approx_test_blank) { chain_approx_test("testdata/blank.png"); } MU_TEST(chain_approx_test_white) { chain_approx_test("testdata/white.png"); } MU_TEST(chain_approx_test_blob_border_w) { chain_approx_test("testdata/blob-border-w.png"); } MU_TEST(chain_approx_test_blobs_borders) { chain_approx_test("testdata/blobs-borders.png"); } MU_TEST(chain_approx_test_blobs_corners) { chain_approx_test("testdata/blobs-corners.png"); } MU_TEST(chain_approx_test_blobs_crossing) { chain_approx_test("testdata/blobs-crossing.png"); } MU_TEST(chain_approx_test_cross) { chain_approx_test("testdata/cross.png"); } MU_TEST(chain_approx_test_letter_e) { chain_approx_test("testdata/letter-e.png"); } MU_TEST(chain_approx_test_paper_example) { chain_approx_test("testdata/paper-example.png"); } MU_TEST(chain_approx_test_paper_example_inv) { chain_approx_test("testdata/paper-example-inv.png"); } MU_TEST(chain_approx_test_single_px) { chain_approx_test("testdata/single-px.png"); } MU_TEST(chain_approx_test_single_px_inv) { chain_approx_test("testdata/single-px-inv.png"); } MU_TEST(chain_approx_test_two_blobs) { chain_approx_test("testdata/two-blobs.png"); } MU_TEST(chain_approx_test_two_px) { chain_approx_test("testdata/two-px.png"); } MU_TEST(chain_approx_test_two_px_inv) { chain_approx_test("testdata/two-px-inv.png"); } MU_TEST(chain_approx_test_contour_tracing_demo_input) { chain_approx_test("testdata/contour_tracing_demo_input.png"); } MU_TEST(test_transform_decomposition) { double scales[] = {0.1, 0.5, 0.9, 0.999999999, 1.0, 1.000000001, 1.1, 1.5, 2.0, 1000}; double ms[] = {0, -5.0, -1.0, -0.1, 0.1, 0.5, 1.0, 2.0, 5.0, 6.123, 100.0}; for (double &s_x : scales) { for (double &s_y : scales) { for (int s_y_sign=0; s_y_sign<2; s_y_sign++) { double s_y_i = s_y_sign ? -s_y : s_y; for (int i_theta=0; i_theta<25; i_theta++) { double theta = i_theta * std::numbers::pi / 12.0; for (double &m : ms) { xform2d xf; //cerr << endl << "testing s_x=" << s_x << ", s_y=" << s_y_i << ", m=" << m << ", theta=" << theta << endl; xf.rotate(theta).skew(m).scale(s_x, s_y_i); //cerr << " -> " << xf.dbg_str() << endl; const auto [dec_s_x, dec_s_y, dec_m, dec_theta] = xf.decompose(); mu_assert(fabs(s_x - dec_s_x) < 1e-9, "s_x incorrect"); mu_assert(fabs(s_y_i - dec_s_y) < 1e-9, "s_y incorrect"); mu_assert(fabs(m - dec_m) < 1e-9, "m incorrect"); double a = dec_theta - theta + std::numbers::pi; mu_assert(fabs(a - floor(a/(2*std::numbers::pi)) * 2 * std::numbers::pi - std::numbers::pi) < 1e-12, "theta incorrect"); } } } } } } MU_TEST_SUITE(nopencv_contours_suite) { MU_RUN_TEST(test_complex_example_from_paper); MU_RUN_TEST(test_round_trip_blank); MU_RUN_TEST(test_round_trip_white); MU_RUN_TEST(test_round_trip_blob_border_w); MU_RUN_TEST(test_round_trip_blobs_borders); MU_RUN_TEST(test_round_trip_blobs_corners); MU_RUN_TEST(test_round_trip_blobs_crossing); MU_RUN_TEST(test_round_trip_cross); MU_RUN_TEST(test_round_trip_letter_e); MU_RUN_TEST(test_round_trip_paper_example); MU_RUN_TEST(test_round_trip_paper_example_inv); MU_RUN_TEST(test_round_trip_single_px); MU_RUN_TEST(test_round_trip_single_px_inv); MU_RUN_TEST(test_round_trip_two_blobs); MU_RUN_TEST(test_round_trip_two_px); MU_RUN_TEST(test_round_trip_two_px_inv); MU_RUN_TEST(test_round_trip_contour_tracing_demo_input); MU_RUN_TEST(test_polygon_area_blank); MU_RUN_TEST(test_polygon_area_white); MU_RUN_TEST(test_polygon_area_blob_border_w); MU_RUN_TEST(test_polygon_area_blobs_borders); MU_RUN_TEST(test_polygon_area_blobs_corners); MU_RUN_TEST(test_polygon_area_blobs_crossing); MU_RUN_TEST(test_polygon_area_cross); MU_RUN_TEST(test_polygon_area_letter_e); MU_RUN_TEST(test_polygon_area_paper_example); MU_RUN_TEST(test_polygon_area_paper_example_inv); MU_RUN_TEST(test_polygon_area_single_px); MU_RUN_TEST(test_polygon_area_single_px_inv); MU_RUN_TEST(test_polygon_area_two_blobs); MU_RUN_TEST(test_polygon_area_two_px); MU_RUN_TEST(test_polygon_area_two_px_inv); MU_RUN_TEST(test_polygon_area_contour_tracing_demo_input); MU_RUN_TEST(chain_approx_test_chromosome); MU_RUN_TEST(chain_approx_test_blank); MU_RUN_TEST(chain_approx_test_white); MU_RUN_TEST(chain_approx_test_blob_border_w); MU_RUN_TEST(chain_approx_test_blobs_borders); MU_RUN_TEST(chain_approx_test_blobs_corners); MU_RUN_TEST(chain_approx_test_blobs_crossing); MU_RUN_TEST(chain_approx_test_cross); MU_RUN_TEST(chain_approx_test_letter_e); MU_RUN_TEST(chain_approx_test_paper_example); MU_RUN_TEST(chain_approx_test_paper_example_inv); MU_RUN_TEST(chain_approx_test_single_px); MU_RUN_TEST(chain_approx_test_single_px_inv); MU_RUN_TEST(chain_approx_test_two_blobs); MU_RUN_TEST(chain_approx_test_two_px); MU_RUN_TEST(chain_approx_test_two_px_inv); MU_RUN_TEST(chain_approx_test_contour_tracing_demo_input); MU_RUN_TEST(test_transform_decomposition); }; int main(int argc, char **argv) { (void)argc; (void)argv; MU_RUN_SUITE(nopencv_contours_suite); MU_REPORT(); return MU_EXIT_CODE; }