fix: fall back to pixel parsing when zone Units=Percent but coords exceed 100
When zones have Units='Percent' in the database but their Coords contain pixel values (>100), ParsePercentagePolygon treats them as percentages, causing wild scaling (e.g., 639% * 1920 / 100 = 12269) followed by clamping to monitor bounds, producing degenerate full-frame zones. Add a pre-check in Zone::Load that scans coordinate values before calling ParsePercentagePolygon. If any value exceeds 100, log a warning and use ParsePolygonString (pixel path) instead. Also add unit tests for both ParsePolygonString and ParsePercentagePolygon. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>pull/4632/head
parent
ad2373af67
commit
b1ff22e58d
|
|
@ -994,8 +994,44 @@ std::vector<Zone> Zone::Load(const std::shared_ptr<Monitor> &monitor) {
|
|||
continue;
|
||||
}
|
||||
} else {
|
||||
// Percentage-based coordinates (default): convert to pixels using monitor dimensions
|
||||
if (!ParsePercentagePolygon(Coords, monitor->Width(), monitor->Height(), polygon)) {
|
||||
// Percentage-based coordinates (default): convert to pixels using monitor dimensions.
|
||||
// However, if any coordinate value exceeds 100, these are actually pixel values
|
||||
// stored with incorrect Units — fall back to pixel parsing with a warning.
|
||||
bool has_pixel_values = false;
|
||||
{
|
||||
const char *s = Coords;
|
||||
while (*s != '\0') {
|
||||
double val = strtod(s, nullptr);
|
||||
if (val > 100.0) {
|
||||
has_pixel_values = true;
|
||||
break;
|
||||
}
|
||||
// Skip to next number: find comma then space (x,y pairs separated by spaces)
|
||||
const char *comma = strchr(s, ',');
|
||||
if (!comma) break;
|
||||
val = strtod(comma + 1, nullptr);
|
||||
if (val > 100.0) {
|
||||
has_pixel_values = true;
|
||||
break;
|
||||
}
|
||||
const char *space = strchr(comma + 1, ' ');
|
||||
if (space) {
|
||||
s = space + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (has_pixel_values) {
|
||||
Warning("Zone %d/%s has Units=Percent but Coords contain pixel values (>100), "
|
||||
"parsing as pixels instead", Id, Name);
|
||||
if (!ParsePolygonString(Coords, polygon)) {
|
||||
Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring",
|
||||
Coords, Id, Name, monitor->Name());
|
||||
continue;
|
||||
}
|
||||
} else if (!ParsePercentagePolygon(Coords, monitor->Width(), monitor->Height(), polygon)) {
|
||||
Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring",
|
||||
Coords, Id, Name, monitor->Name());
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ set(TEST_SOURCES
|
|||
zm_onvif_renewal.cpp
|
||||
zm_poly.cpp
|
||||
zm_utils.cpp
|
||||
zm_vector2.cpp)
|
||||
zm_vector2.cpp
|
||||
zm_zone.cpp)
|
||||
|
||||
add_executable(tests main.cpp ${TEST_SOURCES})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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::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)));
|
||||
}
|
||||
Loading…
Reference in New Issue