fix: carry the zone's alarm colour on the alarmed and filtered pixel methods

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
This commit is contained in:
Isaac ConnorandClaude Opus 5 committed 2026-09-22 09:57:19 -04:00
1 parent 5b2dd2abbe
commit bdf688eecb
6 files changed
+279 -27

No files matched your search

+22 -17
View File
@@ -1175,14 +1175,15 @@ void Image::Assign(const Image &image) {
}
}
Image *Image::HighlightEdges(
Image *Image::BuildHighlight(
Rgb colour,
unsigned int p_colours,
unsigned int p_subpixelorder,
const Box *limits
const Box *limits,
bool edges_only
) {
if ( imagePixFormat != AV_PIX_FMT_GRAY8 ) {
Panic("Attempt to highlight image edges when colours = %d", colours);
Panic("Attempt to highlight image when colours = %d", colours);
}
/* Convert the colour's RGBA subpixel order into the image's subpixel order */
@@ -1215,12 +1216,13 @@ Image *Image::HighlightEdges(
const uint8_t* p = buffer + (y * src_linesize) + lo_x;
uint8_t* phigh = high_buff + (y * dst_linesize) + lo_x;
for ( unsigned int x = lo_x; x <= hi_x; x++, p++, phigh++ ) {
bool edge = false;
bool mark = false;
if ( *p ) {
edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1))
mark = !edges_only
|| (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1))
|| (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize));
}
if ( edge ) {
if ( mark ) {
*phigh = colour;
}
}
@@ -1230,12 +1232,13 @@ Image *Image::HighlightEdges(
const uint8_t* p = buffer + (y * src_linesize) + lo_x;
uint8_t* phigh = high_buff + (y * dst_linesize) + lo_x * 3;
for ( unsigned int x = lo_x; x <= hi_x; x++, p++, phigh += 3 ) {
bool edge = false;
bool mark = false;
if ( *p ) {
edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1))
mark = !edges_only
|| (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1))
|| (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize));
}
if ( edge ) {
if ( mark ) {
RED_PTR_RGBA(phigh) = RED_VAL_RGBA(colour);
GREEN_PTR_RGBA(phigh) = GREEN_VAL_RGBA(colour);
BLUE_PTR_RGBA(phigh) = BLUE_VAL_RGBA(colour);
@@ -1247,12 +1250,13 @@ Image *Image::HighlightEdges(
const uint8_t* p = buffer + (y * src_linesize) + lo_x;
Rgb* phigh = (Rgb*)(high_buff + (y * dst_linesize) + (lo_x << 2));
for ( unsigned int x = lo_x; x <= hi_x; x++, p++, phigh++ ) {
bool edge = false;
bool mark = false;
if ( *p ) {
edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1))
mark = !edges_only
|| (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1))
|| (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize));
}
if ( edge ) {
if ( mark ) {
*phigh = colour;
}
}
@@ -1260,24 +1264,25 @@ Image *Image::HighlightEdges(
} else if ( zm_is_yuv420(p_pixfmt) ) {
// Single alarm colour over a transparent (Y=0) background; Overlay() onto
// a YUV420 image keys on a non-zero luma marker. Write the colour's luma
// at each edge pixel and its chroma at the shared 2x2 chroma sample.
// at each marked pixel and its chroma at the shared 2x2 chroma sample.
const YUV yuv = brg_to_yuv(colour);
const uint8_t Yc = Y_VAL(yuv), Uc = U_VAL(yuv), Vc = V_VAL(yuv);
uint8_t *hplane[4] = {};
int hstride[4] = {};
if (av_image_fill_arrays(hplane, hstride, high_buff, p_pixfmt, width, height, 32) < 0) {
Error("HighlightEdges: av_image_fill_arrays failed for YUV420 %ux%u", width, height);
Error("Highlight: av_image_fill_arrays failed for YUV420 %ux%u", width, height);
return high_image;
}
for ( unsigned int y = lo_y; y <= hi_y; y++ ) {
const uint8_t* p = buffer + (y * src_linesize) + lo_x;
for ( unsigned int x = lo_x; x <= hi_x; x++, p++ ) {
bool edge = false;
bool mark = false;
if ( *p ) {
edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1))
mark = !edges_only
|| (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1))
|| (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize));
}
if ( edge ) {
if ( mark ) {
hplane[0][y * hstride[0] + x] = Yc ? Yc : 1; // keep the luma marker non-zero
hplane[1][(y / 2) * hstride[1] + (x / 2)] = Uc;
hplane[2][(y / 2) * hstride[2] + (x / 2)] = Vc;
+7 -1
View File
@@ -277,7 +277,13 @@ class Image {
uint8 size = 1,
Rgb fg_colour = kRGBWhite,
Rgb bg_colour = kRGBBlack);
Image *HighlightEdges( Rgb colour, unsigned int p_colours, unsigned int p_subpixelorder, const Box *limits=0 );
// Build a colour highlight image from this GRAY8 mask. edges_only traces the
// outline of each marked region (what the blob check method shows); false
// fills every marked pixel (what the alarmed/filtered pixel methods show).
Image *BuildHighlight( Rgb colour, unsigned int p_colours, unsigned int p_subpixelorder, const Box *limits=0, bool edges_only=true );
Image *HighlightEdges( Rgb colour, unsigned int p_colours, unsigned int p_subpixelorder, const Box *limits=0 ) {
return BuildHighlight(colour, p_colours, p_subpixelorder, limits, true);
}
//Image *HighlightEdges( Rgb colour, const Polygon &polygon );
void Timestamp(const char *label, SystemTimePoint when, const Vector2 &coord, int label_size);
void Colourise(const unsigned int p_reqcolours, const unsigned int p_reqsubpixelorder);
+31 -9
View File
@@ -154,6 +154,24 @@ 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;
@@ -795,15 +813,19 @@ bool Zone::CheckAlarms(const Image *delta_image) {
// 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.
AVPixelFormat capture_fmt = zm_pixformat_from_colours(monitor->Colours(), monitor->SubpixelOrder());
if (capture_fmt == AV_PIX_FMT_GRAY8) {
image = diff_image->HighlightEdges(alarm_rgb, ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB, &polygon.Extent());
} else {
image = diff_image->HighlightEdges(alarm_rgb, monitor->Colours(), monitor->SubpixelOrder(), &polygon.Extent());
}
// Only need to delete this when 'image' becomes detached and points somewhere else
delete diff_image;
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)
+1
View File
@@ -226,6 +226,7 @@ class Zone {
int GetExtendAlarmFrames();
void SetScore(unsigned int nScore);
void SetAlarmImage(const Image* srcImage);
void BuildAlarmHighlight(Image *mask, bool edges_only);
inline const Image *getPgImage() const { return pg_image; }
inline const Range *getRanges() const { return ranges; }