Эх сурвалжийг харах

perf: ShowVisionRender异步优化 - 解决图像处理UI卡顿

- Subscribe改为ThreadOption.BackgroundThread,Publish立即返回
- UpdateRenderModuleSource后台深拷贝ICogImage防像素覆盖
- 新增RequestSaveImage方法: SemaphoreSlim并发控制 + CancellationToken支持
- SaveImage改为SaveImageAsync: 移除嵌套Task.Run
- 新增UpdateDelaySaveImage: lock保护延迟列表 + 资源释放
- 新增ShowVisionRender_Unloaded: 取消令牌 + Bitmap兜底清理
- 新增DelaySaveImage模型和DelaySaveImageNotification事件
- ShowRender新增IsDelaySaveImage属性
- 保留AOI-3分支3D模型(Is3DModel/CogImageFile)支持
zly 2 сар өмнө
parent
commit
5877bb466b

+ 11 - 0
TeamAAS-VM/Events/DelaySaveImageNotification.cs

@@ -0,0 +1,11 @@
+using Prism.Events;
+
+namespace TeamAAS_VP.Events
+{
+    /// <summary>
+    /// 延迟保存图像通知事件。发布 true 表示执行保存, false 表示取消并丢弃所有待保存项。
+    /// </summary>
+    public class DelaySaveImageNotification : PubSubEvent<bool>
+    {
+    }
+}

+ 20 - 0
TeamAAS-VM/Models/DelaySaveImage.cs

@@ -0,0 +1,20 @@
+using System.Drawing;
+
+namespace TeamAAS_VP.Models
+{
+    /// <summary>
+    /// 延迟保存图像的数据项
+    /// </summary>
+    public class DelaySaveImage
+    {
+        public Enums.ProcedureSaveImageModel SaveImageModel { get; set; }
+        public Enums.ProcedureSaveImagePathModel SaveImagePathModel { get; set; }
+        public string SavePath { get; set; }
+        public Cognex.VisionPro.ICogImage Image { get; set; }
+        public Bitmap clonedBitmap { get; set; }
+        public bool Result { get; set; }
+        public bool IsCompress { get; set; }
+        public string ImageFileName { get; set; }
+        public bool Is3DModel { get; set; }
+    }
+}

+ 10 - 0
TeamAAS-VM/Models/ShowRender.cs

@@ -166,5 +166,15 @@ namespace TeamAAS_VP.Models
             get { return _GreyImage; }
             set { SetProperty(ref _GreyImage, value); }
         }
+
+        private bool _IsDelaySaveImage;
+        /// <summary>
+        /// 是否延迟保存图像(等待统一触发保存)
+        /// </summary>
+        public bool IsDelaySaveImage
+        {
+            get { return _IsDelaySaveImage; }
+            set { SetProperty(ref _IsDelaySaveImage, value); }
+        }
     }
 }

+ 2 - 1
TeamAAS-VM/Views/Home/ShowVisionRender.xaml

@@ -11,7 +11,8 @@
              lex:ResxLocalizationProvider.DefaultAssembly="TeamAAS-VP"
              lex:ResxLocalizationProvider.DefaultDictionary="Lang"
              prism:ViewModelLocator.AutoWireViewModel="True"
-             FontFamily="{DynamicResource DefaultFont}">
+             FontFamily="{DynamicResource DefaultFont}"
+             Unloaded="ShowVisionRender_Unloaded">
     <b:Interaction.Triggers>
         <b:EventTrigger EventName="Loaded">
             <b:InvokeCommandAction Command="{Binding LoadedCommand}" />

+ 300 - 173
TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs

@@ -45,9 +45,9 @@ namespace TeamAAS_VP.Views.Home
         private readonly IProductService _productService;
         private readonly Prism.Services.Dialogs.IDialogService _dialogService;
 
-        // ====== 异步保存控制 ======
         private CancellationTokenSource _saveImageCts = new CancellationTokenSource();
         private readonly SemaphoreSlim _saveSemaphore = new SemaphoreSlim(5, 5);
+        private List<Models.DelaySaveImage> delaySaveImages = new List<Models.DelaySaveImage>();
 
         public ShowVisionRender()
         {
@@ -62,22 +62,13 @@ namespace TeamAAS_VP.Views.Home
             _dialogService = ((Prism.PrismApplicationBase)App.Current).Container.Resolve<Prism.Services.Dialogs.IDialogService>();
 
             viewModel.UpdateLayout += ViewModel_UpdateLayout;
-            // ThreadOption.BackgroundThread:Publish() 立即返回,图像处理不等待 UI 更新和文件 I/O
-            _eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(UpdateRenderModuleSource, ThreadOption.BackgroundThread);
+            // ThreadOption.BackgroundThread:让订阅者在后台线程执行,Publish() 立即返回,
+            // 图像处理流程不再等待 UI 更新和保存完成才回复完成信号
+            _eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(
+                UpdateRenderModuleSource,
+                ThreadOption.BackgroundThread);
             _eventAggregator.GetEvent<ProductChangedNotification>().Subscribe(ProductChanged);
-
-            // 控件卸载时取消所有保存任务,防止页面关闭后仍在写文件导致闪退
-            this.Unloaded += ShowVisionRender_Unloaded;
-        }
-
-        private void ShowVisionRender_Unloaded(object sender, RoutedEventArgs e)
-        {
-            try
-            {
-                _saveImageCts.Cancel();
-                _saveImageCts.Dispose();
-            }
-            catch { }
+            _eventAggregator.GetEvent<DelaySaveImageNotification>().Subscribe(UpdateDelaySaveImage);
         }
 
         private bool GetView3DUse()
@@ -101,15 +92,12 @@ namespace TeamAAS_VP.Views.Home
             }
         }
 
-        /// <summary>
-        /// 更新 2D/3D 显示并触发异步保存。
-        /// 运行在后台线程(ThreadOption.BackgroundThread),不阻塞图像处理主流程。
-        /// </summary>
         private void UpdateRenderModuleSource(ShowRender render)
         {
-            if (render == null) return;
+            if (render == null)
+                return;
 
-            // 【关键】在后台线程深拷贝 ICogImage,防止下一次 ToolBlock.Run() 覆盖像素数据
+            // 【关键】在后台线程深拷贝 ICogImage,防止下一次视觉流程运行覆盖像素数据
             ICogImage imageCopy = null;
             try
             {
@@ -120,139 +108,84 @@ namespace TeamAAS_VP.Views.Home
                 LogHelper.WriteLogError("深拷贝 ICogImage 失败", ex);
             }
 
-            var token = _saveImageCts.Token;
+            // ICogRecord 和 Graphic 通常每次运行新创建,引用不会被覆盖,无需深拷贝
+            var graphic = render.Graphic;
+            var record = render.Record;
+            var rangeImage = render.RangeImage;
+            var greyImage = render.GreyImage;
 
-            // 派发 UI 更新到 WPF UI 线程(fire-and-forget)
+            // 派发 UI 更新到 WPF UI 线程(fire-and-forget,不等待完成
             this.gridRender.Dispatcher.BeginInvoke(new Action(() =>
             {
-                try
+                if (render.Is3DModel == true && GetView3DUse() == true)
                 {
-                    if (token.IsCancellationRequested) return;
-
-                    // ==== 3D 显示 ====
-                    bool is3D = render.Is3DModel && GetView3DUse();
-                    if (is3D)
-                    {
-                        if (vmRenders.TryGetValue(Get3DKey(render.Id), out var host3D))
-                        {
-                            if (host3D.Child is Cognex.VisionPro3D.Cog3DDisplayV2WF display3D)
-                            {
-                                try
-                                {
-                                    display3D.Clear();
-                                    if (render.RangeImage != null && render.GreyImage != null)
-                                    {
-                                        var rImgG = new Cog3DRangeImageGraphic(render.RangeImage, render.GreyImage);
-                                        display3D.Add(rImgG, "__default");
-                                        display3D.FitView();
-                                    }
-                                }
-                                catch (System.Windows.Forms.AxHost.InvalidActiveXStateException) { }
-                            }
-                        }
-                    }
-
-                    // ==== 2D 显示 ====
-                    if (!vmRenders.TryGetValue(Get2DKey(render.Id), out var host2D))
+                    if (!vmRenders.TryGetValue(Get3DKey(render.Id), out var host3D))
                         return;
 
-                    if (!(host2D.Child is CogRecordDisplay recordDisplay))
+                    var display3D = host3D.Child as Cognex.VisionPro3D.Cog3DDisplayV2WF;
+                    if (display3D == null)
                         return;
 
-                    recordDisplay.Record = null;
-                    recordDisplay.Image = imageCopy;            // 深拷贝后的图像
-
-                    recordDisplay.StaticGraphics.Clear();
-                    if (render.Graphic != null)
-                        recordDisplay.StaticGraphics.AddList(render.Graphic, "");
+                    try
+                    {
+                        display3D.Clear();
 
-                    if (render.Record != null)
-                        recordDisplay.Record = render.Record;
+                        if (rangeImage == null || greyImage == null)
+                            return;
 
-                    // ==== 异步保存(fire-and-forget,不阻塞 UI 线程) ====
-                    if (render.IsSaveImage)
+                        var rImgG = new Cog3DRangeImageGraphic(rangeImage, greyImage);
+                        display3D.Add(rImgG, "__default");
+                        display3D.FitView();
+                    }
+                    catch (System.Windows.Forms.AxHost.InvalidActiveXStateException)
                     {
-                        RequestSaveImage(recordDisplay, imageCopy, render, token);
                     }
                 }
-                catch (System.Windows.Forms.AxHost.InvalidActiveXStateException) { }
-            }));
-        }
 
-        /// <summary>
-        /// 在 WinForms 控件线程上截图,然后后台异步保存。
-        /// </summary>
-        private void RequestSaveImage(
-            CogRecordDisplay recordDisplay,
-            ICogImage imageCopy,
-            ShowRender render,
-            CancellationToken token)
-        {
-            recordDisplay.BeginInvoke(new Action(() =>
-            {
-                Bitmap clonedBitmap = null;
+                if (!vmRenders.TryGetValue(Get2DKey(render.Id), out var host2D))
+                    return;
+
+                var recordDisplay = host2D.Child as Cognex.VisionPro.CogRecordDisplay;
+                if (recordDisplay == null)
+                    return;
+
                 try
                 {
-                    if (token.IsCancellationRequested) return;
-                    if (recordDisplay.IsDisposed || !recordDisplay.IsHandleCreated) return;
+                    recordDisplay.Record = null;
+                    recordDisplay.Image = imageCopy;    // ✅ 深拷贝,安全
 
-                    Bitmap uiBitmap = (Bitmap)recordDisplay.CreateContentBitmap(
-                        Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
-                    clonedBitmap = (Bitmap)uiBitmap.Clone();
-                    uiBitmap.Dispose();
+                    recordDisplay.StaticGraphics.Clear();
+                    if (graphic != null)
+                        recordDisplay.StaticGraphics.AddList(graphic, "");
 
-                    var capturedBitmap = clonedBitmap;
-                    var capturedRender = render;
-                    var capturedImage = imageCopy;
+                    if (record != null)
+                        recordDisplay.Record = record;
 
-                    Task.Run(async () =>
+                    if (render.IsSaveImage)
                     {
-                        await _saveSemaphore.WaitAsync(token);
-                        try
+                        // 传递深拷贝后的图像引用给保存逻辑
+                        var renderForSave = new ShowRender
                         {
-                            if (token.IsCancellationRequested) return;
-
-                            if (string.IsNullOrEmpty(_productService.CurrentProductCode))
-                            {
-                                _eventAggregator.GetEvent<TaskMessageNotification>()
-                                    .Publish(new Models.MessageStruct()
-                                    {
-                                        Message = "当前产品SN不存在,无法保存图片!",
-                                        level = MessageLevel.Alarm
-                                    });
-                                return;
-                            }
-
-                            LogHelper.WriteLogInfo("开始异步保存图像");
-                            await SaveImageAsync(
-                                capturedRender.SaveImageModel,
-                                capturedRender.SaveImagePathModel,
-                                capturedRender.SavePath,
-                                capturedImage,
-                                capturedBitmap,
-                                capturedRender.Result,
-                                capturedRender.IsCompress,
-                                capturedRender.ImageFileName,
-                                capturedRender.Is3DModel,
-                                token
-                            );
-                        }
-                        catch (OperationCanceledException) { }
-                        catch (Exception ex)
-                        {
-                            LogHelper.WriteLogError("后台保存图像任务出错!", ex);
-                        }
-                        finally
-                        {
-                            try { capturedBitmap?.Dispose(); } catch { }
-                            _saveSemaphore.Release();
-                        }
-                    }, token);
+                            Id = render.Id,
+                            Image = imageCopy,
+                            Record = record,
+                            Graphic = graphic,
+                            Result = render.Result,
+                            IsSaveImage = render.IsSaveImage,
+                            SaveImageModel = render.SaveImageModel,
+                            SaveImagePathModel = render.SaveImagePathModel,
+                            SavePath = render.SavePath,
+                            IsCompress = render.IsCompress,
+                            IsDelaySaveImage = render.IsDelaySaveImage,
+                            ImageFileName = render.ImageFileName,
+                            ProceductName = render.ProceductName,
+                            Is3DModel = render.Is3DModel,
+                        };
+                        RequestSaveImage(renderForSave, recordDisplay);
+                    }
                 }
-                catch (Exception ex)
+                catch (System.Windows.Forms.AxHost.InvalidActiveXStateException)
                 {
-                    LogHelper.WriteLogError("RequestSaveImage(UI截图阶段) 出错!", ex);
-                    try { clonedBitmap?.Dispose(); } catch { }
                 }
             }));
         }
@@ -370,15 +303,6 @@ namespace TeamAAS_VP.Views.Home
         Grid grid20 = new Grid();
         private 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;
             
             if (vmRenders.Count > 0)
@@ -717,22 +641,135 @@ namespace TeamAAS_VP.Views.Home
         }
 
         /// <summary>
-        /// 保存图像(保持你原有逻辑不变,补充 Dispose 兜底,防止压缩分支不释放导致闪退)
+        /// 请求保存图像(在 WinForms 消息循环线程截图,然后派发到后台线程保存)
+        /// 使用 SemaphoreSlim 控制并发,支持 CancellationToken 取消
         /// </summary>
+        private void RequestSaveImage(ShowRender render, Cognex.VisionPro.CogRecordDisplay disp)
+        {
+            if (render == null) return;
+            if (disp == null) return;
+            if (!render.IsSaveImage) return;
+
+            bool needRecordedBitmap =
+                render.SaveImageModel == ProcedureSaveImageModel.Recorded ||
+                render.SaveImageModel == ProcedureSaveImageModel.OriginalAndRecorded;
+
+            var token = _saveImageCts.Token;
+
+            disp.BeginInvoke(new Action(() =>
+            {
+                Bitmap clonedBitmap = null;
+                try
+                {
+                    if (token.IsCancellationRequested) return;
+                    if (needRecordedBitmap)
+                    {
+                        if (disp.IsDisposed || !disp.IsHandleCreated) return;
+                        Bitmap uiBitmap = (Bitmap)disp.CreateContentBitmap(
+                            Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
+                        clonedBitmap = (Bitmap)uiBitmap.Clone();
+                        uiBitmap.Dispose();
+                    }
+
+                    var capturedBitmap = clonedBitmap;
+                    var capturedRender = render;
+
+                    Task.Run(async () =>
+                    {
+                        await _saveSemaphore.WaitAsync(token);
+                        try
+                        {
+                            if (token.IsCancellationRequested) return;
+
+                            if (capturedRender.IsDelaySaveImage)
+                            {
+                                // 延迟保存:存入列表,等 DelaySaveImageNotification 时统一写入
+                                ICogImage imageDeepCopy = null;
+                                try
+                                {
+                                    imageDeepCopy = capturedRender.Image?
+                                        .CopyBase(CogImageCopyModeConstants.CopyPixels);
+                                }
+                                catch (Exception ex)
+                                {
+                                    LogHelper.WriteLogError("延迟保存深拷贝 ICogImage 失败", ex);
+                                }
+
+                                var delayItem = new Models.DelaySaveImage()
+                                {
+                                    SaveImageModel = capturedRender.SaveImageModel,
+                                    SaveImagePathModel = capturedRender.SaveImagePathModel,
+                                    SavePath = capturedRender.SavePath,
+                                    Image = imageDeepCopy,
+                                    clonedBitmap = capturedBitmap != null
+                                        ? (Bitmap)capturedBitmap.Clone() : null,
+                                    Result = capturedRender.Result,
+                                    IsCompress = capturedRender.IsCompress,
+                                    ImageFileName = capturedRender.ImageFileName,
+                                    Is3DModel = capturedRender.Is3DModel,
+                                };
+
+                                lock (delaySaveImages)
+                                {
+                                    delaySaveImages.Add(delayItem);
+                                }
+                            }
+                            else
+                            {
+                                if (string.IsNullOrEmpty(_productService.CurrentProductCode))
+                                {
+                                    _eventAggregator.GetEvent<TaskMessageNotification>()
+                                        .Publish(new MessageStruct()
+                                        {
+                                            Message = "当前产品SN不存在,无法保存图片!",
+                                            level = MessageLevel.Alarm
+                                        });
+                                    return;
+                                }
+                                LogHelper.WriteLogInfo("开始异步保存图像");
+                                await SaveImageAsync(
+                                    capturedRender.SaveImageModel,
+                                    capturedRender.SaveImagePathModel,
+                                    capturedRender.SavePath,
+                                    capturedRender.Image,
+                                    capturedBitmap,
+                                    capturedRender.Result,
+                                    capturedRender.IsCompress,
+                                    capturedRender.ImageFileName,
+                                    capturedRender.Is3DModel,
+                                    token
+                                );
+                            }
+                        }
+                        catch (OperationCanceledException) { }
+                        catch (Exception ex)
+                        {
+                            LogHelper.WriteLogError("后台保存图像任务出错!", ex);
+                        }
+                        finally
+                        {
+                            try { capturedBitmap?.Dispose(); } catch { }
+                            _saveSemaphore.Release();
+                        }
+                    }, token);
+                }
+                catch (Exception ex)
+                {
+                    LogHelper.WriteLogError("RequestSaveImage(UI截图阶段) 出错!", ex);
+                    try { clonedBitmap?.Dispose(); } catch { }
+                }
+            }));
+        }
+
         /// <summary>
-        /// 异步保存图像(调用者已在后台线程,不再嵌套 Task.Run)
+        /// 异步保存图像(调用者已在后台线程,不再嵌套 Task.Run;通过 CancellationToken 支持取消
         /// </summary>
         private async Task SaveImageAsync(
             ProcedureSaveImageModel saveImageModel,
             ProcedureSaveImagePathModel saveImagePathModel,
-            string path,
-            ICogImage image,
-            Bitmap reimage,
-            bool result,
-            bool isCompress,
-            string imageFileName,
-            bool is3Dmodel,
-            CancellationToken cancellationToken)
+            string path, ICogImage image, Bitmap reimage,
+            bool result, bool isCompress, string imageFileName,
+            bool is3Dmodel, CancellationToken cancellationToken)
         {
             if (string.IsNullOrEmpty(path) || string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path))
             {
@@ -746,6 +783,7 @@ namespace TeamAAS_VP.Views.Home
                 cancellationToken.ThrowIfCancellationRequested();
 
                 DateTime now = DateTime.Now;
+                //图片名
                 string imgName = now.ToString("yyyy-MM-dd HH_mm_ss");
                 if (!string.IsNullOrEmpty(imageFileName))
                 {
@@ -778,10 +816,12 @@ namespace TeamAAS_VP.Views.Home
                     }
                     else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
                     {
+                        // 当前产品配方名(过滤非法字符)
                         string formulaName = _productService.GetCurrentProduct().Name;
                         foreach (char c in Path.GetInvalidFileNameChars())
                             formulaName = formulaName.Replace(c, '_');
 
+                        // 产品SN(过滤非法字符)
                         string productSN = _productService.CurrentProductCode;
                         foreach (char c in Path.GetInvalidFileNameChars())
                             productSN = productSN.Replace(c, '_');
@@ -856,26 +896,27 @@ namespace TeamAAS_VP.Views.Home
                 }
                 else if (saveImageModel == ProcedureSaveImageModel.OriginalAndRecorded)
                 {
-                    string Originalpath = is3Dmodel
-                        ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.idb"
-                        : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
-
+                    string Originalpath = "";
+                    if (is3Dmodel == true)
+                    {
+                        Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.idb";
+                    }
+                    else
+                    {
+                        Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
+                    }
                     string Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
 
                     if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_OkNG_Time)
                     {
                         if (result)
                         {
-                            Originalpath = is3Dmodel
-                                ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.idb"
-                                : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
+                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
                             Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
                         }
                         else
                         {
-                            Originalpath = is3Dmodel
-                                ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.idb"
-                                : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
+                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
                             Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
                         }
                     }
@@ -883,9 +924,7 @@ namespace TeamAAS_VP.Views.Home
                     {
                         if (result)
                         {
-                            Originalpath = is3Dmodel
-                                ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.idb"
-                                : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
+                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
                             Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
                         }
                         else
@@ -897,9 +936,7 @@ namespace TeamAAS_VP.Views.Home
                     {
                         if (!result)
                         {
-                            Originalpath = is3Dmodel
-                                ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.idb"
-                                : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
+                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
                             Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
                         }
                         else
@@ -921,8 +958,14 @@ namespace TeamAAS_VP.Views.Home
                         }
 
                         string resultStr = result ? "OK" : "NG";
-                        string ext = is3Dmodel ? ".idb" : ".bmp";
-                        Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}{ext}";
+                        if (is3Dmodel == true)
+                        {
+                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.idb";
+                        }
+                        else
+                        {
+                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp";
+                        }
                         Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp";
                     }
 
@@ -953,10 +996,7 @@ namespace TeamAAS_VP.Views.Home
                     }
                 }
             }
-            catch (OperationCanceledException)
-            {
-                // 正常取消,不记录日志
-            }
+            catch (OperationCanceledException) { }
             catch (Exception ex)
             {
                 LogHelper.WriteLogError("保存流程运行结果的图片时出错!", ex);
@@ -967,6 +1007,93 @@ namespace TeamAAS_VP.Views.Home
             }
         }
 
+        /// <summary>
+        /// 延迟保存图像处理:isSave=true 时批量保存所有待保存项;false 时丢弃并清理
+        /// </summary>
+        private async void UpdateDelaySaveImage(bool isSave)
+        {
+            if (!isSave)
+            {
+                lock (delaySaveImages)
+                {
+                    foreach (var item in delaySaveImages)
+                    {
+                        try { item.clonedBitmap?.Dispose(); } catch { }
+                        try { item.Image = null; } catch { }
+                    }
+                    delaySaveImages.Clear();
+                }
+                return;
+            }
+
+            List<Models.DelaySaveImage> itemsToSave;
+            lock (delaySaveImages)
+            {
+                itemsToSave = new List<Models.DelaySaveImage>(delaySaveImages);
+                delaySaveImages.Clear();
+            }
+
+            var token = _saveImageCts.Token;
+            foreach (var item in itemsToSave)
+            {
+                try
+                {
+                    if (token.IsCancellationRequested) break;
+
+                    if (string.IsNullOrEmpty(_productService.CurrentProductCode))
+                    {
+                        _eventAggregator.GetEvent<TaskMessageNotification>()
+                            .Publish(new MessageStruct()
+                            {
+                                Message = "当前产品SN不存在,无法保存图片!",
+                                level = MessageLevel.Alarm
+                            });
+                        break;
+                    }
+
+                    LogHelper.WriteLogInfo("开始异步保存延迟图像");
+                    await SaveImageAsync(
+                        item.SaveImageModel,
+                        item.SaveImagePathModel,
+                        item.SavePath,
+                        item.Image,
+                        item.clonedBitmap,
+                        item.Result,
+                        item.IsCompress,
+                        item.ImageFileName,
+                        item.Is3DModel,
+                        token
+                    );
+                }
+                catch (OperationCanceledException) { break; }
+                catch (Exception ex)
+                {
+                    LogHelper.WriteLogError("延迟保存图像任务出错!", ex);
+                }
+                finally
+                {
+                    try { item.clonedBitmap?.Dispose(); } catch { }
+                    try { item.Image = null; } catch { }
+                }
+            }
+        }
+
+        private void ShowVisionRender_Unloaded(object sender, RoutedEventArgs e)
+        {
+            try { _saveImageCts.Cancel(); _saveImageCts.Dispose(); } catch { }
+
+            // 释放延迟保存列表中的 Bitmap 资源
+            lock (delaySaveImages)
+            {
+                foreach (var item in delaySaveImages)
+                {
+                    try { item.clonedBitmap?.Dispose(); } catch { }
+                    try { item.Image = null; } catch { }
+                }
+                delaySaveImages.Clear();
+            }
+        }
+
         private void ProductChanged(ProductModel product)
         {
             this.gridRender.Dispatcher.BeginInvoke(new Action(() =>