mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-11-05 04:02:56 -05:00
Monitor::Decode scales the Y-channel image when the camera's resolution differs from the monitor's. That image is get_y_image()'s zero-copy wrap of the decoder's Y plane, whose stride is the decoder's linesize[0], not the FFALIGN(width,32) that Scale assumed when it handed the buffer to SWScale::Convert at align 32. Each row was read from the wrong offset, so the scaled analysis image, and the alarm pixels drawn from it, came out sheared. Set up the source and destination frames in Scale, override plane 0's stride with the Image's linesize as Rotate and Flip already do, and convert through SWScale::Convert(AVFrame*, AVFrame*). The output layout is unchanged (align 32). tests/zm_image_linesize.cpp: scale a 40 wide GRAY8 plane with a 48 byte stride and check that every output row is still flat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
176 lines
6.6 KiB
C++
176 lines
6.6 KiB
C++
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "zm_catch2.h"
|
|
|
|
#include "zm_config.h"
|
|
#include "zm_image.h"
|
|
#include "zm_rgb.h"
|
|
|
|
#include <cstdint>
|
|
#include <vector>
|
|
|
|
// get_y_image() wraps a decoder Y plane whose real stride (linesize[0]) can be
|
|
// smaller than FFALIGN(width,32). Image::Rotate/Flip must use the source
|
|
// Image's own linesize, not a re-derived 32-aligned stride, or they read past
|
|
// the end of the borrowed plane (crash) and skew the output. Width 15 is
|
|
// deliberately not a multiple of 32 (FFALIGN(15,32)=32) and the source is
|
|
// packed tight (linesize == width), exactly like the decoder's Y plane.
|
|
// width*height=150 keeps every pixel value distinct in a byte so any stride
|
|
// drift is detectable.
|
|
namespace {
|
|
constexpr unsigned int kW = 15;
|
|
constexpr unsigned int kH = 10;
|
|
|
|
// Image::Initialise() dereferences config.font_file_location, which is null in
|
|
// the unit-test harness (no zm.conf loaded). Give it a non-null value so the
|
|
// first Image construction doesn't throw before exercising the rotate/flip code.
|
|
void EnsureImageInit() {
|
|
if (!config.font_file_location) config.font_file_location = "";
|
|
}
|
|
|
|
// Backing allocation larger than the tight 15x10 plane so that a regression
|
|
// (an over-read using stride FFALIGN(15,32)=32) stays inside our allocation and
|
|
// corrupts the result rather than segfaulting the test runner.
|
|
std::vector<uint8_t> MakeTightGray8Plane() {
|
|
std::vector<uint8_t> backing(32 * kH + 64, 0);
|
|
for (unsigned int y = 0; y < kH; y++)
|
|
for (unsigned int x = 0; x < kW; x++)
|
|
backing[y * kW + x] = static_cast<uint8_t>(y * kW + x);
|
|
return backing;
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("Image::Rotate 180 honors a non-32-aligned source linesize", "[Image]") {
|
|
EnsureImageInit();
|
|
std::vector<uint8_t> backing = MakeTightGray8Plane();
|
|
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
|
|
|
|
img.Rotate(180);
|
|
|
|
REQUIRE(img.Width() == kW);
|
|
REQUIRE(img.Height() == kH);
|
|
for (unsigned int y = 0; y < kH; y++) {
|
|
for (unsigned int x = 0; x < kW; x++) {
|
|
const uint8_t expected = static_cast<uint8_t>((kH - 1 - y) * kW + (kW - 1 - x));
|
|
REQUIRE(*img.Buffer(x, y) == expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Image::Flip horizontal honors a non-32-aligned source linesize", "[Image]") {
|
|
EnsureImageInit();
|
|
std::vector<uint8_t> backing = MakeTightGray8Plane();
|
|
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
|
|
|
|
img.Flip(true);
|
|
|
|
REQUIRE(img.Width() == kW);
|
|
REQUIRE(img.Height() == kH);
|
|
for (unsigned int y = 0; y < kH; y++) {
|
|
for (unsigned int x = 0; x < kW; x++) {
|
|
const uint8_t expected = static_cast<uint8_t>(y * kW + (kW - 1 - x));
|
|
REQUIRE(*img.Buffer(x, y) == expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Image::Flip vertical honors a non-32-aligned source linesize", "[Image]") {
|
|
EnsureImageInit();
|
|
std::vector<uint8_t> backing = MakeTightGray8Plane();
|
|
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
|
|
|
|
img.Flip(false);
|
|
|
|
REQUIRE(img.Width() == kW);
|
|
REQUIRE(img.Height() == kH);
|
|
for (unsigned int y = 0; y < kH; y++) {
|
|
for (unsigned int x = 0; x < kW; x++) {
|
|
const uint8_t expected = static_cast<uint8_t>((kH - 1 - y) * kW + x);
|
|
REQUIRE(*img.Buffer(x, y) == expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 90/270 also swap dimensions (dst is kH wide x kW tall), exercising both the
|
|
// source-stride fix and the destination dimension/stride handling.
|
|
TEST_CASE("Image::Rotate 90 honors a non-32-aligned source linesize", "[Image]") {
|
|
EnsureImageInit();
|
|
std::vector<uint8_t> backing = MakeTightGray8Plane();
|
|
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
|
|
|
|
img.Rotate(90);
|
|
|
|
REQUIRE(img.Width() == kH);
|
|
REQUIRE(img.Height() == kW);
|
|
// 90deg: dst(X,Y) == src(x=Y, y=kH-1-X)
|
|
for (unsigned int Y = 0; Y < kW; Y++) {
|
|
for (unsigned int X = 0; X < kH; X++) {
|
|
const uint8_t expected = static_cast<uint8_t>((kH - 1 - X) * kW + Y);
|
|
REQUIRE(*img.Buffer(X, Y) == expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Image::Rotate 270 honors a non-32-aligned source linesize", "[Image]") {
|
|
EnsureImageInit();
|
|
std::vector<uint8_t> backing = MakeTightGray8Plane();
|
|
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
|
|
|
|
img.Rotate(270);
|
|
|
|
REQUIRE(img.Width() == kH);
|
|
REQUIRE(img.Height() == kW);
|
|
// 270deg: dst(X,Y) == src(x=kW-1-Y, y=X)
|
|
for (unsigned int Y = 0; Y < kW; Y++) {
|
|
for (unsigned int X = 0; X < kH; X++) {
|
|
const uint8_t expected = static_cast<uint8_t>(X * kW + (kW - 1 - Y));
|
|
REQUIRE(*img.Buffer(X, Y) == expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
// When the camera's resolution differs from the monitor's, Monitor::Decode
|
|
// scales the Y-channel image get_y_image() made. That image borrows the
|
|
// decoder's Y plane, whose stride is neither the tight width nor
|
|
// FFALIGN(width,32), so Scale must read rows at the Image's own linesize.
|
|
// Each source row is one flat value and the row padding is 255: read at the
|
|
// wrong stride, output rows pick up neighbouring rows and padding and stop
|
|
// being flat.
|
|
TEST_CASE("Image::Scale honors a borrowed source linesize", "[Image]") {
|
|
EnsureImageInit();
|
|
constexpr unsigned int kSrcW = 40; // FFALIGN(40,32) = 64
|
|
constexpr unsigned int kStride = 48;
|
|
constexpr unsigned int kSrcH = 8;
|
|
std::vector<uint8_t> backing(kStride * kSrcH, 255);
|
|
for (unsigned int y = 0; y < kSrcH; y++)
|
|
for (unsigned int x = 0; x < kSrcW; x++)
|
|
backing[y * kStride + x] = static_cast<uint8_t>(20 * (y + 1));
|
|
Image img(kSrcW, kStride, kSrcH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
|
|
|
|
img.Scale(kSrcW / 2, kSrcH / 2);
|
|
|
|
REQUIRE(img.Width() == kSrcW / 2);
|
|
REQUIRE(img.Height() == kSrcH / 2);
|
|
for (unsigned int y = 0; y < kSrcH / 2; y++) {
|
|
const uint8_t first = *img.Buffer(0, y);
|
|
REQUIRE(first < 200);
|
|
for (unsigned int x = 1; x < kSrcW / 2; x++)
|
|
REQUIRE(*img.Buffer(x, y) == first);
|
|
}
|
|
}
|