123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525 |
- // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
- // SPDX-License-Identifier: MIT
- #include <TestFramework.h>
- #include <Image/ZoomImage.h>
- #include <Image/Surface.h>
- #include <Image/BlitSurface.h>
- #include <Jolt/Core/Profiler.h>
- //////////////////////////////////////////////////////////////////////////////////////////
- // ImageFilter
- //
- // Abstract class and some implementations of a filter, essentially an 1D weighting function
- // which is not zero for t e [-GetSupport(), GetSupport()] and zero for all other t
- // The integrand is usually 1 although it is not required for this implementation,
- // since the filter is renormalized when it is sampled.
- //////////////////////////////////////////////////////////////////////////////////////////
- class ImageFilter
- {
- public:
- // Destructor
- virtual ~ImageFilter() = default;
- // Get support of this filter (+/- the range the filter function is not zero)
- virtual float GetSupport() const = 0;
- // Sample filter function at a certain point
- virtual float GetValue(float t) const = 0;
- };
-
- class ImageFilterBox : public ImageFilter
- {
- virtual float GetSupport() const override
- {
- return 0.5f;
- }
- virtual float GetValue(float t) const override
- {
- if (abs(t) <= 0.5f)
- return 1.0f;
- else
- return 0.0f;
- }
- };
- class ImageFilterTriangle : public ImageFilter
- {
- virtual float GetSupport() const override
- {
- return 1.0f;
- }
- virtual float GetValue(float t) const override
- {
- t = abs(t);
- if (t < 1.0f)
- return 1.0f - t;
- else
- return 0.0f;
- }
- };
- class ImageFilterBell : public ImageFilter
- {
- virtual float GetSupport() const override
- {
- return 1.5f;
- }
- virtual float GetValue(float t) const override
- {
- t = abs(t);
- if (t < 0.5f)
- return 0.75f - t * t;
- else if (t < 1.5f)
- {
- t = t - 1.5f;
- return 0.5f * t * t;
- }
- else
- return 0.0f;
- }
- };
- class ImageFilterBSpline : public ImageFilter
- {
- virtual float GetSupport() const override
- {
- return 2.0f;
- }
- virtual float GetValue(float t) const override
- {
- t = abs(t);
- if (t < 1.0f)
- {
- float tt = t * t;
- return (0.5f * tt * t) - tt + (2.0f / 3.0f);
- }
- else if (t < 2.0f)
- {
- t = 2.0f - t;
- return (1.0f / 6.0f) * (t * t * t);
- }
- else
- return 0.0f;
- }
- };
- class ImageFilterLanczos3 : public ImageFilter
- {
- virtual float GetSupport() const override
- {
- return 3.0f;
- }
- virtual float GetValue(float t) const override
- {
- t = abs(t);
- if (t < 3.0f)
- return Sinc(t) * Sinc(t / 3.0f);
- else
- return 0.0f;
- }
- private:
- static float Sinc(float x)
- {
- x *= JPH_PI;
- if (abs(x) < 1.0e-5f)
- return 1.0f;
- return Sin(x) / x;
- }
- };
- class ImageFilterMitchell : public ImageFilter
- {
- virtual float GetSupport() const override
- {
- return 2.0f;
- }
- virtual float GetValue(float t) const override
- {
- float tt = t * t;
- t = abs(t);
- if (t < 1.0f)
- return (7.0f * (t * tt) - 12.0f * tt + (16.0f / 3.0f)) / 6.0f;
- else if (t < 2.0f)
- return ((-7.0f / 3.0f) * (t * tt) + 12.0f * tt + -20.0f * t + (32.0f / 3.0f)) / 6.0f;
- else
- return 0.0f;
- }
- };
- static const ImageFilter &GetFilter(EFilter inFilter)
- {
- static ImageFilterBox box;
- static ImageFilterTriangle triangle;
- static ImageFilterBell bell;
- static ImageFilterBSpline bspline;
- static ImageFilterLanczos3 lanczos3;
- static ImageFilterMitchell mitchell;
- switch (inFilter)
- {
- case FilterBox: return box;
- case FilterTriangle: return triangle;
- case FilterBell: return bell;
- case FilterBSpline: return bspline;
- case FilterLanczos3: return lanczos3;
- case FilterMitchell: return mitchell;
- default: JPH_ASSERT(false); return mitchell;
- }
- }
- //////////////////////////////////////////////////////////////////////////////////////////
- // ZoomSettings
- //////////////////////////////////////////////////////////////////////////////////////////
- const ZoomSettings ZoomSettings::sDefault;
- ZoomSettings::ZoomSettings() :
- mFilter(FilterMitchell),
- mWrapFilter(true),
- mBlur(1.0f)
- {
- }
- bool ZoomSettings::operator == (const ZoomSettings &inRHS) const
- {
- return mFilter == inRHS.mFilter
- && mWrapFilter == inRHS.mWrapFilter
- && mBlur == inRHS.mBlur;
- }
- //////////////////////////////////////////////////////////////////////////////////////////
- // Resizing a surface
- //////////////////////////////////////////////////////////////////////////////////////////
- // Structure used for zooming
- struct Contrib
- {
- int mOffset; // Offset of this pixel (relative to start of scanline)
- int mWeight; // Weight of this pixel in 0.12 fixed point format
- };
- static void sPrecalculateFilter(const ZoomSettings &inZoomSettings, int inOldLength, int inNewLength, int inOffsetFactor, Array<Array<Contrib>> &outContrib)
- {
- JPH_PROFILE("PrecalculateFilter");
- // Get filter
- const ImageFilter &filter = GetFilter(inZoomSettings.mFilter);
- // Get scale
- float scale = float(inNewLength) / inOldLength;
- float fwidth, fscale;
- if (scale < 1.0f)
- {
- // Minify, broaden filter
- fwidth = filter.GetSupport() / scale;
- fscale = scale;
- }
- else
- {
- // Enlarge, filter is always used as is
- fwidth = filter.GetSupport();
- fscale = 1.0f;
- }
- // Adjust filter for blur
- fwidth *= inZoomSettings.mBlur;
- fscale /= inZoomSettings.mBlur;
- float min_fwidth = 1.0f;
- if (fwidth < min_fwidth)
- {
- fwidth = min_fwidth;
- fscale = filter.GetSupport() / min_fwidth;
- }
- // Make room for a whole scanline
- outContrib.resize(inNewLength);
- // Loop over the whole scanline
- for (int i = 0; i < inNewLength; ++i)
- {
- // Compute center and left- and rightmost pixels affected
- float center = float(i) / scale;
- int left = int(floor(center - fwidth));
- int right = int(ceil(center + fwidth));
- // Reserve required elements
- Array<Contrib> &a = outContrib[i];
- a.reserve(right - left + 1);
- // Total sum of all weights, for renormalization of the filter
- int filter_sum = 0;
- // Compute the contributions for each
- for (int source = left; source <= right; ++source)
- {
- Contrib c;
- // Initialize the offset
- c.mOffset = source;
- // Compute weight at this position in 0.12 fixed point
- c.mWeight = int(4096.0f * filter.GetValue(fscale * (center - source)));
- if (c.mWeight == 0) continue;
-
- // Add weight to filter total
- filter_sum += c.mWeight;
- // Reflect the filter at the edges if the filter is not to be wrapped (clamp)
- if (!inZoomSettings.mWrapFilter && (c.mOffset < 0 || c.mOffset >= inOldLength))
- c.mOffset = -c.mOffset - 1;
- // Wrap the offset so that it falls within the image
- c.mOffset = (c.mOffset % inOldLength + inOldLength) % inOldLength;
- // Check that the offset falls within the image
- JPH_ASSERT(c.mOffset >= 0 && c.mOffset < inOldLength);
- // Multiply the offset with the specified factor
- c.mOffset *= inOffsetFactor;
-
- // Add the filter element
- a.push_back(c);
- }
-
- // Normalize the filter to 0.12 fixed point
- if (filter_sum != 0)
- for (uint n = 0; n < a.size(); ++n)
- a[n].mWeight = (a[n].mWeight * 4096) / filter_sum;
- }
- }
- static void sZoomHorizontal(RefConst<Surface> inSrc, Ref<Surface> ioDst, const ZoomSettings &inZoomSettings)
- {
- JPH_PROFILE("ZoomHorizontal");
- // Check zoom parameters
- JPH_ASSERT(inSrc->GetHeight() == ioDst->GetHeight());
- JPH_ASSERT(inSrc->GetFormat() == ioDst->GetFormat());
- const int width = ioDst->GetWidth();
- const int height = ioDst->GetHeight();
- const int components = ioDst->GetNumberOfComponents();
- const int delta_s = -components;
- const int delta_d = ioDst->GetBytesPerPixel() - components;
- // Pre-calculate filter contributions for a row
- Array<Array<Contrib>> contrib;
- sPrecalculateFilter(inZoomSettings, inSrc->GetWidth(), ioDst->GetWidth(), inSrc->GetBytesPerPixel(), contrib);
- // Do the zoom
- for (int y = 0; y < height; ++y)
- {
- const uint8 *s = inSrc->GetScanLine(y);
- uint8 *d = ioDst->GetScanLine(y);
- for (int x = 0; x < width; ++x)
- {
- const Array<Contrib> &line = contrib[x];
- const size_t line_size_min_one = line.size() - 1;
- int c = components;
- do
- {
- int pixel = 0;
- // Apply the filter for one color component
- size_t j = line_size_min_one;
- do
- {
- const Contrib &cmp = line[j];
- pixel += cmp.mWeight * s[cmp.mOffset];
- }
- while (j--);
- // Clamp the pixel value
- if (pixel <= 0)
- *d = 0;
- else if (pixel >= (255 << 12))
- *d = 255;
- else
- *d = uint8(pixel >> 12);
- ++s;
- ++d;
- }
- while (--c);
- // Skip unused components if there are any
- s += delta_s;
- d += delta_d;
- }
- }
- }
- static void sZoomVertical(RefConst<Surface> inSrc, Ref<Surface> ioDst, const ZoomSettings &inZoomSettings)
- {
- JPH_PROFILE("ZoomVertical");
- // Check zoom parameters
- JPH_ASSERT(inSrc->GetWidth() == ioDst->GetWidth());
- JPH_ASSERT(inSrc->GetFormat() == ioDst->GetFormat());
- const int width = ioDst->GetWidth();
- const int height = ioDst->GetHeight();
- const int components = ioDst->GetNumberOfComponents();
- const int delta_s = inSrc->GetBytesPerPixel() - components;
- const int delta_d = ioDst->GetBytesPerPixel() - components;
-
- // Pre-calculate filter contributions for a row
- Array<Array<Contrib>> contrib;
- sPrecalculateFilter(inZoomSettings, inSrc->GetHeight(), ioDst->GetHeight(), inSrc->GetStride(), contrib);
- // Do the zoom
- for (int y = 0; y < height; ++y)
- {
- const uint8 *s = inSrc->GetScanLine(0);
- uint8 *d = ioDst->GetScanLine(y);
- const Array<Contrib> &line = contrib[y];
- const size_t line_size_min_one = line.size() - 1;
- for (int x = 0; x < width; ++x)
- {
- int c = components;
- do
- {
- int pixel = 0;
- // Apply the filter for one color component
- size_t j = line_size_min_one;
- do
- {
- const Contrib &cmp = line[j];
- pixel += cmp.mWeight * s[cmp.mOffset];
- }
- while (j--);
- // Clamp the pixel value
- if (pixel <= 0)
- *d = 0;
- else if (pixel >= (255 << 12))
- *d = 255;
- else
- *d = uint8(pixel >> 12);
- ++s;
- ++d;
- }
- while (--c);
- // Skip unused components if there are any
- s += delta_s;
- d += delta_d;
- }
- }
- }
- bool ZoomImage(RefConst<Surface> inSrc, Ref<Surface> ioDst, const ZoomSettings &inZoomSettings)
- {
- JPH_PROFILE("ZoomImage");
- // Get filter
- const ImageFilter &filter = GetFilter(inZoomSettings.mFilter);
-
- // Determine the temporary format that will require the least amount of components to be zoomed and the least amount of bytes pushed around
- ESurfaceFormat tmp_format;
- ESurfaceFormat src_format = inSrc->GetClosest8BitFormat();
- ESurfaceFormat dst_format = ioDst->GetClosest8BitFormat();
- const FormatDescription &src_desc = GetFormatDescription(src_format);
- const FormatDescription &dst_desc = GetFormatDescription(dst_format);
- if (src_desc.GetNumberOfComponents() < dst_desc.GetNumberOfComponents())
- tmp_format = src_format;
- else if (src_desc.GetNumberOfComponents() > dst_desc.GetNumberOfComponents())
- tmp_format = dst_format;
- else if (src_desc.GetBytesPerPixel() < dst_desc.GetBytesPerPixel())
- tmp_format = src_format;
- else
- tmp_format = dst_format;
- // Create temporary source buffer if nessecary
- RefConst<Surface> src = inSrc;
- if (inSrc->GetFormat() != tmp_format)
- {
- Ref<Surface> tmp = new SoftwareSurface(inSrc->GetWidth(), inSrc->GetHeight(), tmp_format);
- if (!BlitSurface(inSrc, tmp))
- return false;
- src = tmp;
- }
- // Create temporary destination buffer if nessecary
- Ref<Surface> dst = ioDst;
- if (ioDst->GetFormat() != tmp_format)
- dst = new SoftwareSurface(ioDst->GetWidth(), ioDst->GetHeight(), tmp_format);
- src->Lock(ESurfaceLockMode::Read);
- dst->Lock(ESurfaceLockMode::Write);
- if (src->GetWidth() == dst->GetWidth())
- {
- // Only vertical zoom required
- sZoomVertical(src, dst, inZoomSettings);
- }
- else if (src->GetHeight() == dst->GetHeight())
- {
- // Only horizontal zoom required
- sZoomHorizontal(src, dst, inZoomSettings);
- }
- else
- {
- // Determine most optimal order
- float operations_vh = float(dst->GetWidth()) * (filter.GetSupport() * src->GetHeight() + filter.GetSupport() * dst->GetHeight());
- float operations_hv = float(dst->GetHeight()) * (filter.GetSupport() * src->GetWidth() + filter.GetSupport() * dst->GetWidth());
- if (operations_vh < operations_hv)
- {
- // Create temporary buffer to hold the vertical scale
- Ref<Surface> tmp = new SoftwareSurface(src->GetWidth(), dst->GetHeight(), tmp_format);
- tmp->Lock(ESurfaceLockMode::ReadWrite);
-
- // First scale vertically then horizontally
- sZoomVertical(src, tmp, inZoomSettings);
- sZoomHorizontal(tmp, dst, inZoomSettings);
- tmp->UnLock();
- }
- else
- {
- // Create temporary buffer to hold the horizontal scale
- Ref<Surface> tmp = new SoftwareSurface(dst->GetWidth(), src->GetHeight(), tmp_format);
- tmp->Lock(ESurfaceLockMode::ReadWrite);
-
- // First scale horizontally then vertically
- sZoomHorizontal(src, tmp, inZoomSettings);
- sZoomVertical(tmp, dst, inZoomSettings);
- tmp->UnLock();
- }
- }
- src->UnLock();
- dst->UnLock();
- // Convert to destination if required
- if (dst != ioDst)
- if (!BlitSurface(dst, ioDst))
- return false;
- return true;
- }
|