Back
PQCHub

亮度直方累積圖

亮度直方累積圖 (Cumulative Histogram) 是直方圖的延伸,它能更細緻地展現影像像素的累積分佈狀況。這對於理解影像的整體對比度、判斷是否適合進行直方圖等化等進階處理,都提供了重要的視覺依據。


1. 什麼是亮度直方累積圖 (Cumulative Brightness Histogram)?

回顧亮度直方圖,它顯示的是每個亮度級別的像素數量。而亮度直方累積圖則更進一步,它在每個亮度級別 i 上,顯示的是亮度值小於或等於 i 的所有像素的總和

用數學式表示就是: CumulativeCount[i] = Sum (PixelCount[j]) for all j <= i

累積直方圖的特性:

  1. X 軸 (橫軸):同樣代表亮度值,範圍從 0 (純黑) 到 255 (純白)。
  2. Y 軸 (縱軸):代表所有亮度值小於等於 X 軸對應值的像素總數。因此,Y 軸的最大值將是影像的總像素數量
  3. 曲線形態:累積直方圖總是呈現一條非遞減的曲線。它從左側接近 0 的值開始,逐漸上升,直到右側達到影像的總像素數。

分析累積直方圖可以幫助我們:

  • 判斷對比度:一條「理想」的、對比度良好的影像,其累積直方圖通常會呈現一條近似直線、均勻上升的斜線。
  • 識別過暗/過亮:如果曲線在左側快速上升,表示影像有很多暗部細節;如果曲線在右側才陡峭上升,表示影像有很多亮部細節。
  • 直方圖等化基礎:累積直方圖是實現直方圖等化 (Histogram Equalization) 演算法的關鍵輸入。

2. 演算法實作步驟

要繪製一張直方累積圖,我們將利用先前計算亮度直方圖的結果,在此基礎上增加「累積」步驟。

  1. 統計亮度分佈 (同亮度直方圖)

    • 首先,我們需要影像的原始亮度直方圖 histogramData,它記錄了每個亮度級別的像素數量。我們可以直接呼叫前面定義的 createBrightnessHistogram 函式來取得這份數據。
    • 同時,我們需要計算影像的總像素數量 totalPixels,這將是累積直方圖的最大值。
  2. 計算累積分佈

    • 建立一個長度為 256 的新陣列 cumulativeHistogramData
    • cumulativeHistogramData[0] 設為 histogramData[0]
    • i = 1255,依序計算: cumulativeHistogramData[i] = cumulativeHistogramData[i-1] + histogramData[i]
    • 最終 cumulativeHistogramData[255] 會等於 totalPixels
  3. 正規化與繪製直方累積圖影像

    • 使用 BmpImage.create(width, height) 建立一張空白的黑色畫布,例如 256x256 像素。
    • 遍歷 cumulativeHistogramData 陣列(從 i = 0255)。
    • 計算出第 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 程式碼說明

  1. 引入 createBrightnessHistogram

    • 為了避免重複程式碼,我們直接引入了 BrightnessHistogram.js 中的 createBrightnessHistogram 函式,用於獲取基礎的亮度分佈數據。
  2. createCumulativeHistogramToBmp 函式

    • 接收來源影像 sourceImage 和可選的 weights 參數,回傳一個代表累積直方圖的 BmpImage 物件。
  3. 計算總像素數 totalPixels

    • 這是累積直方圖 Y 軸的理論最大值。
  4. 計算累積分佈 (cumulativeHistogramData)

    • 這是這個函式最核心的部分。一個簡單的迴圈實現了累積計算,cumulativeHistogramData[i] 會儲存從 0 到 i 所有亮度值的像素總和。
  5. 繪製迴圈

    • 遍歷 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. 小結與延伸思考

  1. 直方圖的進階理解:亮度直方累積圖讓我們從另一個角度理解影像的像素分佈。它不僅告訴我們每個亮度有多少像素,更告訴我們有多少像素是「暗於」或「亮於」某個特定值。
  2. 直方圖等化的基礎:這份數據是實現直方圖等化演算法的直接基礎。透過將這條累積曲線「拉直」,我們就能實現影像對比度的自動增強。
  3. 模組化開發:我們再次展示了如何重用已有的程式碼 (createBrightnessHistogram),這使得我們的工具集更高效、更易於維護。

延伸操作提示:

  1. 彩色累積直方圖:你能否為 R, G, B 三個通道分別生成累積直方圖,並繪製在同一張圖片上?它們會是三條不同的曲線。
  2. 理論與實踐:找到一張對比度很差的圖片 (例如整體偏暗或偏亮),觀察它的累積直方圖是什麼樣子,然後想像如果這條曲線變平坦,影像會如何變化。這將為下一章節的直方圖等化打下基礎。