626:内存流;bitmapImage.CacheOption;类级变量释放;噪点;_halconControl

问题1

怎么理解创建内存流:图片数据先存在内存里,不写磁盘

MemoryStream 是什么

MemoryStream = 存在内存(RAM)里的"虚拟文件"。

code复制 复制代码
普通文件流(FileStream):
    数据 → 内存 → 磁盘文件(.png/.jpg)
                            ↑
                        永久保存

内存流(MemoryStream):
    数据 → 内存(虚拟文件)
                   ↑
               程序退出就没了

为什么不用文件流

方案1:用文件流(❌ 不推荐)

charp复制 复制代码
// 先存到磁盘
bmp.Save(@"C:\temp\xray.png", ImageFormat.Png);

// 再从磁盘加载
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.UriSource = new Uri(@"C:\temp\xray.png");
bitmapImage.EndInit();    

问题:

慢:磁盘读写比内存慢 1000 倍

产生垃圾文件:每次显示图像都生成一个 .png 文件

需要文件权限:程序要有写磁盘的权限

路径问题:临时文件路径要管理,可能冲突

方案2:用内存流(✅ 当前代码)

csharp复制 复制代码
// 直接存在内存
var ms = new MemoryStream();
bmp.Save(ms, ImageFormat.Png);

// 从内存加载
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = ms;  // ← 从内存流加载
bitmapImage.EndInit();

优点:

快:内存读写速度极快

不产生垃圾文件:程序退出,内存自动释放

不需要文件权限:不碰磁盘

线程安全:内存流可以传给其他线程

为什么不能直接用 Bitmap

csharp复制 复制代码
// ❌ WPF 的 Image 控件不认识 Bitmap
_imageControl.Source = bmp;  // ← 编译错误!

原因:

Bitmap 是 System.Drawing 命名空间的(WinForms 用的)

WPF 的 Image.Source 只认 ImageSource(包括 BitmapImage)

所以需要转换:Bitmap → MemoryStream(PNG) → BitmapImage

code 复制代码
Bitmap(WinForms 图片)
    ↓ 像" raw 数据"
    ↓ 需要"翻译"成 WPF 能懂的格式

MemoryStream(内存流)
    ↓ 像"中转站"
    ↓ 把 raw 数据压缩成 PNG(标准格式)

BitmapImage(WPF 图片)
    ↓ 像"最终产品"
    ↓ 可以直接显示到界面

怎么理解bitmapImage.CacheOption = BitmapCacheOption.OnLoad;

好问题,这行代码很关键,不设置会报错。

BitmapImage 的加载机制

BitmapImage 默认是"延迟加载"的 ------ 创建对象时不读流,等到真正要显示到界面时才去读。

问题:流已经释放了

csharp复制 复制代码
var ms = new MemoryStream();
bmp.Save(ms, ImageFormat.Png);
ms.Seek(0, SeekOrigin.Begin);

var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = ms;
// bitmapImage.CacheOption = BitmapCacheOption.OnLoad;  // ← 没设这行
bitmapImage.EndInit();

// ms 离开作用域,被 GC 回收
// 或者显式 ms.Dispose()

_imageControl.Source = bitmapImage;
// ↑ 这里才真正去读流 → 但流已经没了 → 报错 ❌

CacheOption.OnLoad 的作用

csharp复制 复制代码
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;

意思:在 EndInit() 时立即把图片数据全部读进内存并缓存,后续不再依赖流。

code复制 复制代码
设了 OnLoad:
    EndInit() 时 → 立即读完整 PNG 数据到内存
                → 流可以安全释放
                → 显示时从内存缓存读 ✅

没设 OnLoad(默认 OnDemand):
    EndInit() 时 → 不读,只记录"流的位置"
                → 显示时才去读流
                → 但流可能已释放 → 报错 ❌

四个选项对比

行为

OnDemand(默认)

延迟加载,显示时才读流,流必须保持打开

OnLoad

EndInit() 时立即加载并缓存,流可以关

None

未指定,等同于 OnDemand

Default

自动选择,通常是 OnDemand

当前代码为什么必须设 OnLoad

因为用的是 MemoryStream,如果不设 OnLoad:

csharp复制 复制代码
// 当前代码
using var bmp = _mockProvider.GetImage();
var ms = new MemoryStream();
bmp.Save(ms, ImageFormat.Png);
ms.Seek(0, SeekOrigin.Begin);

var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = ms;
// 没设 OnLoad
bitmapImage.EndInit();

// ms 没有被引用,GC 随时可能回收它
// 界面显示时去读 ms → 已经没了 → 空白/报错 ❌

设了 OnLoad 后:

csharp复制 复制代码
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
// ↑ 这里已经把 PNG 数据完整读到内存了

// ms 可以安全释放(或者被 GC 回收)
// 显示时从内存缓存读,不依赖流 ✅

一句话总结

OnLoad = 创建 BitmapImage 时就立即把图片数据全部读进内存并缓存,这样流(MemoryStream)释放了也不影响显示。如果不设,等到显示时才去读流,流已经释放了就会报错。

语法3

_currentImage?.Dispose():释放上一张图像的内存(HALCON 图像很大,必须手动释放) 意思因为大,所以运行完必须手动释放一次,为啥不写在末尾

因为 _currentImage 是字段(类级变量),不是局部变量。

两种写法对比

写法1:写在末尾(❌ 错误)

csharp复制 复制代码
private void ShowXrayImageInternal(double posX, double posY)
{
    // 创建新图像
    _currentImage = HImage.GenImageConst("byte", 640, 480);
    
    // 处理 + 显示
    _halconControl.HalconWindow.DispObj(_currentImage);
    
    // ❌ 写在末尾:刚创建的新图像被释放了!
    _currentImage?.Dispose();
}

问题:

code复制 复制代码
第1次调用:
    _currentImage = 图像A
    → 显示图像A
    → Dispose(图像A)  ← 刚显示的图片被释放了!

第2次调用(用户又点扫描):
    _currentImage?.Dispose()  ← 这时 _currentImage 是 null(刚被释放了)
    → 创建图像B
    → 显示图像B
    → Dispose(图像B)  ← 又释放了!

写法2:写在开头(✅ 正确,当前代码)

csharp复制 复制代码
private void ShowXrayImageInternal(double posX, double posY)
{
    // ✅ 写在开头:释放上一次的旧图像
    _currentImage?.Dispose();
    
    // 创建新图像
    _currentImage = HImage.GenImageConst("byte", 640, 480);
    
    // 处理 + 显示
    _halconControl.HalconWindow.DispObj(_currentImage);
    
    // 末尾不释放,保留在 _currentImage 里
}

为什么正确:

code复制 复制代码
第1次调用:
    _currentImage = null(初始值)
    → Dispose(null)  ← 跳过,没东西可释放
    → 创建图像A,_currentImage = 图像A

第2次调用:
    → Dispose(图像A)  ← 释放上一次的旧图像 ✅
    → 创建图像B,_currentImage = 图像B

第3次调用:
    → Dispose(图像B)  ← 释放上一次的旧图像 ✅
    → 创建图像C,_currentImage = 图像C

核心原因

_currentImage 是"保留上次结果"的字段,不是"临时变量"。

问题4

噪点是代码处理的问题吗

不是,噪点主要是物理/硬件问题,不是代码处理的问题。

噪点从哪里来

  1. 物理噪点(主要来源,代码无法避免)
code复制 复制代码
X 光发射源
    ↓
X 光穿透被测物体
    ↓
散射 X 光(杂散光)→ 噪点 ⚠️
    ↓
探测器接收
    ↓
探测器电子噪声 → 噪点 ⚠️
    ↓
形成图像(已有噪点)

这些噪点在图像进入代码之前就已经存在了,代码只能"去噪",不能"避免噪点产生"。

  1. 代码处理可能"加重"噪点吗?

有可能,如果处理不当:

但当前代码里的处理(均值滤波)是在"去噪",不是"加重噪点"。

  1. 代码能做什么

代码不能"避免"噪点产生,但可以"减少"噪点影响:

code复制 复制代码
原始图像(有噪点)
    ↓
代码处理(去噪算法)
    ↓
图像质量改善 ✅

实际场景

噪点来源(按影响大小排序)

曝光时间太短 → 光子数不足 → 量子噪声(最主要)

X 光电压/电流不稳定 → 信号波动 → 噪点

探测器温度太高 → 电子噪声增加

物体太厚 → X 光穿透不足 → 信噪比低

这些都是硬件/参数问题,代码解决不了,只能调整硬件参数。

代码能做的(有限)

csharp复制// 复制代码
using var denoised = rawImage.MedianImage("circle", 3, 0);

// 代码做不了的:让硬件不产生噪点
// (需要调曝光时间、电压、电流等硬件参数)

一句话总结

噪点主要是物理/硬件问题(X 光散射、探测器噪声、曝光不足),不是代码处理的问题。代码只能"去噪"(改善图像质量),不能"避免噪点产生"。如果噪点太严重,要调硬件参数(曝光时间、电压、电流),不是改代码。

噪点

X 光图像里的噪点是什么

真实物理意义

code复制 复制代码
理想 X 光图像:
    被测物体(芯片/PCB)
        → X 光穿透
        → 探测器接收
        → 形成清晰图像

实际 X 光图像(有噪点):
    除了物体信号
        → 还有散射 X 光(杂散光)
        → 探测器电子噪声
        → 光子噪声(量子噪声)
        → 这些"干扰信号"就是噪点

噪点在图像上长什么样

code复制 复制代码
无噪点(理想):
    [128][128][128][128]  ← 均匀灰度

有噪点(实际):
    [125][131][127][134]  ← 灰度值随机波动
    ↑↑↑
    这些突然偏高/偏低的像素就是噪点

X 光图像的常见噪点类型:

噪点对 检测的影响

场景:BGA 焊球检测

code复制 复制代码
无噪点:
    BGA 焊球边缘清晰
        → 算法能精确测量直径
        → 判断"有空洞"还是"无空洞"

有噪点:
    BGA 焊球边缘模糊 + 噪点
        → 算法误判"有空洞"(其实是噪点)
        → NG 率虚高 ❌

去噪方法对比(HALCON 里)

推荐用什么

取决于检测对象:

语法5

HWindowControl _halconControl _halconControl是啥

好,我用最直白的方式重新讲。

_halconControl 是什么

它就是界面上那块"显示 X 光图像的黑框"。

用比喻理解

code复制 复制代码
MainWindow(主窗口)
    ↓
里面有个"黑色区域"(HALCON 控件)
    ↓
这块区域就是 _halconControl
    ↓
代码里通过 _halconControl
    告诉这块区域"显示什么图像"

就像 WinForms 里的 PictureBox、WPF 里的 Image 控件,只不过 _halconControl 是 HALCON 专用的,能显示 X 光图像。

在代码里的角色

csharp复制 复制代码
// ViewModel 里
private HSmartWindowControl _halconControl;

public void ShowImage()
{
    // 通过 _halconControl 操作界面上的图像区域
    _halconControl.HalconWindow.ClearWindow();   // 清空
    _halconControl.HalconWindow.DispObj(image);  // 显示图像
}

如果没有 _halconControl:

ViewModel 不知道"图像要显示在哪里"

就像你知道要显示图像,但不知道往哪个控件上显示

它和 HImage 的区别

_halconControl = 界面上那块显示 X 光图像的黑框,代码通过它来操作"显示/清空/叠加文字"。没有它,图像不知道往哪里显示。

_halconControl == null || !_halconControl.HalconWindow.IsInitialized()

好,我用最直白的方式重新讲。

_halconControl == null || !_halconControl.HalconWindow.IsInitialized() 是什么

这是"安全检查",防止程序崩溃。

为什么需要这个检查

场景1:_halconControl == null

code复制 复制代码
程序刚启动
    ↓
MainWindow 正在加载
    ↓
XrayImageVM 已经创建了
    ↓
但界面上的 HALCON 控件还没创建完
    ↓
这时收到 AxisPositionReadyMessage → 调用 ShowXrayImageInternal
    ↓
_halconControl.ClearWindow()  ← ❌ 崩溃!_halconControl 是 null

加了检查:

csharp复制 复制代码
if (_halconControl == null) return;  // ← 直接返回,不崩溃 ✅

场景2:!_halconControl.HalconWindow.IsInitialized()

code复制 复制代码
HALCON 控件创建了(不是 null)
    ↓
但 HALCON 窗口还没初始化完(HalconWindow 对象还没准备好)
    ↓
这时调用 _halconControl.HalconWindow.ClearWindow()
    ↓
❌ 崩溃!窗口没初始化完

加了检查:

csharp复制 复制代码
if (!_halconControl.HalconWindow.IsInitialized()) return;  // ← 直接返回,不崩溃 ✅

为什么两个条件都要

因为 null 检查和初始化检查是两步:

csharp复制// 复制代码
if (_halconControl == null) return;
_halconControl.HalconWindow.ClearWindow();  // ← 如果窗口没初始化,这里崩 ❌

// ❌ 如果只检查 IsInitialized
if (!_halconControl.HalconWindow.IsInitialized()) return;  // ← 如果 _halconControl 是 null,这里先崩 ❌

// ✅ 两个都检查(短路求值,安全)
if (_halconControl == null || !_halconControl.HalconWindow.IsInitialized()) return;
// ↑ 如果 _halconControl 是 null,后面的就不执行了(短路)

一句话总结

这行代码是"防御性编程":确保 HALCON 控件存在且初始化完,才去操作它,否则直接返回,避免崩溃。

发烧了