mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-03-25 01:01:53 -04:00
The zone loader now ignores the Units DB field and detects the coordinate format by checking for decimal points: decimal values are percentages, integer-only values are legacy pixels. This fixes motion detection being broken when zones had Units=Pixels but percentage coordinates (or vice versa), which resulted in a ~99x99 pixel zone on a 2560x1440 monitor. The PHP zone view now always forces Units=Percent when saving, since it always works in percentage space. convertPixelPointsToPercent() now returns bool to indicate whether conversion occurred. Tests added for: truncation bug via atoi, correct percentage-to-pixel conversion, auto-detect heuristic, and resolution independence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
326 lines
12 KiB
C++
326 lines
12 KiB
C++
/*
|
|
* 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_zone.h"
|
|
|
|
TEST_CASE("Zone::ParsePercentagePolygon: full-frame zone at 1920x1080", "[Zone]") {
|
|
Polygon polygon;
|
|
bool result = Zone::ParsePercentagePolygon(
|
|
"0.00,0.00 100.00,0.00 100.00,100.00 0.00,100.00",
|
|
1920, 1080, polygon);
|
|
|
|
REQUIRE(result == true);
|
|
REQUIRE(polygon.GetVertices().size() == 4);
|
|
REQUIRE(polygon.GetVertices()[0] == Vector2(0, 0));
|
|
REQUIRE(polygon.GetVertices()[1] == Vector2(1920, 0));
|
|
REQUIRE(polygon.GetVertices()[2] == Vector2(1920, 1080));
|
|
REQUIRE(polygon.GetVertices()[3] == Vector2(0, 1080));
|
|
}
|
|
|
|
TEST_CASE("Zone::ParsePercentagePolygon: center 50% zone", "[Zone]") {
|
|
Polygon polygon;
|
|
bool result = Zone::ParsePercentagePolygon(
|
|
"25.00,25.00 75.00,25.00 75.00,75.00 25.00,75.00",
|
|
1920, 1080, polygon);
|
|
|
|
REQUIRE(result == true);
|
|
REQUIRE(polygon.GetVertices().size() == 4);
|
|
REQUIRE(polygon.GetVertices()[0] == Vector2(480, 270));
|
|
REQUIRE(polygon.GetVertices()[1] == Vector2(1440, 270));
|
|
REQUIRE(polygon.GetVertices()[2] == Vector2(1440, 810));
|
|
REQUIRE(polygon.GetVertices()[3] == Vector2(480, 810));
|
|
}
|
|
|
|
TEST_CASE("Zone::ParsePercentagePolygon: fractional percentages", "[Zone]") {
|
|
Polygon polygon;
|
|
bool result = Zone::ParsePercentagePolygon(
|
|
"50.25,75.50 60.00,75.50 60.00,85.00 50.25,85.00",
|
|
1920, 1080, polygon);
|
|
|
|
REQUIRE(result == true);
|
|
REQUIRE(polygon.GetVertices().size() == 4);
|
|
// 50.25% of 1920 = 964.8 -> 965
|
|
REQUIRE(polygon.GetVertices()[0].x_ == 965);
|
|
// 75.50% of 1080 = 815.4 -> 815
|
|
REQUIRE(polygon.GetVertices()[0].y_ == 815);
|
|
}
|
|
|
|
TEST_CASE("Zone::ParsePercentagePolygon: different resolution", "[Zone]") {
|
|
Polygon polygon;
|
|
bool result = Zone::ParsePercentagePolygon(
|
|
"0.00,0.00 100.00,0.00 100.00,100.00 0.00,100.00",
|
|
640, 480, polygon);
|
|
|
|
REQUIRE(result == true);
|
|
REQUIRE(polygon.GetVertices()[1] == Vector2(640, 0));
|
|
REQUIRE(polygon.GetVertices()[2] == Vector2(640, 480));
|
|
}
|
|
|
|
TEST_CASE("Zone::ParsePercentagePolygon: clamping beyond 100%", "[Zone]") {
|
|
Polygon polygon;
|
|
bool result = Zone::ParsePercentagePolygon(
|
|
"0.00,0.00 110.00,0.00 100.00,100.00 0.00,100.00",
|
|
1920, 1080, polygon);
|
|
|
|
REQUIRE(result == true);
|
|
// 110% should be clamped to monitor width
|
|
REQUIRE(polygon.GetVertices()[1].x_ == 1920);
|
|
}
|
|
|
|
TEST_CASE("Zone::ParsePercentagePolygon: triangle", "[Zone]") {
|
|
Polygon polygon;
|
|
bool result = Zone::ParsePercentagePolygon(
|
|
"50.00,10.00 90.00,90.00 10.00,90.00",
|
|
1000, 1000, polygon);
|
|
|
|
REQUIRE(result == true);
|
|
REQUIRE(polygon.GetVertices().size() == 3);
|
|
REQUIRE(polygon.GetVertices()[0] == Vector2(500, 100));
|
|
REQUIRE(polygon.GetVertices()[1] == Vector2(900, 900));
|
|
REQUIRE(polygon.GetVertices()[2] == Vector2(100, 900));
|
|
}
|
|
|
|
TEST_CASE("Zone::ParsePercentagePolygon: too few points", "[Zone]") {
|
|
Polygon polygon;
|
|
bool result = Zone::ParsePercentagePolygon(
|
|
"0.00,0.00 100.00,0.00",
|
|
1920, 1080, polygon);
|
|
|
|
REQUIRE(result == false);
|
|
}
|
|
|
|
TEST_CASE("Zone::ParsePercentagePolygon: integer coords still work", "[Zone]") {
|
|
// strtod handles integers fine
|
|
Polygon polygon;
|
|
bool result = Zone::ParsePercentagePolygon(
|
|
"0,0 100,0 100,100 0,100",
|
|
1920, 1080, polygon);
|
|
|
|
REQUIRE(result == true);
|
|
REQUIRE(polygon.GetVertices()[0] == Vector2(0, 0));
|
|
REQUIRE(polygon.GetVertices()[1] == Vector2(1920, 0));
|
|
REQUIRE(polygon.GetVertices()[2] == Vector2(1920, 1080));
|
|
REQUIRE(polygon.GetVertices()[3] == Vector2(0, 1080));
|
|
}
|
|
|
|
TEST_CASE("Zone::ParsePolygonString: basic pixel parsing", "[Zone]") {
|
|
Polygon polygon;
|
|
|
|
SECTION("parses a simple rectangle") {
|
|
bool ok = Zone::ParsePolygonString("0,0 639,0 639,479 0,479", polygon);
|
|
REQUIRE(ok);
|
|
|
|
auto const &verts = polygon.GetVertices();
|
|
REQUIRE(verts.size() == 4);
|
|
REQUIRE(verts[0] == Vector2(0, 0));
|
|
REQUIRE(verts[1] == Vector2(639, 0));
|
|
REQUIRE(verts[2] == Vector2(639, 479));
|
|
REQUIRE(verts[3] == Vector2(0, 479));
|
|
}
|
|
|
|
SECTION("parses a triangle") {
|
|
bool ok = Zone::ParsePolygonString("100,100 200,100 150,200", polygon);
|
|
REQUIRE(ok);
|
|
|
|
auto const &verts = polygon.GetVertices();
|
|
REQUIRE(verts.size() == 3);
|
|
REQUIRE(verts[0] == Vector2(100, 100));
|
|
REQUIRE(verts[1] == Vector2(200, 100));
|
|
REQUIRE(verts[2] == Vector2(150, 200));
|
|
}
|
|
|
|
SECTION("rejects string with fewer than 3 vertices") {
|
|
// Two vertices — not enough for a polygon, but ParsePolygonString
|
|
// returns true if any vertices were parsed (vertices.empty() check)
|
|
bool ok = Zone::ParsePolygonString("10,20 30,40", polygon);
|
|
REQUIRE(ok);
|
|
// The polygon won't be properly formed but parsing itself doesn't fail
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Zone::ParsePercentagePolygon: percentage to pixel conversion", "[Zone]") {
|
|
Polygon polygon;
|
|
unsigned int width = 1920;
|
|
unsigned int height = 1080;
|
|
|
|
SECTION("full-frame 0-100% rectangle") {
|
|
bool ok = Zone::ParsePercentagePolygon("0,0 100,0 100,100 0,100", width, height, polygon);
|
|
REQUIRE(ok);
|
|
|
|
auto const &verts = polygon.GetVertices();
|
|
REQUIRE(verts.size() == 4);
|
|
REQUIRE(verts[0] == Vector2(0, 0));
|
|
REQUIRE(verts[1] == Vector2(1920, 0));
|
|
REQUIRE(verts[2] == Vector2(1920, 1080));
|
|
REQUIRE(verts[3] == Vector2(0, 1080));
|
|
}
|
|
|
|
SECTION("50% rectangle converts to half-resolution pixels") {
|
|
bool ok = Zone::ParsePercentagePolygon("0,0 50,0 50,50 0,50", width, height, polygon);
|
|
REQUIRE(ok);
|
|
|
|
auto const &verts = polygon.GetVertices();
|
|
REQUIRE(verts.size() == 4);
|
|
REQUIRE(verts[0] == Vector2(0, 0));
|
|
REQUIRE(verts[1] == Vector2(960, 0));
|
|
REQUIRE(verts[2] == Vector2(960, 540));
|
|
REQUIRE(verts[3] == Vector2(0, 540));
|
|
}
|
|
|
|
SECTION("values are clamped to monitor bounds") {
|
|
// 100% should clamp to exact monitor dimensions
|
|
bool ok = Zone::ParsePercentagePolygon("0,0 100,0 100,100 0,100", width, height, polygon);
|
|
REQUIRE(ok);
|
|
|
|
auto const &verts = polygon.GetVertices();
|
|
for (auto const &v : verts) {
|
|
REQUIRE(v.x_ >= 0);
|
|
REQUIRE(v.x_ <= static_cast<int>(width));
|
|
REQUIRE(v.y_ >= 0);
|
|
REQUIRE(v.y_ <= static_cast<int>(height));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Zone: pixel values through ParsePercentagePolygon produce wrong results", "[Zone]") {
|
|
// This test demonstrates the bug: when pixel coordinates (>100) are fed
|
|
// through ParsePercentagePolygon, they get treated as percentages and
|
|
// scaled wildly, then clamped to monitor bounds.
|
|
Polygon polygon;
|
|
unsigned int width = 1920;
|
|
unsigned int height = 1080;
|
|
|
|
// These are pixel coordinates for a 640x480 region
|
|
bool ok = Zone::ParsePercentagePolygon("0,0 639,0 639,479 0,479", width, height, polygon);
|
|
REQUIRE(ok);
|
|
|
|
auto const &verts = polygon.GetVertices();
|
|
REQUIRE(verts.size() == 4);
|
|
|
|
// 639% of 1920 = 12268.8 -> clamped to 1920
|
|
// 479% of 1080 = 5173.2 -> clamped to 1080
|
|
// All non-zero coords get clamped to monitor bounds — the zone is
|
|
// degenerate (covers the full monitor instead of a sub-region)
|
|
REQUIRE(verts[1] == Vector2(static_cast<int>(width), 0));
|
|
REQUIRE(verts[2] == Vector2(static_cast<int>(width), static_cast<int>(height)));
|
|
}
|
|
|
|
// --- Auto-detect format tests ---
|
|
// The zone loader uses strchr(Coords, '.') to decide format:
|
|
// decimal point present -> ParsePercentagePolygon
|
|
// no decimal point -> ParsePolygonString (legacy pixels)
|
|
// These tests verify both parsers handle the inputs they'll receive
|
|
// under auto-detection, and document the broken case that auto-detection prevents.
|
|
|
|
TEST_CASE("Zone: ParsePolygonString truncates decimal coords via atoi", "[Zone]") {
|
|
// This is the bug that broke motion detection: percentage coords like
|
|
// "0.00,0.00 99.96,0.00 99.96,99.93 0.00,99.93" parsed by
|
|
// ParsePolygonString (which uses atoi) get truncated to a 99x99 pixel zone
|
|
Polygon polygon;
|
|
bool ok = Zone::ParsePolygonString("0.00,0.00 99.96,0.00 99.96,99.93 0.00,99.93", polygon);
|
|
REQUIRE(ok);
|
|
|
|
auto const &verts = polygon.GetVertices();
|
|
REQUIRE(verts.size() == 4);
|
|
// atoi("99.96") = 99, atoi("99.93") = 99
|
|
// On a 2560x1440 monitor this would be a 99x99 pixel zone — essentially no coverage
|
|
REQUIRE(verts[1] == Vector2(99, 0));
|
|
REQUIRE(verts[2] == Vector2(99, 99));
|
|
}
|
|
|
|
TEST_CASE("Zone: decimal coords through ParsePercentagePolygon give correct pixels", "[Zone]") {
|
|
// Same coords as above, but correctly routed to ParsePercentagePolygon
|
|
// by the auto-detect logic (decimal point present)
|
|
Polygon polygon;
|
|
unsigned int width = 2560;
|
|
unsigned int height = 1440;
|
|
|
|
bool ok = Zone::ParsePercentagePolygon(
|
|
"0.00,0.00 99.96,0.00 99.96,99.93 0.00,99.93", width, height, polygon);
|
|
REQUIRE(ok);
|
|
|
|
auto const &verts = polygon.GetVertices();
|
|
REQUIRE(verts.size() == 4);
|
|
// 99.96% of 2560 = 2558.976 -> 2559
|
|
REQUIRE(verts[1].x_ == 2559);
|
|
REQUIRE(verts[1].y_ == 0);
|
|
// 99.93% of 1440 = 1438.992 -> 1439
|
|
REQUIRE(verts[2].x_ == 2559);
|
|
REQUIRE(verts[2].y_ == 1439);
|
|
}
|
|
|
|
TEST_CASE("Zone: integer pixel coords stay as pixels", "[Zone]") {
|
|
// Legacy integer-only coords should be parsed as raw pixel values
|
|
// Auto-detect: no decimal point -> ParsePolygonString
|
|
Polygon polygon;
|
|
bool ok = Zone::ParsePolygonString("0,0 2559,0 2559,1439 0,1439", polygon);
|
|
REQUIRE(ok);
|
|
|
|
auto const &verts = polygon.GetVertices();
|
|
REQUIRE(verts.size() == 4);
|
|
REQUIRE(verts[0] == Vector2(0, 0));
|
|
REQUIRE(verts[1] == Vector2(2559, 0));
|
|
REQUIRE(verts[2] == Vector2(2559, 1439));
|
|
REQUIRE(verts[3] == Vector2(0, 1439));
|
|
}
|
|
|
|
TEST_CASE("Zone: auto-detect heuristic — strchr for decimal point", "[Zone]") {
|
|
// Verify the heuristic used by the zone loader:
|
|
// strchr(coords, '.') distinguishes percentage from pixel coords
|
|
|
|
// Percentage coords always have decimal points from round(..., 2)
|
|
const char *pct_coords = "0.00,0.00 99.96,0.00 99.96,99.93 0.00,99.93";
|
|
REQUIRE(strchr(pct_coords, '.') != nullptr);
|
|
|
|
// Legacy pixel coords are always integers
|
|
const char *px_coords = "0,0 2559,0 2559,1439 0,1439";
|
|
REQUIRE(strchr(px_coords, '.') == nullptr);
|
|
|
|
// Edge case: small pixel zone that looks like it could be percentages
|
|
// but has no decimal points — correctly detected as pixels
|
|
const char *small_px = "0,0 50,0 50,50 0,50";
|
|
REQUIRE(strchr(small_px, '.') == nullptr);
|
|
}
|
|
|
|
TEST_CASE("Zone: percentage coords at various resolutions", "[Zone]") {
|
|
Polygon polygon;
|
|
|
|
// The same percentage zone should produce proportional pixel coords
|
|
// regardless of monitor resolution
|
|
const char *coords = "10.00,20.00 90.00,20.00 90.00,80.00 10.00,80.00";
|
|
|
|
SECTION("640x480") {
|
|
bool ok = Zone::ParsePercentagePolygon(coords, 640, 480, polygon);
|
|
REQUIRE(ok);
|
|
auto const &v = polygon.GetVertices();
|
|
REQUIRE(v[0] == Vector2(64, 96)); // 10% of 640, 20% of 480
|
|
REQUIRE(v[1] == Vector2(576, 96)); // 90% of 640
|
|
REQUIRE(v[2] == Vector2(576, 384)); // 80% of 480
|
|
}
|
|
|
|
SECTION("3840x2160 (4K)") {
|
|
bool ok = Zone::ParsePercentagePolygon(coords, 3840, 2160, polygon);
|
|
REQUIRE(ok);
|
|
auto const &v = polygon.GetVertices();
|
|
REQUIRE(v[0] == Vector2(384, 432)); // 10% of 3840, 20% of 2160
|
|
REQUIRE(v[1] == Vector2(3456, 432)); // 90% of 3840
|
|
REQUIRE(v[2] == Vector2(3456, 1728)); // 80% of 2160
|
|
}
|
|
}
|