透過 Bitmap.LockBits 取得記憶體指標,配合 unsafe 區塊直接操作像素陣列,實現高效能影像處理。
using System;
using System.Drawing;
using System.Drawing.Imaging;
public static class Filters
{
// 像素格式常數:24bpp RGB (每像素 3 bytes: B, G, R)
private const PixelFormat PixelFmt = PixelFormat.Format24bppRgb;
/// 色彩反轉:每通道 255 - value
public static bool Invert(Bitmap bmp)
{
return ProcessBitmap(bmp, (ptr, stride, width, height) =>
{
int offset = stride - width * 3;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width * 3; x++)
{
ptr[0] = (byte)(255 - ptr[0]);
ptr++;
}
ptr += offset;
}
});
}
/// 灰階:加權平均法 (ITU-R BT.601)
public static bool Grayscale(Bitmap bmp)
{
return ProcessBitmap(bmp, (ptr, stride, width, height) =>
{
int offset = stride - width * 3;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
byte blue = ptr[0];
byte green = ptr[1];
byte red = ptr[2];
// 灰階權重:R*0.299 + G*0.587 + B*0.114
byte gray = (byte)(0.299 * red + 0.587 * green + 0.114 * blue);
ptr[0] = ptr[1] = ptr[2] = gray;
ptr += 3;
}
ptr += offset;
}
});
}
/// 亮度調整:[-255, 255] 範圍
public static bool Brightness(Bitmap bmp, int adjustment)
{
if (adjustment < -255 || adjustment > 255) return false;
return ProcessBitmap(bmp, (ptr, stride, width, height) =>
{
int offset = stride - width * 3;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width * 3; x++)
{
int val = ptr[0] + adjustment;
if (val < 0) val = 0;
else if (val > 255) val = 255;
ptr[0] = (byte)val;
ptr++;
}
ptr += offset;
}
});
}
/// 通用 Bitmap 處理器:LockBits -> Unsafe 處理 -> UnlockBits
private static bool ProcessBitmap(Bitmap bmp, Action processor)
{
if (bmp == null) return false;
try
{
BitmapData data = bmp.LockBits(
new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadWrite, PixelFmt);
unsafe
{
byte* ptr = (byte*)data.Scan0.ToPointer();
processor(ptr, data.Stride, bmp.Width, bmp.Height);
}
bmp.UnlockBits(data);
return true;
}
catch
{
return false;
}
}
}
using System;
using System.Drawing;
using System.Windows.Forms;
public class ImageProcessorForm : Form
{
private Bitmap originalBitmap;
private Bitmap workingBitmap;
private PictureBox pictureBox;
private MenuStrip menuStrip;
public ImageProcessorForm()
{
InitializeComponent();
originalBitmap = new Bitmap(1, 1); // 避免 null
workingBitmap = new Bitmap(1, 1);
}
private void InitializeComponent()
{
// PictureBox 設定
pictureBox = new PictureBox
{
Dock = DockStyle.Fill,
SizeMode = PictureBoxSizeMode.AutoSize
};
// 啟用雙緩衝與自動捲動
this.AutoScroll = true;
pictureBox.Paint += (s, e) =>
{
if (workingBitmap != null)
e.Graphics.DrawImage(workingBitmap, AutoScrollPosition);
};
// 選單建立
menuStrip = new MenuStrip();
var fileMenu = new ToolStripMenuItem("檔案(&F)");
fileMenu.DropDownItems.Add("開啟(&O)", null, MenuOpen_Click);
fileMenu.DropDownItems.Add("儲存(&S)", null, MenuSave_Click);
fileMenu.DropDownItems.Add(new ToolStripSeparator());
fileMenu.DropDownItems.Add("結束(&X)", null, (s,e) => Close());
var filterMenu = new ToolStripMenuItem("影像處理(&I)");
filterMenu.DropDownItems.Add("色彩反轉(&I)", null, MenuInvert_Click);
filterMenu.DropDownItems.Add("灰階(&G)", null, MenuGrayscale_Click);
filterMenu.DropDownItems.Add("亮度調整(&B)", null, MenuBrightness_Click);
menuStrip.Items.Add(fileMenu);
menuStrip.Items.Add(filterMenu);
Controls.Add(pictureBox);
Controls.Add(menuStrip);
MainMenuStrip = menuStrip;
Text = "C# 數位影像處理示範";
ClientSize = new Size(800, 600);
}
private void MenuOpen_Click(object sender, EventArgs e)
{
using (var dlg = new OpenFileDialog
{
Filter = "影像檔案|*.bmp;*.jpg;*.jpeg;*.png;*.tiff|所有檔案|*.*"
})
{
if (dlg.ShowDialog() == DialogResult.OK)
{
originalBitmap?.Dispose();
workingBitmap?.Dispose();
originalBitmap = new Bitmap(dlg.FileName);
workingBitmap = new Bitmap(originalBitmap); // 複本供處理
pictureBox.Image = workingBitmap;
AutoScrollMinSize = workingBitmap.Size;
Invalidate();
}
}
}
private void MenuSave_Click(object sender, EventArgs e)
{
if (workingBitmap == null) return;
using (var dlg = new SaveFileDialog
{
Filter = "BMP|*.bmp|JPEG|*.jpg|PNG|*.png|TIFF|*.tiff"
})
{
if (dlg.ShowDialog() == DialogResult.OK)
{
ImageFormat fmt = ImageFormat.Bmp;
string ext = Path.GetExtension(dlg.FileName).ToLower();
if (ext == ".jpg" || ext == ".jpeg") fmt = ImageFormat.Jpeg;
else if (ext == ".png") fmt = ImageFormat.Png;
else if (ext == ".tiff") fmt = ImageFormat.Tiff;
workingBitmap.Save(dlg.FileName, fmt);
}
}
}
private void ApplyFilter(Func filter, string name)
{
if (workingBitmap == null) return;
if (filter(workingBitmap))
{
pictureBox.Image = workingBitmap;
Invalidate();
Text = $"{name} 完成 - C# 數位影像處理";
}
else
{
MessageBox.Show($"{name} 處理失敗");
}
}
private void MenuInvert_Click(object sender, EventArgs e)
=> ApplyFilter(Filters.Invert, "色彩反轉");
private void MenuGrayscale_Click(object sender, EventArgs e)
=> ApplyFilter(Filters.Grayscale, "灰階");
private void MenuBrightness_Click(object sender, EventArgs e)
{
using (var dlg = new BrightnessDialog())
{
if (dlg.ShowDialog() == DialogResult.OK)
ApplyFilter(b => Filters.Brightness(b, dlg.Value), "亮度調整");
}
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ImageProcessorForm());
}
}
// 亮度調整對話框
public class BrightnessDialog : Form
{
public int Value { get; private set; }
private TrackBar trackBar;
public BrightnessDialog()
{
trackBar = new TrackBar
{
Minimum = -255, Maximum = 255, Value = 0,
Dock = DockStyle.Top, TickFrequency = 10
};
var btnOk = new Button { Text = "確定", DialogResult = DialogResult.OK, Dock = DockStyle.Bottom };
var btnCancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel, Dock = DockStyle.Bottom };
Controls.Add(trackBar);
Controls.Add(btnOk);
Controls.Add(btnCancel);
AcceptButton = btnOk;
CancelButton = btnCancel;
Text = "亮度調整 (-255 ~ 255)";
ClientSize = new Size(300, 150);
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
if (DialogResult == DialogResult.OK) Value = trackBar.Value;
base.OnClosing(e);
}
}
或在 `.csproj` 加入:
true
BitmapData data = bmp.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
// data.Scan0: 第一像素位址 (IntPtr)
// data.Stride: 每掃描行位元組數 (含記憶體對齊填充,通常 >= Width * 3)
// data.Width/Height: 影像尺寸
Stride 可能大於 Width * 3:Windows 要求掃描行 4 位元組對齊,故每行結尾可能有填充位元組。
unsafe
{
byte* ptr = (byte*)data.Scan0.ToPointer(); // 起始指標
int offset = stride - width * 3; // 每行結尾填充位元組數
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
// BGR 順序 (GDI+ 內部格式)
byte b = ptr[0];
byte g = ptr[1];
byte r = ptr[2];
// ... 處理邏輯 ...
ptr += 3; // 移至下一像素
}
ptr += offset; // 跳過填充,移至下一行開頭
}
}
| 方法 | 10MP 影像處理時間 | 相對效能 |
|---|---|---|
GetPixel/SetPixel | ~15,000 ms | 1x (基準) |
LockBits + unsafe 指標 | ~15 ms | 1000x |
LockBits + Marshal.Copy 陣列 | ~30 ms | 500x |
| ImageSharp/SkiaSharp | ~20 ms | 750x (跨平台) |
public static bool Contrast(Bitmap bmp, double factor) // factor: 0.0 ~ 3.0
{
if (factor <= 0) return false;
byte[] lut = new byte[256];
for (int i = 0; i < 256; i++)
{
double val = (i / 255.0 - 0.5) * factor + 0.5;
val = Math.Max(0, Math.Min(1, val)) * 255;
lut[i] = (byte)val;
}
return ProcessBitmap(bmp, (ptr, stride, width, height) =>
{
int offset = stride - width * 3;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width * 3; x++)
ptr[x] = lut[ptr[x]];
ptr += stride;
}
});
}
public static bool Sharpen(Bitmap bmp)
{
// 銳化核: [[0,-1,0], [-1,5,-1], [0,-1,0]]
// 需要完整影像緩衝,建議先 Marshal.Copy 到陣列處理
// 此處略過完整實作,展示概念
return ProcessBitmap(bmp, (ptr, stride, width, height) =>
{
// 卷積運算需讀取鄰近像素,指標操作較複雜
// 建議:複製到 byte[,] 或 float[,] 陣列運算後寫回
});
}
// NuGet: SixLabors.ImageSharp
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
public void ProcessWithImageSharp(string inputPath, string outputPath)
{
using (Image image = Image.Load(inputPath))
{
// 色彩反轉
image.Mutate(x => x.Invert());
// 灰階
// image.Mutate(x => x.Grayscale());
// 亮度/對比度
// image.Mutate(x => x.Brightness(1.2f).Contrast(1.1f));
image.Save(outputPath);
}
}
LockBits + unsafe 指標是 GDI+ 高效能像素存取的標準模式Format24bppRgb 記憶體順序為 BGR (非 RGB)Stride 包含記憶體對齊填充,行末需跳過 offset = stride - width * bytesPerPixel0.299R + 0.587G + 0.114Bclamp(value + adjustment, 0, 255)Bitmap、Graphics、BitmapData) 必須正確釋放ImageSharp 或 SkiaSharp,避免 unsafe、跨平台、無 GDI+ 限制