diff --git a/modules/videoio/include/opencv2/videoio/utils.private.hpp b/modules/videoio/include/opencv2/videoio/utils.private.hpp new file mode 100644 index 0000000000..e331aaf2ac --- /dev/null +++ b/modules/videoio/include/opencv2/videoio/utils.private.hpp @@ -0,0 +1,15 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef OPENCV_VIDEOIO_UTILS_PRIVATE_HPP +#define OPENCV_VIDEOIO_UTILS_PRIVATE_HPP + +#include "opencv2/core/cvdef.h" +#include + +namespace cv { +CV_EXPORTS std::string icvExtractPattern(const std::string& filename, unsigned *offset); +} + +#endif // OPENCV_VIDEOIO_UTILS_PRIVATE_HPP diff --git a/modules/videoio/src/cap_images.cpp b/modules/videoio/src/cap_images.cpp index b506dd1c06..d667e12090 100644 --- a/modules/videoio/src/cap_images.cpp +++ b/modules/videoio/src/cap_images.cpp @@ -51,8 +51,8 @@ #include "precomp.hpp" #include "opencv2/imgcodecs.hpp" - #include "opencv2/core/utils/filesystem.hpp" +#include "opencv2/videoio/utils.private.hpp" #if 0 #define CV_WARN(message) @@ -200,7 +200,7 @@ bool CvCapture_Images::setProperty(int id, double value) return false; } -static +// static std::string icvExtractPattern(const std::string& filename, unsigned *offset) { size_t len = filename.size(); diff --git a/modules/videoio/test/test_images.cpp b/modules/videoio/test/test_images.cpp new file mode 100644 index 0000000000..ccf507a50d --- /dev/null +++ b/modules/videoio/test/test_images.cpp @@ -0,0 +1,294 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "test_precomp.hpp" +#include "opencv2/core/utils/filesystem.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/videoio/utils.private.hpp" + +using namespace std; + +namespace opencv_test { namespace { + +struct ImageCollection +{ + string dirname; + string base; + string ext; + size_t first_idx; + size_t last_idx; + size_t width; +public: + ImageCollection(const char *dirname_template = "opencv_test_images") + : first_idx(0), last_idx(0), width(0) + { + dirname = cv::tempfile(dirname_template); + cv::utils::fs::createDirectory(dirname); + } + ~ImageCollection() + { + cleanup(); + } + void cleanup() + { + cv::utils::fs::remove_all(dirname); + } + void generate(size_t count, size_t first = 0, size_t width_ = 4, const string & base_ = "test", const string & ext_ = "png") + { + base = base_; + ext = ext_; + first_idx = first; + last_idx = first + count - 1; + width = width_; + for (size_t idx = first_idx; idx <= last_idx; ++idx) + { + const string filename = getFilename(idx); + imwrite(filename, getFrame(idx)); + } + } + string getFilename(size_t idx = 0) const + { + ostringstream buf; + buf << dirname << "/" << base << setw(width) << setfill('0') << idx << "." << ext; + return buf.str(); + } + string getPatternFilename() const + { + ostringstream buf; + buf << dirname << "/" << base << "%0" << width << "d" << "." << ext; + return buf.str(); + } + string getFirstFilename() const + { + return getFilename(first_idx); + } + Mat getFirstFrame() const + { + return getFrame(first_idx); + } + size_t getCount() const + { + return last_idx - first_idx + 1; + } + string getDirname() const + { + return dirname; + } + static Mat getFrame(size_t idx) + { + const int sz = 100; // 100x100 or bigger + Mat res(sz, sz, CV_8UC3, Scalar::all(0)); + circle(res, Point(idx % 100), idx % 50, Scalar::all(255), 2, LINE_8); + return res; + } +}; + +//================================================================================================== + +TEST(videoio_images, basic_read) +{ + ImageCollection col; + col.generate(20); + VideoCapture cap(col.getFirstFilename(), CAP_IMAGES); + ASSERT_TRUE(cap.isOpened()); + size_t idx = 0; + while (cap.isOpened()) // TODO: isOpened is always true, even if there are no more images + { + Mat img; + const bool read_res = cap.read(img); + if (!read_res) + break; + EXPECT_MAT_N_DIFF(img, col.getFrame(idx), 0); + ++idx; + } + EXPECT_EQ(col.getCount(), idx); +} + +TEST(videoio_images, basic_write) +{ + // writer should create files: test0000.png, ... test0019.png + ImageCollection col; + col.generate(1); + VideoWriter wri(col.getFirstFilename(), CAP_IMAGES, 0, 0, col.getFrame(0).size()); + ASSERT_TRUE(wri.isOpened()); + size_t idx = 0; + while (wri.isOpened()) + { + wri << col.getFrame(idx); + Mat actual = imread(col.getFilename(idx)); + EXPECT_MAT_N_DIFF(col.getFrame(idx), actual, 0); + if (++idx >= 20) + break; + } + wri.release(); + ASSERT_FALSE(wri.isOpened()); +} + +TEST(videoio_images, bad) +{ + ImageCollection col; + { + ostringstream buf; buf << col.getDirname() << "/missing0000.png"; + VideoCapture cap(buf.str(), CAP_IMAGES); + EXPECT_FALSE(cap.isOpened()); + Mat img; + EXPECT_FALSE(cap.read(img)); + } +} + +TEST(videoio_images, seek) +{ + // check files: test0005.png, ..., test0024.png + // seek to valid and invalid frame numbers + // position is zero-based: valid frame numbers are 0, ..., 19 + const int count = 20; + ImageCollection col; + col.generate(count, 5); + VideoCapture cap(col.getFirstFilename(), CAP_IMAGES); + ASSERT_TRUE(cap.isOpened()); + EXPECT_EQ((size_t)count, (size_t)cap.get(CAP_PROP_FRAME_COUNT)); + vector positions { count / 2, 0, 1, count - 1, count, count + 100, -1, -100 }; + for (const auto &pos : positions) + { + Mat img; + const bool res = cap.set(CAP_PROP_POS_FRAMES, pos); + if (pos >= count || pos < 0) // invalid position + { +// EXPECT_FALSE(res); // TODO: backend clamps invalid value to valid range, actual result is 'true' + } + else + { + EXPECT_TRUE(res); + EXPECT_GE(1., cap.get(CAP_PROP_POS_AVI_RATIO)); + EXPECT_NEAR((double)pos / (count - 1), cap.get(CAP_PROP_POS_AVI_RATIO), 1e-2); + EXPECT_EQ(pos, static_cast(cap.get(CAP_PROP_POS_FRAMES))); + EXPECT_TRUE(cap.read(img)); + EXPECT_MAT_N_DIFF(img, col.getFrame(col.first_idx + pos), 0); + } + } +} + +TEST(videoio_images, pattern_overflow) +{ + // check files: test0.png, ..., test11.png + ImageCollection col; + col.generate(12, 0, 1); + + { + VideoCapture cap(col.getFirstFilename(), CAP_IMAGES); + ASSERT_TRUE(cap.isOpened()); + for (size_t idx = col.first_idx; idx <= col.last_idx; ++idx) + { + Mat img; + EXPECT_TRUE(cap.read(img)); + EXPECT_MAT_N_DIFF(img, col.getFrame(idx), 0); + } + } + { + VideoCapture cap(col.getPatternFilename(), CAP_IMAGES); + ASSERT_TRUE(cap.isOpened()); + for (size_t idx = col.first_idx; idx <= col.last_idx; ++idx) + { + Mat img; + EXPECT_TRUE(cap.read(img)); + EXPECT_MAT_N_DIFF(img, col.getFrame(idx), 0); + } + } +} + +TEST(videoio_images, pattern_max) +{ + // max supported number width for starting image is 9 digits + // but following images can be read as well + // test999999999.png ; test1000000000.png + ImageCollection col; + col.generate(2, 1000000000 - 1); + { + VideoCapture cap(col.getFirstFilename(), CAP_IMAGES); + ASSERT_TRUE(cap.isOpened()); + Mat img; + EXPECT_TRUE(cap.read(img)); + EXPECT_MAT_N_DIFF(img, col.getFrame(col.first_idx), 0); + EXPECT_TRUE(cap.read(img)); + EXPECT_MAT_N_DIFF(img, col.getFrame(col.first_idx + 1), 0); + } + { + VideoWriter wri(col.getFirstFilename(), CAP_IMAGES, 0, 0, col.getFirstFrame().size()); + ASSERT_TRUE(wri.isOpened()); + Mat img = col.getFrame(0); + wri.write(img); + wri.write(img); + Mat actual; + actual = imread(col.getFilename(col.first_idx)); + EXPECT_MAT_N_DIFF(actual, img, 0); + actual = imread(col.getFilename(col.first_idx)); + EXPECT_MAT_N_DIFF(actual, img, 0); + } +} + +TEST(videoio_images, extract_pattern) +{ + unsigned offset = 0; + + // Min and max values + EXPECT_EQ("%01d.png", cv::icvExtractPattern("0.png", &offset)); + EXPECT_EQ(0u, offset); + EXPECT_EQ("%09d.png", cv::icvExtractPattern("999999999.png", &offset)); + EXPECT_EQ(999999999u, offset); + + // Regular usage - start, end, middle + EXPECT_EQ("abc%04ddef.png", cv::icvExtractPattern("abc0048def.png", &offset)); + EXPECT_EQ(48u, offset); + EXPECT_EQ("%05dabcdef.png", cv::icvExtractPattern("00049abcdef.png", &offset)); + EXPECT_EQ(49u, offset); + EXPECT_EQ("abcdef%06d.png", cv::icvExtractPattern("abcdef000050.png", &offset)); + EXPECT_EQ(50u, offset); + + // Minus handling (should not handle) + EXPECT_EQ("abcdef-%01d.png", cv::icvExtractPattern("abcdef-8.png", &offset)); + EXPECT_EQ(8u, offset); + + // Two numbers (should select first) + // TODO: shouldn't it be last number? + EXPECT_EQ("%01d-abcdef-8.png", cv::icvExtractPattern("7-abcdef-8.png", &offset)); + EXPECT_EQ(7u, offset); + + // Paths (should select filename) + EXPECT_EQ("images005/abcdef%03d.png", cv::icvExtractPattern("images005/abcdef006.png", &offset)); + EXPECT_EQ(6u, offset); + // TODO: fix + // EXPECT_EQ("images03\\abcdef%02d.png", cv::icvExtractPattern("images03\\abcdef04.png", &offset)); + // EXPECT_EQ(4, offset); + EXPECT_EQ("/home/user/test/0/3348/../../3442/./0/1/3/4/5/14304324234/%01d.png", + cv::icvExtractPattern("/home/user/test/0/3348/../../3442/./0/1/3/4/5/14304324234/2.png", &offset)); + EXPECT_EQ(2u, offset); + + // Patterns '%0?[0-9][du]' + EXPECT_EQ("test%d.png", cv::icvExtractPattern("test%d.png", &offset)); + EXPECT_EQ(0u, offset); + EXPECT_EQ("test%0d.png", cv::icvExtractPattern("test%0d.png", &offset)); + EXPECT_EQ(0u, offset); + EXPECT_EQ("test%09d.png", cv::icvExtractPattern("test%09d.png", &offset)); + EXPECT_EQ(0u, offset); + EXPECT_EQ("test%5u.png", cv::icvExtractPattern("test%5u.png", &offset)); + EXPECT_EQ(0u, offset); + + // Invalid arguments + EXPECT_THROW(cv::icvExtractPattern(string(), &offset), cv::Exception); + // TODO: fix? + // EXPECT_EQ(0u, offset); + EXPECT_THROW(cv::icvExtractPattern("test%010d.png", &offset), cv::Exception); + EXPECT_EQ(0u, offset); + EXPECT_THROW(cv::icvExtractPattern("1000000000.png", &offset), cv::Exception); + EXPECT_EQ(0u, offset); + EXPECT_THROW(cv::icvExtractPattern("1.png", NULL), cv::Exception); +} + +// TODO: should writer overwrite files? +// TODO: is clamping good for seeking? +// TODO: missing files? E.g. 3, 4, 6, 7, 8 (should it finish OR jump over OR return empty frame?) +// TODO: non-numbered files (https://github.com/opencv/opencv/pull/23815) +// TODO: when opening with pattern (e.g. test%01d.png), first frame can be only 0 (test0.png) + +}} // opencv_test:::: diff --git a/modules/videoio/test/test_precomp.hpp b/modules/videoio/test/test_precomp.hpp index cffdf2bef4..e9ac63d999 100644 --- a/modules/videoio/test/test_precomp.hpp +++ b/modules/videoio/test/test_precomp.hpp @@ -7,6 +7,7 @@ #include #include "opencv2/ts.hpp" +#include "opencv2/ts/ocl_test.hpp" #include "opencv2/videoio.hpp" #include "opencv2/videoio/registry.hpp" #include "opencv2/core/private.hpp"