586 lines
15 KiB
C#
586 lines
15 KiB
C#
|
using System;
|
|||
|
using System.Drawing;
|
|||
|
using System.Drawing.Imaging;
|
|||
|
using System.IO;
|
|||
|
using System.Reactive;
|
|||
|
using System.Threading.Tasks;
|
|||
|
using Avalonia;
|
|||
|
using Avalonia.Media;
|
|||
|
using Avalonia.Media.Imaging;
|
|||
|
using Avalonia.Threading;
|
|||
|
using DispenserCommon.Aop;
|
|||
|
using DispenserCommon.Events;
|
|||
|
using DispenserCommon.Scheduler;
|
|||
|
using DispenserCore.Service;
|
|||
|
using DispenserHal.Camera.Factory;
|
|||
|
using DispenserHal.Camera.VO;
|
|||
|
using DispenserUI.Utils;
|
|||
|
using DispenserUI.ViewModels.Recorder;
|
|||
|
using DispenserUI.Views.Controls;
|
|||
|
using DispenserUI.Views.Windows;
|
|||
|
using Masuit.Tools;
|
|||
|
using ReactiveUI;
|
|||
|
using Serilog;
|
|||
|
using SharpAvi;
|
|||
|
using Bitmap = System.Drawing.Bitmap;
|
|||
|
using Point = Avalonia.Point;
|
|||
|
|
|||
|
namespace DispenserUI.ViewModels.Components;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 相机查看器
|
|||
|
/// </summary>
|
|||
|
[GlobalTry]
|
|||
|
public class CameraViewerVM : ViewModelBase
|
|||
|
{
|
|||
|
private readonly SystemParamsService _systemParamsService = new();
|
|||
|
|
|||
|
|
|||
|
private const string TaskName = "CameraViewer#UpdateDisplay";
|
|||
|
|
|||
|
private bool _blank = true;
|
|||
|
|
|||
|
private Bitmap _captureImage;
|
|||
|
|
|||
|
private bool _capturingPicture;
|
|||
|
|
|||
|
private bool _capturingVideo;
|
|||
|
|
|||
|
private bool _recordingVideo;
|
|||
|
|
|||
|
private bool _isFullScreen;
|
|||
|
|
|||
|
private bool _showingCrosshair;
|
|||
|
|
|||
|
private bool _showingGrid;
|
|||
|
|
|||
|
private double _acquisitionFps;
|
|||
|
|
|||
|
private int _imageCount;
|
|||
|
|
|||
|
private int _displayImageCount;
|
|||
|
|
|||
|
private string _bandwidth;
|
|||
|
|
|||
|
private string _originSize;
|
|||
|
|
|||
|
private string _pixelCoordinate;
|
|||
|
|
|||
|
private Point _centerPixelPoint = new(2048, 1500);
|
|||
|
|
|||
|
private PixelFormat _pixelFormat;
|
|||
|
|
|||
|
private int _receivedPerSecond;
|
|||
|
|
|||
|
private int _tuningStep = 1;
|
|||
|
|
|||
|
private Stretch _stretch = Stretch.Uniform;
|
|||
|
|
|||
|
private readonly Stretch[] _stretchs =
|
|||
|
[
|
|||
|
Stretch.None, Stretch.Uniform, Stretch.UniformToFill
|
|||
|
];
|
|||
|
|
|||
|
private int _stretchIndex = 1;
|
|||
|
|
|||
|
private string _scaleRatio = "100%";
|
|||
|
|
|||
|
private ImageViewer _imageViewer;
|
|||
|
|
|||
|
private CameraViewerWindow _window;
|
|||
|
|
|||
|
private ScreenRecorder? _recorder;
|
|||
|
|
|||
|
private double _cameraPositionX;
|
|||
|
|
|||
|
private double _cameraPositionY;
|
|||
|
|
|||
|
public CameraViewerVM()
|
|||
|
{
|
|||
|
ToCapturePhoto = ReactiveCommand.CreateFromTask(CapturePhoto);
|
|||
|
ToggleCaptureVideo = ReactiveCommand.Create(CaptureVideo);
|
|||
|
ToToggleRecordingVideo = ReactiveCommand.Create(ToggleRecordingVideo);
|
|||
|
ToggleShowCrosshair = ReactiveCommand.Create(ShowCrosshair);
|
|||
|
ToggleShowGrid = ReactiveCommand.Create(ShowGrid);
|
|||
|
ToChangeStretch = ReactiveCommand.Create(ChangeStretch);
|
|||
|
ToSavePhoto = ReactiveCommand.Create(SavePhoto);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
public bool Blank
|
|||
|
{
|
|||
|
get => _blank;
|
|||
|
private set => this.RaiseAndSetIfChanged(ref _blank, value);
|
|||
|
}
|
|||
|
|
|||
|
public bool CapturingVideo
|
|||
|
{
|
|||
|
get => _capturingVideo;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _capturingVideo, value);
|
|||
|
}
|
|||
|
|
|||
|
public bool CapturingPicture
|
|||
|
{
|
|||
|
get => _capturingPicture;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _capturingPicture, value);
|
|||
|
}
|
|||
|
|
|||
|
public Bitmap CaptureImage
|
|||
|
{
|
|||
|
get => _captureImage;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _captureImage, value);
|
|||
|
}
|
|||
|
|
|||
|
public bool ShowingCrosshair
|
|||
|
{
|
|||
|
get => _showingCrosshair;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _showingCrosshair, value);
|
|||
|
}
|
|||
|
|
|||
|
public bool ShowingGrid
|
|||
|
{
|
|||
|
get => _showingGrid;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _showingGrid, value);
|
|||
|
}
|
|||
|
|
|||
|
public double AcquisitionFps
|
|||
|
{
|
|||
|
get => _acquisitionFps;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _acquisitionFps, value);
|
|||
|
}
|
|||
|
|
|||
|
public int ImageCount
|
|||
|
{
|
|||
|
get => _imageCount;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _imageCount, value);
|
|||
|
}
|
|||
|
|
|||
|
public int DisplayImageCount
|
|||
|
{
|
|||
|
get => _displayImageCount;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _displayImageCount, value);
|
|||
|
}
|
|||
|
|
|||
|
public string Bandwidth
|
|||
|
{
|
|||
|
get => _bandwidth;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _bandwidth, value);
|
|||
|
}
|
|||
|
|
|||
|
public string OriginSize
|
|||
|
{
|
|||
|
get => _originSize;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _originSize, value);
|
|||
|
}
|
|||
|
|
|||
|
public string PixelCoordinate
|
|||
|
{
|
|||
|
get => _pixelCoordinate;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _pixelCoordinate, value);
|
|||
|
}
|
|||
|
|
|||
|
public Point CenterPixelPoint
|
|||
|
{
|
|||
|
get => _centerPixelPoint;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _centerPixelPoint, value);
|
|||
|
}
|
|||
|
|
|||
|
public int TuningStep
|
|||
|
{
|
|||
|
get => _tuningStep;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _tuningStep, value);
|
|||
|
}
|
|||
|
|
|||
|
public bool IsFullScreen
|
|||
|
{
|
|||
|
get => _isFullScreen;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _isFullScreen, value);
|
|||
|
}
|
|||
|
|
|||
|
public Stretch Stretch
|
|||
|
{
|
|||
|
get => _stretch;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _stretch, value);
|
|||
|
}
|
|||
|
|
|||
|
public int StretchIndex
|
|||
|
{
|
|||
|
get => _stretchIndex;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _stretchIndex, value);
|
|||
|
}
|
|||
|
|
|||
|
public bool RecordingVideo
|
|||
|
{
|
|||
|
get => _recordingVideo;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _recordingVideo, value);
|
|||
|
}
|
|||
|
|
|||
|
public string ScaleRatio
|
|||
|
{
|
|||
|
get => _scaleRatio;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _scaleRatio, value);
|
|||
|
}
|
|||
|
|
|||
|
public double CameraPositionX
|
|||
|
{
|
|||
|
get => _cameraPositionX;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _cameraPositionX, value);
|
|||
|
}
|
|||
|
|
|||
|
public double CameraPositionY
|
|||
|
{
|
|||
|
get => _cameraPositionY;
|
|||
|
set => this.RaiseAndSetIfChanged(ref _cameraPositionY, value);
|
|||
|
}
|
|||
|
|
|||
|
public ReactiveCommand<Unit, Unit> ToCapturePhoto { get; }
|
|||
|
|
|||
|
public ReactiveCommand<Unit, Unit> ToSavePhoto { get; }
|
|||
|
|
|||
|
public ReactiveCommand<Unit, Unit> ToggleCaptureVideo { get; }
|
|||
|
public ReactiveCommand<Unit, Unit> ToToggleRecordingVideo { get; }
|
|||
|
|
|||
|
public ReactiveCommand<Unit, Unit> ToChangeStretch { get; }
|
|||
|
|
|||
|
public ReactiveCommand<Unit, Unit> ToggleShowCrosshair { get; }
|
|||
|
|
|||
|
public ReactiveCommand<Unit, Unit> ToggleShowGrid { get; }
|
|||
|
|
|||
|
public ReactiveCommand<Unit, Unit> ToMoveToWaferHeight { get; }
|
|||
|
|
|||
|
public ReactiveCommand<Unit, Unit> ToMoveToCrosshairCenter { get; }
|
|||
|
|
|||
|
public ReactiveCommand<Unit, Unit> ToMoveToSubstrateHeight { get; }
|
|||
|
|
|||
|
public void InitViewer(ImageViewer imageViewer)
|
|||
|
{
|
|||
|
_imageViewer = imageViewer;
|
|||
|
}
|
|||
|
|
|||
|
public void InitWindow(CameraViewerWindow window)
|
|||
|
{
|
|||
|
_window = window;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 切换图片拉伸模式
|
|||
|
/// </summary>
|
|||
|
private void ChangeStretch()
|
|||
|
{
|
|||
|
StretchIndex = ++StretchIndex % _stretchs.Length;
|
|||
|
Stretch = _stretchs[StretchIndex];
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 保存截图
|
|||
|
/// </summary>
|
|||
|
private void SavePhoto()
|
|||
|
{
|
|||
|
Task.Run(() =>
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
var systemParams = _systemParamsService.GetSystemParams();
|
|||
|
var dir = systemParams.CameraViewerStoragePath ??
|
|||
|
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
|
|||
|
|
|||
|
var viewBox = _imageViewer.GetViewer();
|
|||
|
|
|||
|
var pixelSize = new PixelSize((int)viewBox.Bounds.Width, (int)viewBox.Bounds.Height);
|
|||
|
var dpi = new Vector(96, 96);
|
|||
|
var renderTargetBitmap = new RenderTargetBitmap(pixelSize, dpi);
|
|||
|
var context = renderTargetBitmap.CreateDrawingContext();
|
|||
|
Dispatcher.UIThread.Invoke(() => { viewBox.Render(context); });
|
|||
|
|
|||
|
dir = Path.Combine(dir, "Dispenser", $"{DateTime.Now:yyyyMMdd}");
|
|||
|
|
|||
|
if (!Directory.Exists(dir))
|
|||
|
{
|
|||
|
Directory.CreateDirectory(dir);
|
|||
|
}
|
|||
|
|
|||
|
using (var stream = File.OpenWrite(Path.Combine(dir, $"{DateTime.Now:yyyyMMddHHmmss}.bmp")))
|
|||
|
{
|
|||
|
renderTargetBitmap.Save(stream);
|
|||
|
}
|
|||
|
|
|||
|
Dispatcher.UIThread.InvokeAsync(() => { new CameraViewerNotifyWindow("图片保存成功", dir).Show(); });
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Log.Error(e, "保存图片失败");
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 采集单张图片
|
|||
|
/// </summary>
|
|||
|
[Operation("采集单张图片")]
|
|||
|
private async Task CapturePhoto()
|
|||
|
{
|
|||
|
var camera = CameraManager.GetCamera()!;
|
|||
|
if (CapturingVideo)
|
|||
|
{
|
|||
|
// 如果正在采集则提示停止
|
|||
|
var confirm = await ConfirmDialog.ToConfirm("正在采集中,是否停止采集?");
|
|||
|
if (!confirm) return;
|
|||
|
// // 先停止采集
|
|||
|
camera.StopCaptureVideo();
|
|||
|
}
|
|||
|
|
|||
|
// 采集单张图片
|
|||
|
camera.TokePhoto();
|
|||
|
|
|||
|
if (camera.CapturingVideo)
|
|||
|
{
|
|||
|
camera.StartCaptureVideo();
|
|||
|
}
|
|||
|
|
|||
|
CapturingPicture = true;
|
|||
|
ShowingCrosshair = false;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 连续采集
|
|||
|
/// </summary>
|
|||
|
[Operation("开启/停止连续采集")]
|
|||
|
private void CaptureVideo()
|
|||
|
{
|
|||
|
ImageCount = 0;
|
|||
|
DisplayImageCount = 0;
|
|||
|
_receivedPerSecond = 0;
|
|||
|
|
|||
|
var camera = CameraManager.GetCamera()!;
|
|||
|
if (CapturingVideo)
|
|||
|
{
|
|||
|
// 先停止采集
|
|||
|
camera.StopCaptureVideo();
|
|||
|
// 开启定时任务去更新显示信息
|
|||
|
JobScheduler.RemoveTask(TaskName);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// 开始连续采集
|
|||
|
camera.StartCaptureVideo();
|
|||
|
// 开启定时任务去更新显示信息
|
|||
|
JobScheduler.AddTask(TaskName, UpdateDisplayInfo, 1000);
|
|||
|
}
|
|||
|
|
|||
|
CapturingVideo = !CapturingVideo;
|
|||
|
ShowingCrosshair = false;
|
|||
|
}
|
|||
|
|
|||
|
public void StopCaptureVideo()
|
|||
|
{
|
|||
|
CapturingVideo = false;
|
|||
|
var camera = CameraManager.GetCamera()!;
|
|||
|
// 先停止采集
|
|||
|
camera.StopCaptureVideo();
|
|||
|
// 开启定时任务去更新显示信息
|
|||
|
JobScheduler.RemoveTask(TaskName);
|
|||
|
ShowingCrosshair = false;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 接受图像回调
|
|||
|
/// </summary>
|
|||
|
/// <param name="type"></param>
|
|||
|
/// <param name="data"></param>
|
|||
|
private async Task CameraCallBack(EventType type, CameraData data)
|
|||
|
{
|
|||
|
await Task.Run(() =>
|
|||
|
{
|
|||
|
if (!CapturingPicture && !CapturingVideo) return;
|
|||
|
|
|||
|
var image = data.Image;
|
|||
|
if (image.IsNullOrEmpty()) return;
|
|||
|
|
|||
|
_pixelFormat = image.PixelFormat;
|
|||
|
_receivedPerSecond++;
|
|||
|
// 更新接收到的图片数
|
|||
|
ImageCount = CapturingVideo ? ImageCount + 1 : 1;
|
|||
|
// 照片尺寸
|
|||
|
OriginSize = $"{image.Width}*{image.Height}";
|
|||
|
|
|||
|
// 更新展示的图片数
|
|||
|
DisplayImageCount++;
|
|||
|
|
|||
|
Dispatcher.UIThread.Invoke(() =>
|
|||
|
{
|
|||
|
if (Blank) Blank = false;
|
|||
|
CaptureImage = image;
|
|||
|
if (CapturingPicture) CapturingPicture = false;
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// 定时更新显示信息
|
|||
|
private void UpdateDisplayInfo()
|
|||
|
{
|
|||
|
if (OriginSize.IsNullOrEmpty()) return;
|
|||
|
var pixelFormatSize = Image.GetPixelFormatSize(_pixelFormat) / 8;
|
|||
|
var size = OriginSize.Split("*");
|
|||
|
|
|||
|
// 开启定时任务,每隔1秒更新一次
|
|||
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|||
|
{
|
|||
|
Bandwidth = GetByteSizeString(
|
|||
|
int.Parse(size[0]) * int.Parse(size[1]) * pixelFormatSize * _receivedPerSecond);
|
|||
|
AcquisitionFps = _receivedPerSecond;
|
|||
|
|
|||
|
_receivedPerSecond = 0;
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 动态就算带宽
|
|||
|
/// </summary>
|
|||
|
/// <param name="sizeInBytes"></param>
|
|||
|
/// <returns></returns>
|
|||
|
static string GetByteSizeString(long sizeInBytes)
|
|||
|
{
|
|||
|
string[] sizeSuffixes = ["Bps", "Kps", "Mps", "Gps"];
|
|||
|
|
|||
|
if (sizeInBytes == 0)
|
|||
|
return "0" + sizeSuffixes[0];
|
|||
|
|
|||
|
var magnitude = (int)Math.Log(sizeInBytes, 1024);
|
|||
|
var adjustedSize = sizeInBytes / Math.Pow(1024, magnitude);
|
|||
|
|
|||
|
return $"{adjustedSize:0.##} {sizeSuffixes[magnitude]}";
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 控制是否显示十字辅助现
|
|||
|
/// </summary>
|
|||
|
private void ShowCrosshair()
|
|||
|
{
|
|||
|
ShowingCrosshair = !ShowingCrosshair;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 屏幕录屏
|
|||
|
/// </summary>
|
|||
|
private void ToggleRecordingVideo()
|
|||
|
{
|
|||
|
if (RecordingVideo)
|
|||
|
{
|
|||
|
Task.Run(StopRecordingVideo);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
Task.Run(StartRecordingVideo);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 开始录屏
|
|||
|
/// </summary>
|
|||
|
[Operation("开始录屏")]
|
|||
|
private void StartRecordingVideo()
|
|||
|
{
|
|||
|
if (RecordingVideo) return;
|
|||
|
RecordingVideo = true;
|
|||
|
|
|||
|
var systemParams = _systemParamsService.GetSystemParams();
|
|||
|
var dir = systemParams.CameraViewerStoragePath ??
|
|||
|
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
|
|||
|
|
|||
|
dir = Path.Combine(dir, "Dispenser", $"{DateTime.Now:yyyyMMdd}");
|
|||
|
|
|||
|
if (!Directory.Exists(dir))
|
|||
|
{
|
|||
|
Directory.CreateDirectory(dir);
|
|||
|
}
|
|||
|
|
|||
|
var lastFileName = Path.Combine(dir, DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss") + ".avi");
|
|||
|
|
|||
|
// ReSharper disable once InconsistentNaming
|
|||
|
FourCC MJPEG_IMAGE_SHARP = "IMG#";
|
|||
|
|
|||
|
_recorder = new ScreenRecorder(lastFileName, MJPEG_IMAGE_SHARP, 10, 100);
|
|||
|
OnWindowResize();
|
|||
|
_recorder.Start();
|
|||
|
}
|
|||
|
|
|||
|
// 停止录屏
|
|||
|
[Operation("停止录屏")]
|
|||
|
public void StopRecordingVideo()
|
|||
|
{
|
|||
|
if (!RecordingVideo || _recorder == null) return;
|
|||
|
RecordingVideo = false;
|
|||
|
var dir = _recorder.GetDirectory();
|
|||
|
_recorder.Stop();
|
|||
|
Dispatcher.UIThread.InvokeAsync(() => { new CameraViewerNotifyWindow("录屏保存成功", dir!).Show(); });
|
|||
|
}
|
|||
|
|
|||
|
public void OnWindowResize()
|
|||
|
{
|
|||
|
// 支取中间的90%画面内容
|
|||
|
if (_recorder != null)
|
|||
|
{
|
|||
|
var container = _imageViewer.GetContainer();
|
|||
|
var width = container.Bounds.Width - 50;
|
|||
|
var height = container.Bounds.Height - 50;
|
|||
|
|
|||
|
var left = _window.Position.X + 20;
|
|||
|
var top = _window.Position.Y + 60;
|
|||
|
|
|||
|
_recorder.Width = (int)width;
|
|||
|
_recorder.Height = (int)height;
|
|||
|
_recorder.Left = left;
|
|||
|
_recorder.Top = top;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public void UpdateScaleRatio(double ratio)
|
|||
|
{
|
|||
|
if (ratio < 1)
|
|||
|
{
|
|||
|
ScaleRatio = ((1 - ratio) * -100).ToString("0.##") + "%";
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
ScaleRatio = (ratio * 100).ToString("0.##") + "%";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 控制是否显示网格线
|
|||
|
/// </summary>
|
|||
|
private void ShowGrid()
|
|||
|
{
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 移除监听
|
|||
|
/// </summary>
|
|||
|
public void DisableCameraListener()
|
|||
|
{
|
|||
|
EventBus<CameraData>.RemoveEventHandler(EventType.CameraData, CameraCallBack);
|
|||
|
JobScheduler.RemoveTask("CameraViewer#WatchCameraPosition");
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
private void TriggerTakePhoto()
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
var camera = CameraManager.GetCamera()!;
|
|||
|
camera.TokePhoto();
|
|||
|
|
|||
|
if (camera.CapturingVideo)
|
|||
|
{
|
|||
|
camera.StartCaptureVideo();
|
|||
|
}
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Log.Error(e, "坐标验证采集照片失败: ");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|