From 062cee2933ebf9390a8ff442f8c806369976da29 Mon Sep 17 00:00:00 2001 From: Berke Date: Fri, 17 Jun 2022 02:10:25 +0300 Subject: [PATCH] new multipage image decoder api - ImageCollection --- .../imgcodecs/include/opencv2/imgcodecs.hpp | 45 +++ modules/imgcodecs/src/loadsave.cpp | 298 +++++++++++++++--- modules/imgcodecs/test/test_read_write.cpp | 177 +++++++++++ 3 files changed, 471 insertions(+), 49 deletions(-) diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index 67ca34fe55..cb4a170c28 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -332,6 +332,51 @@ CV_EXPORTS_W bool haveImageReader( const String& filename ); */ CV_EXPORTS_W bool haveImageWriter( const String& filename ); +/** @brief To read Multi Page images on demand + +The ImageCollection class provides iterator API to read multi page images on demand. Create iterator +to the collection of the images and iterate over the collection. Decode the necessary page with operator*. + +The performance of page decoding is O(1) if collection is increment sequentially. If the user wants to access random page, +then the time Complexity is O(n) because the collection has to be reinitialized every time in order to go to the correct page. +However, the intermediate pages are not decoded during the process, so typically it's quite fast. +This is required because multipage codecs does not support going backwards. +After decoding the one page, it is stored inside the collection cache. Hence, trying to get Mat object from already decoded page is O(1). +If you need memory, you can use .releaseCache() method to release cached index. +The space complexity is O(n) if all pages are decoded into memory. The user is able to decode and release images on demand. +*/ +class CV_EXPORTS ImageCollection { +public: + struct CV_EXPORTS iterator { + iterator(ImageCollection* col); + iterator(ImageCollection* col, int end); + Mat& operator*(); + Mat* operator->(); + iterator& operator++(); + iterator operator++(int); + friend bool operator== (const iterator& a, const iterator& b) { return a.m_curr == b.m_curr; }; + friend bool operator!= (const iterator& a, const iterator& b) { return a.m_curr != b.m_curr; }; + + private: + ImageCollection* m_pCollection; + int m_curr; + }; + + ImageCollection(); + ImageCollection(const String& filename, int flags); + void init(const String& img, int flags); + size_t size() const; + const Mat& at(int index); + const Mat& operator[](int index); + void releaseCache(int index); + iterator begin(); + iterator end(); + + class Impl; + Ptr getImpl(); +protected: + Ptr pImpl; +}; //! @} imgcodecs diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index e9b6d0517c..a973c86b4f 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -54,6 +54,8 @@ #include #include #include +#include + /****************************************************************************************\ @@ -658,57 +660,14 @@ bool imreadmulti(const String& filename, std::vector& mats, int start, int static size_t imcount_(const String& filename, int flags) { - /// Search for the relevant decoder to handle the imagery - ImageDecoder decoder; - -#ifdef HAVE_GDAL - if (flags != IMREAD_UNCHANGED && (flags & IMREAD_LOAD_GDAL) == IMREAD_LOAD_GDAL) { - decoder = GdalDecoder().newDecoder(); - } - else { -#else - CV_UNUSED(flags); -#endif - decoder = findDecoder(filename); -#ifdef HAVE_GDAL - } -#endif - - /// if no decoder was found, return nothing. - if (!decoder) { - return 0; - } - - /// set the filename in the driver - decoder->setSource(filename); - - // read the header to make sure it succeeds - try - { - // read the header to make sure it succeeds - if (!decoder->readHeader()) - return 0; - } - catch (const cv::Exception& e) - { - std::cerr << "imcount_('" << filename << "'): can't read header: " << e.what() << std::endl << std::flush; - return 0; - } - catch (...) - { - std::cerr << "imcount_('" << filename << "'): can't read header: unknown exception" << std::endl << std::flush; + try{ + ImageCollection collection(filename, flags); + return collection.size(); + } catch(cv::Exception const& e) { + // Reading header or finding decoder for the filename is failed return 0; } - - size_t result = 1; - - - while (decoder->nextPage()) - { - ++result; - } - - return result; + return 0; } size_t imcount(const String& filename, int flags) @@ -1032,6 +991,247 @@ bool haveImageWriter( const String& filename ) return !encoder.empty(); } +class ImageCollection::Impl { +public: + Impl() = default; + Impl(const std::string& filename, int flags); + void init(String const& filename, int flags); + size_t size() const; + Mat& at(int index); + Mat& operator[](int index); + void releaseCache(int index); + ImageCollection::iterator begin(ImageCollection* ptr); + ImageCollection::iterator end(ImageCollection* ptr); + Mat read(); + int width() const; + int height() const; + bool readHeader(); + Mat readData(); + bool advance(); + int currentIndex() const; + void reset(); + +private: + String m_filename; + int m_flags{}; + std::size_t m_size{}; + int m_width{}; + int m_height{}; + int m_current{}; + std::vector m_pages; + ImageDecoder m_decoder; +}; + +ImageCollection::Impl::Impl(std::string const& filename, int flags) { + this->init(filename, flags); +} + +void ImageCollection::Impl::init(String const& filename, int flags) { + m_filename = filename; + m_flags = flags; + +#ifdef HAVE_GDAL + if (m_flags != IMREAD_UNCHANGED && (m_flags & IMREAD_LOAD_GDAL) == IMREAD_LOAD_GDAL) { + m_decoder = GdalDecoder().newDecoder(); + } + else { +#endif + m_decoder = findDecoder(filename); +#ifdef HAVE_GDAL + } +#endif + + + CV_Assert(m_decoder); + m_decoder->setSource(filename); + CV_Assert(m_decoder->readHeader()); + + // count the pages of the image collection + size_t count = 1; + while(m_decoder->nextPage()) count++; + + m_size = count; + m_pages.resize(m_size); + // Reinitialize the decoder because we advanced to the last page while counting the pages of the image +#ifdef HAVE_GDAL + if (m_flags != IMREAD_UNCHANGED && (m_flags & IMREAD_LOAD_GDAL) == IMREAD_LOAD_GDAL) { + m_decoder = GdalDecoder().newDecoder(); + } + else { +#endif + m_decoder = findDecoder(m_filename); +#ifdef HAVE_GDAL + } +#endif + + m_decoder->setSource(m_filename); + m_decoder->readHeader(); +} + +size_t ImageCollection::Impl::size() const { return m_size; } + +Mat ImageCollection::Impl::read() { + auto result = this->readHeader(); + if(!result) { + return {}; + } + return this->readData(); +} + +int ImageCollection::Impl::width() const { + return m_width; +} + +int ImageCollection::Impl::height() const { + return m_height; +} + +bool ImageCollection::Impl::readHeader() { + bool status = m_decoder->readHeader(); + m_width = m_decoder->width(); + m_height = m_decoder->height(); + return status; +} + +// readHeader must be called before calling this method +Mat ImageCollection::Impl::readData() { + int type = m_decoder->type(); + if ((m_flags & IMREAD_LOAD_GDAL) != IMREAD_LOAD_GDAL && m_flags != IMREAD_UNCHANGED) { + if ((m_flags & IMREAD_ANYDEPTH) == 0) + type = CV_MAKETYPE(CV_8U, CV_MAT_CN(type)); + + if ((m_flags & IMREAD_COLOR) != 0 || + ((m_flags & IMREAD_ANYCOLOR) != 0 && CV_MAT_CN(type) > 1)) + type = CV_MAKETYPE(CV_MAT_DEPTH(type), 3); + else + type = CV_MAKETYPE(CV_MAT_DEPTH(type), 1); + } + + // established the required input image size + Size size = validateInputImageSize(Size(m_width, m_height)); + + Mat mat(size.height, size.width, type); + bool success = false; + try { + if (m_decoder->readData(mat)) + success = true; + } + catch (const cv::Exception &e) { + std::cerr << "ImageCollection class: can't read data: " << e.what() << std::endl << std::flush; + } + catch (...) { + std::cerr << "ImageCollection class:: can't read data: unknown exception" << std::endl << std::flush; + } + if (!success) + return cv::Mat(); + + if ((m_flags & IMREAD_IGNORE_ORIENTATION) == 0 && m_flags != IMREAD_UNCHANGED) { + ApplyExifOrientation(m_decoder->getExifTag(ORIENTATION), mat); + } + + return mat; +} + +bool ImageCollection::Impl::advance() { ++m_current; return m_decoder->nextPage(); } + +int ImageCollection::Impl::currentIndex() const { return m_current; } + +ImageCollection::iterator ImageCollection::Impl::begin(ImageCollection* ptr) { return ImageCollection::iterator(ptr); } + +ImageCollection::iterator ImageCollection::Impl::end(ImageCollection* ptr) { return ImageCollection::iterator(ptr, this->size()); } + +void ImageCollection::Impl::reset() { + m_current = 0; +#ifdef HAVE_GDAL + if (m_flags != IMREAD_UNCHANGED && (m_flags & IMREAD_LOAD_GDAL) == IMREAD_LOAD_GDAL) { + m_decoder = GdalDecoder().newDecoder(); + } + else { +#endif + m_decoder = findDecoder(m_filename); +#ifdef HAVE_GDAL + } +#endif + + m_decoder->setSource(m_filename); + m_decoder->readHeader(); +} + +Mat& ImageCollection::Impl::at(int index) { + CV_Assert(index >= 0 && size_t(index) < m_size); + return operator[](index); +} + +Mat& ImageCollection::Impl::operator[](int index) { + if(m_pages.at(index).empty()) { + // We can't go backward in multi images. If the page is not in vector yet, + // go back to first page and advance until the desired page and read it into memory + if(m_current != index) { + reset(); + for(int i = 0; i != index && advance(); ++i) {} + } + m_pages[index] = read(); + } + return m_pages[index]; +} + +void ImageCollection::Impl::releaseCache(int index) { + CV_Assert(index >= 0 && size_t(index) < m_size); + m_pages[index].release(); +} + +/* ImageCollection API*/ + +ImageCollection::ImageCollection() : pImpl(new Impl()) {} + +ImageCollection::ImageCollection(const std::string& filename, int flags) : pImpl(new Impl(filename, flags)) {} + +void ImageCollection::init(const String& img, int flags) { pImpl->init(img, flags); } + +size_t ImageCollection::size() const { return pImpl->size(); } + +const Mat& ImageCollection::at(int index) { return pImpl->at(index); } + +const Mat& ImageCollection::operator[](int index) { return pImpl->operator[](index); } + +void ImageCollection::releaseCache(int index) { pImpl->releaseCache(index); } + +Ptr ImageCollection::getImpl() { return pImpl; } + +/* Iterator API */ + +ImageCollection::iterator ImageCollection::begin() { return pImpl->begin(this); } + +ImageCollection::iterator ImageCollection::end() { return pImpl->end(this); } + +ImageCollection::iterator::iterator(ImageCollection* col) : m_pCollection(col), m_curr(0) {} + +ImageCollection::iterator::iterator(ImageCollection* col, int end) : m_pCollection(col), m_curr(end) {} + +Mat& ImageCollection::iterator::operator*() { + CV_Assert(m_pCollection); + return m_pCollection->getImpl()->operator[](m_curr); +} + +Mat* ImageCollection::iterator::operator->() { + CV_Assert(m_pCollection); + return &m_pCollection->getImpl()->operator[](m_curr); +} + +ImageCollection::iterator& ImageCollection::iterator::operator++() { + if(m_pCollection->pImpl->currentIndex() == m_curr) { + m_pCollection->pImpl->advance(); + } + m_curr++; + return *this; +} + +ImageCollection::iterator ImageCollection::iterator::operator++(int) { + iterator tmp = *this; + ++(*this); + return tmp; +} + } /* End of file. */ diff --git a/modules/imgcodecs/test/test_read_write.cpp b/modules/imgcodecs/test/test_read_write.cpp index 9dbd2e33c7..b81d34fdf5 100644 --- a/modules/imgcodecs/test/test_read_write.cpp +++ b/modules/imgcodecs/test/test_read_write.cpp @@ -303,4 +303,181 @@ TEST(Imgcodecs_Image, write_umat) EXPECT_EQ(0, remove(dst_name.c_str())); } +TEST(Imgcodecs_Image, multipage_collection_size) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "readwrite/multipage.tif"; + + ImageCollection collection(filename, IMREAD_ANYCOLOR); + EXPECT_EQ((std::size_t)6, collection.size()); +} + +TEST(Imgcodecs_Image, multipage_collection_read_pages_iterator) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "readwrite/multipage.tif"; + const string page_files[] = { + root + "readwrite/multipage_p1.tif", + root + "readwrite/multipage_p2.tif", + root + "readwrite/multipage_p3.tif", + root + "readwrite/multipage_p4.tif", + root + "readwrite/multipage_p5.tif", + root + "readwrite/multipage_p6.tif" + }; + + ImageCollection collection(filename, IMREAD_ANYCOLOR); + + auto collectionBegin = collection.begin(); + for(size_t i = 0; i < collection.size(); ++i, ++collectionBegin) + { + double diff = cv::norm(collectionBegin.operator*(), imread(page_files[i]), NORM_INF); + EXPECT_EQ(0., diff); + } +} + +TEST(Imgcodecs_Image, multipage_collection_two_iterator) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "readwrite/multipage.tif"; + const string page_files[] = { + root + "readwrite/multipage_p1.tif", + root + "readwrite/multipage_p2.tif", + root + "readwrite/multipage_p3.tif", + root + "readwrite/multipage_p4.tif", + root + "readwrite/multipage_p5.tif", + root + "readwrite/multipage_p6.tif" + }; + + ImageCollection collection(filename, IMREAD_ANYCOLOR); + auto firstIter = collection.begin(); + auto secondIter = collection.begin(); + + // Decode all odd pages then decode even pages -> 1, 0, 3, 2 ... + firstIter++; + for(size_t i = 1; i < collection.size(); i += 2, ++firstIter, ++firstIter, ++secondIter, ++secondIter) { + Mat mat = *firstIter; + double diff = cv::norm(mat, imread(page_files[i]), NORM_INF); + EXPECT_EQ(0., diff); + Mat evenMat = *secondIter; + diff = cv::norm(evenMat, imread(page_files[i-1]), NORM_INF); + EXPECT_EQ(0., diff); + } +} + +TEST(Imgcodecs_Image, multipage_collection_operator_plusplus) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "readwrite/multipage.tif"; + + // operator++ test + ImageCollection collection(filename, IMREAD_ANYCOLOR); + auto firstIter = collection.begin(); + auto secondIter = firstIter++; + + // firstIter points to second page, secondIter points to first page + double diff = cv::norm(*firstIter, *secondIter, NORM_INF); + EXPECT_NE(diff, 0.); +} + +TEST(Imgcodecs_Image, multipage_collection_backward_decoding) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "readwrite/multipage.tif"; + const string page_files[] = { + root + "readwrite/multipage_p1.tif", + root + "readwrite/multipage_p2.tif", + root + "readwrite/multipage_p3.tif", + root + "readwrite/multipage_p4.tif", + root + "readwrite/multipage_p5.tif", + root + "readwrite/multipage_p6.tif" + }; + + ImageCollection collection(filename, IMREAD_ANYCOLOR); + EXPECT_EQ((size_t)6, collection.size()); + + // backward decoding -> 5,4,3,2,1,0 + for(int i = (int)collection.size() - 1; i >= 0; --i) + { + cv::Mat ithPage = imread(page_files[i]); + EXPECT_FALSE(ithPage.empty()); + double diff = cv::norm(collection[i], ithPage, NORM_INF); + EXPECT_EQ(diff, 0.); + } + + for(int i = 0; i < (int)collection.size(); ++i) + { + collection.releaseCache(i); + } + + double diff = cv::norm(collection[2], imread(page_files[2]), NORM_INF); + EXPECT_EQ(diff, 0.); +} + +TEST(ImgCodecs, multipage_collection_decoding_range_based_for_loop_test) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "readwrite/multipage.tif"; + const string page_files[] = { + root + "readwrite/multipage_p1.tif", + root + "readwrite/multipage_p2.tif", + root + "readwrite/multipage_p3.tif", + root + "readwrite/multipage_p4.tif", + root + "readwrite/multipage_p5.tif", + root + "readwrite/multipage_p6.tif" + }; + + ImageCollection collection(filename, IMREAD_ANYCOLOR); + + size_t index = 0; + for(auto &i: collection) + { + cv::Mat ithPage = imread(page_files[index]); + EXPECT_FALSE(ithPage.empty()); + double diff = cv::norm(i, ithPage, NORM_INF); + EXPECT_EQ(0., diff); + ++index; + } + EXPECT_EQ(index, collection.size()); + + index = 0; + for(auto &&i: collection) + { + cv::Mat ithPage = imread(page_files[index]); + EXPECT_FALSE(ithPage.empty()); + double diff = cv::norm(i, ithPage, NORM_INF); + EXPECT_EQ(0., diff); + ++index; + } + EXPECT_EQ(index, collection.size()); +} + +TEST(ImgCodecs, multipage_collection_two_iterator_operatorpp) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "readwrite/multipage.tif"; + + ImageCollection imcol(filename, IMREAD_ANYCOLOR); + + auto it0 = imcol.begin(), it1 = it0, it2 = it0; + vector img(6); + for (int i = 0; i < 6; i++) { + img[i] = *it0; + it0->release(); + ++it0; + } + + for (int i = 0; i < 3; i++) { + ++it2; + } + + for (int i = 0; i < 3; i++) { + auto img2 = *it2; + auto img1 = *it1; + ++it2; + ++it1; + EXPECT_TRUE(cv::norm(img2, img[i+3], NORM_INF) == 0); + EXPECT_TRUE(cv::norm(img1, img[i], NORM_INF) == 0); + } +} + }} // namespace