Files
community/Ink Canvas/MainWindow_cs/MW_VideoPresenter.cs
T
2026-02-20 14:01:53 +08:00

761 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using AForge.Imaging;
using AForge.Imaging.Filters;
using AForge.Math.Geometry;
using Ink_Canvas.Helpers;
using Ink_Canvas.Models;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media.Imaging;
namespace Ink_Canvas
{
public partial class MainWindow : Window
{
// 标记:用于在保存/恢复白板内容时排除“展台实时上屏”画面
private const string VideoPresenterLiveFrameTag = "__VideoPresenterLiveFrame";
private CameraService _cameraService;
private readonly object _videoPresenterFrameLock = new object();
private Bitmap _lastFrame;
private readonly List<CapturedImage> _capturedPhotos = new List<CapturedImage>();
private const int MaxCapturedPhotos = 50; // 容量上限:比 UI 显示的 30 项多一些,避免频繁清理
// 按页绑定:每一页对应一个“实时画面”元素与布局/设备信息
private readonly Dictionary<int, System.Windows.Controls.Image> _liveFrameImageByPage = new Dictionary<int, System.Windows.Controls.Image>();
private readonly HashSet<int> _liveEnabledPages = new HashSet<int>();
private readonly Dictionary<int, int> _cameraIndexByPage = new Dictionary<int, int>();
private readonly Dictionary<int, (double left, double top, double width)> _liveFrameLayoutByPage =
new Dictionary<int, (double left, double top, double width)>();
private DateTime _lastCaptureTime = DateTime.MinValue;
private const int VideoPresenterCaptureCooldownMs = 1000;
private const int CorrectedPaperHeight = 600;
private void BtnToggleVideoPresenter_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
ToggleVideoPresenterSidebar();
}
private void ToggleVideoPresenterSidebar()
{
if (VideoPresenterSidebar == null) return;
if (VideoPresenterSidebar.Visibility == Visibility.Visible)
{
VideoPresenterSidebar.Visibility = Visibility.Collapsed;
return;
}
VideoPresenterSidebar.Visibility = Visibility.Visible;
EnsureCameraService();
if (BtnCapturePhoto != null) BtnCapturePhoto.IsEnabled = false;
RefreshVideoPresenterDeviceList();
if (ToggleBtnPhotoCorrection != null)
{
ToggleBtnPhotoCorrection.IsChecked = Settings?.Automation?.IsEnablePhotoCorrection ?? false;
}
// 同步“上屏”按钮状态(按页绑定)
if (BtnToggleVideoPresenterLiveOnCanvas != null)
{
BtnToggleVideoPresenterLiveOnCanvas.IsChecked = _liveEnabledPages.Contains(GetCurrentPageIndex());
}
}
private void BtnCloseVideoPresenter_Click(object sender, RoutedEventArgs e)
{
if (VideoPresenterSidebar != null)
{
VideoPresenterSidebar.Visibility = Visibility.Collapsed;
}
}
private void EnsureCameraService()
{
if (_cameraService != null) return;
_cameraService = new CameraService();
_cameraService.FrameReceived += CameraService_FrameReceived;
_cameraService.ErrorOccurred += CameraService_ErrorOccurred;
}
private void CameraService_ErrorOccurred(object sender, string e)
{
try
{
LogHelper.WriteLogToFile($"视频展台摄像头错误: {e}", LogHelper.LogType.Error);
}
catch { }
}
private void CameraService_FrameReceived(object sender, Bitmap frame)
{
if (frame == null) return;
try
{
Bitmap serviceCopy;
try
{
serviceCopy = (Bitmap)frame.Clone();
}
catch
{
// 可能在下一帧到来时被 CameraService 释放,直接忽略这一帧
return;
}
lock (_videoPresenterFrameLock)
{
_lastFrame?.Dispose();
_lastFrame = (Bitmap)serviceCopy.Clone();
}
var preview = ConvertBitmapToBitmapImage(serviceCopy);
serviceCopy.Dispose();
if (preview == null) return;
Dispatcher.BeginInvoke(new Action(() =>
{
if (VideoPresenterPreviewImage != null)
{
VideoPresenterPreviewImage.Source = preview;
}
if (BtnCapturePhoto != null)
{
BtnCapturePhoto.IsEnabled = true;
}
// 实时上屏:刷新当前页的画面元素
TryUpdateLiveFrameOnCanvas(preview);
}));
}
catch
{
// 忽略预览刷新异常
}
}
private int GetCurrentPageIndex()
{
return Math.Max(1, CurrentWhiteboardIndex);
}
private void TryUpdateLiveFrameOnCanvas(BitmapImage preview)
{
try
{
int page = GetCurrentPageIndex();
if (!_liveEnabledPages.Contains(page)) return;
if (inkCanvas == null) return;
if (!_liveFrameImageByPage.TryGetValue(page, out var img) || img == null) return;
if (!inkCanvas.Children.Contains(img))
{
inkCanvas.Children.Add(img);
}
img.Source = preview;
img.Visibility = Visibility.Visible;
}
catch { }
}
private const double VideoPresenterLiveFrameScreenRatio = 0.75;
private System.Windows.Controls.Image EnsureLiveFrameElementForPage(int page)
{
if (_liveFrameImageByPage.TryGetValue(page, out var existing) && existing != null) return existing;
double canvasW = inkCanvas?.ActualWidth ?? 0;
double canvasH = inkCanvas?.ActualHeight ?? 0;
double w = canvasW > 10 && canvasH > 10
? canvasW * VideoPresenterLiveFrameScreenRatio
: 520;
double h = canvasW > 10 && canvasH > 10
? canvasH * VideoPresenterLiveFrameScreenRatio
: 390;
var img = new System.Windows.Controls.Image
{
Tag = VideoPresenterLiveFrameTag,
Stretch = System.Windows.Media.Stretch.Uniform,
Width = w,
Height = h,
Visibility = Visibility.Visible,
Opacity = 1.0
};
try
{
InitializeElementTransform(img);
BindElementEvents(img);
}
catch { }
_liveFrameImageByPage[page] = img;
return img;
}
private void ApplyLiveFrameLayoutForPage(int page, System.Windows.Controls.Image img)
{
if (img == null) return;
if (_liveFrameLayoutByPage.TryGetValue(page, out var layout))
{
if (!double.IsNaN(layout.width) && layout.width > 10) img.Width = layout.width;
InkCanvas.SetLeft(img, Math.Max(0, layout.left));
InkCanvas.SetTop(img, Math.Max(0, layout.top));
return;
}
// 默认尺寸:画布宽高的 75%;位置居中
double cw = inkCanvas?.ActualWidth ?? 0;
double ch = inkCanvas?.ActualHeight ?? 0;
if (cw > 10 && ch > 10)
{
img.Width = cw * VideoPresenterLiveFrameScreenRatio;
img.Height = ch * VideoPresenterLiveFrameScreenRatio;
}
double x = (inkCanvas?.ActualWidth ?? 0) / 2 - img.Width / 2;
double y = (inkCanvas?.ActualHeight ?? 0) / 2 - img.Height / 2;
if (double.IsNaN(x) || double.IsInfinity(x)) x = 100;
if (double.IsNaN(y) || double.IsInfinity(y)) y = 100;
InkCanvas.SetLeft(img, Math.Max(0, x));
InkCanvas.SetTop(img, Math.Max(0, y));
}
private void RefreshVideoPresenterDeviceList()
{
if (_cameraService == null) return;
if (CameraDevicesStackPanel == null) return;
_cameraService.RefreshCameraList();
CameraDevicesStackPanel.Children.Clear();
if (_cameraService.AvailableCameras == null || _cameraService.AvailableCameras.Count == 0)
{
var tb = new TextBlock
{
Text = "未检测到摄像头设备",
FontSize = 12,
Margin = new Thickness(5),
HorizontalAlignment = HorizontalAlignment.Center
};
tb.SetResourceReference(TextBlock.ForegroundProperty, "FloatBarForeground");
CameraDevicesStackPanel.Children.Add(tb);
return;
}
for (int i = 0; i < _cameraService.AvailableCameras.Count; i++)
{
int idx = i;
var dev = _cameraService.AvailableCameras[i];
var rb = new RadioButton
{
Content = dev.Name,
Margin = new Thickness(0, 2, 0, 2),
FontSize = 12,
Tag = idx,
};
rb.SetResourceReference(Control.ForegroundProperty, "FloatBarForeground");
rb.Checked += (s, e) => StartVideoPresenterPreview(idx);
CameraDevicesStackPanel.Children.Add(rb);
}
// 自动启动第一个摄像头
if (_cameraService.AvailableCameras.Count > 0)
{
if (CameraDevicesStackPanel.Children.Count > 0 && CameraDevicesStackPanel.Children[0] is RadioButton first)
{
first.IsChecked = true;
}
else
{
StartVideoPresenterPreview(0);
}
}
}
private void StartVideoPresenterPreview(int cameraIndex)
{
try
{
EnsureCameraService();
_cameraIndexByPage[GetCurrentPageIndex()] = cameraIndex;
if (_cameraService.StartPreview(cameraIndex))
{
if (BtnCapturePhoto != null) BtnCapturePhoto.IsEnabled = true;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动视频展台预览失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void BtnToggleVideoPresenterLiveOnCanvas_Checked(object sender, RoutedEventArgs e)
{
int page = GetCurrentPageIndex();
_liveEnabledPages.Add(page);
var img = EnsureLiveFrameElementForPage(page);
ApplyLiveFrameLayoutForPage(page, img);
if (inkCanvas != null && !inkCanvas.Children.Contains(img))
{
inkCanvas.Children.Add(img);
}
try
{
SetCurrentToolMode(InkCanvasEditingMode.Select);
UpdateCurrentToolMode("select");
HideSubPanels("select");
}
catch { }
// 立即用侧栏预览刷新一次
if (VideoPresenterPreviewImage?.Source is BitmapImage bi)
{
img.Source = bi;
}
}
private void BtnToggleVideoPresenterLiveOnCanvas_Unchecked(object sender, RoutedEventArgs e)
{
int page = GetCurrentPageIndex();
_liveEnabledPages.Remove(page);
if (_liveFrameImageByPage.TryGetValue(page, out var img) && img != null)
{
try
{
if (inkCanvas != null && inkCanvas.Children.Contains(img))
{
inkCanvas.Children.Remove(img);
}
}
catch { }
}
}
// 翻页前调用:保存当前页实时画面的位置/大小
private void VideoPresenter_BeforePageLeave()
{
try
{
int page = GetCurrentPageIndex();
if (!_liveFrameImageByPage.TryGetValue(page, out var img) || img == null) return;
double left = InkCanvas.GetLeft(img);
double top = InkCanvas.GetTop(img);
if (double.IsNaN(left)) left = 0;
if (double.IsNaN(top)) top = 0;
_liveFrameLayoutByPage[page] = (left, top, img.Width);
}
catch { }
}
// 翻页后调用:根据该页状态恢复实时画面,并同步设备选择
private void VideoPresenter_OnPageChanged()
{
try
{
int page = GetCurrentPageIndex();
// 同步“上屏”按钮状态
if (BtnToggleVideoPresenterLiveOnCanvas != null)
{
BtnToggleVideoPresenterLiveOnCanvas.IsChecked = _liveEnabledPages.Contains(page);
}
// 若该页上屏,恢复画面元素(RestoreStrokes 会清空 inkCanvas.Children
if (_liveEnabledPages.Contains(page))
{
var img = EnsureLiveFrameElementForPage(page);
ApplyLiveFrameLayoutForPage(page, img);
if (inkCanvas != null && !inkCanvas.Children.Contains(img))
{
inkCanvas.Children.Add(img);
}
if (VideoPresenterPreviewImage?.Source is BitmapImage bi)
{
img.Source = bi;
}
}
// 按页摄像头索引:切页后自动切回该页的摄像头
if (_cameraIndexByPage.TryGetValue(page, out int idx))
{
EnsureCameraService();
_cameraService?.StartPreview(idx);
}
}
catch { }
}
private void BtnCapturePhoto_Click(object sender, RoutedEventArgs e)
{
try
{
if ((DateTime.Now - _lastCaptureTime).TotalMilliseconds < VideoPresenterCaptureCooldownMs) return;
_lastCaptureTime = DateTime.Now;
Bitmap frame;
lock (_videoPresenterFrameLock)
{
if (_lastFrame == null) return;
frame = (Bitmap)_lastFrame.Clone();
}
Task.Run(() =>
{
try
{
using (frame)
{
Bitmap toSave = frame;
if (Settings?.Automation?.IsEnablePhotoCorrection == true
&& TryDetectPaperCorners(toSave, out List<AForge.IntPoint> corners))
{
var corrected = ApplyPerspectiveCorrection(toSave, corners);
if (corrected != null) toSave = corrected;
}
var bmpImage = ConvertBitmapToBitmapImage(toSave);
if (!ReferenceEquals(toSave, frame))
{
toSave.Dispose();
}
if (bmpImage == null) return;
Dispatcher.BeginInvoke(new Action(() =>
{
var ci = new CapturedImage(bmpImage);
_capturedPhotos.Insert(0, ci);
while (_capturedPhotos.Count > MaxCapturedPhotos)
{
var oldPhoto = _capturedPhotos[_capturedPhotos.Count - 1];
_capturedPhotos.RemoveAt(_capturedPhotos.Count - 1);
}
UpdateCapturedPhotosDisplay();
}));
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"视频展台拍照失败: {ex.Message}", LogHelper.LogType.Error);
}
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"视频展台拍照失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void BtnRotateImage_Click(object sender, RoutedEventArgs e)
{
try
{
EnsureCameraService();
_cameraService.RotationAngle = (_cameraService.RotationAngle + 1) % 4;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"视频展台旋转失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ToggleBtnPhotoCorrection_Checked(object sender, RoutedEventArgs e)
{
if (Settings?.Automation == null) return;
Settings.Automation.IsEnablePhotoCorrection = true;
SaveSettingsToFile();
}
private void ToggleBtnPhotoCorrection_Unchecked(object sender, RoutedEventArgs e)
{
if (Settings?.Automation == null) return;
Settings.Automation.IsEnablePhotoCorrection = false;
SaveSettingsToFile();
}
private void UpdateCapturedPhotosDisplay()
{
if (CapturedPhotosStackPanel == null) return;
CapturedPhotosStackPanel.Children.Clear();
foreach (var photo in _capturedPhotos.Take(30))
{
var btn = new Button
{
Margin = new Thickness(0, 0, 0, 6),
Padding = new Thickness(0),
BorderThickness = new Thickness(0),
Background = System.Windows.Media.Brushes.Transparent,
Tag = photo
};
btn.Click += (s, e) =>
{
if (btn.Tag is CapturedImage p) InsertPhotoToCanvas(p);
};
var img = new System.Windows.Controls.Image
{
Source = photo.Thumbnail,
Stretch = System.Windows.Media.Stretch.UniformToFill,
Height = 90
};
btn.Content = img;
CapturedPhotosStackPanel.Children.Add(btn);
}
}
private void InsertPhotoToCanvas(CapturedImage photo)
{
if (photo?.Image == null) return;
try
{
var img = new System.Windows.Controls.Image
{
Source = photo.Image,
Stretch = System.Windows.Media.Stretch.Uniform,
Width = 500
};
double x = (inkCanvas?.ActualWidth ?? 0) / 2 - img.Width / 2;
double y = (inkCanvas?.ActualHeight ?? 0) / 2 - 200;
if (double.IsNaN(x) || double.IsInfinity(x)) x = 100;
if (double.IsNaN(y) || double.IsInfinity(y)) y = 100;
InkCanvas.SetLeft(img, Math.Max(0, x));
InkCanvas.SetTop(img, Math.Max(0, y));
InitializeElementTransform(img);
BindElementEvents(img);
timeMachine.CommitElementInsertHistory(img);
inkCanvas?.Children.Add(img);
SetCurrentToolMode(InkCanvasEditingMode.Select);
UpdateCurrentToolMode("select");
HideSubPanels("select");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"插入展台照片失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void VideoPresenter_OnExitWhiteboardMode()
{
try
{
// 收起侧栏
if (VideoPresenterSidebar != null)
{
VideoPresenterSidebar.Visibility = Visibility.Collapsed;
}
if (BtnToggleVideoPresenterLiveOnCanvas != null)
{
BtnToggleVideoPresenterLiveOnCanvas.IsChecked = false;
}
if (inkCanvas != null)
{
foreach (var kv in _liveFrameImageByPage.ToList())
{
var img = kv.Value;
if (img == null) continue;
try
{
if (inkCanvas.Children.Contains(img))
{
inkCanvas.Children.Remove(img);
}
img.Visibility = Visibility.Collapsed;
}
catch { }
}
}
try { _cameraService?.StopPreview(); } catch { }
}
catch { }
}
private static BitmapImage ConvertBitmapToBitmapImage(Bitmap bitmap)
{
try
{
if (bitmap == null) return null;
using (var ms = new MemoryStream())
{
bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
ms.Position = 0;
var bi = new BitmapImage();
bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.StreamSource = ms;
bi.EndInit();
bi.Freeze();
return bi;
}
}
catch
{
return null;
}
}
private static bool TryDetectPaperCorners(Bitmap frame, out List<AForge.IntPoint> cornersOut)
{
cornersOut = null;
try
{
if (frame == null) return false;
int targetWidth = 640;
int ow = frame.Width;
int oh = frame.Height;
double scale = 1.0;
Bitmap work = frame;
if (ow > targetWidth)
{
int nh = (int)Math.Round(oh * (targetWidth / (double)ow));
var resize = new ResizeBilinear(targetWidth, nh);
work = resize.Apply(frame);
scale = (double)ow / targetWidth;
}
var gray = Grayscale.CommonAlgorithms.BT709.Apply(work);
var blur = new GaussianBlur(3, 3);
blur.ApplyInPlace(gray);
var canny = new CannyEdgeDetector();
canny.ApplyInPlace(gray);
var dilate = new Dilatation3x3();
dilate.ApplyInPlace(gray);
var bc = new BlobCounter
{
FilterBlobs = true,
MinHeight = 50,
MinWidth = 50,
ObjectsOrder = ObjectsOrder.Size
};
bc.ProcessImage(gray);
var blobs = bc.GetObjectsInformation();
var sc = new SimpleShapeChecker();
List<AForge.IntPoint> best = null;
double bestArea = 0;
foreach (var blob in blobs)
{
var edgePoints = bc.GetBlobsEdgePoints(blob);
if (edgePoints == null || edgePoints.Count < 4) continue;
if (sc.IsQuadrilateral(edgePoints, out List<AForge.IntPoint> crn))
{
double area = Math.Abs(PolygonArea(crn));
if (area > bestArea)
{
bestArea = area;
best = crn;
}
}
}
if (best != null)
{
var pts = best
.Select(p => new AForge.IntPoint((int)Math.Round(p.X * scale), (int)Math.Round(p.Y * scale)))
.ToList();
pts.Sort((a, b) => a.Y.CompareTo(b.Y));
if (pts[0].X > pts[1].X) (pts[0], pts[1]) = (pts[1], pts[0]);
if (pts[2].X > pts[3].X) (pts[2], pts[3]) = (pts[3], pts[2]);
cornersOut = pts;
if (!ReferenceEquals(work, frame)) work.Dispose();
gray.Dispose();
return true;
}
if (!ReferenceEquals(work, frame)) work.Dispose();
gray.Dispose();
return false;
}
catch
{
return false;
}
}
private static Bitmap ApplyPerspectiveCorrection(Bitmap frame, List<AForge.IntPoint> corners)
{
try
{
if (frame == null || corners == null || corners.Count != 4) return null;
var tl = corners[0];
var tr = corners[1];
var bl = corners[2];
var br = corners[3];
double topW = Math.Sqrt((tr.X - tl.X) * (tr.X - tl.X) + (tr.Y - tl.Y) * (tr.Y - tl.Y));
double bottomW = Math.Sqrt((br.X - bl.X) * (br.X - bl.X) + (br.Y - bl.Y) * (br.Y - bl.Y));
double leftH = Math.Sqrt((bl.X - tl.X) * (bl.X - tl.X) + (bl.Y - tl.Y) * (bl.Y - tl.Y));
double rightH = Math.Sqrt((br.X - tr.X) * (br.X - tr.X) + (br.Y - tr.Y) * (br.Y - tr.Y));
double avgW = (topW + bottomW) / 2.0;
double avgH = (leftH + rightH) / 2.0;
if (avgH <= 0) avgH = 1;
double ratio = avgW / avgH;
int targetH = CorrectedPaperHeight;
int targetW = Math.Max(1, (int)Math.Round(targetH * ratio));
var orderedCorners = new List<AForge.IntPoint> { tl, tr, br, bl };
var qtf = new QuadrilateralTransformation(orderedCorners, targetW, targetH);
return qtf.Apply(frame);
}
catch
{
return null;
}
}
private static double PolygonArea(List<AForge.IntPoint> pts)
{
int n = pts.Count;
if (n < 3) return 0;
long sum = 0;
for (int i = 0; i < n; i++)
{
var p = pts[i];
var q = pts[(i + 1) % n];
sum += (long)p.X * q.Y - (long)p.Y * q.X;
}
return 0.5 * sum;
}
}
}