From 564ab243cc3fa7c0239d71a6dacb4a6c7765e9f5 Mon Sep 17 00:00:00 2001 From: jaseg Date: Thu, 3 Jun 2021 23:45:11 +0200 Subject: Add svg-flatten SVG feature tests --- .gitmodules | 3 + svg-flatten/Makefile | 15 +- svg-flatten/src/nopencv_test.cpp | 405 --------------------- svg-flatten/src/test/nopencv_test.cpp | 405 +++++++++++++++++++++ svg-flatten/src/test/svg_tests.py | 75 ++++ svg-flatten/testdata/svg/circles.svg | 78 ++++ svg-flatten/testdata/svg/compound_xform.svg | 102 ++++++ svg-flatten/testdata/svg/empty.svg | 11 + svg-flatten/testdata/svg/empty_inkscape.svg | 54 +++ svg-flatten/testdata/svg/groups.svg | 96 +++++ svg-flatten/testdata/svg/pattern_fill.svg | 135 +++++++ svg-flatten/testdata/svg/pattern_stroke.svg | 89 +++++ svg-flatten/testdata/svg/pattern_stroke_dashed.svg | 89 +++++ svg-flatten/testdata/svg/rect.svg | 65 ++++ svg-flatten/testdata/svg/rect_occlusion.svg | 79 ++++ svg-flatten/testdata/svg/rotation.svg | 66 ++++ svg-flatten/testdata/svg/rotation_90.svg | 66 ++++ svg-flatten/testdata/svg/scale.svg | 91 +++++ svg-flatten/testdata/svg/shear.svg | 59 +++ svg-flatten/testdata/svg/stroke.svg | 67 ++++ svg-flatten/testdata/svg/stroke_caps.svg | 68 ++++ svg-flatten/testdata/svg/stroke_dashes.svg | 75 ++++ svg-flatten/testdata/svg/stroke_joins.svg | 68 ++++ svg-flatten/testdata/svg/text.svg | 108 ++++++ upstream/filesystem | 1 + 25 files changed, 1956 insertions(+), 414 deletions(-) delete mode 100644 svg-flatten/src/nopencv_test.cpp create mode 100644 svg-flatten/src/test/nopencv_test.cpp create mode 100644 svg-flatten/src/test/svg_tests.py create mode 100644 svg-flatten/testdata/svg/circles.svg create mode 100644 svg-flatten/testdata/svg/compound_xform.svg create mode 100644 svg-flatten/testdata/svg/empty.svg create mode 100644 svg-flatten/testdata/svg/empty_inkscape.svg create mode 100644 svg-flatten/testdata/svg/groups.svg create mode 100644 svg-flatten/testdata/svg/pattern_fill.svg create mode 100644 svg-flatten/testdata/svg/pattern_stroke.svg create mode 100644 svg-flatten/testdata/svg/pattern_stroke_dashed.svg create mode 100644 svg-flatten/testdata/svg/rect.svg create mode 100644 svg-flatten/testdata/svg/rect_occlusion.svg create mode 100644 svg-flatten/testdata/svg/rotation.svg create mode 100644 svg-flatten/testdata/svg/rotation_90.svg create mode 100644 svg-flatten/testdata/svg/scale.svg create mode 100644 svg-flatten/testdata/svg/shear.svg create mode 100644 svg-flatten/testdata/svg/stroke.svg create mode 100644 svg-flatten/testdata/svg/stroke_caps.svg create mode 100644 svg-flatten/testdata/svg/stroke_dashes.svg create mode 100644 svg-flatten/testdata/svg/stroke_joins.svg create mode 100644 svg-flatten/testdata/svg/text.svg create mode 160000 upstream/filesystem diff --git a/.gitmodules b/.gitmodules index deedaea..b3b249b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "upstream/stb"] path = upstream/stb url = https://github.com/nothings/stb +[submodule "upstream/filesystem"] + path = upstream/filesystem + url = https://github.com/gulrak/filesystem diff --git a/svg-flatten/Makefile b/svg-flatten/Makefile index a980c05..2c9ff6c 100644 --- a/svg-flatten/Makefile +++ b/svg-flatten/Makefile @@ -1,5 +1,5 @@ -CXX := clang++ +CXX ?= clang++ LD ?= ld INSTALL := install PKG_CONFIG ?= pkg-config @@ -45,7 +45,7 @@ SOURCES += $(CLIPPER_SOURCES) INCLUDES := -Iinclude -Isrc $(CLIPPER_INCLUDES) $(VORONOI_INCLUDES) $(POISSON_INCLUDES) $(BASE64_INCLUDES) $(ARGAGG_INCLUDES) $(CAVC_INCLUDES) $(SUBPROCESS_INCLUDES) $(MINUNIT_INCLUDES) $(STB_INCLUDES) CXXFLAGS := -std=c++2a -g -Wall -Wextra -O0 -LDFLAGS := -lm -lc -lstdc++ +LDFLAGS := -lm PKG_CONFIG_DEPS := ifdef USE_SYSTEM_PUGIXML @@ -71,19 +71,16 @@ $(BUILDDIR)/%.o: %.cpp $(BUILDDIR)/$(TARGET): $(SOURCES:%.cpp=$(BUILDDIR)/%.o) @mkdir -p $(dir $@) - if [ $$(uname -s) = "Darwin" ]; then \ - $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $^; \ - else \ - $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ -Wl,--start-group $^ -lstdc++fs -Wl,--end-group; \ - fi + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $^ -$(BUILDDIR)/nopencv-tests: src/nopencv_test.cpp src/nopencv.cpp +$(BUILDDIR)/nopencv-test: src/test/nopencv_test.cpp src/nopencv.cpp @mkdir -p $(dir $@) $(CXX) $(CXXFLAGS) $(INCLUDES) $(LDFLAGS) -o $@ $^ .PHONY: tests -tests: $(BUILDDIR)/nopencv-tests +tests: $(BUILDDIR)/nopencv-test + $(BUILDDIR)/nopencv-test .PHONY: install install: diff --git a/svg-flatten/src/nopencv_test.cpp b/svg-flatten/src/nopencv_test.cpp deleted file mode 100644 index 17ba71e..0000000 --- a/svg-flatten/src/nopencv_test.cpp +++ /dev/null @@ -1,405 +0,0 @@ - -#include -#include -#include -#include -#include - -#include "nopencv.hpp" - -#include -#include - -#include "stb_image.h" - -using namespace gerbolyze; -using namespace gerbolyze::nopencv; - -char msg[1024]; - -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) { - 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) { - Image32 ref_img; - mu_assert(ref_img.load(fn), "Input image failed to load"); - ref_img.binarize(); - 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(); - - 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"); } - -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(); - 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_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(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(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); -}; - -int main(int argc, char **argv) { - (void)argc; - (void)argv; - - MU_RUN_SUITE(nopencv_contours_suite); - MU_REPORT(); - return MU_EXIT_CODE; -} diff --git a/svg-flatten/src/test/nopencv_test.cpp b/svg-flatten/src/test/nopencv_test.cpp new file mode 100644 index 0000000..2f358b2 --- /dev/null +++ b/svg-flatten/src/test/nopencv_test.cpp @@ -0,0 +1,405 @@ + +#include +#include +#include +#include +#include + +#include "nopencv.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) { + 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) { + Image32 ref_img; + mu_assert(ref_img.load(fn), "Input image failed to load"); + ref_img.binarize(); + 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(); + + 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"); } + +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(); + 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_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(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(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); +}; + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + + MU_RUN_SUITE(nopencv_contours_suite); + MU_REPORT(); + return MU_EXIT_CODE; +} diff --git a/svg-flatten/src/test/svg_tests.py b/svg-flatten/src/test/svg_tests.py new file mode 100644 index 0000000..a5139ec --- /dev/null +++ b/svg-flatten/src/test/svg_tests.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +import tempfile +import unittest +from pathlib import Path +import subprocess +import os + +from PIL import Image +import numpy as np + +def run_svg_flatten(input_file, output_file, **kwargs): + if 'SVG_FLATTEN' in os.environ: + svg_flatten = os.environ.get('SVG_FLATTEN') + elif (Path(__file__) / '../../build/svg-flatten').is_file(): + svg_flatten = '../../build/svg-flatten' + elif Path('./build/svg-flatten').is_file(): + svg_flatten = './build/svg-flatten' + else: + svg_flatten = 'svg-flatten' + + args = [ svg_flatten, + *(arg for (key, value) in kwargs.items() for arg in (f'--{key.replace("_", "-")}', value)), + str(input_file), str(output_file) ] + + try: + proc = subprocess.run(args, capture_output=True, check=True) + except: + print('Subprocess stdout:') + print(proc.stdout) + print('Subprocess stderr:') + print(proc.stderr) + raise + +class SVGRoundTripTests(unittest.TestCase): + + def compare_images(self, reference, output, test_name, mean=0.01): + ref = np.array(Image.open(reference)) + out = np.array(Image.open(output)) + delta = np.abs(out - ref).astype(float) / 255 + + #print(f'{test_name}: mean={delta.mean():.5g}') + + self.assertTrue(delta.mean() < mean, + f'Expected mean pixel difference between images to be <{mean}, was {delta.mean():.5g}') + + def run_svg_round_trip_test(self, test_in_svg): + with tempfile.NamedTemporaryFile(suffix='.svg') as tmp_out_svg,\ + tempfile.NamedTemporaryFile(suffix='.png') as tmp_out_png,\ + tempfile.NamedTemporaryFile(suffix='.png') as tmp_in_png: + + run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg') + + subprocess.run(['resvg', tmp_out_svg.name, tmp_out_png.name], check=True, stdout=subprocess.DEVNULL) + subprocess.run(['resvg', test_in_svg, tmp_in_png.name], check=True, stdout=subprocess.DEVNULL) + + try: + self.compare_images(tmp_in_png, tmp_out_png, test_in_svg.stem) + except AssertionError as e: + import shutil + shutil.copyfile(tmp_in_png.name, f'/tmp/gerbolyze-fail-{test_in_svg.stem}-in.png') + shutil.copyfile(tmp_out_png.name, f'/tmp/gerbolyze-fail-{test_in_svg.stem}-out.png') + foo = list(e.args) + foo[0] += '\nFailing test renderings copied to:\n' + foo[0] += f' /tmp/gerbolyze-fail-{test_in_svg.stem}-{{in|out}}.png\n' + e.args = tuple(foo) + raise e + +for test_in_svg in Path('testdata/svg').glob('*.svg'): + # We need to make sure we capture the loop variable's current value here. + gen = lambda testcase: lambda self: self.run_svg_round_trip_test(testcase) + setattr(SVGRoundTripTests, f'test_{test_in_svg.stem}', gen(test_in_svg)) + +if __name__ == '__main__': + unittest.main() diff --git a/svg-flatten/testdata/svg/circles.svg b/svg-flatten/testdata/svg/circles.svg new file mode 100644 index 0000000..9be474e --- /dev/null +++ b/svg-flatten/testdata/svg/circles.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/compound_xform.svg b/svg-flatten/testdata/svg/compound_xform.svg new file mode 100644 index 0000000..229e05d --- /dev/null +++ b/svg-flatten/testdata/svg/compound_xform.svg @@ -0,0 +1,102 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/empty.svg b/svg-flatten/testdata/svg/empty.svg new file mode 100644 index 0000000..d2cdc52 --- /dev/null +++ b/svg-flatten/testdata/svg/empty.svg @@ -0,0 +1,11 @@ + + + diff --git a/svg-flatten/testdata/svg/empty_inkscape.svg b/svg-flatten/testdata/svg/empty_inkscape.svg new file mode 100644 index 0000000..716379f --- /dev/null +++ b/svg-flatten/testdata/svg/empty_inkscape.svg @@ -0,0 +1,54 @@ + + + + + + + + image/svg+xml + + + + + + + diff --git a/svg-flatten/testdata/svg/groups.svg b/svg-flatten/testdata/svg/groups.svg new file mode 100644 index 0000000..d94dad0 --- /dev/null +++ b/svg-flatten/testdata/svg/groups.svg @@ -0,0 +1,96 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/pattern_fill.svg b/svg-flatten/testdata/svg/pattern_fill.svg new file mode 100644 index 0000000..21789e6 --- /dev/null +++ b/svg-flatten/testdata/svg/pattern_fill.svg @@ -0,0 +1,135 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/pattern_stroke.svg b/svg-flatten/testdata/svg/pattern_stroke.svg new file mode 100644 index 0000000..5564df0 --- /dev/null +++ b/svg-flatten/testdata/svg/pattern_stroke.svg @@ -0,0 +1,89 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/pattern_stroke_dashed.svg b/svg-flatten/testdata/svg/pattern_stroke_dashed.svg new file mode 100644 index 0000000..3d8145e --- /dev/null +++ b/svg-flatten/testdata/svg/pattern_stroke_dashed.svg @@ -0,0 +1,89 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/rect.svg b/svg-flatten/testdata/svg/rect.svg new file mode 100644 index 0000000..297ff7e --- /dev/null +++ b/svg-flatten/testdata/svg/rect.svg @@ -0,0 +1,65 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/rect_occlusion.svg b/svg-flatten/testdata/svg/rect_occlusion.svg new file mode 100644 index 0000000..ce0e531 --- /dev/null +++ b/svg-flatten/testdata/svg/rect_occlusion.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/rotation.svg b/svg-flatten/testdata/svg/rotation.svg new file mode 100644 index 0000000..961ef0f --- /dev/null +++ b/svg-flatten/testdata/svg/rotation.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/rotation_90.svg b/svg-flatten/testdata/svg/rotation_90.svg new file mode 100644 index 0000000..dcca867 --- /dev/null +++ b/svg-flatten/testdata/svg/rotation_90.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/scale.svg b/svg-flatten/testdata/svg/scale.svg new file mode 100644 index 0000000..91b28d6 --- /dev/null +++ b/svg-flatten/testdata/svg/scale.svg @@ -0,0 +1,91 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/shear.svg b/svg-flatten/testdata/svg/shear.svg new file mode 100644 index 0000000..ec7875f --- /dev/null +++ b/svg-flatten/testdata/svg/shear.svg @@ -0,0 +1,59 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/svg-flatten/testdata/svg/stroke.svg b/svg-flatten/testdata/svg/stroke.svg new file mode 100644 index 0000000..6a4b334 --- /dev/null +++ b/svg-flatten/testdata/svg/stroke.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/stroke_caps.svg b/svg-flatten/testdata/svg/stroke_caps.svg new file mode 100644 index 0000000..6b89a34 --- /dev/null +++ b/svg-flatten/testdata/svg/stroke_caps.svg @@ -0,0 +1,68 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/stroke_dashes.svg b/svg-flatten/testdata/svg/stroke_dashes.svg new file mode 100644 index 0000000..c1d6f5c --- /dev/null +++ b/svg-flatten/testdata/svg/stroke_dashes.svg @@ -0,0 +1,75 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/stroke_joins.svg b/svg-flatten/testdata/svg/stroke_joins.svg new file mode 100644 index 0000000..ee3e1ac --- /dev/null +++ b/svg-flatten/testdata/svg/stroke_joins.svg @@ -0,0 +1,68 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/svg-flatten/testdata/svg/text.svg b/svg-flatten/testdata/svg/text.svg new file mode 100644 index 0000000..bd8c390 --- /dev/null +++ b/svg-flatten/testdata/svg/text.svg @@ -0,0 +1,108 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + Gerbolyze + Gerbolyze + Gerbolyze + diff --git a/upstream/filesystem b/upstream/filesystem new file mode 160000 index 0000000..4e21ab3 --- /dev/null +++ b/upstream/filesystem @@ -0,0 +1 @@ +Subproject commit 4e21ab305794f5309a1454b4ae82ab9a0f5e0d25 -- cgit