Add & fix vectorizer tests

This commit is contained in:
jaseg 2021-06-04 23:28:36 +02:00
parent 6193fa151e
commit 61887e9ee1
9 changed files with 103 additions and 40 deletions

View file

@ -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;
}

View file

@ -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 {

View file

@ -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<string>() : "");
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;

View file

@ -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;

View file

@ -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);
}

View file

@ -19,6 +19,7 @@
#pragma once
#include <pugixml.hpp>
#include <gerbolyze.hpp>
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 */

View file

@ -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);

View file

@ -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)

View file

@ -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 <scale_featuresize_factor> 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<double> 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; i<diagram.numsites; i++) {
const jcv_point center = sites[i].p;
cerr << " site center " << center.x << ", " << center.y << endl;
double fill_factor_ours = fill_factors[sites[i].index];
/* Do not render halftone blobs that are too small */
@ -289,6 +295,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
e = e->next;
}
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 <<","<<cols<<" " << off_x << "," << off_y << endl;
if (std::regex_match(par_align, match, reg)) {
assert (match.size() == 3);
if (match[1].str() == "Min") {
@ -402,7 +410,6 @@ void gerbolyze::handle_aspect_ratio(string spec, double &scale_x, double &scale_
scale_x = scale_y = scale;
}
cerr << "res: "<< off_x << "," << off_y << endl;
}
@ -428,12 +435,16 @@ void gerbolyze::OpenCVContoursVectorizer::vectorize_image(xform2d &mat, const pu
draw_bg_rect(local_xf, width, height, clip_path, sink);
img->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);
}