/*
* This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
*
* 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, see .
*/
#include "zm_catch2.h"
#include "zm_config.h"
#include "zm_image.h"
#include "zm_monitor.h"
#include "zm_rgb.h"
#include "zm_zone.h"
#include
#include
#include
#include
// The alarmed/filtered pixel check methods used to hand Overlay() the raw GRAY8
// scoring mask. Overlay keys on a non-zero source pixel and copies it, so the
// highlight took whatever shape that byte had in the destination format: the
// red channel on an RGB32 monitor (which is why alarms were "always red"), all
// three channels on RGB24 (white), luma alone on YUV420. The zone's configured
// Alarm Colour was only ever honoured on the blob path, via HighlightEdges.
//
// The highlight must now carry the zone's alarm colour on every check method
// and colour depth, filled for the pixel methods and outlined for blobs.
namespace {
void EnsureConfig() {
if (!config.font_file_location) config.font_file_location = "";
if (!config.event_close_mode) config.event_close_mode = "idle";
}
class TestMonitor : public Monitor {
public:
TestMonitor(unsigned int w, unsigned int h, unsigned int c) : Monitor() {
width = w;
height = h;
colours = c;
// >1 is what gates building the highlight at all.
savejpegs = 2;
}
};
std::shared_ptr MakeMonitor(unsigned int w, unsigned int h, unsigned int c) {
return std::static_pointer_cast(std::make_shared(w, h, c));
}
Polygon FullFramePolygon(unsigned int w, unsigned int h) {
std::vector vertices = {Vector2(0, 0), Vector2(w - 1, 0),
Vector2(w - 1, h - 1), Vector2(0, h - 1)};
return Polygon(vertices);
}
std::unique_ptr MakeSaturatedDelta(unsigned int w, unsigned int h) {
auto img = std::unique_ptr(
new Image(w, h, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE));
for (unsigned int y = 0; y < h; y++)
for (unsigned int x = 0; x < w; x++) *img->Buffer(x, y) = 255;
return img;
}
// A colour no channel-smearing bug can produce by accident: it is not white,
// not pure red, and its three channels are all different.
constexpr Rgb kAlarmColour = 0x20c0ff;
constexpr uint8_t kR = 0x20, kG = 0xc0, kB = 0xff;
// Monitor::SubpixelOrder() reads through the camera, which a bare test Monitor
// does not have, so which byte holds which channel is not pinned down here.
// Checking the three bytes as a set still fails for every way the old code got
// this wrong: white is {ff,ff,ff}, red-channel-only is {xx,00,00}.
bool ChannelsAre(const uint8_t *px, uint8_t a, uint8_t b, uint8_t c) {
std::vector got = {px[0], px[1], px[2]};
std::vector want = {a, b, c};
std::sort(got.begin(), got.end());
std::sort(want.begin(), want.end());
return got == want;
}
} // namespace
TEST_CASE("Zone alarm highlight carries the zone's alarm colour", "[Zone]") {
EnsureConfig();
// 720 is deliberately not 32-byte aligned in either depth, so this also
// exercises the padded-stride path the striping fix covers.
const unsigned int w = 720, h = 480;
SECTION("RGB24 monitor: filled with the alarm colour, not white") {
auto monitor = MakeMonitor(w, h, ZM_COLOUR_RGB24);
Zone zone(monitor, 1, "full", Zone::ACTIVE, FullFramePolygon(w, h),
kAlarmColour, Zone::ALARMED_PIXELS,
/*min_pixel_threshold*/ 10, /*max_pixel_threshold*/ 0,
/*min_alarm_pixels*/ 1, /*max_alarm_pixels*/ static_cast(w * h));
auto delta = MakeSaturatedDelta(w, h);
REQUIRE(zone.CheckAlarms(delta.get()));
const Image *alarm = zone.AlarmImage();
REQUIRE(alarm != nullptr);
REQUIRE(alarm->Colours() == ZM_COLOUR_RGB24);
const uint8_t *px = alarm->Buffer(w / 2, h / 2);
CHECK(ChannelsAre(px, kR, kG, kB));
// The old behaviour wrote the mask byte into all three channels.
CHECK_FALSE((px[0] == 0xff && px[1] == 0xff && px[2] == 0xff));
}
SECTION("RGB32 monitor: alarm colour, not just the red channel") {
auto monitor = MakeMonitor(w, h, ZM_COLOUR_RGB32);
Zone zone(monitor, 1, "full", Zone::ACTIVE, FullFramePolygon(w, h),
kAlarmColour, Zone::ALARMED_PIXELS,
/*min_pixel_threshold*/ 10, /*max_pixel_threshold*/ 0,
/*min_alarm_pixels*/ 1, /*max_alarm_pixels*/ static_cast(w * h));
auto delta = MakeSaturatedDelta(w, h);
REQUIRE(zone.CheckAlarms(delta.get()));
const Image *alarm = zone.AlarmImage();
REQUIRE(alarm != nullptr);
REQUIRE(alarm->Colours() == ZM_COLOUR_RGB32);
const uint8_t *px = alarm->Buffer(w / 2, h / 2);
CHECK(ChannelsAre(px, kR, kG, kB));
// The old behaviour left two of the three channels at zero.
CHECK_FALSE((px[1] == 0 && px[2] == 0));
}
SECTION("the highlight is filled, not just outlined, for alarmed pixels") {
auto monitor = MakeMonitor(w, h, ZM_COLOUR_RGB24);
Zone zone(monitor, 1, "full", Zone::ACTIVE, FullFramePolygon(w, h),
kAlarmColour, Zone::ALARMED_PIXELS,
/*min_pixel_threshold*/ 10, /*max_pixel_threshold*/ 0,
/*min_alarm_pixels*/ 1, /*max_alarm_pixels*/ static_cast(w * h));
auto delta = MakeSaturatedDelta(w, h);
REQUIRE(zone.CheckAlarms(delta.get()));
const Image *alarm = zone.AlarmImage();
REQUIRE(alarm != nullptr);
// Every pixel alarmed, so every pixel must be painted -- an outline would
// leave the interior black.
unsigned int painted = 0;
for (unsigned int y = 0; y < h; y++) {
const uint8_t *row = alarm->Buffer(0, y);
for (unsigned int x = 0; x < w; x++)
if (row[x * 3] || row[x * 3 + 1] || row[x * 3 + 2]) painted++;
}
CHECK(painted == w * h);
}
}
// The highlight is built from a GRAY8 mask into a wider destination whose row
// stride is padded independently of the source's. Indexing the destination
// with the source's stride sheared the highlight diagonally across the frame
// and ran past the end of the destination on the last rows -- the same
// width-vs-linesize family as the striping fixes, but inside the highlight
// builder, where it stayed latent until the pixel check methods started using
// it. Run the same assertion at a padded width and an aligned one so a failure
// says whether the stride is to blame.
TEST_CASE("Image::BuildHighlight lands the highlight where the mask marked it", "[Image]") {
EnsureConfig();
auto check = [](unsigned int w, unsigned int h) {
Image mask(w, w, h, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE);
memset(mask.Buffer(), 0, static_cast(w) * h);
// One solid rectangle, well inside the frame.
const unsigned int r_lo_x = 100, r_hi_x = 199, r_lo_y = 50, r_hi_y = 149;
for (unsigned int y = r_lo_y; y <= r_hi_y; y++)
memset(mask.Buffer() + static_cast(y) * w + r_lo_x, 0xff,
r_hi_x - r_lo_x + 1);
std::unique_ptr high(mask.BuildHighlight(
kAlarmColour, ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB, nullptr, false));
REQUIRE(high != nullptr);
unsigned int painted = 0, first_y = h, last_y = 0, first_x = w, last_x = 0;
for (unsigned int y = 0; y < h; y++) {
const uint8_t *row = high->Buffer(0, y);
for (unsigned int x = 0; x < w; x++) {
if (row[x * 3] || row[x * 3 + 1] || row[x * 3 + 2]) {
painted++;
first_y = std::min(first_y, y);
last_y = std::max(last_y, y);
first_x = std::min(first_x, x);
last_x = std::max(last_x, x);
}
}
}
INFO("width " << w << " mask linesize " << mask.LineSize()
<< " highlight linesize " << high->LineSize());
CHECK(painted == (r_hi_x - r_lo_x + 1) * (r_hi_y - r_lo_y + 1));
CHECK(first_y == r_lo_y);
CHECK(last_y == r_hi_y);
CHECK(first_x == r_lo_x);
CHECK(last_x == r_hi_x);
};
SECTION("720 wide, GRAY8 row padded to 736") { check(720, 480); }
SECTION("640 wide, GRAY8 row already aligned") { check(640, 480); }
}