ParsePercentagePolygon clamped to [0, width] / [0, height], so a new
zone created at 100% produced pixel coords equal to the monitor
dimensions. The rasterizer requires hi_x < width and hi_y < height,
so the runtime then logged warnings like
"polygon hi_x (1920) >= image width (1920), clamping".
Clamp to width-1 / height-1 instead. Updates existing tests that
encoded the previous behavior.
Zone threshold fields (MinAlarmPixels, MaxAlarmPixels, MinFilterPixels,
MaxFilterPixels, MinBlobPixels, MaxBlobPixels) were being corrupted on
every save because the PHP conversion used monitor pixel area instead of
zone pixel area. This caused values to inflate progressively, breaking
motion detection.
The fix changes the storage model: thresholds are now stored as
percentages of zone area (DECIMAL(7,2) columns) matching the percentage
coordinate system from zm_update-1.39.2. The C++ Zone::Load() converts
percentages to pixel counts at runtime using polygon.Area(). Legacy
pixel-coordinate zones pass through unchanged.
JS changes:
- submitForm() converts to percentages when in Pixels display mode
- Init skips applyZoneUnits() since DB values are already percentages
- limitRange() only updates field.value when constrained value differs,
preventing oninput from stripping decimal points mid-keystroke
fixes#4690
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
The lo_x/lo_y/hi_x/hi_y variables were unsigned int despite being assigned
from signed int32 Vector2 members and compared against signed int dimensions.
This caused scattered (int) and (unsigned int) casts throughout CheckAlarms,
std_alarmedpixels, and Setup. Switching to int eliminates the casts, fixes
the signed/unsigned comparison warnings, and makes the -1 sentinel check in
std_alarmedpixels explicit rather than relying on unsigned wraparound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a camera reconnects at a different resolution than the zone polygons
were configured for, the polygon bounding box can exceed the frame
dimensions. This caused a heap buffer overflow in the memset that zeros
the bbox rows of the diff image, confirmed by Valgrind (invalid write
at 0 bytes past a 921600-byte / 1280x720 block).
- Validate delta_image buffer and dimensions at entry
- Clamp hi_y/hi_x to image height/width before memset and pixel loops
- Guard filter, blob, and HighlightEdges loops against empty rows where
ranges[y].lo_x is -1 (would wrap to UINT_MAX as unsigned)
- Clamp extents in std_alarmedpixels independently for defense in depth
Warning() logs identify which zones need polygon reconfiguration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cherry-picked src/ changes from edge branch commit 17450d122.
Adds ParsePercentagePolygon() to convert percentage-based zone coordinates
to pixel values using monitor dimensions. Zone::Load() now checks the Units
column to dispatch between legacy pixel parsing and percentage-based parsing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of copying the entire delta_image into the per-zone mask buffer
every frame (4MB memcpy for 4MP), allocate a persistent grayscale mask
and only zero the polygon bounding-box rows. alarmedpixels_row now
reads from delta_image (const, read-only) and writes threshold results
to the separate mask buffer.
Key changes:
- alarmedpixels_row takes separate pdelta (read) and pmask (write) pointers
- std_alarmedpixels accepts both delta_image (const) and mask_image
- CheckAlarms allocates mask with explicit linesize == width to match
filter/blob pointer arithmetic (avoids FFALIGN padding mismatch)
- Full buffer zero on allocation; bbox-only zero on reuse
- No functional change to filter, blob, or HighlightEdges stages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restructure std_alarmedpixels inner loop so GCC/Clang can auto-vectorize
it at -O2/-O3. The compiler now emits 16-byte SIMD (SSE2/NEON) processing
16 pixels per iteration instead of 1.
Three changes enable this:
- Extract inner loop into static alarmedpixels_row() with __restrict__
on function parameters, giving the compiler a strong no-alias guarantee
- Use branchless bitwise AND instead of short-circuit && to avoid
branches that block vectorization
- Remove per-row Debug(7) call that clobbered memory from the compiler's
perspective, invalidating pointer analysis
The original hand-written SSE2 ASM (removed in 2011, commit 8e9ccfe1e)
had alignment restrictions and didn't use per-row polygon ranges. This
approach is portable, maintainable, and achieves equivalent throughput.
GCC confirms: "loop vectorized using 16 byte vectors"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace delete/new Image cycle with lazy-alloc + Assign(). When the
buffer already exists (every frame after the first), Assign() detects
matching dimensions and does a plain memcpy into the existing
allocation, eliminating an aligned malloc+free of ~2 MB per zone per
analyzed frame.
With 4 zones at 15 fps this removes 60 alloc/free cycles per second
from the analysis hot path. The HighlightEdges code path (analysis
images) still allocates a new Image and deletes the old diff buffer,
which is correct — the next Assign() will reallocate once to restore
the single-channel format, then resume reuse.
Behaviorally equivalent: Zone dimensions are constant during zone
lifetime, the destructor already handles cleanup via delete image,
and the only external consumer (Monitor::Analyse → AlarmImage →
Overlay) reads the image without storing pointers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Apply credentials to secondary stream URL in FFmpegCamera (was causing 401 Unauthorized)
- Add empty check for rtsp_second_path in RTSP2WebManager before applying credentials
- Replace unsafe sprintf pattern in Monitor::DumpSettings with std::string + stringtf
- Refactor Zone::DumpSettings to return std::string instead of writing to char buffer
- Add decimal precision to event duration debug output
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>