blob: a917044ae4a13a75570c2cc8989ec0ef8b58dfc1 [file] [log] [blame]
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <climits>
#include <cmath>
#include "pw_unit_test/framework.h"
#include "samples/risp4ml/common/constants.h"
#include "samples/risp4ml/common/test_utils.h"
#include "samples/risp4ml/isp_stages/downscale.h"
static constexpr uint16_t kScalePrecision = 8;
static constexpr uint16_t kScaleFixedOne = (1 << kScalePrecision);
static constexpr uint16_t kInterpolatePrecision = 8;
static constexpr uint16_t kInterpolateShift = 1;
static constexpr float kOutBitsShift = 1 << (kRawPipelineBpp - kPipeOutputBpp);
class DownscaleTest : public ::testing::Test {
protected:
void setup(uint16_t in_ch, uint16_t in_height, uint16_t in_width,
uint16_t out_ch, uint16_t out_height, uint16_t out_width) {
in_ = image_new(in_ch, in_height, in_width);
out_ = imageu8_new(out_ch, out_height, out_width);
params_.enable = true;
params_.scale_precision = kScalePrecision;
params_.interpolate_precision = kScalePrecision;
params_.interpolate_shift = kInterpolateShift;
params_.scale_fixed_one = kScaleFixedOne;
params_.scale_fraction_mask = kScaleFixedOne - 1;
params_.weight_shift = 0;
}
void TearDown() override {
image_delete(in_);
imageu8_delete(out_);
}
ImageU8* imageu8_new(uint16_t num_channels, uint16_t height, uint16_t width);
void imageu8_delete(ImageU8* image) {
if (image) {
if (image->data) free(image->data);
free(image);
}
}
pixel_type_t imageu8_pixel_val(ImageU8* image, uint16_t c, uint16_t y,
uint16_t x) {
const uint32_t stride_c = image->width * image->height;
const uint16_t stride_y = image->width;
const uint16_t stride_x = 1;
return *(image->data + c * stride_c + y * stride_y + x * stride_x);
}
float ExpectedOut(Image* in, uint16_t y, uint16_t x, float hor_scale,
float ver_scale, float hor_initial_offset,
float ver_initial_offset);
void ScaleRampImageTest(float hor_scale, float ver_scale,
float hor_initial_offset, float ver_initial_offset,
uint16_t input_width = 640,
uint16_t input_height = 480);
struct BilinearScaleTestValues {
float hor_scale;
float ver_scale;
float hor_initial_offset;
float ver_initial_offset;
uint16_t input_width;
uint16_t input_height;
};
Image* in_;
ImageU8* out_;
DownscaleParams params_;
};
ImageU8* DownscaleTest::imageu8_new(uint16_t num_channels, uint16_t height,
uint16_t width) {
ImageU8* image = (ImageU8*)malloc(sizeof(ImageU8));
if (image) {
image->num_channels = num_channels;
image->height = height;
image->width = width;
uint32_t num_pixels = width * height * num_channels;
image->data = (uint8_t*)malloc(num_pixels * sizeof(uint8_t));
}
return image;
}
float DownscaleTest::ExpectedOut(Image* in, uint16_t y, uint16_t x,
float hor_scale, float ver_scale,
float hor_initial_offset,
float ver_initial_offset) {
float input_x = x * hor_scale + hor_initial_offset;
float input_y = y * ver_scale + ver_initial_offset;
uint16_t int_input_x_pre = static_cast<uint16_t>(floorf(input_x));
uint16_t int_input_x_post = int_input_x_pre + 1;
uint16_t int_input_y_pre = static_cast<uint16_t>(floorf(input_y));
uint16_t int_input_y_post = int_input_y_pre + 1;
float p0 = (input_y - int_input_y_pre) *
image_pixel_val(in, 0, int_input_y_post, int_input_x_pre) +
(int_input_y_post - input_y) *
image_pixel_val(in, 0, int_input_y_pre, int_input_x_pre);
float p1 = (input_y - int_input_y_pre) *
image_pixel_val(in, 0, int_input_y_post, int_input_x_post) +
(int_input_y_post - input_y) *
image_pixel_val(in, 0, int_input_y_pre, int_input_x_post);
float expected_out =
(input_x - int_input_x_pre) * p1 + (int_input_x_post - input_x) * p0;
// Downscaler out_ is kPipeOutputBpp bits wide.
// Shift right by kRawPipelineBpp - kPipeOutputBpp and round.
expected_out = floorf(expected_out / kOutBitsShift);
return expected_out;
}
// Helper function for 2D ramp tests. image is downscaled successfully.
void DownscaleTest::ScaleRampImageTest(float hor_scale, float ver_scale,
float hor_initial_offset,
float ver_initial_offset,
uint16_t input_width,
uint16_t input_height) {
constexpr int kTolerance = 1; // Tolerance for rounding error.
uint16_t output_width =
static_cast<uint16_t>((input_width - hor_initial_offset) / hor_scale);
uint16_t output_height =
static_cast<uint16_t>((input_height - ver_initial_offset) / ver_scale);
setup(1, input_height, input_width, 1, output_height, output_width);
// Fill in_ images as 2D ramp whose values are increased from the
// top-left corner to the bottom-right corner.
for (uint16_t y = 0; y < input_height; ++y) {
for (uint16_t x = 0; x < input_width; ++x) {
*image_pixel(in_, 0, y, x) = (y * input_width + x) % (1024);
}
}
params_.ver_initial_offset = ver_initial_offset * kScaleFixedOne;
params_.hor_initial_offset = hor_initial_offset * kScaleFixedOne;
params_.hor_scale_factor = hor_scale * kScaleFixedOne;
params_.ver_scale_factor = ver_scale * kScaleFixedOne;
set_downscale_param(&params_);
downscale_process(in_, out_);
for (uint16_t y = 0; y < output_height; ++y) {
for (uint16_t x = 0; x < output_width; ++x) {
float expected_out = ExpectedOut(in_, y, x, hor_scale, ver_scale,
hor_initial_offset, ver_initial_offset);
float diff =
std::abs((float)imageu8_pixel_val(out_, 0, y, x) - expected_out);
ASSERT_LE(diff, kTolerance);
}
}
}
TEST_F(DownscaleTest, NoScaleTest) {
constexpr uint16_t kOutputWidth = 128;
constexpr uint16_t kInputHeight = 96;
setup(1, kInputHeight, kOutputWidth, 1, kInputHeight, kOutputWidth);
// Generate random image.
InitImageRandom(in_, 0, USHRT_MAX);
params_.ver_initial_offset = 0;
params_.hor_initial_offset = 0;
params_.hor_scale_factor = 1 << kScalePrecision;
params_.ver_scale_factor = 1 << kScalePrecision;
set_downscale_param(&params_);
downscale_process(in_, out_);
// Verify the out_ image is identical to the in_ image.
for (uint16_t y = 0; y < kInputHeight; ++y) {
for (uint16_t x = 0; x < kOutputWidth; ++x) {
ASSERT_EQ(imageu8_pixel_val(out_, 0, y, x),
static_cast<pixel_type_t>(
floorf(image_pixel_val(in_, 0, y, x) >>
(kRawPipelineBpp - kPipeOutputBpp))));
}
}
}
TEST_F(DownscaleTest, VGAZeroOffsetTest) {
std::vector<BilinearScaleTestValues> tests = {
{2.f, 2.f, 0, 0},
{4.f, 4.f, 320, 240}, // Offset from center
{2.f, 4.f, 200, 100},
{1.5f, 3.4f, 17, 55}};
for (const auto& test : tests) {
ScaleRampImageTest(test.hor_scale, test.ver_scale, test.hor_initial_offset,
test.ver_initial_offset);
}
}
// TODO(alexkaplan): parametrize this test for different in_ size and scale
// parameters.
TEST_F(DownscaleTest, DownscaleTest) {
std::vector<BilinearScaleTestValues> tests = {
{7.3f, 5.1f, 5, 2, 64, 64}, // Test odd values
{4, 4, 320, 240, 640, 480}, // Offset from center
{2, 4, 200, 100, 640, 480},
{1.5f, 3.4f, 17, 55, 200, 100}};
for (const auto& test : tests) {
ScaleRampImageTest(test.hor_scale, test.ver_scale, test.hor_initial_offset,
test.ver_initial_offset, test.input_width,
test.input_height);
}
}
TEST_F(DownscaleTest, Trivial3DTest) {
constexpr uint16_t kChannels = 3;
constexpr uint16_t kInputHeight = 4;
constexpr uint16_t kInputWidth = 4;
constexpr uint16_t kVerScale = 2;
constexpr uint16_t kHorScale = 2;
constexpr uint16_t kOutputHeight = kInputHeight / kVerScale;
constexpr uint16_t kOutputWidth = kInputWidth / kHorScale;
setup(kChannels, kInputHeight, kInputWidth, kChannels, kOutputHeight,
kOutputWidth);
for (uint16_t c = 0; c < kChannels; ++c) {
for (uint16_t y = 0; y < kInputHeight; ++y) {
for (uint16_t x = 0; x < kInputWidth; ++x) {
*image_pixel(in_, c, y, x) = ((y * kInputWidth + x) * 10 + c) << 8;
}
}
}
params_.ver_initial_offset = 0;
params_.hor_initial_offset = 0;
params_.ver_scale_factor = kVerScale * kScaleFixedOne;
params_.hor_scale_factor = kHorScale * kScaleFixedOne;
set_downscale_param(&params_);
downscale_process(in_, out_);
// out_ is in interleaved order, so strides are different than defined in
// g_image.h
const uint16_t kInterleavedChStride = 1;
const uint16_t kInterleavedXStride = kChannels;
const uint32_t kInterleavedYStride = kChannels * kOutputWidth;
// for exact integer ratios out_ is just downsampled in_
for (uint16_t c = 0; c < kChannels; ++c) {
for (uint16_t y = 0; y < kOutputHeight; ++y) {
for (uint16_t x = 0; x < kOutputWidth; ++x) {
ASSERT_EQ(out_->data[y * kInterleavedYStride + x * kInterleavedXStride +
c * kInterleavedChStride],
image_pixel_val(in_, c, y * kVerScale, x * kHorScale) >> 8);
}
}
}
}