
C#高位深相机图像保存:12位16位图像保存方案,OpenCvSharp和Halcon实现
8位存得好好的,12位16位Bitmap直接废了:高位深图像保存方案
前几篇聊图像存储,评论区有人说:"相机设置里有个Mono12,拍出来的图用Bitmap.Save存了,打开一看全黑,到底怎么回事?"
存成8位没问题,换12位就全黑了。问题出在System.Drawing.Bitmap压根不认识16位灰度。
后来换成OpenCvSharp和Halcon才搞定。
一、为什么需要高位深图像保存?
先搞清楚一个问题:8位图256个灰度级,不够用吗?
精密测量场景,8位确实不够。
一个锂电池极片表面的细微划痕,在16位图里灰度差异可能有30-50个灰度级,肉眼和算法都能轻松识别。但在8位图里,同样一条划痕,因为位深被压缩,灰度差异可能只剩下2-3个灰度级,几乎淹没在背景噪声里。
这不是夸张。16位图有65536个灰度级,8位图只有256个。把16位压成8位,相当于每256个灰度级合并成1个。那些微弱但关键的细节,就在这个合并过程中被抹掉了。
另一个典型场景是强光+暗区同时存在的图像:高光部分需要短曝光才能看清楚,暗区需要长曝光才能看清细节。折中曝光,两头都顾不上。但高位深图可以用一条"曝光时间"把整个动态范围吃下来,高光不溢出、暗区有细节,后期通过调整显示映射来查看不同区域。
高位深图像的三个核心价值:
① 保留更多细节,尤其是暗部。 12位图比8位图多16倍信息量,16位图比8位图多256倍信息量。相机感光能力越来越强,硬件能拍到的细节,存不下来就是浪费。
② 后期处理有余量。 8位图做一次亮度和对比度拉伸,直方图就出现断档(空洞)。16位图做完同样的处理,直方图依然连续。裁剪、旋转、缩放、滤波,8位图每处理一次就损失一层精度,16位图经得起折腾。
③ 跨平台兼容性。 医疗影像(DICOM)、遥感卫星、科研显微镜,这些领域的标准格式全是16位起步。工业检测的数据如果将来要对接这些系统,提前用高位深保存就省了后面重新采集的麻烦。
什么时候必须用高位深?
| 场景 | 是否需要高位深 | 原因 |
|---|---|---|
| 普通外观检测 | 不一定 | 肉眼判断,8位够用 |
| 精密尺寸测量 | 必须 | 微米级测量需要精细灰度梯度 |
| 深度学习训练 | 推荐 | 模型泛化需要更多信息,但很多模型训练时还是会归一化到0-1,信息保留在归一化环节 |
| 高动态范围场景(HDR合成) | 必须 | 多张8位合成HDR,不如直接拍高位深 |
| 激光线扫描/3D点云 | 必须 | 高度信息本身就对应灰度值,精度要求极高 |
| 缺陷追溯/仲裁 | 推荐 | 高位深存档,随时调出复核,不受压缩损失影响 |
二、Bitmap为什么不行
日常用的8位图,每个像素0-255,一共256个灰度级。
12位图是0-4095,16位图是0-65535。信息量差了十几倍到几十倍。
Bitmap的问题在于:它只支持8位灰度(Format8bppIndexed)和24/32位彩色。你把12位数据塞进去,Bitmap会强行截断成8位------高4位直接扔掉,像素值从0-4095被硬压成0-255。有用的信息全丢了,这就是"存出来全黑"的原因。
三、OpenCvSharp方案:直接认16位
OpenCV的Mat原生支持CV_16UC1(16位无符号单通道)。从相机拿到数据后直接创建Mat,保存时指定PNG或TIFF就行。
csharp
using OpenCvSharp;
// 假设相机回调给了IntPtr pData,指向12位或16位原始数据
void Save16BitImage(IntPtr pData, int width, int height, string filePath)
{
// 直接包装成16位Mat,零拷贝
using (Mat mat = new Mat(height, width, MatType.CV_16UC1, pData))
{
// 保存为PNG(无损,支持16位)
Cv2.ImWrite(filePath + ".png", mat);
// 或者保存为TIFF(工业标准)
Cv2.ImWrite(filePath + ".tiff", mat);
}
}
OpenCV的ImWrite会根据扩展名自动选择编码器。PNG和TIFF都支持16位无损存储。
如果相机给的是byte[]而不是IntPtr:
csharp
byte[] imageData = GetImageFromCamera();
// 16位图每两个byte组成一个像素(小端序)
ushort[] pixelData = new ushort[imageData.Length / 2];
Buffer.BlockCopy(imageData, 0, pixelData, 0, imageData.Length);
using (Mat mat = new Mat(height, width, MatType.CV_16UC1))
{
// 逐行写入
System.Runtime.InteropServices.Marshal.Copy(pixelData, 0, mat.Data, pixelData.Length);
Cv2.ImWrite("output.tiff", mat);
}
注意Buffer.BlockCopy是按字节拷贝的,不需要担心大小端问题------它忠实地把每两个byte拼成一个ushort。
如果相机出的是12位(Mono12) :大部分相机SDK输出的12位数据是"左对齐"或"右对齐"存储在16位空间里的。直接按16位保存,高4位或者低4位是无效数据。建议先在MVS或pylon Viewer里确认一下相机的实际存储方式,再决定要不要做移位处理。
四、Halcon方案:WriteImage一把梭
Halcon的HImage原生支持各种位深,保存用WriteImage算子,格式选tiff就行。
csharp
using HalconDotNet;
void Save16BitWithHalcon(IntPtr pData, int width, int height, string filePath)
{
// 从指针创建HImage(16位灰度)
HImage image = new HImage("uint2", width, height, pData);
// 保存为TIFF(Halcon支持所有像素类型)
HOperatorSet.WriteImage(image, "tiff", 0, filePath + ".tif");
// 也可以存PNG
// HOperatorSet.WriteImage(image, "png", 0, filePath + ".png");
image.Dispose();
}
Halcon的WriteImage算子对TIFF格式支持所有HALCON像素类型,包括uint2(16位无符号)。BMP和JPEG只支持8位byte类型,16位图用这两个格式保存会报错或截断。
从相机SDK的HObject直接保存:
如果项目里已经用Halcon的采集接口拿到了HObject:
csharp
HObject ho_Image;
// ... 从相机采集到ho_Image
// 直接保存,Halcon自动识别位深
HOperatorSet.WriteImage(ho_Image, "tiff", 0, "result.tif");
WriteImage的第二个参数"tiff"也可以带压缩选项,比如"tiff lzw"用LZW无损压缩减小文件大小。
五、两种方案对比
| 对比项 | OpenCvSharp | Halcon |
|---|---|---|
| 16位图支持 | ✅ CV_16UC1 | ✅ uint2 |
| 12位处理 | 需手动移位 | 自动识别 |
| 代码量 | 中等 | 少 |
| 依赖 | OpenCV库 | Halcon运行时 |
| 成本 | 免费 | 商业授权 |
| TIFF支持 | ✅ | ✅(更完善) |
| PNG 16位 | ✅ | ✅ |
OpenCvSharp适合已经用了OpenCV、不想额外装Halcon 的项目。Halcon适合已经在用Halcon做算法、顺便存图的项目。
如果项目里既没有OpenCV也没有Halcon,还有一个纯C#的选择:SixLabors.ImageSharp 。它原生支持Gray16类型,纯托管代码,跨平台:
csharp
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
// pixelData是ushort[]
using (var image = Image.LoadPixelData<Gray16>(pixelData, width, height))
{
image.SaveAsPng("output.png");
}
六、格式怎么选
TIFF:工业检测的标准格式,完美支持8/10/12/16位,可选LZW无损压缩。Halcon和OpenCV都支持得很好。
PNG:也支持16位无损,文件比TIFF小,兼容性更好。但工业软件对PNG的16位支持不如TIFF普遍。
RAW:纯原始数据,不带任何文件头,文件最小,但需要额外保存宽高和位深信息。
建议:存原始数据用TIFF,给人看用PNG,算法训练用RAW+元数据。
七、几个容易踩的坑
坑一:相机出的是12位,直接当16位存
12位数据通常左对齐或右对齐存放在16位空间里。直接存的话,有效数据只占一部分,另外4位是无效的。建议先在MVS/pylon Viewer里确认存储方式,必要时做移位归一化。
坑二:存成8位图丢了高位信息
Bitmap.Save默认会把16位截断成8位。用OpenCV或Halcon存,别用Bitmap。
坑三:TIFF存了16位,但看图软件打不开
有些看图软件只支持8位TIFF。用ImageJ或专用工业看图软件打开,或者同时生成一份8位预览图。
八、总结
12位16位图像保存,记住三件事:
- Bitmap.Save不能用,它会截断高位数据
- 用OpenCvSharp的CV_16UC1 直接包装指针保存,或者Halcon的WriteImage一把梭
- 格式选TIFF或PNG,别碰JPEG和BMP
把上面任意一段代码塞进你的采集回调里,12位16位的图就能原样保存下来,不丢一比特。
小提示:不确定相机出的12位数据是左对齐还是右对齐,先在MVS或pylon Viewer里存一张16位TIFF,用ImageJ打开看看直方图------如果直方图集中在低端或者高端,说明需要移位。如果均匀分布在0-4095之间,直接存就行。