问题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
噪点是代码处理的问题吗
不是,噪点主要是物理/硬件问题,不是代码处理的问题。
噪点从哪里来
- 物理噪点(主要来源,代码无法避免)
code复制
X 光发射源
↓
X 光穿透被测物体
↓
散射 X 光(杂散光)→ 噪点 ⚠️
↓
探测器接收
↓
探测器电子噪声 → 噪点 ⚠️
↓
形成图像(已有噪点)
这些噪点在图像进入代码之前就已经存在了,代码只能"去噪",不能"避免噪点产生"。
- 代码处理可能"加重"噪点吗?
有可能,如果处理不当:

但当前代码里的处理(均值滤波)是在"去噪",不是"加重噪点"。
- 代码能做什么
代码不能"避免"噪点产生,但可以"减少"噪点影响:
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 控件存在且初始化完,才去操作它,否则直接返回,避免崩溃。


发烧了