From: <md...@us...> - 2008-01-18 14:44:44
|
Revision: 4875 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=4875&view=rev Author: mdboom Date: 2008-01-18 06:44:10 -0800 (Fri, 18 Jan 2008) Log Message: ----------- Add line simplification, to cut down on the number of line segments that need to be stroked. Affects *Agg and Gdk backends. Modified Paths: -------------- trunk/matplotlib/lib/matplotlib/backends/backend_gdk.py trunk/matplotlib/lib/matplotlib/path.py trunk/matplotlib/src/_backend_agg.cpp trunk/matplotlib/src/_path.cpp trunk/matplotlib/src/agg_py_path_iterator.h Modified: trunk/matplotlib/lib/matplotlib/backends/backend_gdk.py =================================================================== --- trunk/matplotlib/lib/matplotlib/backends/backend_gdk.py 2008-01-17 04:13:27 UTC (rev 4874) +++ trunk/matplotlib/lib/matplotlib/backends/backend_gdk.py 2008-01-18 14:44:10 UTC (rev 4875) @@ -84,7 +84,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): transform = transform + Affine2D(). \ scale(1.0, -1.0).translate(0, self.height) - polygons = path.to_polygons(transform) + polygons = path.to_polygons(transform, self.width, self.height) for polygon in polygons: # draw_polygon won't take an arbitrary sequence -- it must be a list # of tuples Modified: trunk/matplotlib/lib/matplotlib/path.py =================================================================== --- trunk/matplotlib/lib/matplotlib/path.py 2008-01-17 04:13:27 UTC (rev 4874) +++ trunk/matplotlib/lib/matplotlib/path.py 2008-01-18 14:44:10 UTC (rev 4875) @@ -283,7 +283,7 @@ new_codes = None return Path(vertices, new_codes) - def to_polygons(self, transform=None): + def to_polygons(self, transform=None, width=0, height=0): """ Convert this path to a list of polygons. Each polygon is an Nx2 array of vertices. In other words, each polygon has no @@ -292,13 +292,13 @@ if transform is not None: transform = transform.frozen() # Deal with the common and simple case - if self.codes is None: + if self.codes is None and len(self.vertices) < 100: if len(self.vertices): return [transform.transform(self.vertices)] return [] # Deal with the case where there are curves and/or multiple # subpaths (using extension code) - return convert_path_to_polygons(self, transform) + return convert_path_to_polygons(self, transform, width, height) _unit_rectangle = None #@classmethod Modified: trunk/matplotlib/src/_backend_agg.cpp =================================================================== --- trunk/matplotlib/src/_backend_agg.cpp 2008-01-17 04:13:27 UTC (rev 4874) +++ trunk/matplotlib/src/_backend_agg.cpp 2008-01-18 14:44:10 UTC (rev 4875) @@ -20,6 +20,7 @@ #include "agg_span_image_filter_gray.h" #include "agg_span_image_filter_rgba.h" #include "agg_span_interpolator_linear.h" +#include "agg_conv_shorten_path.h" #include "util/agg_color_conv_rgb8.h" #include "ft2font.h" @@ -84,31 +85,6 @@ return Py::String(PyString_FromStringAndSize((const char*)data, height*stride), true); } -template<class VertexSource> class conv_quantize -{ -public: - conv_quantize(VertexSource& source, bool quantize) : - m_source(&source), m_quantize(quantize) {} - - void rewind(unsigned path_id) { - m_source->rewind(path_id); - } - - unsigned vertex(double* x, double* y) { - unsigned cmd = m_source->vertex(x, y); - if (m_quantize && agg::is_vertex(cmd)) { - *x = round(*x) + 0.5; - *y = round(*y) + 0.5; - } - return cmd; - } - -private: - VertexSource* m_source; - bool m_quantize; -}; - - GCAgg::GCAgg(const Py::Object &gc, double dpi) : dpi(dpi), isaa(true), linewidth(1.0), alpha(1.0), dashOffset(0.0) @@ -358,8 +334,8 @@ template<class Path> bool should_snap(Path& path, const agg::trans_affine& trans) { - // If this contains only straight horizontal or vertical lines, quantize to nearest - // pixels + // If this contains only straight horizontal or vertical lines, it should be + // quantized to the nearest pixels double x0, y0, x1, y1; unsigned code; @@ -392,6 +368,11 @@ return true; } +template<class Path> +bool should_simplify(Path& path) { + return !path.has_curves() && path.total_vertices() > 5; +} + Py::Object RendererAgg::copy_from_bbox(const Py::Tuple& args) { //copy region in bbox to buffer and return swig/agg buffer object @@ -479,7 +460,7 @@ Py::Object RendererAgg::draw_markers(const Py::Tuple& args) { typedef agg::conv_transform<PathIterator> transformed_path_t; - typedef conv_quantize<transformed_path_t> quantize_t; + typedef SimplifyPath<transformed_path_t> simplify_t; typedef agg::conv_curve<transformed_path_t> curve_t; typedef agg::conv_stroke<curve_t> stroke_t; typedef agg::pixfmt_amask_adaptor<pixfmt, alpha_mask_type> pixfmt_amask_type; @@ -510,8 +491,8 @@ bool snap = should_snap(path, trans); transformed_path_t path_transformed(path, trans); GCAgg gc = GCAgg(gc_obj, dpi); - quantize_t path_quantized(path_transformed, snap); - path_quantized.rewind(0); + simplify_t path_simplified(path_transformed, snap, false, width, height); + path_simplified.rewind(0); facepair_t face = _get_rgba_face(face_obj, gc.alpha); @@ -564,7 +545,7 @@ agg::serialized_scanlines_adaptor_aa8::embedded_scanline sl; if (has_clippath) { - while (path_quantized.vertex(&x, &y) != agg::path_cmd_stop) { + while (path_simplified.vertex(&x, &y) != agg::path_cmd_stop) { pixfmt_amask_type pfa(*pixFmt, *alphaMask); amask_ren_type r(pfa); amask_aa_renderer_type ren(r); @@ -579,7 +560,7 @@ agg::render_scanlines(sa, sl, ren); } } else { - while (path_quantized.vertex(&x, &y) != agg::path_cmd_stop) { + while (path_simplified.vertex(&x, &y) != agg::path_cmd_stop) { if (face.first) { rendererAA->color(face.second); sa.init(fillCache, fillSize, x, y); @@ -881,8 +862,8 @@ Py::Object RendererAgg::draw_path(const Py::Tuple& args) { typedef agg::conv_transform<PathIterator> transformed_path_t; - typedef conv_quantize<transformed_path_t> quantize_t; - typedef agg::conv_curve<quantize_t> curve_t; + typedef SimplifyPath<transformed_path_t> simplify_t; + typedef agg::conv_curve<simplify_t> curve_t; _VERBOSE("RendererAgg::draw_path"); args.verify_length(3, 4); @@ -906,9 +887,11 @@ trans *= agg::trans_affine_scaling(1.0, -1.0); trans *= agg::trans_affine_translation(0.0, (double)height); bool snap = should_snap(path, trans); + bool simplify = should_simplify(path); + transformed_path_t tpath(path, trans); - quantize_t quantized(tpath, snap); - curve_t curve(quantized); + simplify_t simplified(tpath, snap, simplify, width, height); + curve_t curve(simplified); if (snap) gc.isaa = false; @@ -934,8 +917,8 @@ const Py::SeqBase<Py::Object>& linestyles_obj, const Py::SeqBase<Py::Int>& antialiaseds) { typedef agg::conv_transform<typename PathGenerator::path_iterator> transformed_path_t; - typedef conv_quantize<transformed_path_t> quantize_t; - typedef agg::conv_curve<quantize_t> quantized_curve_t; + typedef SimplifyPath<transformed_path_t> simplify_t; + typedef agg::conv_curve<simplify_t> simplified_curve_t; typedef agg::conv_curve<transformed_path_t> curve_t; GCAgg gc(dpi); @@ -1068,12 +1051,12 @@ gc.isaa = bool(Py::Int(antialiaseds[i % Naa])); transformed_path_t tpath(path, trans); - quantize_t quantized(tpath, snap); + simplify_t simplified(tpath, snap, false, width, height); if (has_curves) { - quantized_curve_t curve(quantized); + simplified_curve_t curve(simplified); _draw_path(curve, has_clippath, face, gc); } else { - _draw_path(quantized, has_clippath, face, gc); + _draw_path(simplified, has_clippath, face, gc); } } else { gc.isaa = bool(Py::Int(antialiaseds[i % Naa])); Modified: trunk/matplotlib/src/_path.cpp =================================================================== --- trunk/matplotlib/src/_path.cpp 2008-01-17 04:13:27 UTC (rev 4874) +++ trunk/matplotlib/src/_path.cpp 2008-01-18 14:44:10 UTC (rev 4875) @@ -1102,17 +1102,23 @@ Py::Object _path_module::convert_path_to_polygons(const Py::Tuple& args) { typedef agg::conv_transform<PathIterator> transformed_path_t; - typedef agg::conv_curve<transformed_path_t> curve_t; + typedef SimplifyPath<transformed_path_t> simplify_t; + typedef agg::conv_curve<simplify_t> curve_t; typedef std::vector<double> vertices_t; - args.verify_length(2); + args.verify_length(4); PathIterator path(args[0]); agg::trans_affine trans = py_to_agg_transformation_matrix(args[1], false); + double width = Py::Float(args[2]); + double height = Py::Float(args[3]); + bool simplify = !path.has_curves(); + transformed_path_t tpath(path, trans); - curve_t curve(tpath); + simplify_t simplified(tpath, false, simplify, width, height); + curve_t curve(simplified); Py::List polygons; vertices_t polygon; Modified: trunk/matplotlib/src/agg_py_path_iterator.h =================================================================== --- trunk/matplotlib/src/agg_py_path_iterator.h 2008-01-17 04:13:27 UTC (rev 4874) +++ trunk/matplotlib/src/agg_py_path_iterator.h 2008-01-18 14:44:10 UTC (rev 4875) @@ -6,6 +6,7 @@ #include "numpy/arrayobject.h" #include "agg_path_storage.h" #include "MPL_isnan.h" +#include <deque> class PathIterator { @@ -46,11 +47,16 @@ static const unsigned code_map[]; private: - inline unsigned vertex(unsigned idx, double* x, double* y) + inline void vertex(const unsigned idx, double* x, double* y) { char* pair = (char*)PyArray_GETPTR2(m_vertices, idx, 0); *x = *(double*)pair; *y = *(double*)(pair + PyArray_STRIDE(m_vertices, 1)); + } + + inline unsigned vertex_with_code(const unsigned idx, double* x, double* y) + { + vertex(idx, x, y); if (m_codes) { return code_map[(int)*(char *)PyArray_GETPTR1(m_codes, idx)]; @@ -65,11 +71,12 @@ inline unsigned vertex(double* x, double* y) { if (m_iterator >= m_total_vertices) return agg::path_cmd_stop; - unsigned code = vertex(m_iterator++, x, y); + unsigned code = vertex_with_code(m_iterator++, x, y); while ((MPL_isnan64(*x) || MPL_isnan64(*y)) && - m_iterator < m_total_vertices) { - vertex(m_iterator++, x, y); - code = agg::path_cmd_move_to; + m_iterator < m_total_vertices) + { + vertex(m_iterator++, x, y); + code = agg::path_cmd_move_to; } return code; } @@ -100,4 +107,368 @@ agg::path_cmd_end_poly | agg::path_flags_close }; +#define DEBUG_SIMPLIFY 0 + +template<class VertexSource> +class SimplifyPath +{ +public: + SimplifyPath(VertexSource& source, bool quantize, bool simplify, + double width = 0.0, double height = 0.0) : + m_source(&source), m_quantize(quantize), m_simplify(simplify), + m_width(width), m_height(height), + m_moveto(true), m_lastx(0.0), m_lasty(0.0), m_clipped(false), + m_do_clipping(width > 0.0 && height > 0.0), + m_origdx(0.0), m_origdy(0.0), + m_origdNorm2(0.0), m_dnorm2Max(0.0), m_dnorm2Min(0.0), + m_haveMin(false), m_lastMax(false), m_maxX(0.0), m_maxY(0.0), + m_minX(0.0), m_minY(0.0), m_lastWrittenX(0.0), m_lastWrittenY(0.0), + m_done(false) +#if DEBUG_SIMPLIFY + , m_pushed(0), m_skipped(0) +#endif + { + // empty + } + +#if DEBUG_SIMPLIFY + ~SimplifyPath() + { + printf("%d %d\n", m_pushed, m_skipped); + } +#endif + + void rewind(unsigned path_id) + { + m_source->rewind(path_id); + } + + unsigned vertex(double* x, double* y) + { + unsigned cmd; + + // The simplification algorithm doesn't support curves or compound paths + // so we just don't do it at all in that case... + if (!m_simplify) + { + cmd = m_source->vertex(x, y); + if (m_quantize && agg::is_vertex(cmd)) + { + *x = round(*x) + 0.5; + *y = round(*y) + 0.5; + } + return cmd; + } + + //idea: we can skip drawing many lines: lines < 1 pixel in length, lines + //outside of the drawing area, and we can combine sequential parallel lines + //into a single line instead of redrawing lines over the same points. + //The loop below works a bit like a state machine, where what it does depends + //on what it did in the last looping. To test whether sequential lines + //are close to parallel, I calculate the distance moved perpendicular to the + //last line. Once it gets too big, the lines cannot be combined. + + // This code was originally written by someone else (John Hunter?) and I + // have modified to work in-place -- meaning not creating an entirely + // new path list each time. In order to do that without too much + // additional code complexity, it keeps a small queue around so that + // multiple points can be emitted in a single call, and those points + // will be popped from the queue in subsequent calls. The following + // block will empty the queue before proceeding to the main loop below. + if (m_queue.size()) + { + const item& front = m_queue.front(); + unsigned cmd = front.cmd; + *x = front.x; + *y = front.y; + m_queue.pop_front(); + return cmd; + } + + // If the queue is now empty, and the path was fully consumed + // in the last call to the main loop, return agg::path_cmd_stop to + // signal that there are no more points to emit. + if (m_done) + return agg::path_cmd_stop; + + // The main simplification loop. The point is consume only as many + // points as necessary until some have been added to the outbound + // queue, not to run through the entire path in one go. This + // eliminates the need to allocate and fill an entire additional path + // array on each draw. + while ((cmd = m_source->vertex(x, y)) != agg::path_cmd_stop) + { + // Do any quantization if requested + if (m_quantize && agg::is_vertex(cmd)) + { + *x = round(*x) + 0.5; + *y = round(*y) + 0.5; + } + + //if we are starting a new path segment, move to the first point + // + init + if (m_moveto) + { + m_queue.push_back(item(agg::path_cmd_move_to, *x, *y)); + m_lastx = *x; + m_lasty = *y; + m_moveto = false; + m_origdNorm2 = 0.0; +#if DEBUG_SIMPLIFY + m_pushed++; +#endif + break; + } + + // Don't render line segments less than one pixel long + if (fabs(*x - m_lastx) < 1.0 && fabs(*y - m_lasty) < 1.0) + { +#if DEBUG_SIMPLIFY + m_skipped++; +#endif + continue; + } + + //skip any lines that are outside the drawing area. Note: More lines + //could be clipped, but a more involved calculation would be needed + if (m_do_clipping && + ((*x < -1 && m_lastx < -1) || + (*x > m_width + 1 && m_lastx > m_width + 1) || + (*y < -1 && m_lasty < -1) || + (*y > m_height + 1 && m_lasty > m_height + 1))) + { + m_lastx = *x; + m_lasty = *y; + m_clipped = true; + continue; + } + + // if we have no orig vector, set it to this vector and + // continue. + // this orig vector is the reference vector we will build + // up the line to + + if (m_origdNorm2 == 0) + { + if (m_clipped) + { + m_queue.push_back(item(agg::path_cmd_move_to, m_lastx, m_lasty)); + m_clipped = false; + } + + m_origdx = *x - m_lastx; + m_origdy = *y - m_lasty; + m_origdNorm2 = m_origdx*m_origdx + m_origdy+m_origdy; + + //set all the variables to reflect this new orig vecor + m_dnorm2Max = m_origdNorm2; + m_dnorm2Min = 0; + m_haveMin = false; + m_lastMax = true; + + m_maxX = *x; + m_maxY = *y; + m_minX = m_lastx; + m_minY = m_lasty; + + m_lastWrittenX = m_lastx; + m_lastWrittenY = m_lasty; + + // set the last point seen + m_lastx = *x; + m_lasty = *y; + continue; + } + + //if got to here, then we have an orig vector and we just got + //a vector in the sequence. + + //check that the perpendicular distance we have moved from the + //last written point compared to the line we are building is not too + //much. If o is the orig vector (we are building on), and v is the + //vector from the last written point to the current point, then the + //perpendicular vector is p = v - (o.v)o, and we normalize o (by + //dividing the second term by o.o). + + // get the v vector + double totdx = *x - m_lastWrittenX; + double totdy = *y - m_lastWrittenY; + double totdot = m_origdx*totdx + m_origdy*totdy; + + // get the para vector ( = (o.v)o/(o.o)) + double paradx = totdot*m_origdx/m_origdNorm2; + double parady = totdot*m_origdy/m_origdNorm2; + double paradNorm2 = paradx*paradx + parady*parady; + + // get the perp vector ( = v - para) + double perpdx = totdx - paradx; + double perpdy = totdy - parady; + double perpdNorm2 = perpdx*perpdx + perpdy*perpdy; + + //if the perp vector is less than some number of (squared) + //pixels in size, then merge the current vector + if (perpdNorm2 < 0.25) + { + //check if the current vector is parallel or + //anti-parallel to the orig vector. If it is parallel, test + //if it is the longest of the vectors we are merging in that + //direction. If anti-p, test if it is the longest in the + //opposite direction (the min of our final line) + + m_lastMax = false; + if (totdot >= 0) + { + if (paradNorm2 > m_dnorm2Max) + { + m_lastMax = true; + m_dnorm2Max = paradNorm2; + m_maxX = m_lastWrittenX + paradx; + m_maxY = m_lastWrittenY + parady; + } + } + else + { + m_haveMin = true; + if (paradNorm2 > m_dnorm2Min) + { + m_dnorm2Min = paradNorm2; + m_minX = m_lastWrittenX + paradx; + m_minY = m_lastWrittenY + parady; + } + } + + m_lastx = *x; + m_lasty = *y; + continue; + } + + //if we get here, then this vector was not similar enough to the + //line we are building, so we need to draw that line and start the + //next one. + + //if the line needs to extend in the opposite direction from the + //direction we are drawing in, move back to we start drawing from + //back there. + if (m_haveMin) + m_queue.push_back(item(agg::path_cmd_line_to, m_minX, m_minY)); + m_queue.push_back(item(agg::path_cmd_line_to, m_maxX, m_maxY)); + + //if we clipped some segments between this line and the next line + //we are starting, we also need to move to the last point. + if (m_clipped) + { + m_queue.push_back(item(agg::path_cmd_move_to, m_lastx, m_lasty)); + } + else if (!m_lastMax) + { + //if the last line was not the longest line, then move back to + //the end point of the last line in the sequence. Only do this + //if not clipped, since in that case lastx,lasty is not part of + //the line just drawn. + + //Would be move_to if not for the artifacts + m_queue.push_back(item(agg::path_cmd_line_to, m_lastx, m_lasty)); + } + + //now reset all the variables to get ready for the next line + + m_origdx = *x - m_lastx; + m_origdy = *y - m_lasty; + m_origdNorm2 = m_origdx*m_origdx + m_origdy*m_origdy; + + m_dnorm2Max = m_origdNorm2; + m_dnorm2Min = 0; + m_haveMin = false; + m_lastMax = true; + m_maxX = *x; + m_maxY = *y; + m_minX = m_lastx; + m_minY = m_lasty; + + m_lastWrittenX = m_lastx; + m_lastWrittenY = m_lasty; + + m_clipped = false; + + m_lastx = *x; + m_lasty = *y; +#if DEBUG_SIMPLIFY + m_pushed++; +#endif + break; + } + + // Fill the queue with the remaining vertices if we've finished the + // path in the above loop. Mark the path as done, so we don't call + // m_source->vertex again and segfault. + if (cmd == agg::path_cmd_stop) + { + if (m_origdNorm2 != 0) + { + if (m_haveMin) + m_queue.push_back(item(agg::path_cmd_line_to, m_minX, m_minY)); + m_queue.push_back(item(agg::path_cmd_line_to, m_maxX, m_maxY)); + } + m_done = true; + } + + // Return the first item in the queue, if any, otherwise + // indicate that we're done. + if (m_queue.size()) + { + const item& front = m_queue.front(); + unsigned cmd = front.cmd; + *x = front.x; + *y = front.y; + m_queue.pop_front(); + return cmd; + } + else + { + return agg::path_cmd_stop; + } + } + +private: + VertexSource* m_source; + bool m_quantize; + bool m_simplify; + double m_width, m_height; + + struct item + { + item(unsigned cmd_, const double& x_, double& y_) : + cmd(cmd_), x(x_), y(y_) {} + unsigned cmd; + double x; + double y; + }; + typedef std::deque<item> ItemQueue; + ItemQueue m_queue; + bool m_moveto; + double m_lastx, m_lasty; + bool m_clipped; + bool m_do_clipping; + + double m_origdx; + double m_origdy; + double m_origdNorm2; + double m_dnorm2Max; + double m_dnorm2Min; + bool m_haveMin; + bool m_lastMax; + double m_maxX; + double m_maxY; + double m_minX; + double m_minY; + double m_lastWrittenX; + double m_lastWrittenY; + bool m_done; + +#if DEBUG_SIMPLIFY + unsigned m_pushed; + unsigned m_skipped; +#endif +}; + #endif // __AGG_PY_PATH_ITERATOR_H__ This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |