homog2d library
Manual

Home page: github.com/skramm/homog2d

This is the user manual for the current master branch of homog2d. For stable releases, see https://github.com/skramm/homog2d/releases .

To get the reference manual (requires Doxygen), you can either type $ make doc or $ doxygen misc/doxyfile (online here

If you are interested in code, check the page homog2d_devinfo.md.

Index is available here.

  1. Introduction
  2. Lines and points
  3. Other geometric primitives
  4. 2D transformation (aka homographies)
  5. Intersections and enclosings determination
  6. Misc. features
  7. Bindings
  8. Drawing things
  9. Numerical data types
  10. SVG import
  11. Technical details
  12. Runtime polymorphism
  13. FAQ
  14. History

1 - Introduction

All the code is in the namespace h2d, so either add using namespace h2d;, either use it as a prefix on each type (There are a lot of sub-namespaces, but unneeded for the end-user).

This library provides several main data types: lines, points, segments, circles, ellipses, polyline and homography matrices. These can be used to transform one of the basic types (planar transformation). It is implemented as a homogeneous 3x3 matrix.

All the data types implement a comparison operator ( == and !=).

This library does not provide exact arithmetic, it relies instead on basic floating-point types for storage and computations, but user can select the underlying type. The types are fully templated by the underlying numerical type. If more precision is required, an external library providing arbitrary binary size for numerical values can be used, see here.

To make things simple, we introduce here only the default type, based on double (see Numerical data types for details).

The API is exposed both as member functions and as free functions. Say for example you have a type AAA on which you can apply the foo() operation. Both of these are possible:

{C++}
AAA thing;
auto v1 = thing.foo();
auto v2 = foo( thing );
BBB some_argument;
auto z1 = thing.foo( some_argument );
auto z2 = foo( thing, some_argument );

2 - Lines and points

2.1 - General usage

Beware, two parallel lines will never cross, and two identical points do not define a line. So if your code attempts to do so, this will trigger a std::runtime_error exception.
If you are unsure, you can check first:

{C++}
if( li1.isParallelTo(li2) )
cout << "Lines are parallel !\n";
if( pt1 == pt2 )
cout << "Points are identical !\n";

For the numerical considerations on this, check below, section Numerical data types.

But if its about printing, then the easiest is to use the overloaded operators:

{C++}
Line2d li;
std::cout << li << '\n';

(BTW: this is the same for all the other primitives, points and others).

For points, you may also use the corresponding free function:

{C++}
Point2d pt1, pt2; // some points
auto d = dist( pt1, pt2 );
{C++}
Line2d line;
Point2d pt;
auto seg_orth = line.getOrthogSegment( pt );

This is illustrated on this figure, showing a rotating point and the computed orthogonal segment to a line (source):

showcase8.gif
showcase8

Upon return, the first point will hold the intersection point (projection of point on line), and the second will hold the given point.

2.2 - Get point(s) lying on line

To get a point lying on a line, you can provide one of its coordinates and get the other coordinate value, using the enum GivenCoord. For example, if you build the line going through (0,0)-(4,2) with:

{C++}
Line2d li(4,2);

You can compute the coordinate of y for x=2 with:

{C++}
auto y = li.getCoord( GivenCoord::X, 2 );

or get the coordinate of x for y=1 with:

{C++}
auto x = li.getCoord( GivenCoord::Y, 1 );

You can also get directly the point with:

{C++}
Point2d pt2 = li.getPoint( GivenCoord::X, 2 );

Some care has to be taken with these functions, because they will throw if unable to provide a result. For example, if you try to get the 'y' coordinate from a vertical line, this will fail. The following code will throw:

{C++}
Line2d li; // vertical line at x=0
auto y = li.getCoord( GivenCoord::X, 0 );

2.3 - Points at equal distance from a point on line

You can compute the two points that are lying on a line and at a given distance from a point.

fig1.png
Computing the 2 points at a given distance of a point on a line

The API provides two ways to get these:

1 - either you provide the x or y coordinate of p0:

{C++}
Line2d li( ..., ... ); // some line
auto ppts = li.getPoints( GivenCoord::X, coord, dist ); // returns a std::pair
Point2d p1 = ppts.first;
Point2d p2 = ppts.second;

The drawback is same as the above paragraph: if line is vertical and you provide a 'x' coordinate, this will throw (and similarly with an horizontal line and a 'y' coordinate).

2 - either you provide directly the point:

{C++}
Line2d li( ..., ... ); // some line
Point2d pt_on_line;
auto ppts = li.getPoints( pt_on_line, dist ); // returns a std::pair

The drawback is that you need to be sure that this point is truely on the line. This will be checked for, and library will throw if not (except if error checking is disabled, see build options).

Upon return, the "first" point will hold the one with smallest 'x' coordinates, or the smallest 'y' coordinates if x coordinates are equal.

2.4 - Orthogonal and parallel lines, angles between two lines

You can compute a line orthogonal to another one at a given coordinate, using the above enum. For example, this:

{C++}
auto lB = lA.getOrthogLine( GivenCoord::X, x1 );

will build lB so that it is orthogonal to lA at x=x1.

fig3.png
Orthogonal lines

Similarly, you can also directly use as input a point on the line. But this time, the point does not need to be on the line:

{C++}
Line2d liB = lA.getOrthogLine( p1 );
OrthogLines

You can get a line parallel to another one with the member function getParallelLine(). This function takes as argument a point that the line will intersect.

{C++}
Line2d li1; // some line
Point2d pt; // some point
auto li2 = li1.getParallelLine( pt ); // pt will be lying on li2, wich is parallel to li1

If you need a parallel line to another at a given distance, then you can use getParallelLines(), provided either as a free function or as a member function. They both return a pair (std::pair) of lines, one on one side, and the other on the other side of the input line:

{C++}
Line2d li; // some line
auto p_lines1 = li.getParallelLines( 100 );
auto p_lines2 = getParallelLines( li, 50 ); // free function
auto liA = p_lines1.first;
auto liB = p_lines1.second;

If you know that two lines are parallel and you want the distance between them, then you have:

{C++}
auto dist = getParallelDistance( li1, li2 );

This will throw if lines are not parallel (unless error checking is disabled).

You can compute the angle in Radians between two lines, either with a member function or with a free function. Whatever the lines, the returned value will lie in the range [0-PI/2] (no negative angles):

{C++}
auto angle1 = li2.getAngle( li1 );
auto angle2 = getAngle( li1, li2 ); // free function

You can rotate a line at a given point (must be lying on the line):

{C++}
auto li1 = pt1 * pt2;
auto li2 = li1.getRotatedLine( pt1, angle /* in rads */ );

As lines are not oriented by design, positive and negative angles will achieve the same rotation Rotation by a value of M_PI (auto li2 = li.getRotatedLine( pt, M_PI );) will produce an identical line.

Any angle value above 90° (in the range [M_PI/2–M_PI]) will produce a line rotated by an angle of M_PI-angle

See demo here: https://godbolt.org/z/j3nT563P4

2.5 - Side of a point related to a line

The function side( Point2d, Line2d ); will return an int value, either -1, 1 or 0 if point lies on the line.

The tipping point is the horizontal line.

side_point_line_1.svg
side_pl1
side_point_line_2.svg
side_pl2

See showcase16 for an example.

2.6 - Homogeneous coordinates and infinity

As points and lines are stored with homogeneous coordinates, one can also build theses using 3 values:

{C++}
// horizontal line at y=0
auto li1 = Line2d( 0, 1, 0 ); // or: Line2d li( 0, 1, 0 );
// a point at x=1, y=1
auto pt1 = Point2d( 1, 1, 1 ); //: or Point2d pt1( 1, 1, 1 );

It is also possible to initialize existing variables with homogeneous coordinates:

{C++}
li1.set( 1, 2, 3 );
pt1.set( 4, 6, 2 ); // point lies at x=2, y=3

A container such as std::vector or std::array can be used too:

{C++}
std::vector<float> vec{1,2,3};
Line2d li( vec );
std::array<double,3> arr{1,2,3};
Point2d pt( arr );

Storing points and lines with homogeneous coordinates allows us to handle points at infinity:

{C++}
Point2d pt(1,2,0);
assert( pt.isInf() );

Such points can still be used, for example to build a line or a segment:

{C++}
Point2d pt(1,0,0);
Line2d li = pt * Point2d();

3 - Other geometric primitives

Besides points and lines, the following primitives are provided:

They all share some common member functions, see section 3.6.

The ones that may cover some area (polyline, rectangle, ellipse, circle) provide a function getBB() that returns a flat rectangle holding the bounding box. This is not available for points, lines, and segments (as a segment can be vertical or horizontal, it does not have an area).

On this figure, you can see a circle, a polyline and and ellipse, along with their corresponding bounding boxes. For the ellipse, you can get both the axis-aligned bounding box (shown in blue here), and the ellipse-axis aligned bounding box, shown in green.

getbb1a.svg
getbb1a

Besides these, you can also get the bounding box of two arbitrary primitives p1,p2, even of different types, with a free function:

{C++}
auto bb = getBB( p1, p2 );

On this figure, you can see three combinations of bounding boxes for some objects.

getbb1b.svg
getbb1b

source

3.1 - Segments

This library provides two types modelling segments that both share the same underlying type (a pair of points), named Segment and OSegment The difference between them is that a OSegment is oriented, while a Segment is not.

Usage is straightforward, and is similar for both types:

{C++}
Segment s1( Point2d(12,34), Point2d(45,67) );
Segment s2; // default value
std::cout << s2; // prints "(0,0) - (1,1)"
s2.set( Point2d(12,34), Point2d(45,67) );

You can also build theses by giving the 4 coordinates, x1,y1 and x2, y2 of the two points, that may even be of different types (float/double/...).

{C++}
Segment s1( x1, y1, x2, y2 );

It can also be initialized (or set) with a pair of points:

{C++}
auto ppts = std::make_pair( Point2d(4,5), Point2d(2,1) );
Segment s1( ppts );
s1.set( ppts );

Besides using a homography matrix, translating the segment can be done with a member function:

{C++}
Segment s1( ... );
s1.translate( dx, dy );

You can get the pair of points (as an std::pair) with getPts(). For type Segment, the points are stored internally with the "smallest" one as first (using x coordinate, or, if equal, using y coordinate). For type OSegment, there as stored "as is":

{C++}
Segment s1( Point2d(100,100), Point2d(10,20) );
OSegment s2( Point2d(100,100), Point2d(10,20) );
auto pair1 = s1.getPts();
auto pair2 = s2.getPts();
std::cout << pair1.first; // will print (10,20)
std::cout << pair2.first; // will print (100,100)

Many operations available with lines apply to segments too:

{C++}
Segment s1( Point2d(12,34), Point2d(45,67) );
Line2d l1 = s1.getLine(); // get supporting line
Line2d l12 = getLine(s1); // or with free function
bool b1 = l1.isParallelTo( s1 );
bool b2 = s1.isParallelTo( l1 ); // also works
auto a1 = l1.getAngle( s1 );
auto a2 = s1.getAngle( l1 );

For angles, if one of the type is a line, or an unoriented segment, then the angle will stay in the range [0:+M_PI]. However, if both of the types are oriented, then the returned value will be in the range [-M_PI:+-M_PI].

Oriented segments can be reversed with the unary - operator:

{C++}
OSegment s( Point2d(1,2), Point2d(3,4) );
std::cout << s; // print [1,2]-[3,4]
s = -s;
std::cout << s; // print [3,4]-[1,2]

For these, you can get the side of point relatively to the segment with getPointSide(). It will return a value of enum type PointSide, either PointSide::Left, PointSide::Right or PointSide::Neither if the point is on the supporting line. And you can get that as a human-readable string with the associated getString():

{C++}
OSegment s( Point2d(0,10), Point2d(100,10) );
std::cout << getString(getPointSide(pt)); // print "Right"

You can get the point lying in the middle of the segment:

{C++}
Segment s1( Point2d(1,2), Point2d(3,4) );
auto p_middle = s1.getCenter(); // or: getCenter(s1)

For oriented segments, you can get a point at an arbitrary (positive) distance from the starting point:

{C++}
OSegment s( 0,0, 10,0 );
auto pt = s.getPointAt(6);
std::cout << pt; // will print [6,0]
auto pt = s.getPointAt(15);
std::cout << pt; // will print [15,0]

This middle point can be used to split a segment into two equal length segments, returned as a std::pair:

{C++}
Segment segu( Point2d(0,0), Point2d(10,0) );
OSegment sego( Point2d(0,0), Point2d(10,0) );
auto p_segsu = segu.split(); // or: split(segu)
auto p_segso = sego.split(); // or: split(sego)
std::cout << p_segsu.first; // print [0,0]--[5,0]
std::cout << p_segso.second; // print [5,0]--[10,0]

Types are preserved: if the source segment is not oriented (Segment), then this will produce two non-oriented segments, il source is oriented (OSegment), this will produce two oriented segments, with same direction.

Similarly, you can split oriented segments at an arbitrary position, defined by a distance (but must be >0 and less than the length of the segment):

{C++}
OSegment seg( Point2d(0,0), Point2d(10,0) );
auto p_segs = seg.split(4); // or split(seg,4)
std::cout << p_segs.first; // print [0,0]--[4,0]
std::cout << p_segs.second; // print [4,0]--[10,0]

The bisector line is available, using a member or free function:

{C++}
Segment seg( Point2d(1,2), Point2d(3,4) );
auto line1 = seg.getBisector(); // or: getBisector(seg)

You can generates a std::pair of two parallel segments, lying at a distance dist, with:

{C++}
Segment seg( pt1 , pt2 );
auto p_segs = seg.getParallelSegs(dist); // or: getParallelSegs(seg,dist)

See

showcase11

for a demo.

The length is available with a member function or a free function:

{C++}
Segment seg( Point2d(1,2), Point2d(3,4) );
auto length = seg.length(); // or: length(seg)

The extended segment is available with a free of member function:

{C++}
Segment seg( pt1, pt2 );
auto seg_ext = seg.getExtended(); // or getExtended(seg);

The extended segment has the same support line, but is "extended" on both sides with the length of the segment. Thus its length is three times the original segments length. On the figure below, the original segment is blue and the extended segment is red. The support line is gray.

segment_extended1.png
extended segment

You can fetch the orthogonal segments or points with getOrthogPts() and getOrthogSegs() (member or free functions). These two member functions return an std::array of size 4, the latter filled with the 4 orthogonal segments, and the first filled with the associated 4 points.

Due to the unoriented nature of the Segment type, these two member functions return points or segments in an unpredictable order.

showcase17.gif
showcase17

The distance between a segment and a point can be computed. The code considers the different situations: shortest distance can be the orthogonal distance to the supporting line, or the distance to one the two points defining the segment.

{C++}
Point2d pt;
Segment seg;
auto dist = set.dist( pt );

This function has an optional second argument: a pointer to an int, that will hold upon return one of these three values:

See showcase8 for an illustration.

3.2 - Flat rectangles

A flat rectangle is provided through the class FRect. It is modeled by its two opposite points.

{C++}
FRect r1; // (0,0) (1,1)
FRect r2( Point2d(0,0), Point2d(10,10) );
FRect r3( 1,5,3,4 ); // (1,4) - (3,4) - (3,5) - (1,5)
r1.set( pt1, pt2 );
r2.set( 1,2, 3,4 );

When using the constructor or the set() member function, there is no constraint on the points themselves: the library will automatically adjust the points to store the two opposite ones, with the one with smallest coordinates as "first".

This means you can give either (p0,p1) or (p2,p3), only p0 and p1 will be stored:

fig2.png
A flat rectangle defined by two points

The only constraint is that no coordinate can be equal. The function will throw if it is not enforced.

You can also build the rectangle by giving the 4 coordinates x1,y1 and x2,y2.

{C++}
FRect r1( x1, y1, x2, y2 );

Or by giving the center point and width and height:

{C++}
FRect r1( p0, w, h );

Or by providing a std::pair of points:

{C++}
std::pair<Point2d,Point2d> ppts = std::make_pair( Point2d(0,0), Point2d(10,10) );
FRect rect( ppts );

You can get the points with two different member functions:

{C++}
FRect rect( pt1, pt2 );
auto pair_pts = rect.getPts(); // returns the 2 points p0,p1 in a std::pair
auto pair_pts2 = getPts(rect); // or use the free function
auto pts = rect.get4Pts(); // return a std::array of 4 points
auto pts2 = get4Pts(rect); // or use the free function

A call to the get4Pts() member or free function will return the points in the following order:

p[1] +------+ p[2]
| |
| |
| |
p[0] +------+ p[3]

You can also fetch the 4 segments of the rectangle, with a member function or a free function:

{C++}
FRect rect( pt1, pt2 );
auto segs = rect.getSegs(); // returns a std::array of 4 segments.
auto segs2 = getSegs(rect); // your choice

The returned array holds the segments in the following order:

s[1]
+-------+
| |
s[0] | | s[2]
| |
+-------+
s[3]

The two diagonal segments can be fetched as a pair of segments:

{C++}
FRect rect( pt1, pt2 );
auto psegs = rect.getDiagonals(); // or: getDiagonals(rect);

The first element will hold the segment holding the point with minimal coordinates (p[0] and p[2] on above drawing).

And of course, its width, height, length, and enclosed area. This is available through member functions or free functions.

{C++}
FRect rect;
auto w = rect.width(); // or: width(rect);
auto h = rect.height(); // or: height(rect);
auto a = rect.area(); // or: area(rect);
auto l = rect.length(); // or: length(rect);

It is possible to translate the rectangle using some dx,dy offset:

{C++}
FRect rect;
rect.translate( dx, dy );

Full step rotation (+90°,-90°,180° or vertical/horizontal mirroring) is available with the rotate() member or free function:

{C++}
FRect rect;
rect.rotate( Rotate::CCW ); // or: rotate( rect, Rotate::CCW );

This will proceed a rotation around origin point (0,0). But most of the times, you want to rotate around some specific point, so you can use this:

{C++}
FRect rect;
Point2d pt(x,y);
rect.rotate( Rotate:CCW, pt ); // or: rotate( rect, Rotate:CCW, pt );

See related polyline function for details.

You can get both the the bounding circle and the inscribed circle:

{C++}
FRect r1(...); // whatever
auto c1 = r1.getBoundingCircle(); // or: getBoundingCircle(r1);
auto c2 = r1.getInscribedCircle(); // or: getInscribedCircle(r1);
showcase4b.gif
showcase4b

The extended flat rectangle is available with a free of member function:

{C++}
FRect r1( pt1, pt2 );
auto rect_ext = r1.getExtended(); // or getExtended(r1);

The extended flat rectangle will have an area 9 times the area of original rectangle.

The diagonal segments are available with a call to a member or free function:

{C++}
FRect r1( pt1, pt2 );
auto p_diags = = r1.getDiagonals(); // or getDiagonals(r1);

The figure below shows the extended rectangle and the diagonals. red: the original rectangle, blue: the extended one, green: the diagonal segments, and gray: the supporting lines (source file).

frect_extended.svg
Extended Rectangle

Check this section about union and intersection area of two rectangles.

3.3 - Circles

These are modeled by their center point and radius. Creation is straightforward, whatever your needs:

{C++}
Circle c1; // at (0,0), radius = 1
Circle c2( center_point ); // radius = 1
Circle c3( center_point, radius );
Circle c4( x0, y0, radius );

Similarly, you can assign values to an existing circle:

{C++}
c1.set( x0, y0, radius );
c2.set( center_pt, radius );
c3.set( radius ); // center doesn't change
c4.set( center_pt ); // radius doesn't change

You can also compute the unique circle from points, either from 2 points (that define the diameter), or from 3 points:

{C++}
Circle c1( pt1, pt2, pt3 );
// or
c1.set( pt1, pt2, pt3 );
Circle c2( pt1, pt2 );
// or
c2.set( pt1, pt2 );

For these latter functions, the 2 (or 3) points must be different, and not colinear (for 3 points). This is checked for and will throw if not the case (unless the "no checking" build option is activated, see here).

showcase6.gif
showcase6

Minimum Enclosing Circle (MEC)

To compute the Minimum Enclosing Circle of a set of points, you can use either a constructor, or the equivalent set() function:

{C++}
std::vector vpts( ... some points ... );
Circle c1( vpts);
// or
c1.set( vpts );

See showcase13 for an example. It uses the Welzl algorithm, that require O(n) time and O(n) memory (recursive technique). The input container can be std::vector, std::array, or std::list. It requires a least 2 points in the container, and will throw if condition not met.

Center and radius can be accessed (read/write) with provided member functions:

{C++}
Circle c1;
c1.radius() = 100;
std::cout << c1.radius(); // prints '100'
c1.center() = Point2d(12,34);
std::cout << c1.center(); // prints '[12,34]'

Corresponding free functions are also provided:

{C++}
radius(c1) = 100;
std::cout << radius(c1); // prints '100'
center(c1) = Point2d(12,34);
std::cout << center(c1); // prints '[12,34]'

You can get area and perimeter with member or free functions. The perimeter function has the name "length" for consistency with other primitives.

{C++}
auto area = c1.area(); // or: area(c1)
auto perim = c1.length(); // or: length(c1)

Two additional free functions provide the segment (or line) between the centers of two circles, and the two segments tangential to two circles:

{C++}
Circle c1, c2;
auto seg = getSegment( c1, c2 ); // as a segment
auto line = getLine( c1, c2 ); // as a line
// same result than: getSegment( c1, c2 ).getLine();
auto pair_segs = getTanSegs( c1, c2 ); // std::pair of Segment
circles1.svg
An example of two circles and the computed cented segment (red) and tangential segments (green and blue)

3.4 - Polyline

This class holds a set of points and models an arbitrary set of joined segments, without orientation. It is available as two classes OPolyline_ (open) and CPolyline_ (closed).

The closed one automatically considers a connection betwen last and first point. It can be used to model a polygon.

3.4.1 - Building

{C++}
OPolyline pl1; // empty
CPolyline pl2;
std::vector<Point2d> vpts;
// fill vpts
pl1.set( vpt ); // sets the points
pl2.set( vpt );

It can be initialised either with a container (std::vector, or std::array, or std::list) holding the points, or (only for the closed version) from a FRect:

{C++}
std::vector<Point2d> vpts{ {0,0},{1,1},{3,1} };
OPolyline op(vpts);
CPolyline cp(vpts);
FRect rect( .... );
CPolyline cp2(rect);
// OPolyline op2(rect); // this does not build

Another constructor enables building a Polyline from a Segment, wich ends up with a Polyline of 2 points:

{C++}
Segment seg; // default constructor
OPolyline po(seg);
CPolyline pc(seg);

Warning: you may not add a point identical to the previous one. Whatever the type, this code will throw:

{C++}
std::vector<Point2d> vpts{ {0,0},{1,1},{3,1},{3,1} };
OPolyline op(vpts);
CPolyline cp(vpts);

An empty polyline is allowed, but the minimum number of points is 2, initializing with a vector holding 1 point will throw.

It has no orientation, meaning that the OPolyline build from this set of points:
(0,0)-(1,0)-(1,1)
will be identical as this one:
(1,1)-(1,0)-(0,0)

It is also possible to build a polyline by importing from a boost::geometry polygon (at present, only possible with the model::point point type). See an example here.

This requires the symbol HOMOG2D_USE_BOOSTGEOM to be defined.

Regular Convex Polygon

It is also possible to build directy a Regular Convex Polygon (RCP) by using a dedicated constructor. For example, this will build a RCP of 5 points, centered at (3,2), with the points at a distance 10:

{C++}
CPolyline pol( 10, 5 );
pol.translate( 3, 2 );

For more on this, see the related set() function.

3.4.2 - Basic attributes

The open/close status can be read, but will return a constexpr value:

{C++}
OPolyline p1;
CPolyline p2;
auto b1 = p1.isClosed(); // always false
auto b2 = p2.isClosed(); // always true

Both types provide access to basic attributes: number of points, number of segments, length and bounding box, all available as member or free functions:

{C++}
auto n1 = pl.size(); // nb of points
auto n2 = size(pl);
auto s1 = pl.nbSegs(); // nb of segments
auto s2 = nbSegs(pl);
auto length1 = pl.length(); // or length(pl);
auto rect1 = pl.getBB(); // or getBB(pl);

3.4.3 - Extracting data

You can extract either points or segments. The number of segments is related to the open/close condition. For example, if we have 4 points, that will generate 4 segments if closed, but only 3 if the polyline is open.

{C++}
std::cout << "nbpts=" << pl.size() << " nb segments=" << pl.nbSegs() << '\n';
auto vec_pts = pl.getPts();
auto vec_segs = pl.getSegs();
auto pt = pl.getPoint( i ); // will throw if point i non-existent
auto seg = pl.getSegment( i ); // will throw if segment i non-existent

3.4.4 - Bounding Box and Convex Hull

The getBB() member (or free) function returns the corresponding Bounding Box. this is demonstrated in the following figures for two Polyline objects, one closed, the other open.

polyline1a.svg
An open Polyline and its bounding box
polyline1b.svg
The same one, but closed

On these figures is also shown the extreme points, Top-most and Left-most are green, the two others are blue. (see code here).

The convex hull of a Polyline can be computed with the member function convexHull(), see here for an example.

3.4.5 - Extremum points

You can get the top-most, left-most, bottom-most, or right-most point with these dedicated member functions:

{C++}
Point2d_<FPT> getTmPoint() const;
Point2d_<FPT> getBmPoint() const;
Point2d_<FPT> getLmPoint() const;
Point2d_<FPT> getRmPoint() const;

Or use the getExtremePoint( CardDir ) member function and passing one of these:

{C++}
CardDir::Bottom
CardDir::Top
CardDir::Left
CardDir::Right

For example:

{C++}
Cpolyline pol;
// .. fill with points
auto top_pt = pol.getTmPoint();
auto left_pt = pol.getExtremePoint( CardDir::Left );

These are also available as free functions:

{C++}
Cpolyline pol;
// .. fill with points
auto top_pt = getTmPoint( pol );
auto left_pt = getLmPoint( pol );
auto right_pt = getExtremePoint( CardDir::Right, pol );

(see misc. section for point set equivalent function).

Warning: These functions will throw if passed an empty polyline object.

3.4.6 - Distance between two Polyline objects

You can get the closest distance between two points belonging to two polyline objects with getClosestPoints() (free function). This will return an object on which you can fetch the corresponding pair of points, as indexes or as points, and the distance value:

{C++}
auto closest = getClosestPoints( poly1, poly2 );
auto ppts = closest.getPoints(); // get the points as a pair ("first" belongs to poly1, "second" to poly2)
auto d = closest.getMinDist() // get the distance value
auto pidx = closest.getIndexes(); // get the indexes related to poly1, poly2

See an example here.

3.4.7 - Type of Polyline

You can check if it fullfilths the requirements to be a simple polygon (must be closed and no intersections).
See definition here.

If it is, you can get its area and its centroid point:

{C++}
CPolyline pl;
// ... set points
if( pl.isSimple() ) { // or : if( isSimple(pl) ) (free function)
std::cout << "area=" << pl.area();
std::cout << "centroid point=" << pl.centroid();
}

Please note that if not a simple polygon, or if applied on a open type, then the area() function will return 0 but the centroid() function will throw.

For closed types, you can determine its convexity:

{C++}
CPolyline plo;
OPolyline plc;
// ... set points
std::cout << pls.isConvex() ? "is convex\n" : "is NOT convex\n"; // or free function: isConvex(plc)
assert( !plo.isConvex() ); // open is not a Simple Polygon, so it can't be convex

3.4.8 - Comparison of Polyline objects

Polyline objects can be compared, however the behavior differs whether it is closed or not. Consider these two sets of points:

(1,1)--(3,4)--(1,3)
(3,4)--(1,3)--(1,1)
polyline2_o1.png
First open Polyline
polyline2_o2.png
Second open Polyline
polyline2_c.png
Same points, but closed type

If they are not closed, then the == operator will return false. But if they are (both) closed, it will return true, as they obviously describe the same polygon.

This is handled through a normalization step, called automatically.

However, comparison of identical objects can fail in some circumstances, because a Polyline object can hold the same point several times (but not contiguous). Consider these two closed Polyline objects:

A: (0,0)-(3,0)-(3,1)-(0,0)-(0,3)-(1,3)
and
B: (3,0)-(3,1)-(0,0)-(0,3)-(1,3)-(0,0)

They both describe the same object:

polyline_comp_1.png
Polyline comparison example

When compared, they will be considered as different because they both use twice the same point (0,0) and their normalisation will produce these sets of points:
A: (0,0)-(3,0)-(3,1)-(0,0)-(0,3)-(1,3) (unchanged)
B: (0,0)-(0,3)-(1,3)-(0,0)-(3,0)-(3,1)

For more details, see homog2d_Polyline.md.

3.4.9 - Rotation/mirroring

All the primitives can be rotated using a homography (see following section), but in some situations you may only need "quarter-circle" rotations (mutiples of 90°). While it is of course possible to proceed these rotations with a homography, the downside is that you may end up with 0 values stored as 1.359 E-16, due to numerical nature of floating point computation. This can be undesirable, so an alternative is provided: You may rotate/mirror all the points of polyline objects with the rotate() member function. it takes as argument an enum value of type Rotate.

The available values are:

{C++}
Cpolyline poly;
// ... fill with points
poly.rotate( Rotate::CW ); // or free function: rotate( poly, Rotate::CW );

These operations are done relatively to the origin (0,0). If you need a rotation on a specific plane location, you may pass that location as second argument:

{C++}
Cpolyline poly;
// ... fill with points
Point2d org( ..., ... );
poly.rotate( Rotate::CW, org ); // or free function: rotate( poly, Rotate::CW, org );

3.4.10 - Building a Parallelogram

The member function setParallelogram() takes 3 points (may be of different floating-point types) and builds the corresponding parallelogram by computing the missing 4th point. Is only available for "closed" type.

showcase18.gif
showcase18

(source)

3.4.11 - Building a Regular Convex Polygon (RCP)

The member function set( dist, nb ) will build a Regular Convex Polygon of nb points, that will be at a distance dist from center. The center will always be (0,0), and the first point will always be at (dist,0).

If some translation is needed, you can use the translate() member or free function, see the related constructor.

This function returns a std::pair holding two numerical values: the distance between two consecutive points as "first", and "second" the radius of the inscribed circle.

The minimum value for nb is 3 (will generate an equilateral triangle), the function will throw if less.

polyline_rcp_1.svg
polyline_rcp_1

source

3.4.12 - Offsetted polyline

For closed Polyline objects, the member function getOffsetPoly() can compute a new polyline at a given distance, by using parallel curves. Its argument is a distance (numerical value). If positive, the returned polyline will be "outside" the original one, if negative, il will be inside.

The source polyline must be "simple" (closed, and no intersections).

An optional second argument of type OffsetPolyParams holds a boolean value _angleSplit that can be set to true to "smoothen" the return polyline, by adding two points on sharp angles (applies only to "outside" computation).

See showcase 22

3.4.13 - Importing from boost::geometry

If the symbol HOMOG2D_USE_BOOSTGEOM is defined (see build options), you can import a Polyline from a boost Polygon type. See demo program that demonstrates that feature.

3.5 - Ellipse

We follow here the traditional parametrization of ellipses: center point, semi-major (a) and semi-minor (b) distances, and angle between main axis and horizontal axis. Internally, it is stored as a conic in its matrix form, see build options for details on how this is handled.

Warning: support for ellipse is only partial at present, several features available with all the other primitives may not all be available for this type

ellipse1.png
ellipse1

It can be build using several constructors:

```C++ Ellipse ell; // centered at (0,0), with null angle, major=2 minor=1 Ellipse ell2( x0,y0 ); // same as above, but centered at (x0,y0) Point pt( ..., ... ); Ellipse ell3( pt ); // same as above, but centered at pt Ellipse ell4( pt, major, minor, angle ); // all parameters given (angle has 0 as default value) Circle cir; Ellipse ell5( cir ); // can be initialized from a circle

Retrieving attributes
(The perimeter function has the name "length" for consistency with the other primitives):
```C++
Ellipse ell;
auto center = ell.center(); // or: center(ell)
auto angle = ell.angle(); // or: angle(ell)
auto area = ell.area(); // or: area(ell)
auto perim = ell.length(); // or: length(ell)
auto majmin = ell.getMajMin(); // returns a pair of floating point values

Two functions are provided to get the bounding box: as with the others primitives, the getBB() member function returns the "flat" bounding box, while the getOBB() returns the "oriented" bounding box as a closed polyline object.

These are also available as free functions:

{C++}
Ellipse ell;
auto bb = getBB(ell);
auto obb = getOBB(ell);

You can get the pair of axis lines:

{C++}
Ellipse ell;
auto pair_lines = ell.getAxisLines();
auto pair_lines2 = getAxisLines( ell ); // or use the free function

To check if the ellipse is a circle:

{C++}
Ellipse ell;
bool b = ell.isCircle();

(An ellipse will be a circle if a=b, but as it is stored in the matrix form, we check here if A=C and B=0.

This will use a threshold, but you can provide the one you want, depending on your applications constraints:

{C++}
Ellipse ell;
bool b = ell.isCircle(1E-15);

There is also a free function available:

{C++}
bool b1 = isCircle( ell );
bool b2 = isCircle( ell, 1E-15 );

3.6 - Common functions

All the types above share some common member functions. These are also declared as virtual in the root class Root. Thus, this enables classical pointer-based runtime polymorphism, see here.

All of these are const. The names of these functions are:

Name Return type
size() size_t (see below)
area() HOMOG2D_INUMTYPE
length() HOMOG2D_INUMTYPE
type() Type (see below)
draw() void

The type() function will return an enum value of type Type having one of these values: Point2d, Line2d, Segment, OSegment, FRect, Circle, Ellipse, OPolyline, CPolyline. You can get a human-readable value ot the object type with getString(Type):

{C++}
Point2d pt;
std::cout << "type is " << getString( pt.type() );

(This example might sound silly but it can be useful in real-time polymorphism situations.)

The return value of the size() member function is as follows, for the different types. They are declared as constexpr, except of course for the last two.

Type Return Value
Point2d 1
Line2d 0
Segment 2
OSegment 2
FRect 4
Circle 1
Ellipse 1
OPolyline Nb of points
CPolyline Nb of points

4 - Homographies

You can manipulate 2D transformations as 3x3 homogeneous matrices (aka "Homography"), using the class Homogr.

4.1 - Homographies for points

{C++}
Homogr h; // unit transformation ("eye" matrix)
h.setTranslation(3,4);
Point2d pt1(1,2);
Point2d pt2 = h * pt1; // pt2 is now (4,6)
h.init(); // reset to unit transformation

This can also be used with all the other types (Segment, FRect, OPolyline, CPolyline, Circle, Ellipse):

{C++}
Homogr h;
... assign some planar transformation
Segment s1( ..., ... );
auto s2 = h * s1;
Polyline pl;
pl = h * pl;
auto a = h * rect; // a is a CPolyline

It must be noted that due to the inherent projective nature of a homography, applying to a flat rectangle will not produce a rectangle but a CPolyline. Similarly, applying a homography to a Circle will generate an Ellipse object:

{C++}
FRect r1;
auto r2 = Homogr() * r1;
Circle c1;
auto c2 = Homogr() * c1;
cout << "r2 type=" << getString( r2.type() ) << '\n'; // will print "CPolyline"
cout << "c2 type=" << getString( c2.type() ) << '\n'; // will print "Ellipse"

4.2 - Homographies for lines

For lines, a known result is that if we have a line lA going through p1 and p2, and a homography H mapping p1 and p2 to p'1 and p'2, then the line lB joining these two points can be computed with lB = H^-T lA.
Since release 2.4, this library automatically handles this inversion, inside the class:

{C++}
Homogr h;
... assign some planar transformation
Point2d p1a( ..., ... );
Point2d p2a( ..., ... );
Line2d lA = p1a * p2a;
auto p1b = h * p1a;
auto p2b = h * p2a;
lB = h * lA; // same as lB = p1b * p2b;

4.3 - Setting up from a given planar transformation

The three planar transformations (rotation, translation, scaling) are available directly through provided member functions. They are available in two forms: "`setXxxx()`" and "`addXxxx()`". The first one starts from the identity transformation and builds the requested one. The second form adds the requested transformation to the matrix.

To fill the matrix with arbitrary values, the best is to build it from a "vector of vectors", as in the example below:

{C++}
std::vector<std::vector<float>> m = {
{ 1, 0, 2 },
{ 0, 1, -1 },
{ 0, 0, 1 }
};
Homogr F = m;

You can also use std::array if needed.

Once you have the desired matrix, you can apply it to about anything you want. For example, here is a rotating polygon, with its bounding box and intersection points with a crossing line (see showcase3.cpp).

showcase3.gif
showcase3

4.4 - Constructors

Four constructors are provided:

{C++}
Homogr Hr( 1. ); // rotation matrix of 1 radian
Homogr Ht( 3., 4. ); // translation matrix of tx=3, ty=4
std::array<std::array<numeric_type,3>,3> ar;
// ... fill array
Homogr Ha( ar );

4.5 - Computing from 2 sets of 4 points

You can also compute the transformation from two sets of 4 (non-colinear) points:

{C++}
std::vector<Point2d> v1(4); // "from" points
std::vector<Point2d> v2(4); // "to" points
... // fill v1 and v2
H.buildFrom4Points( v1, v2 );
auto H2 = buildFrom4Points( v1, v2 ); // or use the free function

For example, the following code:

{C++}
std::vector<Point2d> v1 { {20,20}, {250,20}, {230,170}, {60,190} };
std::vector<Point2d> v2 { {60,60}, {280,50}, {220,150}, {90,130} };
auto H = buildFrom4Points(v1, v2);
std::cout << H;

will produce this matrix:

| 0.347509 0.140962 48.967 |
| -0.147718 0.510036 51.4901 |
| -0.0021031 0.00105012 1 |
homog_from_pts.png
Example of Homography computing

However, this requires the solving of a linear system of 8 equations with 8 unknowns (i.e. computing the inverse of a 8x8 matrix). This task does not enter in the scope of this library, thus we rely on others ones to do this. The consequence is that this feature is ONLY AVAILABLE if one of the two options below are available on system at build time.

The two options available are:

The default is Opencv, thus it will fail to build if not installed on system (check out bindings for more on this). However, if none of these are installed on you system, the easiest to install is with no doubt Eigen, as it is a header-only library.

The member function buildFrom4Points() accepts as third argument an int, 0 means using Opencv, 1 means using Eigen.

5 - Intersections and enclosings determination

5.1 - Intersections between primitives

This library has a homogeneous API for all intersections between the provided geometrical primitives. That is, whatever a and b (excepts points of course), there is a member function intersects() that both gives the answer to the question "do theses primitives intersect?" but also provides the intersections points.

If you are only interested in the first answer, you can write:

{C++}
if( a.intersects(b)() ) // or b.intersects(a)()
... then do something

If you need the intersection points, then just store the returned value:

{C++}
auto res = a.intersects(b); // or b.intersects(a)
if( res() ) // does intersect !
{
std::cout << "number of intersections: " << res.size() << '\n';
auto pts = res.get(); // get the points
}

The number of intersection points will depend on the primitives, thus the access method (get()) will return different types. It will also throw if there is no intersection! And whatever the primitives, you can always get the number of intersection points with the size() member function.

The table below summarizes the number of intersection points to expect:

Line2d Segment FRect Circle Polyline
Line2d 0 or 1
Segment 0 or 1 0 or 1
FRect 0, 1, or 2 0,1, or 2 0-4
Circle 0 or 2 0,1, or 2 0,2,4 0,1, or 2
Polyline 0 - n 0 - n 0 - n 0 - n 0 - n

For Polyline, the number of intersections is of course depending on the number of segments.

See the provided demo for a runnable example (relies on Opencv backend).

For the functions returning a pair of points, the returned pair will always hold as "first" the point with the lowest x value, and if equal, the point with the lowest y value.

For details on the algorithms involved, check this page.

5.1.1 - Details on intersections

When a segment has a point lying on another segment, such as in the figure below, this will be considered as an intersection point:

segment1.png
segment1

This has a consequence on rectangle intersections: when the rectangles are overlapping such as the figure below, we will have here 4 intersection points.

html img/frect1.svg "frect1"

Similarly, in the situation as in the figure below, we will have 2 intersection points:

html img/frect2.svg "frect2"

5.2 - Enclosing determination

This library provides an answer to the question: "is this object inside this other object?", whatever their type. In many cases, the question doesnt really make any sense (a point cannot be inside a point, nor can a line be inside a segment), but to have a homogeneous interface, the code a.isInside(b) will always compile. In the nonsense situations described above, it will simply return false (as a constexpr value).

Note: these functions use a strict condition: if point is on an edge, it will not be considered as inside the other object.

The table below summarizes what type (lines) can be used to check if it is inside another object of same or different type (columns).

Point2d Line2d Segment Frect CPolyline OPolyline Circle Ellipse
Point2d F F F T/F T/F F T/F T/F
Line2d F F F F F F F F
Segment F F F T/F T/F F T/F T/F
FRect F F F T/F T/F F T/F T/F
CPolyline F F F T/F T/F F T/F T/F
OPolyline F F F T/F T/F F T/F T/F
Circle F F F T/F T/F F T/F NI
Ellipse F F F T/F T/F F T/F NI

5.3 - Union and Intersection area of two rectangles

You can compute the rectangle corresponding to the intersection of two (flat) rectangles: one can use either the intersectArea() member function or (same name) free function, or the & operator. This will return an object that can be checked for with the () operator (returns a bool). It will return false if there is no intersection.

{C++}
auto a1 = r1.intersectArea(r2);
auto a2 = r1 & r2;
auto a3 = intersectArea( r1, r2 ); // free function
if( a1() )
std::cout << "common area is " << a1.get(); // prints the FRect object of the intersection

For the union of two rectangles, the usage is similar, but it will return a (closed) Polyline object. You may use either the unionArea() (member or free) function, or the | operator. And there is no get() function, as you will always get a valid Polyline object.

{C++}
auto b1 = r1.unionArea( r2 );
auto b2 = r1 | r2;
auto b3 = unionArea( r1, r2 ); // free function
std::cout << "union is " << b1.get();

Additionaly, the getBB() free function returns the bounding box holding the two rectangles, as shown in gray in the two examples below.

showcase2a.gif
Example of intersection area
showcase2b.gif
Example of union area

Note that we may not have an intersection area even if there are some intersection points found, because these can be due to a shared segment, or a single intersection point.
For the union, if there is no intersection, the function will return an empty CPolyline object.

If one rectangle is inside the other one, then the union will return the largest rectangle (as a Polyline object), ant the intersection will return the smallest one.

For conveniency, a function IoU() is provided. It computes the ratio of the area of the intersection of two rectangles over the area of the union of these two rectangles. See this WP page where this is described. This page from OpenCV also provides all the details.

6 - Misc. features

6.1 - Convex Hull

You can compute the convex hull of a set of points that are stored in a standard container (std::vector, std::list, or std::array) with the convexHull() free function. This function will return a CPolyline object. This function can also take a Polyline (CPolyline or OPolyline) object as input:

{C++}
std::vector<Point2d> vec;
// ... fill with at least 3 points
auto ch1 = convexHull( vec );
CPolyline pl( vec );
auto ch2 = convexHull( pl ); // will be the same as ch1
auto ch3 = pl.convexHull(); // member function
CH of a set of points CH of a Polyline
convex-hull-1.svg
Convex hull of a set of points
convex-hull-2.svg
Convex hull of a polygon

source code included

6.2 - Bounding Box of a set of objects

You can compute the Bounding Box of a set of objects that lie in a container, whatever that container type (std::vector, std::list or std::array). This function will return a FRect object:

{C++}
std::vector<Point2d> vec;
// ... fill with at least 2 points
auto bb = getBB( vec );
draw( img, vec );
bb.draw( img );

This function will throw if the returned rectangle ends up in an invalid state. This means that for points or segments, the container's size needs to be at least 2. For polylines, circles and rectangles, the size needs to be at least 1.

However, as the returned rectangle may not have a null area, this function will throw if the container holds objects on which no bounding box can be defined. For example, if the container holds 3 identical points. Or even different points, but sharing a identical x or y coordinate (think: [0,0]-[0,5]) The same situation may occur with segments, for example a container holding these two segments: [0,0]-[0,5] and [0,10]-[0,20].

Examples (generated with this file):

bb_Points.svg
bounding box of a set of points
bb_Rects.svg
bounding box of a set of rectangles
bb_Poly.svg
bounding box of a set of polygons
bb_Circles.svg
bounding box of a set of circles
bb_Segs.svg
bounding box of a set of segments
bb_Ellipses.svg
bounding box of a set of ellipses

For points, the Top-most and Left-most point are shown in green, and the Right-most and Bottom-most points are shown in blue (see next section about this).

Note: this function also works for containers holding "variant" type objects (CommonType), see RTP section. For example, this figure shows how you can detect the bounding box of a set containing variant-based primitives, generated with this file:

bb_variant.svg
bounding box of a set of variant

6.3 - Extremum points

Given a set of points in a container (std::vector or std::array), you can get the Top-most / Bottom-most / Left-most / Right-most point with a simple call to one of these:

{C++}
std::vector<Point2d> vec { ... fill with points... };
auto pt_L = getLmPoint( vec ); // Left-most
auto pt_R = getRmPoint( vec ); // Right-most
auto pt_T = getTmPoint( vec ); // Top-most
auto pt_B = getBmPoint( vec ); // Bottom-most

All these function actually return a pair of values, the first element being the point itself, and the second holds the distance from beggining of container (index).

Or use the generic function with a CardDir value (see here for that enum definition).

{C++}
std::vector<Point2d> vec { ... fill with points... };
auto pt_R = getExtremePoint( Card::Right, vec ); // Right-most

see here for Polyline equivalent functions

For an example, see previous section.

6.4 - Colinearity of 3 points

You can check if three points lie on the same line with this:

{C++}
bool b = areCollinear( pt1, pt2, pt3 );

6.5 - Finding nearest/farthest point in a container

Say you have container (std::vector, std::list, std::array) holding a bunch of points. Three functions allow you to find among these wich one is the closest or the farthest to a given point.

{C++}
Point2d SomePoint( ...,...);
std::vector<Point2d> vpts;
// fill vector
auto idx1 = findNearestPoint( SomePoint, vpts );
cout << "nearest point is " << vpts[idx1] << '\n';
auto idx2 = findFarthestPoint( SomePoint, vpts );
cout << "farthest point is " << vpts[idx2] << '\n';

If you need to get both:

{C++}
auto pidx = findNearestFarthestPoint( SomePoint, vpts );
cout << "nearest point is " << vpts[pidx.first]
<< ", farthest point is " << vpts[pidx.second] << '\n';

(or you could of course call the two previous function sequentially.)

see showcase

Please note:

6.6 - Extracting data from sets/containers of primitives

If you have a container (std::vector, std::list or std::array) holding either segments, circles or ellipses, you can get at once all the center points, grouped in a vector:

{C++}
std::vector<Circle> vec1;
std::vector<Segment> vec2;
std::vector<Ellipse> vec3;
... fill these
auto v1 = getCenters( vec1 ); // v1, v2, v3 hold a vector of Point2d
auto v2 = getCenters( vec2 );
auto v3 = getCenters( vec3 );

Similarly, if you have a container holding segments, you can get at once all the supporting lines:

{C++}
std::list<Segment> vec;
... fill vec
auto v_lines = getLines( vec );

6.7 - Finding points inside a shape

If you have a container (std::vector, std::list but not std::array) holding a set of points, you get can fetch the set of points lying inside a primitive with the free function getPtsInside():

{C++}
std::vector<Point2d> vec;
// fill vector
Circle cir( ... );
auto res = getPtsInside( vec, cir );

The only primitives allowed are the ones having an area (FRect, Circle, CPolyline) and Ellipse). Using others will fail at build time. The returned type will be the same as the input type (vector/vector or list/list)

Examples (generated with this file):

pts_inside_rect.svg
pts inside FRect
pts_inside_pol.svg
pts inside CPol
pts_inside_ell.svg
pts inside Ellipse
pts_inside_circle.svg
pts inside Circle

7 - Bindings with other libraries

Besides the type conversions described here, a more general binding is provided with the ttmath library. It allows increasing range and precision of numerical values. See here for details.

7.1 - Generic type conversion

Import from other types is pretty much straight forward. For points, a templated constructor is provided that can be used with any type having an 'x' and 'y' member. For example, say you have a point type:

{C++}
struct MyPoint
{
float x,y;
};

Then you can build a point (or a line going through (0,0) and that point) with:

{C++}
MyPoint myPoint;
Point2d pt(myPoint);
Line2d li(myPoint);

But in case your type uses other identifiers than x and y, no problem: just define the symbol HOMOG2D_BIND_X and HOMOG2D_BIND_Y with the ones you use and the above code will be ok.

For homographies, you can import directly from std::vector<std::vector<T>> or std::array<std::array<T,3>,3>.

For the first case, it is mandatory that all the vectors sizes are equal to 3 (the 3 embedded ones and the global one).

7.2 - Generic conversion into other types

For points, as long as the other type provides a 2-args numerical constructor, you can easily convert a h2d::Point2d into these, for example:

{C++}
Point2d pt;
...
auto ptcv = pt.getPt<cv::Point2d >(); // of free function: getPt<cv::Point2d >(pt);
auto ptbg = pt.getPt<boost::geometry::model::point_xy<double>>();

This is also possible for a std::vector:

{C++}
std::vector<Point2d> vin;
// ... fill vin
auto vec1_out = getPts<cv::Point2d>(vin); // convert to a vector of Opencv points
auto vec2_out = getPts<boost::geometry::model::point_xy<float>>(vin); // convert to a vector of boost geometry points

7.3 - Data conversion from/to Opencv data types

Optional functions are provided to make interface with Opencv easier. These features are enabled by defining the symbol HOMOG2D_USE_OPENCV at build time, before "#include"'ing the file. You can then write this:

{C++}
Point2d pt;
...
cv::Point2d ptcv1 = pt.getCvPtd(); // double coordinates
cv::Point2f ptcv2 = pt.getCvPtf(); // float coordinates
cv::Point2i ptcv3 = pt.getCvPti(); // integer coordinates

This is also available as free functions:

{C++}
Point2d pt;
...
cv::Point2d ptcv1 = getCvPtd(pt);
cv::Point2f ptcv2 = getCvPtf(pt);
cv::Point2i ptcv3 = getCvPti(pt);
...
auto ptcv3 = getCvPt<cv::Point2d>(pt); // templated version

Reverse operation as simple as this:

{C++}
cv::Point2d ptcv(4,5);
Point2d pt1(ptcv); // using constructor
Point2d pt2 = ptcv; // or assignment operator

(works the same for cv::Point2f and cv::Point2i)

Similarly, you can build a line using Opencv points:

{C++}
cv::Point2d ptcv(4,5);
Line2d l1( ptcv ); // line going trough (0,0) and (4,5)
Line2d l2( ptcv, cv::Point2d(8,9) ); // line going trough (4,5) and (8,9)

This also enables conversions between matrices types. You can build a homography using a provided cv::Mat:

{C++}
cv::Mat m; // needs to be 3x3, floating point type (either CV_32F or CV_64F)
... // fill m
Homog H = m; // call of dedicated constructor
H = m; // or call assignment operator

7.4 - Binding with Boost Geometry

From release 2.10, a preliminar binding is provided with Boost Geometry. It can be used to import Boost point types, or boost polygons. This is made available by defining the symbol HOMOG2D_USE_BOOSTGEOM.

You may build a point using either of the two point types:

{C++}
namespace bg = boost::geometry;
bg::model::point<double, 2, bg::cs::cartesian> pboost_1;
bg::model::d2::point_xy<double> pboost_2;
...
Point2d p1(pboost_1);
Point2d p2(pboost_2);
p1.set(pboost_1); // this works too
p2.set(pboost_2);

The Cpolyline and OPolyline types have a constructor that takes a Boost polygon as argument. See a demo in this file.

Exporting of points to the Boost point types is also possible, if that latter type is Cartesian and has 2 dimensions (free or member function):

{C++}
using boost_point = bg::model::point<double, 2, bg::cs::cartesian>;
Point2d pt( ..., ... );
boost_point bpta = pt.getPt<boost_point>();
boost_point bptb = h2d::getPt<boost_point>(pt);

8 - Drawing things

This library provides a way to render data graphically, using two backends. However please note that this is not a "high-end" drawing tool, the rendering has no extended fine-tuning on how things are rendered, the goal is only to quickly see what is going on.

8.1 - Available backends

The user can select between two backends, both can be usable at the same time. They both are accessed through the templated datatype Image, lying in the sub-namespace img.

The two concrete types that can be used with Image are either img::SvgImage, to generate a SVG file, or Opencv cv::Mat type, that requires that the symbol HOMOG2D_USE_OPENCV is defined and that the library is installed on system.

The difference between these two backends is that with SVG, you may only generate a file; with OpenCv, you can build an interactive app, through the "HighGui" part of that library, whith mouse and keyboard callbacks. This is demonstrated in a provided demo program that you can try with:

$ make demo

This requires that Opencv is installed on your machine. In case you have some trouble building this program, please read this.

To have an idea, here are two renderings of the same objects done with the two backends (done with this program.)

SVG rendering OpenCv rendering
comparison_1.svg
SVG drawing
comparison_2.png
Opencv drawing

With both types, you can store the generated image with the write() member function (see examples below). A streaming operator is also provided for the SvgImage type, so you may use it to stream on std::cout (or a previously opened file) in your app to check the output, and redirect to a Svg file:

$ myapp > myFile.svg

8.2 - Drawing objects

Generic drawing member functions are provided for all the types. For example, creating a SVG file in current folder, holding a circle:

{C++}
img::Image<img::SvgImage> im1( 300, 400 ); // 300 x 400 pixels
Circle c1( 100,100,80 );
c1.draw( im1 );
im1.write( "circles1.svg" );

Same, using Opencv to create a png file:

{C++}
img::Image<cv::Mat> im2( 300, 400 );
Circle c2( 100,100,80 );
c2.draw( im2 );
im2.write( "circles2.png" );

A corresponding free function is available, can be used for any type:

{C++}
Point2d pt(100,100);
Circle c( pt,80 );
draw( img, c );
draw( img, pt );

If you use Opencv, you might need the cv::Mat object for other tasks. It is available with the getReal() member function that returns a reference on that object (const or not):

{C++}
img::Image<cv::Mat> img( 300, 400 );
cv::Mat& ocv = img.getReal()

8.3 - Drawing Text

You can draw text with the drawText() free or member function, with either Opencv or Svg backend:

{C++}
img::Image<cv::Mat> im( 300, 400 );
Point2d loc( x, y );
drawText( im, "Some Text", loc ); // or: im.drawText( "Some Text", loc );

8.4 - Drawing parameters

All these drawing functions (member and free function) support an additional optional argument of type img::DrawParams, also back-end library independent. that holds various parameters for drawing. It holds several member functions that allow to tweak the drawing parameters. All of these functions support the "chained-call" syntax.

So you can for example set the color and line width with:

{C++}
li.draw( img, img::DrawParams().setThickness(2 /* pixels */).setColor(r,g,b) );

with r,g,b as bytes (uint8_t) in the range [0,255].

A local type holding these 3 values can also be used:

{C++}
auto c = img::Color(255,0,222);
li.draw( img, img::DrawParams().setColor(c) );

The drawing parameters default values can be changed anytime with a call to setDefault(), and values will be retained, unless explicitely changed, as showed in the example below;

{C++}
img::DrawParams dp; // default line thickness is 1
dp.setColor( 0, 0, 250 ).setThickness(3);
dp.setDefault(); // default is now blue, with thickness=3
line.draw( img ); // use default settings (blue,...)
line.draw( img. img::DrawParams().setColor( 0,0,0) ); // warning, black, but line thickness=3 !

You can at any time return to the "factory" settings with a call to a static function:

{C++}
img::DrawParams::resetDefault();

You can also save a style in a variable, to avoid lengthy lines:

{C++}
auto color_red = img::DrawParams().setColor( 250, 0, 0 );
something.draw( img, color_red );

The available functions are given in the table below:

Function Arguments Comment
setColor() 3 ints ([0-255])
setColor() img::Color
setPointStyle() enum PtStyle: Plus,Times,Star,Diam,Squ,Dot
setPointSize() 1 int (pixels) odd number required
setThickness() 1 int (pixels)
showPoints() bool (default is true) Draws the points for
Segment and Polyline types
setFontSize() int (size in pixels) Only used for drawText()

The following table shows the rendering of the different point styles (PtStyle) and several point sizes, with the OpenCv and SVG backend, with width (setThickness()) set to 1 and 2.

width png (OpenCv) Svg
1
drawparams_1.png
demo_pt
drawparams_1.svg
demo_pt
2
drawparams_2.png
demo_pt
drawparams_2.svg
demo_pt

source

For Svg back-end only, the user can add some specific Svg attributes with setAttribString(). For example:

{C++}
FRect r( 100,100,200,200);
r.draw( im, DrawParams().setAttrString("fill=\"rgb(100,200,150)\"") );

Make sure you add valid attributes, check here.

html img/demo_svg_attrib.svg "demo_svg_attrib"

For Opencv back-end, this is ignored.

8.4 - Drawing containers

If you have a container (std::vector, std::array or std::list) filled with one of the primitives or a std::pair of primitives (can be different types), you can draw them at once with a call to the same free function draw():

{C++}
std::vector<Segment> vseg;
// ... fill vseg with data
std::pair<Circle,FRect> pair;
// ... fill the pair
draw( img, vseg ); // use default parameters
draw( img, vseg, dp ); // pass some drawing parameters
draw( img, pair ); // draw the pair circle, rectangle
draw( img, pair, dp ); // you can even pass drawing parameters

For containers, a second generic function is provided that has as third argument a std::function. This is useful to enable having different drawing properties (think: color) for each element of the set:

{C++}
void draw( img::Image<U>& img, const T& cont, std::function<img::DrawParams(int)>& func )

The passed function must return a valid img::DrawParams object. It may be build depending on the index of the object.

A typical example would be:

{C++}
std::vector<Segment> vseg;
/// fill vseg with data
std::vector<Color> vcol( vseg.size() );
/// fill vcol with colors
auto fl = [&](int i) // lambda
{
return img::DrawParams().setColor( vcol[i] );
};
std::function<DrawParams(int)> func(fl);
draw( img, vseg, func );

Checkout here to see an example. A helper function img::genRandomColors() is provided, it will return a vector of img::Color objects filled with random RGB colors. For example:

{C++}
auto vcol1 = img::genRandomColors(10); // return 10 random colors with values in the range [20-250]
auto vcol2 = img::genRandomColors(10,100); // return 10 random colors with values in the range [100-250]
auto vcol3 = img::genRandomColors(10,30,150); // return 10 random colors with values in the range [30-150]

You can then use these with the setColor() member function:

{C++}
something.draw( image, img::DrawParams().setColor( vcol1.at(i) ) );

8.5 - Drawing containers holding variant objects (runtime polymorphism)

If the build time option is enabled (symbol HOMOG2D_ENABLE_VRTP), you can draw at once all the objects in a container holding "variant" types

{C++}
std::vector<CommonType> vec;
vec.emplace_back( Circle() );
vec.emplace_back( Frect() );
vec.emplace_back( Segment() );
img::Image<img::SvgImage> im( width, height );
draw( im, vec );

You can also pass an optional third parameter of type DrawParams.

See example

9 - Numerical data types

9.1 - Underlying data type: standard library floating point types

The library is fully templated, the user has the ability to select for each type either float, double or long double as underlying numerical datatype, on a per-object basis.

The default datatype used for all the primitives (Point2d, Line2d, Homogr, Segment, OSegment, FRect, Circle, Polyline, Ellipse) is double. The other types can be selected by an additional suffix letter added after the type:

F for float, D for double and L for long double.

For example: Point2dF, Line2dF or SegmentF are float types.

If you prefer the classical template notation, it is also available by adding an underscore after the type:

{C++}
Point2d_<float> ptf;
FRect_<double> rectd;
Segment_<long double> sld;

However, this only applies to storage. All the computation (distances, angles, intersections, ...) use (and returns) the type defined by symbol HOMOG2D_INUMTYPE, that defaults to double.

If you need extra precision (and if your compiler supports it), you may configure the library to use long double by adding this before the "include":

{C++}
#define HOMOG2D_INUMTYPE long double

or add that as a compile flag: "-DHOMOG2D_INUMTYPE long double" ...
(don't forget the quotes!)

Numerical type and size access

For any object, you may know its underlying floating-point type with the dtype() (member or free) function. It will return an enum value of type Dtype, either Dtype::Float, Dtype::Double or Dtype::LongDouble.

It can be printed out for humans with getString(), this will print "Float":

{C++}
Circle c1;
assert( c1.dtype() == Dtype::Double );
CircleF c2;
assert( dtype(c2) == Dtype::Float );
std::cout << getString( dtype(c2) );

You may also check the size in bits of corresponding mantissa and exponent with the (member of free) function dsize() (assuming IEEE754 implementation). It will return a std::pair of integers, the first being the size of the mantissa, the second being the size of exponent.
(Also works for "big numbers", see below ).

{C++}
Circle c1; // default is double
assert( c1.dsize().first == 53 );
assert( dsize(c1).second == 10 );

9.2 - Numerical type conversion

It is possible to convert to/from an object templated by a different type (at the cost of a potential precision loss):

{C++}
Point2dD pt_double;
Line2dL li_long;
Point2dF pt_float = pt_double;
Line2dD li_double = li_long;
SegmentD sd;
SegmentL sl = sd;

9.3 - Numerical issues

For the tests on null values and floating-point comparisons, some compromises had to be done. As you may know, the concept of "equal floating point values" is very tricky. In this library, this can hurt in several ways:

This library will ensure these conditions, and will throw an exception (of type std::runtime_error) if that kind of thing happens. The thresholds all have default values. They are implemented as static values, that user code can change any time.

More details and complete list on threshold page.

9.4 - "Big numbers" support

From release 2.10, there is a preliminar support for the ttmath library, that enables selecting the number of machine words for both mantissa and exponent. This can improve both precision of computation and maximum size of numbers, as it can extend the maximum size allowed by the standard type long double. This library is header-only, so it's very simple to install.

To enable this, you need to define the symbol HOMOG2D_USE_TTMATH. This will also define a default value for HOMOG2D_INUMTYPE (homog2d internal numerical type) as ttmath::Big<2,2>. This definition means that both mantissa and exponent will be stored as 2 machine words. On a 64-bit platform, that will end up as 128 bits for both.

This can be overridden,

The downside is that once the symbol HOMOG2D_USE_TTMATH is defined, you cannot use anymore the "standard types" (float, double, long double).

See this file for example.

Please note that you will probably need to adjust the relevant thresholds according to you choice of precision, see the threshold page.

For more on how this is handled in the code, see here.

10 - SVG import

10.1 - Requirements

A minimal SVG import code is present, it relies on the well-known tinyxml2 library, thus it needs to be present on machine to build an application using this feature.

On Debian-based distros, this can be easily done with $ sudo apt install libtinyxml2-dev

To enable its usage in your code, you need to do two things:

10.2 - Example

Importing is pretty simple: Instanciate a Tinyxml XMLDocument object, and use it to read the file. Then create a "visitor" object, and fetch a vector of the objects in the file:

{C++}
tinyxml2::XMLDocument doc;
doc.LoadFile( "filename.svg" );
h2d::svg::Visitor visitor;
doc.Accept( &visitor );
auto data = visitor.get();

The latter function returns a vector of polymorphic objects CommonType. This is a wrapper around a std::variant, on which the library provides some helper functions. See RTP section on how to use this. This polymorphic behavior is kept optional, it is enabled only if symbol HOMOG2D_ENABLE_VRTP is defined (which is automatically done if HOMOG2D_USE_SVG_IMPORT is defined).

For safety, you can check if the file has been correctly read by testing the return value of the LoadFile() member function:

{C++}
tinyxml2::XMLDocument doc;
auto error = doc.LoadFile( "filename.svg" );
if( err != tinyxml2::XML_SUCCESS )
FAILURE!

You can fetch the size of image in the SVG file (as double) with getImgSize(). However, this will throw if no size can be found, so you might consider using that in a "try/catch":

{C++}
std::pair<double,double> imSize(500.,500.); // default size
try
{
imSize = svg::getImgSize( doc );
std::cout << "size: " << imSize.first << " x " << imSize.second << '\n';
}
catch( const std::exception& error )
{
std::cout << "input file has no size, size set to 500x500\n -msg=" << error.what() << '\n';
}

10.3 - Technical details on svg file import

When importing a SVG file, the following points must be considered:

SVG Reference: https://www.w3.org/TR/SVG2/shapes.html

10.4 - Demo

If you have cloned the whole repo and have Tinyxml2 installed, you may build a demo program with:
$ make demo-import

This will build the file BUILD/demo_svg_import (source here) that will import any SVG file, print its content on screen (SVG general attributes and number of shapes), and generate another svg file demo-import.svg in current folder.

For example:
$ BUILD/demo_svg_import docs/img/comparison_1.svg

The previous line imports a file that has been generated by this library, so to have something more realistic, you can run this:
$ BUILD/demo_svg_import misc/test_files/France.svg

This will produce this image, with the bounding boxes of the different regions drawn with different colors:

import_demo_France.svg
import_demo_France

11 - Technical details

For more details on the code, check this page.

11.1 - Testing

A unit-test program is included, can be run locally and is also used by GitHub CI. It is uses the Catch2 library. The Github CI (aka "Github Actions") loads the 2.13.6 release. It is build and run with $ make test The CI launches the tests with both Ubuntu 20 (gcc9.4) and Ubuntu 22, and also makes sure that the Microsoft C++ compiler is able to build the software (but it does not run the tests with it).

If you have Opencv installed on your machine, you can run the additional tests that make sure the Opencv binding stuff runs fine by passing make option USE_OPENCV=Y:

make test USE_OPENCV=Y

Similarly, if you have Tinyxml2 installed, you can run the additional SVG import tests by passing this flag:

make test USE_TINYXML2=Y

A second test target is included: $ make testall. It will build and run the tests programs 3 times, one for each numerical data type (float, double, and long double), through the symbol HOMOG2D_INUMTYPE.

The test target also attempts to build the files in the folder misc/no_build. These demonstrate some code that should NOT build, thus Make will fail if any of these does build. This is just to make sure that some invalid code does, indeed, not build.

Timing Using the Catch v2 library has a small drawback: build time is pretty long (but will succeed!). For example:

$ time make test -j2
real 0m41,986s
user 1m21,940s
sys 0m1,699s

See some additional details on "dev" page

11.2 - Build options

Below are some options that can be passed, to activate them, just define the symbol. You can do that in the makefile by adding -DHOMOG2D_SYMBOL to the compiler call options, or just add a #define on top of your program before the #include "homog2d.hpp"

11.2.1 - Build symbols related to bindings with other libs:

11.2.2 - Other build symbols:

12 - Runtime Polymorphism

12.1 - Introduction

Runtime Polymorphism is the ability to store in a container (std::vector, std::list, ...) objects of different types and calling on those elements some free or member function. With this library, this is implemented using two different techniques, one with the classical pointer-based approach, and the other using a std::variant, enabled by upgrading to C++17.

At present, these two techniques can be used, they both have pros and cons, but the pointer-based approach might be shortly deprecated, So it is advised to use nowadays the "variant-based" technique, as it is more type-safe. And there is no feature available with the other approach that isn't available with the variant-based approach.

12.2 - Pointer-based runtime polymorphism

This is the "classical C++" technique: you store (smart)pointers of type Root in the container. All the geometric primitives will inherit from this (non-templated) class. That class (and its inheritance) will only exist if the symbol HOMOG2D_ENABLE_PRTP is defined:

{C++}
std::vector<std::shared_ptr<rtp::Root>> vec;
vec.push_back( std::make_shared<Circle>( Circle() ) );
vec.push_back( std::make_shared<Segment>( Segment() ) );
vec.push_back( std::make_shared<FRect>( FRect() ) );

You may then call one of the polymorphic member functions on each of them:

{C++}
for( auto& e: vec )
{
std::cout << getString(e->type())
<< ": " << *e
<< "\n -area = "
<< e->area()
<< "\n -length = "
if( e->type() != Type::Line2d )
std::cout << e->length() << '\n';
else
std::cout << "infinite\n";

Remarks:

To do something more elaborate, you need to convert them using a dynamic cast:

{C++}
if( e->type() == Type::CPolyline )
{
auto pl1 = std::dynamic_pointer_cast<CPolyline>( e );
std::cout << "Polyline has " << pl1->size() << " points\n";
}

At present, the only polymorphic member functions available:

Potential pitfall: there is no checking on the correct cast operation, it is up to the user code to make sure to cast the pointer to the correct type. Bad casting will very probably lead to a segfault.

Check test file homog2d_test_rtp_1.cpp for an example.

12.3 - Variant-based runtime polymorphism

This feature is made available by defining the symbol HOMOG2D_ENABLE_VRTP. This will enable a templated common type CommonType_ that holds all the geometrical primitives, as a std::variant (requiring a move to C++17). It follows the naming conventions of primitives, so you can use CommonTypeF, CommonTypeD or CommonTypeL, with CommonType defaulting to HOMOG2D_INUMTYPE (double by default).

So you can now stack up different primitives in a container of that type:

{C++}
std::vector<CommonType_<double>> vec;
vec.emplace_back( Circle() );
vec.emplace_back( FRect() );
vec.emplace_back( Point2d() );
...

To get the actual type of the object, you can call the free function type(), see here.

A set of functors to be used with std::visit() is available, lying in the fct sub-namespace (see here). But the easyest is probably to use the associated free functions, so you don't have the hassle of functors.

For example, with the above vector, you can print their type, length and area with:

{C++}
for( auto& e: vec )
{
std::cout << getString(type(e))
<< "\n -area=" << area(e)
<< "\n -length=" << length(e)
<< "\n";
}

(warning, the length() function will throw here if a Line2d is in the vector).

To draw these on an img::Image, you can do this:

{C++}
fct::DrawFunct vde( im ); // or fct::DrawFunct vde( im, dp ); if you need to pass some drawing parameters
for( auto& e: vec )
std::visit( vde, e );

Or, much simpler:

{C++}
for( auto& e: vec )
draw( e ); // or: draw( e, drawParams );

To apply the same homography on each element (and store them in-place), simple as this:

{C++}
auto h = Homogr().addTranslation(3,3).addScale(15); // whatever...
for( auto& e: vec )
e = transform( h, e );

But you may also use the associated functors and std::visit() if you prefer:

{C++}
fct::TransformFunct transf( Homogr().addTranslation(3,3).addScale(15) );
for( auto& e: vec )
e = std::visit( transf, e );

If you need to get the object as its base type, a dedicated functor is provided:

{C++}
if( type(e) == Type::Circle )
Circle c = fct::VariantUnwrapper{e};

Checkout all the default functors here, and the list of free functions that handle basic types or variant types (CommonType) here.

Check test file homog2d_test_rtp_2.cpp for an example.

12.4 - Pros and cons of the two techniques