Compare commits

2 Commits
master ... Test

Author SHA1 Message Date
wangjialiang
14756339ee Testnew 2025-11-28 15:20:45 +08:00
wangjialiang
ad0fa4b047 Test 2025-11-28 15:13:48 +08:00
3 changed files with 355 additions and 156 deletions

View File

@@ -1,10 +1,7 @@
using Basler.Pylon;
using Microsoft.Win32;
using Microsoft.WindowsAPICodePack.Dialogs;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
@@ -17,6 +14,14 @@ namespace WpfApp1
private ICameraInfo? _selectedCameraInfo;
private bool _isCameraStarted = false;
// 复用同一个转换器和缓冲区,减少 GC 压力
private readonly PixelDataConverter _converter = new PixelDataConverter
{
OutputPixelFormat = PixelType.BGRA8packed
};
private byte[]? _buffer;
public MainWindow()
{
InitializeComponent();
@@ -26,38 +31,136 @@ namespace WpfApp1
ButtonPanel1.IsEnabled = false;
StatusText.Text = "正在初始化相机...请稍后";
Task.Run(() => InitializeCameraAsync());
// 在 Loaded 事件中做异步初始化,避免构造函数里 Task.Run 套娃
Loaded += async (_, __) => await InitializeCameraAsync();
}
/// <summary>
/// 异步初始化:枚举一次相机,默认选第一台
/// </summary>
private async Task InitializeCameraAsync()
{
try
{
var cameras = CameraFinder.Enumerate();
// 相机枚举可能比较耗时,放到后台线程
var cameras = await Task.Run(() => CameraFinder.Enumerate());
if (cameras.Count == 0)
{
Dispatcher.Invoke(() =>
{
ButtonPanel1.IsEnabled = true;
StatusText.Text = "未检测到相机";
});
ButtonPanel1.IsEnabled = true;
StatusText.Text = "未检测到相机";
return;
}
_selectedCameraInfo = cameras[0];
await Task.CompletedTask;
Dispatcher.Invoke(() =>
{
ButtonPanel1.IsEnabled = true;
StatusText.Text = "相机初始化完成,请选择相机并随后‘开启推流’开始取流";
StartCameraButton.Content = "开启推流";
});
ButtonPanel1.IsEnabled = true;
StatusText.Text = "相机初始化完成,请选择相机并随后‘开启推流’开始取流";
StartCameraButton.Content = "开启推流";
}
catch (Exception ex)
{
Dispatcher.Invoke(() => MessageBox.Show("初始化失败: " + ex.Message));
ButtonPanel1.IsEnabled = true;
MessageBox.Show("初始化失败: " + ex.Message);
}
}
/// <summary>
/// 确保当前选择的相机已经打开(不含取流)
/// </summary>
private bool EnsureCameraOpen()
{
if (_camera != null && _camera.IsOpen)
{
return true;
}
if (_selectedCameraInfo == null)
{
StatusText.Text = "请先选择一个相机";
return false;
}
CloseCamera();
try
{
_camera = new Camera(_selectedCameraInfo);
_camera.Open();
_camera.Parameters[PLCamera.AcquisitionMode].SetValue(PLCamera.AcquisitionMode.Continuous);
_isCameraStarted = false;
string serial = _selectedCameraInfo[CameraInfoKey.SerialNumber];
StatusText.Text = $"已选择并打开相机:{serial}(未取流)。点击‘开启推流’开始/停止推流";
StartCameraButton.Content = "开启推流";
return true;
}
catch (Exception ex)
{
StatusText.Text = "打开相机失败:" + ex.Message;
return false;
}
}
/// <summary>
/// 停止取流但不关闭相机
/// </summary>
private void StopGrabbing()
{
if (_camera == null)
{
return;
}
try
{
if (_camera.StreamGrabber.IsGrabbing)
{
_camera.StreamGrabber.Stop();
}
_camera.StreamGrabber.ImageGrabbed -= OnImageGrabbed;
}
catch
{
// 可在此记录日志
}
finally
{
_isCameraStarted = false;
StartCameraButton.Content = "开启推流";
StatusText.Text = "推流已关闭";
}
}
/// <summary>
/// 关闭相机(内部会先停止取流)
/// </summary>
private void CloseCamera()
{
if (_camera == null)
{
return;
}
StopGrabbing();
try
{
if (_camera.IsOpen)
{
_camera.Close();
}
}
catch (Exception ex)
{
StatusText.Text = "关闭相机时出错:" + ex.Message;
}
finally
{
_camera = null;
}
}
@@ -65,54 +168,26 @@ namespace WpfApp1
{
try
{
// 保相机已打开
if ((_camera == null || !_camera.IsOpen) && _selectedCameraInfo != null)
// 保相机已打开
if (!EnsureCameraOpen())
{
if (_camera != null)
{
try
{
_camera.StreamGrabber.Stop();
_camera.StreamGrabber.ImageGrabbed -= OnImageGrabbed;
_camera.Close();
}
catch { }
finally { _camera = null; }
}
_camera = new Camera(_selectedCameraInfo);
_camera.Open();
_camera.Parameters[PLCamera.AcquisitionMode].SetValue(PLCamera.AcquisitionMode.Continuous);
}
if (_camera == null || !_camera.IsOpen)
{
StatusText.Text = "请先选择一个相机";
return;
}
if (!_isCameraStarted)
{
_camera.StreamGrabber.ImageGrabbed += OnImageGrabbed;
_camera.StreamGrabber.Start(GrabStrategy.LatestImages, GrabLoop.ProvidedByStreamGrabber);
_camera.StreamGrabber.Start(
GrabStrategy.LatestImages,
GrabLoop.ProvidedByStreamGrabber);
_isCameraStarted = true;
StatusText.Text = "推流已开启";
StartCameraButton.Content = "关闭推流";
}
else
{
try
{
_camera.StreamGrabber.Stop();
_camera.StreamGrabber.ImageGrabbed -= OnImageGrabbed;
_isCameraStarted = false;
StatusText.Text = "推流已关闭";
StartCameraButton.Content = "开启推流";
}
catch (Exception ex)
{
StatusText.Text = "关闭推流失败:" + ex.Message;
}
StopGrabbing();
}
}
catch (Exception ex)
@@ -121,6 +196,9 @@ namespace WpfApp1
}
}
/// <summary>
/// 连续取流回调:更新预览
/// </summary>
private void OnImageGrabbed(object sender, ImageGrabbedEventArgs e)
{
try
@@ -129,30 +207,48 @@ namespace WpfApp1
{
if (!result.GrabSucceeded) return;
var converter = new PixelDataConverter();
converter.OutputPixelFormat = PixelType.BGRA8packed;
int width = result.Width;
int height = result.Height;
int stride = width * 4;
int bufferSize = stride * height;
byte[] buffer = new byte[result.Width * result.Height * 4];
converter.Convert(buffer, result);
// 每帧自己分配一块 buffer
byte[] localBuffer = new byte[bufferSize];
Dispatcher.Invoke(() =>
_converter.Convert(localBuffer, result);
Dispatcher.BeginInvoke(new Action(() =>
{
CameraImage.Source = BitmapSource.Create(
result.Width, result.Height, 96, 96,
System.Windows.Media.PixelFormats.Bgra32,
null, buffer, result.Width * 4);
});
try
{
CameraImage.Source = BitmapSource.Create(
width,
height,
96,
96,
System.Windows.Media.PixelFormats.Bgra32,
null,
localBuffer,
stride);
}
catch (ArgumentException ex)
{
// 这里可以打个日志看看具体参数,如果还出错
System.Diagnostics.Debug.WriteLine("Create bitmap failed: " + ex);
}
}));
}
}
catch
catch (Exception ex)
{
// 忽略异常
System.Diagnostics.Debug.WriteLine("OnImageGrabbed error: " + ex);
}
}
private void CaptureButton_Click(object sender, RoutedEventArgs e)
{
if (CameraImage.Source == null)
if (CameraImage.Source is not BitmapSource bitmap)
{
StatusText.Text = "没有图像可保存";
return;
@@ -160,10 +256,16 @@ namespace WpfApp1
try
{
string filePath = Path.Combine(_saveFolder,
if (!Directory.Exists(_saveFolder))
{
Directory.CreateDirectory(_saveFolder);
}
string filePath = Path.Combine(
_saveFolder,
$"Capture_{DateTime.Now:yyyyMMdd_HHmmss}.png");
SaveImage(CameraImage.Source as BitmapSource, filePath);
SaveImage(bitmap, filePath);
StatusText.Text = $"图像已保存到:{filePath}";
}
@@ -195,62 +297,35 @@ namespace WpfApp1
}
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
base.OnClosing(e);
if (_camera != null)
{
try
{
_camera.StreamGrabber.Stop();
_camera.StreamGrabber.ImageGrabbed -= OnImageGrabbed;
_camera.Close();
}
catch (Exception ex)
{
StatusText.Text = "关闭相机时出错:" + ex.Message;
}
finally
{
_camera = null;
_isCameraStarted = false;
}
}
}
private void SelectCameraButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new SelectCamera();
dlg.Owner = this;
var dlg = new SelectCamera
{
Owner = this
};
if (dlg.ShowDialog() == true)
{
var selectedInfo = dlg.SelectedCameraInfo;
_selectedCameraInfo = selectedInfo;
_selectedCameraInfo = dlg.SelectedCameraInfo;
// 关闭旧相机,打开新相机,但不取流
try
{
if (_camera != null)
CloseCamera();
if (_selectedCameraInfo != null)
{
try
{
_camera.StreamGrabber.Stop();
_camera.StreamGrabber.ImageGrabbed -= OnImageGrabbed;
_camera.Close();
}
catch { }
finally { _camera = null; _isCameraStarted = false; }
_camera = new Camera(_selectedCameraInfo);
_camera.Open();
_camera.Parameters[PLCamera.AcquisitionMode].SetValue(PLCamera.AcquisitionMode.Continuous);
_isCameraStarted = false;
string serial = _selectedCameraInfo[CameraInfoKey.SerialNumber];
StatusText.Text =
$"已选择并打开相机:{serial}(未取流)。点击‘开启推流’开始/停止推流";
StartCameraButton.Content = "开启推流";
}
_camera = new Camera(_selectedCameraInfo);
_camera.Open();
_camera.Parameters[PLCamera.AcquisitionMode].SetValue(PLCamera.AcquisitionMode.Continuous);
_isCameraStarted = false;
StatusText.Text = $"已选择并打开相机:{selectedInfo[CameraInfoKey.SerialNumber]}(未取流)。点击‘开启推流’开始/停止推流";
StartCameraButton.Content = "开启推流";
}
catch (Exception ex)
{
@@ -266,15 +341,27 @@ namespace WpfApp1
private void TimedCaptureButton_Click(object sender, RoutedEventArgs e)
{
// 定时拍摄只要求相机已打开,无需取流
if (_camera == null || !_camera.IsOpen)
// 定时拍摄只要求相机已打开,无需连续取流
if (!EnsureCameraOpen())
{
MessageBox.Show("请先选择相机(会自动打开但不取流)");
return;
}
var dlg = new TimedCapture(_camera);
dlg.Owner = this;
// 避免和定时抓拍争用同一个 StreamGrabber
StopGrabbing();
var dlg = new TimedCapture(_camera)
{
Owner = this
};
dlg.ShowDialog();
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
base.OnClosing(e);
CloseCamera();
}
}
}

View File

@@ -36,8 +36,11 @@
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="10">
<Button x:Name="TimedCaptureStart" Content="开始拍摄" Width="100" Margin="0,0,20,0" Click="TimedCaptureStart_Click"/>
<Button x:Name="TimedCaptureStop" Content="停止拍摄" Width="100" Click="TimedCaptureStop_Click"/>
<Button x:Name="ToggleCaptureButton"
Content="开始定时拍照"
Width="140"
Padding="10,4"
Click="ToggleCaptureButton_Click"/>
</StackPanel>
</Grid>

View File

@@ -14,23 +14,73 @@ namespace WpfApp1
{
private readonly Camera _camera;
private readonly DispatcherTimer _timer = new DispatcherTimer();
private string _saveFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
private string _saveFolder =
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
private int _intervalMs = 1000;
private bool _isCapturing = false;
// 复用一个转换器,避免每次 new
private readonly PixelDataConverter _converter = new PixelDataConverter
{
OutputPixelFormat = PixelType.BGRA8packed
};
public TimedCapture(Camera camera)
{
InitializeComponent();
_camera = camera;
var name = _camera?.CameraInfo?[CameraInfoKey.FriendlyName];
CameraInfoTextBlock.Text = string.IsNullOrWhiteSpace(name) ? "未知相机" : name;
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
var name = _camera.CameraInfo?[CameraInfoKey.FriendlyName];
CameraInfoTextBlock.Text = string.IsNullOrWhiteSpace(name)
? "未知相机"
: name;
FolderTextBox.Text = _saveFolder;
IntervalTextBox.Text = _intervalMs.ToString();
_timer.Tick += OnTimerTick;
_timer.IsEnabled = false;
UpdateUiState();
}
/// <summary>
/// 根据 _isCapturing 更新按钮文本和状态栏
/// </summary>
private void UpdateUiState(string? extraStatus = null)
{
if (_isCapturing)
{
ToggleCaptureButton.Content = "停止定时拍照";
if (extraStatus == null)
{
StatusTextBlock.Text = "状态:拍摄中";
}
else
{
StatusTextBlock.Text = extraStatus;
}
}
else
{
ToggleCaptureButton.Content = "开始定时拍照";
if (extraStatus == null)
{
StatusTextBlock.Text = "状态:已停止";
}
else
{
StatusTextBlock.Text = extraStatus;
}
}
}
/// <summary>
/// 选择保存路径
/// </summary>
private void BrowseButton_Click(object sender, RoutedEventArgs e)
{
var selected = FolderHelper.SelectFolder("请选择保存目录");
@@ -41,78 +91,136 @@ namespace WpfApp1
}
}
private void TimedCaptureStart_Click(object sender, RoutedEventArgs e)
/// <summary>
/// 单按钮:开始 / 停止 定时拍照
/// </summary>
private void ToggleCaptureButton_Click(object sender, RoutedEventArgs e)
{
if (!_isCapturing)
{
// 当前是“未拍照”状态,点击后尝试开始
if (!StartCapture())
{
// 开始失败,不改变 _isCapturing
return;
}
_isCapturing = true;
UpdateUiState();
}
else
{
// 当前在拍照,点击后停止
StopCapture();
_isCapturing = false;
UpdateUiState();
}
}
/// <summary>
/// 尝试开始定时拍照(参数校验、目录检查等)
/// 返回 true 表示成功启动
/// </summary>
private bool StartCapture()
{
if (_camera == null || !_camera.IsOpen)
{
MessageBox.Show("相机未打开");
return;
return false;
}
// 读取间隔
if (!int.TryParse(IntervalTextBox.Text, out var ms) || ms <= 0)
{
MessageBox.Show("请输入有效的拍摄间隔(毫秒)");
return;
MessageBox.Show("请输入有效的拍摄间隔(毫秒>0");
return false;
}
_intervalMs = ms;
_timer.Interval = TimeSpan.FromMilliseconds(_intervalMs);
// 读取保存路径
if (string.IsNullOrWhiteSpace(FolderTextBox.Text))
{
MessageBox.Show("请选择保存文件夹");
return;
return false;
}
_saveFolder = FolderTextBox.Text.Trim();
if (!Directory.Exists(_saveFolder))
{
try { Directory.CreateDirectory(_saveFolder); } catch (Exception ex) { MessageBox.Show("创建文件夹失败:" + ex.Message); return; }
try
{
Directory.CreateDirectory(_saveFolder);
}
catch (Exception ex)
{
MessageBox.Show("创建文件夹失败:" + ex.Message);
return false;
}
}
StatusTextBlock.Text = "状态:拍摄中";
TimedCaptureStart.IsEnabled = false;
TimedCaptureStop.IsEnabled = true;
_timer.Start();
UpdateUiState("状态:拍摄中");
return true;
}
private void TimedCaptureStop_Click(object sender, RoutedEventArgs e)
/// <summary>
/// 停止定时拍照
/// </summary>
private void StopCapture()
{
_timer.Stop();
StatusTextBlock.Text = "状态:已停止";
TimedCaptureStart.IsEnabled = true;
TimedCaptureStop.IsEnabled = false;
UpdateUiState("状态:已停止");
}
/// <summary>
/// 定时抓拍:每次 Tick 抓一帧并保存
/// (现在 Tick 在 UI 线程执行,如果将来发现卡顿,可以再改成后台线程抓拍)
/// </summary>
private void OnTimerTick(object? sender, EventArgs e)
{
try
{
// 单次抓拍
using (IGrabResult grabResult = _camera.StreamGrabber.GrabOne(_intervalMs))
{
if (grabResult != null && grabResult.GrabSucceeded)
if (grabResult == null || !grabResult.GrabSucceeded)
{
var converter = new PixelDataConverter { OutputPixelFormat = PixelType.BGRA8packed };
byte[] buffer = new byte[grabResult.Width * grabResult.Height * 4];
converter.Convert(buffer, grabResult);
var bitmap = BitmapSource.Create(
grabResult.Width,
grabResult.Height,
96, 96,
System.Windows.Media.PixelFormats.Bgra32,
null,
buffer,
grabResult.Width * 4);
string file = Path.Combine(_saveFolder, $"Timed_{DateTime.Now:yyyyMMdd_HHmmss_fff}.png");
SaveImage(bitmap, file);
StatusTextBlock.Text = "状态:已保存 " + System.IO.Path.GetFileName(file);
UpdateUiState("状态:抓拍失败(无效图像)");
return;
}
int width = grabResult.Width;
int height = grabResult.Height;
int stride = width * 4;
int bufferSize = stride * height;
// 每次使用局部 buffer避免多线程和尺寸问题
byte[] buffer = new byte[bufferSize];
_converter.Convert(buffer, grabResult);
var bitmap = BitmapSource.Create(
width,
height,
96,
96,
System.Windows.Media.PixelFormats.Bgra32,
null,
buffer,
stride);
string filePath = Path.Combine(
_saveFolder,
$"Timed_{DateTime.Now:yyyyMMdd_HHmmss_fff}.png");
SaveImage(bitmap, filePath);
UpdateUiState("状态:已保存 " + System.IO.Path.GetFileName(filePath));
}
}
catch (Exception ex)
{
StatusTextBlock.Text = "状态:抓拍失败 - " + ex.Message;
UpdateUiState("状态:抓拍失败 - " + ex.Message);
}
}
@@ -120,6 +228,7 @@ namespace WpfApp1
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
encoder.Save(stream);
}