diff --git a/src/zm_image.cpp b/src/zm_image.cpp index ccf37e0e0..e10ebb4b6 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -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(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; diff --git a/src/zm_zone.cpp b/src/zm_zone.cpp index ca810b75e..edace6d09 100644 --- a/src/zm_zone.cpp +++ b/src/zm_zone.cpp @@ -790,7 +790,13 @@ bool Zone::CheckAlarms(const Image *delta_image) { } } // end for y - if (monitor->Colours() == ZM_COLOUR_GRAY8) { + // 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. + 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()); diff --git a/tests/zm_image.cpp b/tests/zm_image.cpp index 4405c5e49..0585eaf2a 100644 --- a/tests/zm_image.cpp +++ b/tests/zm_image.cpp @@ -207,3 +207,79 @@ TEST_CASE("Image::Flip YUV420P", "[image]") { REQUIRE(dst.data[1][(h / 2 - 1 - 30) * dst.stride[1] + 40] == 210); } } + +// Regression: a zone alarm highlight (RGB) overlaid onto a planar YUV420P +// analysis image used to call Colourise() — which only handles GRAY8, so it +// bailed with "already colourised, colours: 1" (the GRAY8/YUV420P alias) and +// then walked the 1.5x planar buffer as 3x packed RGB (OOB write). Highlights +// are now built in the target's YUV420P format and Overlay() copies luma plus +// the shared chroma sample, keyed on a non-zero source luma marker. +TEST_CASE("Image::Overlay YUV420P highlight", "[image]") { + bootstrap_image_config(); + const int w = 64, h = 48; + + Image target(w, h, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P); + Planes tgt = plane_view(target, AV_PIX_FMT_YUV420P, w, h); + memset(tgt.data[0], 16, static_cast(tgt.stride[0]) * h); // dim luma + memset(tgt.data[1], 128, static_cast(tgt.stride[1]) * (h / 2)); // neutral chroma + memset(tgt.data[2], 128, static_cast(tgt.stride[2]) * (h / 2)); + + // Transparent (Y=0) highlight with a 2x2 luma marker fully covering chroma + // sample (5,5), plus a single-pixel marker partially covering sample (10,10). + Image high(w, h, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P); + Planes hi = plane_view(high, AV_PIX_FMT_YUV420P, w, h); + memset(hi.data[0], 0, static_cast(hi.stride[0]) * h); + memset(hi.data[1], 0, static_cast(hi.stride[1]) * (h / 2)); + memset(hi.data[2], 0, static_cast(hi.stride[2]) * (h / 2)); + for (int y : {10, 11}) for (int x : {10, 11}) hi.data[0][y * hi.stride[0] + x] = 200; + hi.data[1][5 * hi.stride[1] + 5] = 84; // red-ish chroma + hi.data[2][5 * hi.stride[2] + 5] = 255; + hi.data[0][20 * hi.stride[0] + 20] = 150; // single covering pixel of sample (10,10) + hi.data[1][10 * hi.stride[1] + 10] = 43; // green-ish chroma + hi.data[2][10 * hi.stride[2] + 10] = 21; + + target.Overlay(high); + + Planes out = plane_view(target, AV_PIX_FMT_YUV420P, w, h); + // Marked luma copied; unmarked luma untouched. + for (int y : {10, 11}) for (int x : {10, 11}) + CHECK(out.data[0][y * out.stride[0] + x] == 200); + CHECK(out.data[0][20 * out.stride[0] + 20] == 150); + CHECK(out.data[0][0] == 16); + CHECK(out.data[0][5 * out.stride[0] + 5] == 16); + // Chroma copied for any covered sample; untouched elsewhere. + CHECK(out.data[1][5 * out.stride[1] + 5] == 84); + CHECK(out.data[2][5 * out.stride[2] + 5] == 255); + CHECK(out.data[1][10 * out.stride[1] + 10] == 43); + CHECK(out.data[2][10 * out.stride[2] + 10] == 21); + CHECK(out.data[1][0] == 128); + CHECK(out.data[2][0] == 128); +} + +// HighlightEdges must be able to emit a YUV420P highlight (so it can be +// overlaid onto a YUV420P analysis image in the same format). A filled blob's +// border pixels become non-zero luma markers carrying the alarm chroma. +TEST_CASE("Image::HighlightEdges YUV420P output", "[image]") { + bootstrap_image_config(); + const int w = 64, h = 48; + + Image src(w, h, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE); + src.Clear(); + // Fill an interior rectangle so it has edges away from the image border. + for (int y = 10; y < 30; y++) + for (int x = 10; x < 40; x++) + src.Buffer()[y * src.LineSize() + x] = 255; + + Rgb red = kRGBRed; // alarm colour + Image *high = src.HighlightEdges(red, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P, nullptr); + REQUIRE(high != nullptr); + REQUIRE(high->PixFormat() == AV_PIX_FMT_YUV420P); + + Planes hi = plane_view(*high, AV_PIX_FMT_YUV420P, w, h); + // A border pixel of the blob must carry a non-zero luma marker; an interior + // pixel (not an edge) and an outside pixel must stay transparent (Y=0). + CHECK(hi.data[0][10 * hi.stride[0] + 20] != 0); // top edge + CHECK(hi.data[0][20 * hi.stride[0] + 25] == 0); // interior, not an edge + CHECK(hi.data[0][0] == 0); // outside the blob + delete high; +}