Update video conversion

This commit is contained in:
2026-04-13 21:55:57 +10:00
parent 8e60126c4c
commit 3349b45ade
3 changed files with 75 additions and 100 deletions

View File

@@ -1801,8 +1801,8 @@ namespace ANSCENTER
bool ANSOPENCV::ImagesToMP4(const std::string& imageFolder, bool ANSOPENCV::ImagesToMP4(const std::string& imageFolder,
const std::string& outputVideoPath, const std::string& outputVideoPath,
int targetDurationSec) { int maxWidth, int fps) {
// Use per-output-file mutex instead of global mutex // Per-output-file mutex for thread safety across concurrent conversions
static std::mutex mapMutex; static std::mutex mapMutex;
static std::map<std::string, std::unique_ptr<std::timed_mutex>> fileMutexes; static std::map<std::string, std::unique_ptr<std::timed_mutex>> fileMutexes;
@@ -1823,13 +1823,16 @@ namespace ANSCENTER
std::unique_lock<std::timed_mutex> lock(*fileMutex, std::defer_lock); std::unique_lock<std::timed_mutex> lock(*fileMutex, std::defer_lock);
if (!lock.try_lock_for(std::chrono::milliseconds(MUTEX_TIMEOUT_MS))) { if (!lock.try_lock_for(std::chrono::milliseconds(MUTEX_TIMEOUT_MS))) {
std::cerr << "Error: Another thread is writing to " << outputVideoPath << std::endl; 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; cv::VideoWriter videoWriter;
try { try {
// Collect all image files efficiently // Clamp FPS to [1, 60]
fps = max(1, min(60, fps));
// Collect all image files
std::vector<cv::String> imageFiles; std::vector<cv::String> imageFiles;
const std::vector<std::string> extensions = { "*.jpg", "*.jpeg", "*.png", "*.bmp" }; const std::vector<std::string> extensions = { "*.jpg", "*.jpeg", "*.png", "*.bmp" };
@@ -1849,6 +1852,17 @@ namespace ANSCENTER
// Sort for consistent ordering // Sort for consistent ordering
std::sort(imageFiles.begin(), imageFiles.end()); std::sort(imageFiles.begin(), imageFiles.end());
// Cap at 5 minutes max duration
const int maxFrames = fps * 300;
if (static_cast<int>(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<int>(imageFiles.size());
// Read first image to determine dimensions // Read first image to determine dimensions
cv::Mat firstImage = cv::imread(imageFiles[0], cv::IMREAD_COLOR); cv::Mat firstImage = cv::imread(imageFiles[0], cv::IMREAD_COLOR);
if (firstImage.empty()) { if (firstImage.empty()) {
@@ -1856,39 +1870,36 @@ namespace ANSCENTER
return false; return false;
} }
// Target video dimensions (1920x1080) int videoWidth = firstImage.cols;
const int targetWidth = 1920; int videoHeight = firstImage.rows;
const int targetHeight = 1080; bool needsResize = false;
// Calculate scaling to fit within bounds while maintaining aspect ratio if (maxWidth > 0 && firstImage.cols > maxWidth) {
const double scaleX = static_cast<double>(targetWidth) / firstImage.cols; // Scale down to fit within maxWidth, preserving aspect ratio
const double scaleY = static_cast<double>(targetHeight) / firstImage.rows; double scale = static_cast<double>(maxWidth) / firstImage.cols;
const double scale = min(scaleX, scaleY); videoWidth = static_cast<int>(std::round(firstImage.cols * scale));
videoHeight = static_cast<int>(std::round(firstImage.rows * scale));
needsResize = true;
}
// Calculate scaled dimensions (ensure even for H.264) // Force even dimensions (required for H.264)
int scaledWidth = static_cast<int>(std::round(firstImage.cols * scale)); videoWidth = (videoWidth / 2) * 2;
int scaledHeight = static_cast<int>(std::round(firstImage.rows * scale)); videoHeight = (videoHeight / 2) * 2;
// Make dimensions even (required for H.264) if (videoWidth < 2 || videoHeight < 2) {
scaledWidth = (scaledWidth / 2) * 2; std::cerr << "Error: Resulting video dimensions too small: "
scaledHeight = (scaledHeight / 2) * 2; << videoWidth << "x" << videoHeight << std::endl;
return false;
// Calculate centering padding }
const int padLeft = (targetWidth - scaledWidth) / 2;
const int padTop = (targetHeight - scaledHeight) / 2;
std::cout << "[Thread " << std::this_thread::get_id() << "] " std::cout << "[Thread " << std::this_thread::get_id() << "] "
<< "Original: " << firstImage.cols << "x" << firstImage.rows << "Image: " << firstImage.cols << "x" << firstImage.rows
<< " -> Scaled: " << scaledWidth << "x" << scaledHeight << std::endl; << " -> Video: " << videoWidth << "x" << videoHeight
<< " | " << numImages << " frames @ " << fps << " FPS"
<< " (~" << (numImages / fps) << "s)" << std::endl;
// Release firstImage to free memory
firstImage.release(); firstImage.release();
// Video parameters
const int targetFPS = 25;
const int videoDurationSec = max(3, targetDurationSec);
const int totalFrames = videoDurationSec * targetFPS;
// Ensure .mp4 extension // Ensure .mp4 extension
std::string mp4OutputPath = outputVideoPath; std::string mp4OutputPath = outputVideoPath;
if (mp4OutputPath.size() < 4 || if (mp4OutputPath.size() < 4 ||
@@ -1906,14 +1917,14 @@ namespace ANSCENTER
bool codecFound = false; bool codecFound = false;
for (const auto& [name, fourcc] : codecs) { for (const auto& [name, fourcc] : codecs) {
videoWriter.open(mp4OutputPath, fourcc, targetFPS, videoWriter.open(mp4OutputPath, fourcc, fps,
cv::Size(targetWidth, targetHeight), true); cv::Size(videoWidth, videoHeight), true);
if (videoWriter.isOpened()) { if (videoWriter.isOpened()) {
std::cout << "Using codec: " << name << std::endl; std::cout << "Using codec: " << name << std::endl;
codecFound = true; codecFound = true;
break; break;
} }
videoWriter.release(); // Release failed attempt videoWriter.release();
} }
if (!codecFound) { if (!codecFound) {
@@ -1921,21 +1932,11 @@ namespace ANSCENTER
return false; return false;
} }
const int numImages = static_cast<int>(imageFiles.size()); // Pre-allocate reusable matrix
// 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
cv::Mat img; cv::Mat img;
cv::Mat resizedImg; cv::Mat resizedImg;
if (numImages <= totalFrames) { // 1 image = 1 frame
// 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) { for (int i = 0; i < numImages; ++i) {
img = cv::imread(imageFiles[i], cv::IMREAD_COLOR); img = cv::imread(imageFiles[i], cv::IMREAD_COLOR);
if (img.empty()) { if (img.empty()) {
@@ -1943,52 +1944,24 @@ namespace ANSCENTER
continue; continue;
} }
// Reset canvas to black if (needsResize || img.cols != videoWidth || img.rows != videoHeight) {
canvas.setTo(cv::Scalar(0, 0, 0)); cv::resize(img, resizedImg, cv::Size(videoWidth, videoHeight),
// Resize and place on canvas
cv::resize(img, resizedImg, cv::Size(scaledWidth, scaledHeight),
0, 0, cv::INTER_AREA); 0, 0, cv::INTER_AREA);
resizedImg.copyTo(canvas(roi)); videoWriter.write(resizedImg);
img.release();
int framesToWrite = framesPerImage + (i < remainingFrames ? 1 : 0);
for (int j = 0; j < framesToWrite; ++j) {
videoWriter.write(canvas);
}
}
} }
else { else {
// Case 2: Many images - sample to fit total frames videoWriter.write(img);
for (int frame = 0; frame < totalFrames; ++frame) {
const double imageIndex = (static_cast<double>(frame) * (numImages - 1)) /
(totalFrames - 1);
const int imageIdx = static_cast<int>(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),
0, 0, cv::INTER_AREA);
resizedImg.copyTo(canvas(roi));
img.release(); img.release();
videoWriter.write(canvas);
}
} }
// Explicit cleanup
canvas.release();
resizedImg.release(); resizedImg.release();
videoWriter.release(); videoWriter.release();
std::cout << "Video created: " << mp4OutputPath std::cout << "Video created: " << mp4OutputPath
<< " (" << totalFrames << " frames, " << numImages << " images)" << std::endl; << " (" << numImages << " frames, "
<< fps << " FPS, ~" << (numImages / fps) << "s)" << std::endl;
return true; 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( extern "C" __declspec(dllexport) int ANSCV_ImagesToMP4_S(
const char* imageFolder, const char* imageFolder,
const char* outputVideoPath, const char* outputVideoPath,
int targetDurationSec) { int maxWidth, int fps) {
try { try {
// Remove the timeImageMutex lock entirely!
// ImagesToMP4 already handles per-file synchronization
if (!imageFolder || strlen(imageFolder) == 0) { if (!imageFolder || strlen(imageFolder) == 0) {
std::cerr << "Error: Invalid image folder path!" << std::endl; std::cerr << "Error: Invalid image folder path!" << std::endl;
return -1; return -1;
} }
if (!outputVideoPath || strlen(outputVideoPath) == 0) {
std::cerr << "Error: Invalid output video path!" << std::endl;
return -1;
}
bool success = ANSCENTER::ANSOPENCV::ImagesToMP4( bool success = ANSCENTER::ANSOPENCV::ImagesToMP4(
imageFolder, outputVideoPath, targetDurationSec); imageFolder, outputVideoPath, maxWidth, fps);
if (!success) { if (!success) {
std::cerr << "Error: Failed to create MP4 from: " std::cerr << "Error: Failed to create MP4 from: "

View File

@@ -85,7 +85,7 @@ namespace ANSCENTER
static cv::Mat resizeImageToFit(const cv::Mat& inputImage, int maxWidth, int maxHeight, int& newWidth, int& newHeight); 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 resizeImage(cv::Mat& inputImage, int resizeWidth, int orginalImageSize=0);
static bool cropImage(cv::Mat& inputImage, const cv::Rect& resizeROI, int originalImageSize=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: private:
void CheckLicense(); 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_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) // IMAQ -> cv::Mat conversion (NI Vision Image*, auto-detects indirection level)
extern "C" __declspec(dllexport) int ANSCV_IMAQ2Image(void* imaqHandle, cv::Mat** imageOut); extern "C" __declspec(dllexport) int ANSCV_IMAQ2Image(void* imaqHandle, cv::Mat** imageOut);

View File

@@ -1366,9 +1366,9 @@ int TestGetImage() {
} }
int GenerateVideo() { int GenerateVideo() {
std::string imageFolder = "E:\\Programs\\DemoAssets\\ImageSeries\\TestImages"; std::string imageFolder = "E:\\Programs\\DemoAssets\\ImageSeries\\20260413_152604.321";
std::string outputVideoPath = "E:\\Programs\\DemoAssets\\ImageSeries\\output.mp4"; std::string outputVideoPath = "E:\\Programs\\DemoAssets\\ImageSeries\\output1.mp4";
bool conversionResult = ANSCV_ImagesToMP4_S(imageFolder.c_str(), outputVideoPath.c_str(), 3.5); int conversionResult = ANSCV_ImagesToMP4_S(imageFolder.c_str(), outputVideoPath.c_str(), 0, 5);
if (!conversionResult) { if (!conversionResult) {
std::cerr << "Failed to convert images to MP4." << std::endl; std::cerr << "Failed to convert images to MP4." << std::endl;
return -1; return -1;
@@ -1419,12 +1419,12 @@ int main()
{ {
ANSCENTER::ANSOPENCV::InitCameraNetwork(); ANSCENTER::ANSOPENCV::InitCameraNetwork();
//OpenCVFunctionTest(); //OpenCVFunctionTest();
//GenerateVideo(); GenerateVideo();
//VideoTestClient(); //VideoTestClient();
// TestGetImage(); // TestGetImage();
//PureOpenCV(); //PureOpenCV();
// RSTPTestClient(); // RSTPTestClient();
RSTPTestCVClient(); //RSTPTestCVClient();
//TestCreateImageFromJpegStringFile(); //TestCreateImageFromJpegStringFile();
//TestCreateImageFromFile(); //TestCreateImageFromFile();
//for (int i = 0; i < 100; i++) { //for (int i = 0; i < 100; i++) {