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,
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<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);
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<cv::String> imageFiles;
const std::vector<std::string> 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<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
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<double>(targetWidth) / firstImage.cols;
const double scaleY = static_cast<double>(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<double>(maxWidth) / firstImage.cols;
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)
int scaledWidth = static_cast<int>(std::round(firstImage.cols * scale));
int scaledHeight = static_cast<int>(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<int>(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<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),
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: "