Просмотр исходного кода

Merge branch 'LCD组装' of http://49.235.130.76/XXF_1122/DaisyNPILine into LCD组装

wanghan 7 месяцев назад
Родитель
Сommit
d478bf672c
2 измененных файлов с 339 добавлено и 44 удалено
  1. 98 8
      TeamAAS-VM/Core/Management.cs
  2. 241 36
      TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs

+ 98 - 8
TeamAAS-VM/Core/Management.cs

@@ -1833,7 +1833,7 @@ namespace TeamAAS_VP.Core
                         }
                     }
                 }
-                // 固定相机取料拍照
+                // 固定相机取料拍照--外壳拍照
                 else if (addressConfig.In_FixedCameraPickExe.Address.Contains(tuple.nodeId))
                 {
                     if (tuple.value is bool state)
@@ -1901,6 +1901,51 @@ namespace TeamAAS_VP.Core
                                 Vector<double> p3 = Vector<double>.Build.Dense(new double[] { visionResults[3].X, visionResults[3].Y });
                                 Vector<double> p4 = Vector<double>.Build.Dense(new double[] { visionResults[0].X, visionResults[0].Y });
 
+                                LogHelper.WriteLogInfo($"P1:{visionResults[1].X};{visionResults[1].Y};");
+                                LogHelper.WriteLogInfo($"P2:{visionResults[2].X};{visionResults[2].Y};");
+                                LogHelper.WriteLogInfo($"P3:{visionResults[3].X};{visionResults[3].Y};");
+                                LogHelper.WriteLogInfo($"P4:{visionResults[0].X};{visionResults[0].Y};");
+                                var pp1 = new Point2d(visionResults[1].X, visionResults[1].Y);
+                                var pp2 = new Point2d(visionResults[2].X, visionResults[2].Y);
+                                var pp3 = new Point2d(visionResults[3].X, visionResults[3].Y);
+                                var pp4 = new Point2d(visionResults[0].X, visionResults[0].Y);
+                                var dist1 = Point2d.Distance(pp1, pp2);//长1
+                                var dist2 = Point2d.Distance(pp2, pp3);//宽1
+                                var dist3 = Point2d.Distance(pp3, pp4);//长2
+                                var dist4 = Point2d.Distance(pp1, pp4);//宽2
+                                LogHelper.WriteLogInfo($"dist1={dist1};dist2={dist2};dist3={dist3};dist4={dist4};");
+                                if (currentProduct.GlobalParamListCollection.Count >= 4)
+                                {
+                                    var param = currentProduct.GlobalParamListCollection.FirstOrDefault(p => p.Number == 2);
+                                    if (param != null)//长
+                                    {
+                                        var length = Double.Parse(param.Param1);
+                                        var limtDown = Double.Parse(param.Param2);
+                                        var LimtUp = Double.Parse(param.Param3);
+                                        if (!(((length + limtDown) <= dist1) && (dist1 <= (length + LimtUp)) && (((length + limtDown) <= dist3) && (dist3 <= (length + LimtUp)))))
+                                        {
+                                            SendTaskMessage($"屏幕长度超范围:长1={dist1},长2={dist3},para1={length},para2={limtDown},para3={LimtUp}", MessageLevel.Alarm);
+                                            await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (Int16)2);
+                                            return;
+                                        }
+                                        SendTaskMessage($"屏幕长度:长1={dist1},长2={dist3},para1={length},para2={limtDown},para3={LimtUp}", MessageLevel.Info);
+                                    }
+                                    param = currentProduct.GlobalParamListCollection.FirstOrDefault(p => p.Number == 3);
+                                    if (param != null)//宽
+                                    {
+                                        var length = Double.Parse(param.Param1);
+                                        var limtDown = Double.Parse(param.Param2);
+                                        var LimtUp = Double.Parse(param.Param3);
+                                        if (!(((length + limtDown) <= dist2) && (dist2 <= (length + LimtUp)) && (((length + limtDown) <= dist4) && (dist4 <= (length + LimtUp)))))
+                                        {
+                                            SendTaskMessage($"屏幕宽度超范围:宽1={dist2},宽2={dist4},para1={length},para2={limtDown},para3={LimtUp}", MessageLevel.Alarm);
+                                            await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (Int16)2);
+                                            return;
+                                        }
+                                        SendTaskMessage($"屏幕宽度:宽1={dist2},宽2={dist4},para1={length},para2={limtDown},para3={LimtUp}", MessageLevel.Info);
+                                    }
+                                }
+
                                 var center = RectangleCenterCalculator.CalculateByPCAWithKnownOrder(new List<Vector<double>> { p1, p2, p3, p4 });
                                 UpCameraPutResults[1].X = (float)center.Center[0];
                                 UpCameraPutResults[1].Y = (float)center.Center[1];
@@ -1923,7 +1968,7 @@ namespace TeamAAS_VP.Core
                         }
                     }
                 }
-                // 固定相机锁附/组装拍照
+                // 固定相机锁附/组装拍照--屏幕拍照
                 else if (addressConfig.In_FixedCameraPutExe.Address.Contains(tuple.nodeId))
                 {
                     if (tuple.value is bool state)
@@ -1990,6 +2035,51 @@ namespace TeamAAS_VP.Core
                                 Vector<double> p3 = Vector<double>.Build.Dense(new double[] { visionResults[3].X, visionResults[3].Y });
                                 Vector<double> p4 = Vector<double>.Build.Dense(new double[] { visionResults[0].X, visionResults[0].Y });
 
+                                LogHelper.WriteLogInfo($"P1:{visionResults[1].X};{visionResults[1].Y};");
+                                LogHelper.WriteLogInfo($"P2:{visionResults[2].X};{visionResults[2].Y};");
+                                LogHelper.WriteLogInfo($"P3:{visionResults[3].X};{visionResults[3].Y};");
+                                LogHelper.WriteLogInfo($"P4:{visionResults[0].X};{visionResults[0].Y};");
+                                var pp1 = new Point2d(visionResults[1].X, visionResults[1].Y);
+                                var pp2 = new Point2d(visionResults[2].X, visionResults[2].Y);
+                                var pp3 = new Point2d(visionResults[3].X, visionResults[3].Y);
+                                var pp4 = new Point2d(visionResults[0].X, visionResults[0].Y);
+                                var dist1 = Point2d.Distance(pp1, pp2);//长1
+                                var dist2 = Point2d.Distance(pp2, pp3);//宽1
+                                var dist3 = Point2d.Distance(pp3, pp4);//长2
+                                var dist4 = Point2d.Distance(pp1, pp4);//宽2
+                                LogHelper.WriteLogInfo($"dist1={dist1};dist2={dist2};dist3={dist3};dist4={dist4};");
+                                if (currentProduct.GlobalParamListCollection.Count >= 4)
+                                {
+                                    var param = currentProduct.GlobalParamListCollection.FirstOrDefault(p => p.Number == 0);
+                                    if (param != null)//长
+                                    {
+                                        var length = Double.Parse(param.Param1);
+                                        var limtDown = Double.Parse(param.Param2);
+                                        var LimtUp = Double.Parse(param.Param3);
+                                        if (!(((length + limtDown) <= dist1) && (dist1 <= (length + LimtUp)) && (((length + limtDown) <= dist3) && (dist3 <= (length + LimtUp)))))
+                                        {
+                                            SendTaskMessage($"外壳长度超范围:长1={dist1},长2={dist3},para1={length},para2={limtDown},para3={LimtUp}", MessageLevel.Alarm);
+                                            await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPutStatus.Address, (Int16)2);
+                                            return;
+                                        }
+                                        SendTaskMessage($"外壳长度:长1={dist1},长2={dist3},para1={length},para2={limtDown},para3={LimtUp}", MessageLevel.Info);
+                                    }
+                                    param = currentProduct.GlobalParamListCollection.FirstOrDefault(p => p.Number == 1);
+                                    if (param != null)//宽
+                                    {
+                                        var length = Double.Parse(param.Param1);
+                                        var limtDown = Double.Parse(param.Param2);
+                                        var LimtUp = Double.Parse(param.Param3);
+                                        if (!(((length + limtDown) <= dist2) && (dist2 <= (length + LimtUp)) && (((length + limtDown) <= dist4) && (dist4 <= (length + LimtUp)))))
+                                        {
+                                            SendTaskMessage($"外壳宽度超范围:宽1={dist2},宽2={dist4},para1={length},para2={limtDown},para3={LimtUp}", MessageLevel.Alarm);
+                                            await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPutStatus.Address, (Int16)2);
+                                            return;
+                                        }
+                                        SendTaskMessage($"外壳宽度:宽1={dist2},宽2={dist4},para1={length},para2={limtDown},para3={LimtUp}", MessageLevel.Info);
+                                    }
+                                }
+
                                 var center = RectangleCenterCalculator.CalculateByPCAWithKnownOrder(new List<Vector<double>> { p1, p2, p3, p4 });
                                 UpCameraPutResults[0].X = (float)center.Center[0];
                                 UpCameraPutResults[0].Y = (float)center.Center[1];
@@ -2111,7 +2201,7 @@ namespace TeamAAS_VP.Core
                         {
                             SendTaskMessage($"收到多相机坐标合并请求,开始合并...", MessageLevel.Debug);
 
-                            if (isDryMode)
+                            if (isDryMode)//空跑模式
                             {
                                 var point1 = currentProduct.ScrewPoints[0].Clone();
                                 SendTaskMessage($"点:1,{point1.X_Position:F3},{point1.Y_Position:F4},{point1.U_Position:F4}", MessageLevel.Info);
@@ -2246,7 +2336,7 @@ namespace TeamAAS_VP.Core
                             await plc.WriteNodeAsync(addressConfig.Out_QrCodePhotoResault.Address, (Int16)0);
                         }
                     }
-                   
+
                 }
                 else
                 {
@@ -2277,7 +2367,7 @@ namespace TeamAAS_VP.Core
 
                                     LogHelper.WriteLogMes($"【mes开始QUERY】");
                                     SendTaskMessage($"mes开始QUERY", MessageLevel.Info);
-                                    (bool isSuccess, string response) = await _mesService.StationGetAsync(i,code, comp, 2);
+                                    (bool isSuccess, string response) = await _mesService.StationGetAsync(i, code, comp, 2);
                                     if (i == 0)
                                     {
                                         SendTaskMessage($"收到当前过站检查请求(当站是否生产)...", MessageLevel.Debug);
@@ -2382,13 +2472,13 @@ namespace TeamAAS_VP.Core
                                     LogHelper.WriteLogMes($"【mes开始ADD】");
                                     SendTaskMessage($"mes开始ADD", MessageLevel.Info);
                                     string gap = ""; string press = ""; string presstime = ""; string assypress = "";
-                                    if (i==0)
+                                    if (i == 0)
                                     {
                                         gap = gapString;
                                         try
                                         {
                                             assypress = currentPressure[0].ToString("F2");
-                                            if (currentPressure[0] >= p1.PressureDownLimit && currentPressure[0]<=p1.PressureUpLimit)
+                                            if (currentPressure[0] >= p1.PressureDownLimit && currentPressure[0] <= p1.PressureUpLimit)
                                             {
                                                 resultIndex = 1;
                                             }
@@ -2511,7 +2601,7 @@ namespace TeamAAS_VP.Core
                                         }
                                     }
                                     //gap-复检gap参数, press-保压站实际压力值, presstime-保压站保压时间, assypress-组装站实际压力值
-                                    (bool isSuccess, string response) = await _mesService.SubmitGetAsync(i,code, comp, gap, press, presstime, assypress, resultIndex == 1 ? true : false, _LastMesCheckTime[i], DateTime.UtcNow);
+                                    (bool isSuccess, string response) = await _mesService.SubmitGetAsync(i, code, comp, gap, press, presstime, assypress, resultIndex == 1 ? true : false, _LastMesCheckTime[i], DateTime.UtcNow);
                                     if (isSuccess)
                                     {
                                         LogHelper.WriteLogMes($"上传mes成功");

+ 241 - 36
TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs

@@ -6,6 +6,7 @@ using System;
 using System.Collections.Generic;
 using System.Drawing;
 using System.IO;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
@@ -35,6 +36,10 @@ namespace TeamAAS_VP.Views.Home
         private readonly ISystemDatabaseService _systemDatabaseService;
         private readonly IProductService _productService;
 
+        // ======================= 【新增】保存任务控制:防闪退/防资源泄露 =======================
+        private CancellationTokenSource _saveImageCts = new CancellationTokenSource();
+        private readonly SemaphoreSlim _saveSemaphore = new SemaphoreSlim(2, 2);
+
         public ShowVisionRender()
         {
             InitializeComponent();
@@ -51,6 +56,19 @@ namespace TeamAAS_VP.Views.Home
             viewModel.UpdateLayout += ViewModel_UpdateLayout;
             _eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(UpdateRenderModuleSource);
             _eventAggregator.GetEvent<ProductChangedNotification>().Subscribe(ProductChanged);
+
+            // 【可选】控件卸载时取消所有保存任务,避免页面关闭后仍在截图/保存导致闪退
+            this.Unloaded += ShowVisionRender_Unloaded;
+        }
+
+        private void ShowVisionRender_Unloaded(object sender, RoutedEventArgs e)
+        {
+            try
+            {
+                _saveImageCts.Cancel();
+                _saveImageCts.Dispose();
+            }
+            catch { }
         }
 
         /// <summary>
@@ -77,25 +95,9 @@ namespace TeamAAS_VP.Views.Home
                             if (render.Record != null)
                                 disp.Record = render.Record;
 
-                            if (render.IsSaveImage)
-                            {
-                                // 注意:CreateContentBitmap 可能比较耗时,但你这里已经在 UI 线程里调用了;
-                                // 如果卡顿明显,建议把 bitmap 生成放到后台线程(需谨慎处理线程访问)
-                                App.Current.Dispatcher.BeginInvoke(new Action(() =>
-                                {
-                                    var bmp = (Bitmap)disp.CreateContentBitmap(Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
-
-                                    SaveImage(
-                                        render.SaveImageModel,
-                                        render.SaveImagePathModel,
-                                        render.SavePath,
-                                        render.Image,
-                                        bmp,
-                                        render.Result,
-                                        render.IsCompress
-                                    );
-                                }));
-                            }
+                            // ======================= 【关键优化】保存图像走安全入口 =======================
+                            RequestSaveImage(render, disp);
+
                             break;
                         }
                     }
@@ -108,6 +110,15 @@ namespace TeamAAS_VP.Views.Home
         /// </summary>
         private async void ViewModel_UpdateLayout(System.Collections.ObjectModel.ObservableCollection<Models.ShowRender> obj)
         {
+            // ======================= 【关键优化】布局刷新前先取消旧保存任务 =======================
+            try
+            {
+                _saveImageCts.Cancel();
+                _saveImageCts.Dispose();
+            }
+            catch { }
+            _saveImageCts = new CancellationTokenSource();
+
             System.Windows.Media.FontFamily font = Application.Current.Resources["DefaultFont"] as System.Windows.Media.FontFamily;
 
             // 1) 清理旧控件
@@ -132,15 +143,7 @@ namespace TeamAAS_VP.Views.Home
             // 3) 限制最大 16(防止越界)
             int count = Math.Min(obj.Count, 16);
 
-            // 4) 计算行列(1~16自动铺满:尽量接近正方形;最大 4*4)
-            //    1  -> 1*1
-            //    2  -> 1*2
-            //    3  -> 2*2
-            //    4  -> 2*2
-            //    5~6 -> 2*3
-            //    7~9 -> 3*3(9满)
-            //    10~12 -> 3*4(12满)
-            //    13~16 -> 4*4(16满)
+            // 4) 计算行列(最大 4*4)
             GetGridSize(count, out int rows, out int cols);
 
             // 5) 创建主Grid的行列
@@ -158,13 +161,39 @@ namespace TeamAAS_VP.Views.Home
                 var sr = obj[i];
 
                 // (1) 创建显示控件(WindowsFormsHost + CogRecordDisplay)
-                var host = new WindowsFormsHost()
+                var disp = new CogRecordDisplay()
                 {
                     Tag = sr.ProceductName,
-                    Child = new CogRecordDisplay()
+                };
+
+                disp.HandleCreated += (s, e) =>
+                {
+                    try
                     {
-                        Tag = sr.ProceductName,
+                        ConfigureCogDisplay(disp); // 句柄创建后再配置,最稳
+                        // 绑定 WinForms 双击事件
+                        //disp.MouseDoubleClick += (r, w) =>
+                        //{
+                        //    try
+                        //    {
+                        //        OnDisplayDoubleClick(disp);
+                        //    }
+                        //    catch (Exception ex)
+                        //    {
+                        //        LogHelper.WriteLogError("双击预览弹窗出错", ex);
+                        //    }
+                        //};
                     }
+                    catch (Exception ex)
+                    {
+                        LogHelper.WriteLogError("ConfigureCogDisplay 失败", ex);
+                    }
+                };
+
+                var host = new WindowsFormsHost()
+                {
+                    Tag = sr.ProceductName,
+                    Child = disp
                 };
                 vmRenders.Add(sr.Id, host);
 
@@ -209,7 +238,6 @@ namespace TeamAAS_VP.Views.Home
         /// </summary>
         private void GetGridSize(int count, out int rows, out int cols)
         {
-            // 默认值
             rows = 1;
             cols = 1;
 
@@ -220,7 +248,6 @@ namespace TeamAAS_VP.Views.Home
             if (count <= 9) { rows = 3; cols = 3; return; }
             if (count <= 12) { rows = 3; cols = 4; return; }
 
-            // 13~16
             rows = 4;
             cols = 4;
         }
@@ -233,11 +260,94 @@ namespace TeamAAS_VP.Views.Home
             disp.HorizontalScrollBar = false;
             disp.VerticalScrollBar = false;
             disp.AutoFit = true;
+            disp.AutoFitWithGraphics = true;
             disp.BackColor = System.Drawing.SystemColors.ActiveCaption;
         }
 
+        // ======================= 【新增】安全保存入口:解决闪退 =======================
+        private void RequestSaveImage(ShowRender render, CogRecordDisplay disp)
+        {
+            if (render == null) return;
+            if (disp == null) return;
+            if (!render.IsSaveImage) return;
+
+            // Original 模式不需要 CreateContentBitmap(你的 SaveImage 里 Original 只写 ICogImage)
+            bool needRecordedBitmap =
+                render.SaveImageModel == ProcedureSaveImageModel.Recorded ||
+                render.SaveImageModel == ProcedureSaveImageModel.OriginalAndRecorded;
+            _saveImageCts = new CancellationTokenSource();
+            var token = _saveImageCts.Token;
+
+            // 必须在控件的 UI 线程上执行 CreateContentBitmap
+            disp.BeginInvoke(new Action(() =>
+            {
+                Bitmap clonedBitmap = null;
+
+                try
+                {
+                    token.ThrowIfCancellationRequested();
+
+                    if (needRecordedBitmap)
+                    {
+                        // UI线程创建
+                        Bitmap uiBitmap = (Bitmap)disp.CreateContentBitmap(
+                            Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
+
+                        // 立刻 Clone,后台线程只用 Clone(防止控件刷新/释放导致崩溃)
+                        clonedBitmap = (Bitmap)uiBitmap.Clone();
+
+                        // UI线程立刻释放
+                        uiBitmap.Dispose();
+                    }
+
+                    // 后台保存(限并发,防止GDI/内存瞬时飙升)
+                    Task.Run(async () =>
+                    {
+                        await _saveSemaphore.WaitAsync(token);
+                        try
+                        {
+                            token.ThrowIfCancellationRequested();
+
+                            SaveImage(
+                                render.SaveImageModel,
+                                render.SaveImagePathModel,
+                                render.SavePath,
+                                render.Image,
+                                clonedBitmap,
+                                render.Result,
+                                render.IsCompress
+                            );
+                        }
+                        catch (OperationCanceledException)
+                        {
+                            // 取消属于正常情况,不记录
+                        }
+                        catch (Exception ex)
+                        {
+                            LogHelper.WriteLogError("后台保存图像任务出错!", ex);
+                        }
+                        finally
+                        {
+                            // 确保释放 Clone 的 Bitmap,避免 GDI 泄漏导致闪退
+                            try { clonedBitmap?.Dispose(); } catch { }
+                            _saveSemaphore.Release();
+                        }
+                    }, token);
+                }
+                catch (OperationCanceledException)
+                {
+                    try { clonedBitmap?.Dispose(); } catch { }
+                }
+                catch (Exception ex)
+                {
+                    try { clonedBitmap?.Dispose(); } catch { }
+                    LogHelper.WriteLogError("RequestSaveImage(UI截图阶段) 出错!", ex);
+                }
+            }));
+        }
+
         /// <summary>
-        /// 保存图像(保持你原有逻辑不变)
+        /// 保存图像(保持你原有逻辑不变,补充 Dispose 兜底,防止压缩分支不释放导致闪退
         /// </summary>
         private void SaveImage(ProcedureSaveImageModel saveImageModel,
                                ProcedureSaveImagePathModel saveImagePathModel,
@@ -358,7 +468,6 @@ namespace TeamAAS_VP.Views.Home
                         else
                         {
                             reimage.Save(Recordedpath);
-                            reimage.Dispose();
                         }
                     }
                     else if (saveImageModel == ProcedureSaveImageModel.OriginalAndRecorded)
@@ -438,7 +547,6 @@ namespace TeamAAS_VP.Views.Home
                         else
                         {
                             reimage.Save(Recordedpath);
-                            reimage.Dispose();
                         }
                     }
                 }
@@ -446,6 +554,11 @@ namespace TeamAAS_VP.Views.Home
                 {
                     LogHelper.WriteLogError("保存流程运行结果的图片时出错!", ex);
                 }
+                finally
+                {
+                    // ======================= 【关键修复】无论是否压缩都释放Bitmap =======================
+                    try { reimage?.Dispose(); } catch { }
+                }
             });
         }
 
@@ -478,5 +591,97 @@ namespace TeamAAS_VP.Views.Home
                 }
             }));
         }
+        private void OnDisplayDoubleClick(CogRecordDisplay disp)
+        {
+            if (disp == null) return;
+
+            // 必须在 WinForms/ActiveX 线程截图
+            disp.BeginInvoke(new Action(() =>
+            {
+                Bitmap bmp = null;
+
+                try
+                {
+                    if (disp.IsDisposed || !disp.IsHandleCreated)
+                        return;
+
+                    // 优先使用显示内容截图(带图形/record叠加)
+                    try
+                    {
+                        var uiBmp = (Bitmap)disp.CreateContentBitmap(
+                            Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
+
+                        bmp = (Bitmap)uiBmp.Clone();
+                        uiBmp.Dispose();
+                    }
+                    catch
+                    {
+                        // 如果 ActiveX 状态不允许截图,退化:尝试从 Image 转 Bitmap
+                        bmp = TryConvertCogImageToBitmap(disp.Image);
+                    }
+
+                    if (bmp == null) return;
+
+                    // 回到 WPF UI 线程弹窗
+                    this.Dispatcher.BeginInvoke(new Action(() =>
+                    {
+                        //var win = new ImagePreviewWindow(bmp, $"预览:{disp.Tag ?? "Display"}");
+                        //win.Owner = Window.GetWindow(this); // 让弹窗归属当前窗口
+                        //win.Show();
+                    }));
+                }
+                catch (Exception ex)
+                {
+                    LogHelper.WriteLogError("OnDisplayDoubleClick 截图/弹窗出错", ex);
+                }
+                finally
+                {
+                    // 这里 bmp 不要 Dispose,因为传给窗口了
+                    // 窗口关闭时会释放
+                }
+            }));
+        }
+
+        /// <summary>
+        /// 将 VisionPro 的 ICogImage 尝试转换成 Bitmap(作为 CreateContentBitmap 失败时的兜底)
+        /// </summary>
+        private Bitmap TryConvertCogImageToBitmap(ICogImage cogImage)
+        {
+            try
+            {
+                if (cogImage == null) return null;
+
+                // VisionPro 常用转换:CogImage8Grey / CogImage24PlanarColor / CogImage24PackedColor 等
+                // 这里给通用兜底:用 CogImageFileBMP 落地到内存流,再读回 Bitmap
+                using (var ms = new MemoryStream())
+                {
+                    // 需要引用 Cognex.VisionPro.ImageFile
+                    using (var bmpFile = new CogImageFileBMP())
+                    {
+                        // CogImageFileBMP 只能写文件路径,不直接写流
+                        // 所以用临时文件方式最稳(如你不想落盘,可以另写转换器)
+                        var tmp = Path.Combine(Path.GetTempPath(), $"vp_preview_{Guid.NewGuid():N}.bmp");
+                        try
+                        {
+                            bmpFile.Open(tmp, CogImageFileModeConstants.Write);
+                            bmpFile.Append(cogImage);
+                            bmpFile.Close();
+
+                            var bitmap = (Bitmap)Bitmap.FromFile(tmp);
+                            return (Bitmap)bitmap.Clone();
+                        }
+                        finally
+                        {
+                            try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
+                        }
+                    }
+                }
+            }
+            catch
+            {
+                return null;
+            }
+        }
+
     }
 }