From 3349b45ade87f5a5bb33333d4991da134cf74628 Mon Sep 17 00:00:00 2001 From: Tuan Nghia Nguyen Date: Mon, 13 Apr 2026 21:55:57 +1000 Subject: [PATCH] Update video conversion --- modules/ANSCV/ANSOpenCV.cpp | 161 ++++++++++-------------- modules/ANSCV/ANSOpenCV.h | 4 +- tests/ANSCV-UnitTest/ANSCV-UnitTest.cpp | 10 +- 3 files changed, 75 insertions(+), 100 deletions(-) diff --git a/modules/ANSCV/ANSOpenCV.cpp b/modules/ANSCV/ANSOpenCV.cpp index e326307..c1f8f36 100644 --- a/modules/ANSCV/ANSOpenCV.cpp +++ b/modules/ANSCV/ANSOpenCV.cpp @@ -1801,8 +1801,8 @@ namespace ANSCENTER bool ANSOPENCV::ImagesToMP4(const std::string& imageFolder, const std::string& outputVideoPath, - int targetDurationSec) { - // Use per-output-file mutex instead of global mutex + int maxWidth, int fps) { + // Per-output-file mutex for thread safety across concurrent conversions static std::mutex mapMutex; static std::map> fileMutexes; @@ -1823,13 +1823,16 @@ namespace ANSCENTER std::unique_lock lock(*fileMutex, std::defer_lock); if (!lock.try_lock_for(std::chrono::milliseconds(MUTEX_TIMEOUT_MS))) { std::cerr << "Error: Another thread is writing to " << outputVideoPath << std::endl; - return false; // Changed from -6 to false for consistency + return false; } cv::VideoWriter videoWriter; try { - // Collect all image files efficiently + // Clamp FPS to [1, 60] + fps = max(1, min(60, fps)); + + // Collect all image files std::vector imageFiles; const std::vector extensions = { "*.jpg", "*.jpeg", "*.png", "*.bmp" }; @@ -1849,6 +1852,17 @@ namespace ANSCENTER // Sort for consistent ordering std::sort(imageFiles.begin(), imageFiles.end()); + // Cap at 5 minutes max duration + const int maxFrames = fps * 300; + if (static_cast(imageFiles.size()) > maxFrames) { + std::cout << "Warning: Truncating from " << imageFiles.size() + << " to " << maxFrames << " images (5-minute limit at " + << fps << " FPS)" << std::endl; + imageFiles.resize(maxFrames); + } + + const int numImages = static_cast(imageFiles.size()); + // Read first image to determine dimensions cv::Mat firstImage = cv::imread(imageFiles[0], cv::IMREAD_COLOR); if (firstImage.empty()) { @@ -1856,39 +1870,36 @@ namespace ANSCENTER return false; } - // Target video dimensions (1920x1080) - const int targetWidth = 1920; - const int targetHeight = 1080; + int videoWidth = firstImage.cols; + int videoHeight = firstImage.rows; + bool needsResize = false; - // Calculate scaling to fit within bounds while maintaining aspect ratio - const double scaleX = static_cast(targetWidth) / firstImage.cols; - const double scaleY = static_cast(targetHeight) / firstImage.rows; - const double scale = min(scaleX, scaleY); + if (maxWidth > 0 && firstImage.cols > maxWidth) { + // Scale down to fit within maxWidth, preserving aspect ratio + double scale = static_cast(maxWidth) / firstImage.cols; + videoWidth = static_cast(std::round(firstImage.cols * scale)); + videoHeight = static_cast(std::round(firstImage.rows * scale)); + needsResize = true; + } - // Calculate scaled dimensions (ensure even for H.264) - int scaledWidth = static_cast(std::round(firstImage.cols * scale)); - int scaledHeight = static_cast(std::round(firstImage.rows * scale)); + // Force even dimensions (required for H.264) + videoWidth = (videoWidth / 2) * 2; + videoHeight = (videoHeight / 2) * 2; - // Make dimensions even (required for H.264) - scaledWidth = (scaledWidth / 2) * 2; - scaledHeight = (scaledHeight / 2) * 2; - - // Calculate centering padding - const int padLeft = (targetWidth - scaledWidth) / 2; - const int padTop = (targetHeight - scaledHeight) / 2; + if (videoWidth < 2 || videoHeight < 2) { + std::cerr << "Error: Resulting video dimensions too small: " + << videoWidth << "x" << videoHeight << std::endl; + return false; + } std::cout << "[Thread " << std::this_thread::get_id() << "] " - << "Original: " << firstImage.cols << "x" << firstImage.rows - << " -> Scaled: " << scaledWidth << "x" << scaledHeight << std::endl; + << "Image: " << firstImage.cols << "x" << firstImage.rows + << " -> Video: " << videoWidth << "x" << videoHeight + << " | " << numImages << " frames @ " << fps << " FPS" + << " (~" << (numImages / fps) << "s)" << std::endl; - // Release firstImage to free memory firstImage.release(); - // Video parameters - const int targetFPS = 25; - const int videoDurationSec = max(3, targetDurationSec); - const int totalFrames = videoDurationSec * targetFPS; - // Ensure .mp4 extension std::string mp4OutputPath = outputVideoPath; if (mp4OutputPath.size() < 4 || @@ -1906,14 +1917,14 @@ namespace ANSCENTER bool codecFound = false; for (const auto& [name, fourcc] : codecs) { - videoWriter.open(mp4OutputPath, fourcc, targetFPS, - cv::Size(targetWidth, targetHeight), true); + videoWriter.open(mp4OutputPath, fourcc, fps, + cv::Size(videoWidth, videoHeight), true); if (videoWriter.isOpened()) { std::cout << "Using codec: " << name << std::endl; codecFound = true; break; } - videoWriter.release(); // Release failed attempt + videoWriter.release(); } if (!codecFound) { @@ -1921,74 +1932,36 @@ namespace ANSCENTER return false; } - const int numImages = static_cast(imageFiles.size()); - - // Pre-create black canvas and ROI (reuse for all frames) - cv::Mat canvas(targetHeight, targetWidth, CV_8UC3, cv::Scalar(0, 0, 0)); - const cv::Rect roi(padLeft, padTop, scaledWidth, scaledHeight); - - // Pre-allocate matrices for reuse + // Pre-allocate reusable matrix cv::Mat img; cv::Mat resizedImg; - if (numImages <= totalFrames) { - // Case 1: Few images - each shown for multiple frames - const int framesPerImage = totalFrames / numImages; - const int remainingFrames = totalFrames % numImages; - - for (int i = 0; i < numImages; ++i) { - img = cv::imread(imageFiles[i], cv::IMREAD_COLOR); - if (img.empty()) { - std::cerr << "Warning: Could not read: " << imageFiles[i] << std::endl; - continue; - } - - // Reset canvas to black - canvas.setTo(cv::Scalar(0, 0, 0)); - - // Resize and place on canvas - cv::resize(img, resizedImg, cv::Size(scaledWidth, scaledHeight), - 0, 0, cv::INTER_AREA); - resizedImg.copyTo(canvas(roi)); - - img.release(); - - int framesToWrite = framesPerImage + (i < remainingFrames ? 1 : 0); - for (int j = 0; j < framesToWrite; ++j) { - videoWriter.write(canvas); - } + // 1 image = 1 frame + for (int i = 0; i < numImages; ++i) { + img = cv::imread(imageFiles[i], cv::IMREAD_COLOR); + if (img.empty()) { + std::cerr << "Warning: Could not read: " << imageFiles[i] << std::endl; + continue; } - } - else { - // Case 2: Many images - sample to fit total frames - for (int frame = 0; frame < totalFrames; ++frame) { - const double imageIndex = (static_cast(frame) * (numImages - 1)) / - (totalFrames - 1); - const int imageIdx = static_cast(std::round(imageIndex)); - img = cv::imread(imageFiles[imageIdx], cv::IMREAD_COLOR); - if (img.empty()) { - std::cerr << "Warning: Could not read: " << imageFiles[imageIdx] << std::endl; - continue; - } - - canvas.setTo(cv::Scalar(0, 0, 0)); - cv::resize(img, resizedImg, cv::Size(scaledWidth, scaledHeight), + if (needsResize || img.cols != videoWidth || img.rows != videoHeight) { + cv::resize(img, resizedImg, cv::Size(videoWidth, videoHeight), 0, 0, cv::INTER_AREA); - resizedImg.copyTo(canvas(roi)); - - img.release(); - videoWriter.write(canvas); + videoWriter.write(resizedImg); } + else { + videoWriter.write(img); + } + + img.release(); } - // Explicit cleanup - canvas.release(); resizedImg.release(); videoWriter.release(); - std::cout << "✓ Video created: " << mp4OutputPath - << " (" << totalFrames << " frames, " << numImages << " images)" << std::endl; + std::cout << "Video created: " << mp4OutputPath + << " (" << numImages << " frames, " + << fps << " FPS, ~" << (numImages / fps) << "s)" << std::endl; return true; } @@ -4514,19 +4487,21 @@ extern "C" __declspec(dllexport) int ANSCV_ImagePatternMatchs_S(cv::Mat** imageI extern "C" __declspec(dllexport) int ANSCV_ImagesToMP4_S( const char* imageFolder, const char* outputVideoPath, - int targetDurationSec) { + int maxWidth, int fps) { try { - // Remove the timeImageMutex lock entirely! - // ImagesToMP4 already handles per-file synchronization - if (!imageFolder || strlen(imageFolder) == 0) { std::cerr << "Error: Invalid image folder path!" << std::endl; return -1; } + if (!outputVideoPath || strlen(outputVideoPath) == 0) { + std::cerr << "Error: Invalid output video path!" << std::endl; + return -1; + } + bool success = ANSCENTER::ANSOPENCV::ImagesToMP4( - imageFolder, outputVideoPath, targetDurationSec); + imageFolder, outputVideoPath, maxWidth, fps); if (!success) { std::cerr << "Error: Failed to create MP4 from: " diff --git a/modules/ANSCV/ANSOpenCV.h b/modules/ANSCV/ANSOpenCV.h index c2d0a8c..b689590 100644 --- a/modules/ANSCV/ANSOpenCV.h +++ b/modules/ANSCV/ANSOpenCV.h @@ -85,7 +85,7 @@ namespace ANSCENTER static cv::Mat resizeImageToFit(const cv::Mat& inputImage, int maxWidth, int maxHeight, int& newWidth, int& newHeight); static bool resizeImage(cv::Mat& inputImage, int resizeWidth, int orginalImageSize=0); static bool cropImage(cv::Mat& inputImage, const cv::Rect& resizeROI, int originalImageSize=0); - static bool ImagesToMP4(const std::string& imageFolder, const std::string& outputVideoPath, int targetDurationSec); + static bool ImagesToMP4(const std::string& imageFolder, const std::string& outputVideoPath, int maxWidth, int fps); private: void CheckLicense(); @@ -165,7 +165,7 @@ extern "C" __declspec(dllexport) int ANSCV_ImageQRDecoder_S(cv::Mat** imageIn, extern "C" __declspec(dllexport) int ANSCV_ImagePatternMatchs_S(cv::Mat** imageIn, const char* templateFilePath, double theshold, LStrHandle detectedMatchedLocations); -extern "C" __declspec(dllexport) int ANSCV_ImagesToMP4_S(const char* imageFolder, const char* outputVideoPath, int targetDurationSec); +extern "C" __declspec(dllexport) int ANSCV_ImagesToMP4_S(const char* imageFolder, const char* outputVideoPath, int maxWidth, int fps); // IMAQ -> cv::Mat conversion (NI Vision Image*, auto-detects indirection level) extern "C" __declspec(dllexport) int ANSCV_IMAQ2Image(void* imaqHandle, cv::Mat** imageOut); diff --git a/tests/ANSCV-UnitTest/ANSCV-UnitTest.cpp b/tests/ANSCV-UnitTest/ANSCV-UnitTest.cpp index 042b399..e0f0bbb 100644 --- a/tests/ANSCV-UnitTest/ANSCV-UnitTest.cpp +++ b/tests/ANSCV-UnitTest/ANSCV-UnitTest.cpp @@ -1366,9 +1366,9 @@ int TestGetImage() { } int GenerateVideo() { - std::string imageFolder = "E:\\Programs\\DemoAssets\\ImageSeries\\TestImages"; - std::string outputVideoPath = "E:\\Programs\\DemoAssets\\ImageSeries\\output.mp4"; - bool conversionResult = ANSCV_ImagesToMP4_S(imageFolder.c_str(), outputVideoPath.c_str(), 3.5); + std::string imageFolder = "E:\\Programs\\DemoAssets\\ImageSeries\\20260413_152604.321"; + std::string outputVideoPath = "E:\\Programs\\DemoAssets\\ImageSeries\\output1.mp4"; + int conversionResult = ANSCV_ImagesToMP4_S(imageFolder.c_str(), outputVideoPath.c_str(), 0, 5); if (!conversionResult) { std::cerr << "Failed to convert images to MP4." << std::endl; return -1; @@ -1419,12 +1419,12 @@ int main() { ANSCENTER::ANSOPENCV::InitCameraNetwork(); //OpenCVFunctionTest(); - //GenerateVideo(); + GenerateVideo(); //VideoTestClient(); // TestGetImage(); //PureOpenCV(); // RSTPTestClient(); - RSTPTestCVClient(); + //RSTPTestCVClient(); //TestCreateImageFromJpegStringFile(); //TestCreateImageFromFile(); //for (int i = 0; i < 100; i++) {