照片其實是一格數字網格
拿出你的手機,打開任何一張照片,用兩指放大。繼續放大——越過臉、越過眼睛、越過睫毛。到了某個程度,那張平滑、栩栩如生的照片就不再平滑了。它會碎裂成一格一格的小方塊,每一格都是單一、扁平的色調。這些方塊一直都在,放大只是讓你看見它們而已。每一個小方塊就是一個像素(pixel,是「圖像元素/picture element」的縮寫),它是電腦能夠儲存或顯示的影像最小單位。
左邊是一張平滑的照片;箭頭放大其中一小塊區域,右邊顯示為一格格獨立的彩色方塊組成的粗網格,每一格標示為一個像素。
整張影像不過就是這些像素的矩形排列,像馬賽克磁磚牆一樣排成一列列、一行行。我們稱它為數位影像:一張以固定像素網格儲存的圖片,其中每個像素都裝著一個或多個數字,用來描述它的顏色或明暗。把它想成一張試算表,或一張方格紙——網格的每一格都裝著一個數字,而對電腦來說,那個數字「就是」這個像素。
因為它是一個網格,我們可以用兩個數字給每個像素一個「地址」:它在第幾個橫列(row)、第幾個直行(column)——就像在電影院裡用排數和座位號來指定一個座位一樣。我們在這整個單元都會用這種「橫列/直行」定位法,所以現在先把它定下來:一個寬 W 像素、高 H 像素的網格,有 H 個橫列由上到下堆疊、W 個直行由左到右排列。每一組(橫列, 直行)都剛好指向一個像素。
灰階:每個像素只用一個數字
顏色會多一層複雜度,所以我們先從最簡單的影像開始,把顏色留到下一篇指南再加。在一張黑白照片裡——更精確地說是灰階影像——每個像素只儲存一個數字:它的亮度。一個像素,一個數字。沒有色相、沒有顏色,只有「這一格有多亮或多暗?」
到目前為止最常見的慣例,是讓這個亮度數字從 0 跑到 255。0 代表全黑(沒有光),255 代表全白(光線最強),中間的每個值都是某種灰——64 是深灰、128 是中灰、200 是淺灰。為什麼偏偏是 255 這個數字,而不是 100 或 1000?這個看似奇怪、又如此精確的上限,來自每個像素被分配到多少位元,而那正是下一節的主題。現在,先把 0–255 當成衡量亮度的標準尺規就好。
# A tiny 4x4 grayscale image, written as a grid of brightness values.
# 0 = black, 255 = white. Each number is ONE pixel.
image = [
[ 0, 0, 255, 255], # row 0: two black squares, then two white
[ 0, 128, 128, 255], # row 1: black, two mid-grays, white
[255, 128, 128, 0], # row 2: white, two mid-grays, black
[255, 255, 0, 0], # row 3: two white squares, then two black
]
# image[row][col] looks up the brightness of one pixel.
print(image[1][2]) # -> 128, the mid-gray pixel in row 1, column 2用電腦的方式來讀這張表。最上面一橫列是黑、黑、白、白,所以圖片的左上角是暗的、右上角是亮的。往下幾列,明暗方塊互換了角落,中央則由柔和的中灰(128)形成一個方塊。如果你把每個數字印成一個對應深淺的方塊,你會看到一個有點像「X」的明暗圖案。這就是全部的把戲:數字控制方塊的亮度,於是一張數字表就變成了你看得見的圖片。
位元深度:一個像素能裝下幾種明暗?
我們一直碰到 256 這個數字(也就是 0 到 255 一共有幾個值)。它從哪裡來?它來自位元深度:一個像素被分配多少個位元(bit)來儲存它的值。一個位元就是一個二進位數字——一個非 0 即 1 的開關。每個像素拿到的位元越多,它能表示的明暗等級就越多。
把亮度想像成一道從黑通往白的樓梯。位元深度決定這道樓梯有幾階。如果只有少少幾階,一片平滑的天空就必須從一種灰突然跳到下一種灰,於是你會看到難看的條紋——這叫做色帶(banding)或色調分離(posterization)。如果階數很多,每次跳動都小到肉眼看來漸層完美平滑。位元越多=階數越多=陰影越平滑;位元越少=色調越粗糙、階梯感越明顯。一張灰階影像就正是這種樓梯的單一通道。
亮度等級的數量 L,每多一個位元 b 就翻倍。
把它讀成「可用等級的數量,等於 2 的(位元數)次方」。逐個符號來看:L 是一個像素能取的可區分亮度值的數量;b 是分配給那個像素的位元數;底數 2 之所以出現,是因為每個位元都是一個二進位的開/關開關,所以每多一個位元,組合的數量就翻一倍。一個位元有 2 種設定;兩個位元有 2×2 = 4 種;三個位元有 2×2×2 = 8 種。每多一個位元,字面上就讓你的選擇翻倍,這就是為什麼數量增長得這麼快。
- 1 位元:L = 2^1 = 2 個等級。只有 0 和 1——純黑或純白,完全沒有灰。這就是傳真機或線稿的樣子。
- 2 位元:L = 2^2 = 4 個等級。黑、深灰、淺灰、白。好一些了,但天空仍會出現明顯的色帶。
- 8 位元:L = 2^8 = 256 個等級。平滑到肉眼幾乎看不出階梯。這是日常的標準,它的 256 個等級就編號為 0 到 255。
這就是上一節那個謎題的答案。我們熟悉的 0–255 範圍,其實就是一個 8 位元像素的 256 個等級。我們從 0 而不是 1 開始數,所以最小的值是 0(黑),最大的值是 256 − 1 = 255(白)。每個像素 8 位元實在太常見了,因此當有人說「一張 8 位元灰階影像」時,你馬上就能想像出一個由整數組成的網格,每個整數都介於 0 到 255 之間。
解析度:有多少像素,以及為什麼重要
到目前為止,我們談的都是「一個像素裝什麼」。現在問:總共有幾個像素?這個數量就是影像的解析度,寫成「寬 × 高」的像素數——例如 1920 × 1080。第一個數字是橫向有幾個像素(直行數),第二個是縱向有幾個(橫列數)。解析度越高的網格,對場景取的樣本越多、每個越小,因此能捕捉越細的細節——但這也代表要儲存和處理的數字越多。
總像素數=寬乘以高。
這表示像素的總數等於寬乘以高。N 是總像素數;W 是以像素為單位的寬度(有幾直行);H 是以像素為單位的高度(有幾橫列)。這其實就是在數網格的格子數:一個寬 1920、高 1080 的網格,含有 N = 1920 × 1080 = 2,073,600 個像素——大約 207 萬。這「兩百萬個像素」,正是相機標榜「200 萬畫素(2 megapixels,mega=百萬)」時的意思。
有一個要避開的陷阱:像素解析度跟「印出來的尺寸」不是同一回事。1920 × 1080 這個網格固定的是「你有多少個樣本」,僅此而已。它印在紙上看起來多大,取決於 DPI(每英寸點數,dots per inch)——印刷時把這些像素排得多密。同一張 1920 × 1080 的影像,可以是一張清晰的 5 吋小照片,也可以是一張模糊的海報,差別只在你把像素鋪得多開。像素數關乎「捕捉了多少資訊」;DPI 關乎「輸出時的實體尺寸」。我們大多時候在意的是前者。
未壓縮的大小(位元組):像素數 × 通道數 × 每個值佔幾位元組。
這估算的是一張未壓縮影像佔多少位元組。W × H 就是我們剛算出的像素數 N。C 是通道數(channels)——每個像素儲存幾個數字;灰階影像 C = 1(一個亮度值),下一篇講顏色時你會看到 C = 3。b 是位元深度,而我們把 b 除以 8,是因為一個位元組(byte)有 8 個位元,這一步把「每個值幾位元」換算成「每個值幾位元組」。代入例子:一張 1920 × 1080、8 位元的灰階影像,大約需要 2,073,600 × 1 × (8/8) = 2,073,600 位元組 ≈ 2.07 MB。換成 3 通道的彩色,大約會變三倍、約 6.2 MB——這就是為什麼彩色照片和高解析度這麼吃記憶體。
從數字回到圖像:建立心智模型
讓我們把整篇指南綁成一個耐用的心智模型,你在這整個學習階梯裡都會靠它。影像是一個數字網格,而要把這句話說得乾淨利落就是:影像是一個函數,它吃進一個位置、回傳存在那裡的值。電腦把它存成一個二維陣列(一張表),就跟我們前面寫的那個 Python 網格一模一樣,而「看這張圖」其實只是從那張表裡讀出數字而已。
把影像看成函數:給它直行 x 和橫列 y,它回傳那個像素的值。
別被函數記號嚇到——它只是「查出這個網格格子裡的數字」的簡寫。I 是影像本身;(x, y) 是某個像素的地址;輸出 I(x, y) 就是存在那裡的值——對灰階影像來說,是一個介於 0 到 255 的亮度值。所以 I(2, 1) = 128 不過是說「直行 2、橫列 1 的那個像素裝著值 128」。沒有比這更神祕的東西。我們替這個「查表」動作取個名字,只是為了之後能乾淨地討論它。
有一個慣例幾乎絆倒每一個初學者,所以現在就把它釘死。在影像裡,原點 (0, 0) 位在左上角,而不是你數學課可能記得的左下角。x 是直行索引,往右遞增;y 是橫列索引,往下遞增。所以當 y 變大時,你是在圖片裡往下走,不是往上。這跟螢幕由上到下一列列繪製的方式一致,也是幾乎所有影像函式庫採用的慣例。在一張 W × H 的影像裡,x 從 0 跑到 W − 1,y 從 0 跑到 H − 1。
一個標註好的像素網格,原點標在左上角,x 軸沿著直行往右、y 軸沿著橫列往下,其中一格被標示出它的 (x, y) 地址與存著的數字。