為什麼一個有小孔的盒子能成像
想像一個完全密封、漆黑的房間,只在一面牆上戳出一個小孔。在晴朗的白天,神奇的事情發生了:對面那面牆上,竟浮現出外面街景的全彩、倒立影像,微微發著光。沒有鏡頭、沒有電子元件、沒有電池——只有一個孔。文藝復興時期的畫家把這個裝置稱為 camera obscura(拉丁文「暗房」之意),他們在裡頭架起畫架,描摹投影下來的景象以精準掌握透視,也在無意間替後世所有相機奠定了基礎。
一個前壁有小孔的暗箱;外面一棵樹射出的直線光線在小孔處交叉,在後內壁上形成一棵上下顛倒的樹。
為什麼一個孔就能成像?想想場景裡某一個亮點——比方說燭火的尖端。那個點會朝四面八方散射光線,像一顆小太陽。這些光線幾乎全部撞上盒子外壁、被擋住,只有正好對準小孔的那些光線能穿過去。從幾何上看,小到這種程度的孔,基本上只讓燭尖射出的一條光線抵達牆面,落在一個點上。場景中每一個點都是如此:世界裡的一個點 → 穿過小孔的一條光線 → 牆上的一個點。這種乾淨的一對一對應,正是「影像」的定義——也正因如此畫面才會清晰。而影像之所以上下顛倒,是因為來自場景頂部的光線沿直線穿過小孔後,會繼續往下走、打在牆的底部;來自底部的光線則交叉上去打在頂部。
為了做乾淨的幾何,我們把這套裝置理想化為針孔相機模型:一個位置固定、完美且無限小的孔(於是每個場景點都恰好對應一個影像點,毫無模糊),孔後方有一塊平坦的影像平面。這個模型丟掉了亮度的煩惱與鏡頭的複雜性,只保留純粹的光線幾何。這張極度精簡的圖,威力卻足以解釋透視、焦距與視野——並在本篇結尾說明:為什麼一張平面照片就是無法告訴你東西有多遠。接下來的一切,全都只是這一張圖的推論而已。
透視:為什麼遠方的東西看起來比較小
藏在針孔模型裡的那一條規則,有個名字:透視投影。它說:想知道任何一個 3D 點會落在影像的哪裡,就從那個點畫一條穿過針孔的直線(光線),看它刺穿影像平面的位置在哪。就這樣。世界上每個可見的點,都沿著那條穿過小孔的光線被「壓平」到 2D 影像上。整個 3D 場景被漏斗般匯聚到單一一點,再攤開到一個平面上。
由此直接冒出兩個重要直覺。第一:距離拉遠一倍,物體看起來就小一半。 想像兩個一模一樣的朋友,一個站在 5 公尺外、一個站在 10 公尺外。較遠那位射來的光線進入針孔的角度更平緩,因此落在影像中更靠近中心的位置——整個人被壓縮成只有一半的高度。把他們移到三倍遠,看起來就只剩三分之一高。影像上的大小與距離成反比。(下一節我們會看到,這「除以深度」其實一字不差地寫在投影公式裡。)
第二:世界中平行的線,看起來會交會於同一點。 站在鐵軌中間往遠方望去——兩條鐵軌明明完全平行,在影像裡卻互相傾斜,彷彿在遠處某個叫做消失點(vanishing point)的位置碰在一起,而這點就坐落在地平線上。長廊裡一排排的地磚、或一條筆直道路的兩側邊緣,也都是同樣的情形。為什麼?鐵軌上越往遠處的點,距離越遠,於是(依第一個直覺)越往影像中心縮;把無窮多個越來越遠的點疊起來,整條鐵軌就塌縮成一個點。在 3D 中方向相同的線,在影像中就共用一個消失點。
一個近的高物體與一個遠的高物體射出的光線都穿過針孔;遠物的光線落得更靠近影像中心,形成較小的影像。
這正是文藝復興畫家用手摸索出來、而你的雙眼每一個清醒的瞬間都在使用的透視——它並不是一種藝術選擇,而是幾何強加給我們的結果。因為針孔是透過單一一點收集光線,所以你拍下(或看見)的任何東西,都自動遵守這些規則。於是透視、消失點、以及遠物變小,全都是同一個現象,全都從上一節那張「單孔成像」的圖流淌而出。接下來,我們要把它量化。
投影方程式(來自相似三角形)
我們把座標釘下來,才能真正計算、而不只是比手畫腳。把針孔放在原點——我們這個小宇宙的中心。建立三條從相機量起的座標軸:X 指向相機的右方,Y 指向上方,Z 則筆直指向相機正前方、伸入場景。於是一個點的 Z 值,就是它在相機前方多遠——也就是它的深度。接著,把平坦的影像平面放在針孔前方固定距離 f 之處,與 Z 軸垂直。(為了避免上下顛倒,我們採用標準技巧,把這塊「虛擬」影像平面畫在孔的前方而非後方;幾何完全相同,但畫面會是正立的。)
現在從側面看,只取 X 與 Z 兩個方向,畫出兩個共用針孔為頂點的三角形。大三角形從針孔延伸到 3D 點:它長 Z(深度)、高 X(該點往右多遠)。小三角形從針孔延伸到影像平面:它長 f(焦距)、高 x'(該點落在影像上的位置)。因為這兩個三角形是由同一條光線交會於同一個針孔所構成的,所以它們相似——形狀相同、大小不同——因此對應邊的比值相等。就憑相似三角形的比值這一個事實,我們便能導出整條公式。
透視投影方程式,直接從相似三角形讀出。
慢慢讀。相似三角形的比值說:x'/f(小三角形的高比長)等於 X/Z(大三角形的高比長);兩邊同乘 f,就得到投影式。逐個符號來看:X、Y 是該點在相機座標系中的左右與上下位置(譬如以公尺計);Z 是它的深度——位於相機前方多遠;f 是焦距,也就是針孔到影像平面的固定距離;而 x'、y' 則是該點落在影像上的座標。Y 的式子和 X 的一模一樣,只是換到上下方向。整件事的核心是除以 Z:點越深(Z 越大),x' 與 y' 就越小,於是遠物朝中心縮小。這個「除以深度」的動作,正是被精確寫下來的透視投影。
我們實際跑一個點。取一顆 50 mm 鏡頭,所以 f = 0.05 公尺;有個點位在光軸右方 1 公尺(X = 1、Y = 0)、深度 5 公尺(Z = 5)。那麼 x' = f·X/Z = 0.05 × 1 / 5 = 0.01 公尺 = 影像中心右方 10 公釐。現在把同一個點推到兩倍遠、Z = 10 公尺:x' = 0.05 × 1 / 10 = 0.005 公尺 = 5 公釐——恰好一半。深度加倍、影像尺寸減半,正如「遠一倍看起來小一半」的直覺所承諾的。注意這個映射是非線性的:因為 Z 位在分母,把 Z 加倍並不是加減一個固定量,而是除法。正是這個棘手的除法,使得透視難以用單純的矩陣代數處理——而這也正是下一篇的齊次座標(homogeneous coordinates)要撫平的皺褶,把這個除法化為乾淨的矩陣乘法。
def project(X, Y, Z, f):
# Pinhole at the origin, camera looking down +Z, image plane at distance f.
# The single division by depth Z is the whole essence of perspective.
x_img = f * X / Z
y_img = f * Y / Z
return x_img, y_img
f = 0.05 # 50 mm focal length, in metres
print(project(1.0, 0.0, 5.0, f)) # (0.01, 0.0) -> 10 mm right of centre
print(project(1.0, 0.0, 10.0, f)) # (0.005, 0.0) -> same point, twice as far, half the size焦距:變焦的旋鈕
我們一直在寫 f,卻沒細究它。從物理上說,焦距就是針孔到影像平面的那段距離——如果你願意,可以想成那只小盒子的「長度」。它是相機製造者(以及你變焦時)能轉動的那一個旋鈕,而在我們的公式裡,它就是乘在所有東西前面的那個數:x' = f·X/Z。所以無論幾何給你的 X/Z 是多少,焦距都會在它落到影像之前把它放大或縮小。
這個乘法賦予了焦距操作上的意義。較大的 f 把每個 x'、y' 都乘上一個更大的數,於是同樣的場景鋪展到影像更大的範圍——物體看起來更大,你只看到近距離的一小片。這就是望遠(telephoto)或「拉近」的鏡頭。較小的 f 則把一切朝中心縮小,把更多的世界塞進畫面——這就是廣角(wide-angle)鏡頭。同樣的場景、同樣的針孔幾何;只是 f 改變了,畫面就變焦了。
你每次在手機上「捏合縮放」時都會感受到這一點。光學上的「拉近」就是相機在拉長它的有效焦距:遠處的臉龐脹大到填滿螢幕,但你再也塞不下整個房間。「拉遠」則是縮短 f:房間重新出現,但每個人都變得很小。所以當你在投影方程式裡讀到 f = 0.05 公尺(一顆 50 mm 鏡頭)時,請想像一個溫和、自然、接近人眼所見的變焦;而 200 mm 望遠鏡頭,就是那個放大率大約四倍。公式裡的符號 f,和你手機上的變焦滑桿,是同一回事。
視野:能裝進多少世界
焦距告訴你放大率;視野(FOV,field of view)則告訴你角度。它是相機所能捕捉的那一錐場景的角寬度,以度為單位,從影像的一邊量到另一邊。涵蓋整個大廳的監視器有著寬廣的視野;賞鳥者用來鎖定遠方一隻貓頭鷹的單筒望遠鏡,視野則極小。關鍵是,視野同時取決於兩件事:焦距 f 以及接住光線的感光元件(或底片、或影像平面)的大小。長鏡頭配大感光元件,與短鏡頭配小感光元件,可以給出完全相同的角度。
由感光元件尺寸 d 與焦距 f 求出視野。
它的來歷,逐個符號來看。d 是感光元件在你所關心方向上的尺寸——水平視野取寬度、垂直視野取高度。f 仍是焦距。看看從針孔到感光元件一側邊緣構成的半個三角形:它的「對邊」是半個感光元件 d/2,「鄰邊」是 f,所以比值 (d/2)/f = d/(2f) 就是半角的正切(tangent)。套上 arctan(正切的反函數)就把這個比值換回真正的角度——也就是半個視野。最後乘以 2,把半角加倍成全角,因為感光元件在中心兩側各凸出 d/2。所以做法是:取半個感光元件、除以焦距、取 arctan、再加倍。
我們來算。取一塊寬 36 公釐的全片幅感光元件(d = 36),配一顆 50 mm 鏡頭(f = 50)。那麼 d/(2f) = 36 / 100 = 0.36,arctan(0.36) ≈ 19.8°,於是 FOV = 2 × 19.8° ≈ 39.6°——自然、近似人眼的視野。現在把焦距減半,換成 25 mm 的廣角鏡頭:d/(2f) = 36 / 50 = 0.72,arctan(0.72) ≈ 35.7°,於是 FOV ≈ 71.5°。把 f 減半,視角幾乎加倍——這顆鏡頭現在吞進了多得多的世界。這就是 f 與視野的反比關係:焦距越長 → 視野越窄(望遠);焦距越短 → 視野越寬(廣角),恰好對上上一節的變焦直覺。(它不是剛好 2 倍,因為 arctan 是非線性的,但方向與大致幅度都成立。)
import math
def fov_degrees(sensor_mm, focal_mm):
# half-angle = arctan( (half the sensor) / focal length )
half_angle = math.atan(sensor_mm / (2 * focal_mm))
return math.degrees(2 * half_angle) # double it for the full angle
print(round(fov_degrees(36, 50), 1)) # 39.6 -> a normal 50 mm lens
print(round(fov_degrees(36, 25), 1)) # 71.5 -> halve f, view nearly doubles (wide-angle)
print(round(fov_degrees(36, 200), 1)) # 10.3 -> long telephoto, a narrow slice反推難題:從 2D 回到 3D
現在來看那個啟動了半個電腦視覺領域的轉折。再看一次 x' = f·X/Z。為了成像,我們除以了 Z——而這麼一除,就把 Z 給扔了。方程式留下了比值 X/Z,卻沒留下 X 和 Z 各自的值。於是沿著穿過針孔的同一條光線排列的每一個 3D 點——不管遠近——都產生相同的 x' 與 y',因而落在同一個像素上。一張平面影像忠實地記錄了每個像素來自哪一條光線,卻對物體位在那條光線上多遠之處完全沉默。這就是透視投影的代價:它是一條丟棄深度的單行道。
把它具體化。一輛長 10 公分、握在離鏡頭 1 公尺處的玩具車,和一輛長 4 公尺、停在 40 公尺外的真車,可以投出像素對像素完全相同的影像:兩者的 X/Z 比值相同,所以都填滿畫面中同一塊區域。單憑這一個視角,你的眼睛與感光元件都分不出它們。這正是「強迫透視」(forced perspective)照片背後的把戲——遊客「扶著」比薩斜塔,或在日落時「捏住」太陽。相機並沒有被騙;它只是忠實地報告:這兩個物體落在同一批光線上而已。
這種損失稱為深度歧義(depth ambiguity),這也是為什麼一般情況下,你無法從單一一張照片重建 3D 場景——數學上資訊已被硬生生抹去。想把深度找回來,你必須加點東西:一個第二視角(兩隻眼睛,或兩台相機 = 立體視覺,兩個視角之間的微小位移會洩漏距離)、隨時間的運動、能直接量測 Z 的深度感測器、或是習得的先驗——一個看過太多車子與房間的神經網路,能從熟悉的大小、模糊、明暗等線索去猜測合理的深度。這每一種,都是在打破「除以 Z」所製造出的那個僵局。