亮度直方累積圖
亮度直方累積圖 (Cumulative Histogram) 是直方圖的延伸,它能更細緻地展現影像像素的累積分佈狀況。這對於理解影像的整體對比度、判斷是否適合進行直方圖等化等進階處理,都提供了重要的視覺依據。
1. 什麼是亮度直方累積圖 (Cumulative Brightness Histogram)?
回顧亮度直方圖,它顯示的是每個亮度級別的像素數量。而亮度直方累積圖則更進一步,它在每個亮度級別 i 上,顯示的是亮度值小於或等於 i 的所有像素的總和。
用數學式表示就是:
CumulativeCount[i] = Sum (PixelCount[j]) for all j <= i
累積直方圖的特性:
- X 軸 (橫軸):同樣代表亮度值,範圍從 0 (純黑) 到 255 (純白)。
- Y 軸 (縱軸):代表所有亮度值小於等於 X 軸對應值的像素總數。因此,Y 軸的最大值將是影像的總像素數量。
- 曲線形態:累積直方圖總是呈現一條非遞減的曲線。它從左側接近 0 的值開始,逐漸上升,直到右側達到影像的總像素數。
分析累積直方圖可以幫助我們:
- 判斷對比度:一條「理想」的、對比度良好的影像,其累積直方圖通常會呈現一條近似直線、均勻上升的斜線。
- 識別過暗/過亮:如果曲線在左側快速上升,表示影像有很多暗部細節;如果曲線在右側才陡峭上升,表示影像有很多亮部細節。
- 直方圖等化基礎:累積直方圖是實現直方圖等化 (Histogram Equalization) 演算法的關鍵輸入。
2. 演算法實作步驟
要繪製一張直方累積圖,我們將利用先前計算亮度直方圖的結果,在此基礎上增加「累積」步驟。
統計亮度分佈 (同亮度直方圖):
- 首先,我們需要影像的原始亮度直方圖
histogramData,它記錄了每個亮度級別的像素數量。我們可以直接呼叫前面定義的createBrightnessHistogram函式來取得這份數據。 - 同時,我們需要計算影像的總像素數量
totalPixels,這將是累積直方圖的最大值。
- 首先,我們需要影像的原始亮度直方圖
計算累積分佈:
- 建立一個長度為 256 的新陣列
cumulativeHistogramData。 - 將
cumulativeHistogramData[0]設為histogramData[0]。 - 從
i = 1到255,依序計算:cumulativeHistogramData[i] = cumulativeHistogramData[i-1] + histogramData[i] - 最終
cumulativeHistogramData[255]會等於totalPixels。
- 建立一個長度為 256 的新陣列
正規化與繪製直方累積圖影像:
- 使用
BmpImage.create(width, height)建立一張空白的黑色畫布,例如 256x256 像素。 - 遍歷
cumulativeHistogramData陣列(從i = 0到255)。 - 計算出第
i個亮度級別正規化後的高度barHeight。由於累積直方圖的最大值是totalPixels,所以正規化公式為:barHeight = round( (cumulativeHistogramData[i] / totalPixels) * (輸出高度 - 1) ) - 在畫布上對應的 X 座標
i處,從底部 (y = 0) 向上畫一條高度為barHeight的白色垂直線。
- 使用
3. 程式碼實作
接下來,我們將這些步驟轉換成 Node.js 程式碼。
3.1 繪製累積直方圖函式
CumulativeHistogram.js
import BmpImage from "./BmpImage.js";
import { createBrightnessHistogram } from "./BrightnessHistogram.js";
/**
* 計算並繪製影像的亮度累積直方圖
* @param {BmpImage} sourceImage - 來源影像
* @param {object} [weights={ r: 0.299, g: 0.587, b: 0.114 }] - 計算亮度的 RGB 權重
* @returns {Array} - 代表累積直方圖的 Array 物件
*/
function createCumulativeHistogram(sourceImage, weights = { r: 0.299, g: 0.587, b: 0.114 }) {
const histogramData = createBrightnessHistogram(sourceImage, weights);
const totalPixels = sourceImage.width * sourceImage.height;
if (totalPixels === 0) {
// 處理空白影像情況
return BmpImage.create(256, 256);
}
// 步驟 2: 計算累積分佈
const cumulativeHistogramData = new Array(256).fill(0);
cumulativeHistogramData[0] = histogramData[0];
for (let i = 1; i < 256; i++) {
cumulativeHistogramData[i] = cumulativeHistogramData[i - 1] + histogramData[i];
}
return cumulativeHistogramData;
}
/**
* 計算並繪製影像的亮度累積直方圖
* @param {BmpImage} sourceImage - 來源影像
* @param {object} [weights={ r: 0.299, g: 0.587, b: 0.114 }] - 計算亮度的 RGB 權重
* @returns {BmpImage} - 代表累積直方圖的 BmpImage 物件
*/
function createCumulativeHistogramToBmp(sourceImage, weights = { r: 0.299, g: 0.587, b: 0.114 }) {
// 步驟 1: 計算累積分佈
const cumulativeHistogramData = createCumulativeHistogram(sourceImage, weights);
// 步驟 2: 累積直方圖的最大值就是總像素數
// const maxCumulativeCount = cumulativeHistogramData[255]; // 實際上就是 totalPixels
// 步驟 3: 繪製累積直方圖影像
const cumulativeHistogramImage = BmpImage.create(256, 256);
for (let i = 0; i < 256; i++) {
// 計算正規化後的高度
// 注意:這裡的正規化是基於 totalPixels,而不是 maxCount
const barHeight = Math.round((cumulativeHistogramData[i] / totalPixels) * 255);
// 畫出垂直的長條
for (let y = 0; y < barHeight; y++) {
const x = i;
cumulativeHistogramImage.setPixel(x, 255 - y, 255, 255, 255); // 畫白色
}
}
return cumulativeHistogramImage;
}
export { createCumulativeHistogram, createCumulativeHistogramToBmp };3.2 程式碼說明
引入
createBrightnessHistogram:- 為了避免重複程式碼,我們直接引入了
BrightnessHistogram.js中的createBrightnessHistogram函式,用於獲取基礎的亮度分佈數據。
- 為了避免重複程式碼,我們直接引入了
createCumulativeHistogramToBmp函式:- 接收來源影像
sourceImage和可選的weights參數,回傳一個代表累積直方圖的BmpImage物件。
- 接收來源影像
計算總像素數
totalPixels:- 這是累積直方圖 Y 軸的理論最大值。
計算累積分佈 (
cumulativeHistogramData):- 這是這個函式最核心的部分。一個簡單的迴圈實現了累積計算,
cumulativeHistogramData[i]會儲存從 0 到i所有亮度值的像素總和。
- 這是這個函式最核心的部分。一個簡單的迴圈實現了累積計算,
繪製迴圈:
- 遍歷 256 個亮度級別。
barHeight的計算是將當前亮度級別的累積像素數cumulativeHistogramData[i],除以totalPixels,然後再乘以255(輸出畫布的最大高度)。cumulativeHistogramImage.setPixel(i, 255 - y, ...):這裡的255 - y確保了繪製時是從畫布的底部向上畫線,符合直方圖的視覺習慣,同時也正確處理了 BMP 的 Y 軸翻轉特性。
4. 使用範例
現在,我們來實際使用這個函式,讀取一張圖片,並為它產生一張累積直方圖。
demo.js (或您的主執行檔案)
import BmpImage from "./BmpImage.js";
import { createCumulativeHistogramToBmp } from "./CumulativeHistogram.js"; // 引入累積直方圖的函式
// 載入來源影像 (您可以替換為任何 24-bit 的 BMP 檔案)
const sourceImg = BmpImage.load("test.bmp");
console.log("Loaded source image:", sourceImg.width, "x", sourceImg.height);
// 繪製亮度累積直方圖
console.log("Generating cumulative histogram...");
const cumulativeHistogramImg = createCumulativeHistogramToBmp(sourceImg);
// 儲存累積直方圖影像
cumulativeHistogramImg.save("test_cumulative_histogram.bmp");
console.log("Saved cumulative histogram image to test_cumulative_histogram.bmp.");
// -------------------------------------------------------------
// 範例:使用不同的灰階權重來產生累積直方圖
const avgWeights = { r: 1/3, g: 1/3, b: 1/3 };
const cumulativeHistogramAvg = createCumulativeHistogramToBmp(sourceImg, avgWeights);
cumulativeHistogramAvg.save("test_cumulative_histogram_avg.bmp");
console.log("Saved average cumulative histogram.");
說明:
- 首先載入
test.bmp圖片。 - 呼叫
createCumulativeHistogramToBmp函式來產生累積直方圖的BmpImage物件。 - 將結果存檔為
test_cumulative_histogram.bmp。 - 執行後,您會得到一張圖片,顯示一條從左下角緩慢上升至右上角的曲線,這正是影像的累積亮度分佈。
5. 小結與延伸思考
- 直方圖的進階理解:亮度直方累積圖讓我們從另一個角度理解影像的像素分佈。它不僅告訴我們每個亮度有多少像素,更告訴我們有多少像素是「暗於」或「亮於」某個特定值。
- 直方圖等化的基礎:這份數據是實現直方圖等化演算法的直接基礎。透過將這條累積曲線「拉直」,我們就能實現影像對比度的自動增強。
- 模組化開發:我們再次展示了如何重用已有的程式碼 (
createBrightnessHistogram),這使得我們的工具集更高效、更易於維護。
延伸操作提示:
- 彩色累積直方圖:你能否為 R, G, B 三個通道分別生成累積直方圖,並繪製在同一張圖片上?它們會是三條不同的曲線。
- 理論與實踐:找到一張對比度很差的圖片 (例如整體偏暗或偏亮),觀察它的累積直方圖是什麼樣子,然後想像如果這條曲線變平坦,影像會如何變化。這將為下一章節的直方圖等化打下基礎。