fix: build zone alarm highlights in YUV420P to stop spurious Colourise warning

The "Target image is already colourised, colours: 1" warning fired from
Image::Colourise() during zone alarm overlay on YUV420P monitors, even with
ZM_COLOUR_JPEG_FILES off.

monitor->Colours() returns 1 for both GRAY8 and planar YUV420P (the GRAY8
alias), so zm_zone.cpp misclassified YUV420P monitors as grayscale and built
an RGB24 alarm highlight. Overlaying that RGB24 highlight onto the YUV420P
analysis_image hit the RGB-on-mono branch, which calls Colourise() - valid
only for GRAY8. Colourise() warned and bailed, after which the per-pixel loop
walked the 1.5x planar buffer as 3x packed RGB (out-of-bounds write).

Resolve the real capture format via zm_pixformat_from_colours() so true GRAY8
still upgrades to an RGB24 highlight while YUV420P keeps its format. Add a
YUV420P output branch to HighlightEdges (single alarm colour written to luma
and the shared chroma sample, transparent elsewhere) and a YUV420P-on-YUV420P
branch to Overlay (copies luma where non-zero, copies each chroma sample when
any covered luma pixel is marked). Colourise() is now only reached for GRAY8.

Add Catch2 coverage for the YUV420P Overlay masking and HighlightEdges output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Isaac ConnorandClaude Opus 4.8 committed 2026-06-26 16:10:07 -04:00
1 parent d4020e6394
commit cf92249820
3 files changed
+155 -3

No files matched your search

+72 -2
View File
@@ -1248,6 +1248,33 @@ 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.
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);
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;
if ( *p ) {
edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1))
|| (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize));
}
if ( edge ) {
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;
}
}
}
}
return high_image;
@@ -1998,8 +2025,51 @@ void Image::Overlay( const Image &image ) {
// chroma. After Colourise() the destination's linesize is updated to the
// new format's stride, so we re-read it inside each branch.
/* Grayscale/YUV420 on top of grayscale/YUV420 - complete */
if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) {
/* YUV420 on top of YUV420 - copy luma + chroma using luma as the mask */
if ( zm_is_yuv420(imagePixFormat) && zm_is_yuv420(image.imagePixFormat) ) {
// The overlay (a zone alarm highlight) is built in the target's format
// with a Clear()ed (Y=0) transparent background, so a non-zero source
// luma marks a pixel to paint. Copy that luma, and copy the shared
// chroma sample whenever any of the luma pixels it covers is marked.
uint8_t *dplane[4] = {};
int dstride[4] = {};
const uint8_t *splane[4] = {};
int sstride[4] = {};
if (av_image_fill_arrays(dplane, dstride, buffer, imagePixFormat, width, height, 32) < 0
|| av_image_fill_arrays(const_cast<uint8_t **>(splane), sstride, image.buffer,
image.imagePixFormat, width, height, 32) < 0) {
Error("Overlay: av_image_fill_arrays failed for YUV420 %ux%u", width, height);
return;
}
for (unsigned int y = 0; y < height; y++) {
const uint8_t *psrc = splane[0] + y * sstride[0];
uint8_t *pdest = dplane[0] + y * dstride[0];
for (unsigned int x = 0; x < width; x++) {
if (psrc[x]) pdest[x] = psrc[x];
}
}
const unsigned int cw = (width + 1) / 2;
const unsigned int ch = (height + 1) / 2;
for (unsigned int cy = 0; cy < ch; cy++) {
for (unsigned int cx = 0; cx < cw; cx++) {
bool marked = false;
for (unsigned int dy = 0; dy < 2 && !marked; dy++) {
const unsigned int ly = cy * 2 + dy;
if (ly >= height) break;
for (unsigned int dx = 0; dx < 2; dx++) {
const unsigned int lx = cx * 2 + dx;
if (lx < width && splane[0][ly * sstride[0] + lx]) { marked = true; break; }
}
}
if (marked) {
dplane[1][cy * dstride[1] + cx] = splane[1][cy * sstride[1] + cx];
dplane[2][cy * dstride[2] + cx] = splane[2][cy * sstride[2] + cx];
}
}
}
/* Grayscale/YUV420 on top of grayscale/YUV420 - complete */
} else if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) {
// Overlay only the luma/primary plane. Width is shared (panic above).
for (unsigned int y = 0; y < height; y++) {
const uint8_t *psrc = image.buffer + y * image.linesize;