Generic polygon to polygon processors

This commit is contained in:
Matthias Koefferlein 2024-01-26 13:05:09 +01:00
parent 9fbc926d67
commit 1a126ede45
4 changed files with 411 additions and 2 deletions

View File

@ -48,6 +48,9 @@ class DB_PUBLIC_TEMPLATE shape_collection_processor
: public tl::Object
{
public:
typedef Shape shape_type;
typedef Result result_type;
/**
* @brief Constructor
*/

View File

@ -238,6 +238,220 @@ private:
bool m_wants_variants;
};
// ---------------------------------------------------------------------------------
// Generic shape processor declarations
template <class ProcessorBase>
class shape_processor_impl
: public ProcessorBase
{
public:
typedef typename ProcessorBase::shape_type shape_type;
typedef typename ProcessorBase::result_type result_type;
shape_processor_impl ()
{
mp_vars = &m_mag_and_orient;
m_wants_variants = true;
m_requires_raw_input = false;
m_result_is_merged = false;
m_result_must_not_be_merged = false;
}
// overrides virtual method
virtual const db::TransformationReducer *vars () const
{
return mp_vars;
}
// maybe overrides virtual method
virtual bool requires_raw_input () const
{
return m_requires_raw_input;
}
void set_requires_raw_input (bool f)
{
m_requires_raw_input = f;
}
// overrides virtual method
virtual bool wants_variants () const
{
return m_wants_variants;
}
void set_wants_variants (bool f)
{
m_wants_variants = f;
}
// overrides virtual method
virtual bool result_is_merged () const
{
return m_result_is_merged;
}
void set_result_is_merged (bool f)
{
m_result_is_merged = f;
}
// overrides virtual method
virtual bool result_must_not_be_merged () const
{
return m_result_must_not_be_merged;
}
void set_result_must_not_be_merged (bool f)
{
m_result_must_not_be_merged = 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;
}
virtual void process (const shape_type &shape, std::vector<result_type> &res) const
{
res = do_process (shape);
}
std::vector<result_type> issue_do_process (const shape_type &) const
{
return std::vector<result_type> ();
}
std::vector<result_type> do_process (const shape_type &shape) const
{
if (f_process.can_issue ()) {
return f_process.issue<shape_processor_impl, std::vector<result_type>, const shape_type &> (&shape_processor_impl::issue_do_process, shape);
} else {
return issue_do_process (shape);
}
}
gsi::Callback f_process;
static gsi::Methods method_decls (bool with_merged_options)
{
gsi::Methods decls =
callback ("process", &shape_processor_impl::issue_do_process, &shape_processor_impl::f_process, gsi::arg ("shape"),
"@brief Processes a shape\n"
"This method is the actual payload. It needs to be reimplemented in a derived class.\n"
"If needs to process the input shape and deliver a list of output shapes.\n"
"The output list may be empty to entirely discard the input shape. It may also contain more than a single shape.\n"
"In that case, the number of total shapes may grow during application of the processor.\n"
);
if (with_merged_options) {
decls +=
method ("requires_raw_input?", &shape_processor_impl::requires_raw_input,
"@brief Gets a value indicating whether the processor needs raw (unmerged) input\n"
"See \\requires_raw_input= for details.\n"
) +
method ("requires_raw_input=", &shape_processor_impl::set_requires_raw_input, gsi::arg ("flag"),
"@brief Sets a value indicating whether the processor needs raw (unmerged) input\n"
"This flag must be set before using this processor. It tells the processor implementation whether the "
"processor wants to have raw input (unmerged). The default value is 'false', meaning that\n"
"the processor will receive merged polygons ('merged semantics').\n"
"\n"
"Setting this value to false potentially saves some CPU time needed for merging the polygons.\n"
"Also, raw input means that strange shapes such as dot-like edges, self-overlapping polygons, "
"empty or degenerated polygons are preserved."
) +
method ("result_is_merged?", &shape_processor_impl::result_is_merged,
"@brief Gets a value indicating whether the processor delivers merged output\n"
"See \\result_is_merged= for details.\n"
) +
method ("result_is_merged=", &shape_processor_impl::set_result_is_merged, gsi::arg ("flag"),
"@brief Sets a value indicating whether the processor delivers merged output\n"
"This flag must be set before using this processor. If the processor maintains the merged condition\n"
"by design (output is merged if input is), it is a good idea to set this predicate to 'true'.\n"
"This will avoid additional merge steps when the resulting collection is used in further operations\n"
"that need merged input\n."
) +
method ("result_must_not_be_merged?", &shape_processor_impl::result_must_not_be_merged,
"@brief Gets a value indicating whether the processor's output must not be merged\n"
"See \\result_must_not_be_merged= for details.\n"
) +
method ("result_must_not_be_merged=", &shape_processor_impl::set_result_must_not_be_merged, gsi::arg ("flag"),
"@brief Sets a value indicating whether the processor's output must not be merged\n"
"This flag must be set before using this processor. The processor can set this flag if it wants to\n"
"deliver shapes that must not be merged - e.g. point-like edges or strange or degenerated polygons.\n."
);
}
decls +=
method ("wants_variants?", &shape_processor_impl::wants_variants,
"@brief Gets a value indicating whether the filter prefers cell variants\n"
"See \\wants_variants= for details.\n"
) +
method ("wants_variants=", &shape_processor_impl::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 for hierarchical applications (deep mode). "
"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 made, if the filter indicates that it will deliver different results\n"
"for scaled or rotated versions of the shape (see \\is_isotropic and the other hints). If a cell\n"
"is present with different qualities - as seen from the top cell - the respective 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."
) +
method ("is_isotropic", &shape_processor_impl::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 (polygon) processors are size or shrink operators. Size or shrink is not dependent "
"on orientation unless size or shrink needs to be different in x and y direction."
) +
method ("is_scale_invariant", &shape_processor_impl::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 (polygon) processor is the rotation operator. Rotation is not depending on scale, "
"but on the original orientation as mirrored versions need to be rotated differently."
) +
method ("is_isotropic_and_scale_invariant", &shape_processor_impl::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 (polygon) processor is the convex decomposition operator. The decomposition of a polygon into "
"convex parts is an operation that is not depending on scale nor orientation."
);
return decls;
}
private:
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;
bool m_result_is_merged;
bool m_result_must_not_be_merged;
};
}
#endif

View File

@ -127,6 +127,102 @@ Class<gsi::PolygonFilterImpl> decl_PolygonFilterImpl ("db", "PolygonFilter",
"This class has been introduced in version 0.29.\n"
);
// ---------------------------------------------------------------------------------
// PolygonProcessor binding
Class<shape_processor_impl<db::PolygonProcessorBase> > decl_PolygonProcessor ("db", "PolygonProcessor",
shape_processor_impl<db::PolygonProcessorBase>::method_decls (true),
"@brief A generic polygon processor adaptor\n"
"\n"
"Polygon processors are an efficient way to process polygons from a Region. To apply a processors, derive your own "
"processors class and pass an instance to \\Region#process or \\Region#processed method.\n"
"\n"
"Conceptually, these methods take each polygon from the region and present it to the processor's 'process' method.\n"
"The result of this call is a list of zero to many output polygons derived from the input polygon.\n"
"The output region is the sum over all these individual results.\n"
"\n"
"The magic happens when deep mode regions are involved. In that case, the processor will use as few calls as possible "
"and exploit the hierarchical compression if possible. It needs to know however, how the processor behaves. You "
"need to configure the processor by calling \\is_isotropic, \\is_scale_invariant or \\is_isotropic_and_scale_invariant "
"before using it.\n"
"\n"
"You can skip this step, but the processor 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 shrinks every polygon to half of the size but does not change the position.\n"
"In this example the 'position' is defined by the center of the bounding box:"
"\n"
"@code\n"
"class ShrinkToHalfProcessor < RBA::PolygonProcessor\n"
"\n"
" # Constructor\n"
" def initialize\n"
" self.is_isotropic_and_scale_invariant # scale or orientation do not matter\n"
" end\n"
" \n"
" # Shrink to half size\n"
" def process(polygon)\n"
" shift = polygon.bbox.center - RBA::Point::new # shift vector\n"
" return [ (polygon.moved(-shift) * 0.5).moved(shift) ]\n"
" end\n"
"\n"
"end\n"
"\n"
"region = ... # some Region\n"
"shrinked_to_half = region.processed(ShrinkToHalf::new)\n"
"@/code\n"
"\n"
"This class has been introduced in version 0.29.\n"
);
Class<shape_processor_impl<db::PolygonToEdgeProcessorBase> > decl_PolygonToEdgeProcessor ("db", "PolygonToEdgeProcessor",
shape_processor_impl<db::PolygonToEdgeProcessorBase>::method_decls (true),
"@brief A generic polygon-to-edge processor adaptor\n"
"\n"
"Polygon processors are an efficient way to process polygons from a Region. To apply a processors, derive your own "
"processors class and pass an instance to \\Region#processed method.\n"
"\n"
"Conceptually, these methods take each polygon from the region and present it to the processor's 'process' method.\n"
"The result of this call is a list of zero to many output edges derived from the input polygon.\n"
"The output edge collection is the sum over all these individual results.\n"
"\n"
"The magic happens when deep mode regions are involved. In that case, the processor will use as few calls as possible "
"and exploit the hierarchical compression if possible. It needs to know however, how the processor behaves. You "
"need to configure the processor by calling \\is_isotropic, \\is_scale_invariant or \\is_isotropic_and_scale_invariant "
"before using it.\n"
"\n"
"You can skip this step, but the processor 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"
"For a basic example see the \\PolygonProcessor class, with the exception that this incarnation has to deliver edges.\n"
"\n"
"This class has been introduced in version 0.29.\n"
);
Class<shape_processor_impl<db::PolygonToEdgePairProcessorBase> > decl_PolygonToEdgePairProcessor ("db", "PolygonToEdgePairProcessor",
shape_processor_impl<db::PolygonToEdgePairProcessorBase>::method_decls (true),
"@brief A generic polygon-to-edge-pair processor adaptor\n"
"\n"
"Polygon processors are an efficient way to process polygons from a Region. To apply a processors, derive your own "
"processors class and pass an instance to \\Region#processed method.\n"
"\n"
"Conceptually, these methods take each polygon from the region and present it to the processor's 'process' method.\n"
"The result of this call is a list of zero to many output edge pairs derived from the input polygon.\n"
"The output edge pair collection is the sum over all these individual results.\n"
"\n"
"The magic happens when deep mode regions are involved. In that case, the processor will use as few calls as possible "
"and exploit the hierarchical compression if possible. It needs to know however, how the processor behaves. You "
"need to configure the processor by calling \\is_isotropic, \\is_scale_invariant or \\is_isotropic_and_scale_invariant "
"before using it.\n"
"\n"
"You can skip this step, but the processor 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"
"For a basic example see the \\PolygonProcessor class, with the exception that this incarnation has to deliver edge pairs.\n"
"\n"
"This class has been introduced in version 0.29.\n"
);
// ---------------------------------------------------------------------------------
// Region binding
@ -411,6 +507,26 @@ static void filter (db::Region *r, const PolygonFilterImpl *f)
r->filter (*f);
}
static db::Region processed_pp (const db::Region *r, const shape_processor_impl<db::PolygonProcessorBase> *f)
{
return r->processed (*f);
}
static void process_pp (db::Region *r, const shape_processor_impl<db::PolygonProcessorBase> *f)
{
r->process (*f);
}
static db::EdgePairs processed_pep (const db::Region *r, const shape_processor_impl<db::PolygonToEdgePairProcessorBase> *f)
{
return r->processed (*f);
}
static db::Edges processed_pe (const db::Region *r, const shape_processor_impl<db::PolygonToEdgeProcessorBase> *f)
{
return r->processed (*f);
}
static db::Region with_perimeter1 (const db::Region *r, db::Region::perimeter_type perimeter, bool inverse)
{
db::RegionPerimeterFilter f (perimeter, perimeter + 1, inverse);
@ -2426,6 +2542,30 @@ Class<db::Region> decl_Region (decl_dbShapeCollection, "db", "Region",
"\n"
"This method has been introduced in version 0.29.\n"
) +
method_ext ("process", &process_pp, gsi::arg ("process"),
"@brief Applies a generic polygon processor in place (replacing the polygons from the Region)\n"
"See \\PolygonProcessor for a description of this feature.\n"
"\n"
"This method has been introduced in version 0.29.\n"
) +
method_ext ("processed", &processed_pp, gsi::arg ("processed"),
"@brief Applies a generic polygon processor and returns a processed copy\n"
"See \\PolygonProcessor for a description of this feature.\n"
"\n"
"This method has been introduced in version 0.29.\n"
) +
method_ext ("processed", &processed_pep, gsi::arg ("processed"),
"@brief Applies a generic polygon-to-edge-pair processor and returns an edge pair collection with the results\n"
"See \\PolygonToEdgePairProcessor for a description of this feature.\n"
"\n"
"This method has been introduced in version 0.29.\n"
) +
method_ext ("processed", &processed_pe, gsi::arg ("processed"),
"@brief Applies a generic polygon-to-edge processor and returns an edge collection with the results\n"
"See \\PolygonToEdgeProcessor 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

@ -44,6 +44,21 @@ class TriangleFilter < RBA::PolygonFilter
end
class ShrinkToHalfProcessor < RBA::PolygonProcessor
# Constructor
def initialize
self.is_isotropic_and_scale_invariant # scale or orientation do not matter
end
# Shrink to half size
def process(polygon)
shift = polygon.bbox.center - RBA::Point::new # shift vector
return [ (polygon.moved(-shift) * 0.5).moved(shift) ]
end
end
class DBRegion_TestClass < TestBase
# Basics
@ -1212,8 +1227,8 @@ class DBRegion_TestClass < TestBase
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)
f.requires_raw_input = true
assert_equal(f.requires_raw_input, true)
# Smoke test
f.is_isotropic
@ -1233,6 +1248,43 @@ class DBRegion_TestClass < TestBase
end
# Generic processors
def test_generic_processors
# Some basic tests for the filter class
f = ShrinkToHalfProcessor::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 = true
assert_equal(f.requires_raw_input, true)
assert_equal(f.result_is_merged, false)
f.result_is_merged = true
assert_equal(f.result_is_merged, true)
assert_equal(f.result_must_not_be_merged, false)
f.result_must_not_be_merged = true
assert_equal(f.result_must_not_be_merged, true)
# 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.processed(ShrinkToHalfProcessor::new).to_s, "(25,25;75,75;75,25);(225,25;225,75;275,75;275,25)")
assert_equal(region.to_s, "(0,0;100,100;100,0);(200,0;200,100;300,100;300,0)")
region.process(ShrinkToHalfProcessor::new)
assert_equal(region.to_s, "(25,25;75,75;75,25);(225,25;225,75;275,75;275,25)")
end
end
load("test_epilogue.rb")