diff options
Diffstat (limited to 'svg-flatten')
-rw-r--r-- | svg-flatten/src/nopencv.cpp | 94 | ||||
-rw-r--r-- | svg-flatten/src/nopencv_test.cpp | 194 |
2 files changed, 186 insertions, 102 deletions
diff --git a/svg-flatten/src/nopencv.cpp b/svg-flatten/src/nopencv.cpp index afe4881..2384fcc 100644 --- a/svg-flatten/src/nopencv.cpp +++ b/svg-flatten/src/nopencv.cpp @@ -247,6 +247,7 @@ double k_cos(const Polygon_i &poly, size_t i, size_t k) { return dp / (sqrt(sq_a)*sqrt(sq_b)); } +constexpr bool debug = false; ContourCallback gerbolyze::nopencv::simplify_contours_teh_chin(ContourCallback cb) { return [&cb](Polygon_i &poly, ContourPolarity cpol) { size_t sz = poly.size(); @@ -261,30 +262,32 @@ ContourCallback gerbolyze::nopencv::simplify_contours_teh_chin(ContourCallback c retain[i] = true; } - cerr << endl; - cerr << "Polarity: " << cpol <<endl; - cerr << "Coords:"<<endl; - cerr << " x: "; - for (size_t i=0; i<sz; i++) { - cerr << setfill(' ') << setw(2) << poly[i][0] << " "; - } - cerr << endl; - cerr << " y: "; - for (size_t i=0; i<sz; i++) { - cerr << setfill(' ') << setw(2) << poly[i][1] << " "; - } - cerr << endl; - cerr << "Metrics:"<<endl; - cerr << "ros: "; - for (size_t i=0; i<sz; i++) { - cerr << setfill(' ') << setw(2) << ros[i] << " "; - } - cerr << endl; - cerr << "sig: "; - for (size_t i=0; i<sz; i++) { - cerr << setfill(' ') << setw(2) << sig[i] << " "; + if (debug) { + cerr << endl; + cerr << "Polarity: " << cpol <<endl; + cerr << "Coords:"<<endl; + cerr << " x: "; + for (size_t i=0; i<sz; i++) { + cerr << setfill(' ') << setw(2) << poly[i][0] << " "; + } + cerr << endl; + cerr << " y: "; + for (size_t i=0; i<sz; i++) { + cerr << setfill(' ') << setw(2) << poly[i][1] << " "; + } + cerr << endl; + cerr << "Metrics:"<<endl; + cerr << "ros: "; + for (size_t i=0; i<sz; i++) { + cerr << setfill(' ') << setw(2) << ros[i] << " "; + } + cerr << endl; + cerr << "sig: "; + for (size_t i=0; i<sz; i++) { + cerr << setfill(' ') << setw(2) << sig[i] << " "; + } + cerr << endl; } - cerr << endl; /* Pass 0 (like opencv): Remove points with zero 1-curvature */ for (size_t i=0; i<sz; i++) { @@ -294,11 +297,13 @@ ContourCallback gerbolyze::nopencv::simplify_contours_teh_chin(ContourCallback c } } - cerr << "pass 0: "; - for (size_t i=0; i<sz; i++) { - cerr << (retain[i] ? "#" : "."); + if (debug) { + cerr << "pass 0: "; + for (size_t i=0; i<sz; i++) { + cerr << (retain[i] ? "#" : "."); + } + cerr << endl; } - cerr << endl; /* 3a, Pass 1: Non-maxima suppression */ for (size_t i=0; i<sz; i++) { @@ -310,11 +315,13 @@ ContourCallback gerbolyze::nopencv::simplify_contours_teh_chin(ContourCallback c } } - cerr << "pass 1: "; - for (size_t i=0; i<sz; i++) { - cerr << (retain[i] ? "#" : "."); + if (debug) { + cerr << "pass 1: "; + for (size_t i=0; i<sz; i++) { + cerr << (retain[i] ? "#" : "."); + } + cerr << endl; } - cerr << endl; /* 3b, Pass 2: Zero-curvature suppression */ for (size_t i=0; i<sz; i++) { @@ -325,11 +332,13 @@ ContourCallback gerbolyze::nopencv::simplify_contours_teh_chin(ContourCallback c } } - cerr << "pass 2: "; - for (size_t i=0; i<sz; i++) { - cerr << (retain[i] ? "#" : "."); + if (debug) { + cerr << "pass 2: "; + for (size_t i=0; i<sz; i++) { + cerr << (retain[i] ? "#" : "."); + } + cerr << endl; } - cerr << endl; /* 3c, Pass 3: Further thinning */ for (size_t i=0; i<sz; i++) { @@ -344,11 +353,13 @@ ContourCallback gerbolyze::nopencv::simplify_contours_teh_chin(ContourCallback c } } - cerr << "pass 3: "; - for (size_t i=0; i<sz; i++) { - cerr << (retain[i] ? "#" : "."); + if (debug) { + cerr << "pass 3: "; + for (size_t i=0; i<sz; i++) { + cerr << (retain[i] ? "#" : "."); + } + cerr << endl; } - cerr << endl; Polygon_i new_poly; for (size_t i=0; i<sz; i++) { @@ -356,7 +367,10 @@ ContourCallback gerbolyze::nopencv::simplify_contours_teh_chin(ContourCallback c new_poly.push_back(poly[i]); } } - cb(new_poly, cpol); + + if (!new_poly.empty()) { + cb(new_poly, cpol); + } }; } diff --git a/svg-flatten/src/nopencv_test.cpp b/svg-flatten/src/nopencv_test.cpp index 4027072..389de60 100644 --- a/svg-flatten/src/nopencv_test.cpp +++ b/svg-flatten/src/nopencv_test.cpp @@ -29,6 +29,40 @@ private: filesystem::path m_path; }; +class SVGPolyRenderer { +public: + SVGPolyRenderer(const char *fn, int width_px, int height_px) + : m_svg(fn) { + m_svg << "<svg width=\"" << width_px << "px\" height=\"" << height_px << "px\" viewBox=\"0 0 " + << width_px << " " << height_px << "\" " + << "xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">" << endl; + m_svg << "<rect width=\"100%\" height=\"100%\" fill=\"black\"/>" << 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 << "<path fill=\"" << ((pol == CP_HOLE) ? "black" : "white") << "\" d=\""; + m_svg << "M " << poly[0][0] << " " << poly[0][1]; + for (size_t i=1; i<poly.size(); i++) { + m_svg << " L " << poly[i][0] << " " << poly[i][1]; + } + m_svg << " Z\"/>" << endl; + }; + } + + void close() { + m_svg << "</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, @@ -74,16 +108,13 @@ MU_TEST(test_complex_example_from_paper) { i2p last; bool first = true; Polygon_i exp = expected_polys[invocation_count-1]; - //cout << "poly: "; for (auto &p : poly) { - //cout << "(" << p[0] << ", " << p[1] << "), "; 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; } - //cout << endl; }); mu_assert_int_eq(3, invocation_count); @@ -127,6 +158,27 @@ MU_TEST(test_complex_example_from_paper) { } } +int render_svg(const char *in_svg, const char *out_png) { + const char *command_line[] = {"resvg", in_svg, out_png, nullptr}; + struct subprocess_s subprocess; + int rc = subprocess_create(command_line, subprocess_option_inherit_environment, &subprocess); + if (rc) + return rc; + + int resvg_rc = -1; + rc = subprocess_join(&subprocess, &resvg_rc); + if (rc) + return rc; + if (resvg_rc) + return -resvg_rc; + + rc = subprocess_destroy(&subprocess); + if (rc) + return rc; + + return 0; +} + static void testdata_roundtrip(const char *fn) { int x, y; uint8_t *data = stbi_load(fn, &x, &y, nullptr, 1); @@ -142,40 +194,11 @@ static void testdata_roundtrip(const char *fn) { TempfileHack tmp_svg(".svg"); TempfileHack tmp_png(".png"); - ofstream svg(tmp_svg.c_str()); - - svg << "<svg width=\"" << x << "px\" height=\"" << y << "px\" viewBox=\"0 0 " - << x << " " << y << "\" " - << "xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">" << endl; - svg << "<rect width=\"100%\" height=\"100%\" fill=\"black\"/>" << endl; - - gerbolyze::nopencv::find_blobs(ref_img, [&svg](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"); + SVGPolyRenderer ctx(tmp_svg.c_str(), x, y); + gerbolyze::nopencv::find_blobs(ref_img, ctx.callback()); + ctx.close(); - svg << "<path fill=\"" << ((pol == CP_HOLE) ? "black" : "white") << "\" d=\""; - svg << "M " << poly[0][0] << " " << poly[0][1]; - for (size_t i=1; i<poly.size(); i++) { - svg << " L " << poly[i][0] << " " << poly[i][1]; - } - svg << " Z\"/>" << endl; - }); - svg << "</svg>" << endl; - svg.close(); - - const char *command_line[] = {"resvg", tmp_svg.c_str(), tmp_png.c_str()}; - struct subprocess_s subprocess; - int rc = subprocess_create(command_line, subprocess_option_inherit_environment, &subprocess); - mu_assert_int_eq(0, rc); - - int resvg_rc = -1; - rc = subprocess_join(&subprocess, &resvg_rc); - mu_assert_int_eq(0, rc); - mu_assert_int_eq(0, resvg_rc); - - rc = subprocess_destroy(&subprocess); - mu_assert_int_eq(0, rc); + mu_assert_int_eq(0, render_svg(tmp_svg.c_str(), tmp_png.c_str())); int out_x, out_y; uint8_t *out_data = stbi_load(tmp_png.c_str(), &out_x, &out_y, nullptr, 1); @@ -213,44 +236,77 @@ MU_TEST(test_round_trip_two_px_inv) { testdata_roundtrip("testdata/two-p static void chain_approx_test(const char *fn) { - int x, y; - uint8_t *data = stbi_load(fn, &x, &y, nullptr, 1); - Image32 ref_img(x, y); - for (int cy=0; cy<y; cy++) { - for (int cx=0; cx<x; cx++) { - ref_img.at(cx, cy) = data[cy*x + cx] / 255; + //cout << endl << "Testing \"" << fn << "\"" << endl; + int w, h; + uint8_t *data = stbi_load(fn, &w, &h, nullptr, 1); + Image32 ref_img(w, h); + for (int y=0; y<h; y++) { + for (int x=0; x<w; x++) { + ref_img.at(x, y) = data[y*w + x] / 255; } } - stbi_image_free(data); TempfileHack tmp_svg(".svg"); + TempfileHack tmp_png(".png"); - //ofstream svg(tmp_svg.c_str()); - ofstream svg("/tmp/teh-chin-test.svg"); - - svg << "<svg width=\"" << x << "px\" height=\"" << y << "px\" viewBox=\"0 0 " - << x << " " << y << "\" " - << "xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">" << endl; - svg << "<rect width=\"100%\" height=\"100%\" fill=\"black\"/>" << endl; - - gerbolyze::nopencv::find_blobs(ref_img, simplify_contours_teh_chin([&svg](Polygon_i &poly, ContourPolarity pol) { - svg << "<path fill=\"" << ((pol == CP_HOLE) ? "black" : "white") << "\" d=\""; - svg << "M " << poly[0][0] << " " << poly[0][1]; - for (size_t i=1; i<poly.size(); i++) { - svg << " L " << poly[i][0] << " " << poly[i][1]; + SVGPolyRenderer ctx(tmp_svg.c_str(), w, h); + gerbolyze::nopencv::find_blobs(ref_img, simplify_contours_teh_chin(ctx.callback())); + ctx.close(); + + mu_assert_int_eq(0, render_svg(tmp_svg.c_str(), "/tmp/out.png")); + + int out_w, out_h; + uint8_t *out_data = stbi_load("/tmp/out.png", &out_w, &out_h, nullptr, 1); + mu_assert_int_eq(w, out_w); + mu_assert_int_eq(h, out_h); + + double max_abs_deviation = 0; + double rms_sum = 0; + double mean_sum = 0; + for (int y=0; y<h; y++) { + for (int x=0; x<w; x++) { + double delta = fabs((double)out_data[y*w + x] - (double)data[y*w + x]) / 255.0; + max_abs_deviation = fmax(max_abs_deviation, delta); + rms_sum += delta*delta; + mean_sum += delta; } - svg << " Z\"/>" << endl; - })); - svg << "</svg>" << endl; - svg.close(); + } + stbi_image_free(data); + stbi_image_free(out_data); + + rms_sum = sqrt(rms_sum / (w*h)); + mean_sum /= w*h; + if (rms_sum > 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_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); @@ -267,8 +323,22 @@ MU_TEST_SUITE(nopencv_contours_suite) { 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(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); }; int main(int argc, char **argv) { |