建立 BMP 類別工具
將複雜的 BMP 檔案操作封裝成類別工具,讓你輕鬆讀取、修改與創造影像,專注於演算法實作。
1 BMP 類別
在先前的教學,我們已經學會如何讀取 BMP 檔頭,並解析基本影像資訊。利用BMP的規則進一步封裝一個自製的 BMP 類別 BmpImage,用來:
- 讀取整張 BMP 影像,包括像素資料
- 存檔 BMP 影像
- 取得或修改單一像素的顏色
- 建立空白影像方便後續實驗或演算法實作。
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 程式碼說明
建構子
constructor
- 初始化寬度、高度與像素資料
- 固定 24-bit BMP(RGB,每像素 3 bytes)
load
方法- 從檔案讀取 BMP,解析 width、height、pixelData
- 只接受 24-bit BMP,如果檔案不是 24-bit 會拋錯
save
方法- 將
pixelData
與 BMP 檔頭寫入檔案 - 自動處理檔案大小與 header 偏移
- 將
getPixel
/setPixel
- 取得或設定指定
(x, y)
的像素 - 自動處理 BMP 的 padding 與 y 軸翻轉
- 取得或設定指定
create
靜態方法- 生成空白影像(黑色),可指定寬高
- 計算每行 padding,確保 BMP 正確儲存
2 讀取與修改像素示範
我們將使用上面建立的 BmpImage
類別,實作以下操作:
- 讀取 BMP 影像像素
- 修改單一像素的顏色
- 儲存修改後的影像
- 簡單示範用迴圈修改整張影像
透過這些練習,你可以熟悉 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 小結
- 使用自製
BmpImage
類別即可完成多種影像操作 - 練習亮度調整、反色、二值化可以讓學生理解演算法對像素的直接影響
- 這種方法不依賴任何第三方模組,對學習底層影像結構非常有幫助
說明:
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 延伸操作提示
你可以用同樣方式實作:
- 反轉顏色 (invert)
- 調整亮度 (brightness)
- 局部修改區域像素
這種方式完全不依賴第三方模組,讓學生理解 像素資料結構 與 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)的基礎