Generic filter for polygons

This commit is contained in:
Matthias Koefferlein 2024-01-25 23:10:12 +01:00
parent 4b7c117cfd
commit b34f539fe1
3 changed files with 259 additions and 9 deletions

View File

@ -61,7 +61,7 @@ struct simple_polygon_defs
}
}
static point_type point (C *c, size_t p)
static point_type point (const C *c, size_t p)
{
if (c->hull ().size () > p) {
return c->hull ()[p];
@ -70,12 +70,12 @@ struct simple_polygon_defs
}
}
static size_t num_points (C *c)
static size_t num_points (const C *c)
{
return c->hull ().size ();
}
static bool is_empty (C *c)
static bool is_empty (const C *c)
{
return c->hull ().size () == 0;
}
@ -868,17 +868,17 @@ struct polygon_defs
}
}
static size_t num_points (C *c)
static size_t num_points (const C *c)
{
return c->vertices ();
}
static bool is_empty (C *c)
static bool is_empty (const C *c)
{
return c->vertices () == 0;
}
static point_type point_hull (C *c, size_t p)
static point_type point_hull (const C *c, size_t p)
{
if (c->hull ().size () > p) {
return c->hull ()[p];
@ -887,7 +887,7 @@ struct polygon_defs
}
}
static point_type point_hole (C *c, unsigned int n, size_t p)
static point_type point_hole (const C *c, unsigned int n, size_t p)
{
if (c->holes () > n && c->contour (n + 1).size () > p) {
return c->contour (n + 1)[p];
@ -896,12 +896,12 @@ struct polygon_defs
}
}
static size_t num_points_hull (C *c)
static size_t num_points_hull (const C *c)
{
return c->hull ().size ();
}
static size_t num_points_hole (C *c, unsigned int n)
static size_t num_points_hole (const C *c, unsigned int n)
{
return c->contour (n + 1).size ();
}

View File

@ -47,6 +47,192 @@
namespace gsi
{
// ---------------------------------------------------------------------------------
// PolygonFilter binding
class PolygonFilterImpl
: public db::AllMustMatchFilter
{
public:
PolygonFilterImpl ()
{
mp_vars = &m_mag_and_orient;
m_wants_variants = true;
m_requires_raw_input = false;
}
bool issue_selected (const db::Polygon &) const
{
return false;
}
virtual bool selected (const db::Polygon &polygon) const
{
if (f_selected.can_issue ()) {
return f_selected.issue<PolygonFilterImpl, bool, const db::Polygon &> (&PolygonFilterImpl::issue_selected, polygon);
} else {
return db::AllMustMatchFilter::selected (polygon);
}
}
virtual bool selected (const db::PolygonRef &polygon) const
{
db::Polygon p;
polygon.instantiate (p);
return selected (p);
}
virtual const db::TransformationReducer *vars () const
{
return mp_vars;
}
virtual bool requires_raw_input () const
{
return m_requires_raw_input;
}
void set_requires_raw_input (bool f)
{
m_requires_raw_input = f;
}
virtual bool wants_variants () const
{
return m_wants_variants;
}
void set_wants_variants (bool f)
{
m_wants_variants = f;
}
void is_isotropic ()
{
mp_vars = &m_mag;
}
void is_scale_invariant ()
{
mp_vars = &m_orientation;
}
void is_isotropic_and_scale_invariant ()
{
mp_vars = 0;
}
public:
const db::TransformationReducer *mp_vars;
db::OrientationReducer m_orientation;
db::MagnificationReducer m_mag;
db::MagnificationAndOrientationReducer m_mag_and_orient;
bool m_requires_raw_input;
bool m_wants_variants;
gsi::Callback f_selected;
};
Class<gsi::PolygonFilterImpl> decl_PluginFactory ("db", "PolygonFilter",
method ("requires_raw_input?", &PolygonFilterImpl::requires_raw_input,
"@brief Gets a value indicating whether the filter needs raw (unmerged) input\n"
"See \\requires_raw_input= for details.\n"
) +
method ("requires_raw_input=", &PolygonFilterImpl::set_requires_raw_input, gsi::arg ("flag"),
"@brief Sets a value indicating whether the filter needs raw (unmerged) input\n"
"This flag must be set before using this filter. It tells the filter implementation whether the "
"filter wants to have raw input (unmerged). The default value is 'false', meaning that\n"
"the filter will receive merged polygons. Setting this value to false potentially saves some\n"
"CPU time needed for merging the polygons.\n"
) +
method ("wants_variants?", &PolygonFilterImpl::wants_variants,
"@brief Gets a value indicating whether the filter prefers cell variants\n"
"See \\wants_variants= for details.\n"
) +
method ("wants_variants=", &PolygonFilterImpl::set_wants_variants, gsi::arg ("flag"),
"@brief Sets a value indicating whether the filter prefers cell variants\n"
"This flag must be set before using this filter. It tells the filter implementation whether cell "
"variants should be created (true, the default) or shape propagation will be applied (false).\n"
"\n"
"This decision needs to be make if the filter indicates that it will deliver different results\n"
"for scaled or rotated versions of the cell (see \\is_isotropic and the other hints). If a cell\n"
"is present with different respective qualities - as seen from the top cell - these instances\n"
"need to be differentiated. Cell variant formation is one way, shape propagation the other way.\n"
"Typically, cell variant formation is less expensive, but the hierarchy will be modified internally."
) +
method ("is_isotropic", &PolygonFilterImpl::is_isotropic,
"@brief Indicates that the filter has isotropic properties\n"
"Call this method before using the filter to indicate that the selection is independent of "
"the orientation of the shape. This helps the filter algorithm optimizing the filter run, specifically in "
"hierarchical mode.\n"
"\n"
"Examples for isotropic filters are area or perimeter filters."
) +
method ("is_scale_invariant", &PolygonFilterImpl::is_scale_invariant,
"@brief Indicates that the filter is scale invariant\n"
"Call this method before using the filter to indicate that the selection is independent of "
"the scale of the shape. This helps the filter algorithm optimizing the filter run, specifically in "
"hierarchical mode.\n"
"\n"
"An example for a scale invariant filter is the bounding box aspect ratio (height/width) filter."
) +
method ("is_isotropic_and_scale_invariant", &PolygonFilterImpl::is_isotropic_and_scale_invariant,
"@brief Indicates that the filter is isotropic and scale invariant\n"
"Call this method before using the filter to indicate that the selection is independent of "
"the scale and orientation of the shape. This helps the filter algorithm optimizing the filter run, specifically in "
"hierarchical mode.\n"
"\n"
"An example for such a filter is the rectangle selector."
) +
callback ("selected", &PolygonFilterImpl::issue_selected, &PolygonFilterImpl::f_selected, gsi::arg ("polygon"),
"@brief Selects a polygon\n"
"This method is the actual payload. It needs to be reimplemented in a derived class.\n"
"It needs to analyze the polygon and return 'true' if it should be kept and 'false' if it should be discarded."
),
"@brief A generic polygon filter adaptor\n"
"\n"
"Polygon filters are an efficient way to filter polygons from a Region. To apply a filter, derive your own "
"filter class and pass an instance to \\Region#filter or \\Region#filtered method.\n"
"\n"
"Conceptually, these methods take each polygon from the region and present it to the filter's 'selected' method.\n"
"Based on the result of this evaluation, the polygon is kept or discarded.\n"
"\n"
"The magic happens when deep mode regions are involved. In that case, the filter will use as few calls as possible "
"and exploit the hierarchical compression if possible. It needs to know however, how the filter behaves. You "
"need to configure the filter by calling \\is_isotropic, \\is_scale_invariant or \\is_isotropic_and_scale_invariant "
"before using the filter.\n"
"\n"
"You can skip this step, but the filter algorithm will assume the worst case then. This usually leads to cell variant "
"formation which is not always desired and blows up the hierarchy.\n"
"\n"
"Here is some example that filters triangles:"
"\n"
"@code\n"
"class TriangleFilter < RBA::PolygonFilter\n"
"\n"
" # Constructor\n"
" def initialize\n"
" self.is_isotropic_and_scale_invariant # the triangle nature is not dependent on the scale or orientation\n"
" end\n"
" \n"
" # Select only triangles\n"
" def selected(polygon)\n"
" return polygon.holes == 0 && polygon.num_points == 3\n"
" end\n"
"\n"
"end\n"
"\n"
"region = ... # some Region\n"
"triangles_only = region.filtered(TriangleFilter::new)\n"
"@/code\n"
"\n"
"This class has been introduced in version 0.29.\n"
);
// ---------------------------------------------------------------------------------
// Region binding
static inline std::vector<db::Region> as_2region_vector (const std::pair<db::Region, db::Region> &rp)
{
std::vector<db::Region> res;
@ -318,6 +504,16 @@ static db::Edges extent_refs_edges (const db::Region *r, double fx1, double fy1,
return r->processed (db::RelativeExtentsAsEdges (fx1, fy1, fx2, fy2));
}
static db::Region filtered (const db::Region *r, const PolygonFilterImpl *f)
{
return r->filtered (*f);
}
static void filter (db::Region *r, const PolygonFilterImpl *f)
{
r->filter (*f);
}
static db::Region with_perimeter1 (const db::Region *r, db::Region::perimeter_type perimeter, bool inverse)
{
db::RegionPerimeterFilter f (perimeter, perimeter + 1, inverse);
@ -2321,6 +2517,18 @@ Class<db::Region> decl_Region (decl_dbShapeCollection, "db", "Region",
"\n"
"This method has been introduced in version 0.28.\n"
) +
method_ext ("filter", &filter, gsi::arg ("filter"),
"@brief Applies a generic filter in place (replacing the polygons from the Region)\n"
"See \\PolygonFilter for a description of this feature.\n"
"\n"
"This method has been introduced in version 0.29.\n"
) +
method_ext ("filtered", &filtered, gsi::arg ("filtered"),
"@brief Applies a generic filter and returns a filtered copy\n"
"See \\PolygonFilter for a description of this feature.\n"
"\n"
"This method has been introduced in version 0.29.\n"
) +
method_ext ("rectangles", &rectangles,
"@brief Returns all polygons which are rectangles\n"
"This method returns all polygons in self which are rectangles."

View File

@ -30,6 +30,20 @@ def csort(s)
s.split(/(?<=\));(?=\()/).sort.join(";")
end
class TriangleFilter < RBA::PolygonFilter
# Constructor
def initialize
self.is_isotropic_and_scale_invariant # the triangle nature is not dependent on the scale or orientation
end
# Select only triangles
def selected(polygon)
return polygon.holes == 0 && polygon.num_points == 3
end
end
class DBRegion_TestClass < TestBase
# Basics
@ -1188,6 +1202,34 @@ class DBRegion_TestClass < TestBase
end
# Generic filters
def test_generic_filters
# Some basic tests for the filter class
f = TriangleFilter::new
assert_equal(f.wants_variants?, true)
f.wants_variants = false
assert_equal(f.wants_variants?, false)
assert_equal(f.requires_raw_input, false)
f.requires_raw_input = false
assert_equal(f.requires_raw_input, false)
# Smoke test
f.is_isotropic
f.is_scale_invariant
# Some application
region = RBA::Region::new
region.insert(RBA::Polygon::new([[0,0], [100, 100], [100,0]]))
region.insert(RBA::Box::new(200, 0, 300, 100))
assert_equal(region.filtered(TriangleFilter::new).to_s, "(0,0;100,100;100,0)")
end
end
load("test_epilogue.rb")