diff options
Diffstat (limited to 'vcl/source/bitmap/BitmapBasicMorphologyFilter.cxx')
-rw-r--r-- | vcl/source/bitmap/BitmapBasicMorphologyFilter.cxx | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/vcl/source/bitmap/BitmapBasicMorphologyFilter.cxx b/vcl/source/bitmap/BitmapBasicMorphologyFilter.cxx new file mode 100644 index 000000000..8516f5861 --- /dev/null +++ b/vcl/source/bitmap/BitmapBasicMorphologyFilter.cxx @@ -0,0 +1,371 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + +#include <sal/config.h> + +#include <comphelper/threadpool.hxx> +#include <sal/log.hxx> +#include <vcl/BitmapBasicMorphologyFilter.hxx> + +#include <bitmap/BitmapWriteAccess.hxx> + +#include <algorithm> + +/* TODO: Use round kernel instead of square one. + This would make the result more natural, e.g. not making rounded square out of circle. + */ + +namespace +{ +struct FilterSharedData +{ + BitmapReadAccess* mpReadAccess; + BitmapWriteAccess* mpWriteAccess; + sal_Int32 mnRadius; + sal_uInt8 mnOutsideVal; + Color maOutsideColor; + + FilterSharedData(Bitmap::ScopedReadAccess& rReadAccess, BitmapScopedWriteAccess& rWriteAccess, + sal_Int32 nRadius, sal_uInt8 nOutsideVal) + : mpReadAccess(rReadAccess.get()) + , mpWriteAccess(rWriteAccess.get()) + , mnRadius(nRadius) + , mnOutsideVal(nOutsideVal) + , maOutsideColor(ColorTransparency, nOutsideVal, nOutsideVal, nOutsideVal, nOutsideVal) + { + } +}; + +// Black is foreground, white is background + +struct ErodeOp +{ + static sal_uInt8 apply(sal_uInt8 v1, sal_uInt8 v2) { return std::max(v1, v2); } + static constexpr sal_uInt8 initVal = 0; +}; + +struct DilateOp +{ + static sal_uInt8 apply(sal_uInt8 v1, sal_uInt8 v2) { return std::min(v1, v2); } + static constexpr sal_uInt8 initVal = SAL_MAX_UINT8; +}; + +// 8 bit per channel case + +template <typename MorphologyOp, int nComponentWidth> struct Value +{ + static constexpr int nWidthBytes = nComponentWidth / 8; + static_assert(nWidthBytes * 8 == nComponentWidth); + + sal_uInt8 aResult[nWidthBytes]; + + // If we are at the start or at the end of the line, consider outside value + Value(FilterSharedData const& rShared, bool bLookOutside) + { + std::fill_n(aResult, nWidthBytes, + bLookOutside ? rShared.mnOutsideVal : MorphologyOp::initVal); + } + + void apply(const BitmapReadAccess* pReadAccess, sal_Int32 x, sal_Int32 y, + sal_uInt8* pHint = nullptr) + { + sal_uInt8* pSource = (pHint ? pHint : pReadAccess->GetScanline(y)) + nWidthBytes * x; + std::transform(pSource, pSource + nWidthBytes, aResult, aResult, MorphologyOp::apply); + } + + void copy(const BitmapWriteAccess* pWriteAccess, sal_Int32 x, sal_Int32 y, + sal_uInt8* pHint = nullptr) + { + sal_uInt8* pDest = (pHint ? pHint : pWriteAccess->GetScanline(y)) + nWidthBytes * x; + std::copy_n(aResult, nWidthBytes, pDest); + } +}; + +// Partial specializations for nComponentWidth == 0, using access' GetColor/SetPixel + +template <typename MorphologyOp> struct Value<MorphologyOp, 0> +{ + static constexpr Color initColor{ ColorTransparency, MorphologyOp::initVal, + MorphologyOp::initVal, MorphologyOp::initVal, + MorphologyOp::initVal }; + + Color aResult; + + // If we are at the start or at the end of the line, consider outside value + Value(FilterSharedData const& rShared, bool bLookOutside) + : aResult(bLookOutside ? rShared.maOutsideColor : initColor) + { + } + + void apply(const BitmapReadAccess* pReadAccess, sal_Int32 x, sal_Int32 y, + sal_uInt8* /*pHint*/ = nullptr) + { + const auto& rSource = pReadAccess->GetColor(y, x); + aResult = Color(ColorAlpha, MorphologyOp::apply(rSource.GetAlpha(), aResult.GetAlpha()), + MorphologyOp::apply(rSource.GetRed(), aResult.GetRed()), + MorphologyOp::apply(rSource.GetGreen(), aResult.GetGreen()), + MorphologyOp::apply(rSource.GetBlue(), aResult.GetBlue())); + } + + void copy(BitmapWriteAccess* pWriteAccess, sal_Int32 x, sal_Int32 y, + sal_uInt8* /*pHint*/ = nullptr) + { + pWriteAccess->SetPixel(y, x, aResult); + } +}; + +bool GetMinMax(sal_Int32 nCenter, sal_Int32 nRadius, sal_Int32 nMaxLimit, sal_Int32& nMin, + sal_Int32& nMax) +{ + nMin = nCenter - nRadius; + nMax = nCenter + nRadius; + bool bLookOutside = false; + if (nMin < 0) + { + bLookOutside = true; + nMin = 0; + } + if (nMax > nMaxLimit) + { + bLookOutside = true; + nMax = nMaxLimit; + } + return bLookOutside; +} + +template <typename MorphologyOp, int nComponentWidth> struct pass +{ + static void Horizontal(FilterSharedData const& rShared, const sal_Int32 nStart, + const sal_Int32 nEnd) + { + BitmapReadAccess* pReadAccess = rShared.mpReadAccess; + BitmapWriteAccess* pWriteAccess = rShared.mpWriteAccess; + + const sal_Int32 nLastIndex = pReadAccess->Width() - 1; + + for (sal_Int32 y = nStart; y <= nEnd; y++) + { + // Optimization + sal_uInt8* const pSourceHint = pReadAccess->GetScanline(y); + sal_uInt8* const pDestHint = pWriteAccess->GetScanline(y); + for (sal_Int32 x = 0; x <= nLastIndex; x++) + { + // This processes [nRadius * 2 + 1] pixels of source per resulting pixel + // TODO: try to optimize this to not process same pixels repeatedly + sal_Int32 iMin, iMax; + const bool bLookOutside = GetMinMax(x, rShared.mnRadius, nLastIndex, iMin, iMax); + Value<MorphologyOp, nComponentWidth> aResult(rShared, bLookOutside); + for (sal_Int32 i = iMin; i <= iMax; ++i) + aResult.apply(pReadAccess, i, y, pSourceHint); + + aResult.copy(pWriteAccess, x, y, pDestHint); + } + } + } + + static void Vertical(FilterSharedData const& rShared, const sal_Int32 nStart, + const sal_Int32 nEnd) + { + BitmapReadAccess* pReadAccess = rShared.mpReadAccess; + BitmapWriteAccess* pWriteAccess = rShared.mpWriteAccess; + + const sal_Int32 nLastIndex = pReadAccess->Height() - 1; + + for (sal_Int32 x = nStart; x <= nEnd; x++) + { + for (sal_Int32 y = 0; y <= nLastIndex; y++) + { + // This processes [nRadius * 2 + 1] pixels of source per resulting pixel + // TODO: try to optimize this to not process same pixels repeatedly + sal_Int32 iMin, iMax; + const bool bLookOutside = GetMinMax(y, rShared.mnRadius, nLastIndex, iMin, iMax); + Value<MorphologyOp, nComponentWidth> aResult(rShared, bLookOutside); + for (sal_Int32 i = iMin; i <= iMax; ++i) + aResult.apply(pReadAccess, x, i); + + aResult.copy(pWriteAccess, x, y); + } + } + } +}; + +typedef void (*passFn)(FilterSharedData const& rShared, sal_Int32 nStart, sal_Int32 nEnd); + +class FilterTask : public comphelper::ThreadTask +{ + passFn mpFunction; + FilterSharedData& mrShared; + sal_Int32 mnStart; + sal_Int32 mnEnd; + +public: + explicit FilterTask(const std::shared_ptr<comphelper::ThreadTaskTag>& pTag, passFn pFunction, + FilterSharedData& rShared, sal_Int32 nStart, sal_Int32 nEnd) + : comphelper::ThreadTask(pTag) + , mpFunction(pFunction) + , mrShared(rShared) + , mnStart(nStart) + , mnEnd(nEnd) + { + } + + virtual void doWork() override { mpFunction(mrShared, mnStart, mnEnd); } +}; + +constexpr sal_Int32 nThreadStrip = 16; + +template <typename MorphologyOp, int nComponentWidth> +void runFilter(Bitmap& rBitmap, const sal_Int32 nRadius, const bool bParallel, + bool bUseValueOutside, sal_uInt8 nValueOutside) +{ + using myPass = pass<MorphologyOp, nComponentWidth>; + const sal_uInt8 nOutsideVal = bUseValueOutside ? nValueOutside : MorphologyOp::initVal; + if (bParallel) + { + try + { + comphelper::ThreadPool& rShared = comphelper::ThreadPool::getSharedOptimalPool(); + auto pTag = comphelper::ThreadPool::createThreadTaskTag(); + + { + Bitmap::ScopedReadAccess pReadAccess(rBitmap); + BitmapScopedWriteAccess pWriteAccess(rBitmap); + FilterSharedData aSharedData(pReadAccess, pWriteAccess, nRadius, nOutsideVal); + + const sal_Int32 nLastIndex = pReadAccess->Height() - 1; + sal_Int32 nStripStart = 0; + for (; nStripStart < nLastIndex - nThreadStrip; nStripStart += nThreadStrip) + { + sal_Int32 nStripEnd = nStripStart + nThreadStrip - 1; + auto pTask(std::make_unique<FilterTask>(pTag, myPass::Horizontal, aSharedData, + nStripStart, nStripEnd)); + rShared.pushTask(std::move(pTask)); + } + // Do the last (or the only) strip in main thread without threading overhead + myPass::Horizontal(aSharedData, nStripStart, nLastIndex); + rShared.waitUntilDone(pTag); + } + { + Bitmap::ScopedReadAccess pReadAccess(rBitmap); + BitmapScopedWriteAccess pWriteAccess(rBitmap); + FilterSharedData aSharedData(pReadAccess, pWriteAccess, nRadius, nOutsideVal); + + const sal_Int32 nLastIndex = pReadAccess->Width() - 1; + sal_Int32 nStripStart = 0; + for (; nStripStart < nLastIndex - nThreadStrip; nStripStart += nThreadStrip) + { + sal_Int32 nStripEnd = nStripStart + nThreadStrip - 1; + auto pTask(std::make_unique<FilterTask>(pTag, myPass::Vertical, aSharedData, + nStripStart, nStripEnd)); + rShared.pushTask(std::move(pTask)); + } + // Do the last (or the only) strip in main thread without threading overhead + myPass::Vertical(aSharedData, nStripStart, nLastIndex); + rShared.waitUntilDone(pTag); + } + } + catch (...) + { + SAL_WARN("vcl.gdi", "threaded bitmap blurring failed"); + } + } + else + { + { + Bitmap::ScopedReadAccess pReadAccess(rBitmap); + BitmapScopedWriteAccess pWriteAccess(rBitmap); + FilterSharedData aSharedData(pReadAccess, pWriteAccess, nRadius, nOutsideVal); + sal_Int32 nFirstIndex = 0; + sal_Int32 nLastIndex = pReadAccess->Height() - 1; + myPass::Horizontal(aSharedData, nFirstIndex, nLastIndex); + } + { + Bitmap::ScopedReadAccess pReadAccess(rBitmap); + BitmapScopedWriteAccess pWriteAccess(rBitmap); + FilterSharedData aSharedData(pReadAccess, pWriteAccess, nRadius, nOutsideVal); + sal_Int32 nFirstIndex = 0; + sal_Int32 nLastIndex = pReadAccess->Width() - 1; + myPass::Vertical(aSharedData, nFirstIndex, nLastIndex); + } + } +} + +template <int nComponentWidth> +void runFilter(Bitmap& rBitmap, BasicMorphologyOp op, sal_Int32 nRadius, bool bUseValueOutside, + sal_uInt8 nValueOutside) +{ + const bool bParallel = true; + + if (op == BasicMorphologyOp::erode) + runFilter<ErodeOp, nComponentWidth>(rBitmap, nRadius, bParallel, bUseValueOutside, + nValueOutside); + else if (op == BasicMorphologyOp::dilate) + runFilter<DilateOp, nComponentWidth>(rBitmap, nRadius, bParallel, bUseValueOutside, + nValueOutside); +} + +} // end anonymous namespace + +BitmapBasicMorphologyFilter::BitmapBasicMorphologyFilter(BasicMorphologyOp op, sal_Int32 nRadius) + : m_eOp(op) + , m_nRadius(nRadius) +{ +} + +BitmapBasicMorphologyFilter::BitmapBasicMorphologyFilter(BasicMorphologyOp op, sal_Int32 nRadius, + sal_uInt8 nValueOutside) + : m_eOp(op) + , m_nRadius(nRadius) + , m_nValueOutside(nValueOutside) + , m_bUseValueOutside(true) +{ +} + +BitmapBasicMorphologyFilter::~BitmapBasicMorphologyFilter() = default; + +BitmapEx BitmapBasicMorphologyFilter::execute(BitmapEx const& rBitmapEx) const +{ + Bitmap result = filter(rBitmapEx.GetBitmap()); + return BitmapEx(result, rBitmapEx.GetAlpha()); +} + +Bitmap BitmapBasicMorphologyFilter::filter(Bitmap const& rBitmap) const +{ + Bitmap bitmapCopy(rBitmap); + ScanlineFormat nScanlineFormat; + { + Bitmap::ScopedReadAccess pReadAccess(bitmapCopy); + nScanlineFormat = pReadAccess ? pReadAccess->GetScanlineFormat() : ScanlineFormat::NONE; + } + + switch (nScanlineFormat) + { + case ScanlineFormat::N24BitTcRgb: + case ScanlineFormat::N24BitTcBgr: + runFilter<24>(bitmapCopy, m_eOp, m_nRadius, m_bUseValueOutside, m_nValueOutside); + break; + case ScanlineFormat::N32BitTcMask: + case ScanlineFormat::N32BitTcBgra: + runFilter<32>(bitmapCopy, m_eOp, m_nRadius, m_bUseValueOutside, m_nValueOutside); + break; + case ScanlineFormat::N8BitPal: + runFilter<8>(bitmapCopy, m_eOp, m_nRadius, m_bUseValueOutside, m_nValueOutside); + break; + // TODO: handle 1-bit images + default: + // Use access' GetColor/SetPixel fallback + runFilter<0>(bitmapCopy, m_eOp, m_nRadius, m_bUseValueOutside, m_nValueOutside); + break; + } + + return bitmapCopy; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |