From 61887e9ee1a108c26cdd4b2d67fc4095d62b6e9d Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 4 Jun 2021 23:28:36 +0200 Subject: Add & fix vectorizer tests --- svg-flatten/include/geom2d.hpp | 8 +++--- svg-flatten/include/gerbolyze.hpp | 1 + svg-flatten/src/main.cpp | 10 +++++-- svg-flatten/src/nopencv.cpp | 2 ++ svg-flatten/src/svg_color.cpp | 14 +++++---- svg-flatten/src/svg_color.h | 7 +++-- svg-flatten/src/svg_doc.cpp | 4 +-- svg-flatten/src/test/svg_tests.py | 60 ++++++++++++++++++++++++++++++++------- svg-flatten/src/vec_core.cpp | 37 ++++++++++++++++-------- 9 files changed, 103 insertions(+), 40 deletions(-) (limited to 'svg-flatten') diff --git a/svg-flatten/include/geom2d.hpp b/svg-flatten/include/geom2d.hpp index 82dbfb5..9f83754 100644 --- a/svg-flatten/include/geom2d.hpp +++ b/svg-flatten/include/geom2d.hpp @@ -72,14 +72,14 @@ namespace gerbolyze { } xform2d &translate(double x, double y) { - x0 += x*xx + y*xy; - y0 += y*yy + x*yx; + xform2d xf(1, 0, 0, 1, x, y); + transform(xf); return *this; } xform2d &scale(double x, double y) { - xx *= x; yx *= y; xy *= x; - yy *= y; x0 *= x; y0 *= y; + xform2d xf(x, 0, 0, y); + transform(xf); return *this; } diff --git a/svg-flatten/include/gerbolyze.hpp b/svg-flatten/include/gerbolyze.hpp index a30d7eb..9b05bdb 100644 --- a/svg-flatten/include/gerbolyze.hpp +++ b/svg-flatten/include/gerbolyze.hpp @@ -166,6 +166,7 @@ namespace gerbolyze { double curve_tolerance_mm; VectorizerSelectorizer &m_vec_sel; bool outline_mode = false; + bool flip_color_interpretation = false; }; class SVGDocument { diff --git a/svg-flatten/src/main.cpp b/svg-flatten/src/main.cpp index 3093d6a..1fce67b 100644 --- a/svg-flatten/src/main.cpp +++ b/svg-flatten/src/main.cpp @@ -34,14 +34,17 @@ int main(int argc, char **argv) { "Number of decimal places use for exported coordinates (gerber: 1-9, SVG: 0-*)", 1}, {"svg_clear_color", {"--clear-color"}, - "SVG color to use for \"clear\" areas (default: white)", + "SVG color to use for \"clear\" areas (SVG output only; default: white)", 1}, {"svg_dark_color", {"--dark-color"}, - "SVG color to use for \"dark\" areas (default: black)", + "SVG color to use for \"dark\" areas (SVG output only; default: black)", 1}, {"flip_gerber_polarity", {"-f", "--flip-gerber-polarity"}, "Flip polarity of all output gerber primitives for --format gerber.", 0}, + {"flip_svg_color_interpretation", {"-i", "--svg-white-is-gerber-dark"}, + "Flip polarity of SVG color interpretation. This affects only SVG primitives like paths and NOT embedded bitmaps. With -i: white -> silk there/\"dark\" gerber primitive.", + 0}, {"min_feature_size", {"-d", "--trace-space"}, "Minimum feature size of elements in vectorized graphics (trace/space) in mm. Default: 0.1mm.", 1}, @@ -420,11 +423,14 @@ int main(int argc, char **argv) { } VectorizerSelectorizer vec_sel(vectorizer, args["vectorizer_map"] ? args["vectorizer_map"].as() : ""); + bool flip_svg_colors = args["flip_svg_color_interpretation"]; + RenderSettings rset { min_feature_size, curve_tolerance, vec_sel, outline_mode, + flip_svg_colors, }; SVGDocument doc; diff --git a/svg-flatten/src/nopencv.cpp b/svg-flatten/src/nopencv.cpp index b490217..0643b20 100644 --- a/svg-flatten/src/nopencv.cpp +++ b/svg-flatten/src/nopencv.cpp @@ -155,6 +155,8 @@ void gerbolyze::nopencv::find_contours(gerbolyze::nopencv::Image32 &img, gerboly * 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; diff --git a/svg-flatten/src/svg_color.cpp b/svg-flatten/src/svg_color.cpp index 5f1d693..76938e8 100644 --- a/svg-flatten/src/svg_color.cpp +++ b/svg-flatten/src/svg_color.cpp @@ -30,7 +30,7 @@ using namespace std; * This function handles transparency: Transparent SVG colors are mapped such that no gerber output is generated for * them. */ -enum gerber_color gerbolyze::svg_color_to_gerber(string color, string opacity, enum gerber_color default_val) { +enum gerber_color gerbolyze::svg_color_to_gerber(string color, string opacity, enum gerber_color default_val, const RenderSettings &rset) { float alpha = 1.0; if (!opacity.empty() && opacity[0] != '\0') { char *endptr = nullptr; @@ -57,8 +57,10 @@ enum gerber_color gerbolyze::svg_color_to_gerber(string color, string opacity, e if (color.length() == 7 && color[0] == '#') { HSVColor hsv(color); - if (hsv.v >= 0.5) { + if ((hsv.v >= 0.5) != rset.flip_color_interpretation) { return GRB_CLEAR; + } else { + return GRB_DARK; } } @@ -107,13 +109,13 @@ enum gerber_color gerbolyze::gerber_color_invert(enum gerber_color color) { } /* Read node's fill attribute and convert it to a gerber color */ -enum gerber_color gerbolyze::gerber_fill_color(const pugi::xml_node &node) { - return svg_color_to_gerber(node.attribute("fill").value(), node.attribute("fill-opacity").value(), GRB_DARK); +enum gerber_color gerbolyze::gerber_fill_color(const pugi::xml_node &node, const RenderSettings &rset) { + return svg_color_to_gerber(node.attribute("fill").value(), node.attribute("fill-opacity").value(), GRB_DARK, rset); } /* Read node's stroke attribute and convert it to a gerber color */ -enum gerber_color gerbolyze::gerber_stroke_color(const pugi::xml_node &node) { - return svg_color_to_gerber(node.attribute("stroke").value(), node.attribute("stroke-opacity").value(), GRB_NONE); +enum gerber_color gerbolyze::gerber_stroke_color(const pugi::xml_node &node, const RenderSettings &rset) { + return svg_color_to_gerber(node.attribute("stroke").value(), node.attribute("stroke-opacity").value(), GRB_NONE, rset); } diff --git a/svg-flatten/src/svg_color.h b/svg-flatten/src/svg_color.h index 2817cd9..752c2ed 100644 --- a/svg-flatten/src/svg_color.h +++ b/svg-flatten/src/svg_color.h @@ -19,6 +19,7 @@ #pragma once #include +#include namespace gerbolyze { @@ -42,10 +43,10 @@ public: HSVColor(const RGBColor &color); }; -enum gerber_color svg_color_to_gerber(std::string color, std::string opacity, enum gerber_color default_val); +enum gerber_color svg_color_to_gerber(std::string color, std::string opacity, enum gerber_color default_val, const RenderSettings &rset); enum gerber_color gerber_color_invert(enum gerber_color color); -enum gerber_color gerber_fill_color(const pugi::xml_node &node); -enum gerber_color gerber_stroke_color(const pugi::xml_node &node); +enum gerber_color gerber_fill_color(const pugi::xml_node &node, const RenderSettings &rset); +enum gerber_color gerber_stroke_color(const pugi::xml_node &node, const RenderSettings &rset); } /* namespace gerbolyze */ diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp index aae5abf..e59a7cc 100644 --- a/svg-flatten/src/svg_doc.cpp +++ b/svg-flatten/src/svg_doc.cpp @@ -208,8 +208,8 @@ void gerbolyze::SVGDocument::export_svg_group(xform2d &mat, const RenderSettings /* Export an SVG path element to gerber. Apply patterns and clip on the fly. */ void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings &rset, const pugi::xml_node &node, Paths &clip_path) { - enum gerber_color fill_color = gerber_fill_color(node); - enum gerber_color stroke_color = gerber_stroke_color(node); + enum gerber_color fill_color = gerber_fill_color(node, rset); + enum gerber_color stroke_color = gerber_stroke_color(node, rset); double stroke_width = usvg_double_attr(node, "stroke-width", /* default */ 1.0); assert(stroke_width > 0.0); diff --git a/svg-flatten/src/test/svg_tests.py b/svg-flatten/src/test/svg_tests.py index c457ed4..48ee8fd 100644 --- a/svg-flatten/src/test/svg_tests.py +++ b/svg-flatten/src/test/svg_tests.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import tempfile +import shutil import unittest from pathlib import Path import subprocess @@ -9,7 +10,7 @@ import os from PIL import Image import numpy as np -def run_svg_flatten(input_file, output_file, **kwargs): +def run_svg_flatten(input_file, output_file, *args, **kwargs): if 'SVG_FLATTEN' in os.environ: svg_flatten = os.environ.get('SVG_FLATTEN') elif (Path(__file__) / '../../build/svg-flatten').is_file(): @@ -19,9 +20,15 @@ def run_svg_flatten(input_file, output_file, **kwargs): 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) ] + args = [ svg_flatten ] + for key, value in kwargs.items(): + key = '--' + key.replace("_", "-") + args.append(key) + + if type(value) is not bool: + args.append(value) + args.append(str(input_file)) + args.append(str(output_file)) try: proc = subprocess.run(args, capture_output=True, check=True) @@ -49,6 +56,11 @@ class SVGRoundTripTests(unittest.TestCase): # Both are expected and OK. 'stroke_dashes_comparison': 0.03, 'stroke_dashes': 0.05, + # The vectorizer tests produce output with lots of edges, which leads to a large amount of aliasing artifacts. + 'vectorizer_simple': 0.05, + 'vectorizer_clip': 0.05, + 'vectorizer_xform': 0.05, + 'vectorizer_xform_clip': 0.05, } # Force use of rsvg-convert instead of resvg for these test cases @@ -60,9 +72,21 @@ class SVGRoundTripTests(unittest.TestCase): 'pattern_stroke_dashed' } - def compare_images(self, reference, output, test_name, mean, rsvg_workaround=False): - ref = np.array(Image.open(reference)) - out = np.array(Image.open(output)) + def compare_images(self, reference, output, test_name, mean, vectorizer_test=False, rsvg_workaround=False): + ref, out = Image.open(reference), Image.open(output) + + if vectorizer_test: + target_size = (100, 100) + ref.thumbnail(target_size, Image.ANTIALIAS) + out.thumbnail(target_size, Image.ANTIALIAS) + ref, out = np.array(ref), np.array(out) + + else: + ref, out = np.array(ref), np.array(out) + + ref, out = ref.astype(float).mean(axis=2), out.astype(float).mean(axis=2) + + if rsvg_workaround: # For some stupid reason, rsvg-convert does not actually output black as in "black" pixels when asked to. # Instead, it outputs #010101. We fix this in post here. @@ -86,9 +110,24 @@ class SVGRoundTripTests(unittest.TestCase): 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') - use_rsvg = test_in_svg.stem in SVGRoundTripTests.rsvg_override + vectorizer_test = test_in_svg.stem.startswith('vectorizer') + contours_test = test_in_svg.stem.startswith('contours') + + if not vectorizer_test: + run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg') + + else: + run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg', + svg_white_is_gerber_dark=True, + clear_color='black', dark_color='white') + + if contours_test: + run_svg_flatten(test_in_svg, tmp_out_svg.name, + clear_color='black', dark_color='white', + svg_white_is_gerber_dark=True, + format='svg', + vectorizer='binary-contours') if not use_rsvg: # default! subprocess.run(['resvg', tmp_out_svg.name, tmp_out_png.name], check=True, stdout=subprocess.DEVNULL) @@ -101,10 +140,9 @@ class SVGRoundTripTests(unittest.TestCase): try: self.compare_images(tmp_in_png, tmp_out_png, test_in_svg.stem, SVGRoundTripTests.test_mean_overrides.get(test_in_svg.stem, SVGRoundTripTests.test_mean_default), - rsvg_workaround=use_rsvg) + vectorizer_test, rsvg_workaround=use_rsvg) 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) diff --git a/svg-flatten/src/vec_core.cpp b/svg-flatten/src/vec_core.cpp index b0221ca..1716380 100644 --- a/svg-flatten/src/vec_core.cpp +++ b/svg-flatten/src/vec_core.cpp @@ -161,8 +161,9 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml /* Set up target transform using SVG transform and x/y attributes */ xform2d local_xf(mat); - local_xf.translate(x, y); local_xf.transform(xform2d(node.attribute("transform").value())); + local_xf.translate(x, y); + cerr << "voronoi vectorizer: local_xf = " << local_xf.dbg_str() << endl; double orig_rows = img->rows(); double orig_cols = img->cols(); @@ -172,9 +173,11 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml double off_y = 0; handle_aspect_ratio(node.attribute("preserveAspectRatio").value(), scale_x, scale_y, off_x, off_y, orig_cols, orig_rows); + cerr << "aspect " << scale_x << ", " << scale_y << " / " << off_x << ", " << off_y << endl; /* Adjust minimum feature size given in mm and translate into px document units in our local coordinate system. */ min_feature_size_px = local_xf.doc2phys_dist(min_feature_size_px); + cerr << " min_feature_size_px = " << min_feature_size_px << endl; draw_bg_rect(local_xf, width, height, clip_path, sink); @@ -195,6 +198,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml /* TODO: support for preserveAspectRatio attribute */ double px_w = width / min_feature_size_px * scale_featuresize_factor; double px_h = height / min_feature_size_px * scale_featuresize_factor; + cerr << " px_size = " << px_w << ", " << px_h << endl; /* Scale intermediate image (step 1.2) to have pixels per min_feature_size. */ cerr << "scaled " << img->cols() << ", " << img->rows() << " -> " << ((int)round(px_w)) << ", " << ((int)round(px_h)) << endl; @@ -235,8 +239,8 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml const jcv_point center = sites[i].p; double pxd = img->at( - (int)round(center.y / (scale_y * orig_rows / img->rows())), - (int)round(center.x / (scale_x * orig_cols / img->cols()))) / 255.0; + (int)round(center.x / (scale_x * orig_cols / img->cols())), + (int)round(center.y / (scale_y * orig_rows / img->rows()))) / 255.0; /* FIXME: This is a workaround for a memory corruption bug that happens with the square-grid setting. When using * square-grid on a fairly small test image, sometimes sites[i].index will be out of bounds here. */ @@ -249,8 +253,10 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml vector adjusted_fill_factors; adjusted_fill_factors.reserve(32); /* Vector to hold adjusted fill factors for each edge for gap filling */ /* now iterate over all voronoi cells again to generate each cell's scaled polygon halftone blob. */ + cerr << " generating cells " << diagram.numsites << endl; for (int i=0; inext; } + cerr << " blob: "; /* Now, generate the actual halftone blob polygon */ ClipperLib::Path cell_path; double last_fill_factor = adjusted_fill_factors.back(); @@ -303,6 +310,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml off_x + center.x + (e->pos[0].x - center.x) * fill_factor, off_y + center.y + (e->pos[0].y - center.y) * fill_factor }); + cerr << " - <" << p[0] << ", " << p[1] << ">"; cell_path.push_back({ (ClipperLib::cInt)round(p[0] * clipper_scale), (ClipperLib::cInt)round(p[1] * clipper_scale) @@ -314,6 +322,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml off_x + center.x + (e->pos[1].x - center.x) * fill_factor, off_y + center.y + (e->pos[1].y - center.y) * fill_factor }); + cerr << " - [" << p[0] << ", " << p[1] << "]"; cell_path.push_back({ (ClipperLib::cInt)round(p[0] * clipper_scale), (ClipperLib::cInt)round(p[1] * clipper_scale) @@ -323,6 +332,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml last_fill_factor = fill_factor; e = e->next; } + cerr << endl; /* Now, clip the halftone blob generated above against the given clip path. We do this individually for each * blob since this way is *much* faster than throwing a million blobs at once at poor clipper. */ @@ -376,10 +386,8 @@ void gerbolyze::handle_aspect_ratio(string spec, double &scale_x, double &scale_ std::regex reg("x(Min|Mid|Max)Y(Min|Mid|Max)"); std::smatch match; - cerr << "data: " <<" "<< scale_x << "/" << scale_y << ": " << scale << endl; off_x = (scale_x - scale) * cols; off_y = (scale_y - scale) * rows; - cerr << rows <<","<binarize(); nopencv::find_contours(*img, [&sink, &local_xf, &clip_path, off_x, off_y, scale_x, scale_y](Polygon_i& poly, nopencv::ContourPolarity pol) { - sink << ((pol == nopencv::CP_CONTOUR) ? GRB_POL_DARK : GRB_POL_CLEAR); - bool is_clockwise = nopencv::polygon_area(poly) > 0; - if (!is_clockwise) + if (pol == nopencv::CP_HOLE) { std::reverse(poly.begin(), poly.end()); + sink << GRB_POL_CLEAR; + + } else { + sink << GRB_POL_DARK; + } ClipperLib::Path out; for (const auto &p : poly) { @@ -484,21 +495,23 @@ gerbolyze::VectorizerSelectorizer::VectorizerSelectorizer(const string default_v m_map[parsed_id] = mapping; } + /* cerr << "parsed " << m_map.size() << " vectorizers" << endl; for (auto &elem : m_map) { cerr << " " << elem.first << " -> " << elem.second << endl; } + */ } ImageVectorizer *gerbolyze::VectorizerSelectorizer::select(const pugi::xml_node &img) { const string id = img.attribute("id").value(); - cerr << "selecting vectorizer for image \"" << id << "\"" << endl; + // cerr << "selecting vectorizer for image \"" << id << "\"" << endl; if (m_map.count(id) > 0) { - cerr << " -> found" << endl; + // cerr << " -> found" << endl; return makeVectorizer(m_map[id]); } - cerr << " -> default" << endl; + // cerr << " -> default" << endl; return makeVectorizer(m_default); } -- cgit