new multipage image decoder api - ImageCollection
authorBerke <iamberkeyavas@gmail.com>
Thu, 16 Jun 2022 23:10:25 +0000 (02:10 +0300)
committerocpalo <iamberkeyavas@gmail.com>
Mon, 19 Sep 2022 17:27:01 +0000 (20:27 +0300)
modules/imgcodecs/include/opencv2/imgcodecs.hpp
modules/imgcodecs/src/loadsave.cpp
modules/imgcodecs/test/test_read_write.cpp

index 67ca34f..cb4a170 100644 (file)
@@ -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<Impl> getImpl();
+protected:
+    Ptr<Impl> pImpl;
+};
 
 //! @} imgcodecs
 
index e9b6d05..a973c86 100644 (file)
@@ -54,6 +54,8 @@
 #include <cerrno>
 #include <opencv2/core/utils/logger.hpp>
 #include <opencv2/core/utils/configuration.private.hpp>
+#include <opencv2/imgcodecs.hpp>
+
 
 
 /****************************************************************************************\
@@ -658,57 +660,14 @@ bool imreadmulti(const String& filename, std::vector<Mat>& 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<cv::Mat> 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::Impl> 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. */
index 9dbd2e3..b81d34f 100644 (file)
@@ -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<Mat> 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