图像格式与转换一文讲透:相机 / OpenCV / Qt

本文把「相机像素格式 → 项目 cam::PixelFormat → OpenCV cv::Mat → Qt QImage/QPixmap」这条转换链上的每一种格式、每一次转换、每一个坑一次讲清楚。


目录

  1. 三套格式体系总览
  2. [相机像素格式(海康 MvGvspPixelType)](#相机像素格式(海康 MvGvspPixelType))
  3. [OpenCV 图像格式(cv::Mat)](#OpenCV 图像格式(cv::Mat))
  4. [Qt 图像格式(QImage / QPixmap)](#Qt 图像格式(QImage / QPixmap))
  5. 三者对照总表
  6. 转换链逐项拆解
  7. [Bayer 去马赛克(Demosaicing)](#Bayer 去马赛克(Demosaicing))
  8. [位深与缩放问题(10/12/16 bit)](#位深与缩放问题(10/12/16 bit))
  9. 内存与拷贝语义
  10. 常见坑汇总

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
  • OpenCVcv::Mat::type(),一个 int 同时编码深度 (每通道字节数)与通道数 ;颜色约定是 BGR(不是 RGB)。
  • QtQImage::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 位版本(BayerRG10BayerRG10_PackedBayerRG12...)。工程把 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::PixelFormatmvsPixelToFormat

MvGvspPixelType 枚举映射到项目自有的 SDK-free 枚举。这一步只做枚举值翻译,不碰像素,让 vision/UI 彻底脱离海康头文件。

6.2 cam::PixelFormat → OpenCV typecvTypeFromPixelFormat

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 FrameDatacv::MatframeDataToMat

这是最核心的一次转换:

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::MatQImagematToQImage

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

关键点:

  1. Bayer 排列必须与相机实际一致,错了会整体偏色(见 §10「偏绿/偏品红」)。
  2. cv::cvtColor 的 Bayer 转换默认输出 BGR...2BGR 后缀),正好匹配工程内部标准。
  3. OpenCV 的 Bayer 转换对 8bit 输入输出 CV_8UC3;对 16bit(10/12/16)输入输出 CV_16UC3不会自动缩放到 8bit,后续显示仍需位深处理(见 §8)。
  4. 本工程用 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 makeFramemake_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/行填充 每行错位(右移、斜条纹) frameDataToMatstride 逐行 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使用教程对照阅读。

相关推荐
不会代码的小猴2 小时前
7. JSON
开发语言·c++·笔记·qt·算法·json
沫璃染墨2 小时前
《Qt从零入门系列(六):信号与槽进阶——从多种连接方式到Lambda表达式》
开发语言·c++·qt·代码规范·设计规范·qt5
恒锐丰小瑞3 小时前
率能SS6810H 32V/1A/双通道H桥驱动芯片,PWM电流控制与混合衰减模式,用于POS打印机/安防相机/机器人
嵌入式硬件·数码相机·机器人·硬件工程
zmzb01034 小时前
Python课后习题训练记录Day194
python·opencv·计算机视觉
恒锐丰小瑞5 小时前
率能SS6812T 40V/1A/双通道H桥驱动芯片,PWM电流控制与混合衰减模式(eTSSOP28),用于POS打印机/安防相机/机器人
单片机·嵌入式硬件·数码相机·机器人
橙橙笔记6 小时前
QT的安装
开发语言·qt·安装·软件
格林威6 小时前
C#图像像素放大:邻域平均、双线性插值实现像素放大的C#实现代码
开发语言·人工智能·数码相机·计算机视觉·c#·视觉检测·工业相机
恒锐丰小瑞7 小时前
率能SS6811H 38V/1.6A/双通道H桥驱动芯片,PWM控制接口与低功耗睡眠模式,用于舞台灯光/安防相机/机器人
单片机·嵌入式硬件·数码相机·机器人
2kπ8 小时前
QT开发笔记
开发语言·笔记·qt