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

WPF 基礎:位圖與像素位元操作完全指南

     編輯:關於.NET

WPF 保留模式圖形系統為 Windows 圖形程式設計帶來巨大變革。程式無需在系統要求時重繪視覺外觀,合成系統保留所有圖形數據並安排至整個視覺樹中。保留模式確實簡化工作流程,但對 Windows 程式設計師,「簡便」非首要目標。正是保留模式圖形系統與通知機制(相依性屬性)的結合,釋放 WPF 的靈活性與功能。圖形物件(路徑、筆刷)看似「存在」於合成系統中,持續回應屬性變更與圖形轉換,從而支援資料繫結與動畫。

近期發現 WPF 位圖同樣具備動態響應特性:呈現的位圖不僅回應圖形轉換,還能響應位圖中實際像素位元的變更。展現此動態響應的兩個類別是 RenderTargetBitmap 與 WriteableBitmap,均繼承自 BitmapSource(WPF 所有位圖支援的抽象基礎類別)。程式可將位圖物件與 Image 元素顯示、用 ImageBrush 製成平鋪筆刷、或用 ImageDrawing 作為大型繪圖一部分,無論何種用法,呈現後不會被忽略,位圖仍留在視覺合成系統中,持續響應應用程式變更。

RenderTargetBitmap:將 Visual 繪製至位圖

RenderTargetBitmap 透過將 Visual 類型物件傳輸至其表面進行有效繪製。建構函數需提供:像素寬高、水平/垂直解析度(DPI)、PixelFormat 物件。建立時必須使用 PixelFormats.Default 或 PixelFormats.Pbgra32,皆可建立 32-bit 透明位圖。

初始完全透明。呼叫 Render 方法傳入 Visual 物件(含 FrameworkElement、Control 等衍生類別)即可繪製。呼叫 Clear 可還原透明。若位圖正在顯示,變更立即反映。

範例:RenderTargetBitmapDemo

class RenderTargetBitmapDemo : Window {
    RenderTargetBitmap bitmap;
    public RenderTargetBitmapDemo() {
        Title = "RenderTargetBitmap Demo";
        SizeToContent = SizeToContent.WidthAndHeight;
        bitmap = new RenderTargetBitmap(1200, 900, 300, 300, PixelFormats.Default);
        Image img = new Image(); img.Stretch = Stretch.None; img.Source = bitmap; Content = img;
    }
    protected override void OnMouseDown(MouseButtonEventArgs args) {
        Point ptMouse = args.GetPosition(this);
        Random rand = new Random();
        Brush brush = new SolidColorBrush(Color.FromRgb((byte)rand.Next(256), (byte)rand.Next(256), (byte)rand.Next(256)));
        DrawingVisual vis = new DrawingVisual();
        DrawingContext dc = vis.RenderOpen();
        dc.DrawEllipse(brush, null, ptMouse, 12, 12);
        dc.Close();
        bitmap.Render(vis); // 圓形即時出現在顯示位圖上
    }
    protected override void OnClosed(EventArgs args) {
        // 以 PNG 儲存合成位圖
        PngBitmapEncoder enc = new PngBitmapEncoder();
        enc.Frames.Add(BitmapFrame.Create(bitmap));
        FileStream stream = new FileStream("RenderTargetBitmapDemo.png", FileMode.Create, FileAccess.Write);
        enc.Save(stream); stream.Close();
    }
}

建構函數指定 300 DPI,結合像素尺寸建立 4x3 英吋位圖。WPF 與裝置無關座標系統中 1 英吋 = 96 單位,故裝置無關寬度 384、高度 288(對應 Width/Height 屬性)。

Image 元素透過 OnRender 中 DrawingContext.DrawImage 顯示位圖。修改位圖時 Image 不會重複呼叫 OnRender,變更發生在視覺合成系統更深層。程式結束時以 PNG 儲存,檔案大小 1200x900 像素,每圓直徑 75 像素(300 DPI 下 1/4 英吋)。

WriteableBitmap:直接操作像素陣列

WriteableBitmap 有兩個建構函數,一個類似 RenderTargetBitmap。前四參數為像素尺寸與解析度,第五參數為 PixelFormat(比 RenderTargetBitmap 彈性大),支援調色盤格式時額外提供調色盤參數。

像素初始化為零,具體含義視像素格式而定:多為全黑,支援透明則為透明,有調色盤則顯示調色盤第一種顏色。

修改 WriteableBitmap 需呼叫 WritePixels,將本地陣列實際像素位元複製至位圖。陣列資料格式與大小必須符合位圖尺寸與像素格式。

範例:AnimatedBitmapBrush 動態平鋪筆刷

class AnimatedBitmapBrush : Window {
    const int COLS = 48, ROWS = 48;
    WriteableBitmap bitmap;
    byte[] pixels = new byte[COLS * ROWS];
    byte pixelLevel = 0x00;
    public AnimatedBitmapBrush() {
        Title = "Animated Bitmap Brush"; Width = Height = 300;
        bitmap = new WriteableBitmap(COLS, ROWS, 96, 96, PixelFormats.Gray8, null);
        ImageBrush brush = new ImageBrush(bitmap);
        brush.TileMode = TileMode.Tile;
        brush.Viewport = new Rect(0, 0, COLS, ROWS);
        brush.ViewportUnits = BrushMappingMode.Absolute;
        Background = brush;
        DispatcherTimer tmr = new DispatcherTimer();
        tmr.Interval = TimeSpan.FromMilliseconds(100);
        tmr.Tick += TimerOnTick; tmr.Start();
    }
    void TimerOnTick(object sender, EventArgs args) {
        for (int row = 0; row < ROWS; row++)
            for (int col = 0; col < COLS; col++) {
                int index = row * COLS + col;
                double distanceFromCenter = 2 * Math.Max(Math.Abs(row - ROWS / 2.0) / ROWS, Math.Abs(col - COLS / 2.0) / COLS);
                pixels[index] = (byte)(0x80 * (1 + distanceFromCenter * pixelLevel));
            }
        bitmap.WritePixels(new Int32Rect(0, 0, COLS, ROWS), pixels, COLS, 0);
        pixelLevel++;
    }
}

圖 3:AnimatedBitmapBrush 顯示效果

使用 PixelFormats.Gray8(8-bit 灰階,0x00 黑、0xFF 白)。像素陣列大小 = COLS × ROWS。WritePixels 第一參數 Int32Rect 指定更新矩形子集(左上角座標、寬高)。更新整個位圖時 X=Y=0,Width/Height 設為位圖 PixelWidth/PixelHeight。後兩參數為 stride(每列位元組數)與位移。

像素陣列深度解析:Stride 與多種格式

非 1 byte/像素格式、或僅更新矩形子集時,WritePixels 較複雜。此技術關鍵在於 BitmapSource.Create、CopyPixels 同樣使用此陣列格式。

位圖總像素數由 PixelWidth、PixelHeight 決定。PixelFormat.BitsPerPixel 範圍 1-128。極端情況:1 byte 儲存 8 像素;另一極端:每像素需 16 byte。可使用 byte、ushort、uint、float 陣列。

Int32Rect 定義位圖矩形子集。像素陣列位元組數必須足夠填充指定列數與行數。複雜處在多像素格式一 byte 儲存多像素,這些格式每列資料必須從 byte 邊界開始。

例:4-bit/像素格式,矩形寬 5 高 12。Int32Rect Width=5, Height=12。第一 byte 含前 2 像素,第二 byte 含次 2 像素,第三 byte 僅含第 5 像素,第四 byte 對應第 2 列前 2 像素。每列需 3 byte,整個矩形需 36 byte。

WritePixels 需 stride 參數(每列像素資料位元組數)。常規計算:

int stride = (width * bitsPerPixel + 7) / 8;

寬度為 Int32Rect.Width。即使用 ushort/uint/float 陣列,stride 單位仍為 byte。陣列總位元組數:height * stride。若用 ushort/uint/float 陣列,分別除以 2/4/8。

Windows API 要求每列位圖資料從 32-bit 記憶體邊界開始(stride 必為 4 倍數),WPF無此要求。但為效能可將 stride 設大於計算值(如用 uint 陣列儲存 1 byte/像素位圖,每元素存 4 像素,期望每列在單位邊界開始,因對齊複製在現代硬體上通常更快)。

位圖像素格式完整圖譜

每個像素由一或多個位元表示顏色。WPF 用 PixelFormat 結構表示具體格式。靜態 PixelFormats 類定義 26 個靜態屬性供建立位圖使用。分兩組:

可寫格式不可寫格式
Indexed1, Indexed2, Indexed4, Indexed8Default
BlackWhite, Gray2, Gray4, Gray8Bgr24, Rgb24
Bgr555, Bgr565Bgr101010
Bgr32, Bgra32, Pbgra32Cmyk32, Gray16, Rgb48, Rgba64, Prgba64
Gray32Float, Rgb128Float, Rgba128Float, Prgba128Float

BitmapSource.Create 可用除 PixelFormats.Default 外任意屬性;WriteableBitmap 僅限可寫格式。

索引格式與灰階格式

以 Indexed 開頭格式需 ColorPalette 物件。每像素為調色盤索引,四格式分別最多關聯 2、4、16、256 色。BlackWhite、Gray2/4/8 為 1/2/4/8-bit 灰階,全 0 為黑、全 1 為白。

16-bit 色彩格式:Bgr555 / Bgr565

均為 16-bit(2 byte)/像素。Bgr555:每原色 5 位(32 階),剩 1 位。兩連續 byte 儲存三原色,綠色位跨越兩 byte。視為 16-bit 無號整數(先存低位 byte)更合理。Bgr565:綠色用 6 位(人眼對綠最敏感)。

圖 6:短整數像素格式編碼關係

32-bit 色彩格式:Bgr32 / Bgra32 / Pbgra32

每像素 4 byte。Bgr32:最後 4 byte 為 0(不透明)。Bgra32:第 4 byte 為 Alpha(0x00 透明~0xFF 不透明)。Pbgra32:預乘 Alpha(見下文)。視為 32-bit 無號整數:低 8 位藍色,高 8 位為 0 或 Alpha。byte 順序對應屬性名 B、G、R、A 順序。

高精度與浮點格式

Gray16、Rgb48、Rgba64、Prgba64:16-bit 灰階與原色,適用醫療影像等高精度需求。8-bit 顯示器或檔案格式會忽略額外精度。

四種浮點格式:基於伽瑪值 1 的 scRGB 色彩空間(非慣用伽瑪 2.2 的 sRGB)。浮點 0.0 對應 0x00,1.0 對應 0xFF,但顯示裝置浮點值可能超過 1(色域寬於視訊顯示器)。

PixelFormats 文件勘誤

Gray16、Rgb48、Rgba64、Prgba64 文件記錄有衝突(標註為伽瑪 1 卻又以 sRGB 記錄)。實際上只有浮點格式使用 scRGB/伽瑪 1,不會造成混淆。

PixelFormat.Mask 屬性提供 PixelFormatChannelMask 集合,依藍、綠、紅、Alpha 順序各有一對應物件,含 byte 集合對應像素 byte 順序。可用於通用像素拆解/構建方法。

WriteableBitmap 備用建構函數可用任意 BitmapSource 建立。實際上可先用不可寫格式建立 BitmapSource,再建立 WriteableBitmap,但會轉換為 Bgr32 或 Pbgra32(視 Alpha 狀態)。

所有位圖可儲存為 BMP、GIF、PNG、JPEG、TIFF、Windows Media Photo。過程中可能轉換格式:存 GIF 轉 Indexed8、存 JPEG 轉 Gray8/Bgr32。目前無組合能產生超過 8-bit/原色的檔案。

預乘 Alpha:提升半透明混成效能

PixelFormats 三屬性以 P 開頭表預乘 Alpha,僅適用有 Alpha 通道位圖。改進部分透明像素混成效率。

例:Color.FromArgb(128, 0, 0, 255) 半透明藍筆刷。黑背景混成 RGB (0,0,128),白背景 (127,127,255)。標準公式:

Rresult = [(255 - Apixel) * Rsurface + Apixel * Rpixel] / 255;
Gresult = [(255 - Apixel) * Gsurface + Apixel * Gpixel] / 255;
Bresult = [(255 - Apixel) * Bsurface + Apixel * Bpixel] / 255;

若像素 R、G、B 已乘以 A 再除 255,可省去每個公式中的第二個乘法。例:Bgra32 像素 ARGB (192, 40, 60, 255) → Pbgra32 為 (192, 30, 45, 192)。RGB 已乘 Alpha 192 除 255。Pbgra32 中任像素 R/G/B 不應大於 A 值。

WriteableBitmap 應用實戰:即時影像處理與非線性轉換

應用中可利用 WriteableBitmap 顯示簡單動態圖形(如長條圖),更新速度快於 WPF 繪製向量圖形。最常見用途:即時影像處理與非線性轉換。

TwistedBitmap 專案允許載入 8/32-bit 位圖,用 Slider 控制影像圍繞中心扭曲。

圖 8:扭曲的位圖效果

使用 BitmapFrame.Create 載入檔案,CopyPixels 複製所有像素至 pixelsSrc。SliderOnValueChanged 將像素從 pixelsSrc 轉換至 pixelsNew 供 WritePixels 使用。

void SliderOnValueChanged(...) {
    // 計算目標像素對應的來源像素座標(反向轉換)
    int colSrc = (int)(xCenter + (col - xCenter) * Math.Cos(twist) - (row - yCenter) * Math.Sin(twist));
    int rowSrc = (int)(yCenter + (col - xCenter) * Math.Sin(twist) + (row - yCenter) * Math.Cos(twist));
    // 邊界限制
    colSrc = Math.Max(0, Math.Min(width - 1, colSrc));
    rowSrc = Math.Max(0, Math.Min(height - 1, rowSrc));
    // 計算索引並傳輸像素
    int index = stride * row + bytesPerPixel * col;
    int indexSrc = stride * rowSrc + bytesPerPixel * colSrc;
    for (int i = 0; i < bytesPerPixel; i++) pixelsNew[index + i] = pixelsSrc[indexSrc + i];
    bitmap.WritePixels(rect, pixelsNew, stride, 0);
}

關鍵原則:位圖轉換時必須執行反向轉換。若檢查原始位圖每像素決定其在新位圖位置,不同原始像素可能映射至同一新像素,導致新位圖某些像素無值(「孔」)。正確做法:對新位圖每個像素,計算原始位圖哪個像素映射至此,確保每個新像素都有計算值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved