aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2021-01-28 22:34:38 +0100
committerjaseg <git@jaseg.de>2021-01-28 22:34:38 +0100
commitf65cd52304a9a57118d82e527469839d5663e7ba (patch)
treeffcf4f638b7df7aaf8d222b2792a811ac79c5068
parent9711fabab7893fcdd0a5708c79a7d48f2ad15167 (diff)
downloadgerbolyze-f65cd52304a9a57118d82e527469839d5663e7ba.tar.gz
gerbolyze-f65cd52304a9a57118d82e527469839d5663e7ba.tar.bz2
gerbolyze-f65cd52304a9a57118d82e527469839d5663e7ba.zip
Binary vectorizer works
-rw-r--r--include/gerbolyze.hpp2
-rw-r--r--src/main.cpp15
-rw-r--r--src/vec_core.cpp186
-rw-r--r--src/vec_core.h13
4 files changed, 173 insertions, 43 deletions
diff --git a/include/gerbolyze.hpp b/include/gerbolyze.hpp
index 3078a6f..01d089a 100644
--- a/include/gerbolyze.hpp
+++ b/include/gerbolyze.hpp
@@ -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:
diff --git a/src/main.cpp b/src/main.cpp
index ba75204..6484295 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -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);
diff --git a/src/vec_core.cpp b/src/vec_core.cpp
index 85b33b0..6beab19 100644
--- a/src/vec_core.cpp
+++ b/src/vec_core.cpp
@@ -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);
+}
diff --git a/src/vec_core.h b/src/vec_core.h
index 9df8598..06099ab 100644
--- a/src/vec_core.h
+++ b/src/vec_core.h
@@ -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);
}