Update video conversion
This commit is contained in:
@@ -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: "
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user