Back
PQCHub

亮度直方圖

亮度直方圖是數位影像處理的基石之一。它將影像的亮度分佈情況視覺化,讓我們能一眼看出影像的曝光是過暗、過亮還是均衡。本章節將教你如何分析一張影像的像素,並繪製出它的亮度直方圖。


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

簡單來說,亮度直方圖是一張圖表,用來顯示影像中每個亮度級別的像素數量。

  1. X 軸 (橫軸):代表亮度值,範圍從 0 (純黑) 到 255 (純白)。
  2. Y 軸 (縱軸):代表擁有該亮度值的像素總數。

如果一張影像的直方圖中,像素大多集中在左側 (0 附近),代表影像偏暗;集中在右側 (255 附近),代表影像偏亮;若分佈均勻,則通常有較好的對比度。


2. 演算法實作步驟

要繪製一張直方圖,我們需要遵循以下幾個核心步驟,並將它封裝成一個函式。這個函式會接收一個 BmpImage 物件,並回傳一個新的、代表直方圖的 BmpImage 物件。

  1. 統計亮度分佈

    • 建立一個長度為 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]++
  2. 尋找最大值並正規化 (Normalization)

    • 完成統計後,histogramData 陣列中的某個值會是最高的,代表該亮度級別的像素最多。我們需要找出這個最大值 maxCount
    • 為了將直方圖繪製到固定高度的影像中(例如 256px 高),我們需要將所有計數值進行「正規化」,也就是按比例縮放。公式為:barHeight = (count / maxCount) * (輸出高度 - 1)
  3. 繪製直方圖影像

    • 使用 BmpImage.create(512, 512) 建立一張 256x256 的空白黑色畫布。
    • 我們的 X 軸有 256 個亮度級別,且畫布寬度為 256 像素。
    • 遍歷 histogramData 陣列(從 i = 0255)。
    • 計算出第 i 個亮度級別正規化後的高度 barHeight
    • 在畫布上對應的 X 座標(i * 2i * 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 程式碼說明

  1. createBrightnessHistogram 函式

    • 將質亮度統計直寫入Array,並回傳。
  2. 統計迴圈

    • 與先前灰階轉換的程式碼幾乎一樣,只是這次不是修改原圖,而是用計算出的 gray 值來更新 histogramData 陣列的計數。
  3. Math.max(...histogramData)

    • 這是 JavaScript 中尋找陣列最大值的簡便語法。
  4. 繪製迴圈 createBrightnessHistogramToBmp

    • 外層迴圈遍歷 256 個亮度級別。
    • 內層迴圈 (yw) 負責在畫布上填滿代表該亮度級別的長方形區域。
    • 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. 小結與延伸思考

  1. 直方圖的價值:我們學會了如何透過程式碼量化並視覺化影像的整體特徵。這不僅僅是畫一張圖,更是許多自動化演算法(如自動曝光、自動對比度)的基礎。
  2. 演算法的組合:這個實作完美地展示了如何結合使用 getPixel, create, setPixel 等我們自訂的工具來完成一個更複雜的分析任務。
  3. 不依賴外部函式庫:同樣地,我們完全基於 Node.js 的 Bufferfs 模組完成了所有操作,加深了對影像資料結構的理解。

延伸操作提示:

  1. 彩色直方圖:你能否修改 createBrightnessHistogram,讓它能分別計算 R, G, B 三個通道的直方圖,並用紅色、綠色、藍色的線條畫在同一張圖上?
  2. 分析不同影像:找一些特別亮、特別暗或對比度很低的圖片,產生它們的直方圖,並試著從直方圖的形狀來「解讀」影像的特性。