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 _capturedPhotos = new List(); private const int MaxCapturedPhotos = 50; // 容量上限:比 UI 显示的 30 项多一些,避免频繁清理 // 按页绑定:每一页对应一个“实时画面”元素与布局/设备信息 private readonly Dictionary _liveFrameImageByPage = new Dictionary(); private readonly HashSet _liveEnabledPages = new HashSet(); private readonly Dictionary _cameraIndexByPage = new Dictionary(); private readonly Dictionary _liveFrameLayoutByPage = new Dictionary(); 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 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 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 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 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 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 { tl, tr, br, bl }; var qtf = new QuadrilateralTransformation(orderedCorners, targetW, targetH); return qtf.Apply(frame); } catch { return null; } } private static double PolygonArea(List 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; } } }