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.
This commit is contained in:
parent
5af6d6af3d
commit
3c85d63655
23
src/zm_box.h
23
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 <cmath>
|
||||
#include <vector>
|
||||
|
||||
//
|
||||
// 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<Vector2> 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<LineSegment> Edges() const {
|
||||
std::vector<LineSegment> edges;
|
||||
edges.reserve(4);
|
||||
|
||||
std::vector<Vector2> 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_);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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<double>(det);
|
||||
|
||||
return position_ + direction_ * t;
|
||||
}
|
||||
|
||||
private:
|
||||
Vector2 position_;
|
||||
Vector2 direction_;
|
||||
};
|
||||
|
||||
#endif //ZONEMINDER_SRC_ZM_LINE_H_
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
#include "zm_poly.h"
|
||||
|
||||
#include "zm_line.h"
|
||||
#include <cmath>
|
||||
|
||||
Polygon::Polygon(std::vector<Vector2> 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<Vector2> 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<Vector2> 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);
|
||||
}
|
||||
|
|
|
@ -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<Vector2> vertices_;
|
||||
Box extent;
|
||||
int area;
|
||||
int32 area;
|
||||
Vector2 centre;
|
||||
};
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
#define ZM_VECTOR2_H
|
||||
|
||||
#include "zm_define.h"
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
//
|
||||
// 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<int32>::max(), std::numeric_limits<int32>::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<int32>(std::lround(x_ * rhs)), static_cast<int32>(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_;
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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<Vector2>{{1, 1}, {5, 1}, {5, 5}, {1, 5}});
|
||||
}
|
||||
|
||||
SECTION("contains") {
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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<Vector2> 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<Vector2> 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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue