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。
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.
图像不是"世界"本身。它是对世界的一次测量,而任何测量都会丢失信息。来自三维场景的光穿过镜头、落到传感器上,再经过一连串有损步骤变成一个数字数组。从第一性原理出发的计算机视觉,就是在每一步都追问:什么活了下来。
Three losses dominate and recur for the rest of the track:
三种损失最为主要,并会在这门课的其余部分反复出现:
- Sampling turns a continuous 2D signal into a finite grid of H × W samples. If the scene varies faster than the grid can represent, the fast variation does not vanish — it disguises itself as something slower and false. That is aliasing (section 2).采样把连续的二维信号变成一个有限的 H × W 采样网格。如果场景变化得比网格所能表示的还快,这种快速变化不会消失——它会伪装成某种更慢、且虚假的东西。这就是混叠(第 2 节)。
- Quantization rounds each sample to one of a finite set of levels — typically 28 = 256 per channel for an 8-bit image. This injects a small rounding noise (≈ ±½ a level) and sets a floor on the precision of anything you compute from pixels.量化把每个采样值取整到有限的若干量化级之一——对 8 位图像来说,每个通道通常是 28 = 256 级。这会注入一点微小的取整噪声(约 ±½ 个量化级),并为你从像素中计算出的一切设下了精度下限。
- Compression (JPEG most often) discards high-frequency detail in 8×8 blocks to save bytes. It changes the statistics of edges and texture — exactly the high-frequency content lesson 02's edge filters key on.压缩(最常见的是 JPEG)在 8×8 的块内丢弃高频细节以节省字节。它改变了边缘和纹理的统计特性——而这恰恰是第 02 课的边缘滤波器所依赖的高频内容。
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(每单位长度的采样数)对它采样。奈奎斯特–香农采样定理指出,只有当采样速度超过信号最高频率的两倍时,你才能忠实地重建它:
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 都会被反射到一个混叠频率上:
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 课。玩一玩下面的小部件,把一个稳定的图案逼成混叠,再看看抗混叠开关如何把它救回来。
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:
具体来说,一张彩色图像就是一个三维数组。下面是一个你应该能背出来的具体例子:
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 课看到,早期卷积滤波器实际上学到了输入的尺度,这正是这种不匹配会在第一层就咬人的原因。
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 编码近似是一个幂律:
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 各通道几乎相等,于是色相计算除以了一个接近零的差值,结果变得数值不稳定且充满噪声:一个灰色像素可能报出剧烈摆动、毫无意义的色相,而极微小的传感器噪声就能让它在色轮上翻来覆去。
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 − xmin、h = 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 稍后将要学到的卷积核的一个特例。
Interview prompts
面试题
- What is aliasing, and why does it appear when downsampling? (§2 — sampling below Nyquist fs/2 reflects high frequencies down to false low-frequency patterns; fix with a low-pass blur before decimating.)什么是混叠,它为什么会在下采样时出现?(§2 —— 在奈奎斯特 fs/2 以下采样,会把高频反射到虚假的低频图案上;修复办法是在抽取前先做一次低通模糊。)
- Why can a model trained on RGB fail when the serving pipeline uses OpenCV defaults? (§3 — OpenCV reads BGR; the swapped R/B planes break the input distribution the first conv layer learned.)为什么一个在 RGB 上训练的模型,会在服务流水线使用 OpenCV 默认设置时失效?(§3 —— OpenCV 读入的是 BGR;被互换的 R/B 平面破坏了第一层卷积所学到的输入分布。)
- Is pixel value 128 half-brightness? (§4 — no; sRGB gamma means 128 ≈ 0.22 of full linear light; correct physical math needs linearization.)像素值 128 是半亮度吗?(§4 —— 不是;sRGB 伽马意味着 128 ≈ 满线性光的 0.22;正确的物理运算需要先线性化。)
- Why is HSV unstable for thresholding near grays? (§4 — hue is an angle computed from the color spread, which → 0 near low saturation, so noise blows up the hue.)为什么 HSV 在接近灰色时用于阈值分割会不稳定?(§4 —— 色相是从颜色差值算出的角度,而该差值在低饱和度附近 → 0,于是噪声会把色相放大失控。)
- You resize 1920×1080 to 640×640 before detection. How do boxes and aspect ratio change? (§5 — letterbox scales by 0.333 and pads; every box must be scaled and shifted by the same transform, or it points at the wrong pixels.)你在检测前把 1920×1080 缩放到 640×640。边界框和长宽比会如何变化?(§5 —— 信箱式填充按 0.333 缩放并做填充;每个框都必须用同一个变换做缩放和平移,否则它会指向错误的像素。)
- How would you test whether a preprocessing mismatch is causing a production accuracy drop? (§3, §5 — diff train vs serve on channel order, normalization μ/σ, resize/interpolation policy, EXIF rotation, and box convention before touching the architecture.)你会如何检验一次预处理不匹配是否正在造成生产环境的精度下降?(§3、§5 —— 在动架构之前,先在通道顺序、归一化 μ/σ、缩放/插值策略、EXIF 旋转以及边界框约定这些方面对比训练与服务。)