mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-09-23 02:55:44 -04:00
The alarmed/filtered pixel check methods handed Overlay() the raw GRAY8 scoring mask. Overlay keys on a non-zero source pixel and copies that byte, so the highlight took whatever shape one byte has in the destination format: the red channel on an RGB32 monitor, which is the whole reason alarms have always come out red; all three channels on RGB24, giving white; luma alone on YUV420. The zone's configured Alarm Colour was honoured only on the blob path, which goes through HighlightEdges. Generalise HighlightEdges into BuildHighlight, which takes an edges_only flag and otherwise fills every marked pixel, and build the highlight for the pixel methods the same way the blob path already builds its outline: in the capture's own pixel format, carrying alarm_rgb, once scoring is finished with the GRAY8 mask. HighlightEdges stays as a thin wrapper so the blob path and its callers are unchanged. This is a behaviour change: monitors left on the default red see no difference, but a zone configured with any other Alarm Colour now paints that colour instead of red or white. Reverting the zone hunk fails the new test in all three of its sections. Full suite: 148 cases, 12535 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBHBB95RBX7D9p8ge2WDZb
1223 lines
46 KiB
C++
1223 lines
46 KiB
C++
//
|
|
// ZoneMinder Zone Class Implementation, $Date$, $Revision$
|
|
// Copyright (C) 2001-2008 Philip Coombes
|
|
//
|
|
// 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, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
//
|
|
|
|
#include "zm_zone.h"
|
|
|
|
#include "zm_fifo.h"
|
|
#include "zm_fifo_debug.h"
|
|
#include "zm_monitor.h"
|
|
|
|
#include <cstdlib>
|
|
|
|
void Zone::Setup(
|
|
ZoneType p_type,
|
|
const Polygon &p_polygon,
|
|
const Rgb p_alarm_rgb,
|
|
CheckMethod p_check_method,
|
|
int p_min_pixel_threshold,
|
|
int p_max_pixel_threshold,
|
|
int p_min_alarm_pixels,
|
|
int p_max_alarm_pixels,
|
|
const Vector2 &p_filter_box,
|
|
int p_min_filter_pixels,
|
|
int p_max_filter_pixels,
|
|
int p_min_blob_pixels,
|
|
int p_max_blob_pixels,
|
|
int p_min_blobs,
|
|
int p_max_blobs,
|
|
int p_overload_frames,
|
|
int p_extend_alarm_frames
|
|
) {
|
|
type = p_type;
|
|
polygon = p_polygon;
|
|
alarm_rgb = p_alarm_rgb;
|
|
check_method = p_check_method;
|
|
min_pixel_threshold = p_min_pixel_threshold;
|
|
max_pixel_threshold = p_max_pixel_threshold;
|
|
min_alarm_pixels = p_min_alarm_pixels;
|
|
max_alarm_pixels = p_max_alarm_pixels;
|
|
filter_box = p_filter_box;
|
|
min_filter_pixels = p_min_filter_pixels;
|
|
max_filter_pixels = p_max_filter_pixels;
|
|
min_blob_pixels = p_min_blob_pixels;
|
|
max_blob_pixels = p_max_blob_pixels;
|
|
min_blobs = p_min_blobs;
|
|
max_blobs = p_max_blobs;
|
|
overload_frames = p_overload_frames;
|
|
extend_alarm_frames = p_extend_alarm_frames;
|
|
|
|
#if 0
|
|
Debug( 1, "Initialised zone %d/%s - %d - %dx%d - Rgb:%06x, CM:%d, MnAT:%d, MxAT:%d, MnAP:%d, MxAP:%d, FB:%dx%d, MnFP:%d, MxFP:%d, MnBS:%d, MxBS:%d, MnB:%d, MxB:%d, OF: %d, AF: %d",
|
|
id, label.c_str(), type, polygon.Width(), polygon.Height(), alarm_rgb, check_method, min_pixel_threshold, max_pixel_threshold, min_alarm_pixels, max_alarm_pixels, filter_box.X(), filter_box.Y(), min_filter_pixels, max_filter_pixels, min_blob_pixels, max_blob_pixels, min_blobs, max_blobs, overload_frames, extend_alarm_frames );
|
|
#endif
|
|
|
|
ResetStats();
|
|
image = nullptr;
|
|
|
|
overload_count = 0;
|
|
extend_alarm_count = 0;
|
|
|
|
pg_image = new Image(monitor->Width(), monitor->Height(), 1, ZM_SUBPIX_ORDER_NONE);
|
|
pg_image->Clear();
|
|
pg_image->Fill(0xff, polygon);
|
|
pg_image->Outline(0xff, polygon);
|
|
|
|
ranges = new Range[monitor->Height()];
|
|
for ( unsigned int y = 0; y < monitor->Height(); y++ ) {
|
|
ranges[y].lo_x = -1;
|
|
ranges[y].hi_x = 0;
|
|
ranges[y].off_x = 0;
|
|
const uint8_t *ppoly = pg_image->Buffer( 0, y );
|
|
for ( unsigned int x = 0; x < monitor->Width(); x++, ppoly++ ) {
|
|
if ( *ppoly ) {
|
|
if ( ranges[y].lo_x == -1 ) {
|
|
ranges[y].lo_x = x;
|
|
}
|
|
if ( ranges[y].hi_x < (int)x ) {
|
|
ranges[y].hi_x = x;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (config.record_diag_images) {
|
|
if (config.record_diag_images_fifo) {
|
|
diag_path = stringtf("%s/diagpipe-%d-poly.jpg",
|
|
staticConfig.PATH_SOCKS.c_str(), id);
|
|
|
|
Fifo::fifo_create_if_missing(diag_path);
|
|
} else {
|
|
diag_path = stringtf("%s/diag-%d-poly.jpg",
|
|
monitor->getStorage()->Path(), id);
|
|
}
|
|
|
|
pg_image->WriteJpeg(diag_path, config.record_diag_images_fifo);
|
|
}
|
|
} // end Zone::Setup
|
|
|
|
Zone::~Zone() {
|
|
if (image)
|
|
delete image;
|
|
delete pg_image;
|
|
delete[] ranges;
|
|
}
|
|
|
|
void Zone::RecordStats(const Event *event) {
|
|
std::string sql = stringtf(
|
|
"INSERT INTO Stats SET MonitorId=%d, ZoneId=%d, EventId=%" PRIu64 ", FrameId=%d, "
|
|
"PixelDiff=%d, AlarmPixels=%d, FilterPixels=%d, BlobPixels=%d, "
|
|
"Blobs=%d, MinBlobSize=%d, MaxBlobSize=%d, "
|
|
"MinX=%d, MinY=%d, MaxX=%d, MaxY=%d, Score=%d",
|
|
monitor->Id(), id, event->Id(), event->Frames(),
|
|
stats.pixel_diff_,
|
|
stats.alarm_pixels_,
|
|
stats.alarm_filter_pixels_,
|
|
stats.alarm_blob_pixels_,
|
|
stats.alarm_blobs_,
|
|
stats.min_blob_size_,
|
|
stats.max_blob_size_,
|
|
stats.alarm_box_.Lo().x_,
|
|
stats.alarm_box_.Lo().y_,
|
|
stats.alarm_box_.Hi().x_,
|
|
stats.alarm_box_.Hi().y_,
|
|
stats.score_
|
|
);
|
|
zmDbDo(sql);
|
|
} // end void Zone::RecordStats( const Event *event )
|
|
|
|
bool Zone::CheckOverloadCount() {
|
|
if ( overload_count ) {
|
|
Debug(4, "In overload mode, %d frames of %d remaining", overload_count, overload_frames);
|
|
overload_count--;
|
|
return false;
|
|
}
|
|
return true;
|
|
} // end bool Zone::CheckOverloadCount()
|
|
|
|
void Zone::SetScore(unsigned int nScore) {
|
|
stats.score_ = nScore;
|
|
} // end void Zone::SetScore(unsigned int nScore)
|
|
|
|
// Replace the GRAY8 analysis mask with a highlight in the capture's own pixel
|
|
// format, carrying this zone's alarm colour, and take ownership of it in
|
|
// `image`. Overlay() onto the analysis image is then a same-format copy.
|
|
// monitor->Colours()==1 aliases both GRAY8 and planar YUV420P, so resolve the
|
|
// real format first: a true GRAY8 monitor has no colour to carry, so upgrade
|
|
// its highlight to RGB24; YUV420P keeps its format and carries the alarm
|
|
// colour in chroma.
|
|
void Zone::BuildAlarmHighlight(Image *mask, bool edges_only) {
|
|
AVPixelFormat capture_fmt = zm_pixformat_from_colours(monitor->Colours(), monitor->SubpixelOrder());
|
|
if (capture_fmt == AV_PIX_FMT_GRAY8) {
|
|
image = mask->BuildHighlight(alarm_rgb, ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB, &polygon.Extent(), edges_only);
|
|
} else {
|
|
image = mask->BuildHighlight(alarm_rgb, monitor->Colours(), monitor->SubpixelOrder(), &polygon.Extent(), edges_only);
|
|
}
|
|
// 'image' is now detached from the mask we were handed, so that is ours to free.
|
|
delete mask;
|
|
}
|
|
|
|
void Zone::SetAlarmImage(const Image* srcImage) {
|
|
if ( image )
|
|
delete image;
|
|
image = new Image(*srcImage);
|
|
} // end void Zone::SetAlarmImage( const Image* srcImage )
|
|
|
|
int Zone::GetOverloadCount() {
|
|
return overload_count;
|
|
} // end int Zone::GetOverloadCount()
|
|
|
|
void Zone::SetOverloadCount(int nOverCount) {
|
|
overload_count = nOverCount;
|
|
} // end void Zone::SetOverloadCount(int nOverCount )
|
|
|
|
int Zone::GetOverloadFrames() {
|
|
return overload_frames;
|
|
} // end int Zone::GetOverloadFrames
|
|
|
|
int Zone::GetExtendAlarmCount() {
|
|
return extend_alarm_count;
|
|
} // end int Zone::GetExtendAlarmCount()
|
|
|
|
void Zone::SetExtendAlarmCount(int nExtendAlarmCount) {
|
|
extend_alarm_count = nExtendAlarmCount;
|
|
} // end void Zone::SetExtendAlarmCount( int nExtendAlarmCount )
|
|
|
|
int Zone::GetExtendAlarmFrames() {
|
|
return extend_alarm_frames;
|
|
} // end int Zone::GetExtendAlarmFrames()
|
|
|
|
bool Zone::CheckExtendAlarmCount() {
|
|
Info("ExtendAlarm count: %d, ExtendAlarm frames: %d", extend_alarm_count, extend_alarm_frames);
|
|
if ( extend_alarm_count ) {
|
|
Debug(3, "In extend mode, %d frames of %d remaining", extend_alarm_count, extend_alarm_frames);
|
|
extend_alarm_count--;
|
|
return true;
|
|
}
|
|
return false;
|
|
} // end bool Zone::CheckExtendAlarmCount
|
|
|
|
bool Zone::CheckAlarms(const Image *delta_image) {
|
|
ResetStats();
|
|
|
|
if (overload_count) {
|
|
Info("In overload mode, %d frames of %d remaining", overload_count, overload_frames);
|
|
overload_count--;
|
|
return false;
|
|
}
|
|
|
|
// Validate delta_image dimensions
|
|
if (!delta_image->Buffer() || delta_image->Width() == 0 || delta_image->Height() == 0) {
|
|
Debug(1, "Zone %s: delta_image has no buffer or zero dimensions (%dx%d), skipping",
|
|
label.c_str(), delta_image->Width(), delta_image->Height());
|
|
return false;
|
|
}
|
|
|
|
// Allocate a persistent grayscale mask buffer (same dimensions as delta).
|
|
// Unlike the old code that copied the entire delta_image, we only zero the
|
|
// bounding-box rows and let alarmedpixels_row read from delta_image directly.
|
|
// This replaces a full-image memcpy with a memset of just the zone bbox rows,
|
|
// and avoids read-side cache pollution from copying delta data we never use.
|
|
if (!image
|
|
|| image->Colours() != 1
|
|
|| image->Width() != delta_image->Width()
|
|
|| image->Height() != delta_image->Height()) {
|
|
delete image;
|
|
// Use the (width, linesize, height, ...) constructor to force linesize == width.
|
|
// The filter/blob stages use Width() as the row stride for pointer arithmetic,
|
|
// so linesize must match Width() exactly. The default constructor uses FFALIGN
|
|
// which can pad linesize beyond Width() for non-32-aligned widths.
|
|
int w = delta_image->Width();
|
|
int h = delta_image->Height();
|
|
image = new Image(w, w, h, 1, ZM_SUBPIX_ORDER_NONE);
|
|
// Zero the entire buffer so Overlay() sees kBlack for all pixels
|
|
// outside the bbox. This only runs on first allocation or after
|
|
// HighlightEdges replaced the image with an RGB alarm image.
|
|
memset(image->Buffer(), 0, static_cast<size_t>(w) * h);
|
|
}
|
|
Image *diff_image = image;
|
|
int diff_width = diff_image->Width();
|
|
int diff_height = diff_image->Height();
|
|
uint8_t* diff_buff = diff_image->Buffer();
|
|
uint8_t* pdiff;
|
|
|
|
unsigned int pixel_diff_count = 0;
|
|
|
|
int alarm_lo_x = 0;
|
|
int alarm_hi_x = 0;
|
|
int alarm_lo_y = 0;
|
|
int alarm_hi_y = 0;
|
|
|
|
int alarm_mid_x = -1;
|
|
int alarm_mid_y = -1;
|
|
|
|
//int lo_x = polygon.Extent().Lo().x_;
|
|
int lo_y = polygon.Extent().Lo().y_;
|
|
int hi_x = polygon.Extent().Hi().x_;
|
|
int hi_y = polygon.Extent().Hi().y_;
|
|
|
|
// Clamp polygon extents to image dimensions to prevent buffer overflows.
|
|
// This can happen if zone polygon coordinates exceed the actual frame size
|
|
// (e.g., camera reconnected at a different resolution).
|
|
if (hi_y >= diff_height) {
|
|
Warning("Zone %s: polygon hi_y (%d) >= image height (%d), clamping",
|
|
label.c_str(), hi_y, diff_height);
|
|
hi_y = diff_height - 1;
|
|
}
|
|
if (hi_x >= diff_width) {
|
|
Warning("Zone %s: polygon hi_x (%d) >= image width (%d), clamping",
|
|
label.c_str(), hi_x, diff_width);
|
|
hi_x = diff_width - 1;
|
|
}
|
|
if (lo_y > hi_y) {
|
|
Debug(1, "Zone %s: lo_y (%d) > hi_y (%d) after clamping, skipping", label.c_str(), lo_y, hi_y);
|
|
return false;
|
|
}
|
|
|
|
// Zero the bbox rows so filter/blob stages see kBlack for pixels outside
|
|
// each row's per-polygon ranges. alarmedpixels_row will write kWhite/kBlack
|
|
// for pixels inside the ranges.
|
|
memset(diff_buff + (lo_y * diff_width), 0, static_cast<size_t>(hi_y - lo_y + 1) * diff_width);
|
|
|
|
Debug(4, "Checking alarms for zone %d/%s in lines %d -> %d", id, label.c_str(), lo_y, hi_y);
|
|
|
|
std_alarmedpixels(delta_image, diff_image, pg_image, &stats.alarm_pixels_, &pixel_diff_count);
|
|
|
|
if (config.record_diag_images) {
|
|
diff_image->WriteJpeg(diag_path, config.record_diag_images_fifo);
|
|
}
|
|
|
|
if (pixel_diff_count && stats.alarm_pixels_)
|
|
stats.pixel_diff_ = pixel_diff_count/stats.alarm_pixels_;
|
|
|
|
Debug(5, "Got %d alarmed pixels, need %d -> %d, avg pixel diff %d",
|
|
stats.alarm_pixels_, min_alarm_pixels, max_alarm_pixels, stats.pixel_diff_);
|
|
|
|
if (config.record_diag_images_fifo) {
|
|
FifoDebug(5, "{\"zone\":%d,\"type\":\"ALRM\",\"pixels\":%d,\"avg_diff\":%d}",
|
|
id, stats.alarm_pixels_, stats.pixel_diff_);
|
|
}
|
|
|
|
if (stats.alarm_pixels_) {
|
|
if (min_alarm_pixels && (stats.alarm_pixels_ < (unsigned int)min_alarm_pixels)) {
|
|
/* Not enough pixels alarmed */
|
|
return false;
|
|
} else if (max_alarm_pixels && (stats.alarm_pixels_ > (unsigned int)max_alarm_pixels)) {
|
|
/* Too many pixels alarmed */
|
|
overload_count = overload_frames;
|
|
return false;
|
|
}
|
|
} else {
|
|
/* No alarmed pixels */
|
|
return false;
|
|
}
|
|
|
|
stats.score_ = (100*stats.alarm_pixels_)/(max_alarm_pixels ? max_alarm_pixels : polygon.Area());
|
|
if (stats.score_ < 1)
|
|
stats.score_ = 1; /* Fix for score of 0 when frame meets thresholds but alarmed area is not big enough */
|
|
Debug(5, "Current score is %d", stats.score_);
|
|
|
|
if (check_method >= FILTERED_PIXELS) {
|
|
int bx = filter_box.x_;
|
|
int by = filter_box.y_;
|
|
int bx1 = bx-1;
|
|
int by1 = by-1;
|
|
|
|
Debug(5, "Checking for filtered pixels");
|
|
if (bx > 1 || by > 1) {
|
|
// Now remove any pixels smaller than our filter size
|
|
unsigned char *cpdiff;
|
|
int ldx, hdx, ldy, hdy;
|
|
bool block;
|
|
for (int y = lo_y; y <= hi_y; y++) {
|
|
int lo_x = ranges[y].lo_x;
|
|
int hi_x = ranges[y].hi_x;
|
|
|
|
// Skip rows with no polygon pixels (lo_x == -1 means no pixels)
|
|
if (lo_x < 0 || lo_x > hi_x) continue;
|
|
|
|
pdiff = diff_image->Buffer(lo_x, y);
|
|
|
|
for (int x = lo_x; x <= hi_x; x++, pdiff++) {
|
|
if (*pdiff == kWhite) {
|
|
// Check participation in an X block
|
|
ldx = (x>=(lo_x+bx1))?-bx1:lo_x-x;
|
|
hdx = (x<=(hi_x-bx1))?0:((hi_x-x)-bx1);
|
|
ldy = (y>=(lo_y+by1))?-by1:lo_y-y;
|
|
hdy = (y<=(hi_y-by1))?0:((hi_y-y)-by1);
|
|
block = false;
|
|
for (int dy = ldy; !block && dy <= hdy; dy++) {
|
|
for (int dx = ldx; !block && dx <= hdx; dx++) {
|
|
block = true;
|
|
for (int dy2 = 0; block && dy2 < by; dy2++) {
|
|
for (int dx2 = 0; block && dx2 < bx; dx2++) {
|
|
cpdiff = diff_buff + (((y+dy+dy2)*diff_width) + (x+dx+dx2));
|
|
if (!*cpdiff ) {
|
|
block = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!block) {
|
|
*pdiff = kBlack;
|
|
continue;
|
|
}
|
|
stats.alarm_filter_pixels_++;
|
|
} // end if white
|
|
} // end for x
|
|
} // end foreach y line
|
|
} else {
|
|
stats.alarm_filter_pixels_ = stats.alarm_pixels_;
|
|
}
|
|
|
|
if (config.record_diag_images) {
|
|
diff_image->WriteJpeg(diag_path, config.record_diag_images_fifo);
|
|
}
|
|
|
|
Debug(5, "Got %d filtered pixels, need %d -> %d",
|
|
stats.alarm_filter_pixels_, min_filter_pixels, max_filter_pixels);
|
|
|
|
if (config.record_diag_images_fifo)
|
|
FifoDebug(5, "{\"zone\":%d,\"type\":\"FILT\",\"pixels\":%d}", id, stats.alarm_filter_pixels_);
|
|
|
|
if (stats.alarm_filter_pixels_) {
|
|
if (min_filter_pixels && (stats.alarm_filter_pixels_ < min_filter_pixels)) {
|
|
/* Not enough pixels alarmed */
|
|
stats.score_ = 0;
|
|
return false;
|
|
} else if (max_filter_pixels && (stats.alarm_filter_pixels_ > max_filter_pixels)) {
|
|
/* Too many pixels alarmed */
|
|
overload_count = overload_frames;
|
|
stats.score_ = 0;
|
|
return false;
|
|
}
|
|
} else {
|
|
/* No filtered pixels */
|
|
stats.score_ = 0;
|
|
return false;
|
|
}
|
|
|
|
if (max_filter_pixels != 0)
|
|
stats.score_ = (100*stats.alarm_filter_pixels_)/max_filter_pixels;
|
|
else
|
|
stats.score_ = (100*stats.alarm_filter_pixels_)/polygon.Area();
|
|
|
|
if (stats.score_ < 1)
|
|
stats.score_ = 1; /* Fix for score of 0 when frame meets thresholds but alarmed area is not big enough */
|
|
Debug(5, "Current score is %d", stats.score_);
|
|
|
|
if (check_method >= BLOBS) {
|
|
Debug(5, "Checking for blob pixels");
|
|
// ICON FIXME Would like to get rid of this memset
|
|
memset(blob_stats, 0, sizeof(BlobStats)*256);
|
|
uint8_t *spdiff;
|
|
uint8_t last_x, last_y;
|
|
BlobStats *bsx, *bsy;
|
|
BlobStats *bsm, *bss;
|
|
for (int y = lo_y; y <= hi_y; y++) {
|
|
int lo_x = ranges[y].lo_x;
|
|
int hi_x = ranges[y].hi_x;
|
|
|
|
// Skip rows with no polygon pixels
|
|
if (lo_x < 0 || lo_x > hi_x) continue;
|
|
|
|
pdiff = diff_image->Buffer(lo_x, y);
|
|
for (int x = lo_x; x <= hi_x; x++, pdiff++) {
|
|
if (*pdiff == kWhite) {
|
|
Debug(9, "Got white pixel at %d,%d (%p)", x, y, pdiff);
|
|
|
|
last_x = ((x > 0) && ( (x-1) >= lo_x )) ? *(pdiff-1) : 0;
|
|
last_y = 0;
|
|
if ( y > 0 ) {
|
|
if ( (y-1) >= lo_y && ranges[(y-1)].lo_x <= x && ranges[(y-1)].hi_x >= x ) {
|
|
last_y = *(pdiff-diff_width);
|
|
}
|
|
}
|
|
|
|
if (last_x) {
|
|
Debug(9, "Left neighbour is %d", last_x);
|
|
bsx = &blob_stats[last_x];
|
|
if (last_y) {
|
|
Debug(9, "Top neighbour is %d", last_y);
|
|
bsy = &blob_stats[last_y];
|
|
if (last_x == last_y) {
|
|
Debug(9, "Matching neighbours, setting to %d", last_x);
|
|
// Add to the blob from the x side (either side really)
|
|
*pdiff = last_x;
|
|
stats.alarm_blob_pixels_++;
|
|
bsx->count++;
|
|
if (x > bsx->hi_x) bsx->hi_x = x;
|
|
if (y > bsx->hi_y) bsx->hi_y = y;
|
|
} else {
|
|
// Aggregate blobs
|
|
bsm = bsx->count>=bsy->count?bsx:bsy;
|
|
bss = bsm==bsx?bsy:bsx;
|
|
|
|
Debug(9,
|
|
"Different neighbours, setting pixels of %d to %d\n"
|
|
"Master blob t:%d, c:%d, lx:%d, hx:%d, ly:%d, hy:%d\n"
|
|
"Slave blob t:%d, c:%d, lx:%d, hx:%d, ly:%d, hy:%d\n",
|
|
bss->tag, bsm->tag,
|
|
bsm->tag, bsm->count, bsm->lo_x, bsm->hi_x, bsm->lo_y, bsm->hi_y,
|
|
bss->tag, bss->count, bss->lo_x, bss->hi_x, bss->lo_y, bss->hi_y
|
|
);
|
|
// Now change all those pixels to the other setting
|
|
int changed = 0;
|
|
for (int sy = bss->lo_y; sy <= bss->hi_y; sy++) {
|
|
int lo_sx = bss->lo_x>=ranges[sy].lo_x?bss->lo_x:ranges[sy].lo_x;
|
|
int hi_sx = bss->hi_x<=ranges[sy].hi_x?bss->hi_x:ranges[sy].hi_x;
|
|
|
|
Debug(9,
|
|
"Changing %d, %d->%d Range %d->%d",
|
|
sy, lo_sx, hi_sx, ranges[sy].lo_x, ranges[sy].hi_x
|
|
);
|
|
spdiff = diff_buff + ((diff_width * sy) + lo_sx);
|
|
for (int sx = lo_sx; sx <= hi_sx; sx++, spdiff++) {
|
|
Debug(9, "Pixel at %d,%d (%p) is %d", sx, sy, spdiff, *spdiff);
|
|
if (*spdiff == bss->tag) {
|
|
Debug(9, "Setting pixel");
|
|
*spdiff = bsm->tag;
|
|
changed++;
|
|
}
|
|
}
|
|
} // end for sy = lo_y .. hi_y
|
|
*pdiff = bsm->tag;
|
|
stats.alarm_blob_pixels_++;
|
|
if (!changed) {
|
|
Info(
|
|
"Master blob t:%d, c:%d, lx:%d, hx:%d, ly:%d, hy:%d\n"
|
|
"Slave blob t:%d, c:%d, lx:%d, hx:%d, ly:%d, hy:%d",
|
|
bsm->tag, bsm->count, bsm->lo_x, bsm->hi_x, bsm->lo_y, bsm->hi_y,
|
|
bss->tag, bss->count, bss->lo_x, bss->hi_x, bss->lo_y, bss->hi_y
|
|
);
|
|
Error("No pixels changed, exiting");
|
|
exit(-1);
|
|
}
|
|
|
|
// Merge the slave blob into the master
|
|
bsm->count += bss->count+1;
|
|
if (x > bsm->hi_x) bsm->hi_x = x;
|
|
if (y > bsm->hi_y) bsm->hi_y = y;
|
|
if (bss->lo_x < bsm->lo_x) bsm->lo_x = bss->lo_x;
|
|
if (bss->lo_y < bsm->lo_y) bsm->lo_y = bss->lo_y;
|
|
if (bss->hi_x > bsm->hi_x) bsm->hi_x = bss->hi_x;
|
|
if (bss->hi_y > bsm->hi_y) bsm->hi_y = bss->hi_y;
|
|
|
|
stats.alarm_blobs_--;
|
|
|
|
Debug(6, "Merging blob %d with %d at %d,%d, %d current blobs",
|
|
bss->tag, bsm->tag, x, y, stats.alarm_blobs_);
|
|
|
|
// Clear out the old blob
|
|
bss->tag = 0;
|
|
bss->count = 0;
|
|
bss->lo_x = 0;
|
|
bss->lo_y = 0;
|
|
bss->hi_x = 0;
|
|
bss->hi_y = 0;
|
|
}
|
|
} else {
|
|
Debug(9, "Setting to left neighbour %d", last_x);
|
|
// Add to the blob from the x side
|
|
*pdiff = last_x;
|
|
stats.alarm_blob_pixels_++;
|
|
bsx->count++;
|
|
if (x > bsx->hi_x) bsx->hi_x = x;
|
|
if (y > bsx->hi_y) bsx->hi_y = y;
|
|
}
|
|
} else {
|
|
if (last_y) {
|
|
Debug(9, "Top neighbour is %d", last_y);
|
|
|
|
// Add to the blob from the y side
|
|
BlobStats *bsy = &blob_stats[last_y];
|
|
|
|
*pdiff = last_y;
|
|
stats.alarm_blob_pixels_++;
|
|
bsy->count++;
|
|
if (x > bsy->hi_x) bsy->hi_x = x;
|
|
if (y > bsy->hi_y) bsy->hi_y = y;
|
|
} else {
|
|
// Create a new blob
|
|
int i;
|
|
for (i = (kWhite-1); i > 0; i--) {
|
|
BlobStats *bs = &blob_stats[i];
|
|
// See if we can recycle one first, only if it's at least two rows up
|
|
if (bs->count && bs->hi_y < (y-1)) {
|
|
if (
|
|
(min_blob_pixels && bs->count < min_blob_pixels)
|
|
||
|
|
(max_blob_pixels && bs->count > max_blob_pixels)
|
|
) {
|
|
if (( monitor->GetOptSaveJPEGs() > 1 ) || config.record_diag_images) {
|
|
for (int sy = bs->lo_y; sy <= bs->hi_y; sy++) {
|
|
spdiff = diff_buff + ((diff_width * sy) + bs->lo_x);
|
|
for (int sx = bs->lo_x; sx <= bs->hi_x; sx++, spdiff++) {
|
|
if (*spdiff == bs->tag) {
|
|
*spdiff = kBlack;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stats.alarm_blobs_--;
|
|
stats.alarm_blob_pixels_ -= bs->count;
|
|
|
|
Debug(6, "Eliminated blob %d, %d pixels (%d,%d - %d,%d), %d current blobs",
|
|
i, bs->count, bs->lo_x, bs->lo_y, bs->hi_x, bs->hi_y, stats.alarm_blobs_);
|
|
|
|
bs->tag = 0;
|
|
bs->count = 0;
|
|
bs->lo_x = 0;
|
|
bs->lo_y = 0;
|
|
bs->hi_x = 0;
|
|
bs->hi_y = 0;
|
|
}
|
|
}
|
|
if (!bs->count) {
|
|
Debug(9, "Creating new blob %d", i);
|
|
*pdiff = i;
|
|
stats.alarm_blob_pixels_++;
|
|
bs->tag = i;
|
|
bs->count++;
|
|
bs->lo_x = bs->hi_x = x;
|
|
bs->lo_y = bs->hi_y = y;
|
|
stats.alarm_blobs_++;
|
|
|
|
Debug(6, "Created blob %d at %d,%d, %d current blobs", bs->tag, x, y, stats.alarm_blobs_);
|
|
break;
|
|
}
|
|
}
|
|
if (i == 0) {
|
|
Warning("Max blob count reached. Unable to allocate new blobs so terminating. Zone settings may be too sensitive.");
|
|
x = hi_x+1;
|
|
y = hi_y+1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (config.record_diag_images) {
|
|
diff_image->WriteJpeg(diag_path, config.record_diag_images_fifo);
|
|
}
|
|
|
|
if (!stats.alarm_blobs_) {
|
|
stats.score_ = 0;
|
|
return false;
|
|
}
|
|
|
|
Debug(5, "Got %d raw blob pixels, %d raw blobs, need %d -> %d, %d -> %d",
|
|
stats.alarm_blob_pixels_, stats.alarm_blobs_, min_blob_pixels, max_blob_pixels, min_blobs, max_blobs);
|
|
|
|
if (config.record_diag_images_fifo) {
|
|
FifoDebug(5, "{\"zone\":%d,\"type\":\"RBLB\",\"pixels\":%d,\"blobs\":%d}",
|
|
id, stats.alarm_blob_pixels_, stats.alarm_blobs_);
|
|
}
|
|
|
|
// Now eliminate blobs under the threshold
|
|
for (uint32 i = 1; i < kWhite; i++) {
|
|
BlobStats *bs = &blob_stats[i];
|
|
if (bs->count) {
|
|
if ((min_blob_pixels && bs->count < min_blob_pixels) || (max_blob_pixels && bs->count > max_blob_pixels)) {
|
|
if (( monitor->GetOptSaveJPEGs() > 1 ) || config.record_diag_images) {
|
|
for (int sy = bs->lo_y; sy <= bs->hi_y; sy++) {
|
|
spdiff = diff_buff + ((diff_width * sy) + bs->lo_x);
|
|
for (int sx = bs->lo_x; sx <= bs->hi_x; sx++, spdiff++) {
|
|
if (*spdiff == bs->tag) {
|
|
*spdiff = kBlack;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stats.alarm_blobs_--;
|
|
stats.alarm_blob_pixels_ -= bs->count;
|
|
|
|
Debug(6, "Eliminated blob %d, %d pixels (%d,%d - %d,%d), %d current blobs",
|
|
i, bs->count, bs->lo_x, bs->lo_y, bs->hi_x, bs->hi_y, stats.alarm_blobs_);
|
|
|
|
bs->tag = 0;
|
|
bs->count = 0;
|
|
bs->lo_x = 0;
|
|
bs->lo_y = 0;
|
|
bs->hi_x = 0;
|
|
bs->hi_y = 0;
|
|
} else {
|
|
Debug(6, "Preserved blob %d, %d pixels (%d,%d - %d,%d), %d current blobs",
|
|
i, bs->count, bs->lo_x, bs->lo_y, bs->hi_x, bs->hi_y, stats.alarm_blobs_);
|
|
if (!stats.min_blob_size_ || bs->count < stats.min_blob_size_) stats.min_blob_size_ = bs->count;
|
|
if (!stats.max_blob_size_ || bs->count > stats.max_blob_size_) stats.max_blob_size_ = bs->count;
|
|
}
|
|
} // end if bs_count
|
|
} // end for i < WHITE
|
|
|
|
if (config.record_diag_images) {
|
|
diff_image->WriteJpeg(diag_path, config.record_diag_images_fifo);
|
|
}
|
|
|
|
Debug(5, "Got %d blob pixels, %d blobs, need %d -> %d, %d -> %d",
|
|
stats.alarm_blob_pixels_, stats.alarm_blobs_, min_blob_pixels, max_blob_pixels, min_blobs, max_blobs);
|
|
|
|
if (config.record_diag_images_fifo) {
|
|
FifoDebug(5, "{\"zone\":%d,\"type\":\"FBLB\",\"pixels\":%d,\"blobs\":%d}",
|
|
id, stats.alarm_blob_pixels_, stats.alarm_blobs_);
|
|
}
|
|
|
|
if (stats.alarm_blobs_) {
|
|
if (min_blobs && (stats.alarm_blobs_ < min_blobs)) {
|
|
/* Not enough pixels alarmed */
|
|
stats.score_ = 0;
|
|
return false;
|
|
} else if (max_blobs && (stats.alarm_blobs_ > max_blobs)) {
|
|
/* Too many pixels alarmed */
|
|
overload_count = overload_frames;
|
|
stats.score_ = 0;
|
|
return false;
|
|
}
|
|
} else {
|
|
/* No blobs */
|
|
stats.score_ = 0;
|
|
return false;
|
|
}
|
|
|
|
if (max_blob_pixels != 0)
|
|
stats.score_ = (100*stats.alarm_blob_pixels_)/max_blob_pixels;
|
|
else
|
|
stats.score_ = (100*stats.alarm_blob_pixels_)/polygon.Area();
|
|
|
|
if (stats.score_ < 1)
|
|
stats.score_ = 1; /* Fix for score of 0 when frame meets thresholds but alarmed area is not big enough */
|
|
Debug(5, "Current score is %d", stats.score_);
|
|
|
|
alarm_lo_x = polygon.Extent().Hi().x_ + 1;
|
|
alarm_hi_x = polygon.Extent().Lo().x_ - 1;
|
|
alarm_lo_y = polygon.Extent().Hi().y_ + 1;
|
|
alarm_hi_y = polygon.Extent().Lo().y_ - 1;
|
|
|
|
for (uint32 i = 1; i < kWhite; i++) {
|
|
BlobStats *bs = &blob_stats[i];
|
|
if (bs->count) {
|
|
if (bs->count == stats.max_blob_size_) {
|
|
if (config.weighted_alarm_centres) {
|
|
unsigned long x_total = 0;
|
|
unsigned long y_total = 0;
|
|
|
|
for (int sy = bs->lo_y; sy <= bs->hi_y; sy++) {
|
|
spdiff = diff_buff + ((diff_width * sy) + bs->lo_x);
|
|
for (int sx = bs->lo_x; sx <= bs->hi_x; sx++, spdiff++) {
|
|
if (*spdiff == bs->tag) {
|
|
x_total += sx;
|
|
y_total += sy;
|
|
}
|
|
}
|
|
} // end for sy = lo_y .. hi_y
|
|
alarm_mid_x = int(round(x_total/bs->count));
|
|
alarm_mid_y = int(round(y_total/bs->count));
|
|
} else {
|
|
alarm_mid_x = int((bs->hi_x+bs->lo_x+1)/2);
|
|
alarm_mid_y = int((bs->hi_y+bs->lo_y+1)/2);
|
|
}
|
|
}
|
|
|
|
if (alarm_lo_x > bs->lo_x) alarm_lo_x = bs->lo_x;
|
|
if (alarm_lo_y > bs->lo_y) alarm_lo_y = bs->lo_y;
|
|
if (alarm_hi_x < bs->hi_x) alarm_hi_x = bs->hi_x;
|
|
if (alarm_hi_y < bs->hi_y) alarm_hi_y = bs->hi_y;
|
|
} // end if bs->count
|
|
} // end for i < WHITE
|
|
} else {
|
|
alarm_mid_x = int((alarm_hi_x+alarm_lo_x+1)/2);
|
|
alarm_mid_y = int((alarm_hi_y+alarm_lo_y+1)/2);
|
|
}
|
|
}
|
|
|
|
if (type == INCLUSIVE) {
|
|
// score >>= 1;
|
|
stats.score_ /= 2;
|
|
} else if (type == EXCLUSIVE) {
|
|
// stats.score <<= 1;
|
|
stats.score_ *= 2;
|
|
}
|
|
|
|
Debug(5, "Adjusted score is %d", stats.score_);
|
|
|
|
// Now outline the changed region
|
|
if (stats.score_) {
|
|
stats.alarm_box_ = Box(Vector2(alarm_lo_x, alarm_lo_y), Vector2(alarm_hi_x, alarm_hi_y));
|
|
|
|
//if ( monitor->followMotion() )
|
|
if ( true ) {
|
|
stats.alarm_centre_ = Vector2(alarm_mid_x, alarm_mid_y);
|
|
} else {
|
|
stats.alarm_centre_ = stats.alarm_box_.Centre();
|
|
}
|
|
|
|
if ((type < PRECLUSIVE) && (check_method >= BLOBS) && (monitor->GetOptSaveJPEGs() > 1)) {
|
|
|
|
int lo_x = polygon.Extent().Lo().x_;
|
|
// First mask out anything we don't want
|
|
for (int y = lo_y; y <= hi_y; y++) {
|
|
int lo_x2 = ranges[y].lo_x;
|
|
int hi_x2 = ranges[y].hi_x;
|
|
|
|
// Skip rows with no polygon pixels
|
|
if (lo_x2 < 0 || lo_x2 > hi_x2) continue;
|
|
|
|
pdiff = diff_buff + ((diff_width * y) + lo_x);
|
|
|
|
int lo_gap = lo_x2-lo_x;
|
|
if (lo_gap > 0) {
|
|
if (lo_gap == 1) {
|
|
*pdiff++ = kBlack;
|
|
} else {
|
|
memset(pdiff, kBlack, lo_gap);
|
|
pdiff += lo_gap;
|
|
}
|
|
}
|
|
|
|
const uint8_t* ppoly = pg_image->Buffer(lo_x2, y);
|
|
for (int x = lo_x2; x <= hi_x2; x++, pdiff++, ppoly++) {
|
|
if (!*ppoly) {
|
|
*pdiff = kBlack;
|
|
}
|
|
}
|
|
|
|
int hi_gap = hi_x-hi_x2;
|
|
if (hi_gap > 0) {
|
|
if (hi_gap == 1) {
|
|
*pdiff = kBlack;
|
|
} else {
|
|
memset(pdiff, kBlack, hi_gap);
|
|
}
|
|
}
|
|
} // end for y
|
|
|
|
// Build the alarm highlight in the capture's own pixel format so Overlay()
|
|
// onto the analysis image is a same-format copy. monitor->Colours()==1
|
|
// aliases both GRAY8 and planar YUV420P, so resolve the real format first:
|
|
// a true GRAY8 monitor has no colour to carry, so upgrade its highlight to
|
|
// RGB24; YUV420P keeps its format and carries the alarm colour in chroma.
|
|
BuildAlarmHighlight(diff_image, true);
|
|
diff_image = nullptr;
|
|
} else if ((type < PRECLUSIVE) && (monitor->GetOptSaveJPEGs() > 1)) {
|
|
// Alarmed/filtered pixels: the mask stays GRAY8 through the analysis
|
|
// above because the scoring reads it as one byte per pixel. Overlay()
|
|
// onto the analysis image then keys on a non-zero source pixel, so a
|
|
// GRAY8 mask painted an undifferentiated highlight -- writing the mask
|
|
// value into the red channel on an RGB32 monitor (hence "alarms are
|
|
// always red"), into all three on RGB24 (white), and into luma alone on
|
|
// YUV420 -- and the zone's configured Alarm Colour was honoured only on
|
|
// the blob path. Build the same colour highlight the blob path builds,
|
|
// filled rather than outlined, now that scoring is done with the mask.
|
|
BuildAlarmHighlight(diff_image, false);
|
|
diff_image = nullptr;
|
|
} // end if ( (type < PRECLUSIVE) && (check_method >= BLOBS) && (monitor->GetOptSaveJPEGs() > 1)
|
|
|
|
Debug(1, "%s: Pixel Diff: %d, Alarm Pixels: %d, Filter Pixels: %d, Blob Pixels: %d, Blobs: %d, Score: %d",
|
|
Label(), stats.pixel_diff_, stats.alarm_pixels_, stats.alarm_filter_pixels_, stats.alarm_blob_pixels_, stats.alarm_blobs_, stats.score_);
|
|
} // end if score
|
|
return true;
|
|
}
|
|
|
|
bool Zone::ParsePolygonString(const char *poly_string, Polygon &polygon) {
|
|
char *str = (char *)poly_string;
|
|
int max_n_coords = strlen(str)/4;
|
|
|
|
std::vector<Vector2> vertices;
|
|
vertices.reserve(max_n_coords);
|
|
|
|
while (*str != '\0') {
|
|
char *cp = strchr(str, ',');
|
|
if (!cp) {
|
|
Error("Bogus coordinate %s found in polygon string", str);
|
|
break;
|
|
}
|
|
|
|
int x = atoi(str);
|
|
int y = atoi(cp + 1);
|
|
Debug(3, "Got coordinate %d,%d from polygon string", x, y);
|
|
vertices.emplace_back(x, y);
|
|
|
|
char *ws = strchr(cp + 2, ' ');
|
|
if (ws) {
|
|
str = ws + 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (vertices.size() > 2) {
|
|
Debug(3, "Successfully parsed polygon string %s", str);
|
|
polygon = Polygon(vertices);
|
|
} else {
|
|
Error("Not enough coordinates to form a polygon!");
|
|
}
|
|
|
|
return !vertices.empty();
|
|
} // end bool Zone::ParsePolygonString(const char *poly_string, Polygon &polygon)
|
|
|
|
bool Zone::ParsePercentagePolygon(const char *poly_string, unsigned int width, unsigned int height, Polygon &polygon) {
|
|
double mon_w = static_cast<double>(width);
|
|
double mon_h = static_cast<double>(height);
|
|
std::vector<Vector2> vertices;
|
|
const char *str = poly_string;
|
|
|
|
while (*str != '\0') {
|
|
const char *cp = strchr(str, ',');
|
|
if (!cp) {
|
|
Error("Bogus coordinate %s found in polygon string", str);
|
|
break;
|
|
}
|
|
|
|
double pct_x = strtod(str, nullptr);
|
|
double pct_y = strtod(cp + 1, nullptr);
|
|
int32 px_x = static_cast<int32>(std::lround(pct_x * mon_w / 100.0));
|
|
int32 px_y = static_cast<int32>(std::lround(pct_y * mon_h / 100.0));
|
|
|
|
// Clamp to monitor bounds. Max valid pixel index is width-1/height-1;
|
|
// values equal to width/height cause out-of-bounds warnings in the rasterizer.
|
|
px_x = std::clamp(px_x, static_cast<int32>(0), static_cast<int32>(width) - 1);
|
|
px_y = std::clamp(px_y, static_cast<int32>(0), static_cast<int32>(height) - 1);
|
|
|
|
Debug(3, "Percentage coord %.2f,%.2f -> pixel %d,%d", pct_x, pct_y, px_x, px_y);
|
|
vertices.emplace_back(px_x, px_y);
|
|
|
|
const char *ws = strchr(cp + 2, ' ');
|
|
if (ws) {
|
|
str = ws + 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (vertices.size() > 2) {
|
|
polygon = Polygon(vertices);
|
|
return true;
|
|
}
|
|
Error("Not enough coordinates to form a polygon from '%s'", poly_string);
|
|
return false;
|
|
} // end bool Zone::ParsePercentagePolygon
|
|
|
|
bool Zone::ParseZoneString(const char *zone_string, unsigned int &zone_id, int &colour, Polygon &polygon) {
|
|
Debug(3, "Parsing zone string '%s'", zone_string);
|
|
|
|
char *str_ptr = new char[strlen(zone_string)+1];
|
|
char *str = str_ptr;
|
|
strcpy(str, zone_string);
|
|
|
|
zone_id = strtol(str, 0, 10);
|
|
Debug(3, "Got zone %d from zone string", zone_id);
|
|
|
|
char *ws = strchr(str, ' ');
|
|
if ( !ws ) {
|
|
Debug(3, "No initial whitespace found in zone string '%s', finishing", str);
|
|
delete[] str_ptr;
|
|
return true;
|
|
}
|
|
|
|
*ws = '\0';
|
|
str = ws+1;
|
|
|
|
colour = strtol(str, 0, 16);
|
|
Debug(3, "Got colour %06x from zone string", colour);
|
|
ws = strchr(str, ' ');
|
|
if ( !ws ) {
|
|
Debug(3, "No secondary whitespace found in zone string '%s', finishing", zone_string);
|
|
delete[] str_ptr;
|
|
return true;
|
|
}
|
|
*ws = '\0';
|
|
str = ws+1;
|
|
|
|
bool result = ParsePolygonString(str, polygon);
|
|
delete[] str_ptr;
|
|
|
|
return result;
|
|
} // end bool Zone::ParseZoneString(const char *zone_string, int &zone_id, int &colour, Polygon &polygon)
|
|
|
|
std::vector<Zone> Zone::Load(const std::shared_ptr<Monitor> &monitor) {
|
|
std::string sql = stringtf("SELECT Id,Name,Type+0,Units,Coords,AlarmRGB,CheckMethod+0,"
|
|
"MinPixelThreshold,MaxPixelThreshold,MinAlarmPixels,MaxAlarmPixels,"
|
|
"FilterX,FilterY,MinFilterPixels,MaxFilterPixels,"
|
|
"MinBlobPixels,MaxBlobPixels,MinBlobs,MaxBlobs,"
|
|
"OverloadFrames,ExtendAlarmFrames"
|
|
" FROM Zones WHERE MonitorId = %d ORDER BY Type, Id", monitor->Id());
|
|
|
|
MYSQL_RES *result = zmDbFetch(sql);
|
|
if (!result) {
|
|
return {};
|
|
}
|
|
|
|
uint32 n_zones = mysql_num_rows(result);
|
|
Debug(1, "Got %d zones for monitor %s", n_zones, monitor->Name());
|
|
|
|
std::vector<Zone> zones;
|
|
zones.reserve(n_zones);
|
|
|
|
for (MYSQL_ROW dbrow = mysql_fetch_row(result); dbrow; dbrow = mysql_fetch_row(result)) {
|
|
int col = 0;
|
|
|
|
int Id = atoi(dbrow[col++]);
|
|
const char *Name = dbrow[col++];
|
|
ZoneType Type = static_cast<ZoneType>(atoi(dbrow[col++]));
|
|
const char *Units = dbrow[col++];
|
|
const char *Coords = dbrow[col++];
|
|
int AlarmRGB = dbrow[col]?atoi(dbrow[col]):0;
|
|
col++;
|
|
Zone::CheckMethod CheckMethod = static_cast<Zone::CheckMethod>(atoi(dbrow[col++]));
|
|
int MinPixelThreshold = dbrow[col]?atoi(dbrow[col]):0;
|
|
col++;
|
|
int MaxPixelThreshold = dbrow[col]?atoi(dbrow[col]):0;
|
|
col++;
|
|
double MinAlarmPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
|
col++;
|
|
double MaxAlarmPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
|
col++;
|
|
int FilterX = dbrow[col]?atoi(dbrow[col]):0;
|
|
col++;
|
|
int FilterY = dbrow[col]?atoi(dbrow[col]):0;
|
|
col++;
|
|
double MinFilterPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
|
col++;
|
|
double MaxFilterPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
|
col++;
|
|
double MinBlobPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
|
col++;
|
|
double MaxBlobPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
|
col++;
|
|
int MinBlobs = dbrow[col]?atoi(dbrow[col]):0;
|
|
col++;
|
|
int MaxBlobs = dbrow[col]?atoi(dbrow[col]):0;
|
|
col++;
|
|
int OverloadFrames = dbrow[col]?atoi(dbrow[col]):0;
|
|
col++;
|
|
int ExtendAlarmFrames = dbrow[col]?atoi(dbrow[col]):0;
|
|
col++;
|
|
|
|
/* HTML colour code is actually BGR in memory, we want RGB */
|
|
AlarmRGB = rgb_convert(AlarmRGB, ZM_SUBPIX_ORDER_BGR);
|
|
|
|
// Auto-detect coordinate format: decimal points mean percentages,
|
|
// integer-only means legacy pixel values. Units field is not trusted.
|
|
Debug(5, "Parsing polygon %s (Units=%s)", Coords, Units);
|
|
Polygon polygon;
|
|
if (strchr(Coords, '.')) {
|
|
// Decimal values present — treat as percentages regardless of Units field
|
|
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;
|
|
}
|
|
} else {
|
|
// Integer-only coordinates — treat as pixel values
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Convert threshold values from DB format to pixel counts for runtime use.
|
|
// Percentage coordinates: thresholds are stored as % of zone area, convert to pixels.
|
|
// Legacy pixel coordinates: thresholds are already pixel counts.
|
|
int MinAlarmPixels, MaxAlarmPixels, MinFilterPixels, MaxFilterPixels, MinBlobPixels, MaxBlobPixels;
|
|
if (strchr(Coords, '.') && polygon.Area() > 0) {
|
|
int zpa = polygon.Area();
|
|
MinAlarmPixels = MinAlarmPixels_pct > 0 ? static_cast<int>(MinAlarmPixels_pct * zpa / 100.0 + 0.5) : 0;
|
|
MaxAlarmPixels = MaxAlarmPixels_pct > 0 ? static_cast<int>(MaxAlarmPixels_pct * zpa / 100.0 + 0.5) : 0;
|
|
MinFilterPixels = MinFilterPixels_pct > 0 ? static_cast<int>(MinFilterPixels_pct * zpa / 100.0 + 0.5) : 0;
|
|
MaxFilterPixels = MaxFilterPixels_pct > 0 ? static_cast<int>(MaxFilterPixels_pct * zpa / 100.0 + 0.5) : 0;
|
|
MinBlobPixels = MinBlobPixels_pct > 0 ? static_cast<int>(MinBlobPixels_pct * zpa / 100.0 + 0.5) : 0;
|
|
MaxBlobPixels = MaxBlobPixels_pct > 0 ? static_cast<int>(MaxBlobPixels_pct * zpa / 100.0 + 0.5) : 0;
|
|
} else {
|
|
MinAlarmPixels = static_cast<int>(MinAlarmPixels_pct);
|
|
MaxAlarmPixels = static_cast<int>(MaxAlarmPixels_pct);
|
|
MinFilterPixels = static_cast<int>(MinFilterPixels_pct);
|
|
MaxFilterPixels = static_cast<int>(MaxFilterPixels_pct);
|
|
MinBlobPixels = static_cast<int>(MinBlobPixels_pct);
|
|
MaxBlobPixels = static_cast<int>(MaxBlobPixels_pct);
|
|
}
|
|
|
|
if (atoi(dbrow[2]) == Zone::INACTIVE) {
|
|
zones.emplace_back(monitor, Id, Name, polygon);
|
|
} else if (atoi(dbrow[2]) == Zone::PRIVACY) {
|
|
zones.emplace_back(monitor, Id, Name, Type, polygon);
|
|
} else {
|
|
zones.emplace_back(
|
|
monitor, Id, Name, Type, polygon, AlarmRGB,
|
|
CheckMethod, MinPixelThreshold, MaxPixelThreshold,
|
|
MinAlarmPixels, MaxAlarmPixels, Vector2(FilterX, FilterY),
|
|
MinFilterPixels, MaxFilterPixels,
|
|
MinBlobPixels, MaxBlobPixels, MinBlobs, MaxBlobs,
|
|
OverloadFrames, ExtendAlarmFrames);
|
|
}
|
|
} // end foreach row
|
|
mysql_free_result(result);
|
|
return zones;
|
|
} // end std::vector<Zone> Zone::Load(Monitor *monitor)
|
|
|
|
std::string Zone::DumpSettings(bool /*verbose*/) const {
|
|
std::string result;
|
|
result.reserve(1024);
|
|
|
|
result += stringtf(" Id : %u\n", id);
|
|
result += stringtf(" Label : %s\n", label.c_str());
|
|
result += stringtf(" Type: %d - %s\n", type,
|
|
type==ACTIVE?"Active":(
|
|
type==INCLUSIVE?"Inclusive":(
|
|
type==EXCLUSIVE?"Exclusive":(
|
|
type==PRECLUSIVE?"Preclusive":(
|
|
type==INACTIVE?"Inactive":(
|
|
type==PRIVACY?"Privacy":"Unknown"
|
|
))))));
|
|
result += stringtf(" Shape : %zu points\n", polygon.GetVertices().size());
|
|
for (size_t i = 0; i < polygon.GetVertices().size(); i++) {
|
|
result += stringtf(" %zu: %d,%d\n", i, polygon.GetVertices()[i].x_, polygon.GetVertices()[i].y_);
|
|
}
|
|
result += stringtf(" Alarm RGB : %06x\n", alarm_rgb);
|
|
result += stringtf(" Check Method: %d - %s\n", check_method,
|
|
check_method==ALARMED_PIXELS?"Alarmed Pixels":(
|
|
check_method==FILTERED_PIXELS?"FilteredPixels":(
|
|
check_method==BLOBS?"Blobs":"Unknown"
|
|
)));
|
|
result += stringtf(" Min Pixel Threshold : %d\n", min_pixel_threshold);
|
|
result += stringtf(" Max Pixel Threshold : %d\n", max_pixel_threshold);
|
|
result += stringtf(" Min Alarm Pixels : %d\n", min_alarm_pixels);
|
|
result += stringtf(" Max Alarm Pixels : %d\n", max_alarm_pixels);
|
|
result += stringtf(" Filter Box : %d,%d\n", filter_box.x_, filter_box.y_);
|
|
result += stringtf(" Min Filter Pixels : %d\n", min_filter_pixels);
|
|
result += stringtf(" Max Filter Pixels : %d\n", max_filter_pixels);
|
|
result += stringtf(" Min Blob Pixels : %d\n", min_blob_pixels);
|
|
result += stringtf(" Max Blob Pixels : %d\n", max_blob_pixels);
|
|
result += stringtf(" Min Blobs : %d\n", min_blobs);
|
|
result += stringtf(" Max Blobs : %d\n", max_blobs);
|
|
return result;
|
|
}
|
|
|
|
// Scan one row of pixels, thresholding against the polygon mask.
|
|
// Reads from pdelta (the frame-difference image) and writes the binary
|
|
// result to pmask (a separate per-zone mask buffer), so the caller
|
|
// never needs to copy the full delta image.
|
|
// Separated into its own function so the compiler sees __restrict__ on
|
|
// function parameters (where GCC fully trusts it) and no calls that
|
|
// clobber memory, allowing auto-vectorization at -O2/-O3.
|
|
static void alarmedpixels_row(
|
|
const uint8_t *__restrict__ pdelta,
|
|
uint8_t *__restrict__ pmask,
|
|
const uint8_t *__restrict__ ppoly,
|
|
unsigned int count,
|
|
uint8_t calc_min,
|
|
uint8_t calc_max,
|
|
uint32_t &pixelsalarmed,
|
|
uint32_t &pixelsdifference) {
|
|
for (unsigned int i = 0; i < count; i++) {
|
|
const uint8_t d = pdelta[i];
|
|
const uint8_t p = ppoly[i];
|
|
// Bitwise AND avoids short-circuit branches that block vectorization
|
|
const bool alarmed = (p != 0) & (d > calc_min) & (d <= calc_max);
|
|
|
|
pixelsalarmed += alarmed;
|
|
pixelsdifference += alarmed ? d : 0;
|
|
pmask[i] = alarmed ? kWhite : kBlack;
|
|
}
|
|
}
|
|
|
|
void Zone::std_alarmedpixels(
|
|
const Image* pdelta_image,
|
|
Image* pmask_image,
|
|
const Image* ppoly_image,
|
|
unsigned int* pixel_count,
|
|
unsigned int* pixel_sum) {
|
|
uint32_t pixelsalarmed = 0;
|
|
uint32_t pixelsdifference = 0;
|
|
|
|
// Cache member variables locally so the compiler can prove loop-invariance
|
|
const uint8_t calc_max = max_pixel_threshold ? max_pixel_threshold : 255;
|
|
const uint8_t calc_min = min_pixel_threshold;
|
|
|
|
const int img_width = static_cast<int>(pdelta_image->Width());
|
|
const int img_height = static_cast<int>(pdelta_image->Height());
|
|
|
|
int lo_y = polygon.Extent().Lo().y_;
|
|
int hi_y = polygon.Extent().Hi().y_;
|
|
|
|
// Clamp to image bounds
|
|
if (hi_y >= img_height) hi_y = img_height - 1;
|
|
if (lo_y > hi_y) { *pixel_count = 0; *pixel_sum = 0; return; }
|
|
|
|
for (int y = lo_y; y <= hi_y; y++) {
|
|
const int lo_x = ranges[y].lo_x;
|
|
const int hi_x = ranges[y].hi_x;
|
|
|
|
if (lo_x < 0 || lo_x > hi_x) continue;
|
|
|
|
// Clamp hi_x to image width
|
|
const int clamped_hi_x = (hi_x >= img_width) ? img_width - 1 : hi_x;
|
|
|
|
alarmedpixels_row(
|
|
pdelta_image->Buffer(lo_x, y),
|
|
pmask_image->Buffer(lo_x, y),
|
|
ppoly_image->Buffer(lo_x, y),
|
|
clamped_hi_x - lo_x + 1,
|
|
calc_min, calc_max,
|
|
pixelsalarmed, pixelsdifference);
|
|
} // end for y = lo_y to hi_y
|
|
|
|
/* Store the results */
|
|
*pixel_count = pixelsalarmed;
|
|
*pixel_sum = pixelsdifference;
|
|
Debug(7, "STORED pixelsalarmed(%d), pixelsdifference(%d)", pixelsalarmed, pixelsdifference);
|
|
} // end void Zone::std_alarmedpixels
|
|
|
|
Zone::Zone(const Zone &z) :
|
|
monitor(z.monitor),
|
|
id(z.id),
|
|
label(z.label),
|
|
type(z.type),
|
|
polygon(z.polygon),
|
|
alarm_rgb(z.alarm_rgb),
|
|
check_method(z.check_method),
|
|
min_pixel_threshold(z.min_pixel_threshold),
|
|
max_pixel_threshold(z.max_pixel_threshold),
|
|
min_alarm_pixels(z.min_alarm_pixels),
|
|
max_alarm_pixels(z.max_alarm_pixels),
|
|
filter_box(z.filter_box),
|
|
min_filter_pixels(z.min_filter_pixels),
|
|
max_filter_pixels(z.max_filter_pixels),
|
|
min_blob_pixels(z.min_blob_pixels),
|
|
max_blob_pixels(z.max_blob_pixels),
|
|
min_blobs(z.min_blobs),
|
|
max_blobs(z.max_blobs),
|
|
overload_frames(z.overload_frames),
|
|
extend_alarm_frames(z.extend_alarm_frames),
|
|
alarmed(z.alarmed),
|
|
was_alarmed(z.was_alarmed),
|
|
stats(z.stats),
|
|
overload_count(z.overload_count),
|
|
extend_alarm_count(z.extend_alarm_count),
|
|
diag_path(z.diag_path) {
|
|
std::copy(z.blob_stats, z.blob_stats+256, blob_stats);
|
|
pg_image = z.pg_image ? new Image(*z.pg_image) : nullptr;
|
|
ranges = new Range[monitor->Height()];
|
|
std::copy(z.ranges, z.ranges+monitor->Height(), ranges);
|
|
image = z.image ? new Image(*z.image) : nullptr;
|
|
//z.stats.debug("Copy Source");
|
|
stats.DumpToLog("Copy dest");
|
|
}
|