Add simple polygon class (#2750)

* Add simple polygon class

* Add documentation for fill_convex_polygon

* Rename alpha_blend to antialias

* Update dlib/geometry/polygon_abstract.h

Co-authored-by: Davis E. King <davis685@gmail.com>

* Update dlib/geometry/polygon_abstract.h

Co-authored-by: Davis E. King <davis685@gmail.com>

* Update dlib/geometry/polygon_abstract.h

Co-authored-by: Davis E. King <davis685@gmail.com>

* Update dlib/geometry/polygon_abstract.h

Co-authored-by: Davis E. King <davis685@gmail.com>

* Update dlib/geometry/polygon_abstract.h

Co-authored-by: Davis E. King <davis685@gmail.com>

* Update dlib/geometry/polygon.h

Co-authored-by: Davis E. King <davis685@gmail.com>

* Update documentation for get_convex_shape

* Add tests for the polygon class

* Remove new line

---------

Co-authored-by: Davis E. King <davis685@gmail.com>
This commit is contained in:
Adrià Arrufat 2023-03-27 10:05:43 +09:00 committed by GitHub
parent 9a13229970
commit 4ffa9b02a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 422 additions and 82 deletions

View File

@ -9,6 +9,7 @@
#include "geometry/border_enumerator.h"
#include "geometry/point_transforms.h"
#include "geometry/line.h"
#include "geometry/polygon.h"
#endif // DLIB_GEOMETRy_HEADER

92
dlib/geometry/polygon.h Normal file
View File

@ -0,0 +1,92 @@
// Copyright (C) 2022 Davis E. King (davis@dlib.net), Adrià Arrufat
// License: Boost Software License See LICENSE.txt for the full license.
#ifndef DLIB_POLYGOn_
#define DLIB_POLYGOn_
#include "polygon_abstract.h"
#include "rectangle.h"
#include "vector.h"
namespace dlib
{
class polygon
{
public:
using size_type = std::vector<point>::size_type;
polygon(std::vector<point> points) : points(std::move(points)) {}
size_type size() const { return points.size(); }
point& operator[](const size_type idx) { return points[idx]; }
const point& operator[](const size_type idx) const { return points[idx]; }
const point& at(const size_type idx) const { return points.at(idx); }
rectangle get_rect() const
{
rectangle rect;
for (const auto& p : points)
rect += p;
return rect;
}
double area() const { return polygon_area(points); }
template <typename alloc>
void get_convex_shape (
const long top,
const long bottom,
std::vector<double, alloc>& left_boundary,
std::vector<double, alloc>& right_boundary
) const
{
using std::min;
using std::max;
left_boundary.assign(bottom-top+1, std::numeric_limits<double>::infinity());
right_boundary.assign(bottom-top+1, -std::numeric_limits<double>::infinity());
// trace out the points along the edge of the polynomial and record them
for (unsigned long i = 0; i < points.size(); ++i)
{
const point p1 = points[i];
const point p2 = points[(i+1)%points.size()];
if (p1.y() == p2.y())
{
if (top <= p1.y() && p1.y() <= bottom)
{
const long y = p1.y() - top;
const double xmin = min(p1.x(), p2.x());
const double xmax = min(p1.x(), p2.x());
left_boundary[y] = min(left_boundary[y], xmin);
right_boundary[y] = max(right_boundary[y], xmax);
}
}
else
{
// Here we trace out the line from p1 to p2 and record where it hits.
// x = m*y + b
const double m = (p2.x() - p1.x())/(double)(p2.y()-p1.y());
const double b = p1.x() - m*p1.y(); // because: x1 = m*y1 + b
const long ymin = max(top,min(p1.y(), p2.y()));
const long ymax = min(bottom,max(p1.y(), p2.y()));
for (long y = ymin; y <= ymax; ++y)
{
const double x = m*y + b;
const unsigned long idx = y-top;
left_boundary[idx] = min(left_boundary[idx], x);
right_boundary[idx] = max(right_boundary[idx], x);
}
}
}
}
private:
std::vector<point> points;
};
}
#endif // polygon_h_INCLUDED

View File

@ -0,0 +1,108 @@
// Copyright (C) 2022 Davis E. King (davis@dlib.net), Adrià Arrufat
// License: Boost Software License See LICENSE.txt for the full license.
#undef DLIB_POLYGOn_ABSTRACT_H_
#ifdef DLIB_POLYGOn_ABSTRACT_H_
#include "rectangle.h"
#include "vector.h"
namespace dlib
{
class polygon
{
/*!
WHAT THIS OBJECT REPRESENTS
This object represents a polygon inside a Cartesian coordinate system.
It is just a wrapper class for a std::vector<point> with some added
methods.
Note that the origin of the coordinate system, i.e. (0,0), is located
at the upper left corner. That is, points such as (1,1) or (3,5)
represent locations that are below and to the right of the origin.
!*/
public:
using size_type = std::vector<point>::size_type;
polygon(std::vector<point> points);
/*!
ensures
- #*this represents a polygon defined by points.
!*/
size_type size() const;
/*!
ensures
- returns the number of points in the polygon.
!*/
point& operator[](const size_type idx);
/*!
requires
- idx < size()
ensures
- returns the point of the polygon at index idx.
!*/
const point& operator[](const size_type idx) const;
/*!
requires
- idx < size()
ensures
- returns the point of the polygon at index idx.
!*/
const point& at(const size_type idx) const;
/*!
ensures
- returns the point of the polygon at index idx.
throws
- std::out_of_range if idx >= size()
!*/
rectangle get_rect() const;
/*!
ensures
- returns smallest rectangle that contains all points in the polygon.
!*/
double area() const;
/*!
ensures
- If you walk the points of #*this in order to make a closed polygon, what is its
area? This function returns that area. It uses the shoelace formula to
compute the result and so works for general non-self-intersecting polygons.
!*/
template <typename alloc>
void get_convex_shape (
const long top,
const long bottom,
std::vector<double, alloc>& left_boundary,
std::vector<double, alloc>& right_boundary
) const;
/*!
requires
- 0 <= top <= bottom
ensures
- interprets points as the coordinates defining a convex polygon. In
particular, we interpret points as a list of the vertices of the polygon
and assume they are ordered in clockwise order.
- #left_boundary.size() == bottom-top+1
- #right_boundary.size() == bottom-top+1
- for all top <= y <= bottom:
- #left_boundary[y-top] == the x coordinate for the left most side of
the polygon at coordinate y.
- #right_boundary[y-top] == the x coordinate for the right most side of
the polygon at coordinate y.
Note that it isn't illegal to call this method if the polygon defined by
points isn't convex. In that case, we won't return a convex shape.
!*/
private:
std::vector<point> points;
};
}
#endif // DLIB_POLYGOn_ABSTRACT_H_

View File

@ -440,85 +440,10 @@ namespace dlib
// ----------------------------------------------------------------------------------------
namespace impl
{
template <typename alloc>
void get_convex_polygon_shape (
const std::vector<point>& points,
const long top,
const long bottom,
std::vector<double,alloc>& left_boundary,
std::vector<double,alloc>& right_boundary
)
/*!
requires
- 0 <= top <= bottom
ensures
- interprets points as the coordinates defining a convex polygon. In
particular, we interpret points as a list of the vertices of the polygon
and assume they are ordered in clockwise order.
- #left_boundary.size() == bottom-top+1
- #right_boundary.size() == bottom-top+1
- for all top <= y <= bottom:
- #left_boundary[y-top] == the x coordinate for the left most side of
the polygon at coordinate y.
- #right_boundary[y-top] == the x coordinate for the right most side of
the polygon at coordinate y.
!*/
{
using std::min;
using std::max;
left_boundary.assign(bottom-top+1, std::numeric_limits<double>::infinity());
right_boundary.assign(bottom-top+1, -std::numeric_limits<double>::infinity());
// trace out the points along the edge of the polynomial and record them
for (unsigned long i = 0; i < points.size(); ++i)
{
const point p1 = points[i];
const point p2 = points[(i+1)%points.size()];
if (p1.y() == p2.y())
{
if (top <= p1.y() && p1.y() <= bottom)
{
const long y = p1.y() - top;
const double xmin = min(p1.x(), p2.x());
const double xmax = min(p1.x(), p2.x());
left_boundary[y] = min(left_boundary[y], xmin);
right_boundary[y] = max(right_boundary[y], xmax);
}
}
else
{
// Here we trace out the line from p1 to p2 and record where it hits.
// x = m*y + b
const double m = (p2.x() - p1.x())/(double)(p2.y()-p1.y());
const double b = p1.x() - m*p1.y(); // because: x1 = m*y1 + b
const long ymin = max(top,min(p1.y(), p2.y()));
const long ymax = min(bottom,max(p1.y(), p2.y()));
for (long y = ymin; y <= ymax; ++y)
{
const double x = m*y + b;
const unsigned long idx = y-top;
left_boundary[idx] = min(left_boundary[idx], x);
right_boundary[idx] = max(right_boundary[idx], x);
}
}
}
}
// ------------------------------------------------------------------------------------
}
template <typename pixel_type>
void draw_solid_convex_polygon (
const canvas& c,
const std::vector<point>& polygon,
const polygon& poly,
const pixel_type& pixel,
const rectangle& area = rectangle(std::numeric_limits<long>::min(), std::numeric_limits<long>::min(),
std::numeric_limits<long>::max(), std::numeric_limits<long>::max())
@ -528,9 +453,7 @@ namespace dlib
using std::min;
const rectangle valid_area(c.intersect(area));
rectangle bounding_box;
for (unsigned long i = 0; i < polygon.size(); ++i)
bounding_box += polygon[i];
const rectangle bounding_box = poly.get_rect();
// Don't do anything if the polygon is totally outside the area we can draw in
// right now.
@ -558,8 +481,7 @@ namespace dlib
std::vector<double> left_boundary;
std::vector<double> right_boundary;
impl::get_convex_polygon_shape(polygon, top, bottom, left_boundary, right_boundary);
poly.get_convex_shape(top, bottom, left_boundary, right_boundary);
// draw the polygon row by row
for (unsigned long i = top_offset; i < left_boundary.size(); ++i)

View File

@ -8,6 +8,7 @@
#include "../pixel.h"
#include "../matrix.h"
#include "../gui_widgets/fonts.h"
#include "../geometry.h"
#include <cmath>
namespace dlib
@ -402,7 +403,160 @@ namespace dlib
}
}
// ----------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------
template <typename image_type, typename pixel_type>
void fill_convex_polygon (
image_type& image,
const polygon& poly,
const pixel_type& pixel,
const bool antialias = true,
const rectangle& area = rectangle(std::numeric_limits<long>::min(), std::numeric_limits<long>::min(),
std::numeric_limits<long>::max(), std::numeric_limits<long>::max())
)
{
using std::max;
using std::min;
const rectangle valid_area(get_rect(image).intersect(area));
const rectangle bounding_box = poly.get_rect();
// Don't do anything if the polygon is totally outside the area we can draw in
// right now.
if (bounding_box.intersect(valid_area).is_empty())
return;
rgb_alpha_pixel alpha_pixel;
assign_pixel(alpha_pixel, pixel);
const unsigned char max_alpha = alpha_pixel.alpha;
// we will only want to loop over the part of left_boundary that is part of the
// valid_area.
long top = max(valid_area.top(),bounding_box.top());
long bottom = min(valid_area.bottom(),bounding_box.bottom());
// Since we look at the adjacent rows of boundary information when doing the alpha
// blending, we want to make sure we always have some boundary information unless
// we are at the absolute edge of the polygon.
const long top_offset = (top == bounding_box.top()) ? 0 : 1;
const long bottom_offset = (bottom == bounding_box.bottom()) ? 0 : 1;
if (top != bounding_box.top())
top -= 1;
if (bottom != bounding_box.bottom())
bottom += 1;
std::vector<double> left_boundary;
std::vector<double> right_boundary;
poly.get_convex_shape(top, bottom, left_boundary, right_boundary);
// draw the polygon row by row
for (unsigned long i = top_offset; i < left_boundary.size(); ++i)
{
long left_x = static_cast<long>(std::ceil(left_boundary[i]));
long right_x = static_cast<long>(std::floor(right_boundary[i]));
left_x = max(left_x, valid_area.left());
right_x = min(right_x, valid_area.right());
if (i < left_boundary.size()-bottom_offset)
{
// draw the main body of the polygon
for (long x = left_x; x <= right_x; ++x)
{
const long y = i+top;
assign_pixel(image(y, x), pixel);
}
}
if (i == 0 || !antialias)
continue;
// Now draw anti-aliased edges so they don't look all pixely.
// Alpha blend the edges on the left side.
double delta = left_boundary[i-1] - left_boundary[i];
if (std::abs(delta) <= 1)
{
if (std::floor(left_boundary[i]) != left_x)
{
const point p(static_cast<long>(std::floor(left_boundary[i])), i+top);
rgb_alpha_pixel temp = alpha_pixel;
temp.alpha = max_alpha-static_cast<unsigned char>((left_boundary[i]-p.x())*max_alpha);
if (valid_area.contains(p))
assign_pixel(image(p.y(), p.x()),temp);
}
}
else if (delta < 0) // on the bottom side
{
for (long x = static_cast<long>(std::ceil(left_boundary[i-1])); x < left_x; ++x)
{
const point p(x, i+top);
rgb_alpha_pixel temp = alpha_pixel;
temp.alpha = static_cast<unsigned char>((x-left_boundary[i-1])/std::abs(delta)*max_alpha);
if (valid_area.contains(p))
assign_pixel(image(p.y(), p.x()),temp);
}
}
else // on the top side
{
const long old_left_x = static_cast<long>(std::ceil(left_boundary[i-1]));
for (long x = left_x; x < old_left_x; ++x)
{
const point p(x, i+top-1);
rgb_alpha_pixel temp = alpha_pixel;
temp.alpha = static_cast<unsigned char>((x-left_boundary[i])/delta*max_alpha);
if (valid_area.contains(p))
assign_pixel(image(p.y(), p.x()),temp);
}
}
// Alpha blend the edges on the right side
delta = right_boundary[i-1] - right_boundary[i];
if (std::abs(delta) <= 1)
{
if (std::ceil(right_boundary[i]) != right_x)
{
const point p(static_cast<long>(std::ceil(right_boundary[i])), i+top);
rgb_alpha_pixel temp = alpha_pixel;
temp.alpha = max_alpha-static_cast<unsigned char>((p.x()-right_boundary[i])*max_alpha);
if (valid_area.contains(p))
assign_pixel(image(p.y(), p.x()),temp);
}
}
else if (delta < 0) // on the top side
{
for (long x = static_cast<long>(std::floor(right_boundary[i-1]))+1; x <= right_x; ++x)
{
const point p(x, i+top-1);
rgb_alpha_pixel temp = alpha_pixel;
temp.alpha = static_cast<unsigned char>((right_boundary[i]-x)/std::abs(delta)*max_alpha);
if (valid_area.contains(p))
assign_pixel(image(p.y(), p.x()),temp);
}
}
else // on the bottom side
{
const long old_right_x = static_cast<long>(std::floor(right_boundary[i-1]));
for (long x = right_x+1; x <= old_right_x; ++x)
{
const point p(x, i+top);
rgb_alpha_pixel temp = alpha_pixel;
temp.alpha = static_cast<unsigned char>((right_boundary[i-1]-x)/delta*max_alpha);
if (valid_area.contains(p))
assign_pixel(image(p.y(), p.x()),temp);
}
}
}
}
template <typename image_type>
inline void draw_solid_convex_polygon (
image_type& image,
const polygon& poly
) { fill_convex_polygon(image, poly, 0); }
// ---------------------------------------------------------------------------------------
template <
typename image_array_type

View File

@ -190,6 +190,35 @@ namespace dlib
- fills the area defined by rect in the given image with the given pixel value.
!*/
// ----------------------------------------------------------------------------------------
template <typename image_type, typename pixel_type>
void fill_convex_polygon (
image_type& image,
const polygon& poly,
const pixel_type& pixel,
const bool antialias = true,
const rectangle& area = rectangle(std::numeric_limits<long>::min(), std::numeric_limits<long>::min(),
std::numeric_limits<long>::max(), std::numeric_limits<long>::max())
);
/*!
requires
- image_type == an image object that implements the interface defined in
dlib/image_processing/generic_image.h
- pixel_traits<pixel_type> is defined
ensures
- Interprets the given polygon object as defining a convex polygon shape.
In particular, the polygon is given by taking the points and drawing
lines between them. That is, imagine drawing a line connecting polygon[i]
and polygon[(i+1)%polygon.size()], for all valid i, and then filling in
the interior of the polygon. That is what this function does.
- When drawing the polygon, only the part of the polygon which overlaps both
the given image and area rectangle is drawn.
- Uses the given pixel color to draw the polygon.
- if (antialias == true) then we draw anti-aliased edges so they don't
look all pixely by alpha-blending them with the image.
!*/
// ----------------------------------------------------------------------------------------
template <

View File

@ -895,6 +895,39 @@ namespace
}
// ----------------------------------------------------------------------------------------
void test_polygon()
{
print_spinner();
/* Let's define this triangle
* 0 . . . . . . . . . . .
* 1 . . . . . . . . .
* 2 . . . . . . .
* 3 . . . . .
* 4 . . .
* 5 .
* 0 1 2 3 4 5 6 7 8 9 10
*/
const polygon triangle({{0, 0}, {5, 5}, {10, 0}});
DLIB_TEST(triangle.area() == 25);
const rectangle rect = triangle.get_rect();
std::vector<double> left_boundary;
std::vector<double> right_boundary;
triangle.get_convex_shape(rect.top(), rect.bottom(), left_boundary, right_boundary);
DLIB_TEST(left_boundary.size() == right_boundary.size());
const long top = rect.top();
const long bottom = rect.bottom();
for (long y = top, i = 0; y <= bottom; ++y, ++i)
DLIB_TEST(left_boundary[y - top] == i);
for (long y = top, i = 10; y <= bottom; ++y, --i)
DLIB_TEST(right_boundary[y - top] == i);
}
// ----------------------------------------------------------------------------------------
class geometry_tester : public tester
@ -922,6 +955,7 @@ namespace
test_find_similarity_transform<float>();
test_find_similarity_transform2<float>();
test_line();
test_polygon();
}
} a;