This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
using MvCamCtrl.NET;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Accord.Video;
|
||||
using Accord.Video.FFMPEG;
|
||||
|
||||
namespace BasicDemo
|
||||
{
|
||||
@@ -54,32 +55,29 @@ namespace BasicDemo
|
||||
private bool _shouldCapture = false;
|
||||
private bool _camerasInitialized = false;
|
||||
|
||||
// 视频保存配置(新增)
|
||||
private string _videoSavePath = @"D:\CameraRecordings\"; // 修改为你的保存路径
|
||||
private double _frameRate = 30.0; // 默认帧率
|
||||
// 视频录制相关变量(修复后的版本)
|
||||
private bool[] recordingFlags = new bool[MAX_CAMERAS];
|
||||
private string[] recordingPaths = new string[MAX_CAMERAS];
|
||||
private int _frameRate = 30;
|
||||
private string _videoSavePath = @"D:\CameraRecordings\";
|
||||
private List<string>[] imagePaths = new List<string>[MAX_CAMERAS]; // 临时保存图片路径
|
||||
private int[] frameCounters = new int[MAX_CAMERAS]; // 帧计数器
|
||||
|
||||
// 添加:帧队列用于异步保存(可选)
|
||||
private ConcurrentQueue<Bitmap>[] frameQueues = new ConcurrentQueue<Bitmap>[MAX_CAMERAS];
|
||||
private Thread[] saveThreads = new Thread[MAX_CAMERAS];
|
||||
private bool[] saveThreadRunning = new bool[MAX_CAMERAS];
|
||||
// 添加视频录制状态标签
|
||||
private Label[] recordingStatusLabels = new Label[MAX_CAMERAS];
|
||||
|
||||
|
||||
|
||||
|
||||
// 构造函数
|
||||
public AutoCameraForm(GetRunStatusDelegate getRunStatusCallback, ReturnToPreviousFormDelegate returnCallback = null, string videoSavePath = null)
|
||||
public AutoCameraForm(GetRunStatusDelegate getRunStatusCallback, ReturnToPreviousFormDelegate returnCallback = null)
|
||||
{
|
||||
_getRunStatusCallback = getRunStatusCallback;
|
||||
// 如果提供了返回回调,就订阅
|
||||
if (returnCallback != null)
|
||||
{
|
||||
OnReturnRequested += returnCallback;
|
||||
}
|
||||
|
||||
//// 设置视频保存路径
|
||||
//if (!string.IsNullOrEmpty(videoSavePath))
|
||||
//{
|
||||
// _videoSavePath = videoSavePath;
|
||||
//}
|
||||
|
||||
//// 确保保存目录存在
|
||||
//Directory.CreateDirectory(_videoSavePath);
|
||||
InitializeComponent();
|
||||
Control.CheckForIllegalCrossThreadCalls = false;
|
||||
|
||||
@@ -92,28 +90,27 @@ namespace BasicDemo
|
||||
// 初始化UI
|
||||
InitializeUI();
|
||||
|
||||
// 窗体加载时初始化相机
|
||||
this.Load += (s, e) => InitializeCameras();
|
||||
// 初始化队列
|
||||
for (int i = 0; i < MAX_CAMERAS; i++)
|
||||
{
|
||||
imagePaths[i] = new List<string>();
|
||||
frameQueues[i] = new ConcurrentQueue<Bitmap>();
|
||||
}
|
||||
|
||||
// 异步初始化相机,不阻塞窗体显示
|
||||
this.Shown += (s, e) =>
|
||||
{
|
||||
Task.Run(() => InitializeCameras());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 初始化UI界面
|
||||
/// </summary>
|
||||
private void InitializeUI()
|
||||
{
|
||||
this.BackColor = Color.FromArgb(240, 240, 240);
|
||||
//Button btnDiagnose = new Button
|
||||
//{
|
||||
// Text = "诊断",
|
||||
// Size = new Size(80, 30),
|
||||
// Location = new Point(1800, 30),
|
||||
// BackColor = Color.Yellow,
|
||||
// ForeColor = Color.Black
|
||||
//};
|
||||
//btnDiagnose.Click += (s, e) => DiagnoseCameras();
|
||||
//this.Controls.Add(btnDiagnose);
|
||||
|
||||
// 创建标题
|
||||
Label titleLabel = new Label
|
||||
{
|
||||
@@ -126,39 +123,19 @@ namespace BasicDemo
|
||||
};
|
||||
this.Controls.Add(titleLabel);
|
||||
|
||||
// 创建状态栏
|
||||
Panel statusBar = new Panel
|
||||
{
|
||||
BackColor = Color.FromArgb(0, 112, 192),
|
||||
Size = new Size(1900, 30),
|
||||
Location = new Point(0, 970)
|
||||
};
|
||||
this.Controls.Add(statusBar);
|
||||
|
||||
Label statusText = new Label
|
||||
{
|
||||
Text = "系统状态:正在初始化...",
|
||||
Font = new Font("Microsoft YaHei", 10),
|
||||
ForeColor = Color.White,
|
||||
Size = new Size(300, 25),
|
||||
Location = new Point(10, 2),
|
||||
TextAlign = ContentAlignment.MiddleLeft
|
||||
};
|
||||
statusBar.Controls.Add(statusText);
|
||||
|
||||
// 创建三个相机显示区域
|
||||
// 创建三个相机显示区域 - 减小高度,为状态栏留出空间
|
||||
int pictureWidth = 620;
|
||||
int pictureHeight = 850;
|
||||
int pictureHeight = 780; // 从850减小到780
|
||||
int margin = 20;
|
||||
|
||||
for (int i = 0; i < MAX_CAMERAS; i++)
|
||||
{
|
||||
// 创建容器面板
|
||||
// 创建容器面板 - 减小高度
|
||||
Panel cameraPanel = new Panel
|
||||
{
|
||||
BackColor = Color.White,
|
||||
BorderStyle = BorderStyle.FixedSingle,
|
||||
Size = new Size(pictureWidth, 900),
|
||||
Size = new Size(pictureWidth, 830), // 从900减小到830
|
||||
Location = new Point(margin + i * (pictureWidth + margin), 80)
|
||||
};
|
||||
this.Controls.Add(cameraPanel);
|
||||
@@ -182,16 +159,28 @@ namespace BasicDemo
|
||||
Font = new Font("Microsoft YaHei", 10),
|
||||
ForeColor = Color.Red,
|
||||
Size = new Size(200, 25),
|
||||
Location = new Point(10, 45),
|
||||
Location = new Point(10, 45), // Y坐标:45
|
||||
TextAlign = ContentAlignment.MiddleLeft
|
||||
};
|
||||
cameraPanel.Controls.Add(statusLabels[i]);
|
||||
|
||||
// 图像显示区域
|
||||
// 录制状态标签 - 调整位置
|
||||
recordingStatusLabels[i] = new Label
|
||||
{
|
||||
Text = "录制状态:等待开始",
|
||||
Font = new Font("Microsoft YaHei", 9),
|
||||
ForeColor = Color.Gray,
|
||||
Size = new Size(580, 20), // 加宽以适应长文本
|
||||
Location = new Point(10, 70), // Y坐标:70(在状态标签下方)
|
||||
TextAlign = ContentAlignment.MiddleLeft
|
||||
};
|
||||
cameraPanel.Controls.Add(recordingStatusLabels[i]);
|
||||
|
||||
// 图像显示区域 - 调整位置
|
||||
pictureBoxes[i] = new PictureBox
|
||||
{
|
||||
Size = new Size(pictureWidth - 40, pictureHeight),
|
||||
Location = new Point(20, 80),
|
||||
Location = new Point(20, 95), // Y坐标:95(在录制状态标签下方)
|
||||
BackColor = Color.Black,
|
||||
SizeMode = PictureBoxSizeMode.StretchImage,
|
||||
BorderStyle = BorderStyle.FixedSingle
|
||||
@@ -199,10 +188,29 @@ namespace BasicDemo
|
||||
cameraPanel.Controls.Add(pictureBoxes[i]);
|
||||
}
|
||||
|
||||
// 创建状态栏 - 保持在底部
|
||||
Panel statusBar = new Panel
|
||||
{
|
||||
BackColor = Color.FromArgb(0, 112, 192),
|
||||
Size = new Size(1900, 30),
|
||||
Location = new Point(0, 970)
|
||||
};
|
||||
this.Controls.Add(statusBar);
|
||||
|
||||
Label statusText = new Label
|
||||
{
|
||||
Text = "系统状态:正在初始化...",
|
||||
Font = new Font("Microsoft YaHei", 10),
|
||||
ForeColor = Color.White,
|
||||
Size = new Size(300, 25),
|
||||
Location = new Point(10, 2),
|
||||
TextAlign = ContentAlignment.MiddleLeft
|
||||
};
|
||||
statusBar.Controls.Add(statusText);
|
||||
|
||||
// 添加返回按钮
|
||||
AddBackButton();
|
||||
}
|
||||
|
||||
|
||||
private void AddBackButton()
|
||||
{
|
||||
Button btnBack = new Button
|
||||
@@ -228,29 +236,23 @@ namespace BasicDemo
|
||||
this.Controls.Add(btnBack);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 初始化相机系统
|
||||
/// </summary>
|
||||
private void InitializeCameras()
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateSystemStatus("正在初始化SDK...");
|
||||
|
||||
// 等待窗体完全加载
|
||||
Application.DoEvents();
|
||||
Thread.Sleep(500);
|
||||
// 在UI线程更新状态
|
||||
this.Invoke(new Action(() => UpdateSystemStatus("正在初始化SDK...")));
|
||||
|
||||
// 1. 初始化SDK
|
||||
int ret = MyCamera.MV_CC_Initialize_NET();
|
||||
if (ret != MyCamera.MV_OK)
|
||||
{
|
||||
MessageBox.Show($"SDK初始化失败,错误代码: {ret}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
this.Invoke(new Action(() =>
|
||||
MessageBox.Show($"SDK初始化失败,错误代码: {ret}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error)));
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateSystemStatus("正在枚举设备...");
|
||||
this.Invoke(new Action(() => UpdateSystemStatus("正在枚举设备...")));
|
||||
|
||||
// 2. 枚举设备
|
||||
ret = MyCamera.MV_CC_EnumDevices_NET(
|
||||
@@ -259,40 +261,53 @@ namespace BasicDemo
|
||||
|
||||
if (ret != MyCamera.MV_OK)
|
||||
{
|
||||
MessageBox.Show($"设备枚举失败,错误代码: {ret}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
this.Invoke(new Action(() =>
|
||||
MessageBox.Show($"设备枚举失败,错误代码: {ret}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error)));
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 检查设备数量
|
||||
if (deviceList.nDeviceNum < MAX_CAMERAS)
|
||||
{
|
||||
MessageBox.Show($"检测到 {deviceList.nDeviceNum} 个设备,需要至少 {MAX_CAMERAS} 个相机",
|
||||
"错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
this.Invoke(new Action(() =>
|
||||
MessageBox.Show($"检测到 {deviceList.nDeviceNum} 个设备,需要至少 {MAX_CAMERAS} 个相机",
|
||||
"错误", MessageBoxButtons.OK, MessageBoxIcon.Error)));
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateSystemStatus($"检测到 {deviceList.nDeviceNum} 个设备,准备初始化前 {MAX_CAMERAS} 个相机...");
|
||||
this.Invoke(new Action(() => UpdateSystemStatus($"检测到 {deviceList.nDeviceNum} 个设备,准备初始化相机...")));
|
||||
|
||||
// 4. 并行初始化相机(加快速度)
|
||||
List<Task> initTasks = new List<Task>();
|
||||
|
||||
// 4. 逐个初始化相机 - 增加间隔时间
|
||||
for (int i = 0; i < MAX_CAMERAS; i++)
|
||||
{
|
||||
if (InitializeSingleCamera(i))
|
||||
int index = i; // 创建本地变量
|
||||
initTasks.Add(Task.Run(() =>
|
||||
{
|
||||
UpdateCameraStatus(i, "设备已就绪", Color.Blue);
|
||||
}
|
||||
Thread.Sleep(1000); // 增加间隔,避免同时打开冲突
|
||||
if (InitializeSingleCamera(index))
|
||||
{
|
||||
this.Invoke(new Action(() =>
|
||||
UpdateCameraStatus(index, "设备已就绪", Color.Blue)));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
_camerasInitialized = true;
|
||||
UpdateSystemStatus("所有相机已初始化就绪,等待测试开始...");
|
||||
// 等待所有相机初始化完成
|
||||
Task.WaitAll(initTasks.ToArray());
|
||||
|
||||
// 5. 启动状态检查线程 - 延迟启动
|
||||
Thread.Sleep(1000);
|
||||
_camerasInitialized = true;
|
||||
|
||||
this.Invoke(new Action(() =>
|
||||
UpdateSystemStatus("所有相机已初始化就绪,等待测试开始...")));
|
||||
|
||||
// 5. 启动状态检查线程
|
||||
StartStatusCheckThread();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"初始化系统时发生异常: {ex.Message}", "异常", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
this.Invoke(new Action(() =>
|
||||
MessageBox.Show($"初始化系统时发生异常: {ex.Message}", "异常", MessageBoxButtons.OK, MessageBoxIcon.Error)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,14 +413,15 @@ namespace BasicDemo
|
||||
UpdateSystemStatus("所有相机已停止采集");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化单个相机
|
||||
/// </summary>
|
||||
private bool InitializeSingleCamera(int cameraIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateCameraStatus(cameraIndex, "正在打开...", Color.Orange);
|
||||
// 延迟启动,避免同时打开冲突
|
||||
Thread.Sleep(cameraIndex * 200);
|
||||
|
||||
this.Invoke(new Action(() =>
|
||||
UpdateCameraStatus(cameraIndex, "正在打开...", Color.Orange)));
|
||||
|
||||
// 1. 创建设备信息
|
||||
MyCamera.MV_CC_DEVICE_INFO device =
|
||||
@@ -419,30 +435,22 @@ namespace BasicDemo
|
||||
int ret = cameras[cameraIndex].MV_CC_CreateDevice_NET(ref device);
|
||||
if (ret != MyCamera.MV_OK)
|
||||
{
|
||||
UpdateCameraStatus(cameraIndex, $"创建设备失败: {ret}", Color.Red);
|
||||
this.Invoke(new Action(() =>
|
||||
UpdateCameraStatus(cameraIndex, $"创建设备失败: {ret}", Color.Red)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 打开设备
|
||||
// 4. 打开设备(设置超时时间)
|
||||
ret = cameras[cameraIndex].MV_CC_OpenDevice_NET();
|
||||
if (ret != MyCamera.MV_OK)
|
||||
{
|
||||
cameras[cameraIndex].MV_CC_DestroyDevice_NET();
|
||||
UpdateCameraStatus(cameraIndex, $"打开设备失败: {ret}", Color.Red);
|
||||
this.Invoke(new Action(() =>
|
||||
UpdateCameraStatus(cameraIndex, $"打开设备失败: {ret}", Color.Red)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 如果是千兆网相机,设置最优包大小
|
||||
if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE)
|
||||
{
|
||||
int packetSize = cameras[cameraIndex].MV_CC_GetOptimalPacketSize_NET();
|
||||
if (packetSize > 0)
|
||||
{
|
||||
cameras[cameraIndex].MV_CC_SetIntValueEx_NET("GevSCPSPacketSize", packetSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 设置采集模式
|
||||
// 5. 简化设置,只设置必要的参数
|
||||
cameras[cameraIndex].MV_CC_SetEnumValue_NET("AcquisitionMode",
|
||||
(uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS);
|
||||
cameras[cameraIndex].MV_CC_SetEnumValue_NET("TriggerMode",
|
||||
@@ -452,7 +460,8 @@ namespace BasicDemo
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateCameraStatus(cameraIndex, $"异常: {ex.Message}", Color.Red);
|
||||
this.Invoke(new Action(() =>
|
||||
UpdateCameraStatus(cameraIndex, $"异常: {ex.Message}", Color.Red)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -478,25 +487,25 @@ namespace BasicDemo
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 初始化录制系统
|
||||
InitializeRecording(cameraIndex);
|
||||
|
||||
|
||||
|
||||
// 2. 设置采集标志
|
||||
// 3. 设置采集标志
|
||||
grabbingFlags[cameraIndex] = true;
|
||||
|
||||
// 3. 创建采集线程
|
||||
// 4. 创建采集线程
|
||||
int index = cameraIndex;
|
||||
grabThreads[cameraIndex] = new Thread(() => GrabThreadProc(index));
|
||||
grabThreads[cameraIndex].Name = $"CameraGrabThread_{cameraIndex}";
|
||||
grabThreads[cameraIndex].IsBackground = true;
|
||||
|
||||
// 启动线程
|
||||
// 5. 启动保存线程(如果使用队列方式)
|
||||
StartSaveThread(cameraIndex);
|
||||
|
||||
// 启动采集线程
|
||||
grabThreads[cameraIndex].Start();
|
||||
|
||||
// 等待线程启动
|
||||
Thread.Sleep(100);
|
||||
|
||||
// 4. 开始采集
|
||||
// 6. 开始采集
|
||||
int ret = cameras[cameraIndex].MV_CC_StartGrabbing_NET();
|
||||
if (ret != MyCamera.MV_OK)
|
||||
{
|
||||
@@ -505,8 +514,7 @@ namespace BasicDemo
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
UpdateCameraStatus(cameraIndex, "正在采集...", Color.Green);
|
||||
UpdateCameraStatus(cameraIndex, "正在采集和录制...", Color.Green);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -515,6 +523,147 @@ namespace BasicDemo
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeRecording(int cameraIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保保存目录存在
|
||||
Directory.CreateDirectory(_videoSavePath);
|
||||
|
||||
// 创建以时间戳命名的子目录 - 添加相机编号前缀
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
string cameraFolder = Path.Combine(_videoSavePath, $"Camera{cameraIndex + 1}_{timestamp}");
|
||||
recordingPaths[cameraIndex] = cameraFolder;
|
||||
|
||||
// 创建frames子目录
|
||||
string framesFolder = Path.Combine(cameraFolder, "frames");
|
||||
Directory.CreateDirectory(framesFolder);
|
||||
|
||||
// 初始化图片路径列表
|
||||
imagePaths[cameraIndex] = new List<string>();
|
||||
frameCounters[cameraIndex] = 0; // 重置帧计数器
|
||||
|
||||
recordingFlags[cameraIndex] = true;
|
||||
|
||||
// 更新录制状态
|
||||
UpdateRecordingStatus(cameraIndex, $"正在录制到: Camera{cameraIndex + 1}_{timestamp}", Color.Blue);
|
||||
Console.WriteLine($"相机{cameraIndex + 1}录制已初始化,保存路径: {cameraFolder}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"初始化录制失败(相机{cameraIndex + 1}): {ex.Message}");
|
||||
recordingFlags[cameraIndex] = false;
|
||||
UpdateRecordingStatus(cameraIndex, "录制初始化失败", Color.Red);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新录制状态显示
|
||||
/// </summary>
|
||||
private void UpdateRecordingStatus(int cameraIndex, string message, Color color)
|
||||
{
|
||||
if (recordingStatusLabels[cameraIndex].InvokeRequired)
|
||||
{
|
||||
recordingStatusLabels[cameraIndex].Invoke(new Action(() =>
|
||||
{
|
||||
recordingStatusLabels[cameraIndex].Text = $"录制状态:{message}";
|
||||
recordingStatusLabels[cameraIndex].ForeColor = color;
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
recordingStatusLabels[cameraIndex].Text = $"录制状态:{message}";
|
||||
recordingStatusLabels[cameraIndex].ForeColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动保存线程(使用队列方式保存图片)
|
||||
/// </summary>
|
||||
private void StartSaveThread(int cameraIndex)
|
||||
{
|
||||
saveThreadRunning[cameraIndex] = true;
|
||||
saveThreads[cameraIndex] = new Thread(() => SaveImageThreadProc(cameraIndex));
|
||||
saveThreads[cameraIndex].Name = $"SaveThread_{cameraIndex}";
|
||||
saveThreads[cameraIndex].IsBackground = true;
|
||||
saveThreads[cameraIndex].Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存图片线程处理
|
||||
/// </summary>
|
||||
private void SaveImageThreadProc(int cameraIndex)
|
||||
{
|
||||
while (saveThreadRunning[cameraIndex] || !frameQueues[cameraIndex].IsEmpty)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (frameQueues[cameraIndex].TryDequeue(out Bitmap frame))
|
||||
{
|
||||
SaveFrameAsImage(cameraIndex, frame);
|
||||
frame.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Sleep(10); // 队列为空时休眠
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"保存图片线程异常(相机{cameraIndex + 1}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveFrameAsImage(int cameraIndex, Bitmap frame)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!recordingFlags[cameraIndex] || string.IsNullOrEmpty(recordingPaths[cameraIndex]))
|
||||
return;
|
||||
|
||||
int currentFrame = frameCounters[cameraIndex]++;
|
||||
string framesFolder = Path.Combine(recordingPaths[cameraIndex], "frames");
|
||||
|
||||
// 添加相机编号到文件名,方便识别
|
||||
string imagePath = Path.Combine(framesFolder, $"Cam{cameraIndex + 1}_frame_{currentFrame:D6}.jpg");
|
||||
|
||||
// 使用高质量压缩保存图片
|
||||
ImageCodecInfo jpgEncoder = GetEncoderInfo("image/jpeg");
|
||||
EncoderParameters encoderParams = new EncoderParameters(1);
|
||||
encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 85L); // 85%质量
|
||||
|
||||
lock (locks[cameraIndex])
|
||||
{
|
||||
frame.Save(imagePath, jpgEncoder, encoderParams);
|
||||
}
|
||||
|
||||
imagePaths[cameraIndex].Add(imagePath);
|
||||
|
||||
// 每50帧更新一次录制状态
|
||||
if (currentFrame % 50 == 0)
|
||||
{
|
||||
UpdateRecordingStatus(cameraIndex, $"已录制 {currentFrame} 帧", Color.Green);
|
||||
Console.WriteLine($"相机{cameraIndex + 1}已保存 {currentFrame} 帧");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"保存图片失败(相机{cameraIndex + 1}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图像编码器
|
||||
private ImageCodecInfo GetEncoderInfo(string mimeType)
|
||||
{
|
||||
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
|
||||
foreach (ImageCodecInfo codec in codecs)
|
||||
{
|
||||
if (codec.MimeType == mimeType)
|
||||
return codec;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -532,9 +681,7 @@ namespace BasicDemo
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 停止单个相机采集
|
||||
/// </summary>
|
||||
|
||||
private void StopSingleCameraCapture(int cameraIndex)
|
||||
{
|
||||
try
|
||||
@@ -554,15 +701,41 @@ namespace BasicDemo
|
||||
// 3. 停止相机采集
|
||||
cameras[cameraIndex].MV_CC_StopGrabbing_NET();
|
||||
|
||||
|
||||
// 4. 清理资源
|
||||
CleanupCameraResources(cameraIndex);
|
||||
|
||||
UpdateCameraStatus(cameraIndex, "已停止采集", Color.Blue);
|
||||
|
||||
// 5. 创建视频文件
|
||||
if (imagePaths[cameraIndex] != null && imagePaths[cameraIndex].Count > 0)
|
||||
{
|
||||
// 立即创建视频文件
|
||||
string videoPath = CreateVideoFromImages(cameraIndex);
|
||||
|
||||
if (!string.IsNullOrEmpty(videoPath))
|
||||
{
|
||||
UpdateRecordingStatus(cameraIndex, $"视频已保存: {Path.GetFileName(videoPath)}", Color.Green);
|
||||
|
||||
// 显示视频文件位置
|
||||
MessageBox.Show($"相机{cameraIndex + 1}视频已保存到:\n{videoPath}\n\n共录制 {imagePaths[cameraIndex].Count} 帧",
|
||||
"录制完成",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"相机{cameraIndex + 1}没有保存任何帧,跳过视频转换");
|
||||
UpdateRecordingStatus(cameraIndex, "没有录制到帧", Color.Orange);
|
||||
}
|
||||
|
||||
// 6. 停止录制标志
|
||||
recordingFlags[cameraIndex] = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateCameraStatus(cameraIndex, $"停止采集异常: {ex.Message}", Color.Red);
|
||||
UpdateRecordingStatus(cameraIndex, "录制出错", Color.Red);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,100 +830,143 @@ namespace BasicDemo
|
||||
}
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 采集线程处理函数 - 简化版本
|
||||
///// </summary>
|
||||
//private void GrabThreadProc(int cameraIndex)
|
||||
//{
|
||||
// MyCamera camera = cameras[cameraIndex];
|
||||
// MyCamera.MV_FRAME_OUT frameOut = new MyCamera.MV_FRAME_OUT();
|
||||
// MyCamera.MV_PIXEL_CONVERT_PARAM convertParam = new MyCamera.MV_PIXEL_CONVERT_PARAM();
|
||||
// MyCamera.MV_DISPLAY_FRAME_INFO displayInfo = new MyCamera.MV_DISPLAY_FRAME_INFO();
|
||||
/// <summary>
|
||||
/// 创建视频文件并返回视频路径
|
||||
/// </summary>
|
||||
private string CreateVideoFromImages(int cameraIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
string framesFolder = Path.Combine(recordingPaths[cameraIndex], "frames");
|
||||
|
||||
// // 帧率控制变量(新增)
|
||||
// int framesWritten = 0;
|
||||
// DateTime lastWriteTime = DateTime.Now;
|
||||
// while (grabbingFlags[cameraIndex])
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// // 获取图像缓冲区
|
||||
// int ret = camera.MV_CC_GetImageBuffer_NET(ref frameOut, 1000);
|
||||
// if (ret == MyCamera.MV_OK)
|
||||
// {
|
||||
// lock (locks[cameraIndex])
|
||||
// {
|
||||
// // 更新帧信息
|
||||
// frameInfos[cameraIndex] = frameOut.stFrameInfo;
|
||||
// 检查文件夹是否存在
|
||||
if (!Directory.Exists(framesFolder))
|
||||
{
|
||||
Console.WriteLine($"相机{cameraIndex + 1}的frames文件夹不存在: {framesFolder}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// // 像素格式转换
|
||||
// convertParam.nWidth = frameOut.stFrameInfo.nWidth;
|
||||
// convertParam.nHeight = frameOut.stFrameInfo.nHeight;
|
||||
// convertParam.enSrcPixelType = frameOut.stFrameInfo.enPixelType;
|
||||
// convertParam.pSrcData = frameOut.pBufAddr;
|
||||
// convertParam.nSrcDataLen = frameOut.stFrameInfo.nFrameLen;
|
||||
// convertParam.pDstBuffer = displayBufs[cameraIndex];
|
||||
// convertParam.nDstBufferSize = displayBufSizes[cameraIndex];
|
||||
// 获取所有jpg文件
|
||||
string[] imageFiles = Directory.GetFiles(framesFolder, $"Cam{cameraIndex + 1}_*.jpg");
|
||||
if (imageFiles.Length == 0)
|
||||
{
|
||||
// 尝试查找任何jpg文件(兼容之前的命名方式)
|
||||
imageFiles = Directory.GetFiles(framesFolder, "*.jpg");
|
||||
if (imageFiles.Length == 0)
|
||||
{
|
||||
Console.WriteLine($"相机{cameraIndex + 1}没有找到jpg图片文件");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// if (displayBitmaps[cameraIndex].PixelFormat == PixelFormat.Format8bppIndexed)
|
||||
// {
|
||||
// convertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// convertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_BGR8_Packed;
|
||||
// }
|
||||
Console.WriteLine($"相机{cameraIndex + 1}找到 {imageFiles.Length} 张图片");
|
||||
|
||||
// camera.MV_CC_ConvertPixelType_NET(ref convertParam);
|
||||
// 创建视频文件路径
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
string videoPath = Path.Combine(recordingPaths[cameraIndex], $"Camera{cameraIndex + 1}_{timestamp}.mp4");
|
||||
|
||||
// // 简单方案:直接更新现有的Bitmap,不创建新实例
|
||||
// BitmapData bitmapData = displayBitmaps[cameraIndex].LockBits(
|
||||
// new Rectangle(0, 0, (int)frameOut.stFrameInfo.nWidth, (int)frameOut.stFrameInfo.nHeight),
|
||||
// ImageLockMode.ReadWrite, displayBitmaps[cameraIndex].PixelFormat);
|
||||
// 创建转换批处理文件
|
||||
string batFilePath = CreateConversionBatchFile(cameraIndex, framesFolder, videoPath, imageFiles.Length);
|
||||
|
||||
// CopyMemory(bitmapData.Scan0, convertParam.pDstBuffer,
|
||||
// (uint)(bitmapData.Stride * displayBitmaps[cameraIndex].Height));
|
||||
if (File.Exists(batFilePath))
|
||||
{
|
||||
// 自动运行转换
|
||||
var process = System.Diagnostics.Process.Start(batFilePath);
|
||||
|
||||
// displayBitmaps[cameraIndex].UnlockBits(bitmapData);
|
||||
// }
|
||||
// 等待转换完成(最大等待30秒)
|
||||
if (process != null)
|
||||
{
|
||||
process.WaitForExit(30000);
|
||||
|
||||
// // 使用SDK显示 - 这是最稳定的方式
|
||||
// displayInfo.hWnd = pictureBoxes[cameraIndex].Handle;
|
||||
// displayInfo.pData = frameOut.pBufAddr;
|
||||
// displayInfo.nDataLen = frameOut.stFrameInfo.nFrameLen;
|
||||
// displayInfo.nWidth = frameOut.stFrameInfo.nWidth;
|
||||
// displayInfo.nHeight = frameOut.stFrameInfo.nHeight;
|
||||
// displayInfo.enPixelType = frameOut.stFrameInfo.enPixelType;
|
||||
// 检查视频文件是否生成
|
||||
if (File.Exists(videoPath))
|
||||
{
|
||||
FileInfo videoInfo = new FileInfo(videoPath);
|
||||
Console.WriteLine($"视频文件已创建: {videoPath} ({videoInfo.Length / 1024 / 1024} MB)");
|
||||
|
||||
// // 在UI线程中调用显示
|
||||
// if (this.InvokeRequired)
|
||||
// {
|
||||
// this.Invoke(new Action(() =>
|
||||
// {
|
||||
// camera.MV_CC_DisplayOneFrame_NET(ref displayInfo);
|
||||
// }));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// camera.MV_CC_DisplayOneFrame_NET(ref displayInfo);
|
||||
// }
|
||||
// 创建视频文件的快捷方式到根目录,方便查找
|
||||
CreateVideoShortcut(cameraIndex, videoPath);
|
||||
|
||||
// // 释放图像缓冲区
|
||||
// camera.MV_CC_FreeImageBuffer_NET(ref frameOut);
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"相机{cameraIndex + 1}采集异常: {ex.Message}");
|
||||
// // 继续循环,不退出
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
return videoPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"创建视频失败(相机{cameraIndex + 1}): {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 采集线程处理函数 - 添加视频录制(修改)
|
||||
/// 在录制根目录创建视频文件的快捷方式(文本文件)
|
||||
/// </summary>
|
||||
private void CreateVideoShortcut(int cameraIndex, string videoPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
string shortcutPath = Path.Combine(_videoSavePath, $"Camera{cameraIndex + 1}_最新视频.txt");
|
||||
|
||||
using (StreamWriter sw = new StreamWriter(shortcutPath, false, Encoding.Default))
|
||||
{
|
||||
sw.WriteLine("相机视频文件位置信息");
|
||||
sw.WriteLine("=========================");
|
||||
sw.WriteLine($"相机编号: {cameraIndex + 1}");
|
||||
sw.WriteLine($"视频文件: {Path.GetFileName(videoPath)}");
|
||||
sw.WriteLine($"文件大小: {new FileInfo(videoPath).Length / 1024 / 1024} MB");
|
||||
sw.WriteLine($"录制时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
sw.WriteLine($"视频路径: {videoPath}");
|
||||
sw.WriteLine();
|
||||
sw.WriteLine("图片文件夹:");
|
||||
sw.WriteLine($" {Path.Combine(recordingPaths[cameraIndex], "frames")}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"已创建视频位置信息文件: {shortcutPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"创建快捷方式失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 创建转换批处理文件
|
||||
/// </summary>
|
||||
private string CreateConversionBatchFile(int cameraIndex, string framesFolder, string videoPath, int imageCount)
|
||||
{
|
||||
try
|
||||
{
|
||||
string batFilePath = Path.Combine(recordingPaths[cameraIndex], $"Convert_Camera{cameraIndex + 1}.bat");
|
||||
|
||||
using (StreamWriter sw = new StreamWriter(batFilePath, false, Encoding.Default))
|
||||
{
|
||||
sw.WriteLine("@echo off");
|
||||
sw.WriteLine($"echo 正在转换相机{cameraIndex + 1}的{imageCount}张图片为视频...");
|
||||
sw.WriteLine($"set INPUT_PATTERN={framesFolder}\\Cam{cameraIndex + 1}_*.jpg");
|
||||
sw.WriteLine($"set OUTPUT_FILE={videoPath}");
|
||||
sw.WriteLine($"set FRAMERATE={_frameRate}");
|
||||
sw.WriteLine();
|
||||
sw.WriteLine("echo 正在转换,请稍候...");
|
||||
sw.WriteLine("ffmpeg -framerate %FRAMERATE% -pattern_type glob -i \"%INPUT_PATTERN%\" -c:v libx264 -pix_fmt yuv420p \"%OUTPUT_FILE%\"");
|
||||
sw.WriteLine("if %ERRORLEVEL% EQU 0 (");
|
||||
sw.WriteLine(" echo 转换成功!");
|
||||
sw.WriteLine(" echo 视频文件: %OUTPUT_FILE%");
|
||||
sw.WriteLine(") else (");
|
||||
sw.WriteLine(" echo 转换失败");
|
||||
sw.WriteLine(")");
|
||||
}
|
||||
|
||||
Console.WriteLine($"已创建转换批处理文件: {batFilePath}");
|
||||
return batFilePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"创建批处理文件失败: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private void GrabThreadProc(int cameraIndex)
|
||||
{
|
||||
MyCamera camera = cameras[cameraIndex];
|
||||
@@ -758,9 +974,9 @@ namespace BasicDemo
|
||||
MyCamera.MV_PIXEL_CONVERT_PARAM convertParam = new MyCamera.MV_PIXEL_CONVERT_PARAM();
|
||||
MyCamera.MV_DISPLAY_FRAME_INFO displayInfo = new MyCamera.MV_DISPLAY_FRAME_INFO();
|
||||
|
||||
// 帧率控制变量(新增)
|
||||
int framesWritten = 0;
|
||||
DateTime lastWriteTime = DateTime.Now;
|
||||
// 帧率控制变量
|
||||
DateTime lastFrameTime = DateTime.Now;
|
||||
TimeSpan frameInterval = TimeSpan.FromMilliseconds(1000.0 / _frameRate);
|
||||
|
||||
while (grabbingFlags[cameraIndex])
|
||||
{
|
||||
@@ -770,6 +986,18 @@ namespace BasicDemo
|
||||
int ret = camera.MV_CC_GetImageBuffer_NET(ref frameOut, 1000);
|
||||
if (ret == MyCamera.MV_OK)
|
||||
{
|
||||
// 控制帧率
|
||||
DateTime currentTime = DateTime.Now;
|
||||
if (currentTime - lastFrameTime < frameInterval)
|
||||
{
|
||||
// 帧率过快,跳过此帧
|
||||
camera.MV_CC_FreeImageBuffer_NET(ref frameOut);
|
||||
continue;
|
||||
}
|
||||
lastFrameTime = currentTime;
|
||||
|
||||
Bitmap frameCopy = null;
|
||||
|
||||
lock (locks[cameraIndex])
|
||||
{
|
||||
// 更新帧信息
|
||||
@@ -804,9 +1032,14 @@ namespace BasicDemo
|
||||
(uint)(bitmapData.Stride * displayBitmaps[cameraIndex].Height));
|
||||
|
||||
displayBitmaps[cameraIndex].UnlockBits(bitmapData);
|
||||
|
||||
// 创建帧的副本用于保存(避免共享资源)
|
||||
if (recordingFlags[cameraIndex])
|
||||
{
|
||||
frameCopy = new Bitmap(displayBitmaps[cameraIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 使用SDK显示
|
||||
displayInfo.hWnd = pictureBoxes[cameraIndex].Handle;
|
||||
displayInfo.pData = frameOut.pBufAddr;
|
||||
@@ -828,6 +1061,12 @@ namespace BasicDemo
|
||||
camera.MV_CC_DisplayOneFrame_NET(ref displayInfo);
|
||||
}
|
||||
|
||||
// 将帧添加到队列用于保存
|
||||
if (recordingFlags[cameraIndex] && frameCopy != null)
|
||||
{
|
||||
frameQueues[cameraIndex].Enqueue(frameCopy);
|
||||
}
|
||||
|
||||
// 释放图像缓冲区
|
||||
camera.MV_CC_FreeImageBuffer_NET(ref frameOut);
|
||||
}
|
||||
@@ -835,13 +1074,12 @@ namespace BasicDemo
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"相机{cameraIndex + 1}采集异常: {ex.Message}");
|
||||
// 继续循环,不退出
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理相机资源(修改)
|
||||
/// 清理相机资源
|
||||
/// </summary>
|
||||
private void CleanupCameraResources(int cameraIndex)
|
||||
{
|
||||
@@ -861,7 +1099,11 @@ namespace BasicDemo
|
||||
|
||||
displayBufSizes[cameraIndex] = 0;
|
||||
|
||||
|
||||
// 清空队列
|
||||
while (frameQueues[cameraIndex].TryDequeue(out Bitmap frame))
|
||||
{
|
||||
frame.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -893,9 +1135,6 @@ namespace BasicDemo
|
||||
Console.WriteLine($"系统状态: {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗体关闭时的清理工作
|
||||
/// </summary>
|
||||
private void AutoCameraForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -906,16 +1145,23 @@ namespace BasicDemo
|
||||
_statusCheckThread.Join(1000);
|
||||
}
|
||||
|
||||
// 停止所有采集线程
|
||||
// 停止所有采集和录制
|
||||
for (int i = 0; i < MAX_CAMERAS; i++)
|
||||
{
|
||||
grabbingFlags[i] = false;
|
||||
recordingFlags[i] = false;
|
||||
saveThreadRunning[i] = false;
|
||||
|
||||
if (grabThreads[i] != null && grabThreads[i].IsAlive)
|
||||
{
|
||||
grabThreads[i].Join(1000);
|
||||
}
|
||||
|
||||
if (saveThreads[i] != null && saveThreads[i].IsAlive)
|
||||
{
|
||||
saveThreads[i].Join(2000);
|
||||
}
|
||||
|
||||
if (cameras[i] != null)
|
||||
{
|
||||
cameras[i].MV_CC_StopGrabbing_NET();
|
||||
@@ -926,8 +1172,6 @@ namespace BasicDemo
|
||||
CleanupCameraResources(i);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 反初始化SDK
|
||||
MyCamera.MV_CC_Finalize_NET();
|
||||
}
|
||||
@@ -937,6 +1181,5 @@ namespace BasicDemo
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,9 @@
|
||||
<HintPath>..\packages\Accord.Video.FFMPEG.3.7.0\lib\net462\Accord.Video.FFMPEG.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="MediaToolkit, Version=1.1.0.1, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MediaToolkit.1.1.0.1\lib\net40\MediaToolkit.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
<package id="Accord" version="3.8.0" targetFramework="net35" requireReinstallation="true" />
|
||||
<package id="Accord.Video" version="3.8.0" targetFramework="net35" requireReinstallation="true" />
|
||||
<package id="Accord.Video.FFMPEG" version="3.7.0" targetFramework="net472" />
|
||||
<package id="MediaToolkit" version="1.1.0.1" targetFramework="net472" />
|
||||
</packages>
|
||||
Reference in New Issue
Block a user