首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# GDI+ 數位影像處理實戰:色彩反轉、灰階、亮度調整與 Unsafe 高效能存取

     編輯:關於C語言

專案架構:WinForms + GDI+ + Unsafe 程式碼

透過 Bitmap.LockBits 取得記憶體指標,配合 unsafe 區塊直接操作像素陣列,實現高效能影像處理。

核心類別:Filters 靜態工具類


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;
        }
    }
}

WinForms 主程式整合


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);
    }
}

專案設定:啟用 Unsafe 程式碼

  1. 專案右鍵 → Properties → Build
  2. 勾選 Allow unsafe code (允許不安全程式碼)

或在 `.csproj` 加入:



  true

關鍵技術深度解析

1. BitmapData 與記憶體配置


BitmapData data = bmp.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
// data.Scan0: 第一像素位址 (IntPtr)
// data.Stride: 每掃描行位元組數 (含記憶體對齊填充,通常 >= Width * 3)
// data.Width/Height: 影像尺寸

Stride 可能大於 Width * 3:Windows 要求掃描行 4 位元組對齊,故每行結尾可能有填充位元組。

2. Unsafe 指標運算


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;  // 跳過填充,移至下一行開頭
    }
}

3. 效能比較

方法10MP 影像處理時間相對效能
GetPixel/SetPixel~15,000 ms1x (基準)
LockBits + unsafe 指標~15 ms1000x
LockBits + Marshal.Copy 陣列~30 ms500x
ImageSharp/SkiaSharp~20 ms750x (跨平台)

進階濾鏡擴充範例

對比度調整


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;
        }
    });
}

銳化濾鏡 (3x3 核卷積)


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[,] 陣列運算後寫回
    });
}

現代替代方案:ImageSharp (純 Managed、跨平台)


// 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 * bytesPerPixel
  • 灰階使用 ITU-R BT.601 加權:0.299R + 0.587G + 0.114B
  • 亮度調整需飽和運算:clamp(value + adjustment, 0, 255)
  • 所有非受控資源 (Bitmap、Graphics、BitmapData) 必須正確釋放
  • 現代專案強烈建議使用 ImageSharp 或 SkiaSharp,避免 unsafe、跨平台、無 GDI+ 限制
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved