| // 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(¶ms_); |
| |
| 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(¶ms_); |
| |
| 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(¶ms_); |
| |
| 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); |
| } |
| } |
| } |
| } |