本文把「相机像素格式 → 项目
cam::PixelFormat→ OpenCVcv::Mat→ QtQImage/QPixmap」这条转换链上的每一种格式、每一次转换、每一个坑一次讲清楚。
目录
- 三套格式体系总览
- [相机像素格式(海康 MvGvspPixelType)](#相机像素格式(海康 MvGvspPixelType))
- [OpenCV 图像格式(cv::Mat)](#OpenCV 图像格式(cv::Mat))
- [Qt 图像格式(QImage / QPixmap)](#Qt 图像格式(QImage / QPixmap))
- 三者对照总表
- 转换链逐项拆解
- [Bayer 去马赛克(Demosaicing)](#Bayer 去马赛克(Demosaicing))
- [位深与缩放问题(10/12/16 bit)](#位深与缩放问题(10/12/16 bit))
- 内存与拷贝语义
- 常见坑汇总
1. 三套格式体系总览
一张图看懂三者各自的「坐标系」:
海康相机(MVS) 中间层 OpenCV Qt
MvGvspPixelType cam::PixelFormat cv::Mat type QImage::Format
───────────────── ───────────────── ───────────── ─────────────
PixelType_Gvsp_ (SDK-free 枚举) CV_<depth><channels> Format_*
Mono8 / Mono10/12/16 → Mono8/10/12/16 CV_8UC1 / CV_16UC1 Format_Grayscale8
BayerRG8/GR8/GB8/BG8 → BayerRG8/... CV_8UC1(→去马赛克BGR) Format_RGB888
RGB8_Packed → RGB8 CV_8UC3(→BGR2RGB) Format_RGB888
BGR8_Packed → BGR8 CV_8UC3 Format_RGB888
RGBA8/BGRA8_Packed → RGBA8/BGRA8 CV_8UC4(→BGR2RGB) Format_RGB888
YUV422_YUYV → YUV422_8 CV_8UC2(→YUV2BGR) Format_RGB888
关键认知:
- 相机 用
MvGvspPixelType(GenICam SFNC 命名),描述「传感器原始输出是什么」,重点是 Bayer / 单色 / 位深 / Packed。 - OpenCV 用
cv::Mat::type(),一个int同时编码深度 (每通道字节数)与通道数 ;颜色约定是 BGR(不是 RGB)。 - Qt 用
QImage::Format,面向显示/绘制 ,颜色约定是 RGB,并有一套「字节序 + Alpha 位置」的命名法。
三者没有任何一个天然对齐,所以需要一个 FrameConverter 专职桥接。
2. 相机像素格式(海康 MvGvspPixelType)
定义在 SDK 头文件 PixelType.h,类型名 MvGvspPixelType(底层 unsigned int),宏形如 PixelType_Gvsp_*。命名遵循 GenICam SFNC(Standard Features Naming Convention):
PixelType_Gvsp_<家族><位深>[_Packed]
│ │ └─ 可选:Packed = 多像素紧凑打包
│ └─ 每像素总位数
└─ 家族:Mono / BayerXX / RGB8 / BGR8 / RGBA8 / YUV422 ...
2.1 单色(Mono)
| 宏 | 每像素位数 | 字节/像素 | 说明 |
|---|---|---|---|
PixelType_Gvsp_Mono8 |
8 | 1 | 最常用灰度 |
PixelType_Gvsp_Mono10 |
10 | 2(未打包,10bit 左对齐在 2 字节) | 高动态范围灰度 |
PixelType_Gvsp_Mono10_Packed |
10 | 1.25(打包,4 像素占 5 字节) | 省带宽 |
PixelType_Gvsp_Mono12 |
12 | 2(未打包) | |
PixelType_Gvsp_Mono12_Packed |
12 | 1.5(打包) | |
PixelType_Gvsp_Mono16 |
16 | 2 |
Packed vs 未打包 :未打包(如
Mono10)每像素独占 2 字节、高 6 位填 0;Packed(如Mono10_Packed)把多个像素的 10bit 紧挨着排进字节流,节省带宽但解码复杂。本文mvsPixelToFormat把两者都归并到同一个cam::PixelFormat::Mono10,未区分 Packed------这是个已知简化(见 §10)。
2.2 Bayer(拜耳阵列,彩色相机默认输出)
彩色工业相机传感器上每个像素只有一个滤色片(R/G/B 之一),原始输出就是 Bayer 马赛克。宏名里的两个字母表示 2×2 单元第一行的排列:
| 宏 | 第一行 | 第二行 |
|---|---|---|
BayerRG8 |
R G | G B |
BayerGR8 |
G R | B G |
BayerGB8 |
G B | R G |
BayerBG8 |
B G | G R |
每种又有 10/12 位版本(BayerRG10、BayerRG10_Packed、BayerRG12...)。工程把 RG/GR/GB/BG × 8/10/12 共 12 种全列进 cam::PixelFormat)。
2.3 打包彩色(RGB / BGR / RGBA / BGRA)
| 宏 | 通道顺序(内存字节序) | 字节/像素 |
|---|---|---|
PixelType_Gvsp_RGB8_Packed |
R, G, B | 3 |
PixelType_Gvsp_BGR8_Packed |
B, G, R | 3 |
PixelType_Gvsp_RGBA8_Packed |
R, G, B, A | 4 |
PixelType_Gvsp_BGRA8_Packed |
B, G, R, A | 4 |
命名里的顺序就是内存中字节的先后顺序 。
BGR8_Packed恰好与 OpenCV 的 BGR 一致(零转换),RGB8_Packed则需交换 R/B。
2.4 YUV 4:2:2
| 宏 | 说明 |
|---|---|
PixelType_Gvsp_YUV422_Packed |
每 2 像素共享一对色度,4 字节 |
PixelType_Gvsp_YUV422_YUYV_Packed |
YUYV 顺序,即 Y0 U Y1 V |
工程把两者都映射为 cam::PixelFormat::YUV422_8,解码时按 YUYV 处理。
2.5 枚举值的编码(可选深入)
以 PixelType_Gvsp_Mono8 == 0x01080001 为例,32 位值由若干字段拼接:
0x 01 08 00 01
│ │ │ └─ 像素 ID 序号
│ │ └─ 填充字节数(0 = 无填充)
│ └─ 有效位深(0x08 = 8 bit;0x0C = 12 bit packed;0x10 = 16 bit)
└─ 像素家族(0x01 = Mono;0x02 = Bayer/RGB 彩色 ...)
这个编码方便 SDK 在不知道具体类型时反解位深与布局。工程并不直接解析它,而是用 switch 显式映射。
3. OpenCV 图像格式(cv::Mat)
3.1 type() 的编码
cv::Mat::type() 返回一个 int,由宏 CV_<depth><channels> 编码:
cpp
CV_8UC1 // 8 位无符号,1 通道,type()==0
CV_8UC3 // 8 位无符号,3 通道,type()==16
CV_16UC1 // 16 位无符号,1 通道,type()==2
编码公式:type = CV_MAKETYPE(depth, channels) = depth + (channels-1)*8。
深度(depth)取值:
| 宏 | 值 | 含义 | 每通道字节 |
|---|---|---|---|
CV_8U |
0 | 8 位无符号 | 1 |
CV_8S |
1 | 8 位有符号 | 1 |
CV_16U |
2 | 16 位无符号 | 2 |
CV_16S |
3 | 16 位有符号 | 2 |
CV_32S |
4 | 32 位有符号整型 | 4 |
CV_32F |
5 | 32 位浮点 | 4 |
CV_64F |
6 | 64 位浮点 | 8 |
常见组合(本文用到的):
| 类型 | type() | 字节/像素 | 用途 |
|---|---|---|---|
CV_8UC1 |
0 | 1 | Mono8 / Bayer8(去马赛克前) |
CV_8UC2 |
8 | 2 | YUV422(解码前) |
CV_8UC3 |
16 | 3 | BGR 彩色(工程内部标准色) |
CV_8UC4 |
24 | 4 | RGBA/BGRA(解码前) |
CV_16UC1 |
2 | 2 | Mono10/12/16 / Bayer10/12 |
3.2 通道顺序约定:BGR
OpenCV 的彩色 Mat 默认是 BGR 顺序(历史原因,早期与某些库/相机对齐),与 Qt/常见的 RGB 相反。这决定了整个工程里「BGR ↔ RGB」是最高频的一次转换。
3.3 step(行步长)与连续性
cpp
mat.step // 每行字节数(可能 > cols*elemSize,因为有行对齐填充)
mat.isContinuous() // true 表示内存完全连续(无填充)
mat.data // 首地址
mat.total()*mat.elemSize() // 紧密排列时的总字节数
3.4 浅拷贝 vs 深拷贝(引用计数)
cpp
cv::Mat a = ...;
cv::Mat b = a; // 浅拷贝:共享同一块 data,引用计数 +1
cv::Mat c = a.clone(); // 深拷贝:独立分配并复制 data
cv::Mat d; a.copyTo(d);// 深拷贝(也可带掩码/改类型)
跨线程把 Mat 交给别的线程前必须 clone()
4. Qt 图像格式(QImage / QPixmap)
4.1 QImage vs QPixmap
| QImage | QPixmap | |
|---|---|---|
| 定位 | 像素数据的容器,可直接读写/操作像素 | 为屏幕显示优化的栅格图像 |
| 存储 | 独立内存缓冲(隐式共享) | 通常驻留在图形系统/显存侧(Qt6 中为软件栅格) |
| 线程 | 可跨线程操作(隐式共享安全) | 只应在 GUI 线程使用 |
| 转换 | QPixmap::fromImage(qimage) |
pixmap.toImage() |
工程显示路径正是:QImage → QPixmap::fromImage → QGraphicsPixmapItem
4.2 QImage::Format 枚举(常用)
QImage 的每个 Format 描述内存字节布局 + 每像素位数 + Alpha 有无。关键项:
| Format | 值 | 通道/位 | 内存字节顺序 |
|---|---|---|---|
Format_Invalid |
0 | --- | 空 |
Format_Mono |
1 | 1 bit/像素 | 位平面 |
Format_Indexed8 |
3 | 8bit 索引 + 调色板 | 索引 |
Format_RGB32 |
4 | 32bit | 0xffRRGGBB(小端内存实际为 B,G,R,0xff) |
Format_ARGB32 |
5 | 32bit | 同上,但 Alpha 有效 |
Format_RGB888 |
13 | 24bit | R, G, B(本工程彩色显示用) |
Format_RGBA8888 |
17 | 32bit | R, G, B, A(字节序固定,无端序歧义) |
Format_Grayscale8 |
24 | 8bit | 单字节灰度(本工程灰度显示用) |
Format_Grayscale16 |
28 | 16bit | 16 位灰度(Qt 6 新增) |
Format_BGR888 |
29 | 24bit | B, G, R(Qt 6.7+ 新增,可直接接 OpenCV BGR 零拷贝) |
注意
Format_RGB32/Format_ARGB32的「0xffRRGGBB」是按大端写的逻辑值 ,在 x86 小端内存里实际字节序是B, G, R, 0xff。而Format_RGB888/Format_RGBA8888/Format_BGR888是明确的逐字节顺序 ,没有端序歧义。建议显示时优先用 RGB888/BGR888,别用 RGB32。
4.3 QImage 隐式共享与生命周期
cpp
QImage img(mat.data, w, h, step, QImage::Format_RGB888); // 只是「引用」mat 的内存!
return img.copy(); // 深拷贝:像素搬进 QImage 自管内存,脱离 mat 生命周期
QImage(const uchar*, ...) 构造只是借用外部缓冲 (不拥有它),外部缓冲析构后 QImage 就悬垂。所以工程 matToQImage 每次都 .copy()
5. 三者对照总表
把「相机格式 → 中间枚举 → OpenCV type → QImage format」一次对齐:
相机 MvGvspPixelType |
cam::PixelFormat |
OpenCV type | QImage Format | 转换动作 |
|---|---|---|---|---|
| Mono8 | Mono8 | CV_8UC1 | Format_Grayscale8 | 直接 memcpy,零转换 |
| Mono10 / Mono10_Packed | Mono10 | CV_16UC1 | (无,需缩放到 8bit) | 16→8 位缩放 |
| Mono12 / Mono12_Packed | Mono12 | CV_16UC1 | 同上 | 同上 |
| Mono16 | Mono16 | CV_16UC1 | Format_Grayscale16 | 或缩放到 8bit |
| BayerRG8/GR8/GB8/BG8 | 同名 | CV_8UC1 → cvtColor→CV_8UC3 | Format_RGB888 | 去马赛克 |
| BayerRG10/.../BG12 | 同名 | CV_16UC1 → cvtColor→CV_8UC3 | Format_RGB888 | 去马赛克 + 缩放 |
| RGB8_Packed | RGB8 | CV_8UC3 → COLOR_RGB2BGR | Format_RGB888 | R/B 交换 |
| BGR8_Packed | BGR8 | CV_8UC3 | Format_RGB888 | cvtColor(BGR2RGB) |
| RGBA8_Packed | RGBA8 | CV_8UC4 → COLOR_RGBA2BGR | Format_RGB888 | 去掉 Alpha + R/B 交换 |
| BGRA8_Packed | BGRA8 | CV_8UC4 → COLOR_BGRA2BGR | Format_RGB888 | 去掉 Alpha |
| YUV422_YUYV | YUV422_8 | CV_8UC2 → COLOR_YUV2BGR_YUYV | Format_RGB888 | YUV→BGR 色度上采样 |
总结:工程内部以「BGR 的 CV_8UC3」为彩色标准,以「RGB888 的 QImage」为显示标准。所有彩色输入最终都归一到 BGR(Mat),再统一转 RGB(QImage)。
6. 转换链逐项拆解
6.1 相机原始 → cam::PixelFormat(mvsPixelToFormat)
把 MvGvspPixelType 枚举映射到项目自有的 SDK-free 枚举。这一步只做枚举值翻译,不碰像素,让 vision/UI 彻底脱离海康头文件。
6.2 cam::PixelFormat → OpenCV type(cvTypeFromPixelFormat)
cpp
Mono8 / Bayer×8 → CV_8UC1 // 8 位单通道
Mono10/12/16 / Bayer×10/12 → CV_16UC1 // >8 位单通道 → 16 位
RGB8 / BGR8 → CV_8UC3 // 8 位 3 通道
RGBA8 / BGRA8 → CV_8UC4 // 8 位 4 通道
YUV422_8 → CV_8UC2 // 4:2:2 → 先按 2 通道装下,再解码
6.3 FrameData → cv::Mat(frameDataToMat)
这是最核心的一次转换:
cpp
cv::Mat mat(height, width, cvType); // ① 按目标类型分配新 Mat
memcpy(mat.data, frame.data(), copySize); // ② 深拷贝像素(frame.data() 是 shared_ptr 自持内存)
// ③ 按格式做颜色/布局归一化到 BGR:
Bayer → cvtColor(mat, bgr, COLOR_BayerXX2BGR) // 去马赛克
RGB8 → cvtColor(mat, bgr, COLOR_RGB2BGR) // R/B 交换
RGBA8 → cvtColor(mat, bgr, COLOR_RGBA2BGR) // 去 Alpha + 交换
BGRA8 → cvtColor(mat, bgr, COLOR_BGRA2BGR) // 去 Alpha
YUV422→ cvtColor(mat, bgr, COLOR_YUV2BGR_YUYV) // YUV 解码
Mono/BGR → 原样返回 // 已是原生布局
注意 :frameDataToMat 返回的 Mat 是「新分配 + memcpy」出来的,已经独立于 FrameData ,所以后续可以安全 clone() 或直接跨线程使用。Bayer/RGB/RGBA/YUV 分支会再产生一个 bgr Mat(多一次分配 + 一次 cvtColor)。
6.4 cv::Mat → QImage(matToQImage)
cpp
case CV_8UC1: // 灰度,零转换
QImage img(mat.data, cols, rows, step, QImage::Format_Grayscale8);
return img.copy(); // 深拷贝
case CV_8UC3: // BGR → RGB
cv::Mat rgb; cvtColor(mat, rgb, COLOR_BGR2RGB);
QImage img(rgb.data, cols, rows, step, QImage::Format_RGB888);
return img.copy();
这一步发生了两次「拷贝」:cvtColor 产出一个新 rgb Mat(一次),img.copy() 又搬一次进 QImage 自管内存(二次)。这是显示链路的主要开销点(见 §10)。
6.5 QImage → 屏幕(LivePreviewWidget)
cpp
void setImage(const QImage& image) {
m_currentImage = image; // 隐式共享,浅拷贝
m_imageItem->setPixmap(QPixmap::fromImage(image)); // QImage → QPixmap(栅格化)
m_scene->setSceneRect(m_imageItem->boundingRect()); // 交给 QGraphicsScene/View 渲染
}
6.6 另一条 setImage(const cv::Mat&)(单图加载路径)
逻辑与 6.4 相同,只是内联在 UI 层:
cpp
if (channels()==1) cvtColor(mat, rgb, COLOR_GRAY2RGB);
else if (channels()==3) cvtColor(mat, rgb, COLOR_BGR2RGB);
QImage qimg(rgb.data, cols, rows, step, QImage::Format_RGB888);
m_currentImage = qimg.copy();
6.7 保存到文件
cpp
// cv::Mat → 文件(BGR 内部约定,imwrite 自动处理) [FrameConverter.cpp:91-94]
cv::imwrite(filePath.toStdString(), mat);
// QImage → 文件(LivePreviewWidget 右键菜单)
m_currentImage.save(filePath); // QImage 自带编码
7. Bayer 去马赛克(Demosaicing)
Bayer 是彩色相机最常见原始格式。去马赛克 = 从「每像素只有 R/G/B 之一」的马赛克插值出「每像素 R、G、B 三通道」。
cpp
BayerRG8/10/12 → cv::COLOR_BayerRG2BGR
BayerGR8/10/12 → cv::COLOR_BayerGR2BGR
BayerGB8/10/12 → cv::COLOR_BayerGB2BGR
BayerBG8/10/12 → cv::COLOR_BayerBG2BGR
关键点:
- Bayer 排列必须与相机实际一致,错了会整体偏色(见 §10「偏绿/偏品红」)。
cv::cvtColor的 Bayer 转换默认输出 BGR (...2BGR后缀),正好匹配工程内部标准。- OpenCV 的 Bayer 转换对 8bit 输入输出
CV_8UC3;对 16bit(10/12/16)输入输出CV_16UC3,不会自动缩放到 8bit,后续显示仍需位深处理(见 §8)。 - 本工程用 OpenCV 的
cvtColor做去马赛克;海康 SDK 也有MV_CC_ConvertPixelTypeEx可做同样的事(在相机侧转成BGR8_Packed)。两种方案等价,工程选 OpenCV 侧是为了让 camera 层保持纯净、vision 层可独立移植。
8. 位深与缩放问题(10/12/16 bit)
这是「>8 位相机」最常踩的坑,工程目前没有完整处理。
8.1 位深数据的存储
10/12 bit 数据左对齐存放在 2 字节里:
Mono10 某像素值 511(10bit 全 1)→ 实际存储 0xFFC0(左对齐,低 6 位为 0)
所以读 CV_16UC1 后,数值范围是 0..1023(10bit)映射到 0..65535 的左对齐空间,直接显示会几乎全黑(或按 16bit 解释则偏暗)。
8.2 正确做法(建议)
cpp
// 16 位 → 8 位(按实际位深右移,或等比缩放)
cv::Mat gray8;
mat.convertTo(gray8, CV_8U, 1.0 / (1 << (16 - bitDepth))); // 右移对齐位
// 或简单线性缩放:
// mat.convertTo(gray8, CV_8U, 255.0 / 65535.0);
Bayer10/12 同理:先 cvtColor(BayerXX2BGR) 得到 CV_16UC3,再 convertTo(CV_8UC3, ...) 缩放。
9. 内存与拷贝语义
贯穿整个转换链的「谁拥有内存」问题,是理解正确性的关键:
| 环节 | 操作 | 拷贝语义 |
|---|---|---|
相机 → FrameData |
makeFrame:make_shared<vector> + memcpy |
深拷贝(脱离 SDK 复用缓冲) |
FrameData 传递 |
拷贝 FrameData |
shared_ptr 引用计数 +1,不拷像素 |
FrameData → Mat |
frameDataToMat:新 Mat + memcpy |
深拷贝(Mat 独立于 FrameData) |
| Mat 跨线程 | .clone() |
深拷贝 |
| Mat → QImage | cvtColor + img.copy() |
两次深拷贝 |
| QImage 传递 | 拷贝 QImage |
Qt 隐式共享,写时才分离(copy-on-write) |
| QImage → QPixmap | QPixmap::fromImage |
栅格化(可能隐式转换/拷贝) |
记忆口诀:跨所有权边界必深拷贝;同层传递靠引用计数/隐式共享。
10. 常见坑汇总
| # | 坑 | 现象 | 解决 |
|---|---|---|---|
| 1 | RGB/BGR 搞反 | 整图偏红/偏蓝(人像肤色发蓝最常见) | 彩色进入 OpenCV 前确认是 RGB 还是 BGR;显示前确认 BGR2RGB |
| 2 | Bayer 排列判断错 | 整体偏绿/偏品红 | 用相机实际 PixelFormat 查表,必要时用 SDK 自动检测 |
| 3 | 16 位/10 位直接显示 | 全黑或几乎不可见 | 先缩放到 8bit(见 §8) |
| 4 | Packed 10/12bit 当未打包处理 | 花屏/错位 | mvsPixelToFormat 目前不区分 Packed,需补 Packed 解包 |
| 5 | 忽略 stride/行填充 | 每行错位(右移、斜条纹) | frameDataToMat 按 stride 逐行 memcpy 而非连续整块拷贝 |
| 6 | QImage 引用 cv::Mat 内存后 Mat 析构 | 悬垂指针/乱码/崩溃 | 必须 img.copy()(工程已做) |
| 7 | Format_RGB32 端序 |
颜色通道错乱 | 用 Format_RGB888/Format_BGR888 代替 RGB32 |
| 8 | 每次显示两次拷贝 | 高帧率 CPU 高 | 预分配缓冲或原地交换 R/B 后共享(见《数据流向》§9.4) |
| 9 | 跨线程传 Mat 未 clone | 数据被覆盖/崩溃 | 交给工作线程前 clone() |
| 10 | YUV 顺序(YUYV vs UYVY vs YVYU) | 颜色错乱 | 确认宏是 YUV422_YUYV 还是 YUV422_UYVY,用对应 cvtColor 码 |
附:本项目「图像格式」一图速查
相机硬件 ──DMA──▶ SDK 缓冲 pData(MvGvspPixelType)
│ mvsPixelToFormat() 枚举翻译
▼
cam::PixelFormat(SDK-free 枚举)
│ cvTypeFromPixelFormat() + memcpy
▼
cv::Mat(内部标准:BGR 的 CV_8UC3;灰度 CV_8UC1)
│ 算法 process() 产出 annotatedImage(仍 BGR)
│ matToQImage(): BGR2RGB
▼
QImage(显示标准:Format_RGB888 / Format_Grayscale8)
│ QPixmap::fromImage()
▼
QGraphicsPixmapItem → QGraphicsView → 屏幕
本文可与MVS_SDK使用教程对照阅读。