Back
PQCHub

建立 BMP 類別工具

將複雜的 BMP 檔案操作封裝成類別工具,讓你輕鬆讀取、修改與創造影像,專注於演算法實作。


1 BMP 類別

在先前的教學,我們已經學會如何讀取 BMP 檔頭,並解析基本影像資訊。利用BMP的規則進一步封裝一個自製的 BMP 類別 BmpImage,用來:

  1. 讀取整張 BMP 影像,包括像素資料
  2. 存檔 BMP 影像
  3. 取得或修改單一像素的顏色
  4. 建立空白影像方便後續實驗或演算法實作。

1.1 BMP 類別程式碼

import fs from "node:fs";

class BmpImage {
    /**
     * @param {number} width - 影像寬度
     * @param {number} height - 影像高度
     * @param {Buffer} pixelData - BGR 格式的原始像素資料
     */
    constructor(width, height, pixelData) {
        this.width = width;
        this.height = height;
        this.pixelData = pixelData;
        this.bytesPerPixel = 3; // 固定 24-bit
    }

    /**
     * 從 BMP 檔案載入影像 (只支援 24-bit)
     * @param {string} filePath - BMP 檔案路徑
     * @returns {BmpImage}
     */
    static load(filePath) {
        const buffer = fs.readFileSync(filePath);

        const width = buffer.readUInt32LE(18);
        const height = buffer.readUInt32LE(22);
        const bitCount = buffer.readUInt16LE(28);
        const dataOffset = buffer.readUInt32LE(10);

        if (bitCount !== 24) {
            throw new Error(`Unsupported BMP format: ${bitCount}-bit. Only 24-bit is supported.`);
        }

        const pixelData = buffer.slice(dataOffset);

        return new BmpImage(width, height, pixelData);
    }

    /**
     * 將影像儲存為 BMP 檔案 (24-bit)
     * @param {string} filePath - 儲存路徑
     */
    save(filePath) {
        const headerSize = 54;
        const pixelDataSize = this.pixelData.length;
        const fileSize = headerSize + pixelDataSize;

        const buffer = Buffer.alloc(fileSize);

        // --- 檔案頭 (14 bytes) ---
        buffer.write('BM', 0); 
        buffer.writeUInt32LE(fileSize, 2);
        buffer.writeUInt32LE(headerSize, 10); // Pixel data offset 固定 54

        // --- 資訊頭 (40 bytes) ---
        buffer.writeUInt32LE(40, 14); // 資訊頭大小
        buffer.writeUInt32LE(this.width, 18);
        buffer.writeUInt32LE(this.height, 22);
        buffer.writeUInt16LE(1, 26); // 平面數
        buffer.writeUInt16LE(this.bytesPerPixel * 8, 28); // 位元深度
        buffer.writeUInt32LE(0, 30); // 無壓縮
        buffer.writeUInt32LE(pixelDataSize, 34);
        buffer.writeUInt32LE(2835, 38); // 水平解析度
        buffer.writeUInt32LE(2835, 42); // 垂直解析度
        buffer.writeUInt32LE(0, 46); // 顏色數
        buffer.writeUInt32LE(0, 50); // 重要顏色數

        // --- 像素資料 ---
        this.pixelData.copy(buffer, headerSize);

        fs.writeFileSync(filePath, buffer);
    }

    /**
     * 取得指定座標的像素顏色 (RGB)
     * @param {number} x - X 座標
     * @param {number} y - Y 座標
     * @returns {object|null} 包含 r, g, b 屬性的物件
     */
    getPixel(x, y) {
        if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
            return null;
        }

        const bmpY = this.height - 1 - y;
        const rowBytes = this.width * this.bytesPerPixel;
        const rowPadding = Math.ceil(rowBytes / 4) * 4 - rowBytes;
        const index = bmpY * (rowBytes + rowPadding) + x * this.bytesPerPixel;

        return {
            r: this.pixelData[index + 2],
            g: this.pixelData[index + 1],
            b: this.pixelData[index]
        };
    }

    /**
     * 設定指定座標的像素顏色 (RGB)
     * @param {number} x - X 座標
     * @param {number} y - Y 座標
     * @param {number} r - 紅色值
     * @param {number} g - 綠色值
     * @param {number} b - 藍色值
     */
    setPixel(x, y, r, g, b) {
        if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
            return;
        }

        const bmpY = this.height - 1 - y;
        const rowBytes = this.width * this.bytesPerPixel;
        const rowPadding = Math.ceil(rowBytes / 4) * 4 - rowBytes;
        const index = bmpY * (rowBytes + rowPadding) + x * this.bytesPerPixel;

        this.pixelData[index] = b;
        this.pixelData[index + 1] = g;
        this.pixelData[index + 2] = r;
    }

    /**
     * 建立一個新的空白影像 (24-bit RGB)
     * @param {number} width - 影像寬度
     * @param {number} height - 影像高度
     * @returns {BmpImage}
     */
    static create(width, height) {
        const bytesPerPixel = 3; 
        const rowBytes = width * bytesPerPixel;
        const rowPadding = Math.ceil(rowBytes / 4) * 4 - rowBytes;
        const pixelDataSize = (rowBytes + rowPadding) * height;

        const pixelData = Buffer.alloc(pixelDataSize, 0); // 初始為黑色
        return new BmpImage(width, height, pixelData);
    }
}

export default BmpImage

1.2 程式碼說明

  1. 建構子 constructor

    • 初始化寬度、高度與像素資料
    • 固定 24-bit BMP(RGB,每像素 3 bytes)
  2. load 方法

    • 從檔案讀取 BMP,解析 width、height、pixelData
    • 只接受 24-bit BMP,如果檔案不是 24-bit 會拋錯
  3. save 方法

    • pixelData 與 BMP 檔頭寫入檔案
    • 自動處理檔案大小與 header 偏移
  4. getPixel / setPixel

    • 取得或設定指定 (x, y) 的像素
    • 自動處理 BMP 的 padding 與 y 軸翻轉
  5. create 靜態方法

    • 生成空白影像(黑色),可指定寬高
    • 計算每行 padding,確保 BMP 正確儲存

2 讀取與修改像素示範

我們將使用上面建立的 BmpImage 類別,實作以下操作:

  1. 讀取 BMP 影像像素
  2. 修改單一像素的顏色
  3. 儲存修改後的影像
  4. 簡單示範用迴圈修改整張影像

透過這些練習,你可以熟悉 BMP 的像素排列 以及 padding 與 y 軸翻轉 的處理方式。


2.1 載入影像與讀取單一像素

import BmpImage from "./BmpImage.js";

// 載入 BMP 影像
const img = BmpImage.load("test.bmp");

// 讀取 (10, 10) 的像素顏色
const pixel = img.getPixel(10, 10);
console.log("Pixel at (10,10):", pixel);

3.5 小結

  1. 使用自製 BmpImage 類別即可完成多種影像操作
  2. 練習亮度調整、反色、二值化可以讓學生理解演算法對像素的直接影響
  3. 這種方法不依賴任何第三方模組,對學習底層影像結構非常有幫助

說明:

  • getPixel(x, y) 回傳物件 { r, g, b }
  • BMP 的像素資料從左下角開始存,所以 y 軸會自動翻轉
  • 這樣可以直接用常見的 (0,0) 左上角座標操作

2.2 修改單一像素

// 設定 (10, 10) 為紅色
img.setPixel(10, 10, 255, 0, 0);

// 儲存修改後的檔案
img.save("test_modified.bmp");
console.log("Saved modified image.");

說明:

  • setPixel(x, y, r, g, b) 將指定像素改為目標顏色
  • 原始 BMP 為 BGR 排列,setPixel 已自動處理順序
  • 儲存後可用任意圖片瀏覽器打開,檢查效果

2.3 批量修改像素

假設我們要將整張影像變成灰階:

for (let y = 0; y < img.height; y++) {
    for (let x = 0; x < img.width; x++) {
        const { r, g, b } = img.getPixel(x, y);
        // 灰階計算公式
        const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
        img.setPixel(x, y, gray, gray, gray);
    }
}

img.save("test_gray.bmp");
console.log("Saved grayscale image.");

說明:

  • 使用雙層迴圈依序讀取每個像素
  • 計算灰階後,再設定回去
  • 這裡完整展示如何用 自製 BMP 類別實作演算法

2.4 延伸操作提示

  1. 你可以用同樣方式實作:

    • 反轉顏色 (invert)
    • 調整亮度 (brightness)
    • 局部修改區域像素
  2. 這種方式完全不依賴第三方模組,讓學生理解 像素資料結構BMP padding


3 進階操作與影像演算法示範

3.1 建立新影像

import BmpImage from "./BmpImage.js";

// 建立一個 200x200 黑色影像
const newImg = BmpImage.create(200, 200);
console.log("Created new image:", newImg.width, "x", newImg.height);

// 將中心 100x100 區域設為紅色
for (let y = 50; y < 150; y++) {
    for (let x = 50; x < 150; x++) {
        newImg.setPixel(x, y, 255, 0, 0);
    }
}

newImg.save("new_image.bmp");
console.log("Saved new image with red square.");

說明:

  • 使用 BmpImage.create(width, height) 生成空白黑色影像
  • 用迴圈修改像素可畫出簡單圖案
  • 完整理解 BMP pixel buffer 與 padding

3.2 亮度調整演算法

function adjustBrightness(img, factor) {
    for (let y = 0; y < img.height; y++) {
        for (let x = 0; x < img.width; x++) {
            const { r, g, b } = img.getPixel(x, y);
            const nr = Math.min(255, Math.max(0, r * factor));
            const ng = Math.min(255, Math.max(0, g * factor));
            const nb = Math.min(255, Math.max(0, b * factor));
            img.setPixel(x, y, nr, ng, nb);
        }
    }
}

const img1 = BmpImage.load("test.bmp");
adjustBrightness(img1, 1.2); // 提高亮度 20%
img1.save("test_bright.bmp");
console.log("Saved brightness-adjusted image.");

說明:

  • 遍歷每個像素,對 RGB 值乘以係數
  • 使用 Math.min / Math.max 限制在 [0,255] 範圍
  • 學生可修改 factor 實驗不同亮度效果

3.3 反色 (Invert) 演算法

function invertImage(img) {
    for (let y = 0; y < img.height; y++) {
        for (let x = 0; x < img.width; x++) {
            const { r, g, b } = img.getPixel(x, y);
            img.setPixel(x, y, 255 - r, 255 - g, 255 - b);
        }
    }
}

const img2 = BmpImage.load("test.bmp");
invertImage(img2);
img2.save("test_invert.bmp");
console.log("Saved inverted image.");

說明:

  • 每個像素 RGB 值取反
  • 這是影像處理常見基礎操作
  • 幫助學生理解 像素操作對影像的直接影響

3.4 二值化 (Thresholding)

function thresholdImage(img, threshold = 128) {
    for (let y = 0; y < img.height; y++) {
        for (let x = 0; x < img.width; x++) {
            const { r, g, b } = img.getPixel(x, y);
            const gray = 0.299 * r + 0.587 * g + 0.114 * b;
            const value = gray >= threshold ? 255 : 0;
            img.setPixel(x, y, value, value, value);
        }
    }
}

const img3 = BmpImage.load("test.bmp");
thresholdImage(img3, 128);
img3.save("test_threshold.bmp");
console.log("Saved thresholded image.");

說明:

  • 將灰階值與閾值比較,產生黑白影像
  • 學生可調整 threshold 觀察效果
  • 二值化是許多影像演算法(邊緣檢測、OCR)的基礎

SNPQ © 2025