From 1f380a6ec6f400088a2dca919c5dfbc97639fb77 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Mon, 13 Aug 2018 10:39:57 -0700 Subject: [PATCH] Introduce new image library, Phase I. (#80) * Overhaul the image library, Phase I. The preps for our upcoming mipgen command line tool by adding filtering capabilities and unit tests for a new LinearImage class, which will subsume Image (deprecated). Filament's new image library is composed of three components: - LinearImage....simple 3D tensor of floats - ImageOps.......free functions for simple transforms - ImageSampler...high quality filtering The old Image class was a simple untyped data blob and is now deprecated. The new LinearImage class is always float32, and always stores pixels in packed scanline order. This makes it easier to implement image-based algorithms because they can be agnostic of the underlying format. Remaining refactorings: - Phase II will migrate imageio to LinearImage - Phase III will migrate all downstream tools to LinearImage - Phase IV will remove old the Image class * Image library code review feedback. --- README.md | 3 +- build/common/test_list.txt | 1 + libs/image/CMakeLists.txt | 13 +- libs/image/include/image/ColorTransform.h | 70 ++++ libs/image/include/image/Image.h | 5 + libs/image/include/image/ImageOps.h | 57 +++ libs/image/include/image/ImageSampler.h | 134 ++++++ libs/image/include/image/LinearImage.h | 105 +++++ libs/image/src/Image.cpp | 2 +- libs/image/src/ImageOps.cpp | 213 ++++++++++ libs/image/src/ImageSampler.cpp | 323 ++++++++++++++ libs/image/src/LinearImage.cpp | 56 +++ libs/image/tests/reference/blurred.png | Bin 0 -> 55656 bytes libs/image/tests/reference/colors.png | Bin 0 -> 27844 bytes libs/image/tests/reference/depths.png | Bin 0 -> 1934 bytes libs/image/tests/reference/grays.png | Bin 0 -> 3739 bytes libs/image/tests/reference/imageops.png | Bin 0 -> 1059 bytes libs/image/tests/reference/normals.png | Bin 0 -> 2490 bytes libs/image/tests/test_image.cpp | 489 ++++++++++++++++++++++ 19 files changed, 1468 insertions(+), 3 deletions(-) create mode 100644 libs/image/include/image/ImageOps.h create mode 100644 libs/image/include/image/ImageSampler.h create mode 100644 libs/image/include/image/LinearImage.h create mode 100644 libs/image/src/ImageOps.cpp create mode 100644 libs/image/src/ImageSampler.cpp create mode 100644 libs/image/src/LinearImage.cpp create mode 100644 libs/image/tests/reference/blurred.png create mode 100644 libs/image/tests/reference/colors.png create mode 100644 libs/image/tests/reference/depths.png create mode 100644 libs/image/tests/reference/grays.png create mode 100644 libs/image/tests/reference/imageops.png create mode 100644 libs/image/tests/reference/normals.png create mode 100644 libs/image/tests/test_image.cpp diff --git a/README.md b/README.md index 8e49b443c4..2412455b48 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,8 @@ Many other features have been either prototyped or planned: - `filaflat`: Serialization/deserialization library used for materials - `filagui`: Helper library for [Dear ImGui](https://github.com/ocornut/imgui) - `filamat`: Material generation library - - `image`: Image library, only intended for internal use + - `image`: Image filtering and simple transforms + - `imageio`: Image file reading / writing, only intended for internal use - `math`: Math library - `utils`: Utility library (threads, memory, data structures, etc.) - `samples`: Sample desktop applications diff --git a/build/common/test_list.txt b/build/common/test_list.txt index 0fd9218b53..302e1cfea9 100644 --- a/build/common/test_list.txt +++ b/build/common/test_list.txt @@ -1,5 +1,6 @@ filament/test/test_filament --gtest_filter=-FilamentTest.FroxelData filament/test/test_filament_exposure --gtest_filter=-FilamentExposureWithEngineTest.SetExposure:FilamentExposureWithEngineTest.ComputeEV100 libs/math/test_math +libs/image/test_image libs/utils/test_utils tools/matc/test_matc \ No newline at end of file diff --git a/libs/image/CMakeLists.txt b/libs/image/CMakeLists.txt index 3d262fb5ba..c04efd027a 100644 --- a/libs/image/CMakeLists.txt +++ b/libs/image/CMakeLists.txt @@ -14,6 +14,9 @@ set(PUBLIC_HDRS set(SRCS src/Image.cpp + src/LinearImage.cpp + src/ImageSampler.cpp + src/ImageOps.cpp ) # ================================================================================================== @@ -23,7 +26,7 @@ include_directories(${PUBLIC_HDR_DIR}) add_library(${TARGET} STATIC ${PUBLIC_HDRS} ${SRCS}) -target_link_libraries(${TARGET} PUBLIC math) +target_link_libraries(${TARGET} PUBLIC math utils) target_include_directories(${TARGET} PUBLIC ${PUBLIC_HDR_DIR}) @@ -40,3 +43,11 @@ target_compile_options(${TARGET} PRIVATE # ================================================================================================== add_library(image_headers INTERFACE) target_include_directories(image_headers INTERFACE ${PUBLIC_HDR_DIR}) + +# ================================================================================================== +# Tests +# ================================================================================================== +if (NOT ANDROID) + add_executable(test_${TARGET} tests/test_image.cpp) + target_link_libraries(test_${TARGET} PRIVATE image imageio gtest) +endif() diff --git a/libs/image/include/image/ColorTransform.h b/libs/image/include/image/ColorTransform.h index d5febf0565..33b9672d46 100644 --- a/libs/image/include/image/ColorTransform.h +++ b/libs/image/include/image/ColorTransform.h @@ -18,6 +18,7 @@ #define IMAGE_COLORSPACE_H_ #include +#include #include #include @@ -198,6 +199,75 @@ std::unique_ptr fromLinearToRGBM(const Image& image) { return dst; } +// Creates a 3-channel sRGB u8 image from a linear f32 image. +// The source image can have three or more channels, but only the first three are honored. +template +std::unique_ptr fromLinearTosRGB(const LinearImage& image) { + using math::float3; + size_t w = image.getWidth(); + size_t h = image.getHeight(); + size_t channels = image.getChannels(); + assert(channels >= 3); + std::unique_ptr dst(new uint8_t[w * h * 3 * sizeof(T)]); + T* d = reinterpret_cast(dst.get()); + for (size_t y = 0; y < h; ++y) { + for (size_t x = 0; x < w; ++x, d += 3) { + float3 const* src = reinterpret_cast(image.getPixelRef(x, y)); + float3 l(linearTosRGB(saturate(*src)) * std::numeric_limits::max()); + for (size_t i = 0; i < 3; i++) { + d[i] = T(l[i]); + } + } + } + return dst; +} + +// Creates a 3-channel RGB u8 image from a f32 image. +// The source image can have three or more channels, but only the first three are honored. +template +std::unique_ptr fromLinearToRGB(const LinearImage& image) { + using math::float3; + size_t w = image.getWidth(); + size_t h = image.getHeight(); + size_t channels = image.getChannels(); + assert(channels >= 3); + std::unique_ptr dst(new uint8_t[w * h * 3 * sizeof(T)]); + T* d = reinterpret_cast(dst.get()); + for (size_t y = 0; y < h; ++y) { + for (size_t x = 0; x < w; ++x, d += 3) { + float3 const* src = reinterpret_cast(image.getPixelRef(x, y)); + float3 l(saturate(*src) * std::numeric_limits::max()); + for (size_t i = 0; i < 3; i++) { + d[i] = T(l[i]); + } + } + } + return dst; +} + +// Creates a 4-channel RGBM u8 image from a f32 image. +// The source image can have three or more channels, but only the first three are honored. +template +std::unique_ptr fromLinearToRGBM(const LinearImage& image) { + using namespace math; + size_t w = image.getWidth(); + size_t h = image.getHeight(); + size_t channels = image.getChannels(); + assert(channels >= 3); + std::unique_ptr dst(new uint8_t[w * h * 4 * sizeof(T)]); + T* d = reinterpret_cast(dst.get()); + for (size_t y = 0; y < h; ++y) { + for (size_t x = 0; x < w; ++x, d += 4) { + float3 const* src = reinterpret_cast(image.getPixelRef(x, y)); + float4 l(linearToRGBM(*src) * std::numeric_limits::max()); + for (size_t i = 0; i < 4; i++) { + d[i] = T(l[i]); + } + } + } + return dst; +} + template static Image toLinear(size_t w, size_t h, size_t bpr, const std::unique_ptr& src, PROCESS proc, TRANSFORM transform) { diff --git a/libs/image/include/image/Image.h b/libs/image/include/image/Image.h index d6609dc5c3..2c6b459fe6 100644 --- a/libs/image/include/image/Image.h +++ b/libs/image/include/image/Image.h @@ -26,6 +26,11 @@ */ namespace image { +/** + * \deprecated + * We are phasing out this class in favor of LinearImage. The latter has a stable and well-defined + * pixel format, making it easier to implement image-based algorithms. + */ class Image { public: Image(); diff --git a/libs/image/include/image/ImageOps.h b/libs/image/include/image/ImageOps.h new file mode 100644 index 0000000000..2684e08cd0 --- /dev/null +++ b/libs/image/include/image/ImageOps.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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. + */ + +#ifndef IMAGE_IMAGEOPS_H +#define IMAGE_IMAGEOPS_H + +#include + +#include + +namespace image { + +// Concatenates images horizontally to create a filmstrip atlas, similar to numpy's hstack. +LinearImage horizontalStack(std::initializer_list images); +LinearImage horizontalStack(LinearImage const* img, size_t count); + +// Concatenates images vertically to create a filmstrip atlas, similar to numpy's vstack. +LinearImage verticalStack(std::initializer_list images); +LinearImage verticalStack(LinearImage const* img, size_t count); + +// Horizontally or vertically mirror the given image. +LinearImage horizontalFlip(const LinearImage& image); +LinearImage verticalFlip(const LinearImage& image); + +// Transforms normals (components live in [-1,+1]) into colors (components live in [0,+1]). +LinearImage vectorsToColors(const LinearImage& image); + +// Constructs a multi-channel image by copying data from a sequence of single-channel images. +LinearImage combineChannels(std::initializer_list images); +LinearImage combineChannels(LinearImage const* img, size_t count); + +// Generates a new image with rows & columns swapped. +LinearImage transpose(const LinearImage& image); + +// Extracts pixels by specifying a crop window where (0,0) is the top-left corner of the image. +// The boundary is specified as Left Top Right Bottom. +LinearImage cropRegion(const LinearImage& image, uint32_t l, uint32_t t, uint32_t r, uint32_t b); + +// Lexicographically compares two images, similar to memcmp. +int compare(const LinearImage& a, const LinearImage& b, float epsilon = 0.0f); + +} // namespace image + +#endif /* IMAGE_LINEARIMAGE_H */ diff --git a/libs/image/include/image/ImageSampler.h b/libs/image/include/image/ImageSampler.h new file mode 100644 index 0000000000..7fc2f5251d --- /dev/null +++ b/libs/image/include/image/ImageSampler.h @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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. + */ + +#ifndef IMAGE_IMAGESAMPLER_H +#define IMAGE_IMAGESAMPLER_H + +#include + +namespace image { + +/** + * Value of a single point sample, allocated according to the number of image channels. + */ +struct SingleSample { + float& operator[](int index) { return *(data + index); } + float* data = nullptr; + ~SingleSample(); +}; + +/** + * Controls the weighted average used across a window of source samples. + */ +enum class Filter { + DEFAULT, // Selects MITCHELL or LANCZOS dynamically. + BOX, // Computes the un-weighted average over the filter radius. + NEAREST, // Copies the source sample nearest to the center of the filter. + HERMITE, // Also known as "smoothstep", has some nice properties. + GAUSSIAN_SCALARS, // Standard Gaussian filter with sigma = 0.5 + GAUSSIAN_NORMALS, // Same as GAUSSIAN_SCALARS, but interpolates unitized vectors. + MITCHELL, // Cubic resampling per Mitchell-Netravali, default for magnification. + LANCZOS, // Popular sinc-based filter, default for minification. + MINIMUM // Takes a min val rather than avg, perhaps useful for depth maps and SDF's. +}; + +/** + * Defines a viewport inside the texture such that (0,0) is at the top-left corner of the top-left + * pixel, and (1,1) is at the bottom-right corner of the bottom-corner pixel. + */ +struct Region { + float left; + float top; + float right; + float bottom; +}; + +/** + * Transforms the texel fetching operation when sampling from adjacent images. + */ +enum class Orientation { + STANDARD = 0, + FLIP_X = 1 << 0, + FLIP_Y = 1 << 1, + FLIP_XY = FLIP_X | FLIP_Y +}; + +/** + * Specifies how to generate samples that lie outside the boundaries of the source region. + */ +struct Boundary { + enum { + EXCLUDE, // Ignore the samples and renormalize the filter. This is probably what you want. + REGION, // Keep samples that are outside sourceRegion if they are still within the image. + CLAMP, // Pretend the edge pixel is repeated forever. Gives edge pixels more weight. + REPEAT, // Resample from the region, wrapping back to the front of the row or column. + MIRROR, // Resample from the region but assume that it has been flipped. + COLOR, // Use the specified constant color. + NEIGHBOR // Sample from an adjacent image. + } mode = EXCLUDE; + SingleSample color; // Used only if mode = COLOR + LinearImage* neighbor = nullptr; // Used only if mode = NEIGHBOR + Orientation orientation; // Used only if mode = NEIGHBOR +}; + +/** + * Configuration for the resampleImage function. Provides reasonable defaults. + */ +struct ImageSampler { + Filter horizontalFilter = Filter::DEFAULT; + Filter verticalFilter = Filter::DEFAULT; + Region sourceRegion = {0, 0, 1, 1}; + float filterRadiusMultiplier = 1; + Boundary east; + Boundary north; + Boundary west; + Boundary south; +}; + +/** + * Resizes or blurs the given linear image, producing a new linear image with the given dimensions. + */ +LinearImage resampleImage(const LinearImage& source, uint32_t width, uint32_t height, + const ImageSampler& sampler); + +/** + * Resizes the given linear image using a simplified API that takes target dimensions and filter. + */ +LinearImage resampleImage(const LinearImage& source, uint32_t width, uint32_t height, + Filter filter = Filter::DEFAULT); + +/** + * Computes a single sample for the given texture coordinate and writes the resulting color + * components into the given output holder. + * + * For decent performance, do not call this across the entire image, instead call resampleImage. + * On the first call, pass in a default SingleSample to allocate the result holder. For example: + * + * SingleSample result; + * computeSingleSample(img, 0.5f, 0.5f, &result); + * printf("r g b = %f %f %f\n", result[0], result[1], result[2]); + * computeSingleSample(img, 0.9f, 0.1f, &result); + * printf("r g b = %f %f %f\n", result[0], result[1], result[2]); + * + * The x y coordinates live in "texture space" such that (0.0f, 0.0f) is the upper-left boundary of + * the top-left pixel and (+1.0f, +1.0f) is the lower-right boundary of the bottom-right pixel. + */ +void computeSingleSample(const LinearImage& source, float x, float y, SingleSample* result, + Filter filter = Filter::BOX); + +} // namespace image + +#endif /* IMAGE_IMAGESAMPLER_H */ diff --git a/libs/image/include/image/LinearImage.h b/libs/image/include/image/LinearImage.h new file mode 100644 index 0000000000..39ec00c2f7 --- /dev/null +++ b/libs/image/include/image/LinearImage.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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. + */ + +#ifndef IMAGE_LINEARIMAGE_H +#define IMAGE_LINEARIMAGE_H + +#include + +/** + * Types and free functions for the Filament core imaging library, primarily used for offline tools, + * but with minimal dependencies to support potential use by the renderer. + */ +namespace image { + +/** + * LinearImage is a handle to packed floating point data arranged into a row-major grid. + * + * We use this object as input/output for core algorithms that wish to be agnostic of source and + * destination formats. The number of channels is arbitrary (1 or more) but we often use 3-channel + * images to represent color data. + * + * The underlying pixel data has shared ownership semantics to allow clients to easily pass around + * the image object without incurring a deep copy. Shared access to pixels is not thread safe. + * + * By convention, we do not use channel major order (i.e. planar). However we provide a free + * function in ImageOps to combine planar data. Pixels are stored such that the row stride is simply + * width * channels * sizeof(float). + */ +class LinearImage { +public: + + ~LinearImage(); + + /** + * Allocates a zeroed-out image. + */ + LinearImage(uint32_t width, uint32_t height, uint32_t channels); + + /** + * Makes a shallow copy with shared pixel data. + */ + LinearImage(const LinearImage& that); + LinearImage& operator=(const LinearImage& that); + + /** + * Creates an empty (invalid) image. + */ + LinearImage() : mDataRef(nullptr), mData(nullptr), mWidth(0), mHeight(0), mChannels(0) {} + + /** + * Gets a pointer to the underlying pixel data. + */ + float* getPixelRef() { return mData; } + + /** + * Gets a pointer to immutable pixel data. + */ + float const* getPixelRef() const { return mData; } + + /** + * Gets a pointer to the pixel data at the given column and row. (not bounds checked) + */ + float* getPixelRef(uint32_t column, uint32_t row) { + return mData + (column + row * mWidth) * mChannels; + } + + /** + * Gets a pointer to the immutable pixel data at the given column and row. (not bounds checked) + */ + float const* getPixelRef(uint32_t column, uint32_t row) const { + return mData + (column + row * mWidth) * mChannels; + } + + uint32_t getWidth() const { return mWidth; } + uint32_t getHeight() const { return mHeight; } + uint32_t getChannels() const { return mChannels; } + void reset() { *this = LinearImage(); } + +private: + + struct SharedReference; + SharedReference* mDataRef = nullptr; + + float* mData; + uint32_t mWidth; + uint32_t mHeight; + uint32_t mChannels; +}; + +} // namespace image + +#endif /* IMAGE_LINEARIMAGE_H */ diff --git a/libs/image/src/Image.cpp b/libs/image/src/Image.cpp index 9ad7bfda12..a30bcfe1d6 100644 --- a/libs/image/src/Image.cpp +++ b/libs/image/src/Image.cpp @@ -85,7 +85,7 @@ void Image::flip(uint32_t flags) { memcpy(b, ptmp, mBpr); } } - // Our hflip implementation is inefficient, but it's never invoked in the renderer. + // Our horizontalFlip implementation is inefficient, but it's never invoked in the renderer. if (flags & Image::FLIP_X) { for (size_t row = 0, nrows = mHeight; row < nrows; ++row) { for (size_t src = 0, ncols = mWidth / 2; src < ncols; ++src) { diff --git a/libs/image/src/ImageOps.cpp b/libs/image/src/ImageOps.cpp new file mode 100644 index 0000000000..2e81037e95 --- /dev/null +++ b/libs/image/src/ImageOps.cpp @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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 + +#include +#include + +#include + +using namespace math; + +namespace image { + +LinearImage horizontalStack(std::initializer_list images) { + size_t count = images.end() - images.begin(); + return horizontalStack(images.begin(), count); +} + +LinearImage horizontalStack(const LinearImage* first, size_t count) { + ASSERT_PRECONDITION(count > 0, "Must supply one or more images for stacking."); + + // Compute the final size and allocate memory. + uint32_t width = 0; + uint32_t height = 0; + uint32_t nchannels = 0; + for (size_t c = 0; c < count; ++c) { + const auto& img = first[c]; + width += img.getWidth(); + if (height == 0) { + height = img.getHeight(); + } else { + ASSERT_PRECONDITION(height == img.getHeight(), "Inconsistent heights."); + } + if (nchannels == 0) { + nchannels = img.getChannels(); + } else { + ASSERT_PRECONDITION(nchannels == img.getChannels(), "Inconsistent channels."); + } + } + LinearImage result(width, height, nchannels); + + // Copy over each row of each source image. + float* dst = result.getPixelRef(); + for (int32_t row = 0; row < height; ++row) { + for (size_t c = 0; c < count; ++c) { + const auto& img = first[c]; + uint32_t swidth = img.getWidth(); + float const* src = img.getPixelRef() + row * swidth * nchannels; + memcpy(dst, src, swidth * nchannels * sizeof(float)); + dst += swidth * nchannels; + } + } + return result; +} + +LinearImage verticalStack(std::initializer_list images) { + size_t count = images.end() - images.begin(); + return verticalStack(images.begin(), count); +} + +// To stack images vertically, we transpose them individually, then horizontalStack them, then transpose the +// result. This is incredibly lazy, but since we use row-major ordering, copying columns would be +// really painful. +LinearImage verticalStack(const LinearImage* first, size_t count) { + ASSERT_PRECONDITION(count > 0, "Must supply one or more images for stacking."); + std::unique_ptr flipped(new LinearImage[count]); + int i = 0; + for (size_t c = 0; c < count; ++c) { + flipped[i++] = transpose(first[c]); + } + auto result = horizontalStack(flipped.get(), count); + return transpose(result); +} + +LinearImage horizontalFlip(const LinearImage& image) { + const uint32_t width = image.getWidth(); + const uint32_t height = image.getHeight(); + const uint32_t channels = image.getChannels(); + LinearImage result(width, height, channels); + for (uint32_t row = 0; row < height; ++row) { + for (uint32_t col = 0; col < width; ++col) { + float* dst = result.getPixelRef(width - 1 - col, row); + float const* src = image.getPixelRef(col, row); + for (uint32_t c = 0; c < channels; ++c) { + dst[c] = src[c]; + } + } + } + return result; +} + +LinearImage verticalFlip(const LinearImage& image) { + const uint32_t width = image.getWidth(); + const uint32_t height = image.getHeight(); + const uint32_t channels = image.getChannels(); + LinearImage result(width, height, channels); + for (uint32_t row = 0; row < height; ++row) { + float const* src = image.getPixelRef(0, row); + float* dst = result.getPixelRef(0, height - 1 - row); + memcpy(dst, src, width * channels * sizeof(float)); + } + return result; +} + +LinearImage vectorsToColors(const LinearImage& image) { + ASSERT_PRECONDITION(image.getChannels() == 3, "Must be a 3-channel image."); + const uint32_t width = image.getWidth(), height = image.getHeight(); + LinearImage result(width, height, 3); + auto src = (float3 const*) image.getPixelRef(); + auto dst = (float3*) result.getPixelRef(); + for (uint32_t n = 0; n < width * height; ++n) { + dst[n] = 0.5f * (src[n] + float3(1)); + } + return result; +} + +LinearImage combineChannels(std::initializer_list images) { + size_t count = images.end() - images.begin(); + return combineChannels(images.begin(), count); +} + +LinearImage combineChannels(LinearImage const* img, size_t count) { + ASSERT_PRECONDITION(count > 0, "Must supply one or more image planes for combining."); + const uint32_t width = img[0].getWidth(); + const uint32_t height = img[0].getHeight(); + for (size_t c = 0; c < count; ++c) { + const LinearImage& plane = img[c]; + ASSERT_PRECONDITION(plane.getWidth() == width, "Planes must all have same width."); + ASSERT_PRECONDITION(plane.getHeight() == height, "Planes must all have same height."); + ASSERT_PRECONDITION(plane.getChannels() == 1, "Planes must be single channel."); + } + LinearImage result(width, height, (uint32_t) count); + float* dst = result.getPixelRef(); + uint32_t sindex = 0, dindex = 0; + while (dindex < width * height * count) { + for (size_t c = 0; c < count; ++c, ++dindex) { + const LinearImage& plane = img[c]; + float const* src = plane.getPixelRef(); + dst[dindex] = src[sindex]; + } + ++sindex; + } + return result; +} + +// The transpose operation does not simply set a flag, it performs actual movement of data. This is +// very handy for separable filters because it (a) improves cache coherency in the second pass, and +// (b) allows the client to consume columns in the same way that it consumes rows. Our +// implementation does not support in-place transposition but it is simple and robust for non-square +// images. +LinearImage transpose(const LinearImage& image) { + const uint32_t width = image.getWidth(); + const uint32_t height = image.getHeight(); + const uint32_t channels = image.getChannels(); + LinearImage result(height, width, channels); + float const* source = image.getPixelRef(); + float* target = result.getPixelRef(); + for (uint32_t n = 0; n < width * height; ++n) { + const uint32_t i = n / width; + const uint32_t j = n % width; + float const* src = source + channels * n; + float* dst = target + channels * (height * j + i); + for (uint32_t c = 0; c < channels; ++c) { + dst[c] = src[c]; + } + } + return result; +} + +LinearImage cropRegion(const LinearImage& image, uint32_t left, uint32_t top, uint32_t right, + uint32_t bottom) { + uint32_t width = right - left; + uint32_t height = bottom - top; + uint32_t channels = image.getChannels(); + LinearImage result(width, height, channels); + float const* source = image.getPixelRef(left, top); + float* target = result.getPixelRef(); + for (int32_t row = 0; row < height; ++row) { + memcpy(target, source, width * channels * sizeof(float)); + target += width * channels; + source += image.getWidth() * channels; + } + return result; +} + +int compare(const LinearImage& a, const LinearImage& b, float epsilon) { + auto w = a.getWidth(); + auto h = a.getHeight(); + auto c = a.getChannels(); + if (b.getWidth() != w || b.getHeight() != h || b.getChannels() != c) { + return -1; + } + float const* adata = a.getPixelRef(); + float const* bdata = b.getPixelRef(); + return std::lexicographical_compare(adata, adata + w * h * c, bdata, bdata + w * h * c, + [epsilon](float x, float y) { return x < y - epsilon; }); +} + +} // namespace image diff --git a/libs/image/src/ImageSampler.cpp b/libs/image/src/ImageSampler.cpp new file mode 100644 index 0000000000..52258c92a1 --- /dev/null +++ b/libs/image/src/ImageSampler.cpp @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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 +#include + +#include +#include + +#include +#include + +using namespace image; + +namespace { + +struct FilterFunction { + float (*fn)(float) = nullptr; + float boundingRadius = 1; + bool rejectExternalSamples = true; +}; + +constexpr float M_PIf = float(M_PI); + +const FilterFunction Box { + .fn = [](float t) { return t <= 0.5f ? 1.0f : 0.0f; }, + .boundingRadius = 1 +}; + +const FilterFunction Nearest { Box.fn, 0.0f }; + +const FilterFunction Gaussian { + .fn = [](float t) { + if (t >= 2.0) return 0.0f; + const float scale = 1.0f / std::sqrt(0.5f * M_PIf); + return std::exp(-2.0f * t * t) * scale; + }, + .boundingRadius = 2 +}; + +const FilterFunction Hermite { + .fn = [](float t) { + if (t >= 1.0f) return 0.0f; + return 2 * t * t * t - 3 * t * t + 1; + }, + .boundingRadius = 1 +}; + +const FilterFunction Mitchell { + .fn = [](float t) { + constexpr float B = 1.0f / 3.0f; + constexpr float C = 1.0f / 3.0f; + constexpr float P0 = ( 6 - 2*B ) / 6.0f; + constexpr float P1 = 0; + constexpr float P2 = (-18 +12*B + 6*C ) / 6.0f; + constexpr float P3 = ( 12 - 9*B - 6*C ) / 6.0f; + constexpr float Q0 = ( 8*B +24*C ) / 6.0f; + constexpr float Q1 = ( -12*B -48*C ) / 6.0f; + constexpr float Q2 = ( 6*B +30*C ) / 6.0f; + constexpr float Q3 = ( - 1*B - 6*C ) / 6.0f; + if (t >= 2.0f) return 0.0f; + if (t >= 1.0f) return Q0 + Q1*t + Q2*t*t + Q3*t*t*t; + return P0 + P1*t + P2*t*t + P3*t*t*t; + }, + .boundingRadius = 2 +}; + +// Not bothering with a fast approximation since we cache results for each row. +float sinc(float t) { + if (t <= 0.00001f) return 1.0f; + return std::sin(M_PIf * t) / (M_PIf * t); +} + +const FilterFunction Lanczos { + .fn = [](float t) { + if (t >= 1.0f) return 0.0f; + return sinc(t) * sinc(t); + }, + .boundingRadius = 1 +}; + +// Describes a Multiply-Add operation: target[targetIndex] += source[sourceIndex] * weight. +// This allows us to cache the weights computed by evaluation of the filter function, as well as +// the indices that are generated from careful alignment of sample and target samples. +// We allow signed source indices to accommodate external source samples whose values depend +// on the wrap-mode configuration in the sampler. +struct MadInstruction { + uint32_t targetIndex; + int32_t sourceIndex; + float weight; +}; + +using MadProgram = std::vector; + +// Generates a list of MAD instructions that transforms a row of samples of length "nsource" +// into a sequence of length "ntarget" using the given filter function. +// +// The given left / right floats define a source range within [0,1] such that 0 is at the left edge +// of the the left-most pixel and 1 is at the right edge of the right-most pixel. +// +// Regarding our nomenclature, we use prefixes as follows: +// n....number of samples in the row +// d....delta (i.e. the normalized width of a single pixel square) +// x....normalized coord in [0..1] where 0/1 are the outer edges of the range. +// i....integer index where 0 is the left-most pixel and n-1 is the right-most pixel. +void generateMadProgram(uint32_t ntarget, uint32_t nsource, float left, float right, + FilterFunction filter, float radiusMultiplier, MadProgram* result) { + const float dtarget = 1.0f / ntarget; + const float fnsource = float(nsource) * (right - left); + const bool minifying = float(ntarget) < fnsource; + const float domainScale = (minifying ? ntarget : fnsource) / radiusMultiplier; + + // As an optimization, compute the "filterBound", which is the half-width of the filter within + // the [0,1] domain. If this were a huge number, the filtered results would look the same, but + // the filter would perform very poorly because it would be iterating over a lot more samples + // than necessary. + const float filterBounds = domainScale * std::abs(filter.boundingRadius); + + // Iterate through target samples. "xtarget" points to the center of each target pixel. + float xtarget = dtarget / 2.0f; + for (uint32_t itarget = 0; itarget < ntarget; ++itarget, xtarget += dtarget) { + + // For this particular target pixel, we'll be accumulating a count and sum so that we can + // adjust the weights afterwards. This allows us to reject some of the source samples. + uint32_t count = 0; + float sum = 0; + + // Iterate through source samples that lie within the bounded region. + const auto isource_lower = int32_t((xtarget - filterBounds) * nsource); + const auto isource_upper = int32_t(std::ceil((xtarget + filterBounds) * nsource)); + for (int32_t isource = isource_lower; isource <= isource_upper; ++isource) { + const float xsource = (((isource + 0.5f) / nsource) - left) / (right - left); + const bool outside_image = isource < 0 || isource >= int32_t(nsource); + const bool outside_range = xsource < 0 || xsource >= 1.0f; + if (filter.rejectExternalSamples && (outside_image || outside_range)) { + continue; + } + const float t = domainScale * std::abs(xsource - xtarget); + const float weight = filter.fn(t); + if (weight != 0) { + result->push_back({itarget, isource, weight}); + sum += weight; + ++count; + } + } + + // Normalize the set of weights that were recently appended to the MAD program. + if (sum != 0) { + MadInstruction* mad = result->data() + result->size() - count; + for (uint32_t i = 0; i < count; ++i, ++mad) { + mad->weight /= sum; + } + } + } +} + +// Transforms a MAD program intended for single-channel data into a program intended for +// multi-channel data. +void expandMadProgram(uint32_t nchannels, MadProgram* program) { + if (nchannels == 1) { + return; + } + MadProgram result; + result.reserve(program->size() * nchannels); + for (auto mad : *program) { + mad.sourceIndex *= nchannels; + mad.targetIndex *= nchannels; + for (uint32_t j = 0; j < nchannels; ++j, ++mad.sourceIndex, ++mad.targetIndex) { + result.push_back(mad); + } + } + program->swap(result); +} + +FilterFunction createFilterFunction(Filter ftype) { + FilterFunction fn; + switch (ftype) { + case Filter::MINIMUM: + case Filter::BOX: fn = Box; break; + case Filter::NEAREST: fn = Nearest; break; + case Filter::HERMITE: fn = Hermite; break; + case Filter::MITCHELL: fn = Mitchell; break; + case Filter::LANCZOS: fn = Lanczos; break; + case Filter::GAUSSIAN_NORMALS: + case Filter::GAUSSIAN_SCALARS: fn = Gaussian; break; + case Filter::DEFAULT: + PANIC_PRECONDITION("Unresolved filter type."); + } + return fn; +} + +void normalize(LinearImage& image) { + ASSERT_PRECONDITION(image.getChannels() == 3, "Must be a 3-channel image."); + const uint32_t width = image.getWidth(), height = image.getHeight(); + auto vecs = (math::float3*) image.getPixelRef(); + for (uint32_t n = 0; n < width * height; ++n) { + vecs[n] = normalize(vecs[n]); + } +} + +LinearImage resampleImage1D(const LinearImage& source, MadProgram* program, + uint32_t twidth, Filter filter, float left, float right, float filterRadiusMultiplier) { + const uint32_t swidth = source.getWidth(); + const uint32_t sheight = source.getHeight(); + const uint32_t nchan = source.getChannels(); + const bool mag = twidth > swidth; + if (filter == Filter::DEFAULT) filter = mag ? Filter::MITCHELL : Filter::LANCZOS; + const FilterFunction hfn = createFilterFunction(filter); + + // Generate a flat list of multiply-add (MAD) instructions. + program->clear(); + generateMadProgram(twidth, swidth, left, right, hfn, filterRadiusMultiplier, program); + expandMadProgram(nchan, program); + + // Allocate the target image. + LinearImage result(twidth, sheight, nchan); + float const* sourceRow = source.getPixelRef(); + float* targetRow = result.getPixelRef(); + + // The MIN filter is special because it starts with non-zero values and ignores filter weights. + if (filter == Filter::MINIMUM) { + for (uint32_t n = 0; n < twidth * sheight * nchan; ++n) { + targetRow[n] = std::numeric_limits::max(); + } + for (uint32_t row = 0; row < sheight; ++row) { + for (auto mad : *program) { + const float a = sourceRow[mad.sourceIndex]; + const float b = targetRow[mad.targetIndex]; + targetRow[mad.targetIndex] = std::min(a, b); + } + targetRow += twidth * nchan; + sourceRow += swidth * nchan; + } + return result; + } + + // Resize the image horizontally by executing the MAD instructions over each row. + for (uint32_t row = 0; row < sheight; ++row) { + for (auto mad : *program) { + targetRow[mad.targetIndex] += sourceRow[mad.sourceIndex] * mad.weight; + } + targetRow += twidth * nchan; + sourceRow += swidth * nchan; + } + + // Perform post processing for the current pass. + if (filter == Filter::GAUSSIAN_NORMALS) { + normalize(result); + } + return result; +} + +} // anonymous namespace + +namespace image { + +SingleSample::~SingleSample() { + delete[] data; +} + +LinearImage resampleImage(const LinearImage& source, uint32_t width, uint32_t height, + const ImageSampler& sampler) { + ASSERT_PRECONDITION( + sampler.east.mode == Boundary::EXCLUDE && + sampler.north.mode == Boundary::EXCLUDE && + sampler.west.mode == Boundary::EXCLUDE && + sampler.south.mode == Boundary::EXCLUDE, "Not yet implemented."); + const auto hfilter = sampler.horizontalFilter; + const auto vfilter = sampler.verticalFilter; + const float radius = sampler.filterRadiusMultiplier; + const float left = sampler.sourceRegion.left; + const float top = sampler.sourceRegion.top; + const float right = sampler.sourceRegion.right; + const float bottom = sampler.sourceRegion.bottom; + MadProgram program; + LinearImage result; + result = transpose(resampleImage1D(source, &program, width, hfilter, left, right, radius)); + result = transpose(resampleImage1D(result, &program, height, vfilter, top, bottom, radius)); + return result; +} + +LinearImage resampleImage(const LinearImage& source, uint32_t width, uint32_t height, + Filter filter) { + return resampleImage(source, width, height, ImageSampler { + .horizontalFilter = filter, + .verticalFilter = filter + }); +} + +void computeSingleSample(const LinearImage& source, float x, float y, SingleSample* result, + Filter filter) { + const float radius = 1.0f; + const float left = x - radius / source.getWidth(); + const float top = y - radius / source.getHeight(); + const float right = x + radius / source.getWidth(); + const float bottom = y + radius / source.getHeight(); + MadProgram program; + LinearImage row = transpose(resampleImage1D(source, &program, 1, filter, left, right, radius)); + row = resampleImage1D(row, &program, 1, filter, top, bottom, radius); + if (!result->data) { + result->data = new float[source.getChannels()]; + } + float* dst = result->data; + float const* src = row.getPixelRef(); + for (uint32_t c = 0; c < source.getChannels(); ++c) { + dst[c] = src[c]; + } +} + +} // namespace image diff --git a/libs/image/src/LinearImage.cpp b/libs/image/src/LinearImage.cpp new file mode 100644 index 0000000000..9e3ea35dda --- /dev/null +++ b/libs/image/src/LinearImage.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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 + +#include // for memset +#include + +namespace image { + +struct LinearImage::SharedReference { + SharedReference(uint32_t width, uint32_t height, uint32_t channels) { + const uint32_t nfloats = width * height * channels; + float* floats = new float[nfloats]; + memset(floats, 0, sizeof(float) * nfloats); + pixels = std::shared_ptr(floats, std::default_delete()); + } + std::shared_ptr pixels; +}; + +LinearImage::~LinearImage() { + delete mDataRef; +} + +LinearImage::LinearImage(uint32_t width, uint32_t height, uint32_t channels) : + mDataRef(new SharedReference(width, height, channels)), + mData(mDataRef->pixels.get()), + mWidth(width), mHeight(height), mChannels(channels) {} + +LinearImage::LinearImage(const LinearImage& that) { + *this = that; +} + +LinearImage& LinearImage::operator=(const LinearImage& that) { + mDataRef = that.mDataRef ? new SharedReference(*that.mDataRef) : nullptr; + mData = that.mData; + mWidth = that.mWidth; + mHeight = that.mHeight; + mChannels = that.mChannels; + return *this; +} + +} // namespace image diff --git a/libs/image/tests/reference/blurred.png b/libs/image/tests/reference/blurred.png new file mode 100644 index 0000000000000000000000000000000000000000..d12c46c398f1f2c510d01620d31f0a2a926f858f GIT binary patch literal 55656 zcmV)#K##wPP)0077c0ssI2k$Z%{00004XF*Lt00D-e zG3b_G001BWNkl{&4zgf47tK`Gn?|?f-|C zmuq>>KjjOKC#OG_cYhaFN0iFRT_noS$KOmx{ z@%!%?3noN6+$Op|f>E*Bz2a@tdc&@;$LgM4*`4n3Il^^%l0W`wTvTD3;F<{Q2-o{*D`oyYxZYL_iCF%%yu(|j_4~>F zJMHRSDBVNMjz29eb0qHk%^QC@{`gJv`>so}|5e|U%AY>=wx0*^CR^>E>q%j&`qv@2 z;LqoTrRD4VrffO*UY^`ztDU?ue=~h#C+UIpL-JD3a}M7Uq37|h(t07RJ#8HcYe()n z19cCy(?EEi+^6RxAQ~vy1<+}hCNAChds?>vyk3vr~X;)Gl;;OPI~N(B+dc2u?Bz1?d=;(yS@kpU|BK|V2NRku8-VZ;7)j)<1aMf^F0nmZi zIb0RAQ>^wV%vi;1hz_rvqu{_w#%uWSY2SeB4Q4+|^*8J)gvMFngqBPMuK2e^ddppG#%=dpfIn;~lIjvrljp zh$gsDSiWFom^i=7xA-0JhKqyQsWAHit`n>XEN56HO84M;o7qnTAs-BE4u^73?tAGV z2e{~9S~Ndbp!lE|cFb-T1&~6*wA`YM+yxY-HC<~(Y-drQ@)@yYcXhUK3zyKr$oo}SYQ(*p->&C30R+-1-5gR^!J9UnY6 zi|X_4RYg^~4yEuz0%W+N`JpkF(b<4h{p+o@9}{9TXAQVY{2M^=1XRXCDOjN*Mgtj@ z8K}IDiWT-i?PzNNMbm3}4ul2*1!pN7BEpQ-!SIqxzl+&r$LWF9p5N(US3BJx{_zvb zngk3qk0u>ZLnN~qbBIYqoSh;B(#yXQ-rT>1C`yX<=VS@Fq05E)hI;!>5U%pN8 zI})#UhS>@+z18?jPU-0U%a2lgp5(pbKvjd*)BR?OmmsVoxmRtKlc;;bI>L3Jbr6Ur ziqGf02VyWg3s@lp4nK6lg8Lx@U_c=D`wHVuBbP5rE5#SMc0g%GtkPDB_vE4ilq$dh zk$5cMbJp=o+_4oG+!Clc78+1S+?KFn1(Q>}CSn5S;H<vv-6pC<#UOJ zva>4`qbhLKxOie(_Y^|{#f90cX%)Mouq!5L!3qKrfWZmT?%47BC%OOl;|?~wQ>qj%%8G8BL>A5~IpPc=_J+0UIoP0qQ_3y`_hB_LF1uax=Jk`?)i!1ij@4;E3 z#R)@ODHp1uN^A#+k8G8+Uif_?cS7n=*1=YdgVv3w2ugqea6hd4UTIx-cphwH&+Him zYy30N&NyojmJY;>s+_ZUW_EfvVVz-RbZ<9807qJ81VRCGc_uXDEPH=L%)(*i_n`GX zT=|vea1c|HUFZa;G^)PCs*G|HF-@bY%6(^7JJZ^^UX6de=l3+K@j^Yps)bRxZitU_ zU5|1Bq17lw;3v8N;NzZ}2vv2$_4$9pR`!X7(}DAS-FM=O^MmsB@wt08w@2jrs51UL z`MuoT4iKR|vrkmjg0Pb72}0<_3HKgo?w5p>^6DO8ol(`qR^gCGCrl4)3Koch1x>($ z)}b=KNd{2)or>B@O?u2A?~Lcj>;aCH^ZZSxL0D1@M?eFcbst0TDV^q=l3Si?&7`m;H>b!aZEGT_s{DprwaSrWKw=1*M8rhC@08!U2@dK&5FNhoBND71J6N|CyHgk;l$I zcdppb^Q2PzoNk$Dy~lH&&`j~CYBWu%#d9k^rZvTX@{>yI9nGHe#759%82BChexUU| zTNPM(pybc#hgAG9k_=L0d$N`GAS?%>zTcGYt=H!B$<+1R@Y} zKUAO{K@tBTmOX8vVp!t+gcU>MAq6O@KqXvM-~xmox+nJutK_VNYY0;E>pN2n6c#lC zVI_A0>xhL-(ABZB@r`zmg-0t^Km}6aN*yx?!qXwnp4s2FD?@D74N2}IBM>^R_b{!h z-)Dp0n^v2nU@BQn!M0$tlVtg--;>!>tsAFr z@w@>9^!Ok-`RHm-{#bw|PLk!Tez()q?8)gH7JJ^X5VWJk=hno+H=vj;zY43tRyFc9 z^oA{5ahv_H2VwQ%fl`>=s7e{=G{n11PH4PN2dV-CO=+h1!B)iyTk$}5zYZa1E3MyA z`DTo;ED6pLToRT*6#0iEzYnUaY*i8CKx`KX0f|wLB)RuUUI|plbpy`@e|9yyVm{|| z9YD2-+&TDtFnc1_(F6-QXBlCgaF#|Oq;QI|GE+kx?*m~DaY8zr!nMVZ+s?EQ{H_D8 zy=e(R#k4B9@6s~wvnzwkfx={0O&~pHanhn_Mu~MU4C-eClk*MyZ&0yUXd#zM^GUZUV^S0L7%UDa^uKxsrlhIA}6u-XF^A2{iT z8VLV1azC;Fz2i^EAHSUnc=z%C3wY<-?PdFd?TXv(VJsiaC0ypQ%)v~v8>ioZTgj-{ zp@)Qf^}rT{ew`oo%c9>0xd*c^9nCoXhU*R2%pgVV@h}jQ>TPBpL;O2q=S&k1VP#Gj zuDB&_F*Yhrs1QKJlsmb`VSpno6NF`Y%xWH{Ku9K|wC1ne1H&7Ml?lXP429Lg2^T@{ zSQx@sc=kX<N4SRsj?#WY!H-FXzMLzRw>1p#S@O z-uAl=D$c&k`r+(1Z@<^MY`1#`vP2n2jBsC1RjBhLh+%wkk%+@9o(@bt-s>o1&_h(*j3Kdd961np2p*3 zDwSfUEA>>2Y1TXm!rFFD=;Tgfgw>!ug&PgR>Tv;j_(mNu8Y!#>YHChMrb(HI!K_K{ z)dRaYvwNvxNwA~=k)kWWpb(@)LGIaaau;$?0$~(BCR=5euLoRfo3UyKteo7*q^QWi z3JmRJ-_DrS;bK>I3@J)!V~Ld`=2(G9IbT)b>N8w)_(C+)gMv8H9+sETG0?34ZOH$l zb=4tG5e;ild^IhX4imXMT-qQVIGWa&**^({JJYIm1qQQ+L*9|Qg+M`PEWH%h%|>M? zA%L<#Xef%BD9Bxn zw%CMLLjAZwaUeRuKr@ zv~(yH+LOB;?25y#8n951yM}H6Gg+`0pqgUnkOtXen7c!%7*)?enEZ|r2o=AF+<(SD z|AD{%4ZnQg|Na8}9UuCiz|X)xfWLj{|Ner%{T=`K<%9qG+s7^^W90sbmoND3D}Mco zUyb4)@p8rM#eH&AfEcrzY<~fsfdw9jw9K;hkXJ`?=R)p}z!%^v@HHs@F~qy^dscv0 zs(r=fhKCnCKI3V@#t;Ov+bpXU&zLd20}4d8be}}7gf%CTowF`Z?rZbM*056WJE*It z8blzS#FYC6<8*QI+63c zgl_0GN|k6ft(=8ERqmdXN}xcCdJ3z#U0G(v)AFi|x?Nowddl`{vlXb|ciHiKc6YG< z@^Nx+=zqg6{~!L>dtO}TaQ)8oME8Z&v7fVD{Y<>lXZ8t%?uI{SkNqz5+i5#T&n0fJ@BkSWZSf z)qV|TUya%86zl3{m+L9MnVE6-+o{pHSWRkLrcEAroB4fZY(%NBEY;9EoUNE5r(d1$ zps;Y1SEHba7AN{pSVRp>8rcYHibZwV>Cl*v0DMj&osbp6?t%6^L4o0iD720_5Kei9 zh=rMIn6L`MS{pXP7HriOT}cGe$|%P_iw0J^Y{Sm9EG=XF?#xc65)v+EFlo-V8fPsX zUG}wM6SGSUlGuNgvce$GHO2ce*LO&}*PQ!StZQAcUVP9z;0n{~z@`vI7l%tbnTQ5O zEab?!)BUte3z=20qEHW<=|1vA*P^tUU9~8_s^qS-T}jy03{)K@+qHa_LjIs5zl@re zu?f{o%Y4PZ|APPh6Mp@IU!Ov}+awrb47dU}=reFhGm4s-oLtTM)zh?0&AD4z=BId= ze!qe)QR)C+TIsU=8IKlk00?+1S@;E;70={t-lz3(NhK3KxnCJR?<&>C*g2f==HyNr zV>J&n@$PIz$`B*>nXQzwaIz>pZ+DV#ktxtnSO~%bH_hR5gEp8BrHkl}+=0SVQdW@L z09=M_ryMfx)M&wK%&cZ0Jk2AYVYM!}8CHaiLkVc48bXRn3~@A8N$Q^5<7=}X0`rUx zMU|}uLNwx*107;gf1yxU^rrnlSX2osGJb|thC|NYp9n!(#QQESBed5IyL8`9wp%#7 zIl0roDosmBEQLiBIT`XVj%Z}w?~%LNRRPNMyNP#Zt#-8;R-(Hom=p4e6$`k5)x+Tc zDn~g*ynmKL{)po!-fJCX^7~i(_7i^n3;y?4{Q897Ec0H-UAoJ5y~M~yfE9Qh>6B5+ zSSP-YQuK1*_rDDBp5)F+?v`!8U~R71+lI?49&GM6%ky`d;!S?v4V+P2adZURsnMA1 zs!n)oSR36KM(!KP6_!vJ2*=pjX+0Jxr6BDyR){r}k!?pKEIOPpJkavU%%8{|%FgPRu6i8e(~s|`$Xjd&`p^NuW?$lHlU=v(~dWVw6yy?Iy z7m9Dit~!N*?ZkUx^(T@0J98#~?t9YV|QqW8WW+JE9YWK$4SB!J&g%>^2jOPAERLAi)UuYYG%ll`_gf_MV>vF{pA|W zo=a-KN4DFc^>JV;O^VOzY%WzF{-6+~xG`;n6*N)}ojOA)$bGc1B;zca$=#1Z?m&Ys znjTn2&|n5NGy;=0P=j_Q5sgtZL3PJd=5-sRF&d*s%O|OXh5dt0vD$$)2g2=?vy9oB zXJ$;fZ@{)AcbB51(R|jRqXW4Q(<|R>P_H4M6fzZ(KrCxs$xu*&L*!Qf* z_rHH?rd=fk&VKW*V<+Ajl7qnu)(dXesCj?F^COty zQl^_JcftnRl)6BOL`YzICl_`&B9wlpbRc&fX_>_){(wcv2@A9kv1WFqK^G-a6(WgM zlB7hUG&6hBx-q*w%kK6|+P%tx3f(RcPVCR??_R%uUGb2S~D*ocDRbL;R+$}BR6LgkmuWbXIuZg&j_^EQADBf(V z^dY#0mcM#h#+7^K*>AYy8rsOSPqcmrso%iiG~s@#vnbYrTfHM=vuUo-?5;-1GBebb zHnv%>!B#pzaZO4&gDI9;1^~6b?6J%y*(x8@iW3r;umGZJ@(Lg{^-(iZV{L@SSjxKp zeJ|O=y?feyr`*d6>hm5YN{IYnIz8&#X=U76!3x-fgsJy2iwTA1HH9W6p-S$NmeEx1 zy5u5VjVe+_vn-Ypp|MYJG=mGON@(hnftG8|C`kUun`#xYnX)Tf)d+Xk=Ksw^mXvgs z%`-b(jFsueB^G+dk2;fd5<-@!Agzl6tD)Q}$UWxjTfr)gYVqY1#KcXbtVkP3eBFcZ z>17M(JSiVQE%jdD-8AhsC)29Pe3JVQJX}*$E~`ZjXGLslkb4&hpfdM?X*AS2gR;xX z8_L}uimY1j^dEK)Bljkr1>#r>lZ{GhQ^tvv@@Bt@Mmet!bKq(EYo@JEw7L65os2sq z8DVWoT}jAOxC&CG%2u9~A)-+3V+6*~Q0`ugZ71;va#v<|LID}A4juBRO5wTmU2IG4y^gt*= zqpKoVNr@#US_zG4!-$1ep3_UJUGACO43}Tq{MS^{QZ)psT~W0wFBqi~3ys`~gjhhL zq>Wq-SWzS>M}!n<5ent56hoY0U4o|Ch?*72nrIMCj?O5^O1_hcW3>=%6#S0Ty0YA3 zMa(ftP(I21k2q4>YWV`BoJh8yD-GLah~kkib~CaPGw@u3if+|+`~i;m~>DyJSKY%<|+@R4iaWDs#4Ns#fi3% zi6?$f4@7aorpn1yqD{34nr(+_Lt_;Zjs>N55Lp2|F|=8IB(Q+2Cs4n0Q%4NnfN?*{ zIkOxYHbb#&Ic6gqvFXv3L`aB*gW0`NLjuK))|fALxRf*rOO2`)MI&b}Ca@4GkuZaa zIhNnTyB;coa#DH$I#DnEPmfr=J>Wf$uiQiNSqgdj zP9J%1z5CO&v5Q2{xoFhqgLnUZ8Eux_0=$nEc~6gp&_`KVR>){mYR2!Ha@N1=x<#^R zrYT6Gy<)a%i4ABn%XxBB-^1~W@mBXvOu~q@xY9xKn-jfxoon_D)QP)II}s^c**T*T zl+nC3ArWae<(^3;&qf$wX=p2LtSs6nwr~~@teiwi9W3W7Ft9-zs9Sv`)R%y&db>)* zF0XIj_?t$w=dbKvU$C+^488_+umBmwSKK;rClY3-DrHrsMo^|^gvAyJRlH2nliMZ^ zSWTy^ML`xcQcL}@kn=l)tVYc5fB*(<@D3{DI(0k-Tv|XoAaHJRx`%6LY~$Wk|MovbVaYRZ#|h?!_1T8J)08$sX+2fM^Yu|O}L<1;O*q+&Te zBc85mYg(89RgREj7h_jtS6H^`LGG%I+1uWSgr5Co(3}?E4)g6g$V=3!9;qvN?pU4K zJF$ReberYyK+gGPqQhM9sA{lX@`A9MD2uX;1>Gb>P>90z(DeVb**1HsUFWMUw5#Rh z+zAg59Z!(5puCS1&@2!JLx(!%U#Bb$tB<9a^RGJnXC z_9fB9cZ^$NWxE-H&? z6JfPwiJ~GR*hDs9g*E^R@oHEV??b6KpkCqTi3Y8MvlbxALZ4WC0zj2Or0QJ)%5KEmt!1r7+A4NK7AwaQ zg;>>ETx}N>rEb1wW!-fvpTo^M0tJ>6v$4NMKuz>2Rw~e7D+f)iI@{{mjj*^Y3AmF< z(b#JwsxeToA|en+^&bI|M}{V0fknA8dpGS7O@@qYZQ@8sxvQ}62&#Tzsv=2MB%FfIkGJLbyD#VXpT6UI!@~BdzAZ@e2DR=2QO};;!Bl}xSP8F0H|C9L<0g%j8@1LN zFWfGTugsm8m>ARxxi{4-^eTE)xri)Xnsn--y^Hj%(?+<7HW5HO(A4Ln1lWKZ@CFpGCgM(Uui-2ox0VtTWt)Njk~q(iNL-@B<7=un2CwW z*X^WTWN{6PvPk+NQMexfGG@25C@Dz9uSX!(49lFbM@miReM-;VFA5lMFSu2l zJR-Vibf%35*`v#-BeEpBP$X7jGF_<=H;k^AOvJ>#*f)!?6cGr^DtKzfSp@2taMsZK z0wHg;?+y36U!Kla8=QN3k42%UEJcMSr|4@kXJ=Q#s7O;ilne6*4bzGNUF254-Xqkp zY7SJaicZsl#Y49%b)b9&geY~B7U6X;&m)D3#P#U>RqBunLMkftrV3E!H~_kQDC0c% zoeqCEJ+u(+ACZ4Q{pdG8C~x7r(lU2xZ8wJ0!tB}e%X&SQzA?Hcxf3a&lWs&e=2zy| z##?K>ExI&Xme!Wb(yp!b*1+7DFSIB3MfEE3D01!n(tF!F^-Z^}Z@kg=+Tq;=L>6eX zakM76>kIH3@C-ZyFLBLlna!C-#or2g$1i%Ob%SW0jff2;yfZB+llSL}dUh}7BUeXb z>Evh!(`H&?V< zP_6fMyz5iTIbbzjzhkTN@V2zgo6~wzFoBMGc2>(=>DbWJn)b$nsWm1hYRp8!%-Oxc zFU4aai;yaJVPOv+R`lImANzT$i0bNM6SI*ZrgR++P3k!>bh>c-d1WLVC&GE z(h_C6N)Ywyd;y-JzXrv>0I$LCThZ@F8+ro}DvpZD5KjoJ$DT^DvyyEqAw&}D*|WGD z5331R@D-6~qowjVl!Gq?_tyL~j%y z7oX2>uvG=2s?iy%DG=6~kAW3ctYZHSFU{W9{xkbNr(mde2Ow(1jnP;e!xzIdGdo)q z1`Wa@5+Sx?4@+T5P?cxDjod|TvQax}hioJp94JF9m^W@)Yqw>&T`&E5)$5h6*X4S>Uav5gZ>_i1jk)5JS)_@y-Y>mh*Y&!t z%k4(Do3_@OZ$tn(^rl!8EW#nRq+HK4@Eh=N;8)=JK<>TV;5(qPp<-o;A*|FLZ}Pz0 z_{MvVM~ka`WTlJEaj~}zCoEPL%w!8i8*^|LH=97*lRHF7g*riepiL~cmdEy`%-gim z1W^@2H^L&H74Pcjexehz7gDZlweuZK^GVLS69~_+iaj@Ct01)`FacT5uB^(j$(xl> zFNA=L7uLqqSedo4Z6l$s$?YnY)HsW%6E&gk8$5#$bAl?x{++KK6Bsf%S8vg=9|EDw zH@m}OXKaU9SUFvHKneK`buS5Vg)thS5sDCGgLP4F-)i3oW_&xQz@w?qiD))N7ZvYbDo~ST^ z@=&AbUc7Hb(Ynn%HK>ZBo;_x_C`Qf3>9jBwrlo0Pv}XLCPFOB3HlAxjUBI2JVq{#r zRMj127S8M&>84gGt(#6&MjggwEiuQA5_Dbszpi?eYGq^B^K(Dy= zl}Z5@`F2udaM8lLFttVtYipPZtCIU5tS0C}+_Oj*03f3I!cm^m*_nL^ZZiQEh}u@sS->nINhzsg z*^cS2z5nZ<|Afc~{_ijN(fcr+$@X2g{Uzi%3bQ}dMvv)DL||5Kx-`99^m>(t2Yq^? zFJCTCPwnYRA0G7KL9bU`7H!S9P={$tq26iR=ys#mS9*Qr=Vxk7iB_PiQqH%r?+fHB z0(A^BUT1#KpZo=8Oo{3J#g^UdCl4xbd{|tNfEphQ=*!d zC%B4cM;5rm(lxKqCS`C7!g8P-D=bW{6|8vLR>V<|0#{UKj~fwtVHCljGel#NvFb+H z6mt|DSoIROw478NDqK}J99&O1D$u&)7P6gUabKMWr6^$ZN_H3a++wfA2`k3Z$bhm( zV&zvBSao0A1@4Fm48KSfZKSHDHWMBN&9v05E5tiGp{rI;DbcG6c7tMwhL5LZ{t5j3 zqr2t(_2-}e=Rf}O*S~#aFVlbh^2`7D```cd%SR%MZOjeeK06)n{cSY%FWn{Fvxy@@ z&X+E}RJuHREgG?jsf8~Wx?cJ5k-mK4uV3lwSN`&ao}TFOk*-&|TxeO)+90gnb=!1Z z^>)*j7kz$~Wl?4UR8>T@cj;ZbsED#?EN_ZXM43T)jcL{23c2sn?%#z;;dfWE#yr7R z#`4++t$U4K@xVb?hJ> zb8?nrMG5Ww_&lw8ladp!S;d9$W{k)a)v-0vS*t z6iO6CfeS$B27(f`NOC1sXwK|Z5o_62jwf=r@}DN?UCplY7GerTRDDX#LV0px3;v)Ww~B2kB^t9C;IyJ^3zY3uV0t1U)$4Ddwk@}g)bK} z+5)_oeO+Z;`^!sze%_X4WnKXlS$ki%ZQHiKZoO^2vk)J)P_^JXiC$uIm*rmcn*(YdIYyn+YY>|+u{2qez ztr1VarF4L?K4hqefmm*`VeJ)WxI7NP`tA5eu*{8VVQtKg)j~n;WE0WzC`T>ObwSo> z5da_B1o?{y1y<;i{cZ|Ss@Wu=&nT7T)m?ndsO;a6D?5IDEndbkbL+8v) z*b}z$`z$BF6lS-PU#T;uMznCdEbY21*XtDz5AErRzkIoT{rd2izg&O%>H6i%^7OP^ zukCW7)`%H^ifHeBUANn9dw#xME;nWXT|~BRUDwy!?RLA}mbR{Kl$m-Q~Psj`xs zNs7XJoq8%|EIgt0duCRo*(6=1n9!K0AoLLk4Jf6m;i+^b%7tCcW>8bGFQyx~dEU>; zZKptJHL+|pj>%eal~KXkS$&5ZIhvLyvTRUj%q~7NF-_FZK<#F6fZty&^E(eC>!j@C z$?1EuX z*XvI|J^ke`k6*uDpPnuc5AAwwh1`4Z{dQYlUvHO7YYjjbp>5OG*X??}T`ted^3vL^ z@k;C~VBNKRMk{)exV(y`s(${lQb;?J){i-dWUfAP3q_+Nsxq?IS@L^&p!Zad414$? zUmCTAmqx~Dh1^Yws#ms0qRDf{&3ZLRhbi~Y=uBHEcjqYzNkutBm>phJ4p{9Z6%H`& zunNw)NAU%&K~Nz@ks8%|>=d@LhJ(&oyfj+ud0`KPjhf9a^JRfU2iWh&g{M4)|Fmg_2HpkueZzPWm%qEdttssE7;xA(_W^6 zBjs29@cU%D7T}I*w2JqcvkEDV7uaNT@G}vSdJn_s%>Z_OcZOP=*`2d8j|@*DD~tAS z-;P&Ih}sSl=&Y7LW(Y~~z&NoMQ;7_Xo>=!~`&}nYE37{j#KtT|)eSC7DTtm1VdZmR z=dELid|{V`LnLP{jcuC8aAUT$^>zuWs=^vl)ViN|AZ)C{(j(hW;(JL%Hl|a-$75m4 z4U$z%?nOgQWZ{g{b;gehVkZVhV_aBSo{O1{6Edv;hifveX0=#0xZw$_IS|@}7Ijcm zEvQ0RrJ<{GV8s%9(@>YutldwhK5(;L(cO+k2OK^(>-eEZ8l9BiXi(=iZ-Scbt+FHP zh3G=uiNV}jySCOZ7prq`kB`gK)Ai};;men&FJB&>o-WsGYmJ!!h)C~!z1^B+V|ssG z*Y^5K*Q;Ky{c>4byS4Vh{7n2xyb`UTPAZ@}9Iok+KA(KAY(GWjo>4lv7gv;+4WsIZ zexO@NFz=BNxcb=?`5Wq#r=?6WO?^=8WfK~SaR^a zw^O3;K+iS%X?})Al~F)6QpuIA0;p&Q=A9TgW|)2LlE*@B5eO~YP~?<~X!qom#0pGw ziJic`x>lIVHXbM=#H?GP)6SW_@tfNCq1+_V~o zZg9jh<43T{Vq1?y{!KZvE1IIKHr6i6CLLH!yDir%UoLdH=(0#_y|tD3#(ZOXCA@kjlu`)PQQoQce0u=+HpD2y znggwCAuOf&bI-}k8?M@h)3HjBg0q;x!BEEROXFF&ld2FIMUp5rMsKza9DMPsSjAk< zOdNghgxO~gJS%S>$ean%39hrKpjDH~Oju_T(SV}FN}S(wTO{Lm*YYC}?#SJjz1sMO z$vE8tA&01Uxav+(RN4G=Of&1ybGY6U7j+u?j7goD;{8jhLY~W}Dse*WpbdVviD*O@ zKf{Vz<1w9MJFTkiQA>fvEwr>^JCI7`R5(K z=iA9l==H(AS%#SfG3Y|nnLD$zrcoK$E|+$_UM`o*^?JEpuMZEE+&S{>m)osfug%J7 zmPK2W*6iIjq8t3N#s(8dnYm;0Q%Uu`$LxO7`_%ZC=Q96hzo}YXg<|~9{tU}1Se~7| z2|)~>0eG(_2BayeqOozFRkr0@m8G#deQv#sGu0_~Zt&EG-e#*aexE+5aF!DU1BzID zY3I-#Rsj@&*q_@-*Z4`TbBE!khAKLb|LZG>N9nljhwZyc=K|1qSaXyMr;oQSE&p_? z8*s#GW>SuQtvvW4`iGHPx_tDF@s83ZskOt3BZVc3)a>Pz4i~YKq)|l_^!6Hl9=0u7 zMuCS^%<28&@Z)D%<_8~o2p2h;){)2Zy}R=iz|> zDZs86GLg^YzZY6he#Gc6MblNi0nyR%?xAhv|`Jx@fg7J?AKFO}dMNrZM8{5daGhkns01Qas?++0kKsL)(k4Yz>~1_9W!>9EZm@YX48M(pf7U(Yt3+ z=cZMok)Kcbc&(-tAmki|P5-D8vBH_%Ax*m~_6bD5^SBFW0(IZ6zUhN{IR8w`=$+rc z@tg1djN|VsxudZE{4E+8nNeT01hMyKH)H zjNZZtgIUVqsh5$VAM0`AZ>oUp*qqGnv$PmBZTX2r>4Ce;uoI0f_$qXO>bt$FmV&w=pVZFyx-liwrpMHnWC%?b(5ocJH z^uSncWKNeXKUHy!TgD)3w>g4B)0|4!Lso$0d&Vb+S59_E-+v?Luz#jyeq>ta^Y}bI zkI&;j{DB9(?@^0qW<~ERFxGd+>Y8%c2i4!?%X~Y8C@Q;`3&Q(E`X3k8NJBRV zXbzpGh?y?I{xr+|e2EVQ$Lbqz#tzUJ79v*F``UV2kfTSjXsSIvZoUlH!FsrCzy{v$ zz5P4b()6z7pZ#m5A>h4_7_ZU9P2qR!aQKPZz=OQZToV~J;EKf3fO-E&afJ9pX=mu$V7nG#cP13<(9+7prlY5Ju1sz{l)%&ONXQ}7;}X`-KXMv zZsb&=+VS5{?zA5Z?|)N&Jihq%eDdMs_@@uP>5fM0$-N2HX8WM}uFbX$ac*VPeGO{w zecO8P+qPLkrdG>r+m+omool5e}HcJ+}gy>CJ_4}JHj?F}Z%A5RGVPS|7E zCPJ|{tjDUc_YEaYt8YVAm|ax_;MjWuDpFsN|HU3-%Z1s)eHU(>uIf00a=e|q-+So2 z$J*xiq$5^@*u4|cAuIOtB%fogj1yIQDrzfjY?Tcw94p@x+)E+V`|S9M3Pg`@7~OMi zuh!Bb!ru7on)6=17l*geyJu17rd7V>!oSJO#kvXz%4Sy}w#JgSOeyv(ju-FW!DFZH zK5=(Fcd&X3sA^hv{@+jT|1TXolj1WoIw^$&7+9bS^a{N~7u6=xM7VeAUAIlvwXbWx z-PU!@dUol(lKXnQt+(5{uKjlF>niK2+eX$0Sfq*AKC7nZtyS!KahC^-BbvIPr)$iw22yUot(S_Fr{g#MEW?%GiEU zDoPo8JswA3E*BP9=^m@&{ic5RZwAOFbX$A8d~*}16lda_@{q+n=Mx+pG+ ztLRnaBGP*2-f7!(U1eQ2>n`x}lJ)GzMNF^H&$pMC_4ReT-DF*LU1{66_tyJGY~-&q?!Rwwj5l}PKRm|Z*D8dN8d<};`O zViA)_jBm7;olr$c1?q%V_othRJ<5;ug01W!QeLF*bHXDsr&y)PO@=z^28YN`H0Z+# zizW9Lj26C4)T5wnN*=M{NbcEEw2zHZNyLE?*RsJ`)U+fj4nP@19BVA<-5vS89&g0L zIH#%=CPyna)?$msGA?wAPv$kCNZ2>8j*U(YD+Ru#ZfNM4k83f`J;zGLoE1R zEEY5c%&cfR? z-*LyT3Q)9z%S6LYR1Leru&WRa8g|9LD|;7f;fr1>`ope>b>DtK#8KM1_6~dazU9I1 zA}mnX4S7r`Rl90?-LSu3Ju|J(w9Jn=cH%Ud-5Nk%N;AmH@{^WPEUJs>*avl8+wDfL zuln-RE3;dtfqRX^o}XWS{q^?SZ~ggMUtZ|-wcT#lb?w`R-kZo(bW`09t1EXcWvpu$ z16#RGEzfU9csiV)6Z%nj9tjI-I(n_hjfV&P4^iV{i=Z(WeWb4|uL0G+0 z2vRmLRc#6=Y0M^0ld#6)v#96teB`Y3Lp=kkqJ-kTTs3>`6s^{qmYl8f8EA^7Mnplv z#X{5x9Gh?0YD|jO0{0q+^=?}NU^BuJbPt3YSg9CXqkelZdzo1j*Qyk>RW+#%cZdpZ z*uzB!l$+mwTJP5`n@ZQ*kC94CNNmvB z^0)3Zg4qf2J!zRZC!Bhq!$rg8h!&tJHL^)&EUF8Pt#nBzbn$MxdXn#{v~>(&1vR#k z)0V#y?zdqx&JyGI;4Gb%RBEjE&PhZd_B^4&@08>&2luPAm8c%E%2(ctg$dVEsA@5| zfW=T!o4iS^lsgkEu@Dn+x904E!tAP$vzEUiDMe*7&^iTH)RnucNLe~j=qhgVY!TDI z>YzzMk=HuHRc&nNdVv+u38&LQ%;16}7UJDDn#ULycJa*sxPwV;&t2cj%zMy`ALk9aruSlyJ0%gk%)-8n3*!8K{#LDjdJ$g4j+p;6Ehi{(t)-rhBq|0j|IU@Blml zkFf{S*sQ~L$ccfoC3&0lw)M7cz7Oj2vn(EW-W;X8YbGx%P_*PXQE@?deaW!RDU8`VP^ISV?%vd>4+41Micu|N{Du@onON?-X`oS}@ zg+hvxQOl)&Ge!xTFnWGy!uYcRr^pe{7~DadUbTOrQGy%z~X;LznPN^KG)|{5z zkz)G1xPCy}NDu*uyT)ts)vHGo16C+vh1t;H1SzIPe0uz%reB=O{_h#VQb4$*)+3*eH3Q{=%5lk0pGA1 z567LhG)6~mYW(9)3>o>E!sU(R9_cc{KqbKMZ);S6i4Y^o9UIQ(A#q@<$l!)P`Y5^=Jl8L_}9QgZzVk$W6=y!t3BUc9qEOm@{Y0 z5)`^v{(2SADCE0Z21)|-5bE(7agY_`)xPT5bxsygF`>yN3s{tqB(B2LmKgu@d!IuU zITEBpQy3%6q!aB3Fn;uQCoRU*cw`am{xsroMO}gc?Dpk-Vr9nBD{P5%7i z(T1xWppsBRzqL#SLekIdlfL@$M)UkyxadKMVZmqp(d{M2?1*EUnl)UbLPY5T*wducc+a;Y^EFjtPEzkY`vPZomJ-^^~sY52O)-rY}M7!8*t@m-R+0$ zffM@|JXeZa!f{^|%QTLGwjZaXBL_}lMIPJj)L&d{bWi|u7Z(9})YZ`|m3KkOSzGj7 zhzu;Z!YR>}6|9#nEJU}4q-QI$x>G(SidA{L^hPfof|al8vU^c&R>HL)L}+rD>-!6#&mAPBG* z6<4F_{lHaIm+#=vNo~AH0zkb|O0a@p6Kf}wVOE55^ru|#n7xFG1DUB zv!yjQZt7YQQs_7n+R#)&peLVuM-{-K*#mS1{d|MspJUG8smV=8N;{-)OG=pGMmK#! zN_?XCqpHx?hsR7C5u zXt{-Nw=+K2esWw0|LT=!`&&5V3!NAvSI>t!97DnP$lWFKG^rC}sErniE;d0XB-?nQBb@uPwEo>p z|1-!5dfVVmkBX}J!~#T^ZVPkGle4YdF0fM%tq zy-#1D1vshwTHWopEs7s+vj^gTZNJ+85jqj_b+u$LMt%s(Sf`c#boI36zdf!Rg*6vj zdEyGktMb*b8x5f~YRUNNSEfG9#RHsb$TCe4X00Vx?WMWCHedX`OJchCdBKLY!ANso zK6X*<&sg`ys{uATW7^-EIXAJ^LP}zC3_`}-D9`A)1XU&d$=c4?gdg(|*601qgbfQm zt?_>m&uLM^(bJt`?v-a&x}ayX8Y0&FJVcl~8-7vQ8+{#(8|rb+A`6AP@_+RFB~(^^{a9rBWxCqi9*aAuoihh6jq^rIv_u{<8FaL0`J+#3+j1+iVe*Yn?ajL{$}Ri z;<2E=NHYuxNDEN~D`g+09ub-Z`;6~SJBtoM=1(Y3U^hkt6O2hUOBzI83y-VGrdDo1 zCV(SdgHbcFEJ-ut;VVHZ8w*QseBEai{dsJ;GYm-~V02RkmcB?*XVR`BOHxEsc4C#lC2y|>%c=p>bDCZj zgWwUK0JNrS$MkmDRGFBj202A-9>fh^yt<#!a>R+_4i>TNEZEFQ`o6z!oufnC{KNj- zNU}(Y`(lQnV0ulrU3J#=6`!SiN_Yt!tO#~zwZ(P@;Q~t#jNOV%8XG5IRm)>`s%%l$ zcj^q(D@d|4HKUrmI3hF2+C(J#@l+UYraYjE|2QGb1r@YSCPJK=+q=i;qm61153i6+8{m4K4&$@-tXB_B#C>o#;evVdZ&5pq(6DvmCASn z<#mS=ez`p9tc}0|Z>v%7WZ^`sDy821{?ul_IKG)K_F24$zP#7F zg=?DztUOQKL1kG_kFCpQ^xdzK>KAiHiR?^%a*Is>0-pZsW3~ZCp&AXU0%MM63scha zhx6122#VDl22ac?0%qFKNB23{9`9lV{ep?KuU@?5YEDHYgSCRcX4l&wXl~mR)wlFWQcPy$HYVShB_0x`Qj*6E@-NG>S{yW<{j)?_l@2N=cifupPClic})bm9_d^ zS%xET#OT8*nOPvggDS`zL&`?rcogD z-3+AcyK2CBZ-PR21&c-?EBjRiRiKd5-DUEmNApC3rqI6n_6OM*Q~immjGv~JhfR^< z!m=&<|s#qe(&_60DyfZ?UKTC=hqNeX zO=Xc6d3GoNJ7Awlv(lijtYTp&0@n! zCqgCP4^V#EedT4!s zm1wPzO@?=v1%+A{`S(e~FJ{S?NL`1d1>3Y>@*Cz`MaGl@dau1&3I)3;F;Xf}mXdP3Yr!1DxN6~?kgrC<<2 zwOj3|;zC8WR*if!d^-%i7{hsR)iTrlhE!0g1!^lTgn8tTrVhv<%VG}dh80wD=|bjF zbv)I1^tP1TRUmxC$QkIBy^oH{LfKF$_*5yyQWZH{)#ffmt5M4|d=C&O!l6#7DW60n zIxi__2?^2HmNyfW>7V@sk}|>;XCx?if9y`{IWSNoA1+_mUTLS5}mKqNl$AJb#M&<7BT%&BaFR}%A}R=lHqFa z*@4N52O=EHvWLEhz5tsb52&}@_**vvMnLK(YkY20PI7a>>XwzTsuLf2s1MLqcI$t`-V^_!=x94Vyv@occayUqS&AD|) zQh#}&%5`XQ=0`)*qA8c-5Oto|!EcVj??ud{Ty{-)!*_g_JZyt!Qbp)zsFRB09vE13 zXGMa&SUCGiX5m39B5i!sYN4u{Uu3vVB!`uIr2noupF8Q;1_G*lcA?6~y^f{v<-moL zV%cOa>g%BaFQ6VmSUogIM_7j6KwXgEB*U&biM(oS!X-8-_ZS)E&G>v4H+p8I)OB@=Uw=u+I>=#H-Sdp-x+-jQR!1 zg!}fKiBr!a`)@&$zN}GCiHfNjBK!QSia=Z0#h+l*L(lsNBxx7(>rVva8N}`y2>TJN zI;JXf9~MxYaWAyo*-MpWQ9(c*rTw}}*f6p#<|Am9aVLolT*EwB68 zS;+SUl-A8axrS4Ia4{Lm2iM@~^2RO8-qQmnuPn^a?b_+%^?8nr zBU;bo6x?!JsUdAea|_$d-|`prub!^v9LNlwsu=t2GDQb!yzEF}>s&GDK3OTmpPl7c zBVTg7zenoy891!IzsI12sz}>Ptu-zE;07qe##3rsC<=%yw-DybpPnT2Dues>D=bX^AQMp&G8{^cII|FYq=bLQ(p3hzgf*%vsn3)}st`4bX}bkI zwm{AEa67^4L;dg#%LMUgy)4)~u(VcG^K`uyL+-hXr^INKSQt2P7VQ9t(RY?Q;jiqx z`bJqRqV)Biml#U`2MobNTBuU~9`ObB`2yc>ACj`-ta9hzaU+Q4cHy}?Y69* z6q~Rn_};HA=;bX{?_qz(I&z)aYNEgYMXtiAW5w3=8@9l}{j2(h@tiS_R@bl8Ba+tK zW5gK7g1)Nu^aacGjVh<}A1lrqQ}jMj_+QCSvq~!Ewo;xx^4pb#_kv420IhxlrIq_D z;1>NWm!6JzkVo3uBf&k~J$M@eP0YM<7-CsNYF&Cg+W?7vX#JY_M4m<};xjNLdU?=w zb>Qjq8mWxd>iJFXM)S%c0|{eN!d-$EI%P_!%mTuyeLOX{MISr7$eX|`9MSwB9Y7Im zA&lys9TKn+Z19XfBO;_wo3(Jm5A<-gvObKYGI{J`bMxk>h3JYJ;}wPJym zJ<^&R+Z>jny&9t!guS{M<-P9Rwt-(}=4}iDSDFD1>?;7rC{|9^>LhPDD|-4c!Gh3n%v#WYr9rA>?Z! z_*bPE(EI{$OZ)a3LNnS$Qx3`Ijt-b@(`5GrlCcG;Z}8UdmTLirtTfl~M8p~s>g=#BfLwyqvLpYLRMJV9yd7q`BYRE{>cuc6dm~Ln z*n%}HYOIZ{2O1#ImQ(M=Gc@!{0!uT?gu5`neB*0G!BU|oQMkd+_E2h#1rZ88Y)w|K zQMbipIt(A{62v}RbGrVtJH|M5(0h#;sne*MZNe`aCa$+7>n6J!womPBO~EWxWQ zT7_i11-pQ)-=2F6?k<(Lkvzo_9+l6MAj;1jaUJPLPJu&RwR^!UadkgOn*92PLMg^4 z1=&8{8`u5p+YAiksvCsVG0jqTLnm7Nk&r@{>be)b3Lh~yB*4qch~8_U`zY1-$0IDC zSK&lX(O=&^L%8F9CDp8>fT|nR-?xVR9y3f7%&&C_Cf_8`Q;R{(suZBs$7v1Ztp#fF zAeUF*bCLsRY}bDX$6mnJM%7`O3S2);>vS+)>;~Tk=(Mh}ev1gCKs&$dV1+HR(%6XB zsMBb6z&NoWZ~9&+fd_~SDFQ96KK?4P5Ex=p`cQ$YpV5Wdg50m9J;9S5%aIb5LZE)t zNr@;h0v0%i#6xvB-%OsR+rJS<@8r2sNx3gzI&AKm(yj(zG%a|3@>V_;+e}#7#%-6l zI1AmEoLcwjYtxpU<1!MpChht*zAj{Zw-4ESzKO6oo??Xf?Kr-5`!!LT+N_m5%8C-b z-Op@X3#@isHdDUrFG2I&KBvi?T55baG`ie@GpuT{Z{c-xJV+3Mw#{S~WUf-yCKRA4UKX}WnJ!1d?SD>y4Fj9qX zA=O1~P-YpW)_8#rO<=x^T^(6%tckJ~nk2LfS-w}S-0|Xxv}WgADL8Sye4;=@@G$DB zVI4L2kfZwnJ*`YKWDHSdU#u}>(M+kpm|%m87qh}&goog7R$f@skMaGB|Q_ zGxAw$=j}D1l%MW5s)V()Q4;;1nuQ4LB9av)^`S09Sy?Di94%Rk0DXev-m}WW+RCWQ zCJbSZ=os<*%thn7wuZ}e*QIe&*0%g;4xhrzx^)G>;bF?95I`*+`_qSaKwQak$}`pc&whYVU|m!Kp2CvAD3o+gwjF19cnA zpE-|)tZ=KHyZU*I7Uwv*91gb|xgvUHd^Px>Zd7=$Afi4A9CzqE3|?R$k7Qm0HP@a8 zPn_V}Aqmy02X$yLRw%{hPhnFmqayVJ>kK0C4~RwjynoD_`at@6!L+Pj7b`4M#zxOl zcB2Sjg4VTstDP1^X982mYB*XdzDD-XD?tz(VA2ZYX9OOihm907AOQrZ`ez-?vV`Bd z7pz(WTWyG6B)vBY4TH?T9L?WAH&s2)X<9;q{ z&L(Z$f7MnoOr1?f!Df{H1oysDwrrf>KePaT?&jr$QOZLNxn+#%a&2L#MPACly2WtW z1En2z0YqUlUg|aGaN()=eUVh0h>yGES&eCjiK!$6zIAmA?=yGOcwJTo3AnBVQ&k#2 zc{l%QLjZW8j%64O_W?^QO-Wphp{l=Ac1s4*T{^4E@r%rEi@DKMrx6l;gxbYJA(vs= z#FUy6V6e1QQ1&9r~MDB-)rjLInm8nEI#;8WWxGVkvA;_ z15~_vDv#=gPP}1Ur_tR}Vb&(9-H%VS?FV{p8Nyt;arhpzo@O_DkpfQB2io#Oz|0}) z(MkqPatQrOdSsN2O5yD(oK34}tzY&L>1H`Zh{*N4egHRkHyPdIKVLA~&M%(2(6i9z z+;x&nAlzov=u-c+kl7+rRU0e52b*J`{L=&CR zO#og3`fx@XDFV*q_w>GaGQ(xBREbqY8Q@?BsBHLZf`G8)$KSMa1EqIGed@lvk;(;NNxh$8 zc5&tJ>lekZj)S{}3}Y20(<#3+?PgTY!f^wmefZATHj-bz;b|&?xz?vB(aUhdC?`p~ zVq`#{wcRmmEMuO~m-Ie#QkV^>BH8=}!WCZC;UJTMdI%KH(!`E}ixB{af7R|)hlNF< zySGpxr+|N*l>jv)IwV$`vd#_0)|~WDv4U(rE*yl2S^Gd6=^gp@ufpxgZSBfc8*Aqo z7_IcwljZzxJe7ZYp?En-NT39KpRiBWYI0nc(EgA){({Ngj)`zek$?dG;$?}AA2zfv!vK!30&9bezBkdXHxJt(k3Ov zxm5bXLEX+o_Nhz342{fsS~*Y&DPGRsq)iCEeT6i+h752+72B$g!(ZZmLl}v zkV+10p5A_mkag7+=Daa2S=?4!peQ425>llV6@0b!%(#wEUL~z233A-C2nLA_i*A{= zIA-Kdg27cyx%@@sLKSZ9NJFnALs@UV+@5zu?I%@Wi95W!RbFg~JBy2GdTQa{oYaSu zpo9_&rlsvj=MD=zW^E2RiN85g0QDEtQL8b$v!63!8c5hX>&P36?*Wa7%w`h;E!Ec^#Fh;wMb-W1K?N6J#F5q zovv*1?0|l27iHIVnIUkq*QhWqyZ#|y(cKrxuZlrdNtD_1mty5Ai4vDLMV|?FMFNBD z(xZ~4GQ)TmN3ZUEwhMaxq^6Qa6DC!$brlMuQh32<28f^qp#4u6hY&V&gCAPnLSc+D zv1FctdPmQ`sC|=Y)E#EQJWE6e6fPc=AEV;t_L{_I19yJLY9N^+#=1V@RLzNn2eD?G z9Mod7k^A%su{O}()Q{631t!orJR^buF>$B!c{xwWLT}ORb6b4c+>T zJ%sTv78;JI^=UbiB(rq5RAw*;$~6G$g}aAMI> zs(JOAP(yrur!w+|m%Zag2JHO6 zMP}E-sn+HcfIbkkM_@S_lC6!jjM17&b z?NZP@M8#J;vP6+hDF&Eo1Z%Eie{gOGagsZ$X%i!vk|P7-xBm9{E? zska&M%a!W+>N1rS9fG2+OEl>gG&yA@@YCYXBK&JE^fsL9MaJ~%cJy*TmHTpkLa7;9 zZE}O*r=;xbbHlxy-PZmKKsN6cNd;e%i^V_}-qxV%LXdhh)%W5gb1|Z$j12e;6taFj zpz`FiQazgXpmwW{MYv0j_C#RAHK>n3dR+`=lyFyXC@1-xQ&VmwmK$EA z50z67Vc@Iq(7OpEZ~s&H@*rI!PAw!R`GKmcS-I+yIvC;1z}~!IX375F`k5md1+{3jByp+v6I) zGON1#N!V74s-OBb%yRZXvrTtbHyo&^1bsiF)OT06Qj-DvXZJ3mad zZj`qpz1`V#sUAx zC(@87oXHV#wAQ_>qK3Auf_3)OzlHH}X&Np=3F}ws`G&hkQXXK6-hZtEgXh$pZpl(A&S1^G&tFl&?Du{C}20pLA&PtaI#ib z=Ax;VfOA`i|L$}60tcHug>1k7N;k8L$FwHnk1sre)b^ShF{ zelL3E%D<9XXHZu8IW}W!PZmVw^T~u9$Zy6qlO;>xiJq>E@VnC66OaoGv*p&mjYw#F!RhV5 zn3!0WKi78BJu{G3U#q`TjKv*bF<_zBJ}_&_hAY+nQeu8ry6^k=3}01$C|g_8tMTvx zUxm58&vVZF}`JTHRDZY2hkV7Dvb-!7@Z-^Ch$~L9$VClY0Q|Hgyy5*HbTfgV8ApyU= zfR7=h%$@o*%aq_vXzw>~WZKu3(MIwe-JspTHJGa4;?A}Etx@pM^LS7>PagJww5}7> zrSv0m$}>Z=b+SF9|0Ka6M(kb=AcPg*wbA8!kPjYxc`Gr?dnC*b$rql=trb~Hvhr=| zY|!6w5qTn+c^G5hE>X_C`c)$^JH1x(Uf^%|Y{5#Dv$C37{{39igZ!j2V~(}&xq=~D zJ!LL~&nEH`Z3S!$9<|@Q^MsM-V(rS0IvM1$dOxa&uZN%=V-lCQvFRt#qq0OczuV~g z>-~ASL#>=oce?>zXWr5RifP^+*TX36UkNsqj;cAl+n+LP1PX73zK2UaJltm<4^wWv zBO*6!?XckcCF1kTr{}H5x&yK>In6iVEBN@M-}Y$k7S5(IR@bHERvcvS<=$cl+9)GAc=aWV zMP8!( z?p5Zj;K_5Ipth|-9=3*1KcRQ{Nw{(3sJ zdH3=zr*7d|Ul~|@&uoV40x%k(fWpML-O zxe*Yonq5#-$aSZq6$cT3K=!7D4c)s2PX8Dwji>nDx4*obkn%f;j8k7Au~L0*Y1wMC zR{0kXY~jD-feHIZSqEa?Pu{_iAnf6BKLCRHcNn&R)c?Z*3+@9Ff6_@4fkDNJ=TE|v zeHFF68>uq1ct9wF*k)*n9=Ekv>Z+|~Md|loe zFj6MEWUhaEOzq-&I=!-_NSKG~Ehxkz9HI@eG}81x;hD(zAo93#<)w%JT(A53p$po8 zQoaA3+GWe?=iJTwdPmTb^XW+y_(sC&_xrgAcB!XKsdn_R(3;QwA<9&D!6ht!{NW{g z{q3*W&fn&*&!+zl_`3NQ;LC5Dk8!%NMwef>2`4S2i7?^zCewYJMB^YXXeHNSF~9aW z7?sa65y(0E{q%+hVIcP~z^C?#u zpqG)kPs#&#Z_)A%}_FzAP{|NzQnP*>(w-Tj1Xoo({m#BWM7fSRR1v{=-#$mF=an9)?gIspLth)Kz{txFOY( zYhNX?1oZ#a-1BphmpwFkWgIyCyeWvO&TTEc;aGAHUZ%>N{0I6MuqaNI!zhxgh1GBD zv9_g#Oy+wOi~x}g1=Fzp!}QvfI~bdlhvqdPFjj_Ztp0EhvSTAPpChp%@9Gb!Y}rl~ zo2b3?39@sET#{nB`^<13LDv=L89(XNk-hRT-ge#E-)lu`PSj$nFT`Fc8%Z6o+k3Xq zpu{`;55;TOyVgabfefvytJK2x<*NVsn)dvrDT-UrFSfJDeRa zknCQgv6tu0Ecm*7cemRB4V<3RQ#-oQEeKDZc;y^#5kB_|_$6Y5mg zCfQhv!R}5-B?(aJ*3Uti;2YcA!mfAHeh{|)83O2T)x(eT%JKn!}9sJ)YUeNjk6uT4#8#gai2E?l}+I)!@2_+)~4}(jy42v{>Vq03r z+Bw9nCv|eXlK#W-f(@UBzWN%CR2BO_07B84A->Z~}iVGm~8(1s?|} z_Q0yv67%QD-P?xA6uxav) z5NQFjm#Y993`?x_@DB$fF$fcF=!^lg{$tk}ohY&X{P!E7u&Cp3X7F)5WjGN_ zQ5qp`ul7#?aV4Hbl}#Ur!3m_DgJh&AXSDuxjXwh!k)8j>S0ZePRaX1OXz%Y?b6h*usiP& z;+!;Ag(K=E80AOJ4UJ}d?RqljVTj8oRkGUAQFj}$Mh5{p><z@|ouwSr}pZQ0U|R zbYp`R`r!0xG>EtYkb~E;2&`pQ`bEPJj5`SWaKQ`}#P6|~#6BZ1?-aliQNhootTR+3 z*5g)Bss4{-FJ=W*xSeBcD~r2B4~y`LHtG=rb9jl;TZ$5m+b6rra+^ke)KLFz zi3<%bt_O-aSUy?fTW09pyjqmd($PeN^bP}*O*f|Xk~5xTyTSJ}?vR`+ZwP5*L{`iD zdI-_ZIg!Qa7L;9LsjReByK)m7lSj@f()@wYFu=6wnYjW{Z((?8n)J?T;OAW*Mesrt zlDvsWA+tr^95$GPKmSUAybKkct2kp9NsoZRZUAoMMIbn9aPh z>b@V@!%r0ri}FZfB-1Zc@N_c#iTj(h-|1DTmoS8go*!=UTNpJ0G4ocXF36rm(uHwO ztpj7S>Z5Cv_`c9$tMN(eWc<#LT@p)|%!bvnP=>r5vM+;Z=3{FVAnT>+Z*mYDiI z(*T%PxfZl!%k6$mzbr;>u=>)mXJa`Z&pYtIIXZ7G|p%GBiLdk|rUtLHk^m zPqoPd)!J_u1xw5Rypr@MGBhK6!giCf(d7^vPn1B`LMooULh$d}{>Z?zQFb*~l4V7>DCyV-Pg~DpvtX4rKwC z!mKxdIgTj1tr{a|J&M8W9{pYmiZQZrE|BvhMoUVxydeZu<^SM%IsO-}7k~W{_3QC6 z|6ISMb_QN3;Nr7hbao^jGdwo?fl`$-@wVI;yh#Q{{jV@4?8Fhq$_D_X`%=c#0+yXv z(xlO;S8-a9C-f6fjxhENAH8Y*i_FRkcKCqQKs&Q$UPQeuRr2T=n#z#2wLZQi89NYF zmp61)!~`K@?gFk{CEY{*IbG#=Wk1cj7nfxZ4@AW<|>9=}hM%DaR_LA1a z&)AEdeECU&5jdJbclZum(6mql7Lo#LX`7;&_hrBNsWU!U&>+@-Xw||x+a13sj->C| z-NF}(ta=wD5f7DgTXl`l@8`kyGWsYqjj|YFwOrT_0?BJ!To{rHXy5>!8j^krnyH0F z8LY4uF&4+rYsRLVAd!PU7}LkvDI!PSGeTI)RP{~N~( zrW4lQ(t`=#=tiDttvUXj(qFmTY8&s8spS>b&cS?m)&5H8X~6mgfwFV={|9*?{%4RE zZ;#5) zd*J&+tmkvl0Ke_9S6rz5Ft>Zl@5u2jqU#Or?O}@Y`E)6c{vVPT&X&j?RHF8R2AwXw zg*$4s>#sA&B80PpM(!NDwTy$NjBPV4&Z(&n5}x)ezkL` z%<%pagR*OPE;O4O*|J$j6OhAh_5%(--0t|-|Hkry{E6$>QR)HbRpr=X2e{#Wy%0>6 z49gKP>1Y~(i!fcy2yA7^97k`;5>yTPg>(_gnl8Lz+ul4GJULTNwJgu5Nl}12E8Ua} zLr)K1Le=4j04QV~6t2@2RE-mo)Uq}ua5`uR_MqDNA*HQ2Ljw;DW#_1%d2rCFkm(ou zY9Fasi^h}bv1!!;RR0*BeGLlfv#{p6dJ^jA=XmOza3V1!E0I2ungrvv<%nRJI*nyl~82erC?Qc%M#rnM> zy>?N)+)%#iKa=0bQNG>m?cXE-J0+Js{tM#;-649HpexP$3?X`kq-{gc9EjFqHn8h? zJ&#_x&&Kdip>^E9I9{}uP$JzkA-?0YFAN)dxfwsnmHHER`isN3VW~FQ!J{>2cVRpl zv{fK;Xv+QDCWGhuw*4Q7pc#XFh{f;F%^kY|eEye{V5E?c!uIzF(hdAGVZAiA#K60n#25pEKi$RMQE8+e3J%7STuKVp0bBs7GUK$Bi(ygTFasv~)t zcug7pR~@bz-8zgY8y(%j`;Clj1$sLDapT!6d+y0qfwqeOE62;Q;=yAY(f>r+JTG8&7+v6gS7gYOdK6DIx4s@-&QRkU8t%u~2XiOax7O(3@y+s<=lEVz|u! z(!YTJhU(S)pQ&B|5#4qes_hQ*9o{odp`_-A^u;0i6otS0z83fG>cE&+!avHi`|?xbR*Ovb%J7sbX{&2HZtL=?Ysc?r4>+GXnc2xo<_ zMMmC3$o)RaU;)Ju10us4n(&0_5<)+Z(97=Op&KVH&m{L0O<;;)VZJtloQcVRj@2N< zQP1SLng=q)3sUtl%r5|bt*RB&e!4xn{G zh(JpiE0*qrLXH@+uYDCrVDk0wUxY7@eI|M%W(=OvwCPlyKpHa-J9))cRSj)H>#ygXwh`+n z1lraeh3o=BC=+z%jGl&0$P7N@fNKve&9shO6M}su2u8CU_qq)ughKl}6-!%9k%g%_ z4gt~R>*tHi4^}gpC}?bQ5$|o_Jc<<;XypUG#RQVa)Z&f)4c!7uwv65*hD99VAluN@ z#cFTxuJg2!XktM6QPtgpaLoxZB29JOQ${a)YRY-mG|(~l|D2Epw3Byf|Ujj$-F1=ENFtaS)+%)J~sOxNjn>~?BiwJ%RHVJw*cX?gd# zsg!+3oPvMgX^1iX0lIQ^zerpTI^Fdh60EQ+-#joZr|28EX#W>wci9%#mMsc6K=9xe z+}%9{ceg;{PH-oLn1Hs+h-3v%?yG3^IKHYuKdG5I%_yLTXYt1$1kaziX z;{HQb5>>C6GgnF}imwvfrbb__F=)iLXuCUoBaa@1rO4b`w;)yYF*Uz(v7( zLQUC@$*i8gMxtnM2p4c1U1??TM$M2uevEry0g9YiO7BEMix)`LMQ@P}lV>>MkfJ3K z#f#L}0O;*;%{Ylds<&8vVjaf6hv|)z+SAaWrgPD|T|FJ@|M+>jw2mIki#a*uq2u-5 zu=a%S?+oebu~~tg2$fL@6N|N`$Z3O4wp^%&Cy{mGdgA0dtH5 z9-~pzqtR7d=ITe~WF}#GV!4BAz$$>&{N;uRBUT0ZA#)DlFCXYK>2B^8R$HzK=~aV3 z5e%dOWn>6S)Q`>6a%WnDt5lZ7=dJPrIIV2!>S*B1fC^n9qXwg^WP-T7`D4;4>~rPE zTg%xL6os?oKuzS+l*$VKosDp}&y*egebxUKSEf=yjHWs=jrSJc&xdLT7SLF14O;mTFd zu)Qh1GKn?e8dST>QOk_`5rl*aNKk+Cy(Bh04bO_4qfc$_sj~eu%FUU@jd*hk&dfg6 zKRvI*v8MU}tSUr!6_7DD2EwS-kOmDJVo_aGq2oLw&Y`7hJYBj;bs}7}cR(JljuJpb zCHq~S&KC*cOUX6Wvar!a-OW;oCW8#$jBp0pLxv19m!|iwh4?8N2e^Vqa#PZ91;1&P z=~_ssk2q7?Xd4U;+3HUUxV| z6|{6Ivt*&QWDTDGW_d}ZBotZT>eCVd5WhYDykv2-+tMV7sb^U%&gU}8)S$Jy25=0y zd#6GvPV2MVIOndeG9P_jAsvLXQa_Rr`PL1HqXRTt=0{z%e+o!^hb}7G?8(}Y)k8j@ zo3;uMy?{xFSAIfaazz^LOnwpsWO*)2d; zJh)9Nv2}hMW%sOxzY3i`zo2lgv?~WyNEHANSI+i~#yN*DV{Wf@cHwn;7DQCc5p`ZFu8WH?e94ZySj8( zQ!P6C;&-DE9$*T?+TaM_dE+%8X$NKUXs8e{dN^}pUUSJCZSsNFovSZ(bug>=I5krR zHF2g$hMf9n=L}POKsT!@U^=>#LyKEkg=3I{jhd6)`X?OORk~m|wmR$_tOfah97RHB)*G#Ydl@yY+j9J5=6my*%TVdxch_lKvf(kDR zR6POp9WJ^e!hn)8In|t!BbSgITgzR6xx~f&vm?8XkIk=_!mr7tq)Mj|rMm82G)Wrf zI|>v%km!^)JB~jh$b@;w@}(k`Xj}{4Z@oK4=a`k4DVLfP3$%B1k`PwA%#tH0i#9LX za7~;`+9jelijfYutXO%8BCiJ~GYbuce@1MXs?rUiiOJXd96cyA(&md%Qg0(8yZ`-+ z^SzGdY~MO26G8w`k?R6&vg;G3cSnQA=eBmctx#H@Rzq5de`4A;pN1 zYC-;J1cnVMhT`KlqOh$X`J@D(ScWAA$1?J4NFN;q0iw()Ne!fjk$LUClmV2cV$SnM zVa$60e+RR(MCsR#A1s*My7Q}P+bvTZGb+x{9L~+U5Lag;iv=#MB9ON-D(0{I@<=l3a=Q&K&%= zmb2ROn{{f@!u%{uUOz*O$kQo0VA=DC3}-Nb49a1vQ0X`RQ2<>B1@62pqSuM6j;{Pk zjM-UT@lITH;*I4^48E;;GMO}(Zs(&m){svaw^cu>(akV7&bNq%63-Wz3Ijl+F}-hr zl9#7ZLg5B_M(%bTjM|wKPr8n#Senx^jI`=g!+RPg5fbCBRCL(MUVw!qbxL6G+Wm%pFqXd<hB_glmv<&Chtz(SuZ}Rz! zKde`52dnD_(bWm&G6+(s8#yXK8$-()aU(4af3nZZumu&-UkI3w#|1{Ea+`{+bLE3( zlPM!2!VGX0{dwykl)}`1mbwwkv_o7WxGHj(6H>gOVxFa4ASXZ_Pvje2nQca>#mrGY zM~)7Wk1O=`FjQTaAPx1%Eji9XifJBTL@K^1Mh{bMmlax!PHn9(pOw{FTFhCj z*LdNU?=Z{5;ChswB!zb3ze{TvC|8WE$$N?=kQ&2ed_eznMt8y+P^zC=D)k8~{Gcf> zZMvTZ1I5P9h?Fo#UMMMA<}ng~eIyAO~$V2JY<-DgOkZ8!>cT~*4#)UCg+ZnpIJ<|w~FAx4RsA)~zWDLJVq~?ciPk`M86h5Bsx2e(d0HHJDh* znYopNnY|Su%JGFBG&p-Gc;NAVEdxNmcFAJnS%P8>tgJ%l$WUALBcVG4IOQWM#_nT- zZ;}XD;m4DAWizBWL8_`p@>n@z4nI7{p4174-+w}^H5+F>zS6kwJK!J8l7i22qHbaB zWa;Es(>}JD22M8AH|RFF*Z>=0_WHcQ8gZx=TF2Pt+PcPtwpQ;9^_g`X>$3jqdb1r>$P)a z&X7fLYLL!2&Sh$r(S$e5io`Um*_;mW#I#CZgVae7YCm zf>?Uh5##D@$pc&7cZ}0OhhitY+{OdshQ>H3HZ!XqNs2*|2U^(irD_=)<@U)@jAX9j zKYrrFu7N9Y#KFEkHxysjg{>{)5BC%2xTx-sj`{YJ)6{h*sJ1U!u)2(sUitU4rPc(p zOJWN3XjL?(E#-7bnzWvhB3#3eWDu;4;= z*><@>pQ~Nx*3lwm*m?QB!Ejm2N_}svEzT38IZVg%r3p0eZTB#C)4B%YUAJ=uGhHAs z(}n#P)Ab1B75q$u`GVvI@3S81Be*nyAyoWt^d-75!hAe+nk8x1+10Q$8UZGO z?Ck%Z>C&@$N%PKDVVP-#Lgn1*GcS+cjND91%K{<P9){pAj|V%bT#8DG|R-wOf`;Z>PcHXKH!lb-W7>G``9rIH~-Lfy zsgU#>J|$Pj2zG+w{$Xlt3iaVXg)UcHPpd|o)_)3JV4>o_6S_b*$4?!rwoa`UR#|rv zZ*xTLBILWs2tLUUU3o!2zj?;LCA{?@xe;(rUk9nEI?ZW{YLxJ+|iU+q_IZA>TGjvcO{}5#yH|&dXVN z%4xQ1(yhLpK+<(CdPApl>0XHr_lpRHK~TKwPoK1-Fx?Hm<6_dH4HxA@pLGY!tR>NR zu_1kM`?fajk^8#4orQQbC^kYs*h-wvAw- zrLFC}(XQKz8nUWvO*KQ}iI)}=;V7uWr(``~U8NJhbGbd=vGV5OEoxNILuvQhA<^hu z&x;N$@neBsGdXk-9@nX>x$imgi>CV)QX2}RC(|!HKN>bSEx1*{J43kEM8rE>d;+*8 zYJR8%<+o}T)SZ0<;5xrgX?{%1$xxBjGf*W_r(GIPTBHYx`YAN4j&5{=>5r`?bvkIB zon`6xE^++hz8O55a-bnqxy)H$ReBDh!#J7l8^5qUL`ioUGqY>L1#*HjmV**cr26y@t%q1QmI{}97 zxURkZy~J~3g!_g^KxtktgCh_AO9Yfmb%4wXwcwEcq%oqoapA$^m^9j!(e!PeT4Sp^ zP7VpHHKj`QIeP}-MtEk$x_!Zta)~Z|3*R(iJFJ#jN@AEMfJ<(yxh0 zLi-;`Fb{e(7hOmZbc_zu=gkw?nxGANlwz>B3g-~i_-yF6Xr%n<>1z9mzt9U80*u{Y0L`?Y;{T3CgFAKJ!Hc509E*yq8yJO znqLra`(w|NZ$Q2e%P$YvFL&UPqw(!>uSD9T@AF0M%l&?=81{1N!#MA!25MbX49TEO zW?iomC->i497Y%__+S~M{jxvy_Z_?Morr$N`TqFtLk#ansd;)`raBvQ8&oy$&g#*5 zd|J5PaX4RDV>86yR~iv^&D3s%gQbiey(^CvLBlS^%nH1hB)y?Leb!CI^l_!M8o{tO zVW{+;XqjKYLLmqWE;gHoX~0z-?z5KD&XYwPLlAt|EmKUVZ9#oPSb*(=$JPqxOdVBl zoUX9&op-|>rpAOPeHbsFPhB_y_Psz@u)+nnIVtIIdW#&DEU7hMVqmBJV<-D=P*CU( zFNHD3avBkN%JO3C{#2)!7j?sW-NYc;+?V_U{)|+p$%6sWqNHG!HA%xYk{RBmUs~*pC8My7Ylh8)HDc z$5i&JOB6Mk>E2vZJy4fv`O4$&_Tmy8<2%Q@)5hTx=H(RT)9Iq?3vzuXujaT(?2X=& zo7NT3({khU)p3W7>mF|o+07d0p#$XE32Hz6H=}E$gbAax($ar}}G zN7g7#R4hQXR4kB_s@&;HYVB=?QE${R%)Q-aW6FmzY90)!S5Mu%{P5N{=p5x6g z0e`t%*Z<*iIkZ}zw%CAOF7RtzG5hw{J~+a}&!_7Z;F;S`kM0k@En4-KcSV3`>~F9p z*B+ia)@>i=U&*4rcR7&*9qXW*ls__8gG*;)){X6z{#@%o;gF0Y6|DgA11Vzy_cONr z8)9J`E!RewEWi^>J-}__Kp$?1Lv5y(dkA-iq#THC%9^$ z`ja@uY5ol7$L;<%0~-Nw9_OAoZ*_z!nB&(o%6y(ZPAY z6@ii|v2>%;4R$psdswK~$9ZlX_xlNZBP+xnG%<0I%sQ7s? zXChVC%hV{nINM*#)ZvW*{q8$=0^cuQUD}WLSAjzuDW!A7FFV`)0tbt_MWlOisJqOA`blRx+a&1QphSGfQo$k)R>sA8e zJVEX2wl^s$%ezxrP!rsPxboqBdD!}B%|b6}t=YeD@-i`89}+HW!kcPv@J(__C3 zDTYPMktm#M9GCle zmV~fyg*T7=^!rLy5e)^_?mI&lH)S4!dMP>r13vtN16n0|oxmKC2r|ynn*~~H6C~KI zS~sh@#b13yaQK`VMHQB+b+U6owIVtBs23gBN5?izwH5yj&&60>mGhbXnj8VyUvHBx zOquIQHuRlO>|I7snZq)lgcd`PcJiPLWwlMC7yzrfZt+CM#%@kv=|$TrgRLeUFO(AX z8tp~F)XDzGzHakNdR~O<8|Q|`J^mwG8OD4-Oe%LS;&f7TV~CAa%JA&K({}&$;q=w7 z{aAhQ&UxC1k}aq&=+madWU~y@40?F->GXcs8(Die^(jihc{=|~=2~v>wmHTB#i-TA zB&+C|h7gmefAtfTa0(BHOr-G?r#42H&!K0KFxoXPCU(UCE{_{(;cgYz@{6hK*bpBZ zUpQEqZ}sNYVhTB4aI9mD7u5#%0NKIV`XJM%@=}S3j{XGDgm(O;&SJndY}xAv66`vUC`HM$!b`$ z5Cn)2jcwz~jU2I%IOBua73Og}sRwc4Y2MNge_^e45Glmku$M2KEyo^);pQ{sgcZq{ z$VHAQg$BTtaX0~x%QcMb@Q&Eh>mLSZU~L=dJr=4D`~sh??b5ROB!piUV}187ioBFM zO8yvK0{@fIMJ#BWEubsteSd!Ut7tr1(Cgl*Sx?()Z0fS8n`RBu4Y$t6s6HQGouysnoeLTnZ%sk0VU&ZEm+O;AUtXPKmQfxJ1= zGNfAuPwNEVu4$kl#)h~KsfWfTbt6bm6px8%mgV$<}=zbIRh3tdO zf~>9Va5pWvOoOjKkD)Qw6BF>TVRE;dtMh*I=hb#UO16Oa<1JX|vRzKz0dHwwq3g+I zxk9}|=fH8&CSlIQ%5@KHBpe2xZ>1^8kSa;K7cKp|dvlIP;=WV0Cv*(11+;ix`Eq#~ zQr58PRF0m%!C-37lF0fn=caa+PbVf3PazXMnPCG3tN%!+rsNE6!gxdSMp{f*l{JMO zLvo$h@Olwyd}tY#%`Ctv;Fy8Vd^VpAV@^|yBZsCMeg9?!d+>I({t+$+vH77!9e$RDNTG!>ti9DRq?YkeI zoUA1B(pb^azshSE*Gn0qXAi#|n`D+KxZ4L}MNI!v5%su?3$M*2d9T#7CCeMiqyeq; z*2veETz8wgevB|A1F!Ue?|DugJ$A}r8D-fuX$(5&SJfU3jso#GSn4`f<=zOJ9mywn z&~iT){f$Ci?{s&7zfD*GZm{k8Ur1fN%`aQVo34qIb;*yHB4^i0sNa(ewHhA9tqMi8 zt!%*e5!wXOXK(Uvy!@%twV`U+5WjSs5@7`PJ4;szoST|fT6d7tx{(k4itjYN98x1m z+MR~q?g9^=bax{WLZ}*FGb79H&;Bo{i{Jm?z%6!y*U0%k{qh?%nL45{+(wGbITB^= z2R8PLYJI@xg}i8+ofX_FX1O;Zr8v%-dCMmUES=v^#nl^N#-%;cFFj&@ZYM3hsFXX+ zAxQov(wu3wm^I(oR}s)Fvw()<2{oJ+EEOxaH+7You88m^!`|!KX3Bw|F>dvkg}t(W zRygzBx&R*;s9Z6xvpdDf$!kkV-xiXq-R&_)=oJs|AA~yy;y~ZuO86kjBs1{*Wpq&> z{JYWhq6)EG&6BojX2EK?bcqo@1(otoqbr8(MBVP?0%Q@evy)V$yz?8_XLaRu-fr$X4RSz-T0yO{4^-lSJub(63lc(SGHR38~tyXt``t<@Cd@d zb~xcxzdrYNdSlg!EZ*QHK7r4RPa83)|KURL#!Vgk-wNA#&TOS=PPQUMU9DEP6cStu zUvb~Dts|303=!=HoO8uL?+0^EO^iaMix$xu70OnV!5(Y`j|je(N?zmoo*vraK64!ue%X&9;){Cy zz9C%c1nIci^2A$IdirQN4$D*>3*y7|nqR~W&QA7%v2`GV0E}9{KajUrRR0uKC>V(y_V%GN5J=D-2kj&!`f?AGzIo%OYEcn5w+hm2Wi1=#ZvK1uocBee)uPS884*pSHwc^0iXukZSRZah0z&u%>D zYGpx5E1<^ZCzsZBFvxoRdbnu;efIVfPY*k-we(oi>SZcGtUsV5PNjW5ee@Bd8jqzI zV7EP1k{D)>%b?%s<2Hu+jP{FFJFqmX{HU7hX(MZlCy5czI56U8ip$I9ZoNqq?y2s}U0aP`Kfa*A?&ARfS$MRntFg5?k{!OQc(GMN zhpL$u z-d<^=D8`#2gX|sex0&yDeuBy0qE2@=hLLZHb-mld4-m`yn%uLILx5p{;v3#oTaVT& zyY+H%jk2o~vr|~T=iBY;tK;V@tCwqeJMNa%r6-@mrzKAue;Ji#ZN#DjMXMd3C7&O* z)DJtVJ}(v9fd8k~Wg}?7A9us#$*o@384Txahz5#e7&01B7|j(ix!Wjw>1?-?jiKnT z=6}O4q6zpSSY*RB``4&j`6TBTm%@BFY8GNQ)O=5=A<6nt&IE0ZMA=1y%0JN!TkNyGH!>j-z4O$`RWn7$PiWPamJV`>X3yZC{_c6dVt4utag? z1A2)0V|}TwqO{Vdd}UMPjPh^iD@?v=gmwSFtgmBLi;@}LAG)Ei_pjDhFxdJ6E#3T^ z^;J%#5cv=5%je&%FCZpR?T__EXdVHK2@42{3@*m_kZ!kND;F^Z8H)h6z8E2@@D1Tx zQ2w&M$bG@qm*zjMFZoyNOJg$^N(Kc}G#>HvcIxBb)|XudQ3iB5@atLb3}ZYomz9_5 zhw{kZt9RbqIrpE{VN@~S&`;hP$_^W8l!i)9K^%*P3JgA_5K1oE2Ki*^<7_|@KAv6g zvma2^I5+?HS#<>k-rtvwk3Tf5%B-fk$~^l#xID~vURQ$suQWYrd7}+nM7$I=sK(%nhz{&0?Rj#M%w?jj<8d>NV2tTkr{HzlwAB?a zzsY#^qpB$x)AKOi(Qo?jx@qSkkm~t^RYgRk;-_0P&%@ za1r&3IJ1mY7&TaVgovg&vzJjD~?!J=98^I(id>QqLJ+v z9*41K5aViwMXy+YQY#cC!7eE&4}wRGy1??gnL%W%7=1@!Yc!-=@Y36HqgdwNK;VN= z{DHF}``Z9-^kccGf?Ou!z;b?Ld!zS_K=Jr^jm=G1?fTix_5Hn1ZA_&;NWXKa(eCll z%iX*8bng18<%X%<(~+&o z?RiZj=DSj!7kM4ie!P0O%3p=p!dP7>J4v8Vg1IF_ohia$^@G$RYwbN@tfdnYEF8^Z1>T_7d%pnFyNJ6(Fz9 z>C--D=9zT1^*jOI_70!A(X~dq`zv~WL47^FrPIQj*59l1J*s4lnko&Cm#(N2JfQP%!*q4a?v)S8g(>r3(Z!vHc0 z7e)>i8eNwThYFu*`MCkM+j$o}tC`DX2!b;S!3DjraaH9KYJwZI0qEwy&kftccg0jO zcXTavH|z|+O(Ve$0p!{d8a=#XlMYQy{nm~O2`MOg<_iVRu*EYK{}6luU#f%gzL#H; z_@7@oeIK_4eGfp-@Q+VsEl?e<6t#^aXZn8Y{(^pj0b8AJf!3o6o}X1od0tkpeCFqZ zG=Z6MVJEE}9X|SjC;Mj4*Wk3qtxMiR)^(N#_4w%2j1q3#Aj~5Ek2e8}R=O6?ep>o0 zT~G@97S)k5av>sy%5Ee!q*q>96U3!o_Kdi_AJC;e7yZZxyZ(A{NwKrWL;IHD+ z^WMU7Eo!em;zLVo3gynw zhNhF25Teo}T$=`3BeH>2DF(K3F=+z&ub<0kCNN;Y^`}Ag_G)1_oHl!y6;NtDq0)w0 zs$>oFzq+NlZ$)Bg67IL1D8Ipoh^HsFO~1lkt^a_%?#Sm~VXu$oI4wToJWgo`ao9a{ zYJ#t@7xM;eYfi-DuW>=&CX^%J=kxWa$&x;N*rWtiq~>(XooD=W*mL3^zL-9pG3VKK zTD5iT@oTPYcWa9Xm7)Of{UPY)$>a3(>ZPX?LR+ryxdl#J<^i0$<3>ExdCeH@Ufz2& zgTD|uPTQr;s=c&dBAa785KTMZ*2-!cwR;|dZLf2`d~Ex*xB{NV@}ML`*BhaUIQ71h zTcPz< zqlw-bKny1=Lvg@U*vo=Wft@F0Fe}97_$;yTeXHs%6h>L#F&fV$6&_8pjqxb%495l@ zDZ>&vJ*4&@+lvWodzIL7t)sFWM_~oK6@}G4{B3(VhSAU3^gSa5(on@6(uRCZ)681W zF$mUw|AiR!k@>^(6SvBKEN=4RUe!o9ND({jqMaH65gLDR!l!jQ8cs+ZEB>LQ6PyMe z-Uahs+Q&d=(@iuivv{g0kIvsd?dz2{y#K_#zJYNs@6K!QBj+b^{)`P`aZ1Xsr{-0G4HC}*z|wYN4k91H7F}t zRkMT9?#(#>d>y_f4005&*q?{ZgwlkH3*%~9ovlWNb^9WwBPopkD5PD}_Q`Y@vs{SK za082;oADGO(#5?eM8q-HpXHRUI=5mW=_6ShY1(Q?NA(NCwX*PB^+dK++WK3_X`0*S zoO?uYNxt2e&wC|dH!o0+Xj@O{5U2?9Rgot_^D!{s%<|W;>kiPvsf*`YLyApny^Gn| z2)|5f9OF%-2A;{f>xt(w_y}?6V|Vj#)$!5*QfwELUKiui|HFG#zkB7qbRVK={=s|c zm^d&My6U|0UKfAyUR;0kULT@@4dJ*xv^+>#7p7D!x*M-ZTfg#Nu|X1NVBX7WTzD^3 z&pg%i+vRB*q$O7@^Y~Xq>fI6)d_|bdS#d-itZFDV;ZM<9d^jv#B)^eQF785-3bti2 zN&K9^y%KZagNa|{O4*^tVg;lj$^gx20CIy*%_>}m)DTV6JAWDNs#!=Jc_H}^3>bu- zIqp?G*6OjmSE4B1(M>&<1x zv$I;CGJ`Uh~vrvbrjRx<-p)%oXt=Rj&-Qp}A5J)!OI@_g03)_#DKR+0M#58pCYAkHpcbuTKg z?q$SLeerqaRrhlFs__rqYk5-%Zpu8{7+~X7iwM6URho3;MTS?2fjR|`q9G3{2pf)| zu3{up{sAtMdjiJxsIwZ6IX?9?vw#FerzFeNXI$hAxn1Njtj&gj#r~1Hz->)A{SXmp5q^+ z%chhfocMyPiDhlZKAD>bZyw1E4Qu~Fi`ux zAf2nAWwNUX&}gZuf)kZV$Z0Aejaxtram!5PP9 z?q)!c!GV7K$p5B$AwdPde@D~xi=WS%{l=);1UF_no!(tlCzqgitRk6nXuILzNpcqAahQapMl%PhO=?(#~(w zFs4JdiZ?}rxk#$`3O$Y6y@V^mmWfI_*cMUgX`l)UtiC&_I^>T|4I+JKs9>sYn`~%~ zq>VO0bUI^RP?~=w_9Ha?XF#bg3yji7PZI>aQ~ZqEbI23~`27fj5l6Nx#o$SxCcCF0 zBqelwaxp@sy~Pi4z3gI1H{lV}VIlKsuiYzKVGa{E`AYEb?=TICNY(Us|5{l{iv~@F z2|~0TqL|BHC41@Eux{x}R&M>Vd3hnbS|@a_WdV_5{_7Nz=!1H7$9&jW8?x(C5>cnj z15A50^YkFiEh_BO%J+L5LfW-vYxd^17J7dXUJV+55ng()gxB3G;WbAJQgEV1a3oH+ zuTY|)KPzpubXEL4RbF0GGF#Cl?f}hI86c7-Px%A5V#vZfYd(S^ym_7SR-N#}SbHdK znT>?eR9{cIO&vF*%_nbhjs!#obq0&jN%*YEpMm9V34J1&lPU6k+7u}!RQeS;-QqMV zN=d6ipU2p(j9DyxJ)o$MoLPp3%EJ(&733}E(73+)O{dmFf>8dBL(SAO3Y#!1SS&0! zA6m0lVp+~x6}@omv(T0KNt!40NvHJLXoEP-tcgH0az0DVg#DfJ zOo|*|?k6_jhas3s%_5Cxh>t;07pJ$)Tp`}B(G$|?&OFhA6wP5rvAv%MbF(;7!K3bt zqUWn4x(AveA>e&({*&;Uz0*WiY`HCb`CXfpdT=hN5F3?v2rKXeM$^2ayn@)Doc>() zo`2~#&|)Vbi)E}8W^$Mks=8k+-KrOEvkt>yQ2$gc_At%6#n1J**GH#P?UvKw{dtJZcUi%R`pLJ%%bgAM%c`$P+?4ye?f*{_H*KCy;iI-uZb!DQ1STD2vc-j37)o1pXT=UmhJX4@Y41h zfqoGxp<-1O(s``>#E0AVc&^+gddnbFF(j-v5bCD=Ke(w8KdQu5Psof(74A&J&6x32 z?P)0I^U;MT^n>TD{Igz&jmY)m|AiOZmuOD>5kO#&C7tg%x#!%kot##(JU zYGMj%TXW54O)_t%+gufbfiE>MTU|Zl2<^RELk}*SYH-_FWoeZp;>Wu!bLu>|8b0JJ z1u-ZlwNDyAdbZ{Xamlkz)WiiGTz8?*-0TRn(XG+*epWu?L(7$=F{(t4YlttyZ>(SPSpoN_l&}k!c%c|n2 z>s`gEPh_&HFgzTwvBA6-9GLgQW{mo3QC;;9-b|mx$l*Al_mU zqE1W58p_2$#c7sEA;DyGAZ_&%nD>Id2lHMPVBX80#R&>xE^ct>mG?T0SoyM5?E034Z?G9g;?b74@?4G1` z>fIsWV_T>Ew%MVh<)DQgro4!o02@7Lgy44nJyd_DBc$QOm zUxZrBsW}B#qD#YE5Co%W9M+OY<2c}~ZolsvOAZeC(oXo229qJj8mLG9EFo`yh$$H$;D zK)6^&5vgr**5Z)NXvN}r@?r3aY(cEO+^htY1|80>ae(5+g)Pu+29Co)2zsyurICYr zqRT9h46h`DQMj)Y-3blt6MXBDwf&@R%aDl1Ky(4e5-V9l$Rgu?2%N|u>MUF(eltB& z2tY&?Vb`WWS6)C_z*yY6YX{624*k@K5>a2Oo8@VLc$lj`T>t}HOh}M^!a^7W$gAHq zmD_*1@EPRm%tO{)i11|J>3Y&4)~2M0FXO(eGFWz4bU17}SvW!d6#^8}`#^-%=&RnX zqM54!0Q^Q9p1A2b3D9-MX!-n4kW2PIKrVjze)PXVF6@5=x!S=X*Y0%x_AAI`pC1LX z?*@<2?Di1AR%?2jVT5C?*Z;{oKK3gx?0ahttw*SfROnd%p(WHNQP@=~ngl@_8Pr>Z zFP8VM7sOC+N{;Lq)Jg5VMKGQ`e*8?OEC|hN9evNNY z2L*Ar@hitKjG5qr%r^Gj3APUjvFcLHFp{E!v3lz`p|<3)@{(5Mq!&S`A7oO|kRi;V z#hW+p=U;09BKG1-t*5FML&%ODHcWJ)7Q>}*7qBw_!{dthr^jU~E5Y?29@p7l9#`%B zz<+sM^RFHk%wHat>3@1$k*^*X79u0=Umlmyzj|CTO(U-!7hCtJVW>kOd&8ZbrZFU) z@Nows&qhrFp0Nu)ozS~eytZ}emP5wTKOmPybRNauAXn}y$kixgA2e?q0k+Xt!}MU4 zrfU;%Ntp`)?VyAe-`#i`v*wq$qm)BS8xv=)8mbc_3Ckb{fEcJ?&H~&qRzK^?$m#E=C~I0D0XoV5z!c4+R`ll zm&bLThzqQd@rc*1$g40=p<$&Bd~f;kuO1gl#LmEuA>BtZ)}vB{0gUAu@C+vTQj9IK z7Tj7_ot8dn((>+09XP8b_4n&pqqOMs2NCF!Yo;AkJuPO}!!&YbJ!0kWEGKkJO8)Y= zj5l>{yw~7mjdH*`$(AG%&h?e4`Eug(FKBggD0<07Vtn;GArwwBC*Ez+tcFxl(oah=} zi{HNM)`86SvXLxLF5=DDdN=XsIMn+Vw$rsP#?eWHe{)>yV2(@nUpTJxzd0_Azc{Y)SB?udiE(tY=wCRlwl5Xh zBeDO_xX8wcUO6sovnqcjdc)BjBEz)s71o{BI3Ot^m9Kd zt3Gmya;!723I8f@zHH5br%*iMq64yrAOpz4qp6dLEuc@t3lf~Jo&{ z^cA|&Vu35Vot^^~-B%TkD5i}WF5QCXZoa^;ll7?Xe=IVjp8|(V7CD11_q?@aRpxB`)`s5UnwLqvP0yZrr($)t zK>%vq&lkFrc(6UfmDhApV4!`@45atOI+*o!1=O^1{yU0!{k*^PykGtCIcgo-5u_Hk zm$$V_hJu$Fd=w<{Hh7UTv5&b0 z{_?muM;)EAaEK(kz`=`*$!}9V<8AP!*;?m1!8PmLzKlqDog!o-$llgnyQbUrv*%&{ z)#9r1T=@a_x5X7H(foG!4{W0F%)DI9wD#jPbiz( zZ>+6=%zEIl_IElY7DB}a#^442(FV(s`b729GoPREt-l`lP_aF#cHRx+B2ic=QbuUS z>gkfS+`A!*mX>{kW{ljfMDkh~KmYc_I=NkKzDHK~=qCRur+U&Pak zIr7b68i3n4$$A#373iTA%4w%+ZnSGmdI+wF=_s<$9IMVqfG>ko@2iAlf~>|+t1X<4 zFLbP~3!q!O`6)(yr%U}rRO3qK!5GyU=86=^O5j8Ur>t8an32-75awbKSbvXf3Zf+( zpm)UA32+_AT8;BMO0(fEk8sA6|Me6&cdKi>y9ZHA-AO#3ttcn!E7AM{<@><<-1>wn ztDSd5{M35;5bH90Z2t0N-RC!Rr*FQsVhecQKp7j)OGFWMo1Vk6y`A3LkmS7%A~3o% zdW*i97)KRtNk`@{fy?(2;S%%ZRp1JgA`-4C`6FFDoLqceY7@1~0N zU8gTn}gxEO2eOVMmx(vn_(ZkiY^LKD5jF z-vSr*yAX}+MdPqx z1IQHsV#N(y7l2=l*|X-tQRAp(o8{kESYge&bKfx)T_LE8x3G8~n_EASwONbC##FP? zI)Qmzy+&*zm=LnS7%5-NukieL#C}-kG(Kx7=e+ zCf-)&!Kew9ZF5Muaw8RJ$t(&$%P~qP>+ZM>}B;)IHL$OXMoMwqk*o{SZ;z#X$hk2B3+KSam$1AWHocLDEPNqdj_wN?YY z2iZTkTJxmgSp_t|baq|(sc(7z)VJK~FJ1M%H*t&$#LzB3^B;Zd64L-6y=~PF2i-$F zMWM&OJ9rPEUl-r;0jKbv9Iu{CZMpcWWjvs5KO*po`{%^{kGc{afeJDU8&HSk?rN5Z zk*$cLbp7kKKNzy2Hd0e5$WmzVw>NdE`Fm}YNc!m)>0QQQIx-9@&pzx_B|R9l|1l5W zEPRg+&;`1bn;j)8Z%C^*Kn^mB{0wOzQ{H&Z! zg@el8rapL>5Fr)Pt$a(qf3$+|sEtXa9qG)wh8TPU!zV(j1w1wb^n~TR6GjR;WluR2g%faa2 zfV*{ZiTL}v{FEJoZs~S#*Me8DeaVIp{V}lY`dg)U4MGOie?Bc7JFi zRZv>gZ8NPkF`ut+^9$p5TTEYFL{h44%9@s+hoO!Y2^eQG5we1fw-}#3S<^2DJKv+0 z3FOw z`nIjTUVdLUZv+f3|6c~z=AwRoI32rWegGw5A|k*PUctWe;#rQ{AD{)}nA1$hTtBnK zBN@%@nQvZ0I?Fk)^OuXjU8Bl5C8(Mb<#UeK*LJ1h_)rvVnWTGa>|{A^bi|opjh1ev zuR-I?`ia7f1gTof>LZ2AVcZ6;1b;uT$UYI4VX^0Cjj$A;SiicNJ=gC;Z-09!-h=PN z;5V1ra@lI4Ea|il=v^5aliadUHX$6gtt+)Oa%~~$_YXkiO7_U(mmq%y!}Wdq1IxoJ zA5+ke6*i{bGB|1G6*DM_6KQk1?_m_N;mOLioZQQ$oX39zLBt^r!Qi2ImpUW1KYEnc z=3mw~%50FOaJi$d1au3Zxg@K0E}(Gf9w}U>s`ZZ)E(4w;g)8Nk!sR){_fz50TgnQ5 zfATsrj5IH~T9h1k03n*Mod6VP;^=(j$YZcq$4+uFv-z1XitbM8;+cGe7V|dQS#_E@ z8Ztn5vLt=Q=0)@H3VTGkVv<;I*SHq5!H`uY&?REV+Yuufm&gRp#xGcJc^%0X@`LQt zHOIAXiiLhl3CfzXqrGsd=^UPCNQBJ8cwJy|+7XYP%+)2jOvk+RkIOjgyp_f>I#eOm zH;bZ34lAYaCWkSel}Hy;Txoc_M%?L=k92y`J5u!>NYiwqck7W8*aEny0r7`<{>i|j zw<>e3FmPJSDQu2sQ4wQiOeHZ`44l=)Vi*0*?>HmThb5Bh!s5b^NbeM((&LoytJZ0S zoaL2PD=x9^sFB2>I0gg+7SJloK6ynbJ4?n?5}de3OO-5X67%|v$Dq|K5#wr58y}r| zUQW`6SJ}!Ex7kmN(44z#ZT%`FiRDPY-ecd6^3eMtxJ47a{BsmbiN}V6FF`IDlCI ztYyOQ`rv=SS`LW9TPBMq(n&hPCW}v|LKNi<=8t-3{CACr3Hsbf)0+W2Znomh+`gl(Biesm2$bJ*UG>c)`4CasK)354aA{u|emL>V@g&HZdcI z5k&!*fRmw#>OQ0yMr$&abFV312(n)`;;LHf=A!mt$)6?Wa3HgLnW#5VWNgMYwbRGLpT$uiGY6(R*(@qI{q-GqobN<1GymAlpr(-qaELj^;C8dNlRkh;9`MGSn<)-zzK8V;dJp z$u~>TGwgK)4U6o7WZggCRp@mzOIHQ zaiLc>rp_32BJ3X4fp>{3T2|x!qj&9IRYRL)8x}im?x@;~_tW;4a*24rdo=H;FX99` z2YUC*lpBTo7L4Oram1|l<4PfB;*)E@m%`ObCoZ9@x55M>Gd!A<*zN1Y?eNI=M z`xurOudyw9^?|mfGy9WVm4qUWpsMNQ$|5`b9L1y7X`f{CUMgAw+TW1>%B@HqhHnN;J4%5*;h+Af(+cD?wWP(wg zLm|jvcXSZB;LWP+LkOb4gXfrZxJ8sXVv~;-HI+o&AK4!j-6{f+`dO{3DvVyg5`X(r zYea!09CLjBB&Z(I0oxByxNNhDULfYj-kJV`*7Eh>yqTwrCz*ZxY1&J z_M`XIedE%q{$^v(grv(i!2A=O5^d6&%$x@Vx}*ia8D_71#}W%)SzHvuz_ zNSe(bskkP`~&5hhu2sh_7X_VrCALc?dATH9zX zNSx(X$*>S#f_R*uw9b%zQ2FPy7r|XjGQ)V*aCbyhc16SlCf^}!hAc9s-nQ`U6S-&b za{LW^Ion#}U=g_^-a#}WOv6Fa_bv@|7KqDNYt5j5Sal@(x_Pq-5juhs`#mdhgIBDz zl^cVXQ|okM6nH_6W0Om(?rH~?UlTy?KS?d^zeug3ow=s`j=t2`m$8@tEX24(vF(&&9x#=$H zTyaMfJ7xZ0umu6=mf1hiEjg0OeY{+r+ShZ>)POFepE--np>$WZnkhXmDB4L2)$)Bi zjPr{Vb3Jlfgva@;Xf4#qCs#>Bq8e z3!9GIZ$)S&R9w4aGoS6!$#%AR>oW4){d>W_BqQ!P&l`F?-^I2BK}bg*A>dumycR4B z@5TUWHaBq-+U~1=7*q$)SvJ(4v0Z#d81o5}LtxOV2P?yS2V<3|%GBAVNGRj$W~*<)q;^~9TA4BhANebv&ubZh!Takk)(OS{m$jcM|WVcKK zc5C!rvyW6&)DUWgQwt!8IAGyjbYEyy0v(kPVYpA%Vb0wjGDl%7)Jb4ZV`(@Xe!qM8 zi{0XJneN3flK#$a)&IqAX_?NgCN@H#c5CDx?A8v*Af@jaN0?qa4KNJ#Os`hz-i`P_M|;gv literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/colors.png b/libs/image/tests/reference/colors.png new file mode 100644 index 0000000000000000000000000000000000000000..0ecaeae3086529f43fbc8413b9a8f36f16178a33 GIT binary patch literal 27844 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 a{?Ye7UOlz&ovYW90Um2TxsTO=h5sL^0WJOj literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/depths.png b/libs/image/tests/reference/depths.png new file mode 100644 index 0000000000000000000000000000000000000000..d229d257cb9bf1f0757689b3e3905e574374dba8 GIT binary patch literal 1934 zcmZuydoMZ3f1(uY(KY#gNjDYr4hFgB53iZTX*)pj(?u#Ip_Pw_w#%|mv5$zx0{->t}+M& zQuA>{7Voe99Rlm;TGqsr(QS=rg2;tFv#y{kaDmZv!uN zqP(k-Lds9suC6KiPCvz2h6Y!W zo?>rA6x82c4{#?sDu7Hui1y)hky*DI}6w)8fLyE0dxv zNDRcm!6BNGmbRPL+1aU0=WsY;Az;{iTj_b?E10U9nje@zAXI+$*rJ*Hx@l!zvb?N4 zIWf^6xf7|Fe=3Xfrxu`K`qS(tb4-o-K8!+~7&13KGy!SMqakC4>X4n(b2+VJ;?B?nuoRlP?+ zTZ-Lh4}?=w46tNgzyV+i8gQmczg}rcN%q)RX)6+m4zl_J(q;U#Cl<@XEPsxRiwplU z$g3>iyJ;|e&zx9Xg{iBn%N|4JtV4~g(9)uciV6T`(vW`|xMz!nB$JJ06y5!QJmW2Q z&ZP+f32Sr1Y!uIIxs|A8N6G(o zz}w9*kuIrdZ*R|w-;I-=X(kUg_;euzjKHll*Ir&I?n$?^P(=8XRk10aBw`Z2_*;|z ztCo*YjOzxL*E2r{TbPutPW@!aSk=Y#mApC@8ntiPK;DCkZxk4t^ zFpEtm`+rH^FdGRLeIqEs8^Fa@PCcKS1)mYP7br`U^12U-*QwW-*yH_hk^S>S{X* zC(=FxFa?cbwok&C-1qv@`SZ@uKKZso4(EyfVFr5?C6!|rsBAVHrK6*BH0z@dqOA=a zV6dZEbwhX=svJ~oAEpaYdm=!nQ`Qe2TL~PX@f-;sA}CCzIW3}h=8M9{dpXl^cZd2I z0%7DY699mxL?6NerMT_>l#u^Gxxexi(Ak-t<>lptIvWsh3>X?J9AS_z&harMIUAM! z>kS>eB!+=13OrzEXJ-YhuGWKXEG%Dt=wED~j?x}Y+kU=xwFtik^T^_=0+y&aFxh)hf_k&xkZ`z+YVF|RgrH?R>r7?a~ zZnV>rx4%UNF)mWM4PA;Ihc(U5#<7Ehme$t3u=?Sr{_TTYLrm~(KBv^LsAx=<&NHO< z@86?%jxV!h)lZ&G)nqE8KPB`w`}aj!<_)L^Mn!rZDJ?BUWiGeAT9PLYKK1v)>fPvD zU_i|SZ(^-f=fBbW;Nli>g%zjqM(U@frG>i89?nbUlpc<34^AQu?7R^WyWemhS6nl_ z#m@}6#n0VZIy;q|*?eAlC^q8PWVh&@e7Oa}Zvk!RTFyXa3l`*o^meU8gx>rwu!ob% literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/grays.png b/libs/image/tests/reference/grays.png new file mode 100644 index 0000000000000000000000000000000000000000..d008939e61c905611307f1592330e5250f509b60 GIT binary patch literal 3739 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 FV1kgt1GE4D literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/imageops.png b/libs/image/tests/reference/imageops.png new file mode 100644 index 0000000000000000000000000000000000000000..26ad02d78c9703c3792f99b8945ce643f7c0bf33 GIT binary patch literal 1059 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4kn<;H+e}VAjOjI=U@UJh#rg!t@i%_Y?B3 zm&Y;uvlevnX%P@sJmSJ(I8e-u>2s@E!>gY~DbJ0~|B&`1$aw$M*#b#QEi*V2)tovy z2ax&uPp@QltmLPiwW+6m)}8tH&*9yiAFcbZ`wm=aAw|>1z1MzL9ed7vEh_u1b+TRA zw_Z*0wmI+H?^u51cD^&v(F$?s?dA6#T)nW>&1(0(2X~Js&-od@lan^Fdc*jW?(voD zEzg;JnR;Pjbnl<_Gz^03w0*HwU;aLrnKtjindw#T`?B7s(I~y{=k8Do@pR!_1zopr0BnL!WB>pF literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/normals.png b/libs/image/tests/reference/normals.png new file mode 100644 index 0000000000000000000000000000000000000000..e53250f41817aa2713a235aff3b6e575b242461c GIT binary patch literal 2490 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 literal 0 HcmV?d00001 diff --git a/libs/image/tests/test_image.cpp b/libs/image/tests/test_image.cpp new file mode 100644 index 0000000000..04a0237dec --- /dev/null +++ b/libs/image/tests/test_image.cpp @@ -0,0 +1,489 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include + +using std::istringstream; +using std::string; +using math::float3; +using std::swap; + +using namespace image; + +class ImageTest : public testing::Test {}; + +enum class ComparisonMode { + SKIP, + COMPARE, + UPDATE, +}; + +static ComparisonMode g_comparisonMode; +static utils::Path g_comparisonPath; + +// Just for fun, define a tiny Ray-Sphere intersector, which we'll use to generate a reasonable +// normal map for testing purposes. +struct Ray { float3 orig, dir; }; +struct Sphere { float3 center; float radius2; }; +static bool intersect(Ray ray, Sphere sphere, float* t); + +// Creates a "size x size" normal map that looks like a hemisphere embedded in a plane. +static LinearImage createNormalMap(uint32_t size); + +// Creates a "size x size" height map that looks like a hemisphere embedded in a plane. +static LinearImage createDepthMap(uint32_t size); + +// Creates a tiny monochrome image from a pattern string. +static LinearImage createGrayFromAscii(const string& pattern); + +// Creates a tiny RGB image from a pattern string. +static LinearImage createColorFromAscii(const string& pattern); + +// Saves an image to disk or does a load-and-compare, depending on g_comparisonMode. +static void updateOrCompare(const LinearImage& limg, const utils::Path& fname); + +// Subtracts two images, does an abs(), then normalizes such that min/max transform to 0/1. +static LinearImage diffImages(const LinearImage& a, const LinearImage& b); + +TEST_F(ImageTest, LuminanceFilters) { // NOLINT + auto tiny = createGrayFromAscii("000 010 000"); + ASSERT_EQ(tiny.getWidth(), 3); + ASSERT_EQ(tiny.getHeight(), 3); + auto src = transpose(createGrayFromAscii("01 23 45")); + auto ref = createGrayFromAscii("024 135"); + ASSERT_EQ(src.getWidth(), 3); + ASSERT_EQ(src.getHeight(), 2); + for (int i = 0; i < 6; i++) { + EXPECT_FLOAT_EQ(src.getPixelRef()[i], ref.getPixelRef()[i]); + } + auto row = createGrayFromAscii("010"); + auto mag1 = resampleImage(row, 6, 1, Filter::HERMITE); + ASSERT_EQ(mag1.getWidth(), 6); + ASSERT_EQ(mag1.getHeight(), 1); + auto mag2 = resampleImage(row, 7, 2, Filter::HERMITE); + ASSERT_EQ(mag2.getWidth(), 7); + ASSERT_EQ(mag2.getHeight(), 2); + auto box = resampleImage(tiny, 6, 6, Filter::BOX); + auto nearest = resampleImage(tiny, 6, 6, Filter::NEAREST); + auto ref3 = createGrayFromAscii("000000 000000 001100 001100 000000 000000"); + for (int i = 0; i < 36; i++) { + EXPECT_FLOAT_EQ(box.getPixelRef()[i], ref3.getPixelRef()[i]); + EXPECT_FLOAT_EQ(nearest.getPixelRef()[i], ref3.getPixelRef()[i]); + } + auto grays0 = resampleImage(tiny, 100, 100, Filter::GAUSSIAN_SCALARS); + auto mag3 = transpose(resampleImage(tiny, 32, 8, Filter::GAUSSIAN_SCALARS)); + auto grays1 = resampleImage(mag3, 100, 100, Filter::NEAREST); + updateOrCompare(horizontalStack({grays0, grays1}), "grays.png"); +} + +TEST_F(ImageTest, ColorFilters) { // NOLINT + // Test color space with a classic RED => GREEN color gradient. + LinearImage color1 = createColorFromAscii("12"); + auto color2 = resampleImage(color1, 100, 100, Filter::NEAREST); + auto color3 = resampleImage(color1, 100, 100, Filter::GAUSSIAN_SCALARS); + auto color4 = resampleImage(color1, 100, 100, Filter::LANCZOS); + auto color5 = diffImages(color3, color4); + + // Try enlarging a 5x5 image using MITCHELL and LANCZOS filters. + LinearImage color6 = createColorFromAscii("44444 41014 40704 41014 44444"); + auto color6b = resampleImage(color6, 100, 100, Filter::NEAREST); + auto color7 = resampleImage(color6, 100, 100, Filter::MITCHELL); + auto color8 = resampleImage(color6, 100, 100, Filter::LANCZOS); + auto color9 = resampleImage(color6, 100, 100, Filter::GAUSSIAN_SCALARS); + + // Minification tests. Each of these do a nearest magnification afterwards for visualization + // purposes. + auto magnify = [](LinearImage img) { + return resampleImage(img, 100, 100, Filter::NEAREST); + }; + auto colora = magnify(resampleImage(color9, 3, 3, Filter::NEAREST)); + auto colorb = magnify(resampleImage(color9, 1, 1, Filter::NEAREST)); + auto colorc = magnify(resampleImage(color9, 3, 3, Filter::BOX)); + auto colord = magnify(resampleImage(color9, 1, 1, Filter::BOX)); + + auto colors0 = horizontalStack({color2, color3, color4, color5}); + auto colors1 = horizontalStack({color6b, color7, color8, color9}); + auto colors2 = horizontalStack({colora, colorb, colorc, colord}); + auto colors = verticalStack({colors0, colors1, colors2}); + + // Even more minification tests.... + auto colore = magnify(resampleImage(colors, 5, 5, Filter::DEFAULT)); + auto colorf = magnify(resampleImage(colors, 50, 50, Filter::DEFAULT)); + auto colorg = magnify(resampleImage(colors, 5, 5, Filter::HERMITE)); + auto colorh = magnify(resampleImage(colors, 50, 50, Filter::HERMITE)); + auto colori = horizontalStack({colore, colorf, colorg, colorh}); + colors = verticalStack({colors, colori}); + updateOrCompare(colors, "colors.png"); + ASSERT_EQ(colors.getWidth(), 400); + ASSERT_EQ(colors.getHeight(), 400); + + // Test radius multiplier (blurring). + ImageSampler sampler; + sampler.horizontalFilter = sampler.verticalFilter = Filter::GAUSSIAN_SCALARS; + sampler.filterRadiusMultiplier = 1; + auto blurred0 = resampleImage(color6b, 100, 100, sampler); + sampler.filterRadiusMultiplier = 10; + auto blurred1 = resampleImage(color6b, 100, 100, sampler); + sampler.filterRadiusMultiplier = 20; + auto blurred2 = resampleImage(color6b, 100, 100, sampler); + auto blurred3 = resampleImage(color6b, 101, 100, sampler); + auto blurred4 = resampleImage(color6b, 99, 100, sampler); + auto blurred = horizontalStack({blurred0, blurred1, blurred2, blurred3, blurred4}); + + // Test extraction via sourceRegion and subsequent blurring. + sampler.sourceRegion = {0, 0.25f, 0.25f, 0.5f}; + sampler.filterRadiusMultiplier = 1; + auto region0 = resampleImage(colors, 100, 100, sampler); + sampler.filterRadiusMultiplier = 10; + auto region1 = resampleImage(colors, 100, 100, sampler); + sampler.filterRadiusMultiplier = 20; + auto region2 = resampleImage(colors, 100, 100, sampler); + auto region3 = resampleImage(colors, 101, 100, sampler); + auto region4 = resampleImage(colors, 99, 100, sampler); + auto region = horizontalStack({region0, region1, region2, region3, region4}); + blurred = verticalStack({blurred, region}); + updateOrCompare(blurred, "blurred.png"); + + // Sample the reddish-white pixel in the post-blurred image. + SingleSample result; + computeSingleSample(colors, 0.375, 0.375, &result); + auto red = int(result[0] * 255.0f); + auto grn = int(result[1] * 255.0f); + auto blu = int(result[2] * 255.0f); + ASSERT_EQ(red, 204); + ASSERT_EQ(grn, 200); + ASSERT_EQ(blu, 200); +} + +TEST_F(ImageTest, VectorFilters) { // NOLINT + auto toColors = vectorsToColors; + auto normals = createNormalMap(1024); + auto wrong = resampleImage(toColors(normals), 16, 16, Filter::GAUSSIAN_SCALARS); + auto right = toColors(resampleImage(normals, 16, 16, Filter::GAUSSIAN_NORMALS)); + auto diff = diffImages(wrong, right); + auto atlas = horizontalStack({wrong, right, diff}); + atlas = resampleImage(atlas, 300, 100, Filter::NEAREST); + updateOrCompare(atlas, "normals.png"); +} + +TEST_F(ImageTest, DepthFilters) { // NOLINT + auto depths = createDepthMap(1024); + auto wrong = resampleImage(depths, 16, 16, Filter::GAUSSIAN_SCALARS); + auto right = resampleImage(depths, 16, 16, Filter::MINIMUM); + auto diff = diffImages(wrong, right); + auto atlas = horizontalStack({wrong, right, diff}); + atlas = resampleImage(atlas, 300, 100, Filter::NEAREST); + updateOrCompare(atlas, "depths.png"); +} + +TEST_F(ImageTest, ImageOps) { // NOLINT + auto finalize = [] (LinearImage image) { + return resampleImage(image, 100, 100, Filter::NEAREST); + }; + LinearImage x22 = [finalize] () { + auto original = createColorFromAscii("12 34"); + auto hflipped = finalize(horizontalFlip(original)); + auto vflipped = finalize(verticalFlip(original)); + return horizontalStack({finalize(original), hflipped, vflipped}); + }(); + LinearImage x23 = [finalize] () { + auto original = createColorFromAscii("123 456"); + auto hflipped = finalize(horizontalFlip(original)); + auto vflipped = finalize(verticalFlip(original)); + return horizontalStack({finalize(original), hflipped, vflipped}); + }(); + LinearImage x32 = [finalize] () { + auto original = createColorFromAscii("12 34 56"); + auto hflipped = finalize(horizontalFlip(original)); + auto vflipped = finalize(verticalFlip(original)); + return horizontalStack({finalize(original), hflipped, vflipped}); + }(); + auto atlas = verticalStack({x22, x23, x32}); + updateOrCompare(atlas, "imageops.png"); +} + +static void printUsage(const char* name) { + std::string exec_name(utils::Path(name).getName()); + std::string usage( + "TEST is a unit test runner for the Filament image library\n" + "Usages:\n" + " TEST compare [gtest options]\n" + " TEST update [gtest options]\n" + " TEST [gtest options]\n" + "\n"); + const std::string from("TEST"); + for (size_t pos = usage.find(from); pos != std::string::npos; pos = usage.find(from, pos)) { + usage.replace(pos, from.length(), exec_name); + } + printf("%s", usage.c_str()); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + if (argc < 2) { + std::cerr << "\nWARNING: No path provided, skipping reference image comparison.\n\n"; + g_comparisonMode = ComparisonMode::SKIP; + return RUN_ALL_TESTS(); + } + const string cmd = argv[1]; + if (cmd == "help") { + printUsage(argv[0]); + return 0; + } + if (cmd == "compare" || cmd == "update") { + if (argc != 3) { + printUsage(argv[0]); + return 1; + } + g_comparisonPath = argv[2]; + } + if (cmd == "compare") { + g_comparisonMode = ComparisonMode::COMPARE; + return RUN_ALL_TESTS(); + } + if (cmd == "update") { + g_comparisonMode = ComparisonMode::UPDATE; + return RUN_ALL_TESTS(); + } + printUsage(argv[0]); + return 1; +} + +static LinearImage createNormalMap(uint32_t size) { + LinearImage result(size, size, 3); + auto vectors = (float3*) result.getPixelRef(); + const float invsize = 1.0f / size; + const Sphere sphere { + .center = float3(0.5, 0.5, 0.0), + .radius2 = 0.15 + }; + for (uint32_t n = 0; n < size * size; ++n) { + const uint32_t row = n / size, col = n % size; + const Ray ray { + .orig = { (col + 0.5f) * invsize, 1.0f - (row + 0.5f) * invsize, 1 }, + .dir = {0, 0, -1} + }; + float t; + bool isect = intersect(ray, sphere, &t); + if (isect) { + float3 p = ray.orig + t * ray.dir; + vectors[n] = normalize(p - sphere.center); + } else { + vectors[n] = {0, 0, 1}; + } + } + return result; +} + +static LinearImage createDepthMap(uint32_t size) { + LinearImage result(size, size, 1); + auto depths = result.getPixelRef(); + const float invsize = 1.0f / size; + const Sphere sphere { + .center = float3(0.5, 0.5, 0.0), + .radius2 = 0.15 + }; + for (uint32_t n = 0; n < size * size; ++n) { + const uint32_t row = n / size, col = n % size; + const Ray ray { + .orig = { (col + 0.5f) * invsize, 1.0f - (row + 0.5f) * invsize, 1 }, + .dir = {0, 0, -1} + }; + float t; + bool isect = intersect(ray, sphere, &t); + if (isect) { + float3 p = ray.orig + t * ray.dir; + depths[n] = p.z; + } else { + depths[n] = 1; + } + } + return result; +} + +static LinearImage createGrayFromAscii(const string& pattern) { + uint32_t width = 0; + uint32_t height = 0; + string row; + + // Compute the required size. + for (istringstream istream(pattern); istream >> row; ++height) { + width = (uint32_t) row.size(); + } + + // Allocate the sequence of pixels. + LinearImage result(width, height, 1); + + // Fill in the pixel data. + istringstream istream(pattern); + float* seq = result.getPixelRef(); + for (int i = 0; istream >> row;) { + for (char c : row) { + seq[i++] = c - '0'; + } + } + return result; +} + +static LinearImage createColorFromAscii(const string& pattern) { + uint32_t width = 0; + uint32_t height = 0; + string row; + + // Compute the required size. + for (istringstream istream(pattern); istream >> row; ++height) { + width = (uint32_t) row.size(); + } + + // Allocate the sequence of pixels. + LinearImage result(width, height, 3); + + // Fill in the pixel data. + istringstream istream(pattern); + float* seq = result.getPixelRef(); + for (int i = 0; istream >> row;) { + for (char c : row) { + uint32_t val = c - (uint32_t)('0'); + seq[i++] = (val >> 0u) & 1u; + seq[i++] = (val >> 1u) & 1u; + seq[i++] = (val >> 2u) & 1u; + auto col = (float3*) (seq + i - 3); + *col = sRGBToLinear(*col); + } + } + return result; +} + +static void updateOrCompare(const LinearImage& limg, const utils::Path& fname) { + if (g_comparisonMode == ComparisonMode::SKIP) { + return; + } + + // Regenerate the PNG file at the given path. + // The encoder isn't yet robust for 1-channel data yet, we expand L to RGB. + if (g_comparisonMode == ComparisonMode::UPDATE) { + std::ofstream out(g_comparisonPath + fname, std::ios::binary | std::ios::trunc); + auto format = ImageEncoder::Format::PNG_LINEAR; + const size_t width = limg.getWidth(), height = limg.getHeight(), nchan = 3; + const size_t bpp = nchan * sizeof(float), bpr = width * bpp, nbytes = bpr * height; + std::unique_ptr data(new uint8_t[nbytes]); + if (nchan == 3) { + memcpy(data.get(), limg.getPixelRef(), nbytes); + Image im(std::move(data), width, height, bpr, bpp, nchan); + ImageEncoder::encode(out, format, im, "", fname); + } else if (nchan == 1) { + auto limg2 = combineChannels({limg, limg, limg}); + memcpy(data.get(), limg2.getPixelRef(), nbytes); + Image im(std::move(data), width, height, bpr, bpp, nchan); + ImageEncoder::encode(out, format, im, "", fname); + } else { + ASSERT_PRECONDITION(false, "This test only supports 3-channel and 1-channel images."); + } + return; + } + + // Load the PNG file at the given path. + const string fullpath = g_comparisonPath + fname; + std::ifstream in(fullpath, std::ios::binary); + ASSERT_PRECONDITION(in, "Unable to open: %s", fullpath.c_str()); + Image img = ImageDecoder::decode(in, g_comparisonPath + fname, + ImageDecoder::ColorSpace::LINEAR); + const size_t width = img.getWidth(), height = img.getHeight(), nchan = img.getChannelsCount(); + ASSERT_PRECONDITION(nchan == 3, "This loaded file must be a 3-channel image."); + + // To keep things simple we always store rthe "expected" image in 3-channel format, so here we + // expand the "actual" image from L to RGB. + LinearImage actual; + if (limg.getChannels() == 1) { + actual = combineChannels({limg, limg, limg}); + } else { + actual = limg; + } + + // Perform a simple comparison of the two images with 0 threshold. + LinearImage expected(width, height, 3); + memcpy(expected.getPixelRef(), img.getData(), width * height * sizeof(float) * 3); + ASSERT_PRECONDITION(compare(actual, expected, 0.0f) == 0, "Image mismatch."); +} + +static bool solve(float a, float b, float c, float *x0, float *x1) { + float discr = b * b - 4 * a * c; + if (discr < 0) return false; + if (discr == 0) { + *x0 = *x1 = -0.5f * b / a; + } else { + float q = (b > 0) ? -0.5f * (b + sqrtf(discr)) : -0.5f * (b - sqrtf(discr)); + *x0 = q / a; + *x1 = c / q; + } + if (*x0 > *x1) swap(*x0, *x1); + return true; +} + +static bool intersect(Ray ray, Sphere sphere, float* t) { + float t0, t1; + float3 L = ray.orig - sphere.center; + float a = dot(ray.dir, ray.dir); + float b = 2 * dot(ray.dir, L); + float c = dot(L, L) - sphere.radius2; + if (!solve(a, b, c, &t0, &t1)) return false; + if (t0 > t1) swap(t0, t1); + if (t0 < 0) { + t0 = t1; + if (t0 < 0) return false; + } + *t = t0; + return true; +} + +static LinearImage diffImages(const LinearImage& a, const LinearImage& b) { + const uint32_t width = a.getWidth(), height = a.getHeight(), nchan = a.getChannels(); + ASSERT_PRECONDITION(width == b.getWidth() && height == b.getHeight() && + nchan == b.getChannels(), "Images must have same shape."); + LinearImage result(width, height, nchan); + float* dst = result.getPixelRef(); + float const* srca = a.getPixelRef(); + float const* srcb = b.getPixelRef(); + float largest = 0; + float smallest = std::numeric_limits::max(); + for (uint32_t n = 0; n < width * height * nchan; ++n) { + float delta = std::abs(srca[n] - srcb[n]); + largest = std::max(largest, delta); + smallest = std::min(smallest, delta); + dst[n] = delta; + } + const float scale = (largest == smallest) ? 1.0f : (1.0f / largest - smallest); + for (uint32_t n = 0; n < width * height * nchan; ++n) { + dst[n] = (dst[n] - smallest) * scale; + } + return result; +}