Histograms是直方圖的意思,在攝影領域一般稱為曝光直方圖,主要用以分析一張照片的曝光是否正確。應用在電腦視覺則主要利用它來判斷白平衡、處理thresholding(閥值,請參考之前的文章OPENCV – more on contours #2),也可用於物體追蹤(例如CamShift演算法即使用彩色直方圖的變化達到跟蹤目的),另外也可用於圖像檢索目的(例如Bag-of-words詞袋演算法)、或應用於機器學習運算…等等。因為Histograms為我們統計出各像素強度頻率的資訊來呈現整張相片的色彩分佈情況,因此我們得以透過此資訊來猜測該圖片的物件及特性。
灰階直方圖
下圖是一張相片的灰階直方圖,表示了所有像素強度從0至255的分佈情形,X軸強度值為0代表全黑,255為全白。我們可以從中看到什麼訊息呢?
我們可以看出:
- 這直方圖當中有三個比較明顯的山峰,第一個主要集中在x=20附近,因此我們可以猜測圖片中應該有個顏色相當深的物體。
- 第二個山峰較為低矮平滑且範圍較廣,約從X=50緩步上升X=90處開始下降至120左右,色彩偏暗,這部份很有可能是圖片中的背景部份。
- 大量的像素集中在第三個山峰X=220至245之間,不過我們暫時無法確定該區域是什麼,但能確定的是,圖片中肯定有一大片白色的區域。
因此從該直方圖中,我們雖無法明確的知道有什麼物體,但卻可以掌握圖片的特性,例如明暗、對比、色溫、像素強度分布等,此外,我們還可以從一堆圖片中指認出那些是屬於該直方圖特色的圖片。
產生直方圖資訊 :
使用cv2.calcHist可產生指定圖片的直方圖資訊。
cv2.calcHist(images, channels, mask, histSize, ranges)
- imaages:要分析的圖片檔
- channels:產生的直方圖類型。例:[0]→灰階,[0, 1, 2]→RGB三色。
- mask:optional,若有提供則僅計算mask的部份。
- histSize:要切分的像素強度值範圍,預設為256。每個channel皆可指定一個範圍。例如,[32,32,32] 表示RGB三個channels皆切分為32區段。
- ranges:X軸(像素強度)的範圍,預設為[0,256](非筆誤,calcHist函式的要求,最後那個值是表示<256)。
下方程式示範如何產生圖片的直方圖資訊並繪製出來:
# 匯入相關模組
from matplotlib import pyplot as plt
import argparse
import cv2
# 取得傳入的相片檔名
ap = argparse.ArgumentParser()
ap.add_argument(“-i", “–image", required=True, help="Path to the image")
args = vars(ap.parse_args())
# 讀取相片
image = cv2.imread(args[“image"]
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # ←轉為灰階
cv2.imshow(“Original", image)
hist = cv2.calcHist([image], [0], None, [256], [0, 256]) # ←計算直方圖資訊
#使用MatPlot繪出 histogram
plt.figure()
plt.title(“Grayscale Histogram")
plt.xlabel(“Bins")
plt.ylabel(“# of Pixels")
plt.plot(hist)
plt.xlim([0, 256])
以下面兩張相片為例,左側是其灰階圖形,後兩張曲線圖是其像素分佈的直方圖,最右側曲線圖差異在於其是normalized的直方圖,兩者在Y軸呈現方式不同,一個是像素數目,一個是像素比例,事實上,右側normalized過的直方圖,其資訊對我們比較有意義。
下方的第二張相片由於少了白襯衫,因此原本亮度接近200的尖峰稍微往左移動,並減少到0.016以下,空出來的黑色椅墊使得30~40左右強度的像素增加。
彩色直方圖
上面是針對灰階圖片(只有1 channel)的直方圖, 如果是擁有3個channels的彩色直方圖呢?我們加個for迴圈便可以了。
chans = cv2.split(image) # 將讀取的image檔split為channels
colors = (“b", “g", “r") # 定義一colors數組,注意openCV split後的channels排序為BGR
plt.figure()
plt.title(“‘Flattened’ Color Histogram")
plt.xlabel(“Bins")
plt.ylabel(“# of Pixels")
# 依次取出chans及colors的B,G,R值繪製直方圖
for (chan, color) in zip(chans, colors):
# create a histogram for the current channel and plot it
hist = cv2.calcHist([chan], [0], None, [256], [0, 256])
plt.plot(hist, color = color)
plt.xlim([0, 256])
我們看到RGB三個channels皆各有一相當突出的山峰位於170至200之間的區域,這是因為相片中有一大片佔了相當大比例的灰白色地板。
第二張相片G與B在強度70~80左右佔的比例增加,可能是多出的地板面積緣故(可使用Photoshop探知該區的RGB數值)。而原本第一張中的R channel在120~130區域是一小山峰,但人員離開後的空座位少了手和臉部的膚色,因此在第二張圖中反而呈現下凹現象,而人員離開後多出的地板,其RGB值介於70~130之間,故第二張圖中此部份的比例上升。
因此,當我們比較兩張相片的normalized直方圖時,大致上看起來可能差不多,但是某些物件的增減會使得各像素強度比例在某些區域有些變化,這些差異資訊未來或許可以導入Scikit-learn進行學習。
2D直方圖
上例的灰階與彩色histogram皆屬於一維直方圖,另外我們也經常使用所謂的多維直方圖進行分析判斷,例如我們想要瞭解Red=10且Green=30時的pixels數目有多少?或某兩個channels的值互為多少時其像素有多少?此時就是使用二維直方圖的時機。
二維直方圖就像一張由上而下俯視的地形圖,不同顏色代表不同高度(像素的數目),深藍色最低矮深紅色最高聳。RGB三色可分別組合成三種G+B、G+R、B+R的二維直方圖,下圖分別為有人與無人的辦公室所產生的2D直方圖,深紅色區域為最高點,深藍色則最低緩。
在上圖及下方的程式中,你會發現X軸不是0~255而是0~15,這是因為基於運算效率考量。如果我們將像素強度維持256階,那麼一張VGA尺寸相片的2D直方圖,電腦需要處理的資訊將有256 x 256(X軸 x Y軸),再乘上三倍(GB+GR+BR三種組合),最後再乘上高度(相片的總像素),總共就會有60,397,977,600個像素資訊需要處理。實際上,我們在進行電腦視覺時也不需要細到每個像素,而只需要by區域進行判斷即可,因此,將256階縮小為32或16是可行的作法。
程式:
fig = plt.figure()
# 繪製Green和Blue的2D直方圖,我們將256種像素強度僅分成16個範圍。
ax = fig.add_subplot(131)
hist = cv2.calcHist([chans[1], chans[0]], [0, 1], None, [16, 16], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation="nearest")
ax.set_title(“2D Color Histogram for G and B")
plt.colorbar(p)
# 繪製Green和Red的2D直方圖,我們將256種像素強度僅分成16個範圍。
ax = fig.add_subplot(132)
hist = cv2.calcHist([chans[1], chans[2]], [0, 1], None, [16, 16], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation="nearest")
ax.set_title(“2D Color Histogram for G and R")
plt.colorbar(p)
# 繪製Blue和Red的2D直方圖,我們將256種像素強度僅分成16個範圍。
ax = fig.add_subplot(133)
hist = cv2.calcHist([chans[0], chans[2]], [0, 1], None, [16, 16], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation="nearest")
ax.set_title(“2D Color Histogram for B and R")
plt.colorbar(p)
圖中這條斜向的一條正比例直線是由相片中由淺而深的大片灰白色地板及桌面所構成的。透過二維直方圖資訊,我們發現它比起一維直方圖來說更容易看出明顯的顏色異動,因此這種表達方式可以更清楚的反映物件異動對於畫面顏色與像素強度的影響。
加上MASK去除不必要區域
有時候,相片中僅有一部份區域是我們關心的,其它部份可省略不看,那麼在處理過程中可以在圖像上加入MASK來排除不需要的區域。
可以發現加入MASK後,直方圖的變化資訊更為明顯: (有人office→無人office)
程式:
from matplotlib import pyplot as plt
import numpy as np
import cv2
#繪製一維直方圖的function
def plot_histogram(image, title, mask=None):
chans = cv2.split(image)
colors = (“b", “g", “r")
plt.figure()
plt.title(title)
plt.xlabel(“Bins")
plt.ylabel(“# of Pixels")
for (chan, color) in zip(chans, colors):
hist = cv2.calcHist([chan], [0], mask, [256], [0, 256])
plt.plot(hist, color=color)
plt.xlim([0, 256])
#繪製2D直方圖的function
def plot_2d(image, title, mask=None):
chans = cv2.split(image)
colors = (“b", “g", “r")
fig = plt.figure()
ax = fig.add_subplot(131)
hist = cv2.calcHist([chans[1], chans[0]], [0, 1], mask, [16, 16], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation="nearest")
ax.set_title(“2D Color Histogram for G and B")
plt.colorbar(p)
ax = fig.add_subplot(132)
hist = cv2.calcHist([chans[1], chans[2]], [0, 1], mask, [16, 16], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation="nearest")
ax.set_title(“2D Color Histogram for G and R")
plt.colorbar(p)
ax = fig.add_subplot(133)
hist = cv2.calcHist([chans[0], chans[2]], [0, 1], mask, [16, 16], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation="nearest")
ax.set_title(“2D Color Histogram for B and R")
plt.colorbar(p)
#讀入相片
image = cv2.imread(“p3.jpg")
cv2.imshow(“Original", image)
#繪製一維直方圖
plot_histogram(image, “Histogram for Original Image")
#繪製2D直方圖
plot_2d(image, “Histogram for Original Image")
# 產生mask,白色區域為保留區。
mask = np.zeros(image.shape[:2], dtype="uint8″)
cv2.rectangle(mask, (0, 0), (320, 208), 255, -1)
cv2.imshow(“Mask", mask)
#此行不一定需要,主要是用於預覽mask後的圖形
masked = cv2.bitwise_and(image, image, mask=mask)
cv2.imshow(“Applying the Mask", masked)
#繪製一維直方圖
plot_histogram(image, “Histogram for Masked Image", mask=mask)
#繪製2D直方圖
plot_2d(image, “Histogram for Masked Image", mask=mask)
plt.show()
cv2.waitKey(0)
到目前為止,除了之前所介紹的邊緣edge及輪廓contour之外,本文的直方圖histogram也是電腦視覺經常使用到的技術,未來將會在更多的實例中應用到它們。