diff options
-rw-r--r-- | include/gerbolyze.hpp | 2 | ||||
-rw-r--r-- | src/main.cpp | 15 | ||||
-rw-r--r-- | src/vec_core.cpp | 186 | ||||
-rw-r--r-- | src/vec_core.h | 13 |
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); } |