From 3c85d63655e776651c8d51ebadfb7015d7e210bf Mon Sep 17 00:00:00 2001 From: Peter Keresztes Schmidt Date: Fri, 14 May 2021 15:11:50 +0200 Subject: [PATCH] Polygon: Implement clipping to a boundary box Using the Sutherland-Hodgman algorithms convex and concave subject polygons can be clipped by convex clip polygons. For now we only need clipping to rectangles (Box), so limit our implementation to that. If needed this can be trivially extended to convex clip polygons (a check whether the clip polygon is actually convex has to be added). If convex clip polygons are needed we have to switch to e.g the Vatti algorithm. --- src/zm_box.h | 23 ++++++++++++- src/zm_line.h | 64 +++++++++++++++++++++++++++++++++++ src/zm_poly.cpp | 33 ++++++++++++++++++ src/zm_poly.h | 18 +++++----- src/zm_vector2.h | 15 +++++++++ tests/CMakeLists.txt | 1 + tests/zm_box.cpp | 2 ++ tests/zm_poly.cpp | 79 ++++++++++++++++++++++++++++++++++++++++++++ tests/zm_vector2.cpp | 12 +++++++ 9 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 src/zm_line.h create mode 100644 tests/zm_poly.cpp diff --git a/src/zm_box.h b/src/zm_box.h index 971fbc36d..7094f2cd0 100644 --- a/src/zm_box.h +++ b/src/zm_box.h @@ -20,8 +20,10 @@ #ifndef ZM_BOX_H #define ZM_BOX_H +#include "zm_line.h" #include "zm_vector2.h" #include +#include // // Class used for storing a box, which is defined as a region @@ -48,7 +50,26 @@ class Box { return {mid_x, mid_y}; } - bool Contains(const Vector2 &coord) const { + // Get vertices of the box in a counter-clockwise order + std::vector Vertices() const { + return {lo_, {hi_.x_, lo_.y_}, hi_, {lo_.x_, hi_.y_}}; + } + + // Get edges of the box in a counter-clockwise order + std::vector Edges() const { + std::vector edges; + edges.reserve(4); + + std::vector v = Vertices(); + edges.emplace_back(v[0], v[1]); + edges.emplace_back(v[1], v[2]); + edges.emplace_back(v[2], v[3]); + edges.emplace_back(v[3], v[0]); + + return edges; + } + + bool Contains(const Vector2 &coord) const { return (coord.x_ >= lo_.x_ && coord.x_ <= hi_.x_ && coord.y_ >= lo_.y_ && coord.y_ <= hi_.y_); } diff --git a/src/zm_line.h b/src/zm_line.h new file mode 100644 index 000000000..ec09fc48e --- /dev/null +++ b/src/zm_line.h @@ -0,0 +1,64 @@ +/* + * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef ZONEMINDER_SRC_ZM_LINE_H_ +#define ZONEMINDER_SRC_ZM_LINE_H_ + +#include "zm_vector2.h" + +// Represents a part of a line bounded by two end points +class LineSegment { + public: + LineSegment(Vector2 start, Vector2 end) : start_(start), end_(end) {} + + public: + Vector2 start_; + Vector2 end_; +}; + +// Represents an infinite line +class Line { + public: + Line(Vector2 p1, Vector2 p2) : position_(p1), direction_(p2 - p1) {} + explicit Line(LineSegment segment) : Line(segment.start_, segment.end_) {}; + + bool IsPointLeftOfOrColinear(Vector2 p) const { + int32 det = direction_.Determinant(p - position_); + + return det >= 0; + } + + Vector2 Intersection(Line const &line) const { + int32 det = direction_.Determinant(line.direction_); + + if (det == 0) { + // lines are parallel or overlap, no intersection + return Vector2::Inf(); + } + + Vector2 c = line.position_ - position_; + double t = c.Determinant(line.direction_) / static_cast(det); + + return position_ + direction_ * t; + } + + private: + Vector2 position_; + Vector2 direction_; +}; + +#endif //ZONEMINDER_SRC_ZM_LINE_H_ diff --git a/src/zm_poly.cpp b/src/zm_poly.cpp index fbeacd06b..0bef5a4fa 100644 --- a/src/zm_poly.cpp +++ b/src/zm_poly.cpp @@ -19,6 +19,7 @@ #include "zm_poly.h" +#include "zm_line.h" #include Polygon::Polygon(std::vector vertices) : vertices_(std::move(vertices)) { @@ -74,3 +75,35 @@ bool Polygon::Contains(const Vector2 &coord) const { } return inside; } + +// Clip the polygon to a rectangular boundary box using the Sutherland-Hodgman algorithm +Polygon Polygon::GetClipped(const Box &boundary) { + std::vector clipped_vertices = vertices_; + + for (LineSegment const& clip_edge : boundary.Edges()) { + // convert our line segment to an infinite line + Line clip_line = Line(clip_edge); + + std::vector to_clip = clipped_vertices; + clipped_vertices.clear(); + + for (size_t i = 0; i < to_clip.size(); ++i) { + Vector2 vert1 = to_clip[i]; + Vector2 vert2 = to_clip[(i + 1) % to_clip.size()]; + + bool vert1_left = clip_line.IsPointLeftOfOrColinear(vert1); + bool vert2_left = clip_line.IsPointLeftOfOrColinear(vert2); + + if (vert2_left) { + if (!vert1_left) { + clipped_vertices.push_back(Line(vert1, vert2).Intersection(clip_line)); + } + clipped_vertices.push_back(vert2); + } else if (vert1_left) { + clipped_vertices.push_back(Line(vert1, vert2).Intersection(clip_line)); + } + } + } + + return Polygon(clipped_vertices); +} diff --git a/src/zm_poly.h b/src/zm_poly.h index bbfd2fc0c..36d7048cc 100644 --- a/src/zm_poly.h +++ b/src/zm_poly.h @@ -40,10 +40,6 @@ struct Edge { } }; -// -// Class used for storing a box, which is defined as a region -// defined by two coordinates -// class Polygon { public: Polygon() : area(0) {} @@ -54,18 +50,20 @@ class Polygon { } const Box &Extent() const { return extent; } - int LoX(int p_lo_x) { return extent.LoX(p_lo_x); } - int HiX(int p_hi_x) { return extent.HiX(p_hi_x); } - int LoY(int p_lo_y) { return extent.LoY(p_lo_y); } - int HiY(int p_hi_y) { return extent.HiY(p_hi_y); } + int32 LoX(int p_lo_x) { return extent.LoX(p_lo_x); } + int32 HiX(int p_hi_x) { return extent.HiX(p_hi_x); } + int32 LoY(int p_lo_y) { return extent.LoY(p_lo_y); } + int32 HiY(int p_hi_y) { return extent.HiY(p_hi_y); } - int Area() const { return area; } + int32 Area() const { return area; } const Vector2 &Centre() const { return centre; } bool Contains(const Vector2 &coord) const; + Polygon GetClipped(const Box &boundary); + private: void calcArea(); void calcCentre(); @@ -73,7 +71,7 @@ class Polygon { private: std::vector vertices_; Box extent; - int area; + int32 area; Vector2 centre; }; diff --git a/src/zm_vector2.h b/src/zm_vector2.h index 522eee51e..0daa51d77 100644 --- a/src/zm_vector2.h +++ b/src/zm_vector2.h @@ -21,6 +21,8 @@ #define ZM_VECTOR2_H #include "zm_define.h" +#include +#include // // Class used for storing an x,y pair, i.e. a coordinate/vector @@ -30,6 +32,11 @@ class Vector2 { Vector2() : x_(0), y_(0) {} Vector2(int32 x, int32 y) : x_(x), y_(y) {} + static Vector2 Inf() { + static const Vector2 inf = {std::numeric_limits::max(), std::numeric_limits::max()}; + return inf; + } + static Vector2 Range(const Vector2 &coord1, const Vector2 &coord2) { Vector2 result((coord1.x_ - coord2.x_) + 1, (coord1.y_ - coord2.y_) + 1); return result; @@ -50,6 +57,9 @@ class Vector2 { Vector2 operator-(const Vector2 &rhs) const { return {x_ - rhs.x_, y_ - rhs.y_}; } + Vector2 operator*(double rhs) const { + return {static_cast(std::lround(x_ * rhs)), static_cast(std::lround(y_ * rhs))}; + } Vector2 &operator+=(const Vector2 &rhs) { x_ += rhs.x_; @@ -62,6 +72,11 @@ class Vector2 { return *this; } + // Calculated the determinant of the 2x2 matrix as given by [[x_, y_], [v.x_y, v.y_]] + int32 Determinant(Vector2 const &v) const { + return (x_ * v.y_) - (y_ * v.x_); + } + public: int32 x_; int32 y_; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ea5c24aa5..880a3d3db 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,7 @@ set(TEST_SOURCES zm_comms.cpp zm_crypt.cpp zm_font.cpp + zm_poly.cpp zm_utils.cpp zm_vector2.cpp) diff --git a/tests/zm_box.cpp b/tests/zm_box.cpp index bd47c0bfb..8141bfa54 100644 --- a/tests/zm_box.cpp +++ b/tests/zm_box.cpp @@ -43,6 +43,8 @@ TEST_CASE("Box: construct from lo and hi") { // Should be: // REQUIRE(b.Centre() == Vector2(3, 3)); REQUIRE(b.Centre() == Vector2(4, 4)); + + REQUIRE(b.Vertices() == std::vector{{1, 1}, {5, 1}, {5, 5}, {1, 5}}); } SECTION("contains") { diff --git a/tests/zm_poly.cpp b/tests/zm_poly.cpp new file mode 100644 index 000000000..817b5a32c --- /dev/null +++ b/tests/zm_poly.cpp @@ -0,0 +1,79 @@ +/* + * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "zm_catch2.h" + +#include "zm_poly.h" + +TEST_CASE("Polygon: default constructor") { + Polygon p; + + REQUIRE(p.Area() == 0); + REQUIRE(p.Centre() == Vector2(0, 0)); +} + +TEST_CASE("Polygon: construct from vertices") { + std::vector vertices{{{0, 0}, {6, 0}, {0, 6}}}; + Polygon p(vertices); + + REQUIRE(p.Area() == 18); + //REQUIRE(p.Centre() == Vector2(2, 2)); + // Mathematically should be: + //REQUIRE(p.Extent().Size() == Vector2(6, 6)); + REQUIRE(p.Extent().Size() == Vector2(7, 7)); +} + +TEST_CASE("Polygon: clipping") { + // This a concave polygon in a shape resembling a "W" + std::vector v = { + {3, 1}, + {5, 1}, + {6, 3}, + {7, 1}, + {9, 1}, + {10, 8}, + {8, 8}, + {7, 5}, + {5, 5}, + {4, 8}, + {2, 8} + }; + + Polygon p(v); + + REQUIRE(p.GetVertices().size() == 11); + REQUIRE(p.Extent().Size() == Vector2(9, 8)); + // should be: + // REQUIRE(p.Extent().Size() == Vector2(8, 7)); + // related to Vector2::Range + + SECTION("boundary box larger than polygon") { + Polygon c = p.GetClipped(Box({1, 0}, {11, 9})); + + REQUIRE(c.GetVertices().size() == 11); + REQUIRE(c.Extent().Size() == Vector2(9, 8)); + } + + SECTION("boundary box smaller than polygon") { + Polygon c = p.GetClipped(Box({2, 4}, {10, 7})); + + REQUIRE(c.GetVertices().size() == 8); + REQUIRE(c.Extent().Size() == Vector2(9, 4)); + // should be: + // REQUIRE(c.Extent().Size() == Vector2(8, 3)); + } +} diff --git a/tests/zm_vector2.cpp b/tests/zm_vector2.cpp index 23930f41a..625420486 100644 --- a/tests/zm_vector2.cpp +++ b/tests/zm_vector2.cpp @@ -79,4 +79,16 @@ TEST_CASE("Vector2: arithmetic operators") { c -= {1, 2}; REQUIRE(c == Vector2(0, -1)); } + + SECTION("scalar multiplication") { + c = c * 2; + REQUIRE(c == Vector2(2, 2)); + } +} + +TEST_CASE("Vector2: determinate") { + Vector2 v(1, 1); + REQUIRE(v.Determinant({0, 0}) == 0); + REQUIRE(v.Determinant({1, 1}) == 0); + REQUIRE(v.Determinant({1, 2}) == 1); }