亮度直方圖
亮度直方圖是數位影像處理的基石之一。它將影像的亮度分佈情況視覺化,讓我們能一眼看出影像的曝光是過暗、過亮還是均衡。本章節將教你如何分析一張影像的像素,並繪製出它的亮度直方圖。
1. 什麼是亮度直方圖 (Brightness Histogram)?
簡單來說,亮度直方圖是一張圖表,用來顯示影像中每個亮度級別的像素數量。
- X 軸 (橫軸):代表亮度值,範圍從 0 (純黑) 到 255 (純白)。
- Y 軸 (縱軸):代表擁有該亮度值的像素總數。
如果一張影像的直方圖中,像素大多集中在左側 (0 附近),代表影像偏暗;集中在右側 (255 附近),代表影像偏亮;若分佈均勻,則通常有較好的對比度。
2. 演算法實作步驟
要繪製一張直方圖,我們需要遵循以下幾個核心步驟,並將它封裝成一個函式。這個函式會接收一個 BmpImage 物件,並回傳一個新的、代表直方圖的 BmpImage 物件。
統計亮度分佈:
- 建立一個長度為 256 的陣列 (
histogramData),並將所有元素初始化為 0。這個陣列的索引i將對應亮度值i。 - 使用雙層迴圈遍歷來源影像的每一個像素
(x, y)。 - 取得該像素的
{ r, g, b }值。 - 使用我們熟悉的灰階公式 $Gray = 0.299 \times R + 0.587 \times G + 0.114 \times B$ 計算其亮度值。
- 將對應亮度值的計數加一,即
histogramData[gray]++。
- 建立一個長度為 256 的陣列 (
尋找最大值並正規化 (Normalization):
- 完成統計後,
histogramData陣列中的某個值會是最高的,代表該亮度級別的像素最多。我們需要找出這個最大值maxCount。 - 為了將直方圖繪製到固定高度的影像中(例如 256px 高),我們需要將所有計數值進行「正規化」,也就是按比例縮放。公式為:
barHeight = (count / maxCount) * (輸出高度 - 1)。
- 完成統計後,
繪製直方圖影像:
- 使用
BmpImage.create(512, 512)建立一張 256x256 的空白黑色畫布。 - 我們的 X 軸有 256 個亮度級別,且畫布寬度為 256 像素。
- 遍歷
histogramData陣列(從i = 0到255)。 - 計算出第
i個亮度級別正規化後的高度barHeight。 - 在畫布上對應的 X 座標(
i * 2和i * 2 + 1)處,從底部 (y = 0) 向上畫一條高度為barHeight的白色垂直線。
- 使用
3. 程式碼實作
現在,我們將上述步驟寫成一個函式。
3.1 繪製直方圖函式
BrightnessHistogram.js
import BmpImage from "./BmpImage.js";
/**
* 計算影像的亮度直方圖
* @param {BmpImage} sourceImage - 來源影像
* @param {object} [weights={ r: 0.299, g: 0.587, b: 0.114 }] - 計算亮度的 RGB 權重
* @returns {ArrayBuffer} - 代表直方圖的 Array
*/
function createBrightnessHistogram(sourceImage, weights = { r: 0.299, g: 0.587, b: 0.114 }) {
const histogramData = new Array(256).fill(0);
for (let y = 0; y < sourceImage.height; y++) {
for (let x = 0; x < sourceImage.width; x++) {
const pixel = sourceImage.getPixel(x, y);
// 使用傳入的權重計算灰階值
const gray = Math.round(
pixel.r * weights.r +
pixel.g * weights.g +
pixel.b * weights.b
);
// 確保灰階值在 0-255 範圍內
const clampedGray = Math.max(0, Math.min(255, gray));
histogramData[clampedGray]++;
}
}
return histogramData;
}
/**
* 計算並繪製影像的亮度直方圖
* @param {BmpImage} sourceImage - 來源影像
* @param {object} [weights={ r: 0.299, g: 0.587, b: 0.114 }] - 計算亮度的 RGB 權重
* @returns {BmpImage} - 代表直方圖的 BmpImage 物件
*/
function createBrightnessHistogramToBmp(sourceImage, weights = { r: 0.299, g: 0.587, b: 0.114 }) {
// 步驟 1: 統計亮度分佈
const histogramData = createBrightnessHistogram(sourceImage, weights);
// 步驟 2: 尋找最大值
const maxCount = Math.max(...histogramData);
if (maxCount === 0) {
// 如果影像為全黑或無像素,直接回傳空白影像避免除以零
return BmpImage.create(256, 256);
}
// 步驟 3: 繪製直方圖影像
const histogramImage = BmpImage.create(256, 256);
for (let i = 0; i < 256; i++) {
// 計算正規化後的高度
const barHeight = Math.round((histogramData[i] / maxCount) * 255);
// 畫出垂直的長條
for (let y = 0; y < barHeight; y++) {
histogramImage.setPixel(i, 255 - y, 255, 255, 255); // 畫白色
}
}
return histogramImage;
}
export { createBrightnessHistogram, createBrightnessHistogramToBmp }3.2 程式碼說明
createBrightnessHistogram函式- 將質亮度統計直寫入Array,並回傳。
統計迴圈
- 與先前灰階轉換的程式碼幾乎一樣,只是這次不是修改原圖,而是用計算出的
gray值來更新histogramData陣列的計數。
- 與先前灰階轉換的程式碼幾乎一樣,只是這次不是修改原圖,而是用計算出的
Math.max(...histogramData)- 這是 JavaScript 中尋找陣列最大值的簡便語法。
繪製迴圈 createBrightnessHistogramToBmp
- 外層迴圈遍歷 256 個亮度級別。
- 內層迴圈 (
y和w) 負責在畫布上填滿代表該亮度級別的長方形區域。 histogramImage.setPixel(255 - x, y, 255, 255, 255)將像素設為白色。255 - y用來處理 Y 軸翻轉,我們從y=0畫起,效果就是從底部往上畫。
4. 使用範例
現在,我們來實際使用這個函式,讀取一張圖片,並為它產生一張直方圖。
import { createBrightnessHistogramToBmp } from "./BrightnessHistogram.js";
import BmpImage from "./BmpImage.js";
// 載入來源影像
const sourceImg = BmpImage.load("test.bmp");
console.log("Loaded source image:", sourceImg.width, "x", sourceImg.height);
// 繪制亮度直方圖
console.log("Generating brightness histogram...");
const histogramImg = createBrightnessHistogramToBmp(sourceImg);
// 儲存直方圖影像
histogramImg.save("test_histogram.bmp");
console.log("Saved histogram image to test_histogram.bmp.");
說明:
- 首先載入
test.bmp(你可以換成任何 24-bit 的 BMP 檔)。 - 呼叫我們剛寫好的
createBrightnessHistogram函式。 - 將回傳的
BmpImage物件存檔為test_histogram.bmp。 - 執行後,你可以用圖片瀏覽器打開
test_histogram.bmp,觀察其亮度分佈。
5. 小結與延伸思考
- 直方圖的價值:我們學會了如何透過程式碼量化並視覺化影像的整體特徵。這不僅僅是畫一張圖,更是許多自動化演算法(如自動曝光、自動對比度)的基礎。
- 演算法的組合:這個實作完美地展示了如何結合使用
getPixel,create,setPixel等我們自訂的工具來完成一個更複雜的分析任務。 - 不依賴外部函式庫:同樣地,我們完全基於 Node.js 的
Buffer和fs模組完成了所有操作,加深了對影像資料結構的理解。
延伸操作提示:
- 彩色直方圖:你能否修改
createBrightnessHistogram,讓它能分別計算 R, G, B 三個通道的直方圖,並用紅色、綠色、藍色的線條畫在同一張圖上? - 分析不同影像:找一些特別亮、特別暗或對比度很低的圖片,產生它們的直方圖,並試著從直方圖的形狀來「解讀」影像的特性。