diff options
author | Mike Kaganski <mike.kaganski@collabora.com> | 2020-05-06 11:08:22 +0300 |
---|---|---|
committer | Mike Kaganski <mike.kaganski@collabora.com> | 2020-05-07 21:56:23 +0200 |
commit | 84808eed2405ed6ee586e87bb664a816f7b91b70 (patch) | |
tree | 38bfad43c2fff3e4185f7e9720f88e84090e1dfe /vcl | |
parent | 547b2891d9fe97dee9df14106e91dc4df659d4d5 (diff) |
Add basic morphology (erode/dilate) bitmap filter
Needed for glow effect (tdf#101181)
Change-Id: Id41daa1dc17e3749a30ce75fa3127878b9e0cfd1
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/93552
Tested-by: Jenkins
Reviewed-by: Tomaž Vajngerl <quikee@gmail.com>
Diffstat (limited to 'vcl')
-rw-r--r-- | vcl/Library_vcl.mk | 1 | ||||
-rw-r--r-- | vcl/qa/cppunit/BitmapFilterTest.cxx | 78 | ||||
-rw-r--r-- | vcl/qa/cppunit/data/testBasicMorphology.png | bin | 0 -> 226 bytes | |||
-rw-r--r-- | vcl/qa/cppunit/data/testBasicMorphologyDilated1.png | bin | 0 -> 219 bytes | |||
-rw-r--r-- | vcl/qa/cppunit/data/testBasicMorphologyDilated1Eroded1.png | bin | 0 -> 224 bytes | |||
-rw-r--r-- | vcl/qa/cppunit/data/testBasicMorphologyDilated2.png | bin | 0 -> 174 bytes | |||
-rw-r--r-- | vcl/qa/cppunit/data/testBasicMorphologyDilated2Eroded1.png | bin | 0 -> 178 bytes | |||
-rw-r--r-- | vcl/source/bitmap/BitmapBasicMorphologyFilter.cxx | 358 |
8 files changed, 426 insertions, 11 deletions
diff --git a/vcl/Library_vcl.mk b/vcl/Library_vcl.mk index 9780ca3575a1..ffe065b24fff 100644 --- a/vcl/Library_vcl.mk +++ b/vcl/Library_vcl.mk @@ -331,6 +331,7 @@ $(eval $(call gb_Library_add_exception_objects,vcl,\ vcl/source/bitmap/bitmap \ vcl/source/bitmap/bitmapfilter \ vcl/source/bitmap/BitmapAlphaClampFilter \ + vcl/source/bitmap/BitmapBasicMorphologyFilter \ vcl/source/bitmap/BitmapMonochromeFilter \ vcl/source/bitmap/BitmapSmoothenFilter \ vcl/source/bitmap/BitmapLightenFilter \ diff --git a/vcl/qa/cppunit/BitmapFilterTest.cxx b/vcl/qa/cppunit/BitmapFilterTest.cxx index a28057a4bf57..fec21fa118f0 100644 --- a/vcl/qa/cppunit/BitmapFilterTest.cxx +++ b/vcl/qa/cppunit/BitmapFilterTest.cxx @@ -7,10 +7,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include <cppunit/TestAssert.h> -#include <cppunit/TestFixture.h> -#include <cppunit/extensions/HelperMacros.h> -#include <cppunit/plugin/TestPlugIn.h> +#include <test/bootstrapfixture.hxx> #include <vcl/bitmap.hxx> #include <vcl/bitmapaccess.hxx> @@ -19,6 +16,7 @@ #include <tools/stream.hxx> #include <vcl/graphicfilter.hxx> +#include <vcl/BitmapBasicMorphologyFilter.hxx> #include <vcl/BitmapFilterStackBlur.hxx> #include <BitmapSymmetryCheck.hxx> @@ -29,15 +27,48 @@ namespace constexpr bool constWriteResultBitmap(false); constexpr bool constEnablePerformanceTest(false); -class BitmapFilterTest : public CppUnit::TestFixture +class BitmapFilterTest : public test::BootstrapFixture { +public: + BitmapFilterTest() + : test::BootstrapFixture(true, false) + { + } + void testBlurCorrectness(); + void testBasicMorphology(); void testPerformance(); CPPUNIT_TEST_SUITE(BitmapFilterTest); CPPUNIT_TEST(testBlurCorrectness); + CPPUNIT_TEST(testBasicMorphology); CPPUNIT_TEST(testPerformance); CPPUNIT_TEST_SUITE_END(); + +private: + OUString getFullUrl(const OUString& sFileName) + { + return m_directories.getURLFromSrc("vcl/qa/cppunit/data/") + sFileName; + } + + BitmapEx loadBitmap(const OUString& sFileName) + { + Graphic aGraphic; + const OUString aURL(getFullUrl(sFileName)); + SvFileStream aFileStream(aURL, StreamMode::READ); + GraphicFilter& rFilter = GraphicFilter::GetGraphicFilter(); + ErrCode aResult = rFilter.ImportGraphic(aGraphic, aURL, aFileStream); + CPPUNIT_ASSERT_EQUAL(ERRCODE_NONE, aResult); + return aGraphic.GetBitmapEx(); + } + + template <class BitmapT> // handle both Bitmap and BitmapEx + void savePNG(const OUString& sWhere, const BitmapT& rBmp) + { + SvFileStream aStream(sWhere, StreamMode::WRITE | StreamMode::TRUNC); + GraphicFilter& rFilter = GraphicFilter::GetGraphicFilter(); + rFilter.compressAsPNG(rBmp, aStream); + } }; void BitmapFilterTest::testBlurCorrectness() @@ -73,9 +104,7 @@ void BitmapFilterTest::testBlurCorrectness() if (constWriteResultBitmap) { - SvFileStream aStream("~/blurBefore.png", StreamMode::WRITE | StreamMode::TRUNC); - GraphicFilter& rFilter = GraphicFilter::GetGraphicFilter(); - rFilter.compressAsPNG(aBitmap24Bit, aStream); + savePNG("~/blurBefore.png", aBitmap24Bit); } // Perform blur @@ -86,9 +115,7 @@ void BitmapFilterTest::testBlurCorrectness() if (constWriteResultBitmap) { - SvFileStream aStream("~/blurAfter.png", StreamMode::WRITE | StreamMode::TRUNC); - GraphicFilter& rFilter = GraphicFilter::GetGraphicFilter(); - rFilter.compressAsPNG(aBitmap24Bit, aStream); + savePNG("~/blurAfter.png", aBitmap24Bit); } // Check blurred bitmap parameters @@ -106,6 +133,35 @@ void BitmapFilterTest::testBlurCorrectness() } } +void BitmapFilterTest::testBasicMorphology() +{ + const BitmapEx aOrigBitmap = loadBitmap("testBasicMorphology.png"); + const BitmapEx aRefBitmapDilated1 = loadBitmap("testBasicMorphologyDilated1.png"); + const BitmapEx aRefBitmapDilated1Eroded1 = loadBitmap("testBasicMorphologyDilated1Eroded1.png"); + const BitmapEx aRefBitmapDilated2 = loadBitmap("testBasicMorphologyDilated2.png"); + const BitmapEx aRefBitmapDilated2Eroded1 = loadBitmap("testBasicMorphologyDilated2Eroded1.png"); + + BitmapEx aTransformBitmap = aOrigBitmap; + BitmapFilter::Filter(aTransformBitmap, BitmapDilateFilter(1)); + if (constWriteResultBitmap) + savePNG("~/Dilated1.png", aTransformBitmap); + CPPUNIT_ASSERT_EQUAL(aRefBitmapDilated1.GetChecksum(), aTransformBitmap.GetChecksum()); + BitmapFilter::Filter(aTransformBitmap, BitmapErodeFilter(1)); + if (constWriteResultBitmap) + savePNG("~/Dilated1Eroded1.png", aTransformBitmap); + CPPUNIT_ASSERT_EQUAL(aRefBitmapDilated1Eroded1.GetChecksum(), aTransformBitmap.GetChecksum()); + + aTransformBitmap = aOrigBitmap; + BitmapFilter::Filter(aTransformBitmap, BitmapDilateFilter(2)); + if (constWriteResultBitmap) + savePNG("~/Dilated2.png", aTransformBitmap); + CPPUNIT_ASSERT_EQUAL(aRefBitmapDilated2.GetChecksum(), aTransformBitmap.GetChecksum()); + BitmapFilter::Filter(aTransformBitmap, BitmapErodeFilter(1)); + if (constWriteResultBitmap) + savePNG("~/Dilated2Eroded1.png", aTransformBitmap); + CPPUNIT_ASSERT_EQUAL(aRefBitmapDilated2Eroded1.GetChecksum(), aTransformBitmap.GetChecksum()); +} + void BitmapFilterTest::testPerformance() { if (!constEnablePerformanceTest) diff --git a/vcl/qa/cppunit/data/testBasicMorphology.png b/vcl/qa/cppunit/data/testBasicMorphology.png Binary files differnew file mode 100644 index 000000000000..5db565779f73 --- /dev/null +++ b/vcl/qa/cppunit/data/testBasicMorphology.png diff --git a/vcl/qa/cppunit/data/testBasicMorphologyDilated1.png b/vcl/qa/cppunit/data/testBasicMorphologyDilated1.png Binary files differnew file mode 100644 index 000000000000..ba335bab3cb5 --- /dev/null +++ b/vcl/qa/cppunit/data/testBasicMorphologyDilated1.png diff --git a/vcl/qa/cppunit/data/testBasicMorphologyDilated1Eroded1.png b/vcl/qa/cppunit/data/testBasicMorphologyDilated1Eroded1.png Binary files differnew file mode 100644 index 000000000000..3b10a949af67 --- /dev/null +++ b/vcl/qa/cppunit/data/testBasicMorphologyDilated1Eroded1.png diff --git a/vcl/qa/cppunit/data/testBasicMorphologyDilated2.png b/vcl/qa/cppunit/data/testBasicMorphologyDilated2.png Binary files differnew file mode 100644 index 000000000000..30d90757ea7e --- /dev/null +++ b/vcl/qa/cppunit/data/testBasicMorphologyDilated2.png diff --git a/vcl/qa/cppunit/data/testBasicMorphologyDilated2Eroded1.png b/vcl/qa/cppunit/data/testBasicMorphologyDilated2Eroded1.png Binary files differnew file mode 100644 index 000000000000..a506577da49e --- /dev/null +++ b/vcl/qa/cppunit/data/testBasicMorphologyDilated2Eroded1.png diff --git a/vcl/source/bitmap/BitmapBasicMorphologyFilter.cxx b/vcl/source/bitmap/BitmapBasicMorphologyFilter.cxx new file mode 100644 index 000000000000..581d65e67770 --- /dev/null +++ b/vcl/source/bitmap/BitmapBasicMorphologyFilter.cxx @@ -0,0 +1,358 @@ +/* -*- 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/bitmapaccess.hxx> +#include <vcl/BitmapBasicMorphologyFilter.hxx> + +#include <bitmapwriteaccess.hxx> + +#include <algorithm> + +namespace +{ +struct FilterSharedData +{ + BitmapReadAccess* mpReadAccess; + BitmapWriteAccess* mpWriteAccess; + long mnRadius; + + FilterSharedData(BitmapReadAccess* pReadAccess, BitmapWriteAccess* pWriteAccess, long nRadius) + : mpReadAccess(pReadAccess) + , mpWriteAccess(pWriteAccess) + , mnRadius(nRadius) + { + } +}; + +// 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; + static constexpr Color initColor = COL_BLACK; +}; + +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 }; + static constexpr Color initColor = COL_TRANSPARENT; +}; + +template <typename MorphologyOp> struct OpHelper +{ + template <int n> static void apply(sal_uInt8 (&rResult)[n], Scanline pSource) + { + std::transform(pSource, pSource + n, rResult, rResult, MorphologyOp::apply); + } + + static void apply(Color& rResult, const Color& rSource) + { + rResult = Color(MorphologyOp::apply(rSource.GetTransparency(), rResult.GetTransparency()), + MorphologyOp::apply(rSource.GetRed(), rResult.GetRed()), + MorphologyOp::apply(rSource.GetGreen(), rResult.GetGreen()), + MorphologyOp::apply(rSource.GetBlue(), rResult.GetBlue())); + } + + template <int n> static void init(sal_uInt8 (&rResult)[n]) + { + std::fill_n(rResult, n, MorphologyOp::initVal); + } +}; + +// 8 bit per channel case + +template <typename MorphologyOp, int nComponentWidth> struct pass +{ + static constexpr int nWidthBytes = nComponentWidth / 8; + static_assert(nWidthBytes * 8 == nComponentWidth); + static void Horizontal(FilterSharedData const& rShared, const long nStart, const long nEnd) + { + BitmapReadAccess* pReadAccess = rShared.mpReadAccess; + BitmapWriteAccess* pWriteAccess = rShared.mpWriteAccess; + + const long nWidth = pReadAccess->Width(); + const long nLastIndex = nWidth - 1; + + const long nRadius = rShared.mnRadius; + + for (long y = nStart; y <= nEnd; y++) + { + const Scanline pScanline = pReadAccess->GetScanline(y); + for (long x = 0; x < nWidth; x++) + { + // This processes [nRadius * 2 + 1] pixels of source per resulting pixel + // TODO: try to optimize this to not process same pixels repeatedly + sal_uInt8 aResult[nWidthBytes]; + OpHelper<MorphologyOp>::init(aResult); + const long iMax = std::min(x + nRadius, nLastIndex); + for (long i = std::max(x - nRadius, 0L); i <= iMax; ++i) + OpHelper<MorphologyOp>::apply(aResult, pScanline + nWidthBytes * i); + + Scanline pDestinationPointer = pWriteAccess->GetScanline(y) + nWidthBytes * x; + for (const auto& val : aResult) + *pDestinationPointer++ = val; + } + } + } + + static void Vertical(FilterSharedData const& rShared, const long nStart, const long nEnd) + { + BitmapReadAccess* pReadAccess = rShared.mpReadAccess; + BitmapWriteAccess* pWriteAccess = rShared.mpWriteAccess; + + const long nHeight = pReadAccess->Height(); + const long nLastIndex = nHeight - 1; + + const long nRadius = rShared.mnRadius; + + for (long x = nStart; x <= nEnd; x++) + { + for (long y = 0; y < nHeight; y++) + { + // This processes [nRadius * 2 + 1] pixels of source per resulting pixel + // TODO: try to optimize this to not process same pixels repeatedly + sal_uInt8 aResult[nWidthBytes]; + OpHelper<MorphologyOp>::init(aResult); + const long iMax = std::min(y + nRadius, nLastIndex); + for (long i = std::max(y - nRadius, 0L); i <= iMax; ++i) + OpHelper<MorphologyOp>::apply(aResult, + pReadAccess->GetScanline(i) + nWidthBytes * x); + + Scanline pDestinationPointer = pWriteAccess->GetScanline(y) + nWidthBytes * x; + for (auto& val : aResult) + *pDestinationPointer++ = val; + } + } + } +}; + +// Partial specializations for nComponentWidth == 0, using acess' GetColor/SetPixel + +template <typename MorphologyOp> struct pass<MorphologyOp, 0> +{ + static void Horizontal(FilterSharedData const& rShared, const long nStart, const long nEnd) + { + BitmapReadAccess* pReadAccess = rShared.mpReadAccess; + BitmapWriteAccess* pWriteAccess = rShared.mpWriteAccess; + + const long nWidth = pReadAccess->Width(); + const long nLastIndex = nWidth - 1; + + const long nRadius = rShared.mnRadius; + + for (long y = nStart; y <= nEnd; y++) + { + for (long x = 0; x < nWidth; x++) + { + // This processes [nRadius * 2 + 1] pixels of source per resulting pixel + // TODO: try to optimize this to not process same pixels repeatedly + Color aResult = MorphologyOp::initColor; + const long iMax = std::min(x + nRadius, nLastIndex); + for (long i = std::max(x - nRadius, 0L); i <= iMax; ++i) + OpHelper<MorphologyOp>::apply(aResult, pReadAccess->GetColor(y, i)); + + pWriteAccess->SetPixel(y, x, aResult); + } + } + } + + static void Vertical(FilterSharedData const& rShared, const long nStart, const long nEnd) + { + BitmapReadAccess* pReadAccess = rShared.mpReadAccess; + BitmapWriteAccess* pWriteAccess = rShared.mpWriteAccess; + + const long nHeight = pReadAccess->Height(); + const long nLastIndex = nHeight - 1; + + const long nRadius = rShared.mnRadius; + + for (long x = nStart; x <= nEnd; x++) + { + for (long y = 0; y < nHeight; y++) + { + // This processes [nRadius * 2 + 1] pixels of source per resulting pixel + // TODO: try to optimize this to not process same pixels repeatedly + Color aResult = MorphologyOp::initColor; + const long iMax = std::min(y + nRadius, nLastIndex); + for (long i = std::max(y - nRadius, 0L); i <= iMax; ++i) + OpHelper<MorphologyOp>::apply(aResult, pReadAccess->GetColor(i, x)); + + pWriteAccess->SetPixel(y, x, aResult); + } + } + } +}; + +typedef void (*passFn)(FilterSharedData const& rShared, long nStart, long nEnd); + +class FilterTask : public comphelper::ThreadTask +{ + passFn mpFunction; + FilterSharedData& mrShared; + long mnStart; + long mnEnd; + +public: + explicit FilterTask(const std::shared_ptr<comphelper::ThreadTaskTag>& pTag, passFn pFunction, + FilterSharedData& rShared, long nStart, long nEnd) + : comphelper::ThreadTask(pTag) + , mpFunction(pFunction) + , mrShared(rShared) + , mnStart(nStart) + , mnEnd(nEnd) + { + } + + virtual void doWork() override { mpFunction(mrShared, mnStart, mnEnd); } +}; + +constexpr long nThreadStrip = 16; + +template <typename MorphologyOp, int nComponentWidth> +void runFilter(Bitmap& rBitmap, const long nRadius, const bool bParallel) +{ + using myPass = pass<MorphologyOp, nComponentWidth>; + if (bParallel) + { + try + { + comphelper::ThreadPool& rShared = comphelper::ThreadPool::getSharedOptimalPool(); + auto pTag = comphelper::ThreadPool::createThreadTaskTag(); + + { + Bitmap::ScopedReadAccess pReadAccess(rBitmap); + BitmapScopedWriteAccess pWriteAccess(rBitmap); + FilterSharedData aSharedData(pReadAccess.get(), pWriteAccess.get(), nRadius); + + const long nLastIndex = pReadAccess->Height() - 1; + long nStripStart = 0; + for (; nStripStart < nLastIndex - nThreadStrip; nStripStart += nThreadStrip) + { + long 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.get(), pWriteAccess.get(), nRadius); + + const long nLastIndex = pReadAccess->Width() - 1; + long nStripStart = 0; + for (; nStripStart < nLastIndex - nThreadStrip; nStripStart += nThreadStrip) + { + long 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.get(), pWriteAccess.get(), nRadius); + long nFirstIndex = 0; + long nLastIndex = pReadAccess->Height() - 1; + myPass::Horizontal(aSharedData, nFirstIndex, nLastIndex); + } + { + Bitmap::ScopedReadAccess pReadAccess(rBitmap); + BitmapScopedWriteAccess pWriteAccess(rBitmap); + FilterSharedData aSharedData(pReadAccess.get(), pWriteAccess.get(), nRadius); + long nFirstIndex = 0; + long nLastIndex = pReadAccess->Width() - 1; + myPass::Vertical(aSharedData, nFirstIndex, nLastIndex); + } + } +} + +template <int nComponentWidth> +void runFilter(Bitmap& rBitmap, BasicMorphologyOp op, sal_Int32 nRadius) +{ + const bool bParallel = true; + + if (op == BasicMorphologyOp::erode) + runFilter<ErodeOp, nComponentWidth>(rBitmap, nRadius, bParallel); + else if (op == BasicMorphologyOp::dilate) + runFilter<DilateOp, nComponentWidth>(rBitmap, nRadius, bParallel); +} + +} // end anonymous namespace + +BitmapBasicMorphologyFilter::BitmapBasicMorphologyFilter(BasicMorphologyOp op, sal_Int32 nRadius) + : m_eOp(op) + , m_nRadius(nRadius) +{ +} + +BitmapBasicMorphologyFilter::~BitmapBasicMorphologyFilter() = default; + +BitmapEx BitmapBasicMorphologyFilter::execute(BitmapEx const& rBitmapEx) const +{ + Bitmap aBitmap = rBitmapEx.GetBitmap(); + Bitmap result = filter(aBitmap); + return BitmapEx(result, rBitmapEx.GetMask()); +} + +Bitmap BitmapBasicMorphologyFilter::filter(Bitmap const& rBitmap) const +{ + Bitmap bitmapCopy(rBitmap); + ScanlineFormat nScanlineFormat; + { + Bitmap::ScopedReadAccess pReadAccess(bitmapCopy); + nScanlineFormat = pReadAccess->GetScanlineFormat(); + } + + switch (nScanlineFormat) + { + case ScanlineFormat::N24BitTcRgb: + case ScanlineFormat::N24BitTcBgr: + runFilter<24>(bitmapCopy, m_eOp, m_nRadius); + break; + case ScanlineFormat::N32BitTcMask: + case ScanlineFormat::N32BitTcBgra: + runFilter<32>(bitmapCopy, m_eOp, m_nRadius); + break; + case ScanlineFormat::N8BitPal: + runFilter<8>(bitmapCopy, m_eOp, m_nRadius); + break; + // TODO: handle 1-bit images + default: + // Use access' GetColor/SetPixel fallback + runFilter<0>(bitmapCopy, m_eOp, m_nRadius); + break; + } + + return bitmapCopy; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |