Binary vectorizer works

This commit is contained in:
jaseg 2021-01-28 22:34:38 +01:00
parent 9711fabab7
commit f65cd52304
4 changed files with 173 additions and 43 deletions

View file

@ -95,6 +95,8 @@ namespace gerbolyze {
public:
virtual void vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px) = 0;
};
ImageVectorizer *makeVectorizer(const std::string &name);
class RenderSettings {
public:

View file

@ -42,6 +42,9 @@ int main(int argc, char **argv) {
{"only_groups", {"-g", "--only-groups"},
"Comma-separated list of group IDs to export.",
1},
{"vectorizer", {"-b", "--vectorizer"},
"Vectorizer to use for bitmap images. One of poisson-disc (default), hex-grid, square-grid, binary-contours.",
1},
{"exclude_groups", {"-e", "--exclude-groups"},
"Comma-separated list of group IDs to exclude from export. Takes precedence over --only-groups.",
1},
@ -165,10 +168,18 @@ int main(int argc, char **argv) {
if (args["exclude_groups"])
id_match(args["exclude_groups"], sel.exclude);
VoronoiVectorizer vec(POISSON_DISC, true);
string vectorizer = args["vectorizer"] ? args["vectorizer"] : "poisson-disc";
ImageVectorizer *vec = makeVectorizer(vectorizer);
if (!vec) {
cerr << "Unknown vectorizer \"" << vectorizer << "\"." << endl;
argagg::fmt_ostream fmt(cerr);
fmt << usage.str() << argparser;
return EXIT_FAILURE;
}
RenderSettings rset {
0.1,
&vec
vec
};
doc.render(rset, flattener ? *flattener : *sink, &sel);

View file

@ -19,6 +19,7 @@
#include <cmath>
#include <string>
#include <iostream>
#include <algorithm>
#include <vector>
#include <opencv2/opencv.hpp>
#include "svg_import_util.h"
@ -29,6 +30,19 @@
using namespace gerbolyze;
using namespace std;
ImageVectorizer *gerbolyze::makeVectorizer(const std::string &name) {
if (name == "poisson-disc")
return new VoronoiVectorizer(POISSON_DISC, /* relax */ true);
else if (name == "hex-grid")
return new VoronoiVectorizer(HEXGRID, /* relax */ false);
else if (name == "square-grid")
return new VoronoiVectorizer(SQUAREGRID, /* relax */ false);
else if (name == "binary-contours")
return new OpenCVContoursVectorizer();
return nullptr;
}
/* debug function */
static void dbg_show_cv_image(cv::Mat &img) {
string windowName = "Debug image";
@ -60,39 +74,28 @@ static void voronoi_relax_points(const jcv_diagram* diagram, jcv_point* points)
}
}
/* Render image into gerber file.
*
* This function renders an image into a number of vector primitives emulating the images grayscale brightness by
* differently sized vector shaped giving an effect similar to halftone printing used in newspapers.
*
* On a high level, this function does this in four steps:
* 1. It preprocesses the source image at the pixel level. This involves several tasks:
* 1.1. It converts the image to grayscale.
* 1.2. It scales the image up or down to match the given minimum feature size.
* 1.3. It applies a blur depending on the given minimum feature size to prevent aliasing artifacts.
* 2. It randomly spread points across the image using poisson disc sampling. This yields points that have a fairly even
* average distance to each other across the image, and that have a guaranteed minimum distance that depends on
* minimum feature size.
* 3. It calculates a voronoi map based on this set of points and it calculats the polygon shape of each cell of the
* voronoi map.
* 4. It scales each of these voronoi cell polygons to match the input images brightness at the spot covered by this
* cell.
*/
void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px) {
void gerbolyze::parse_img_meta(const pugi::xml_node &node, double &x, double &y, double &width, double &height) {
/* Read XML node attributes */
auto x = usvg_double_attr(node, "x", 0.0);
auto y = usvg_double_attr(node, "y", 0.0);
auto width = usvg_double_attr(node, "width", 0.0);
auto height = usvg_double_attr(node, "height", 0.0);
x = usvg_double_attr(node, "x", 0.0);
y = usvg_double_attr(node, "y", 0.0);
width = usvg_double_attr(node, "width", 0.0);
height = usvg_double_attr(node, "height", 0.0);
assert (width > 0 && height > 0);
cerr << "image elem: w="<<width<<", h="<<height<<endl;
}
string gerbolyze::read_img_data(const pugi::xml_node &node) {
/* Read image from data:base64... URL */
string img_data = parse_data_iri(node.attribute("xlink:href").value());
if (img_data.empty()) {
cerr << "Warning: Empty or invalid image element with id \"" << node.attribute("id").value() << "\"" << endl;
return;
return "";
}
return img_data;
}
cv::Mat read_img_opencv(const pugi::xml_node &node) {
string img_data = read_img_data(node);
/* slightly annoying round-trip through the std:: and cv:: APIs */
vector<unsigned char> img_vec(img_data.begin(), img_data.end());
@ -102,19 +105,12 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_
if (img.empty()) {
cerr << "Warning: Could not decode content of image element with id \"" << node.attribute("id").value() << "\"" << endl;
return;
}
/* Set up target transform using SVG transform and x/y attributes */
cairo_save(cr);
apply_cairo_transform_from_svg(cr, node.attribute("transform").value());
cairo_translate(cr, x, y);
/* Adjust minimum feature size given in mm and translate into px document units in our local coordinate system. */
double f_x = min_feature_size_px, f_y = 0;
cairo_device_to_user_distance(cr, &f_x, &f_y);
min_feature_size_px = sqrt(f_x*f_x + f_y*f_y);
return img;
}
void gerbolyze::draw_bg_rect(cairo_t *cr, double width, double height, ClipperLib::Paths &clip_path, PolygonSink &sink, cairo_matrix_t &viewport_matrix) {
/* For both our debug SVG output and for the gerber output, we have to paint the image's bounding box in black as
* background for our halftone blobs. We cannot simply draw a rect here, though. Instead we have to first intersect
* the bounding box with the clip path we get from the caller, then we have to translate it into Cairo-SVG's
@ -149,8 +145,6 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_
cairo_restore(cr);
/* Second, draw into gerber. */
cairo_save(cr);
cairo_identity_matrix(cr);
for (const auto &poly : rect_out) {
vector<array<double, 2>> out;
for (const auto &p : poly)
@ -159,7 +153,46 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_
});
sink << GRB_POL_CLEAR << out;
}
cairo_restore(cr);
}
/* Render image into gerber file.
*
* This function renders an image into a number of vector primitives emulating the images grayscale brightness by
* differently sized vector shaped giving an effect similar to halftone printing used in newspapers.
*
* On a high level, this function does this in four steps:
* 1. It preprocesses the source image at the pixel level. This involves several tasks:
* 1.1. It converts the image to grayscale.
* 1.2. It scales the image up or down to match the given minimum feature size.
* 1.3. It applies a blur depending on the given minimum feature size to prevent aliasing artifacts.
* 2. It randomly spread points across the image using poisson disc sampling. This yields points that have a fairly even
* average distance to each other across the image, and that have a guaranteed minimum distance that depends on
* minimum feature size.
* 3. It calculates a voronoi map based on this set of points and it calculats the polygon shape of each cell of the
* voronoi map.
* 4. It scales each of these voronoi cell polygons to match the input images brightness at the spot covered by this
* cell.
*/
void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px) {
double x, y, width, height;
parse_img_meta(node, x, y, width, height);
cv::Mat img = read_img_opencv(node);
if (img.empty())
return;
cairo_save(cr);
/* Set up target transform using SVG transform and x/y attributes */
apply_cairo_transform_from_svg(cr, node.attribute("transform").value());
cairo_translate(cr, x, y);
/* Adjust minimum feature size given in mm and translate into px document units in our local coordinate system. */
double f_x = min_feature_size_px, f_y = 0;
cairo_device_to_user_distance(cr, &f_x, &f_y);
min_feature_size_px = sqrt(f_x*f_x + f_y*f_y);
draw_bg_rect(cr, width, height, clip_path, sink, viewport_matrix);
/* Set up a poisson-disc sampled point "grid" covering the image. Calculate poisson disc parameters from given
* minimum feature size. */
@ -320,8 +353,6 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_
cairo_restore(cr);
/* And finally, export halftone blob to gerber. */
cairo_save(cr);
cairo_identity_matrix(cr);
for (const auto &poly : polys) {
vector<array<double, 2>> out;
for (const auto &p : poly)
@ -330,7 +361,6 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_
});
sink << GRB_POL_DARK << out;
}
cairo_restore(cr);
}
blurred.release();
@ -339,4 +369,80 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_
cairo_restore(cr);
}
void gerbolyze::OpenCVContoursVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px) {
double x, y, width, height;
parse_img_meta(node, x, y, width, height);
cv::Mat img = read_img_opencv(node);
if (img.empty())
return;
cairo_save(cr);
/* Set up target transform using SVG transform and x/y attributes */
apply_cairo_transform_from_svg(cr, node.attribute("transform").value());
cairo_translate(cr, x, y);
draw_bg_rect(cr, width, height, clip_path, sink, viewport_matrix);
vector<vector<cv::Point>> contours;
vector<cv::Vec4i> hierarchy;
cv::findContours(img, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_TC89_KCOS);
queue<pair<size_t, bool>> child_stack;
child_stack.push({ 0, true });
while (!child_stack.empty()) {
bool dark = child_stack.front().second;
for (int i=child_stack.front().first; i>=0; i = hierarchy[i][0]) {
if (hierarchy[i][2] >= 0) {
child_stack.push({ hierarchy[i][2], !dark });
}
sink << (dark ? GRB_POL_DARK : GRB_POL_CLEAR);
bool is_clockwise = cv::contourArea(contours[i], true) > 0;
if (!is_clockwise)
std::reverse(contours[i].begin(), contours[i].end());
ClipperLib::Path out;
for (const auto &p : contours[i]) {
double x = (double)p.x / (double)img.cols * (double)width;
double y = (double)p.y / (double)img.rows * (double)height;
cairo_user_to_device(cr, &x, &y);
out.push_back({ (ClipperLib::cInt)round(x * clipper_scale), (ClipperLib::cInt)round(y * clipper_scale) });
}
ClipperLib::Clipper c;
c.AddPath(out, ClipperLib::ptSubject, /* closed */ true);
if (!clip_path.empty()) {
c.AddPaths(clip_path, ClipperLib::ptClip, /* closed */ true);
}
c.StrictlySimple(true);
ClipperLib::Paths polys;
c.Execute(ClipperLib::ctIntersection, polys, ClipperLib::pftNonZero, ClipperLib::pftNonZero);
/* Finally, translate into Cairo-SVG's document units and draw. */
cairo_save(cr);
cairo_set_matrix(cr, &viewport_matrix);
cairo_new_path(cr);
ClipperLib::cairo::clipper_to_cairo(polys, cr, CAIRO_PRECISION, ClipperLib::cairo::tNone);
cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0);
/* First, draw into SVG */
cairo_fill(cr);
cairo_restore(cr);
/* Second, draw into gerber. */
for (const auto &poly : polys) {
vector<array<double, 2>> out;
for (const auto &p : poly)
out.push_back(std::array<double, 2>{
((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale
});
sink << out;
}
}
child_stack.pop();
}
cairo_restore(cr);
}

View file

@ -30,10 +30,21 @@ namespace gerbolyze {
public:
VoronoiVectorizer(grid_type grid, bool relax=true) : m_relax(relax), m_grid_type(grid) {}
void vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px);
virtual void vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px);
private:
double m_relax;
grid_type m_grid_type;
};
class OpenCVContoursVectorizer : public ImageVectorizer {
public:
OpenCVContoursVectorizer() {}
virtual void vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px);
};
void parse_img_meta(const pugi::xml_node &node, double &x, double &y, double &width, double &height);
std::string read_img_data(const pugi::xml_node &node);
void draw_bg_rect(cairo_t *cr, double width, double height, ClipperLib::Paths &clip_path, PolygonSink &sink, cairo_matrix_t &viewport_matrix);
}