云计算百科
云计算领域专业知识百科平台

计算机视觉01:图像基础 — 像素、通道、色彩空间

计算机视觉01:图像基础 — 像素、通道、色彩空间

图像是光样本的张量。你将使用的每一个视觉模型都从这个事实出发。

学习目标

  • 解释连续场景如何被离散化为像素,以及为什么采样/量化决策决定了所有下游模型的上限
  • 以 NumPy 数组的形式读取、切片和检查图像,并在 HWC 和 CHW 布局之间自如切换
  • 在 RGB、灰度、HSV 和 YCbCr 之间进行转换,并解释每种色彩空间存在的原因
  • 按照 torchvision 的要求执行像素级预处理(归一化、标准化、缩放、通道优先)

问题描述

你将阅读的每篇论文、将下载的每个预训练权重、将调用的每个视觉 API 都假定了一种特定的输入编码。传入 uint8 图像而模型期望 float32,它仍然会运行——并静默地产生垃圾结果。将 BGR 喂给在 RGB 上训练的网络,准确率会骤降十个百分点。给期望通道优先的模型传入通道最后的输入,第一个卷积层会把高度当作特征通道。这些都不会抛出错误。它只会毁掉你的指标,你会花一周时间去寻找一个存在于文件加载方式中的 bug。

卷积并不复杂,一旦你知道它在滑动什么。困难的部分在于"图像"对相机、JPEG 解码器、PIL、OpenCV、torchvision 和 CUDA kernel 来说意味着不同的东西。每个栈都有自己的轴顺序、字节范围和通道约定。一个不能理清这些的视觉工程师会交付有问题的流水线。

本课修复基础,以便本阶段的其余部分可以在此基础上构建。到最后,你将知道像素是什么、为什么每个像素有三个数字而不是一个、"用 ImageNet 统计量标准化"实际上做了什么、以及如何在本阶段其他每节课都会假设的两种或三种布局之间切换。

概念讲解

完整预处理流水线一览

每个生产视觉系统都是相同的可逆变换序列。一步出错,模型看到的输入就与训练时的不同。

#mermaid-svg-Ewnw9UhPLH6eYfPc{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ewnw9UhPLH6eYfPc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ewnw9UhPLH6eYfPc .error-icon{fill:#552222;}#mermaid-svg-Ewnw9UhPLH6eYfPc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ewnw9UhPLH6eYfPc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ewnw9UhPLH6eYfPc .marker.cross{stroke:#333333;}#mermaid-svg-Ewnw9UhPLH6eYfPc svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ewnw9UhPLH6eYfPc p{margin:0;}#mermaid-svg-Ewnw9UhPLH6eYfPc .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ewnw9UhPLH6eYfPc .cluster-label text{fill:#333;}#mermaid-svg-Ewnw9UhPLH6eYfPc .cluster-label span{color:#333;}#mermaid-svg-Ewnw9UhPLH6eYfPc .cluster-label span p{background-color:transparent;}#mermaid-svg-Ewnw9UhPLH6eYfPc .label text,#mermaid-svg-Ewnw9UhPLH6eYfPc span{fill:#333;color:#333;}#mermaid-svg-Ewnw9UhPLH6eYfPc .node rect,#mermaid-svg-Ewnw9UhPLH6eYfPc .node circle,#mermaid-svg-Ewnw9UhPLH6eYfPc .node ellipse,#mermaid-svg-Ewnw9UhPLH6eYfPc .node polygon,#mermaid-svg-Ewnw9UhPLH6eYfPc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ewnw9UhPLH6eYfPc .rough-node .label text,#mermaid-svg-Ewnw9UhPLH6eYfPc .node .label text,#mermaid-svg-Ewnw9UhPLH6eYfPc .image-shape .label,#mermaid-svg-Ewnw9UhPLH6eYfPc .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ewnw9UhPLH6eYfPc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ewnw9UhPLH6eYfPc .rough-node .label,#mermaid-svg-Ewnw9UhPLH6eYfPc .node .label,#mermaid-svg-Ewnw9UhPLH6eYfPc .image-shape .label,#mermaid-svg-Ewnw9UhPLH6eYfPc .icon-shape .label{text-align:center;}#mermaid-svg-Ewnw9UhPLH6eYfPc .node.clickable{cursor:pointer;}#mermaid-svg-Ewnw9UhPLH6eYfPc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ewnw9UhPLH6eYfPc .arrowheadPath{fill:#333333;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ewnw9UhPLH6eYfPc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ewnw9UhPLH6eYfPc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ewnw9UhPLH6eYfPc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ewnw9UhPLH6eYfPc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ewnw9UhPLH6eYfPc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ewnw9UhPLH6eYfPc .cluster text{fill:#333;}#mermaid-svg-Ewnw9UhPLH6eYfPc .cluster span{color:#333;}#mermaid-svg-Ewnw9UhPLH6eYfPc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ewnw9UhPLH6eYfPc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ewnw9UhPLH6eYfPc rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ewnw9UhPLH6eYfPc .icon-shape,#mermaid-svg-Ewnw9UhPLH6eYfPc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ewnw9UhPLH6eYfPc .icon-shape p,#mermaid-svg-Ewnw9UhPLH6eYfPc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ewnw9UhPLH6eYfPc .icon-shape .label rect,#mermaid-svg-Ewnw9UhPLH6eYfPc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ewnw9UhPLH6eYfPc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ewnw9UhPLH6eYfPc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ewnw9UhPLH6eYfPc :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

Image file(JPEG/PNG)

Decodeuint8 HWC

Convertcolorspace(RGB/BGR/YCbCr)

Resizeshorter side

Center cropmodel size

Divide by 255float32 [0,1]

Subtract meanDivide by std

TransposeHWC → CHW

BatchCHW → NCHW

Model

红色和蓝色的两个方框是 80% 静默失败的来源:缺失的标准化和错误的布局。

像素是采样点,不是方块

相机传感器计算落在微小探测器网格上的光子数量。每个探测器在几分之一秒内积分光线,并发出与击中它的光子数成正比的电压。然后传感器将该电压离散化为整数。一个探测器成为一个像素。

Continuous scene Sensor grid Digital image
(infinite detail) (H x W detectors) (H x W integers)

~~~~~ +–+–+–+–+–+ 210 198 180 155 120
~ ~ ~ | | | | | | 205 195 178 152 118
~ light ~ —-> +–+–+–+–+–+ —-> 200 190 175 150 115
~~~~~ | | | | | | 195 185 170 148 112
+–+–+–+–+–+ 188 180 165 145 108

在这一步会做出两个选择,它们决定了所有下游处理的上限:

  • 空间采样 决定场景每度有多少个探测器。太少,边缘会变得锯齿状(混叠)。太多,存储和计算量会爆炸。
  • 强度量化 决定电压被分成多细的桶。8 位提供 256 个级别,是显示的标准。10、12、16 位提供更平滑的梯度,对医学成像、HDR 和原始传感器流水线很重要。

像素不是一个有面积的彩色方块。它是一个单一的测量值。当你缩放或旋转时,你是在重新采样那个测量网格。

为什么是三个通道

一个探测器在整个可见光谱范围内计数光子——那就是灰度。为了获得颜色,传感器用红、绿、蓝滤镜的马赛克覆盖网格。去马赛克后,每个空间位置有三个整数:附近红色滤镜探测器、绿色滤镜探测器和蓝色滤镜探测器的响应。这三个整数就是像素的 RGB 三元组。

One pixel in memory:

(R, G, B) = (210, 140, 30) <- reddish-orange

An H x W RGB image:

shape (H, W, 3) stored as H rows of W pixels of 3 values
each in [0, 255] for uint8

三不是什么神奇的数字。深度相机添加 Z 通道。卫星添加红外和紫外波段。医学扫描通常只有一个通道(X光、CT)或很多通道(高光谱)。通道数是最后一个轴;卷积层学习在它上面进行混合。

两种布局约定:HWC 和 CHW

同一个张量,两种排列方式。每个库选择一种。

HWC (height, width, channels) CHW (channels, height, width)

W -> H ->
+—–+—–+—–+ +—–+—–+
H |R G B|R G B|R G B| C |R R R R R R|
+—–+—–+—–+ | +—–+—–+
|R G B|R G B|R G B| v |G G G G G G|
+—–+—–+—–+ +—–+—–+
|B B B B B B|
+—–+—–+

PIL, OpenCV, matplotlib, PyTorch, most deep learning
almost every image file on disk frameworks, cuDNN kernels

CHW 的存在是因为卷积核在 H 和 W 上滑动。将通道轴放在前面意味着每个核在每个通道上看到一个连续的二维平面,这可以很好地向量化。磁盘格式保持 HWC 是因为那与传感器输出扫描线的方式匹配。

你会敲一千次的一行转换:

img_chw = img_hwc.transpose(2, 0, 1) # NumPy
img_chw = img_hwc.permute(2, 0, 1) # PyTorch tensor

内存布局,可视化:

#mermaid-svg-4aZYKz0EFviScPPV{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4aZYKz0EFviScPPV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4aZYKz0EFviScPPV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4aZYKz0EFviScPPV .error-icon{fill:#552222;}#mermaid-svg-4aZYKz0EFviScPPV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4aZYKz0EFviScPPV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4aZYKz0EFviScPPV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4aZYKz0EFviScPPV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4aZYKz0EFviScPPV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4aZYKz0EFviScPPV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4aZYKz0EFviScPPV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4aZYKz0EFviScPPV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4aZYKz0EFviScPPV .marker.cross{stroke:#333333;}#mermaid-svg-4aZYKz0EFviScPPV svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4aZYKz0EFviScPPV p{margin:0;}#mermaid-svg-4aZYKz0EFviScPPV .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-4aZYKz0EFviScPPV .cluster-label text{fill:#333;}#mermaid-svg-4aZYKz0EFviScPPV .cluster-label span{color:#333;}#mermaid-svg-4aZYKz0EFviScPPV .cluster-label span p{background-color:transparent;}#mermaid-svg-4aZYKz0EFviScPPV .label text,#mermaid-svg-4aZYKz0EFviScPPV span{fill:#333;color:#333;}#mermaid-svg-4aZYKz0EFviScPPV .node rect,#mermaid-svg-4aZYKz0EFviScPPV .node circle,#mermaid-svg-4aZYKz0EFviScPPV .node ellipse,#mermaid-svg-4aZYKz0EFviScPPV .node polygon,#mermaid-svg-4aZYKz0EFviScPPV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4aZYKz0EFviScPPV .rough-node .label text,#mermaid-svg-4aZYKz0EFviScPPV .node .label text,#mermaid-svg-4aZYKz0EFviScPPV .image-shape .label,#mermaid-svg-4aZYKz0EFviScPPV .icon-shape .label{text-anchor:middle;}#mermaid-svg-4aZYKz0EFviScPPV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4aZYKz0EFviScPPV .rough-node .label,#mermaid-svg-4aZYKz0EFviScPPV .node .label,#mermaid-svg-4aZYKz0EFviScPPV .image-shape .label,#mermaid-svg-4aZYKz0EFviScPPV .icon-shape .label{text-align:center;}#mermaid-svg-4aZYKz0EFviScPPV .node.clickable{cursor:pointer;}#mermaid-svg-4aZYKz0EFviScPPV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4aZYKz0EFviScPPV .arrowheadPath{fill:#333333;}#mermaid-svg-4aZYKz0EFviScPPV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4aZYKz0EFviScPPV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4aZYKz0EFviScPPV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4aZYKz0EFviScPPV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4aZYKz0EFviScPPV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4aZYKz0EFviScPPV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4aZYKz0EFviScPPV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4aZYKz0EFviScPPV .cluster text{fill:#333;}#mermaid-svg-4aZYKz0EFviScPPV .cluster span{color:#333;}#mermaid-svg-4aZYKz0EFviScPPV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4aZYKz0EFviScPPV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4aZYKz0EFviScPPV rect.text{fill:none;stroke-width:0;}#mermaid-svg-4aZYKz0EFviScPPV .icon-shape,#mermaid-svg-4aZYKz0EFviScPPV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4aZYKz0EFviScPPV .icon-shape p,#mermaid-svg-4aZYKz0EFviScPPV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4aZYKz0EFviScPPV .icon-shape .label rect,#mermaid-svg-4aZYKz0EFviScPPV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4aZYKz0EFviScPPV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4aZYKz0EFviScPPV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4aZYKz0EFviScPPV :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

transpose(2, 0, 1)

transpose(1, 2, 0)

CHW — channels stored as stacked planes (PyTorch, cuDNN)

plane R: entire H x W of red values

plane G: entire H x W of green values

plane B: entire H x W of blue values

HWC — pixels stored interleaved (PIL, OpenCV, JPEG)

row 0: R G B | R G B | R G B …

row 1: R G B | R G B | R G B …

row 2: R G B | R G B | R G B …

字节范围和数据类型

三种约定占主导地位:

约定dtype范围你在哪里看到它
原始 uint8 [0, 255] 磁盘上的文件、PIL、OpenCV 输出
归一化 float32 [0.0, 1.0] 执行 img.astype('float32') / 255 之后
标准化 float32 大约 [-2, +2] 减去均值并除以标准差之后

卷积网络在标准化输入上训练。ImageNet 统计量 mean=[0.485, 0.456, 0.406]、std=[0.229, 0.224, 0.225] 是完整 ImageNet 训练集三个通道的算术均值和标准差,在 [0, 1] 归一化像素上计算。将原始 uint8 喂给期望标准化浮点数的模型是应用视觉中最常见的静默失败。

色彩空间及其存在的原因

RGB 是捕获格式,但不总是模型最有用的表示。

RGB HSV YCbCr / YUV

R red H hue (angle 0-360) Y luminance (brightness)
G green S saturation (0-1) Cb chroma blue-yellow
B blue V value/brightness (0-1) Cr chroma red-green

Linear to Separates color from Separates brightness from
sensor output brightness. Useful for color. JPEG and most video
color thresholding, UI codecs compress the chroma
sliders, simple filters channels harder because the
human eye is less sensitive
to chroma detail than to Y.

对于大多数现代 CNN,你喂入 RGB。你会遇到其他空间的情况:

  • HSV — 传统 CV 代码、基于颜色的分割、白平衡。
  • YCbCr — 读取 JPEG 内部结构、视频流水线、仅在 Y 上操作的超分辨率模型。
  • 灰度 — OCR、文档模型、颜色是干扰变量而非信号的任何情况。

从 RGB 到灰度是加权和,不是平均值,因为人眼对绿色比对红色或蓝色更敏感:

Y = 0.299 R + 0.587 G + 0.114 B (ITU-R BT.601, the classic weights)

宽高比、缩放和插值

每个模型都有固定的输入尺寸(大多数 ImageNet 分类器为 224×224,现代检测器为 384×384 或 512×512)。你的图像很少匹配。三种重要的缩放选择:

  • 缩放短边,然后中心裁剪 — 标准 ImageNet 方案。保持宽高比,丢弃一条边缘像素。
  • 缩放并填充 — 保持宽高比和每个像素,添加黑边。检测和 OCR 的标准做法。
  • 直接缩放到目标尺寸 — 拉伸图像。代价低,扭曲几何,对许多分类任务来说没问题。

插值方法决定当新网格与旧网格不对齐时如何计算中间像素:

Nearest neighbour fastest, blocky, only choice for masks/labels
Bilinear fast, smooth, default for most image resizing
Bicubic slower, sharper on upscaling
Lanczos slowest, best quality, used for final display

经验法则:训练用 bilinear,需要查看的资源用 bicubic 或 lanczos,包含整数类 ID 的任何内容用 nearest。

动手构建

第 1 步:加载图像并检查其形状

使用 Pillow 加载任意 JPEG 或 PNG,转换为 NumPy,并打印你得到的内容。对于可离线运行的确定性示例,可以合成一个。

import numpy as np
from PIL import Image

def synthetic_rgb(h=128, w=192, seed=0):
rng = np.random.default_rng(seed)
yy, xx = np.meshgrid(np.linspace(0, 1, h), np.linspace(0, 1, w), indexing="ij")
r = (np.sin(xx * 6) * 0.5 + 0.5) * 255
g = yy * 255
b = (1 yy) * xx * 255
rgb = np.stack([r, g, b], axis=1) + rng.normal(0, 6, (h, w, 3))
return np.clip(rgb, 0, 255).astype(np.uint8)

arr = synthetic_rgb()
# Or load from disk:
# arr = np.asarray(Image.open("your_image.jpg").convert("RGB"))

print(f"type: {type(arr).__name__}")
print(f"dtype: {arr.dtype}")
print(f"shape: {arr.shape} # (H, W, C)")
print(f"min: {arr.min()}")
print(f"max: {arr.max()}")
print(f"pixel at (0, 0): {arr[0, 0]}")

预期输出:shape: (H, W, 3)、dtype: uint8、范围 [0, 255]。这是规范的本磁盘上表示,无论字节来自相机、JPEG 解码器还是合成生成器。

第 2 步:分离通道并重排布局

分别提取 R、G、B,然后将 HWC 转换为 CHW 以供 PyTorch 使用。

R = arr[:, :, 0]
G = arr[:, :, 1]
B = arr[:, :, 2]
print(f"R shape: {R.shape}, mean: {R.mean():.1f}")
print(f"G shape: {G.shape}, mean: {G.mean():.1f}")
print(f"B shape: {B.shape}, mean: {B.mean():.1f}")

arr_chw = arr.transpose(2, 0, 1)
print(f"\\nHWC shape: {arr.shape}")
print(f"CHW shape: {arr_chw.shape}")

三个灰度平面,每个通道一个。CHW 只是重新排列轴;当内存布局允许时,严格来说不需要数据复制。

第 3 步:灰度和 HSV 转换

加权和灰度,然后手动从 RGB 转 HSV。

def rgb_to_grayscale(rgb):
weights = np.array([0.299, 0.587, 0.114], dtype=np.float32)
return (rgb.astype(np.float32) @ weights).astype(np.uint8)

def rgb_to_hsv(rgb):
rgb_f = rgb.astype(np.float32) / 255.0
r, g, b = rgb_f[..., 0], rgb_f[..., 1], rgb_f[..., 2]
cmax = np.max(rgb_f, axis=1)
cmin = np.min(rgb_f, axis=1)
delta = cmax cmin

h = np.zeros_like(cmax)
mask = delta > 0
rmax = mask & (cmax == r)
gmax = mask & (cmax == g)
bmax = mask & (cmax == b)
h[rmax] = ((g[rmax] b[rmax]) / delta[rmax]) % 6
h[gmax] = ((b[gmax] r[gmax]) / delta[gmax]) + 2
h[bmax] = ((r[bmax] g[bmax]) / delta[bmax]) + 4
h = h * 60.0

s = np.where(cmax > 0, delta / cmax, 0)
v = cmax
return np.stack([h, s, v], axis=1)

gray = rgb_to_grayscale(arr)
hsv = rgb_to_hsv(arr)
print(f"gray shape: {gray.shape}, range: [{gray.min()}, {gray.max()}]")
print(f"hsv shape: {hsv.shape}")
print(f"hue range: [{hsv[..., 0].min():.1f}, {hsv[..., 0].max():.1f}] degrees")
print(f"sat range: [{hsv[..., 1].min():.2f}, {hsv[..., 1].max():.2f}]")
print(f"val range: [{hsv[..., 2].min():.2f}, {hsv[..., 2].max():.2f}]")

色调以度为单位输出,饱和度和明度在 [0, 1] 范围内。这与 OpenCV 的 hsv_full 约定一致。

第 4 步:归一化、标准化及其逆向操作

从原始字节到预训练 ImageNet 模型期望的精确张量,然后再转回来。

mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)

def preprocess_imagenet(rgb_uint8):
x = rgb_uint8.astype(np.float32) / 255.0
x = (x mean) / std
x = x.transpose(2, 0, 1)
return x

def deprocess_imagenet(chw_float32):
x = chw_float32.transpose(1, 2, 0)
x = x * std + mean
x = np.clip(x * 255.0, 0, 255).astype(np.uint8)
return x

x = preprocess_imagenet(arr)
print(f"preprocessed shape: {x.shape} # (C, H, W)")
print(f"preprocessed dtype: {x.dtype}")
print(f"preprocessed mean per channel: {x.mean(axis=(1, 2)).round(3)}")
print(f"preprocessed std per channel: {x.std(axis=(1, 2)).round(3)}")

roundtrip = deprocess_imagenet(x)
max_diff = np.abs(roundtrip.astype(int) arr.astype(int)).max()
print(f"roundtrip max pixel diff: {max_diff} # should be 0 or 1")

每通道均值应接近零,标准差接近一。preprocess/deprocess 对正是每个 torchvision transforms.Normalize 调用在底层所做的事情。

第 5 步:用三种插值方法缩放

在放大时比较最近邻、双线性和双三次,使差异可见。

target = (arr.shape[0] * 3, arr.shape[1] * 3)

nearest = np.asarray(Image.fromarray(arr).resize(target[::1], Image.NEAREST))
bilinear = np.asarray(Image.fromarray(arr).resize(target[::1], Image.BILINEAR))
bicubic = np.asarray(Image.fromarray(arr).resize(target[::1], Image.BICUBIC))

def local_roughness(x):
gy = np.diff(x.astype(float), axis=0)
gx = np.diff(x.astype(float), axis=1)
return float(np.abs(gy).mean() + np.abs(gx).mean())

for name, out in [("nearest", nearest), ("bilinear", bilinear), ("bicubic", bicubic)]:
print(f"{name:>8} shape={out.shape} roughness={local_roughness(out):6.2f}")

最近邻在粗糙度上得分最高,因为它保留了硬边缘。双线性最平滑。双三次介于两者之间,在不产生阶梯伪影的情况下保留感知锐度。

实际应用

torchvision.transforms 将上述所有内容打包成一个可组合的流水线。以下代码精确复现了 preprocess_imagenet 的功能,加上缩放和裁剪。

import torch
from torchvision import transforms
from PIL import Image

img = Image.fromarray(synthetic_rgb(256, 256))

pipeline = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

x = pipeline(img)
print(f"tensor type: {type(x).__name__}")
print(f"tensor dtype: {x.dtype}")
print(f"tensor shape: {tuple(x.shape)} # (C, H, W)")
print(f"per-channel mean: {x.mean(dim=(1, 2)).tolist()}")
print(f"per-channel std: {x.std(dim=(1, 2)).tolist()}")

batch = x.unsqueeze(0)
print(f"\\nbatched shape: {tuple(batch.shape)} # (N, C, H, W) — ready for a model")

四步,严格按照此顺序:Resize(256) 将较短边缩放到 256;CenterCrop(224) 从中间取 224×224 的补丁;ToTensor() 除以 255 并将 HWC 转换为 CHW;Normalize 减去 ImageNet 均值并除以标准差。颠倒该顺序会静默地改变到达模型的内容。

交付物

本课产生:

  • outputs/prompt-vision-preprocessing-audit.md — 一个提示,可将任何模型卡或数据集卡转化为团队必须遵守的精确预处理不变量检查清单。
  • outputs/skill-image-tensor-inspector.md — 一个技能,给定任何图像形状的张量或数组,报告 dtype、布局、范围,以及它看起来是原始的、归一化的还是标准化的。

练习

  • (简单) 用 OpenCV(cv2.imread)和 Pillow 分别加载一个 JPEG。打印两者的形状和 (0, 0) 处的像素。解释通道顺序的差异,然后写一行转换使 OpenCV 数组与 Pillow 数组相同。
  • (中等) 编写 standardize(img, mean, std) 及其逆函数,两者联合在任意 uint8 图像上通过 roundtrip_max_diff <= 1 测试。你的函数必须在同一次调用中既适用于 HWC 的单张图像,也适用于 NCHW 的批次。
  • (困难) 取一个 3 通道 ImageNet 标准化张量,通过一个 1×1 卷积运行,该卷积学习 RGB 到单个灰度通道的加权和。将权重初始化为 [0.299, 0.587, 0.114],冻结它们,并验证输出与手动 rgb_to_grayscale 在浮点误差范围内匹配。还有哪些经典色彩空间变换可以写成 1×1 卷积?
  • 关键术语

    术语人们常说的实际含义
    像素 “一个彩色方块” 一个网格位置上光强度的一次采样——彩色时三个数字,灰度时一个
    通道 “颜色” 堆叠到图像张量中的平行空间网格之一;HWC 中为最后一个轴,CHW 中为第一个
    HWC / CHW “形状” 图像张量的轴顺序;磁盘和 PIL 使用 HWC,PyTorch 和 cuDNN 使用 CHW
    归一化 “缩放图像” 除以 255 使像素落在 [0, 1] — 必要但不充分
    标准化 “零中心化” 按通道减去均值并除以标准差,使输入分布与模型训练时的匹配
    灰度转换 “对通道取平均” 系数为 0.299/0.587/0.114 的加权和,匹配人眼亮度感知
    插值 “缩放如何选取像素” 当新网格与旧网格不对齐时决定输出值的规则——标签用 nearest,训练用 bilinear,显示用 bicubic
    宽高比 “宽度除以高度” 区分"缩放并填充"和"缩放并拉伸"的比率
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 计算机视觉01:图像基础 — 像素、通道、色彩空间
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!