all lessons / computer_vision / 01 · image representation lesson 1 / 19

Image representation, pixels, color, coordinates

图像表示、像素、颜色与坐标

The track index framed CV as four questions — what signal is in the data, what bias the model needs, which task head follows, what system ships it. Everything downstream consumes one object: a sampled, quantized, noisy grid of numbers. Before we learn to read it (lesson 02 onward), we have to know exactly what it is, what was thrown away making it, and which silent conventions around it cause the bulk of real production bugs.

课程索引把计算机视觉归结为四个问题——数据里有什么信号、模型需要什么偏置、后面接什么任务头、用什么系统把它落地。下游的一切都消费同一个对象:一个经过采样、量化、带噪声的数字网格。在学会读它之前(从第 02 课起),我们必须清楚它到底是什么、在生成它的过程中丢掉了什么,以及围绕它的哪些隐性约定造成了绝大部分真实的生产环境 bug。

The plan计划
Five moves. (1) Trace how a continuous scene becomes a discrete tensor, and name what each step discards. (2) Make sampling and aliasing concrete — when the grid is too coarse, high-frequency content lies to you (this is the deep reason lesson 02 must blur before it shrinks). (3) Pin down the tensor itself: shape, dtype, range, channel order. (4) Color: why pixel values are not linear light (gamma/sRGB), and why HSV goes haywire near gray. (5) Coordinate and box conventions (xyxy vs xywh) — the boring stuff that breaks pipelines. We close on the rule that ties the whole track together: preprocessing is part of the model contract. 五步走。(1) 追踪一个连续场景如何变成离散张量,并指出每一步丢掉了什么。(2) 把采样与混叠讲具体——当网格太粗时,高频内容会欺骗你(这正是第 02 课在缩小前必须先模糊的深层原因)。(3) 钉死张量本身:形状、dtype、取值范围、通道顺序。(4) 颜色:为什么像素值不是线性光(伽马/sRGB),以及为什么 HSV 在接近灰色时会失控。(5) 坐标与边界框约定(xyxy 与 xywh)——那些看似无聊却会搞垮流水线的东西。最后我们落到贯穿整门课的一条规则:预处理是模型契约的一部分。

1 · From a scene to a tensor — what survives

1 · 从场景到张量——什么活了下来

An image is not "the world." It is a measurement of the world, and every measurement loses information. Light from a 3D scene passes through a lens, lands on a sensor, and is turned into an array of numbers by a chain of lossy steps. First-principles CV begins by asking, at each step, what survived.

图像不是"世界"本身。它是对世界的一次测量,而任何测量都会丢失信息。来自三维场景的光穿过镜头、落到传感器上,再经过一连串有损步骤变成一个数字数组。从第一性原理出发的计算机视觉,就是在每一步都追问:什么活了下来

scene ──optics──▶ irradiance(x,y) ──sampling──▶ grid[i,j] ──quantize──▶ 8-bit ints 3D, continuous continuous 2D discrete H×W 256 levels/channel │ │ │ blur, vignetting ALIASING risk rounding noise │ ──demosaic──▶ ──gamma/sRGB encode──▶ ──compress(JPEG)──▶ file ──decode/resize/normalize──▶ model input 1→3 chans perceptual coding drops high freq train≠serve risk

Three losses dominate and recur for the rest of the track:

三种损失最为主要,并会在这门课的其余部分反复出现:

The practical consequence: the same scene yields different tensors depending on the lens, ISO, codec, and resize policy. A robust model must be insensitive to the parts that are measurement artifacts and sensitive to the parts that are scene content — which is the entire game of invariance we will chase from lesson 03 through the self-supervised and multimodal lessons.

现实后果是:同一个场景会因镜头、ISO、编解码器和缩放策略的不同而产生不同的张量。一个鲁棒的模型必须对属于测量伪影的部分不敏感、对属于场景内容的部分敏感——这就是我们从第 03 课一路追到自监督和多模态各课的整个不变性游戏。

2 · Sampling and aliasing — when the grid lies

2 · 采样与混叠——当网格撒谎时

This is the one idea from this lesson that the rest of the track keeps leaning on, so we earn it carefully. Consider a 1D signal — a pattern of stripes — that oscillates as a sinusoid at frequency f (cycles per unit length). We sample it at rate fs (samples per unit length). The Nyquist–Shannon sampling theorem says you can faithfully reconstruct the signal only if you sample more than twice as fast as its highest frequency:

这是本课中被后续内容反复倚重的一个思想,所以我们把它扎实地讲清楚。考虑一个一维信号——一组条纹——它以频率 f(每单位长度的周期数)像正弦波一样振荡。我们以采样率 fs(每单位长度的采样数)对它采样。奈奎斯特–香农采样定理指出,只有当采样速度超过信号最高频率的两倍时,你才能忠实地重建它:

fs > 2·fmax   ⟺   fmax < fs/2   (the Nyquist limit).

Why a factor of two? A sinusoid has two things to pin down per cycle — roughly, its peak and its trough. Fewer than two samples per cycle and you can no longer tell which sinusoid you saw. Worse: a too-fast sinusoid sampled too slowly produces samples that exactly coincide with those of a slower sinusoid. The math says any frequency f above Nyquist gets reflected down to an alias frequency:

为什么是两倍?一条正弦波每个周期有两样东西需要确定——大致就是它的波峰和波谷。每个周期少于两个采样,你就再也分不清自己看到的是哪条正弦波了。更糟的是:一条过快的正弦波若采样过慢,其采样点会与一条更慢的正弦波的采样点完全重合。数学告诉我们,任何高于奈奎斯特频率的 f 都会被反射到一个混叠频率上:

falias = | f − fs · round(f / fs) | .

Worked number. Sample a striped pattern of f = 0.45 cycles/pixel at the pixel grid's natural rate fs = 1 sample/pixel. Nyquist is fs/2 = 0.5, and 0.45 < 0.5, so we are safe: the stripes read correctly. Now make the stripes finer, f = 0.8 > 0.5. The alias is |0.8 − 1·round(0.8)| = |0.8 − 1| = 0.2 cycles/pixel — a slow, fat fake stripe pattern that was never in the scene. The grid is lying to you, and confidently.

算个具体数。用像素网格的自然采样率 fs = 1 采样/像素去采样一组 f = 0.45 周期/像素的条纹。奈奎斯特频率是 fs/2 = 0.5,而 0.45 < 0.5,所以我们是安全的:条纹被正确地读出。现在把条纹变得更细,f = 0.8 > 0.5。混叠频率是 |0.8 − 1·round(0.8)| = |0.8 − 1| = 0.2 周期/像素——一组缓慢而粗大、场景里根本不存在的虚假条纹。网格正在骗你,而且骗得理直气壮。

This is why moiré shimmer appears on a striped shirt in a photo, and why downsampling an image without blurring first creates jaggies and false texture: shrinking the grid lowers fs, dragging the Nyquist limit down below detail that was faithfully sampled before. The fix is to remove the too-high frequencies before you drop samples, by low-pass filtering (blurring). We cannot build that blur yet — it is a convolution — so this is the precise hand-off into lesson 02. Play with the widget below to make a stable pattern collapse into an alias, then watch the anti-alias toggle rescue it.

这就是为什么照片里条纹衬衫上会出现摩尔纹闪烁,以及为什么不先模糊就对图像下采样会产生锯齿和虚假纹理:缩小网格会降低 fs,把奈奎斯特限拉到原本已被忠实采样的细节之下。解决办法是在丢弃采样点之前,用低通滤波(模糊)去掉过高的频率。我们现在还造不出这个模糊——它是一个卷积——所以这里正好精确地交接到第 02 课。玩一玩下面的小部件,把一个稳定的图案逼成混叠,再看看抗混叠开关如何把它救回来。

Sampling & aliasing — push a clean pattern below Nyquist
采样与混叠——把一个干净的图案压到奈奎斯特限以下
Top: the true continuous stripe pattern (sinusoid at signal frequency f). Bottom: what the pixel grid actually records at sample rate fs. Raise f past the Nyquist limit fs/2 and a slow alias pattern appears that was never in the signal. Flip the anti-alias (low-pass) filter on to blur the signal before sampling — the false pattern is killed.
上图:真实的连续条纹图案(频率为信号频率 f 的正弦波)。下图:像素网格在采样率 fs 下实际记录到的东西。把 f 抬过奈奎斯特限 fs/2,就会出现一个信号里从来没有的缓慢混叠图案。打开抗混叠(低通)滤波器,在采样之前先模糊信号——虚假图案就被消灭了。
Signal f
信号 f
0.30
Nyquist fs/2
奈奎斯特 fs/2
0.50
Aliased?
是否混叠?
no
Apparent freq
表观频率
0.30
Show the core JS查看核心代码
// continuous signal, sampled at rate fs, optionally low-passed first
const NY = fs / 2;                       // Nyquist limit
const aliased = f > NY;                   // will the grid lie?
const fApparent = aliased
  ? Math.abs(f - fs * Math.round(f / fs)) // reflected-down alias frequency
  : f;

function sampleAt(x){                     // value the grid records at position x
  let fEff = f;
  if (antiAlias && f > NY) fEff = 0;       // low-pass removes >Nyquist content → flat
  return Math.sin(2*Math.PI*fEff*x);
}
// draw true sinusoid (fine), then stems at integer-ish sample positions x = k/fs,
// then reconstruct what the grid "sees" — a sinusoid at fApparent.
Trap陷阱
Aliasing is not blurriness — it is structured fiction. A downsampled logo can grow stripes that fool a classifier or a hash. The defenders: low-pass (blur) before any decimation, use an antialiased resize (area / Lanczos, not raw nearest-neighbor), and never feed a model raw nearest-resized inputs it was not trained on. 混叠不是模糊——它是有结构的虚构。一个下采样后的 logo 可能长出条纹,从而骗过分类器或哈希。防守手段是:任何抽取(decimation)前先做低通(模糊),使用带抗混叠的缩放(area / Lanczos,而不是原始的最近邻),并且绝不要把模型没有训练过的原始 nearest 缩放输入喂给它。

3 · The tensor: shape, dtype, range, channel order

3 · 张量:形状、dtype、取值范围、通道顺序

Concretely, a color image is a 3D array. A worked example you should be able to recite:

具体来说,一张彩色图像就是一个三维数组。下面是一个你应该能背出来的具体例子:

shape
形状
(H, W, C) = (1080, 1920, 3) — HWC layout (NumPy/OpenCV); deep-learning tensors are usually CHW, and batched NCHW
(H, W, C) = (1080, 1920, 3) —— HWC 布局(NumPy/OpenCV);深度学习的张量通常是 CHW,加上批量维后是 NCHW
dtype
dtype
uint8 from the decoder (0–255), or float32 after normalization
解码器给出的是 uint8(0–255),或归一化之后的 float32
range
取值范围
[0, 255] raw → often /255 → [0, 1] → (x−μ)/σ per-channel → roughly [−2.6, 2.6]
原始 [0, 255] → 通常 /255 → [0, 1] → 逐通道 (x−μ)/σ → 大致 [−2.6, 2.6]
channels
通道
RGB (most ML) vs BGR (OpenCV default) — a silent swap of the R and B planes
RGB(多数 ML)与 BGR(OpenCV 默认)—— R 与 B 两个平面被悄悄互换

The numbers matter. A normalization step is just an affine rescale x' = (x − μ)/σ applied per channel; it changes the scale the optimizer sees (and therefore gradient magnitudes and learning-rate sensitivity) but carries the same information as the raw pixels. The "roughly [−2.6, 2.6]" above is not magic: ImageNet's standard per-channel stats are about μ ≈ 0.45, σ ≈ 0.225, so a black pixel maps to (0 − 0.45)/0.225 ≈ −2.0 and a white one to (1 − 0.45)/0.225 ≈ +2.4; spread over the three channels' slightly different μ/σ, the extremes land near ±2.6. Mixing the three ranges — feeding [0,255] ints into a model trained on mean/std-normalized floats — does not crash; it silently destroys accuracy because the first conv layer's learned edge/color statistics are calibrated to a distribution the input no longer matches. We will see in lesson 06 that early conv filters literally learn the input scale, which is why the mismatch bites at layer one.

这些数字很重要。归一化步骤只是逐通道施加的一个仿射缩放 x' = (x − μ)/σ;它改变了优化器看到的尺度(从而改变梯度幅值和对学习率的敏感度),但携带的信息与原始像素完全相同。上面那个"大致 [−2.6, 2.6]"并不神秘:ImageNet 的标准逐通道统计量约为 μ ≈ 0.45σ ≈ 0.225,所以一个黑色像素映射为 (0 − 0.45)/0.225 ≈ −2.0,白色像素映射为 (1 − 0.45)/0.225 ≈ +2.4;分摊到三个通道略有差异的 μ/σ 上,极值就落在 ±2.6 附近。把这三种范围混用——把 [0,255] 的整数喂给一个在均值/标准差归一化浮点上训练出来的模型——不会崩溃;它会悄无声息地摧毁精度,因为第一层卷积学到的边缘/颜色统计特性是针对某个分布校准的,而输入不再匹配那个分布。我们将在第 06 课看到,早期卷积滤波器实际上学到了输入的尺度,这正是这种不匹配会在第一层就咬人的原因。

The BGR foot-gunBGR 这把"走火枪"
OpenCV's imread returns BGR, but almost every model and visualization assumes RGB. Forget the swap and reds become blues — accuracy quietly sags rather than erroring, so it survives code review. This single bug is responsible for a startling fraction of "the model works in the notebook but not in prod" reports. OpenCV 的 imread 返回的是 BGR,但几乎所有模型和可视化都假设是 RGB。忘了做这个互换,红色就会变成蓝色——精度会悄悄下滑而不会报错,于是它顺利通过了代码评审。仅这一个 bug,就要为惊人比例的"模型在 notebook 里能跑、在生产里却不行"的报告负责。

4 · Color is not linear light — gamma, sRGB, and HSV

4 · 颜色不是线性光——伽马、sRGB 与 HSV

Here is a subtlety that surprises people: the 8-bit value 128 is not half the light of 255. Pixel values are stored in a perceptually-coded space, not in linear physical intensity. Human vision is far more sensitive to dark tones than bright ones, so cameras and files spend more code values on shadows by applying a nonlinear gamma curve. The standard sRGB encoding is approximately a power law:

这里有一个让人意外的微妙之处:8 位数值 128 并不是 255 的一半亮度。像素值存储在一个感知编码的空间里,而不是线性的物理强度里。人眼对暗调远比对亮调敏感,所以相机和文件通过施加一条非线性的伽马曲线,把更多的码值花在阴影上。标准的 sRGB 编码近似是一个幂律:

Vencoded ≈ Llinear 1/2.2   (and decode L ≈ V2.2), with L, V ∈ [0,1].

Worked number. A surface emitting half the linear light, L = 0.5, is stored as V = 0.51/2.2 ≈ 0.73 — i.e. about 186/255, not 128. So the midpoint code 128 is actually only (128/255)2.2 ≈ 0.22 of full linear intensity, roughly a fifth.

算个具体数。一个发出一半线性光的表面,L = 0.5,被存成 V = 0.51/2.2 ≈ 0.73——也就是约 186/255,而不是 128。所以中点码值 128 实际上只有满线性强度的 (128/255)2.2 ≈ 0.22,大约五分之一。

Why you must care: any operation that is supposed to model physics — averaging pixels for a correct downscale, alpha-blending/compositing, computing optical flow under brightness change, or resizing — is only correct in linear light. Averaging two sRGB-encoded values gives a result that is too dark, which is why high-quality resamplers decode to linear, average, then re-encode. For most learned models we feed sRGB directly and let the network adapt, but the instant you do hand math on pixels (lesson 02's filters, lesson 05's photometric stereo, lesson 16's flow), the nonlinearity is waiting.

你为什么必须在意:任何本该建模物理的操作——为正确缩小而对像素求平均、alpha 混合/合成、在亮度变化下计算光流,或缩放——都只有在线性光下才是正确的。对两个 sRGB 编码值求平均,得到的结果会偏暗,这正是高质量重采样器先解码到线性、求平均、再重新编码的原因。对大多数学习型模型,我们直接喂 sRGB 并让网络自己去适应,但只要你一动手对像素做数学运算(第 02 课的滤波器、第 05 课的光度立体、第 16 课的光流),这个非线性就在那儿等着你。

Color spaces, and why HSV is treacherous. RGB is display-oriented. YUV/YCbCr separates luma (brightness) from chroma, which is how JPEG and video subsample color cheaply. HSV (Hue, Saturation, Value) is popular for simple color thresholds because "redness" lives on one axis. But hue is an angle, and the angle of a vector is undefined when the vector has near-zero length. Near low saturation — grays, whites, blacks — the RGB channels are nearly equal, so the hue computation divides by a near-zero range and the result becomes numerically unstable and noisy: a gray pixel can report a wildly swinging, meaningless hue, and tiny sensor noise flips it across the color wheel.

色彩空间,以及 HSV 为何暗藏杀机。RGB 是面向显示的。YUV/YCbCr 把亮度(luma)与色度(chroma)分开,这正是 JPEG 和视频得以廉价地对颜色做子采样的方式。HSV(色相、饱和度、明度)在简单颜色阈值分割中很受欢迎,因为"红不红"落在一个单一的轴上。但色相是一个角度,而当一个向量的长度接近零时,它的角度是没有定义的。在低饱和度附近——灰、白、黑——RGB 各通道几乎相等,于是色相计算除以了一个接近零的差值,结果变得数值不稳定且充满噪声:一个灰色像素可能报出剧烈摆动、毫无意义的色相,而极微小的传感器噪声就能让它在色轮上翻来覆去。

H is computed from the spread max(R,G,B) − min(R,G,B); as S = (max−min)/max → 0, H = (small noise)/(≈0) → garbage.

Concrete example. Take a near-gray pixel RGB = (130, 130, 132): the spread is max − min = 132 − 130 = 2, so hue is computed by dividing through that tiny denominator. One unit of sensor noise on any channel (a ±1 wobble) changes which channel is the max and swings the computed hue by tens of degrees across the color wheel — the angle of a vector that has almost no length is meaningless.

具体例子。取一个接近灰色的像素 RGB = (130, 130, 132):差值是 max − min = 132 − 130 = 2,于是色相是通过除以这个极小的分母算出来的。任何一个通道上一单位的传感器噪声(±1 的抖动)都会改变哪个通道是最大值,并让算出来的色相在色轮上摆动几十度——一个几乎没有长度的向量,其角度是没有意义的。

So an HSV color threshold that works on a vivid object falls apart on washed-out, shadowed, or overexposed regions exactly where saturation collapses — a classic reason a "just threshold the hue" demo dies in the real world.

因此,一个在鲜艳物体上好用的 HSV 颜色阈值,会在褪色、阴影或过曝的区域彻底失效——恰恰是饱和度崩塌的地方。这正是"只要对色相做个阈值就行"的演示在真实世界里挂掉的经典原因。

5 · Coordinate and box conventions — the boring bugs

5 · 坐标与边界框约定——那些无聊的 bug

The last category of loss is not physical, it is clerical, and it ships more incidents than any model flaw. The root mismatch: arrays index as img[row, col] = img[y, x], but geometry speaks (x, y) — column first. Swap them and your point lands transposed.

最后一类损失不是物理的,而是文书性质的,而它引发的线上事故比任何模型缺陷都多。根源上的不匹配是:数组的索引方式是 img[row, col] = img[y, x],而几何学讲的是 (x, y)——列在前。把它们弄反,你的点就会落到转置后的位置上。

Bounding boxes carry their own minefield. The same rectangle has several encodings, and libraries disagree on which they expect:

边界框自带一片雷区。同一个矩形有好几种编码方式,而各个库对它们期望的是哪一种意见并不一致:

Format格式Stores存储内容Same box (example)同一个框(示例)Used by使用方
xyxy(xmin, ymin, xmax, ymax) — two corners(xmin, ymin, xmax, ymax) —— 两个角点(100, 50, 260, 210)Pascal VOC, torchvision
xywh(xmin, ymin, width, height) — corner + size(xmin, ymin, width, height) —— 角点 + 尺寸(100, 50, 160, 160)COCO annotations
cxcywh(xcenter, ycenter, width, height)(xcenter, ycenter, width, height)(180, 130, 160, 160)YOLO, DETR (often normalized to [0,1])

Convert xyxy → xywh by w = xmax − xmin, h = ymax − ymin; the corner stays. Mistake the third number for a width when it is actually xmax and every box balloons. Add two more traps: boxes may be normalized to [0,1] (so they survive resizing) or in absolute pixels (so they do not), and the origin pixel may be addressed at its corner (0,0) or its center (0.5, 0.5) — a half-pixel shift that quietly degrades dense tasks like segmentation (lesson 10) and keypoints (lesson 11).

xyxy → xywh 的换算是 w = xmax − xminh = ymax − ymin;角点保持不变。若把第三个数误当成宽度,而它其实是 xmax,那每个框都会膨胀。再加两个陷阱:框可能被归一化[0,1](这样它们能在缩放后存活)或以绝对像素表示(这样就不能),而原点像素可能定位在它的角 (0,0) 或它的中心 (0.5, 0.5)——一个半像素的偏移,会悄悄拖垮像分割(第 10 课)和关键点(第 11 课)这类稠密任务。

And when you resize the image, you must apply the same transform to the labels. Letterbox a 1920×1080 image into a 640×640 square (the standard detector input) and you have scaled by 640/1920 = 0.333 and padded 107 px top and bottom; every box coordinate must be multiplied by 0.333 and shifted by the pad, or the supervision points at the wrong pixels.

而当你缩放图像时,必须对标注施加同一个变换。把一张 1920×1080 的图像做信箱式(letterbox)填充装进 640×640 的正方形(标准的检测器输入),你就按 640/1920 = 0.333 做了缩放,并在上下各填充了 107 px;每个框坐标都必须乘以 0.333 再加上填充偏移,否则监督信号就会指向错误的像素。

Where this points next

这将指向何处

We now know precisely what a model receives: a sampled, gamma-encoded, quantized, conventions-laden grid. But raw pixel values are a terrible thing to learn directly from — they are unstable to a one-pixel shift, a lighting change, or a JPEG re-save, and we saw that even sampling them faithfully requires a blur we have not built. The first tool that tames all of this is the one we needed in section 2: a convolution — a small sliding weighted sum that extracts local structure (edges, blobs) and that, run as a low-pass filter, is exactly the anti-alias step that makes downsampling honest. Lesson 02 builds it from scratch, by hand, and shows that the hand-designed filters of classical CV are just a special case of the kernels a CNN will later learn.

现在我们精确地知道了模型收到的是什么:一个经过采样、伽马编码、量化、并背负着各种约定的网格。但原始像素值是一种糟糕透顶的直接学习对象——它们对一个像素的平移、一次光照变化、一次 JPEG 重新保存都不稳定,而且我们已经看到,即便只是忠实地采样它们,也需要一个我们还没造出来的模糊。第一个能驯服这一切的工具,正是我们在第 2 节就需要的那个:卷积——一个小小的滑动加权求和,它提取局部结构(边缘、斑点),而当它作为低通滤波器运行时,恰恰就是让下采样变得诚实的那个抗混叠步骤。第 02 课将从零、亲手把它造出来,并展示经典计算机视觉里那些手工设计的滤波器,不过是 CNN 稍后将要学到的卷积核的一个特例。

Takeaway要点
A digital image is a lossy measurement: a continuous scene sampled into an H×W×C grid, quantized to 256 levels, gamma-encoded into a perceptual (non-linear) space, and compressed. Sampling below the Nyquist limit fs/2 causes aliasing — fast detail masquerading as slow, fake structure — which is why you must low-pass before downsampling. Pixel values are not linear light (L ≈ V2.2), so physical pixel math needs linearization, and HSV's hue is unstable near low saturation because it divides by a vanishing color spread. Most CV "model bugs" are actually convention bugs: HWC vs CHW, RGB vs BGR, the three value ranges, xyxy vs xywh, and unmatched resize transforms. The governing rule for the whole track: preprocessing is part of the model contract — train and serve must match. 一张数字图像是一次有损测量:一个连续场景被采样成一个 H×W×C 的网格、量化到 256 个级、伽马编码进一个感知(非线性)空间,再被压缩。在奈奎斯特限 fs/2 以下采样会导致混叠——快速细节伪装成缓慢的虚假结构——这就是你必须在下采样前先做低通的原因。像素值不是线性光(L ≈ V2.2),所以对像素做物理运算需要先线性化,而 HSV 的色相在低饱和度附近不稳定,因为它除以了一个趋于消失的颜色差值。大多数计算机视觉的"模型 bug"其实是约定 bug:HWC 与 CHW、RGB 与 BGR、三种取值范围、xyxy 与 xywh,以及不匹配的缩放变换。贯穿整门课的统领规则是:预处理是模型契约的一部分——训练与服务必须一致。

Interview prompts

面试题