diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a38f936..9c2f59c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15) cmake_policy(SET CMP0091 NEW) # enable new "MSVC runtime library selection" (https://cmake.org/cmake/help/latest/variable/CMAKE_MSVC_RUNTIME_LIBRARY.html) project(libCZI - VERSION 0.54.3 + VERSION 0.55.0 HOMEPAGE_URL "https://github.com/ZEISS/libczi" DESCRIPTION "libCZI is an Open Source Cross-Platform C++ library to read and write CZI") diff --git a/Src/CMakeLists.txt b/Src/CMakeLists.txt index aa835964..54ba5e5a 100644 --- a/Src/CMakeLists.txt +++ b/Src/CMakeLists.txt @@ -44,6 +44,8 @@ if (LIBCZI_BUILD_CURL_BASED_STREAM) # configure libcurl-build as a static library, for possible options -> c.f. https://github.com/curl/curl/blob/master/CMakeLists.txt set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE) + set(BUILD_STATIC_CURL ON CACHE BOOL "" FORCE) + set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE) set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "" FORCE) set(ENABLE_UNICODE ON CACHE BOOL "" FORCE) @@ -59,4 +61,4 @@ endif(LIBCZI_BUILD_CZICMD) if (LIBCZI_BUILD_UNITTESTS) add_subdirectory(libCZI_UnitTests) -endif(LIBCZI_BUILD_UNITTESTS) \ No newline at end of file +endif(LIBCZI_BUILD_UNITTESTS) diff --git a/Src/CZICmd/cmdlineoptions.cpp b/Src/CZICmd/cmdlineoptions.cpp index 6b229062..0e980e8b 100644 --- a/Src/CZICmd/cmdlineoptions.cpp +++ b/Src/CZICmd/cmdlineoptions.cpp @@ -1263,7 +1263,7 @@ bool CCmdLineOptions::TryParseDisplaySettings(const std::string& s, std::mapat(get<0>(it)) = get<1>(it); + multiChannelCompositeChannelInfos->operator[](get<0>(it)) = get<1>(it); } } diff --git a/Src/libCZI/BitmapOperations.cpp b/Src/libCZI/BitmapOperations.cpp index dd0138d1..dac1588b 100644 --- a/Src/libCZI/BitmapOperations.cpp +++ b/Src/libCZI/BitmapOperations.cpp @@ -283,7 +283,7 @@ using namespace std; for (int y = 0; y < h; ++y) { void* p = static_cast(ptr) + (y * static_cast(stride)); - memset(p, val, stride); + memset(p, val, w); } } diff --git a/Src/libCZI/CreateBitmap.cpp b/Src/libCZI/CreateBitmap.cpp index 9254312a..4f8d77d5 100644 --- a/Src/libCZI/CreateBitmap.cpp +++ b/Src/libCZI/CreateBitmap.cpp @@ -46,7 +46,7 @@ static std::shared_ptr CreateBitmapFromSubBlock_Uncompresse const auto& sbBlkInfo = subBlk->GetSubBlockInfo(); - // TODO: How exactly shoud the stride be derived? It seems that stride must be exactly linesize. + // The stride with an uncompressed bitmap in CZI is exactly the linesize. const std::uint32_t stride = sbBlkInfo.physicalSize.w * CziUtils::GetBytesPerPel(sbBlkInfo.pixelType); if (static_cast(stride) * sbBlkInfo.physicalSize.h > size) { diff --git a/Src/libCZI/Doc/version-history.markdown b/Src/libCZI/Doc/version-history.markdown index 625d1fd9..5996d5c5 100644 --- a/Src/libCZI/Doc/version-history.markdown +++ b/Src/libCZI/Doc/version-history.markdown @@ -7,3 +7,6 @@ version history {#version_history} 0.53.1 | [68](https://github.com/ZEISS/libczi/pull/68) | preserve the order of entries in attachment-directory (as they were added to the writer) 0.53.2 | [70](https://github.com/ZEISS/libczi/pull/70) | add option to write 'duplicate' subblocks 0.54.0 | [71](https://github.com/ZEISS/libczi/pull/71) | introduce 'streamsLib', add curl-based stream class + 0.54.2 | [74](https://github.com/ZEISS/libczi/pull/74) | minor bug fix + 0.54.3 | [79](https://github.com/ZEISS/libczi/pull/79) | add option _kCurlHttp_FollowLocation_ to follow HTTP redirects tp curl_http_inputstream + 0.55.0 | [78](https://github.com/ZEISS/libczi/pull/78) | optimization: for multi-tile-composition, check relevant tiles for visibility before loading them (and do not load/decode non-visible tiles) \ No newline at end of file diff --git a/Src/libCZI/SingleChannelAccessorBase.cpp b/Src/libCZI/SingleChannelAccessorBase.cpp index 91252188..b0a9c815 100644 --- a/Src/libCZI/SingleChannelAccessorBase.cpp +++ b/Src/libCZI/SingleChannelAccessorBase.cpp @@ -4,6 +4,7 @@ #include "SingleChannelAccessorBase.h" #include "BitmapOperations.h" +#include "utilities.h" using namespace std; using namespace libCZI; @@ -14,7 +15,7 @@ bool CSingleChannelAccessorBase::TryGetPixelType(const libCZI::IDimCoordinate* p planeCoordinate->TryGetPosition(libCZI::DimensionIndex::C, &c); // the idea is: for the cornerstone-case where we do not have a C-index, the call to "TryGetSubBlockInfoOfArbitrarySubBlockInChannel" - // will igonore the specified index _if_ there are no C-indices at all + // will ignore the specified index _if_ there are no C-indices at all pixeltype = Utils::TryDeterminePixelTypeForChannel(this->sbBlkRepository.get(), c); return (pixeltype != PixelType::Invalid) ? true : false; } @@ -80,3 +81,56 @@ void CSingleChannelAccessorBase::CheckPlaneCoordinates(const libCZI::IDimCoordin } } } + +std::vector CSingleChannelAccessorBase::CheckForVisibility(const libCZI::IntRect& roi, int count, const std::function& get_subblock_index) const +{ + return CSingleChannelAccessorBase::CheckForVisibilityCore( + roi, + count, + get_subblock_index, + [this](int subblock_index) -> IntRect + { + SubBlockInfo subblock_info; + bool b = this->sbBlkRepository->TryGetSubBlockInfo(subblock_index, &subblock_info); + return subblock_info.logicalRect; + }); +} + +/*static*/std::vector CSingleChannelAccessorBase::CheckForVisibilityCore(const libCZI::IntRect& roi, int count, const std::function& get_subblock_index, const std::function& get_rect_of_subblock) +{ + std::vector result; + + // handle the trivial cases + if (count == 0 || !roi.IsNonEmpty()) + { + return result; + } + + const int64_t total_pixel_count = static_cast(roi.w) * roi.h; + result.reserve(count); + RectangleCoverageCalculator coverage_calculator; + int64_t covered_pixel_count = 0; + for (int i = count -1; i >= 0; --i) // we start at the end, because that is the subblock which is rendered last (and thus is on top) + { + const int subblock_index = get_subblock_index(i); + coverage_calculator.AddRectangle(get_rect_of_subblock(subblock_index)); + const int64_t new_covered_pixel_count = coverage_calculator.CalcAreaOfIntersectionWithRectangle(roi); + if (new_covered_pixel_count > covered_pixel_count) // if the covered pixel count has increased, it means that this subblock covers some new pixels, + { // some pixels which were not overdrawn by all the previous ones + // this means - when drawing this subblock, some new pixels will be covered which were not covered before, + // so we need to draw this subblock, therefore we add it to our result vector + result.push_back(subblock_index); + + covered_pixel_count = new_covered_pixel_count; + if (new_covered_pixel_count == total_pixel_count) + { + // if the whole ROI is covered now, then we are done + break; + } + } + } + + // now, reverse the result vector, so that the subblocks are in the order in which they are to be rendered + std::reverse(result.begin(), result.end()); + return result; +} diff --git a/Src/libCZI/SingleChannelAccessorBase.h b/Src/libCZI/SingleChannelAccessorBase.h index 1d48b401..7a18d993 100644 --- a/Src/libCZI/SingleChannelAccessorBase.h +++ b/Src/libCZI/SingleChannelAccessorBase.h @@ -12,7 +12,7 @@ class CSingleChannelAccessorBase protected: std::shared_ptr sbBlkRepository; - explicit CSingleChannelAccessorBase(std::shared_ptr sbBlkRepository) + explicit CSingleChannelAccessorBase(const std::shared_ptr& sbBlkRepository) : sbBlkRepository(sbBlkRepository) {} @@ -21,4 +21,49 @@ class CSingleChannelAccessorBase static void Clear(libCZI::IBitmapData* bm, const libCZI::RgbFloatColor& floatColor); void CheckPlaneCoordinates(const libCZI::IDimCoordinate* planeCoordinate) const; + + /// This method is used to do a visibility test of a list of subblocks. The mode of operation is as follows: + /// - The method is given a ROI, and the number of subblocks to check. + /// - The functor 'get_subblock_index' is called with the argument being a counter, starting with count-1 and counting down to zero. + /// If called with value count-1, the subblock is the **last** one to be rendered; if called with value count-2, the subblock is the + /// second-to-the-last one to be rendered, and so on. If called with 0, the subblock is the **first** one to be rendered. + /// The value it returns is the subblock index (in the subblock repository) to check. + /// - The subblocks are assumed to be rendered in the order given, so the one we get by calling 'get_subblock_index' with + /// argument 0 is the first one to be rendered, the one with argument 1 is the second one, and so on. The rendering + /// is assumed to be done with the 'painter's algorithm", so what is rendered last is on top. + /// - We return a list of indices which are to be rendered, potentially leaving out some which have been determined + /// as not being visible. The indices returned are "indices as used by the 'get_subblock_index' functor", i.e. + /// it is **not** the subblock-index, but the argument that was passed to the functor. + /// - The caller can then use this list to render the subblocks (in the order as given in this vector). + /// + /// \param roi The roi - if this is empty or invalid, then an empty vector is returned. + /// \param count Number of subblocks (specifying how many times the get_subblock_index-functor is being called). + /// \param get_subblock_index Functor which gives the subblock index to check. This index is the index in the subblock repository. + /// + /// \returns A list of indices of "arguments to the functor which delivered a visible subblock". If the subblocks are rendered in the order + /// given here, then the result is guaranteed to be the same as if all subblocks were rendered. + std::vector CheckForVisibility(const libCZI::IntRect& roi, int count, const std::function& get_subblock_index) const; + + /// Do a visibility check for a list of subblocks. This is the core method, which is used by the public method 'CheckForVisibility'. + /// What this function does, is: + /// - The method is given a ROI, and the number of subblocks to check. + /// - The function 'get_subblock_index' will be called with the argument being a counter, starting with count-1 and counting down to zero. + /// If called with value count-1, the subblock is the **last** one to be rendered; if called with value count-2, the subblock is the + /// second-to-the-last one to be rendered, and so on. If called with 0, the subblock is the **first** one to be rendered. + /// - The value it returned by the functor 'get_subblock_index' is then used with the functor 'get_rect_of_subblock' to get the rectangle. The + /// index returned by 'get_subblock_index' is passed in to the functor 'get_rect_of_subblock'. The rectangle returned by 'get_rect_of_subblock' + /// is the rectangle of the subblock, the region where this subblock is rendered. + /// - We return a list of indices which are to be rendered, potentially leaving out some which have been determined + /// as not being visible. The indices returned are "indices as used by the 'get_subblock_index' functor", i.e. + /// it is **not** the subblock-index, but the argument that was passed to the functor. + /// + /// \param roi The roi - if this is empty or invalid, then an empty vector is returned. + /// \param count Number of subblocks (specifying how many times the get_subblock_index-functor is being called). + /// \param get_subblock_index Functor which gives the subblock index for a given counter. The counter starts with count-1 and counts down to zero. + /// \param get_rect_of_subblock Functor which gives the subblock rectangle for a subblock-index (as returned by 'get_subblock_index'). + /// + /// \returns A list of indices of "arguments to the functor which delivered a visible subblock". If the subblocks are rendered in the order + /// given here, then the result is guaranteed to be the same as if all subblocks were rendered. Non-visible subblocks are not + /// part of this list. + static std::vector CheckForVisibilityCore(const libCZI::IntRect& roi, int count, const std::function& get_subblock_index, const std::function& get_rect_of_subblock); }; diff --git a/Src/libCZI/SingleChannelPyramidLevelTileAccessor.cpp b/Src/libCZI/SingleChannelPyramidLevelTileAccessor.cpp index e162e3a7..cf83d8fc 100644 --- a/Src/libCZI/SingleChannelPyramidLevelTileAccessor.cpp +++ b/Src/libCZI/SingleChannelPyramidLevelTileAccessor.cpp @@ -10,14 +10,20 @@ using namespace libCZI; using namespace std; -CSingleChannelPyramidLevelTileAccessor::CSingleChannelPyramidLevelTileAccessor(std::shared_ptr sbBlkRepository) +CSingleChannelPyramidLevelTileAccessor::CSingleChannelPyramidLevelTileAccessor(const std::shared_ptr& sbBlkRepository) : CSingleChannelAccessorBase(sbBlkRepository) { } /*virtual*/std::shared_ptr CSingleChannelPyramidLevelTileAccessor::Get(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, const PyramidLayerInfo& pyramidInfo, const ISingleChannelPyramidLayerTileAccessor::Options* pOptions) { - if (pOptions == nullptr) { Options opt; opt.Clear(); return this->Get(roi, planeCoordinate, pyramidInfo, &opt); } + if (pOptions == nullptr) + { + Options opt; + opt.Clear(); + return this->Get(roi, planeCoordinate, pyramidInfo, &opt); + } + libCZI::PixelType pixelType; const bool b = this->TryGetPixelType(planeCoordinate, pixelType); if (b == false) @@ -30,7 +36,13 @@ CSingleChannelPyramidLevelTileAccessor::CSingleChannelPyramidLevelTileAccessor(s /*virtual*/std::shared_ptr CSingleChannelPyramidLevelTileAccessor::Get(libCZI::PixelType pixeltype, const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, const PyramidLayerInfo& pyramidInfo, const libCZI::ISingleChannelPyramidLayerTileAccessor::Options* pOptions) { - if (pOptions == nullptr) { Options opt; opt.Clear(); return this->Get(pixeltype, roi, planeCoordinate, pyramidInfo, &opt); } + if (pOptions == nullptr) + { + Options opt; + opt.Clear(); + return this->Get(pixeltype, roi, planeCoordinate, pyramidInfo, &opt); + } + const int sizeOfPixel = CalcSizeOfPixelOnLayer0(pyramidInfo); const IntSize sizeOfBitmap{ static_cast(roi.w / sizeOfPixel),static_cast(roi.h / sizeOfPixel) }; if (sizeOfBitmap.w == 0 || sizeOfBitmap.h == 0) @@ -46,7 +58,14 @@ CSingleChannelPyramidLevelTileAccessor::CSingleChannelPyramidLevelTileAccessor(s /*virtual*/void CSingleChannelPyramidLevelTileAccessor::Get(libCZI::IBitmapData* pDest, int xPos, int yPos, const libCZI::IDimCoordinate* planeCoordinate, const PyramidLayerInfo& pyramidInfo, const Options* pOptions) { - if (pOptions == nullptr) { Options opt; opt.Clear(); this->Get(pDest, xPos, yPos, planeCoordinate, pyramidInfo, &opt); return; } + if (pOptions == nullptr) + { + Options opt; + opt.Clear(); + this->Get(pDest, xPos, yPos, planeCoordinate, pyramidInfo, &opt); + return; + } + const int sizeOfPixel = CalcSizeOfPixelOnLayer0(pyramidInfo); this->InternalGet(pDest, xPos, yPos, sizeOfPixel, planeCoordinate, pyramidInfo, *pOptions); } @@ -74,7 +93,7 @@ void CSingleChannelPyramidLevelTileAccessor::InternalGet(libCZI::IBitmapData* pD }); } -void CSingleChannelPyramidLevelTileAccessor::ComposeTiles(libCZI::IBitmapData* bm, int xPos, int yPos, int sizeOfPixel, int bitmapCnt, const Options& options, std::function getSbInfo) +void CSingleChannelPyramidLevelTileAccessor::ComposeTiles(libCZI::IBitmapData* bm, int xPos, int yPos, int sizeOfPixel, int bitmapCnt, const Options& options, const std::function& getSbInfo) { Compositors::ComposeSingleTileOptions composeOptions; composeOptions.Clear(); composeOptions.drawTileBorder = options.drawTileBorder; @@ -92,13 +111,12 @@ void CSingleChannelPyramidLevelTileAccessor::ComposeTiles(libCZI::IBitmapData* b return true; } - return false; + return false; }, bm, - 0, - 0, - &composeOptions); - + 0, + 0, + &composeOptions); } libCZI::IntRect CSingleChannelPyramidLevelTileAccessor::CalcDestinationRectFromSourceRect(const libCZI::IntRect& roi, const PyramidLayerInfo& pyramidInfo) @@ -226,16 +244,16 @@ void CSingleChannelPyramidLevelTileAccessor::GetAllSubBlocks(const libCZI::IntRe } } - if (Utilities::DoIntersect(roi, info.logicalRect)) - { - SbInfo sbinfo; - sbinfo.logicalRect = info.logicalRect; - sbinfo.physicalSize = info.physicalSize; - sbinfo.mIndex = info.mIndex; - sbinfo.index = idx; - appender(sbinfo); - } + if (Utilities::DoIntersect(roi, info.logicalRect)) + { + SbInfo sbinfo; + sbinfo.logicalRect = info.logicalRect; + sbinfo.physicalSize = info.physicalSize; + sbinfo.mIndex = info.mIndex; + sbinfo.index = idx; + appender(sbinfo); + } - return true; + return true; }); } diff --git a/Src/libCZI/SingleChannelPyramidLevelTileAccessor.h b/Src/libCZI/SingleChannelPyramidLevelTileAccessor.h index a8db2b17..6f62ecfb 100644 --- a/Src/libCZI/SingleChannelPyramidLevelTileAccessor.h +++ b/Src/libCZI/SingleChannelPyramidLevelTileAccessor.h @@ -28,7 +28,7 @@ class CSingleChannelPyramidLevelTileAccessor : public CSingleChannelAccessorBase }; public: - explicit CSingleChannelPyramidLevelTileAccessor(std::shared_ptr sbBlkRepository); + explicit CSingleChannelPyramidLevelTileAccessor(const std::shared_ptr& sbBlkRepository); public: // interface ISingleChannelTileAccessor std::shared_ptr Get(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, const PyramidLayerInfo& pyramidInfo, const Options* pOptions) override; @@ -51,7 +51,7 @@ class CSingleChannelPyramidLevelTileAccessor : public CSingleChannelAccessorBase int CalcPyramidLayerNo(const libCZI::IntRect& logicalRect, const libCZI::IntSize& physicalSize, int minificationFactor); - void ComposeTiles(libCZI::IBitmapData* bm, int xPos, int yPos, int sizeOfPixel, int bitmapCnt, const Options& options, std::function getSbInfo); + void ComposeTiles(libCZI::IBitmapData* bm, int xPos, int yPos, int sizeOfPixel, int bitmapCnt, const Options& options, const std::function& getSbInfo); void InternalGet(libCZI::IBitmapData* pDest, int xPos, int yPos, int sizeOfPixelOnLayer0, const libCZI::IDimCoordinate* planeCoordinate, const PyramidLayerInfo& pyramidInfo, const Options& options); }; diff --git a/Src/libCZI/SingleChannelScalingTileAccessor.cpp b/Src/libCZI/SingleChannelScalingTileAccessor.cpp index 881e9026..d99d988f 100644 --- a/Src/libCZI/SingleChannelScalingTileAccessor.cpp +++ b/Src/libCZI/SingleChannelScalingTileAccessor.cpp @@ -10,7 +10,7 @@ using namespace libCZI; using namespace std; -CSingleChannelScalingTileAccessor::CSingleChannelScalingTileAccessor(std::shared_ptr sbBlkRepository) +CSingleChannelScalingTileAccessor::CSingleChannelScalingTileAccessor(const std::shared_ptr& sbBlkRepository) : CSingleChannelAccessorBase(sbBlkRepository) { } @@ -22,7 +22,12 @@ CSingleChannelScalingTileAccessor::CSingleChannelScalingTileAccessor(std::shared /*virtual*/ std::shared_ptr CSingleChannelScalingTileAccessor::Get(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, float zoom, const libCZI::ISingleChannelScalingTileAccessor::Options* pOptions) { - if (pOptions == nullptr) { Options opt; opt.Clear(); return this->Get(roi, planeCoordinate, zoom, &opt); } + if (pOptions == nullptr) + { + Options opt; opt.Clear(); + return this->Get(roi, planeCoordinate, zoom, &opt); + } + libCZI::PixelType pixelType; const bool b = this->TryGetPixelType(planeCoordinate, pixelType); if (b == false) @@ -35,7 +40,12 @@ CSingleChannelScalingTileAccessor::CSingleChannelScalingTileAccessor(std::shared /*virtual*/std::shared_ptr CSingleChannelScalingTileAccessor::Get(libCZI::PixelType pixeltype, const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, float zoom, const libCZI::ISingleChannelScalingTileAccessor::Options* pOptions) { - if (pOptions == nullptr) { Options opt; opt.Clear(); return this->Get(pixeltype, roi, planeCoordinate, zoom, &opt); } + if (pOptions == nullptr) + { + Options opt; opt.Clear(); + return this->Get(pixeltype, roi, planeCoordinate, zoom, &opt); + } + const IntSize sizeOfBitmap = InternalCalcSize(roi, zoom); auto bmDest = GetSite()->CreateBitmap(pixeltype, sizeOfBitmap.w, sizeOfBitmap.h); this->InternalGet(bmDest.get(), roi, planeCoordinate, zoom, *pOptions); @@ -44,7 +54,11 @@ CSingleChannelScalingTileAccessor::CSingleChannelScalingTileAccessor(std::shared /*virtual*/void CSingleChannelScalingTileAccessor::Get(libCZI::IBitmapData* pDest, const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, float zoom, const libCZI::ISingleChannelScalingTileAccessor::Options* pOptions) { - if (pOptions == nullptr) { Options opt; opt.Clear(); return this->Get(pDest, roi, planeCoordinate, zoom, &opt); } + if (pOptions == nullptr) + { + Options opt; opt.Clear(); + return this->Get(pDest, roi, planeCoordinate, zoom, &opt); + } const IntSize sizeOfBitmap = InternalCalcSize(roi, zoom); if (sizeOfBitmap.w != pDest->GetWidth() || sizeOfBitmap.h != pDest->GetHeight()) @@ -70,7 +84,7 @@ void CSingleChannelScalingTileAccessor::ScaleBlt(libCZI::IBitmapData* bmDest, fl if (GetSite()->IsEnabled(LOGLEVEL_CHATTYINFORMATION)) { stringstream ss; - ss << " bounds: " << Utils::DimCoordinateToString(&sb->GetSubBlockInfo().coordinate); + ss << " bounds: " << Utils::DimCoordinateToString(&sb->GetSubBlockInfo().coordinate) << " M=" << (Utils::IsValidMindex(sbInfo.mIndex) ? to_string(sbInfo.mIndex) : "invalid"); GetSite()->Log(LOGLEVEL_CHATTYINFORMATION, ss); } @@ -103,7 +117,7 @@ void CSingleChannelScalingTileAccessor::ScaleBlt(libCZI::IBitmapData* bmDest, fl { // calculate the intersection of the subblock (logical rect) and the destination const auto intersect = Utilities::Intersect(sbInfo.logicalRect, roi); - + const double roiSrcTopLeftX = double(intersect.x - sbInfo.logicalRect.x) / sbInfo.logicalRect.w; const double roiSrcTopLeftY = double(intersect.y - sbInfo.logicalRect.y) / sbInfo.logicalRect.h; const double roiSrcBttmRightX = double(intersect.x + intersect.w - sbInfo.logicalRect.x) / sbInfo.logicalRect.w; @@ -195,7 +209,12 @@ std::vector CSingleChannelScalingTileAccessor::CreateSortByZoom(const std:: } else { - std::sort(byZoom.begin(), byZoom.end(), [&](const int i1, const int i2)->bool {return sbBlks.at(i1).GetZoom() < sbBlks.at(i2).GetZoom(); }); + // Sort by zoom only - note that we use "stable_sort" here, because otherwise the order of subblocks with the same zoom-level would be arbitrary. + // This would mean that the result is not idem-potent, i.e. if we call this function twice with the same input, we would get different results. + // This is not a problem for the "sort by M-index" case, because there we have a deterministic sorting. + // With "stable_sort" we ensure that the order of subblocks with the same zoom-level is preserved. This randomness was actually observed + // in case of with stdlibc++ - with MSVC on Windows, the order was always the same. + std::stable_sort(byZoom.begin(), byZoom.end(), [&](const int i1, const int i2)->bool {return sbBlks.at(i1).GetZoom() < sbBlks.at(i2).GetZoom(); }); } return byZoom; } @@ -273,19 +292,19 @@ void CSingleChannelScalingTileAccessor::InternalGet(libCZI::IBitmapData* bmDest, // we only have to deal with a single scene (or: the document does not include a scene-dimension at all), in this // case we do not have group by scene and save some cycles auto sbSetsortedByZoom = this->GetSubSetFilteredBySceneSortedByZoom(roi, planeCoordinate, scenesInvolved, options.sortByM); - this->Paint(bmDest, roi, sbSetsortedByZoom, zoom); + this->Paint(bmDest, roi, sbSetsortedByZoom, zoom, options.useVisibilityCheckOptimization); } else { const auto sbSetSortedByZoomPerScene = this->GetSubSetSortedByZoomPerScene(scenesInvolved, roi, planeCoordinate, options.sortByM); for (const auto& it : sbSetSortedByZoomPerScene) { - this->Paint(bmDest, roi, get<1>(it), zoom); + this->Paint(bmDest, roi, get<1>(it), zoom, options.useVisibilityCheckOptimization); } } } -void CSingleChannelScalingTileAccessor::Paint(libCZI::IBitmapData* bmDest, const libCZI::IntRect& roi, const SubSetSortedByZoom& sbSetSortedByZoom, float zoom) +void CSingleChannelScalingTileAccessor::Paint(libCZI::IBitmapData* bmDest, const libCZI::IntRect& roi, const SubSetSortedByZoom& sbSetSortedByZoom, float zoom, bool useCoverageOptimization) { const int idxOf1stSubBlockOfZoomGreater = this->GetIdxOf1stSubBlockWithZoomGreater(sbSetSortedByZoom.subBlocks, sbSetSortedByZoom.sortedByZoom, zoom); if (idxOf1stSubBlockOfZoomGreater < 0) @@ -298,29 +317,66 @@ void CSingleChannelScalingTileAccessor::Paint(libCZI::IBitmapData* bmDest, const return; } - std::vector::const_iterator it = sbSetSortedByZoom.sortedByZoom.cbegin(); - std::advance(it, idxOf1stSubBlockOfZoomGreater); + // start_iterator points into the "sortedByZoom" vector, which contains indices into the "subBlocks" vector + std::vector::const_iterator start_iterator = sbSetSortedByZoom.sortedByZoom.cbegin(); + std::advance(start_iterator, idxOf1stSubBlockOfZoomGreater); - const float startZoom = sbSetSortedByZoom.subBlocks.at(*it).GetZoom(); - - for (; it != sbSetSortedByZoom.sortedByZoom.cend(); ++it) + // find the end_iterator - this is the first element in the sortedByZoom-vector which has a zoom-level that is about twice that of the first element + const float startZoom = sbSetSortedByZoom.subBlocks.at(*start_iterator).GetZoom(); + auto end_iterator = start_iterator + 1; + for (; end_iterator != sbSetSortedByZoom.sortedByZoom.cend(); ++end_iterator) { - const SbInfo& sbInfo = sbSetSortedByZoom.subBlocks.at(*it); - + const SbInfo& sbInfo = sbSetSortedByZoom.subBlocks.at(*end_iterator); // as an interim solution (in fact... this seems to be a rather good solution...), stop when we arrive at subblocks with a zoom-level about twice that what we started with if (sbInfo.GetZoom() >= startZoom * 1.9f) { break; } + } - if (GetSite()->IsEnabled(LOGLEVEL_CHATTYINFORMATION)) + if (!useCoverageOptimization) + { + for (auto it = start_iterator; it != end_iterator; ++it) { - stringstream ss; - ss << " Drawing subblock: idx=" << sbInfo.index << " Log.: " << sbInfo.logicalRect << " Phys.Size: " << sbInfo.physicalSize; - GetSite()->Log(LOGLEVEL_CHATTYINFORMATION, ss); + const SbInfo& sbInfo = sbSetSortedByZoom.subBlocks.at(*it); + + if (GetSite()->IsEnabled(LOGLEVEL_CHATTYINFORMATION)) + { + stringstream ss; + ss << " Drawing subblock: idx=" << sbInfo.index << " Log.: " << sbInfo.logicalRect << " Phys.Size: " << sbInfo.physicalSize; + GetSite()->Log(LOGLEVEL_CHATTYINFORMATION, ss); + } + + this->ScaleBlt(bmDest, zoom, roi, sbInfo); } + } + else + { + const auto indices_of_visible_tiles = this->CheckForVisibility( + roi, + static_cast(distance(start_iterator, end_iterator)), // how many subblocks we have in the range [start_iterator, end_iterator) + [&](int index)->int + { + // dereference the iterator (advanced by the index we get), this gives us an index into the + // subBlocks-vector, which we then use to get the subblock-index of the subblock + return sbSetSortedByZoom.subBlocks[*(start_iterator + index)].index; + }); + + // Now, draw only the subblocks which are visible - the vector "indices_of_visible_tiles" contains the indices "as they were passed to the lambda". + for (const auto i : indices_of_visible_tiles) + { + // dereference the iterator (advanced by the index from out loop variable), this gives us an index into the + // subBlocks-vector + const SbInfo& sbInfo = sbSetSortedByZoom.subBlocks.at(*(start_iterator + i)); + if (GetSite()->IsEnabled(LOGLEVEL_CHATTYINFORMATION)) + { + stringstream ss; + ss << " Drawing subblock: idx=" << sbInfo.index << " Log.: " << sbInfo.logicalRect << " Phys.Size: " << sbInfo.physicalSize; + GetSite()->Log(LOGLEVEL_CHATTYINFORMATION, ss); + } - this->ScaleBlt(bmDest, zoom, roi, sbInfo); + this->ScaleBlt(bmDest, zoom, roi, sbInfo); + } } } @@ -383,7 +439,7 @@ std::vectorGetSubSet(roi, &coord, nullptr); sbset.sortedByZoom = this->CreateSortByZoom(sbset.subBlocks, sortByM); - result.emplace_back(make_tuple(sceneIdx, sbset)); + result.emplace_back(sceneIdx, sbset); } return result; diff --git a/Src/libCZI/SingleChannelScalingTileAccessor.h b/Src/libCZI/SingleChannelScalingTileAccessor.h index e7c3c8a9..476ddec4 100644 --- a/Src/libCZI/SingleChannelScalingTileAccessor.h +++ b/Src/libCZI/SingleChannelScalingTileAccessor.h @@ -25,7 +25,7 @@ class CSingleChannelScalingTileAccessor : public CSingleChannelAccessorBase, pub }; public: - explicit CSingleChannelScalingTileAccessor(std::shared_ptr sbBlkRepository); + explicit CSingleChannelScalingTileAccessor(const std::shared_ptr& sbBlkRepository); public: // interface ISingleChannelScalingTileAccessor libCZI::IntSize CalcSize(const libCZI::IntRect& roi, float zoom) const override; @@ -44,14 +44,16 @@ class CSingleChannelScalingTileAccessor : public CSingleChannelAccessorBase, pub std::vector DetermineInvolvedScenes(const libCZI::IntRect& roi, const libCZI::IIndexSet* pSceneIndexSet); + /// This struct contains a vector of subblocks, and a vector of indices into this vector which gives an ordering + /// by zoom of the subblocks. struct SubSetSortedByZoom { - std::vector subBlocks; - std::vector sortedByZoom; + std::vector subBlocks; ///< The vector containing the subblocks (which are in no particular order). + std::vector sortedByZoom; ///< Vector with indices (into the vector 'subBlocks') which gives the ordering by zoom. }; SubSetSortedByZoom GetSubSetFilteredBySceneSortedByZoom(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, const std::vector& allowedScenes, bool sortByM); std::vector> GetSubSetSortedByZoomPerScene(const std::vector& scenes, const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, bool sortByM); - void Paint(libCZI::IBitmapData* bmDest, const libCZI::IntRect& roi, const SubSetSortedByZoom& sbSetSortedByZoom, float zoom); + void Paint(libCZI::IBitmapData* bmDest, const libCZI::IntRect& roi, const SubSetSortedByZoom& sbSetSortedByZoom, float zoom, bool useCoverageOptimization); }; diff --git a/Src/libCZI/SingleChannelTileAccessor.cpp b/Src/libCZI/SingleChannelTileAccessor.cpp index d58452d2..fcfc6c0b 100644 --- a/Src/libCZI/SingleChannelTileAccessor.cpp +++ b/Src/libCZI/SingleChannelTileAccessor.cpp @@ -7,13 +7,12 @@ #include "utilities.h" #include "SingleChannelTileCompositor.h" #include "Site.h" -#include #include "bitmapData.h" using namespace libCZI; using namespace std; -CSingleChannelTileAccessor::CSingleChannelTileAccessor(std::shared_ptr sbBlkRepository) +CSingleChannelTileAccessor::CSingleChannelTileAccessor(const std::shared_ptr& sbBlkRepository) : CSingleChannelAccessorBase(sbBlkRepository) { } @@ -45,26 +44,66 @@ CSingleChannelTileAccessor::CSingleChannelTileAccessor(std::shared_ptr& subBlocksSet, const ISingleChannelTileAccessor::Options& options) { - Compositors::ComposeSingleTileOptions composeOptions; composeOptions.Clear(); + Compositors::ComposeSingleTileOptions composeOptions; + composeOptions.Clear(); composeOptions.drawTileBorder = options.drawTileBorder; - Compositors::ComposeSingleChannelTiles( - [&](int index, std::shared_ptr& spBm, int& xPosTile, int& yPosTile)->bool - { - if (index < static_cast(subBlocksSet.size())) + + if (options.useVisibilityCheckOptimization) + { + // Try to reduce the number of subblocks to be rendered by doing a visibility check, and only rendering those which are visible. + // We report the subblocks in the order as they are given in the vector 'subBlocksSet', the lambda will be called with the + // argument 'index' counting down from subBlocksSet.size()-1 to 0. The subblock index we report for 'index=0' is the first one + // to be rendered, and 'index=subBlocksSet.size()-1' is the last one to be rendered (on top of all the others). + // We get a vector with the indices of the subblocks to be rendered, and then render them in the order as given in this vector + // (index here means - the number as passed to the lambda). + const auto indices_of_visible_tiles = this->CheckForVisibility( + { xPos, yPos, static_cast(pBm->GetWidth()), static_cast(pBm->GetHeight()) }, + static_cast(subBlocksSet.size()), + [&](int index)->int { - const auto sb = this->sbBlkRepository->ReadSubBlock(subBlocksSet[index].index); - spBm = sb->CreateBitmap(); - xPosTile = sb->GetSubBlockInfo().logicalRect.x; - yPosTile = sb->GetSubBlockInfo().logicalRect.y; - return true; - } + return subBlocksSet[index].index; + }); - return false; - }, - pBm, + Compositors::ComposeSingleChannelTiles( + [&](int index, std::shared_ptr& spBm, int& xPosTile, int& yPosTile)->bool + { + if (index < static_cast(indices_of_visible_tiles.size())) + { + const auto sb = this->sbBlkRepository->ReadSubBlock(subBlocksSet[indices_of_visible_tiles[index]].index); + spBm = sb->CreateBitmap(); + xPosTile = sb->GetSubBlockInfo().logicalRect.x; + yPosTile = sb->GetSubBlockInfo().logicalRect.y; + return true; + } + + return false; + }, + pBm, xPos, yPos, &composeOptions); + } + else + { + Compositors::ComposeSingleChannelTiles( + [&](int index, std::shared_ptr& spBm, int& xPosTile, int& yPosTile)->bool + { + if (index < static_cast(subBlocksSet.size())) + { + const auto sb = this->sbBlkRepository->ReadSubBlock(subBlocksSet[index].index); + spBm = sb->CreateBitmap(); + xPosTile = sb->GetSubBlockInfo().logicalRect.x; + yPosTile = sb->GetSubBlockInfo().logicalRect.y; + return true; + } + + return false; + }, + pBm, + xPos, + yPos, + &composeOptions); + } } void CSingleChannelTileAccessor::InternalGet(int xPos, int yPos, libCZI::IBitmapData* pBm, const IDimCoordinate* planeCoordinate, const ISingleChannelTileAccessor::Options* pOptions) @@ -79,13 +118,13 @@ void CSingleChannelTileAccessor::InternalGet(int xPos, int yPos, libCZI::IBitmap this->CheckPlaneCoordinates(planeCoordinate); Clear(pBm, pOptions->backGroundColor); const IntSize sizeBm = pBm->GetSize(); - IntRect roi{ xPos,yPos,static_cast(sizeBm.w),static_cast(sizeBm.h) }; + const IntRect roi{ xPos,yPos,static_cast(sizeBm.w),static_cast(sizeBm.h) }; const std::vector subBlocksSet = this->GetSubBlocksSubset(roi, planeCoordinate, pOptions->sortByM); this->ComposeTiles(pBm, xPos, yPos, subBlocksSet, *pOptions); } -std::vector CSingleChannelTileAccessor::GetSubBlocksSubset(const IntRect& roi, const IDimCoordinate* planeCoordinate, bool sortByM /*,libCZI::PixelType* pPixelTypeOfFirstFoundSubBlock=nullptr*/) +std::vector CSingleChannelTileAccessor::GetSubBlocksSubset(const IntRect& roi, const IDimCoordinate* planeCoordinate, bool sortByM) { // ok... for a first tentative, experimental and quick-n-dirty implementation, simply // get all subblocks by enumerating all @@ -106,7 +145,7 @@ std::vector CSingleChannelTileAccessor::G return subBlocksSet; } -void CSingleChannelTileAccessor::GetAllSubBlocks(const IntRect& roi, const IDimCoordinate* planeCoordinate, std::function appender/*, libCZI::PixelType* pPixelTypeOfFirstFoundSubBlock*/) +void CSingleChannelTileAccessor::GetAllSubBlocks(const IntRect& roi, const IDimCoordinate* planeCoordinate, const std::function& appender) const { this->sbBlkRepository->EnumSubset(planeCoordinate, nullptr, true, [&](int idx, const SubBlockInfo& info)->bool @@ -116,7 +155,7 @@ void CSingleChannelTileAccessor::GetAllSubBlocks(const IntRect& roi, const IDimC appender(idx, info.mIndex); } - return true; + return true; }); } diff --git a/Src/libCZI/SingleChannelTileAccessor.h b/Src/libCZI/SingleChannelTileAccessor.h index d126fc9a..b9007e3b 100644 --- a/Src/libCZI/SingleChannelTileAccessor.h +++ b/Src/libCZI/SingleChannelTileAccessor.h @@ -13,7 +13,7 @@ class CSingleChannelTileAccessor : public CSingleChannelAccessorBase, public libCZI::ISingleChannelTileAccessor { public: - explicit CSingleChannelTileAccessor(std::shared_ptr sbBlkRepository); + explicit CSingleChannelTileAccessor(const std::shared_ptr& sbBlkRepository); public: // interface ISingleChannelTileAccessor std::shared_ptr Get(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, const libCZI::ISingleChannelTileAccessor::Options* pOptions) override; @@ -21,8 +21,7 @@ class CSingleChannelTileAccessor : public CSingleChannelAccessorBase, public lib void Get(libCZI::IBitmapData* pDest, int xPos, int yPos, const libCZI::IDimCoordinate* planeCoordinate, const Options* pOptions) override; private: void InternalGet(int xPos, int yPos, libCZI::IBitmapData* pBm, const libCZI::IDimCoordinate* planeCoordinate, const libCZI::ISingleChannelTileAccessor::Options* pOptions); - //std::shared_ptr InternalGet(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, const ISingleChannelTileAccessor::Options* pOptions); - void GetAllSubBlocks(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, std::function appender/*, libCZI::PixelType* pPixelTypeOfFirstFoundSubBlock*/); + void GetAllSubBlocks(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, const std::function& appender) const; struct IndexAndM { @@ -30,6 +29,6 @@ class CSingleChannelTileAccessor : public CSingleChannelAccessorBase, public lib int mIndex; }; - std::vector GetSubBlocksSubset(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, bool sortByM/*, libCZI::PixelType* pPixelTypeOfFirstFoundSubBlock = nullptr*/); + std::vector GetSubBlocksSubset(const libCZI::IntRect& roi, const libCZI::IDimCoordinate* planeCoordinate, bool sortByM); void ComposeTiles(libCZI::IBitmapData* pBm, int xPos, int yPos, const std::vector& subBlocksSet, const libCZI::ISingleChannelTileAccessor::Options& options); }; diff --git a/Src/libCZI/SingleChannelTileCompositor.cpp b/Src/libCZI/SingleChannelTileCompositor.cpp index 514dfde3..a787c373 100644 --- a/Src/libCZI/SingleChannelTileCompositor.cpp +++ b/Src/libCZI/SingleChannelTileCompositor.cpp @@ -10,8 +10,8 @@ using namespace libCZI; /*static*/void CSingleChannelTileCompositor::Compose(libCZI::IBitmapData* dest, libCZI::IBitmapData* source, int x, int y, bool drawTileBorder) { - ScopedBitmapLockerP srcLck{ source }; - ScopedBitmapLockerP dstLck{ dest }; + const ScopedBitmapLockerP srcLck{ source }; + const ScopedBitmapLockerP dstLck{ dest }; CBitmapOperations::CopyWithOffsetInfo info; info.xOffset = x; info.yOffset = y; @@ -35,7 +35,7 @@ using namespace libCZI; /*-----------------------------------------------------------------------------------------------*/ /*static*/void libCZI::Compositors::ComposeSingleChannelTiles( - std::function&, int&, int&)> getTiles, + const std::function&, int&, int&)>& getTiles, libCZI::IBitmapData* dest, int xPos, int yPos, @@ -43,7 +43,8 @@ using namespace libCZI; { if (pOptions == nullptr) { - ComposeSingleTileOptions options; options.Clear(); + ComposeSingleTileOptions options; + options.Clear(); ComposeSingleChannelTiles(getTiles, dest, xPos, yPos, &options); return; } diff --git a/Src/libCZI/libCZI_Compositor.h b/Src/libCZI/libCZI_Compositor.h index 17e66e09..c5e0d2e1 100644 --- a/Src/libCZI/libCZI_Compositor.h +++ b/Src/libCZI/libCZI_Compositor.h @@ -29,7 +29,7 @@ namespace libCZI class IAccessor { protected: - virtual ~IAccessor() {} + virtual ~IAccessor() = default; }; /// This accessor creates a multi-tile composite of a single channel (and a single plane). @@ -63,6 +63,12 @@ namespace libCZI /// Otherwise the Z-order is arbitrary. bool sortByM; + /// If true, then the tile-visibility-check-optimization is used. When doing the multi-tile composition, + /// all relevant tiles are checked whether they are visible in the destination bitmap. If a tile is not visible, then + /// the corresponding sub-block is not read. This can speed up the operation considerably. The result is the same as + /// without this optimization - i.e. there should be no reason to turn it off besides potential bugs. + bool useVisibilityCheckOptimization; + /// If true, then a one-pixel wide boundary will be drawn around /// each tile (in black color). bool drawTileBorder; @@ -75,6 +81,7 @@ namespace libCZI { this->backGroundColor.r = this->backGroundColor.g = this->backGroundColor.b = std::numeric_limits::quiet_NaN(); this->sortByM = true; + this->useVisibilityCheckOptimization = false; this->drawTileBorder = false; this->sceneFilter.reset(); } @@ -249,6 +256,12 @@ namespace libCZI /// is given here, then no filtering is applied. std::shared_ptr sceneFilter; + /// If true, then the tile-visibility-check-optimization is used. When doing the multi-tile composition, + /// all relevant tiles are checked whether they are visible in the destination bitmap. If a tile is not visible, then + /// the corresponding sub-block is not read. This can speed up the operation considerably. The result is the same as + /// without this optimization - i.e. there should be no reason to turn it off besides potential bugs. + bool useVisibilityCheckOptimization; + /// Clears this object to its blank state. void Clear() { @@ -256,6 +269,7 @@ namespace libCZI this->sortByM = true; this->backGroundColor.r = this->backGroundColor.g = this->backGroundColor.b = std::numeric_limits::quiet_NaN(); this->sceneFilter.reset(); + this->useVisibilityCheckOptimization = false; } }; @@ -332,7 +346,7 @@ namespace libCZI /// The y-coordinate of the top-left of the destination bitmap. /// Options for controlling the operation. This argument is optional (may be nullptr). static void ComposeSingleChannelTiles( - std::function& src, int& x, int& y)> getTiles, + const std::function& src, int& x, int& y)>& getTiles, libCZI::IBitmapData* dest, int xPos, int yPos, diff --git a/Src/libCZI/libCZI_Pixels.h b/Src/libCZI/libCZI_Pixels.h index 48d9b128..1b718bfe 100644 --- a/Src/libCZI/libCZI_Pixels.h +++ b/Src/libCZI/libCZI_Pixels.h @@ -26,6 +26,9 @@ namespace libCZI /// Returns a boolean indicating whether this rectangle contains valid information. bool IsValid() const { return this->w >= 0 && this->h >= 0; } + /// Returns a boolean indicating whether this rectangle is valid and non-empty. + bool IsNonEmpty() const { return this->w > 0 && this->h > 0; } + /// Determine whether this rectangle intersects with the specified one. /// \param r The other rectangle. /// \return True if the two rectangles intersect, false otherwise. diff --git a/Src/libCZI/utilities.cpp b/Src/libCZI/utilities.cpp index 8b9930d5..0128ef23 100644 --- a/Src/libCZI/utilities.cpp +++ b/Src/libCZI/utilities.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #if !defined(_WIN32) #include #endif @@ -133,7 +134,7 @@ tString trimImpl(const tString& str, const tString& whitespace) guid.Data1, guid.Data2, guid.Data3, - { guid.Data4[0],guid.Data4[1],guid.Data4[2],guid.Data4[3],guid.Data4[4],guid.Data4[5],guid.Data4[6],guid.Data4[7] }}; + { guid.Data4[0],guid.Data4[1],guid.Data4[2],guid.Data4[3],guid.Data4[4],guid.Data4[5],guid.Data4[6],guid.Data4[7] } }; return guid_value; #else std::mt19937 rng; @@ -423,3 +424,156 @@ tString trimImpl(const tString& str, const tString& whitespace) LoHiBytePackStrided_C(ptrSrc, sizeSrc, width, height, stride, dest); } #endif + +void RectangleCoverageCalculator::AddRectangle(const libCZI::IntRect& rectangle) +{ + if (!rectangle.IsValid()) + { + return; + } + + if (this->splitters_.empty()) + { + this->splitters_.push_back(rectangle); + } + else + { + for (auto splitter = this->splitters_.begin(); splitter != this->splitters_.end(); ++splitter) + { + // does the rectangle intersect with one of existing rectangles? + if (splitter->IntersectsWith(rectangle) == true) + { + // check if it is completely contained in the current existing rectangle + if (RectangleCoverageCalculator::IsCompletelyContained(*splitter, rectangle) == true) + { + // ok, in this case we have nothing to do! + return; + } + + // check if the existing rectangle is completely contained in the new one + if (RectangleCoverageCalculator::IsCompletelyContained(rectangle, *splitter) == true) + { + // in this case we remove the (smaller) rect splitters[i] (which is fully + // contained in rectangle) from our list and add rectangle + this->splitters_.erase(splitter); + this->AddRectangle(rectangle); + return; + } + + // ok, the rectangle overlap only partially... then let's cut the new rectangle into pieces + // (which do not intersect with the currently investigated rectangle) and try + // to add those pieces + std::array splitUpRects; + const int number_of_split_up_rects = SplitUpIntoNonOverlapping(*splitter, rectangle, splitUpRects); + for (int n = 0; n < number_of_split_up_rects; ++n) + { + this->AddRectangle(splitUpRects[n]); + } + + return; + } + } + + // if we end up here this means that the new rectangle does not + // overlap with an existing one -> we can happily add it now! + this->splitters_.push_back(rectangle); + } +} + +/*static*/int RectangleCoverageCalculator::SplitUpIntoNonOverlapping(const libCZI::IntRect& rectangle_a, const libCZI::IntRect& rectangle_b, std::array& result) +{ + // precondition: rectangle_b is not completely contained in rectangle_a (and rectangle_a not completely contained in rectangle_b)! + int result_index = 0; + if (rectangle_b.x >= rectangle_a.x && rectangle_b.x + rectangle_b.w <= rectangle_a.x + rectangle_a.w) + { + if (rectangle_a.y > rectangle_b.y) + { + result[result_index++] = libCZI::IntRect{ rectangle_b.x, rectangle_b.y, rectangle_b.w, rectangle_a.y - rectangle_b.y }; + } + + if (rectangle_b.y + rectangle_b.h > rectangle_a.y + rectangle_a.h) + { + result[result_index++] = libCZI::IntRect{ rectangle_b.x, rectangle_a.y + rectangle_a.h, rectangle_b.w, rectangle_b.y + rectangle_b.h - rectangle_a.y - rectangle_a.h }; + } + } + else if (rectangle_b.x < rectangle_a.x && rectangle_b.x + rectangle_b.w <= rectangle_a.x + rectangle_a.w) + { + result[result_index++] = libCZI::IntRect{ rectangle_b.x, rectangle_b.y, rectangle_a.x - rectangle_b.x, rectangle_b.h }; + if (rectangle_b.y < rectangle_a.y) + { + result[result_index++] = libCZI::IntRect{ rectangle_a.x, rectangle_b.y, rectangle_b.x + rectangle_b.w - rectangle_a.x, rectangle_a.y - rectangle_b.y }; + } + + if (rectangle_b.y + rectangle_b.h > rectangle_a.y + rectangle_a.h) + { + result[result_index++] = libCZI::IntRect{ rectangle_a.x, rectangle_a.y + rectangle_a.h, rectangle_b.x + rectangle_b.w - rectangle_a.x, rectangle_b.y + rectangle_b.h - rectangle_a.y - rectangle_a.h }; + } + } + else if (rectangle_b.x >= rectangle_a.x && rectangle_b.x + rectangle_b.w > rectangle_a.x + rectangle_a.w) + { + result[result_index++] = libCZI::IntRect{ rectangle_a.x + rectangle_a.w, rectangle_b.y, rectangle_b.x + rectangle_b.w - rectangle_a.x - rectangle_a.w, rectangle_b.h }; + + if (rectangle_b.y < rectangle_a.y) + { + result[result_index++] = libCZI::IntRect{ rectangle_b.x, rectangle_b.y, rectangle_a.x + rectangle_a.w - rectangle_b.x, rectangle_a.y - rectangle_b.y }; + } + + if (rectangle_b.y + rectangle_b.h > rectangle_a.y + rectangle_a.h) + { + result[result_index++] = libCZI::IntRect{ rectangle_b.x, rectangle_a.y + rectangle_a.h, rectangle_a.x + rectangle_a.w - rectangle_b.x, rectangle_b.y + rectangle_b.h - rectangle_a.y - rectangle_a.h }; + } + } + else if (rectangle_b.x <= rectangle_a.x && rectangle_b.x + rectangle_b.w >= rectangle_a.x + rectangle_a.w) + { + result[result_index++] = libCZI::IntRect{ rectangle_b.x, rectangle_b.y, rectangle_a.x - rectangle_b.x, rectangle_b.h }; + result[result_index++] = libCZI::IntRect{ rectangle_a.x + rectangle_a.w, rectangle_b.y, rectangle_b.x + rectangle_b.w - rectangle_a.x - rectangle_a.w, rectangle_b.h }; + + if (rectangle_a.y > rectangle_b.y) + { + result[result_index++] = libCZI::IntRect{ rectangle_a.x, rectangle_b.y, rectangle_a.w, rectangle_a.y - rectangle_b.y }; + } + else if (rectangle_a.y + rectangle_a.h > rectangle_b.y && rectangle_a.y + rectangle_a.h < rectangle_b.y + rectangle_b.h) + { + result[result_index++] = libCZI::IntRect{ rectangle_a.x, rectangle_a.y + rectangle_a.h, rectangle_a.w, rectangle_b.y + rectangle_b.h - rectangle_a.y - rectangle_a.h }; + } + } + + return result_index; +} + + +/*static*/bool RectangleCoverageCalculator::IsCompletelyContained(const libCZI::IntRect& outer, const libCZI::IntRect& inner) +{ + return inner.x >= outer.x && inner.x + inner.w <= outer.x + outer.w && + inner.y >= outer.y && inner.y + inner.h <= outer.y + outer.h; +} + +std::int64_t RectangleCoverageCalculator::CalcAreaOfIntersectionWithRectangle(const libCZI::IntRect& query_rectangle) const +{ + if (!query_rectangle.IsValid()) + { + return 0; + } + + int64_t area = 0; + for (const auto& r : this->splitters_) + { + auto intersection = r.Intersect(query_rectangle); + if (intersection.IsValid()) + { + area += intersection.w * static_cast(intersection.h); + } + } + + return area; +} + +bool RectangleCoverageCalculator::IsCompletelyCovered(const libCZI::IntRect& query_rectangle) const +{ + if (!query_rectangle.IsValid()) + { + return true; + } + + return this->CalcAreaOfIntersectionWithRectangle(query_rectangle) == static_cast(query_rectangle.w) * query_rectangle.h; +} diff --git a/Src/libCZI/utilities.h b/Src/libCZI/utilities.h index a5ff8a41..083600ca 100644 --- a/Src/libCZI/utilities.h +++ b/Src/libCZI/utilities.h @@ -204,3 +204,80 @@ struct ParseEnumHelper } }; +/// This class allows to calculate the area covered by a set of rectangles. The mode of operation +/// is: +/// - Create an instance of the class and add the rectangles to it (using AddRectangle) . +/// - Then, for a given rectangle, call CalcAreaOfIntersectionWithRectangle in order to get the area of the intersection +/// of this rectangle with the union of the rectangles added before. +/// The rectangles being added do not have to follow any order, or are required to be non-overlapping. +class RectangleCoverageCalculator +{ +private: + /// This vector contains the rectangles added to the state of the instance. The rectangles + /// in this vector are guaranteed to be non-overlapping. That is why the name 'splitters' is used - + /// if a rectangles is added which is overlapping with the existing ones, the rectangle is split into smaller + /// rectangles (which we call the 'splitters') which are non-overlapping with the existing ones. + std::vector splitters_; +public: + /// Adds a rectangle to the state. The runtime of this method increases with the number of + /// splitters in the state of the instance and the number of rectangles this rectangle + /// needs to be split into. For a modest number of rectangles, the runtime is for + /// sure negligible. + /// A pathologic case to be aware of is when there are many existing and then a large rectangle + /// is added (which is overlapping with many of the existing ones). In this case, it would be + /// greatly beneficial to add the large rectangle first, and then the smaller ones. + /// + /// \param rectangle The rectangle to be added. + void AddRectangle(const libCZI::IntRect& rectangle); + + /// Adds the rectangles given by the iterator to the state of the instance. + /// + /// \typeparam tIterator Type of the iterator. + /// \param begin The begin. + /// \param end The end. + template + void AddRectangles(tIterator begin, tIterator end) + { + for (tIterator it = begin; it != end; ++it) + { + this->AddRectangle(*it); + } + } + + /// Calculates the area of intersection of the specified rectangle with the + /// union of the rectangles added before. + /// If the query_rectangle rectangle is invalid, the return value is 0. + /// + /// \param query_rectangle The query rectangle. + /// + /// \returns The calculated area of intersection of the specified rectangle with the + /// union of the rectangles added before. + std::int64_t CalcAreaOfIntersectionWithRectangle(const libCZI::IntRect& query_rectangle) const; + + /// Query if 'rectQuery' is completely covered is completely covered by the union of the rectangles added before. + /// If the query_rectangle rectangle is invalid, the return value is true. + /// + /// \param query_rectangle The query rectangle. + /// + /// \returns True if completely covered; false otherwise. + bool IsCompletelyCovered(const libCZI::IntRect& query_rectangle) const; +private: + /// Test whether the rectangle 'inner' is completely contained in the rectangle 'outer'. + /// + /// \param outer The outer rectangle. + /// \param inner The inner rectangle. + /// + /// \returns True if completely contained, false if not. + static bool IsCompletelyContained(const libCZI::IntRect& outer, const libCZI::IntRect& inner); + + /// The area of rectangle_b which does not overlap with rectangle_a is determined and + /// we return this part as a list of 4 rectangles at most. The return value is the number + /// of rectangles in the result array. + /// + /// \param rectangle_a The rectangle a. + /// \param rectangle_b The rectangle b. + /// \param [out] result The resulting splitters are put into this array. There will be 4 rectangles at most. + /// + /// \returns The number of valid rectangles in the 'result' array. + static int SplitUpIntoNonOverlapping(const libCZI::IntRect& rectangle_a, const libCZI::IntRect& rectangle_b, std::array& result); +}; diff --git a/Src/libCZI_UnitTests/CMakeLists.txt b/Src/libCZI_UnitTests/CMakeLists.txt index 8fb01461..8d30312e 100644 --- a/Src/libCZI_UnitTests/CMakeLists.txt +++ b/Src/libCZI_UnitTests/CMakeLists.txt @@ -61,10 +61,12 @@ ADD_EXECUTABLE(libCZI_UnitTests test_Accessors.cpp test_CZIParse.cpp test_streamslib.cpp - test_curlhttpstream.cpp) + test_curlhttpstream.cpp + test_rectanglecoverage.cpp + test_TileAccessorCoverageOptimization.cpp) TARGET_LINK_LIBRARIES(libCZI_UnitTests PRIVATE libCZIStatic GTest::gtest GTest::gmock) target_compile_definitions(libCZI_UnitTests PRIVATE _LIBCZISTATICLIB) -add_test(NAME libCZI_UnitTests COMMAND libCZI_UnitTests) \ No newline at end of file +add_test(NAME libCZI_UnitTests COMMAND libCZI_UnitTests) diff --git a/Src/libCZI_UnitTests/test_Accessors.cpp b/Src/libCZI_UnitTests/test_Accessors.cpp index 7bc8073a..49f325a7 100644 --- a/Src/libCZI_UnitTests/test_Accessors.cpp +++ b/Src/libCZI_UnitTests/test_Accessors.cpp @@ -576,7 +576,7 @@ TEST(Accessor, CreateDocumentAndExerciseScalingAccessorAllowingForInaccuracy) // act constexpr float zoom = 1 - numeric_limits::epsilon(); // use a zoom a tiny bit less than 1 - IntSize resulting_size = accessor->CalcSize(IntRect{ 0,0,5121,5121 }, zoom); + const IntSize resulting_size = accessor->CalcSize(IntRect{ 0,0,5121,5121 }, zoom); const auto composite_bitmap = accessor->Get( PixelType::Gray8, IntRect{ 0,0,5121,5121 }, @@ -596,10 +596,10 @@ TEST(Accessor, CreateDocumentAndExerciseScalingAccessorAllowingForInaccuracy) { for (size_t x = 0; x < composite_bitmap->GetWidth(); ++x) { - uint8_t expected_value = (x < 761 && y >= 2671 && y < 2671 + 2449) ? 0x2a : 0; + const uint8_t expected_value = (x < 761 && y >= 2671 && y < 2671 + 2449) ? 0x2a : 0; // allow both values for the exact borders of the subblock, i.e. allow for the bitmap to be one pixel smaller on the edges - bool inaccuracy_allowed = ((y == 2670 || y == 2671 || y == 2670 + 2449 || y == 2671 + 2449) && (x < 760)); + const bool inaccuracy_allowed = ((y == 2670 || y == 2671 || y == 2670 + 2449 || y == 2671 + 2449) && (x < 760)); const uint8_t* p = static_cast(lock_info_bitmap.ptrDataRoi) + y * lock_info_bitmap.stride + x; if (*p != expected_value) { diff --git a/Src/libCZI_UnitTests/test_TileAccessorCoverageOptimization.cpp b/Src/libCZI_UnitTests/test_TileAccessorCoverageOptimization.cpp new file mode 100644 index 00000000..037020d9 --- /dev/null +++ b/Src/libCZI_UnitTests/test_TileAccessorCoverageOptimization.cpp @@ -0,0 +1,584 @@ +// SPDX-FileCopyrightText: 2023 Carl Zeiss Microscopy GmbH +// +// SPDX-License-Identifier: LGPL-3.0-or-later + + +#include "include_gtest.h" +#include +#include +#include "inc_libCZI.h" +#include "../libCZI/SingleChannelTileAccessor.h" +#include "../libCZI/SingleChannelScalingTileAccessor.h" +#include "MemOutputStream.h" +#include "utils.h" + +using namespace libCZI; +using namespace std; + +/// This is a shim for the ISubBlockRepository interface, which keeps track of the subblocks that were read. +class SubBlockRepositoryShim : public ISubBlockRepository +{ +private: + std::shared_ptr subblock_repository_; + vector subblocks_read_; +public: + explicit SubBlockRepositoryShim(std::shared_ptr subblock_repository) + : subblock_repository_(std::move(subblock_repository)) + {} + + /// Gets a vector containing the indices of the subblocks that were read + /// (by calling the ReadSubBlock-method). + /// + /// \returns The indices of the subblocks read. + const vector& GetSubblocksRead() const + { + return this->subblocks_read_; + } + + /// Clears the subblocks-read history. + void ClearSubblockReadHistory() + { + this->subblocks_read_.clear(); + } + + void EnumerateSubBlocks(const std::function& funcEnum) override + { + this->subblock_repository_->EnumerateSubBlocks(funcEnum); + } + void EnumSubset(const IDimCoordinate* planeCoordinate, const IntRect* roi, bool onlyLayer0, const std::function& funcEnum) override + { + this->subblock_repository_->EnumSubset(planeCoordinate, roi, onlyLayer0, funcEnum); + } + std::shared_ptr ReadSubBlock(int index) override + { + this->subblocks_read_.push_back(index); + return this->subblock_repository_->ReadSubBlock(index); + } + bool TryGetSubBlockInfoOfArbitrarySubBlockInChannel(int channelIndex, SubBlockInfo& info) override + { + return this->subblock_repository_->TryGetSubBlockInfoOfArbitrarySubBlockInChannel(channelIndex, info); + } + bool TryGetSubBlockInfo(int index, SubBlockInfo* info) const override + { + return this->subblock_repository_->TryGetSubBlockInfo(index, info); + } + SubBlockStatistics GetStatistics() override + { + return this->subblock_repository_->GetStatistics(); + } + PyramidStatistics GetPyramidStatistics() override + { + return this->subblock_repository_->GetPyramidStatistics(); + } +}; + +/// This struct is used for creating a test CZI document - it contains the X-Y-position/width/height +/// and the M-index of a subblock to be ceated in the document. +struct SubBlockPositions +{ + IntRect rectangle; + int mIndex; +}; + +static tuple, size_t> CreateTestCzi(const vector& subblocks) +{ + const auto writer = CreateCZIWriter(); + const auto outStream = make_shared(0); + + const auto spWriterInfo = make_shared( + GUID{ 0,0,0,{ 0,0,0,0,0,0,0,0 } }, + CDimBounds{ { DimensionIndex::T, 0, 1 }, { DimensionIndex::C, 0, 1 } }, // set a bounds for Z and C + 0, static_cast(subblocks.size() - 1)); + + writer->Create(outStream, spWriterInfo); + + int count = 0; + for (const auto& block : subblocks) + { + ++count; + const size_t size_of_bitmap = static_cast(block.rectangle.w) * block.rectangle.h; + unique_ptr bitmap(new uint8_t[size_of_bitmap]); + memset(bitmap.get(), count, size_of_bitmap); + AddSubBlockInfoStridedBitmap addSbBlkInfo; + addSbBlkInfo.Clear(); + addSbBlkInfo.coordinate.Set(DimensionIndex::C, 0); + addSbBlkInfo.coordinate.Set(DimensionIndex::T, 0); + addSbBlkInfo.mIndexValid = true; + addSbBlkInfo.mIndex = block.mIndex; + addSbBlkInfo.x = block.rectangle.x; + addSbBlkInfo.y = block.rectangle.y; + addSbBlkInfo.logicalWidth = block.rectangle.w; + addSbBlkInfo.logicalHeight = block.rectangle.h; + addSbBlkInfo.physicalWidth = block.rectangle.w; + addSbBlkInfo.physicalHeight = block.rectangle.h; + addSbBlkInfo.PixelType = PixelType::Gray8; + addSbBlkInfo.ptrBitmap = bitmap.get(); + addSbBlkInfo.strideBitmap = block.rectangle.w; + writer->SyncAddSubBlock(addSbBlkInfo); + } + + const auto metaDataBuilder = writer->GetPreparedMetadata(PrepareMetadataInfo{}); + + WriteMetadataInfo write_metadata_info; + const auto& strMetadata = metaDataBuilder->GetXml(); + write_metadata_info.szMetadata = strMetadata.c_str(); + write_metadata_info.szMetadataSize = strMetadata.size() + 1; + write_metadata_info.ptrAttachment = nullptr; + write_metadata_info.attachmentSize = 0; + writer->SyncWriteMetadata(write_metadata_info); + + writer->Close(); + + return make_tuple(outStream->GetCopy(nullptr), outStream->GetDataSize()); +} + +template +void ThreeOverlappingSubBlockWithVisibilityOptimizationTest(tAccessorHandler handler) +{ + // We create a CZI with 3 subblocks, each containing a 2x2 bitmap. + // 1st subblock is at (0,0), 2nd subblock is at (1,1), 3rd subblock is at (2,2). + // We then query for the ROI (1,1,1,1) and check that only the 2nd subblock is read - + // because subblock #0 is not visible (overdrawn by #1), and #2 does not intersect. + + // arrange + auto czi_document_as_blob = CreateTestCzi(vector{{ {0, 0, 2, 2}, 0 }, { {1, 1, 2, 2}, 1 }, { {2, 2, 2, 2}, 2 }}); + const auto memory_stream = make_shared(get<0>(czi_document_as_blob).get(), get<1>(czi_document_as_blob)); + const auto reader = CreateCZIReader(); + reader->Open(memory_stream); + auto subblock_repository_with_read_history = make_shared(reader); + handler.Initialize(subblock_repository_with_read_history); + const CDimCoordinate plane_coordinate{ {DimensionIndex::C, 0}, {DimensionIndex::T, 0} }; + + // act + ISingleChannelTileAccessor::Options options; + options.Clear(); + options.useVisibilityCheckOptimization = true; + const auto tile_composite_bitmap = handler.GetBitmap(PixelType::Gray8, IntRect{ 1, 1, 1, 1 }, &plane_coordinate, true, false); + + // assert + EXPECT_EQ(tile_composite_bitmap->GetWidth(), 1); + EXPECT_EQ(tile_composite_bitmap->GetHeight(), 1); + const ScopedBitmapLockerSP locked_tile_composite_bitmap{ tile_composite_bitmap }; + EXPECT_EQ(*(static_cast(locked_tile_composite_bitmap.ptrDataRoi)), 2); + + // check that subblock #0 and #2 have NOT been read + EXPECT_TRUE( + find(subblock_repository_with_read_history->GetSubblocksRead().cbegin(), + subblock_repository_with_read_history->GetSubblocksRead().cend(), + 0) == subblock_repository_with_read_history->GetSubblocksRead().cend()) << "subblock #0 is not expected to be read"; + EXPECT_TRUE( + find(subblock_repository_with_read_history->GetSubblocksRead().cbegin(), + subblock_repository_with_read_history->GetSubblocksRead().cend(), + 2) == subblock_repository_with_read_history->GetSubblocksRead().cend()) << "subblock #2 is not expected to be read"; +} + +template +void ThreeSubBlocksAtSamePositionWithVisibilityOptimizationTest(tAccessorHandler handler) +{ + // Now the three subblocks are all positioned at (0,0). We query for the ROI (1,1,1,1) and check that + // only the top-most subblock (Which is #2) is read, because the other two are not visible (are overdrawn). + + // arrange + auto czi_document_as_blob = CreateTestCzi(vector{{ {0, 0, 2, 2}, 0 }, { {0, 0, 2, 2}, 1 }, { {0, 0, 2, 2}, 2 }}); + const auto memory_stream = make_shared(get<0>(czi_document_as_blob).get(), get<1>(czi_document_as_blob)); + const auto reader = CreateCZIReader(); + reader->Open(memory_stream); + auto subblock_repository_with_read_history = make_shared(reader); + handler.Initialize(subblock_repository_with_read_history); + const CDimCoordinate plane_coordinate{ {DimensionIndex::C, 0}, {DimensionIndex::T, 0} }; + + // act + ISingleChannelTileAccessor::Options options; + options.Clear(); + options.useVisibilityCheckOptimization = true; + const auto tile_composite_bitmap = handler.GetBitmap(PixelType::Gray8, IntRect{ 1, 1, 1, 1 }, &plane_coordinate, true, false); + + // assert + EXPECT_EQ(tile_composite_bitmap->GetWidth(), 1); + EXPECT_EQ(tile_composite_bitmap->GetHeight(), 1); + const ScopedBitmapLockerSP locked_tile_composite_bitmap{ tile_composite_bitmap }; + EXPECT_EQ(*(static_cast(locked_tile_composite_bitmap.ptrDataRoi)), 3); + + // check that subblock #0 and #1 have NOT been read + EXPECT_TRUE( + find(subblock_repository_with_read_history->GetSubblocksRead().cbegin(), + subblock_repository_with_read_history->GetSubblocksRead().cend(), + 0) == subblock_repository_with_read_history->GetSubblocksRead().cend()) << "subblock #0 is not expected to be read"; + EXPECT_TRUE( + find(subblock_repository_with_read_history->GetSubblocksRead().cbegin(), + subblock_repository_with_read_history->GetSubblocksRead().cend(), + 1) == subblock_repository_with_read_history->GetSubblocksRead().cend()) << "subblock #1 is not expected to be read"; +} + +template +void RandomSubblocksAndCompareRenderingWithAndWithoutVisibilityOptimization(tAccessorHandler handler) +{ + // Here we place a random number of subblocks at random positions, and then check that the + // rendering result w/ and w/o visibility-optimization is the same + + random_device dev; + mt19937 rng(dev()); + uniform_int_distribution distribution(0, 99); // distribution in range [0, 99] + + static constexpr IntRect kRoi{ 0, 0, 120, 120 }; + + for (int repeat = 0; repeat < 10; repeat++) // let's repeat this 10 times + { + const int number_of_rectangles = distribution(rng) + 1; + + vector subblocks; + subblocks.reserve(number_of_rectangles); + for (int i = 0; i < number_of_rectangles; ++i) + { + subblocks.emplace_back(SubBlockPositions{ IntRect{ distribution(rng), distribution(rng), 1 + distribution(rng), 1 + distribution(rng) }, i }); + } + + // Shuffle the vector into a random order + std::shuffle(subblocks.begin(), subblocks.end(), rng); + + // now, create the test CZI document (in memory) + auto czi_document_as_blob = CreateTestCzi(subblocks); + + const auto memory_stream = make_shared(get<0>(czi_document_as_blob).get(), get<1>(czi_document_as_blob)); + const auto reader = CreateCZIReader(); + reader->Open(memory_stream); + + // We construct a subblock-repository shim here which keeps track of the subblocks that were read - which + // is not really necessary here, but we do it anyway to make sure that the visibility-optimization + // is actually reducing the number of subblocks read. + auto subblock_repository_with_read_history = make_shared(reader); + handler.Initialize(subblock_repository_with_read_history); + const CDimCoordinate plane_coordinate{ {DimensionIndex::C, 0}, {DimensionIndex::T, 0} }; + + const auto tile_composite_bitmap_with_visibility_optimization = handler.GetBitmapWithOptimization(PixelType::Gray8, kRoi, &plane_coordinate); + const auto number_of_subblocks_read_with_visibility_optimization = subblock_repository_with_read_history->GetSubblocksRead().size(); + + subblock_repository_with_read_history->ClearSubblockReadHistory(); + const auto tile_composite_bitmap_without_visibility_optimization = handler.GetBitmapWithoutOptimization(PixelType::Gray8, kRoi, &plane_coordinate); + const auto number_of_subblocks_read_without_visibility_optimization = subblock_repository_with_read_history->GetSubblocksRead().size(); + + EXPECT_TRUE(AreBitmapDataEqual(tile_composite_bitmap_with_visibility_optimization, tile_composite_bitmap_without_visibility_optimization)) << + "tile-composites w/ and w/o visibility-optimization are found to differ"; + + EXPECT_LE(number_of_subblocks_read_with_visibility_optimization, number_of_subblocks_read_without_visibility_optimization) << + "the number of subblocks actually read w/ visibility-optimization must be less or equal to the number w/o this optimization"; + } +} + +class SingleChannelTileAccessorHandler +{ + shared_ptr accessor_; + bool sort_by_m_; +public: + explicit SingleChannelTileAccessorHandler(bool sortByM = true) : sort_by_m_(sortByM) + { + } + + void Initialize(const shared_ptr& repository) + { + this->accessor_ = make_shared(repository); + } + + shared_ptr GetBitmap(PixelType pixeltype, const IntRect& roi, const IDimCoordinate* planeCoordinate, bool with_optimization, bool with_background_clear) const + { + ISingleChannelTileAccessor::Options options; + options.Clear(); + options.useVisibilityCheckOptimization = with_optimization; + options.sortByM = this->sort_by_m_; + if (with_background_clear) + { + options.backGroundColor = RgbFloatColor{ 0,0,0 }; + } + + return this->accessor_->Get(pixeltype, roi, planeCoordinate, &options); + } + + shared_ptr GetBitmapWithOptimization(PixelType pixeltype, const IntRect& roi, const IDimCoordinate* planeCoordinate) const + { + return this->GetBitmap(pixeltype, roi, planeCoordinate, true, true); + } + + shared_ptr GetBitmapWithoutOptimization(PixelType pixeltype, const IntRect& roi, const IDimCoordinate* planeCoordinate) const + { + return this->GetBitmap(pixeltype, roi, planeCoordinate, false, true); + } +}; + +class SingleChannelScalingTileAccessorHandler +{ + shared_ptr accessor_; + bool sort_by_m_; +public: + explicit SingleChannelScalingTileAccessorHandler(bool sortByM = true) : sort_by_m_(sortByM) + { + } + + void Initialize(const shared_ptr& repository) + { + this->accessor_ = make_shared(repository); + } + + shared_ptr GetBitmap(PixelType pixeltype, const IntRect& roi, const IDimCoordinate* planeCoordinate, bool with_optimization, bool with_background_clear) const + { + ISingleChannelScalingTileAccessor::Options options; + options.Clear(); + options.useVisibilityCheckOptimization = with_optimization; + options.sortByM = this->sort_by_m_; + if (with_background_clear) + { + options.backGroundColor = RgbFloatColor{ 0,0,0 }; + } + + return this->accessor_->Get(pixeltype, roi, planeCoordinate, 1.f, &options); + } + + shared_ptr GetBitmapWithOptimization(PixelType pixeltype, const IntRect& roi, const IDimCoordinate* planeCoordinate) const + { + return this->GetBitmap(pixeltype, roi, planeCoordinate, true, true); + } + + shared_ptr GetBitmapWithoutOptimization(PixelType pixeltype, const IntRect& roi, const IDimCoordinate* planeCoordinate) const + { + return this->GetBitmap(pixeltype, roi, planeCoordinate, false, true); + } +}; + +TEST(TileAccessorCoverageOptimization, ThreeOverlappingSubBlockWithVisibilityOptimizationTest_SingleChannelTileAccessor) +{ + ThreeOverlappingSubBlockWithVisibilityOptimizationTest(SingleChannelTileAccessorHandler{}); +} + +TEST(TileAccessorCoverageOptimization, ThreeOverlappingSubBlockWithVisibilityOptimizationTest_SingleChannelScalingTileAccessor) +{ + ThreeOverlappingSubBlockWithVisibilityOptimizationTest(SingleChannelScalingTileAccessorHandler{}); +} + +TEST(TileAccessorCoverageOptimization, ThreeSubBlocksAtSamePositionWithVisibilityOptimizationTest_SingleChannelTileAccessor) +{ + ThreeSubBlocksAtSamePositionWithVisibilityOptimizationTest(SingleChannelTileAccessorHandler{}); +} + +TEST(TileAccessorCoverageOptimization, ThreeSubBlocksAtSamePositionWithVisibilityOptimizationTest_SingleChannelScalingTileAccessor) +{ + ThreeSubBlocksAtSamePositionWithVisibilityOptimizationTest(SingleChannelScalingTileAccessorHandler{}); +} + +TEST(TileAccessorCoverageOptimization, RandomSubblocksCompareRenderingWithAndWithoutVisibilityOptimization_SingleChannelTileAccessor) +{ + RandomSubblocksAndCompareRenderingWithAndWithoutVisibilityOptimization(SingleChannelTileAccessorHandler{}); +} + +TEST(TileAccessorCoverageOptimization, RandomSubblocksCompareRenderingWithAndWithoutVisibilityOptimization_SingleChannelScalingTileAccessor) +{ + RandomSubblocksAndCompareRenderingWithAndWithoutVisibilityOptimization(SingleChannelScalingTileAccessorHandler{}); +} + +TEST(TileAccessorCoverageOptimization, RandomSubblocksCompareRenderingWithAndWithoutVisibilityOptimizationWithoutSortByM_SingleChannelTileAccessor) +{ + RandomSubblocksAndCompareRenderingWithAndWithoutVisibilityOptimization(SingleChannelTileAccessorHandler{ false }); +} + +TEST(TileAccessorCoverageOptimization, RandomSubblocksCompareRenderingWithAndWithoutVisibilityOptimizationWithoutSortByM_SingleChannelScalingTileAccessor) +{ + RandomSubblocksAndCompareRenderingWithAndWithoutVisibilityOptimization(SingleChannelScalingTileAccessorHandler{ false }); +} + +// Stub to bridge the access restrictions +class CSingleChannelAccessorBaseToTestStub : public CSingleChannelAccessorBase +{ +public: + CSingleChannelAccessorBaseToTestStub() : CSingleChannelAccessorBase(nullptr) + {} + using CSingleChannelAccessorBase::CheckForVisibilityCore; +}; + +TEST(TileAccessorCoverageOptimization, CheckForVisibility_TwoSubblocksWhere1stOneIsCompleteyOverdrawn) +{ + static constexpr array kSubBlocks{ IntRect{0,0,2,2}, IntRect{0,0,3,3} }; + + // We have two subblocks (0,0,2,2) and (0,0,3,3), and we the order in which they are passed to the + // rendering is a stated above. So, we draw first (0,0,2,2), then (0,0,3,3), which means (0,0,3,3) + // is "on top". We then query for the visibility of the ROI (0,0,2,2), which is completely covered + // by (0,0,3,3), so we expect that only the second subblock is returned as visible. The first one + // (0,0,2,2) is completely overdrawn by the second one, so it is not visible. + + const auto indices_of_visible_tiles = CSingleChannelAccessorBaseToTestStub::CheckForVisibilityCore( + { 0,0,2,2 }, + kSubBlocks.size(), + [&](int index)->int + { + return index; + }, + [&](int subblock_index)->IntRect + { + return kSubBlocks[subblock_index]; + }); + + ASSERT_EQ(indices_of_visible_tiles.size(), 1); + EXPECT_EQ(indices_of_visible_tiles[0], 1); +} + +TEST(TileAccessorCoverageOptimization, CheckForVisibility_EmptyRoi) +{ + static constexpr array kSubBlocks{ IntRect{0,0,2,2}, IntRect{0,0,3,3} }; + + // here we pass an empty ROI, and we expect that no subblock is returned as visible + const auto indices_of_visible_tiles = CSingleChannelAccessorBaseToTestStub::CheckForVisibilityCore( + { 0,0,0,0 }, + kSubBlocks.size(), + [&](int index)->int + { + return index; + }, + [&](int subblock_index)->IntRect + { + return kSubBlocks[subblock_index]; + }); + + ASSERT_EQ(indices_of_visible_tiles.size(), 0); +} + +TEST(TileAccessorCoverageOptimization, CheckForVisibility_InvalidRoi) +{ + static constexpr array kSubBlocks{ IntRect{0,0,2,2}, IntRect{0,0,3,3} }; + + // here we pass an invalid ROI, and we expect that no subblock is returned as visible + IntRect roi; + roi.Invalidate(); + const auto indices_of_visible_tiles = CSingleChannelAccessorBaseToTestStub::CheckForVisibilityCore( + roi, + kSubBlocks.size(), + [&](int index)->int + { + return index; + }, + [&](int subblock_index)->IntRect + { + return kSubBlocks[subblock_index]; + }); + + ASSERT_EQ(indices_of_visible_tiles.size(), 0); +} + +TEST(TileAccessorCoverageOptimization, CheckForVisibility_SubblocksNotIntersectionRoi) +{ + static constexpr array kSubBlocks1{ IntRect{5,5,5,5} }; + + auto indices_of_visible_tiles = CSingleChannelAccessorBaseToTestStub::CheckForVisibilityCore( + { 0, 0, 4, 4 }, + kSubBlocks1.size(), + [&](int index)->int + { + return index; + }, + [&](int subblock_index)->IntRect + { + return kSubBlocks1[subblock_index]; + }); + + ASSERT_TRUE(indices_of_visible_tiles.empty()); + + static constexpr array kSubBlocks2{ IntRect{5,5,5,5}, IntRect{10,10,5,5}, IntRect{10,5,5,5} }; + + indices_of_visible_tiles = CSingleChannelAccessorBaseToTestStub::CheckForVisibilityCore( + { 0, 0, 4, 4 }, + kSubBlocks2.size(), + [&](int index)->int + { + return index; + }, + [&](int subblock_index)->IntRect + { + return kSubBlocks2[subblock_index]; + }); + + ASSERT_TRUE(indices_of_visible_tiles.empty()); +} + +TEST(TileAccessorCoverageOptimization, CheckForVisibility_TestCase1) +{ + static constexpr array kSubBlocks{ IntRect{0,0,2,1}, IntRect{0,0,3,3} }; + + // We report {0,0,3,3} as the subblock being rendered *last* (the one with index 1), and {0,0,2,1} as the one + // being rendered before. {0,0,2,1} is completely overdrawn by {0,0,3,3}, so we expect that only the last + // index (i.e. "1") is returned as visible. + const auto indices_of_visible_tiles = CSingleChannelAccessorBaseToTestStub::CheckForVisibilityCore( + { 0, 0, 3, 3 }, + kSubBlocks.size(), + [&](int index)->int + { + return index; + }, + [&](int subblock_index)->IntRect + { + return kSubBlocks[subblock_index]; + }); + + ASSERT_EQ(indices_of_visible_tiles.size(), 1); + EXPECT_EQ(indices_of_visible_tiles[0], 1); +} + +TEST(TileAccessorCoverageOptimization, CheckForVisibility_TestCase2) +{ + static constexpr array kSubBlocks{ IntRect{0,0,1,3}, IntRect{0,1,1,1}, IntRect{0,2,1,1}, IntRect{0,0,1,1}, IntRect{1,0,2,3} }; + + const auto indices_of_visible_tiles = CSingleChannelAccessorBaseToTestStub::CheckForVisibilityCore( + { 0, 0, 3, 3 }, + kSubBlocks.size(), + [&](int index)->int + { + return index; + }, + [&](int subblock_index)->IntRect + { + return kSubBlocks[subblock_index]; + }); + + ASSERT_EQ(indices_of_visible_tiles.size(), 4); + EXPECT_EQ(indices_of_visible_tiles[0], 1); + EXPECT_EQ(indices_of_visible_tiles[1], 2); + EXPECT_EQ(indices_of_visible_tiles[2], 3); + EXPECT_EQ(indices_of_visible_tiles[3], 4); +} + +TEST(TileAccessorCoverageOptimization, CheckForVisibility_TestCase3) +{ + static constexpr array kSubBlocks{ IntRect{0,0,1,1}, IntRect{0,0,1,1}, IntRect{1,0,1,2}, IntRect{2,0,1,1}, IntRect{1,0,2,3} }; + + const auto indices_of_visible_tiles = CSingleChannelAccessorBaseToTestStub::CheckForVisibilityCore( + { 0, 0, 3, 3 }, + kSubBlocks.size(), + [&](int index)->int + { + return index; + }, + [&](int subblock_index)->IntRect + { + return kSubBlocks[subblock_index]; + }); + + ASSERT_EQ(indices_of_visible_tiles.size(), 2); + EXPECT_EQ(indices_of_visible_tiles[0], 1); + EXPECT_EQ(indices_of_visible_tiles[1], 4); +} + +TEST(TileAccessorCoverageOptimization, CheckForVisibility_TestCase4) +{ + static constexpr array kSubBlocks{ IntRect{0,0,1,1}, IntRect{0,0,1,1}, IntRect{1,0,1,2}, IntRect{2,0,3,3}, IntRect{2,0,1,1}, IntRect{1,0,2,3} }; + + const auto indices_of_visible_tiles = CSingleChannelAccessorBaseToTestStub::CheckForVisibilityCore( + { 0, 0, 3, 3 }, + kSubBlocks.size(), + [&](int index)->int + { + return index; + }, + [&](int subblock_index)->IntRect + { + return kSubBlocks[subblock_index]; + }); + + ASSERT_EQ(indices_of_visible_tiles.size(), 2); + EXPECT_EQ(indices_of_visible_tiles[0], 1); + EXPECT_EQ(indices_of_visible_tiles[1], 5); +} diff --git a/Src/libCZI_UnitTests/test_rectanglecoverage.cpp b/Src/libCZI_UnitTests/test_rectanglecoverage.cpp new file mode 100644 index 00000000..478116d0 --- /dev/null +++ b/Src/libCZI_UnitTests/test_rectanglecoverage.cpp @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2023 Carl Zeiss Microscopy GmbH +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "include_gtest.h" +#include "inc_libCZI.h" +#include "../libCZI/utilities.h" +#include + +using namespace libCZI; +using namespace std; + +/// A simplistic reference implementation of a rectangle coverage calculator. We calculate +/// what area of the query rectangle is covered by the given vector of rectangles. +/// +/// \param rectangles The vector of rectangles. +/// \param queryRect The query rectangle. +/// \returns The area of the query rectangle being covered by the rectangles of the rectangles vector. +static int64_t CalcAreaOfIntersectionWithRectangleReference(const vector& rectangles, const IntRect& queryRect) +{ + // what we do here is: + // - we create a bitmap of the size of the query rectangle + // - we then fill the bitmap with 0xff in the areas that are covered by the rectangles + // - then we count how many pixels are set to 0xff + const auto bitmap = CStdBitmapData::Create(PixelType::Gray8, queryRect.w, queryRect.h); + const ScopedBitmapLockerSP bitmap_locked{ bitmap }; + CBitmapOperations::Fill_Gray8(bitmap->GetWidth(), bitmap->GetHeight(), bitmap_locked.ptrDataRoi, bitmap_locked.stride, 0); + for (const auto& rect : rectangles) + { + const auto intersection = rect.Intersect(queryRect); + if (!intersection.IsValid()) + { + continue; + } + + CBitmapOperations::Fill_Gray8( + intersection.w, + intersection.h, + static_cast(bitmap_locked.ptrDataRoi) + intersection.x + intersection.y * static_cast(bitmap_locked.stride), + bitmap_locked.stride, + 0xff); + } + + // now, we simply have to count the number of pixels that are set to 0xff + int64_t set_pixel_count = 0; + for (uint32_t y = 0; y < bitmap->GetHeight(); ++y) + { + const uint8_t* ptr = static_cast(bitmap_locked.ptrDataRoi) + y * static_cast(bitmap_locked.stride); + for (uint32_t x = 0; x < bitmap->GetWidth(); ++x) + { + if (*ptr++ == 0xff) + { + ++set_pixel_count; + } + } + } + + return set_pixel_count; +} + +TEST(CoverageCalculator, RandomRectanglesCompareWithReferenceImplementation) +{ + std::random_device dev; + std::mt19937 rng(dev()); + std::uniform_int_distribution distribution(0, 99); // distribution in range [0, 99] + + static constexpr IntRect kQueryRect{ 0, 0, 100, 100 }; + + for (int repeat = 0; repeat < 10; repeat++) + { + const int number_of_rectangles = 1 + distribution(rng); + + vector rectangles; + rectangles.reserve(number_of_rectangles); + for (int i = 0; i < number_of_rectangles; ++i) + { + rectangles.emplace_back(IntRect{ distribution(rng), distribution(rng), 1 + distribution(rng), 1 + distribution(rng) }); + } + + const int64_t reference_result_for_covered_area = CalcAreaOfIntersectionWithRectangleReference(rectangles, kQueryRect); + + RectangleCoverageCalculator calculator; + calculator.AddRectangles(rectangles.cbegin(), rectangles.cend()); + const int64_t total_covered_area = calculator.CalcAreaOfIntersectionWithRectangle(kQueryRect); + EXPECT_EQ(reference_result_for_covered_area, total_covered_area); + } +} + +struct CoverageCoverageCalculatorFixture : public testing::TestWithParam, int64_t>> {}; + +TEST_P(CoverageCoverageCalculatorFixture, CreateDocumentAndUseSingleChannelScalingTileAccessorWithSortByMAndCheckResult) +{ + const auto parameters = GetParam(); + + RectangleCoverageCalculator calculator; + calculator.AddRectangles(get<0>(parameters).cbegin(), get<0>(parameters).cend()); + + const int64_t totalCoveredArea = calculator.CalcAreaOfIntersectionWithRectangle({ 0, 0, 100, 100 }); + EXPECT_EQ(totalCoveredArea, get<1>(parameters)); +} + +INSTANTIATE_TEST_SUITE_P( + CoverageCalculator, + CoverageCoverageCalculatorFixture, + testing::Values( + // Non-Overlapping Rectangles + make_tuple(vector{ IntRect{ 10, 10, 20, 20 }, IntRect{ 40, 40, 20, 20 }, IntRect{ 70, 70, 20, 20 } }, 1200), + // Partially Overlapping Rectangles + make_tuple(vector{ IntRect{ 10, 10, 30, 30 }, IntRect{ 20, 20, 30, 30 }, IntRect{ 30, 30, 30, 30 } }, 1900), + // Fully Overlapping Rectangles + make_tuple(vector{ IntRect{ 10, 10, 30, 30 }, IntRect{ 10, 10, 30, 30 }}, 900), + // Rectangles Completely Outside the Query Rectangle + make_tuple(vector{ IntRect{ -40, -50, 30, 30 }, IntRect{ 110, 110, 30, 30 }}, 0), + // Rectangles Partially Outside the Main Rectangle + make_tuple(vector{ IntRect{ 90, 90, 20, 20 }, IntRect{ -10, 0, 20, 100 }}, 1100), + // Combination of Overlapping and Non-Overlapping Rectangles + make_tuple(vector{ IntRect{ 10, 10, 30, 30 }, IntRect{ 40, 40, 30, 30 }, IntRect{ 20, 20, 50, 50 } }, 3000), + // FullyOverlappingRectangles + make_tuple(vector{ IntRect{ 10, 10, 20, 20 }, IntRect{ 10, 10, 20, 20 }, IntRect{ 10, 10, 20, 20 } }, 400), + // Partially Overlapping Rectangles + make_tuple(vector{ IntRect{ 10, 10, 40, 40 }, IntRect{ 30, 30, 30, 30 }, IntRect{ 65, 65, 25, 25 } }, 2725) +));