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