C# 手動解析灰度PNG圖片為Bitmap

来源:https://www.cnblogs.com/qyqj/archive/2023/09/28/17735737.html
-Advertisement-
Play Games

問題: 當直接使用文件路徑載入8位灰度PNG圖片為Bitmap時,Bitmap的格式將會是Format32bppArgb,而不是Format8bppIndexed,這對一些判斷會有影響,所以需要手動解析PNG的數據來構造Bitmap 步驟 1. 判斷文件格式 若對PNG文件格式不是很瞭解,閱讀本文前 ...


問題:

當直接使用文件路徑載入8位灰度PNG圖片為Bitmap時,Bitmap的格式將會是Format32bppArgb,而不是Format8bppIndexed,這對一些判斷會有影響,所以需要手動解析PNG的數據來構造Bitmap

步驟

1. 判斷文件格式

若對PNG文件格式不是很瞭解,閱讀本文前可以參考PNG的文件格式 PNG文件格式詳解

簡而言之,PNG文件頭有8個固定位元組來標識它,他們是

private static byte[] PNG_IDENTIFIER = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };

2. 判斷是否為8位灰度圖

識別為PNG文件後,需要判斷該PNG文件是否為8位的灰度圖

在PNG的文件頭標識後是PNG文件的第一個數據塊IHDR,它的數據域由13個位元組組成

域的名稱 數據位元組數 說明
Width 4 bytes 圖像寬度,以像素為單位
Height 4 bytes 圖像高度,以像素為單位
Bit depth 1 byte 圖像深度:索引彩色圖像:1,2,4或8 ;灰度圖像:1,2,4,8或16 ;真彩色圖像:8或16
ColorType 1 byte 顏色類型:0:灰度圖像, 1,2,4,8或16;2:真彩色圖像,8或16;3:索引彩色圖像,1,2,4或84:帶α通道數據的灰度圖像,8或16;6:帶α通道數據的真彩色圖像,8或16
Compression method 1 byte 壓縮方法(LZ77派生演算法)
Filter method 1 byte 濾波器方法
Interlace method 1 byte 隔行掃描方法:0:非隔行掃描;1: Adam7(由Adam M. Costello開發的7遍隔行掃描方法)

這裡我們看顏色深度以及顏色類型就行

 var ihdrData = data[(PNG_IDENTIFIER.Length + 8)..(PNG_IDENTIFIER.Length + 8 + 13)];
 var bitDepth = Convert.ToInt32(ihdrData[8]);
 var colorType = Convert.ToInt32(ihdrData[9]);

這裡的data是表示PNG文件的byte數組,+8是因為PNG文件的每個數據塊的數據域前都有4個位元組的數據域長度和4個位元組的數據塊類型(名稱)


3. 獲取全部圖像數據塊

PNG文件的圖像數據由一個或多個圖像數據塊IDAT構成,並且他們是順序排列的

這裡通過while迴圈找到所有的IDAT

var compressedSubDats = new List<byte[]>();
var firstDatOffset = FindChunk(data, "IDAT");
var firstDatLength = GetChunkDataLength(data, firstDatOffset);
var firstDat = new byte[firstDatLength];

Array.Copy(data, firstDatOffset + 8, firstDat, 0, firstDatLength);
compressedSubDats.Add(firstDat);

var dataSpan = data.AsSpan().Slice(firstDatOffset + 12 + firstDatLength);
while (Encoding.ASCII.GetString(dataSpan[4..8]) == "IDAT")
{
    var datLength = dataSpan.ReadBinaryInt(0, 4);
    var dat = new byte[datLength];
    dataSpan.Slice(8, datLength).CopyTo(dat);
    compressedSubDats.Add(dat);
    dataSpan = dataSpan.Slice(12 + datLength);
}

var compressedDatLength = compressedSubDats.Sum(a => a.Length);
var compressedDat = new byte[compressedDatLength].AsSpan();
var index = 0;
for (int i = 0; i < compressedSubDats.Count; i++)
{
    var subDat = compressedSubDats[i];
    subDat.CopyTo(compressedDat.Slice(index, subDat.Length));
    index += subDat.Length;
}

4. 解壓DAT數據

上一步獲得的DAT數據是由Deflate演算法壓縮後的,我們需要將它解壓縮,這裡使用.NET自帶的DeflateStream進行解壓縮

IDAT的數據流以zlib格式存儲,結構為

名稱 長度
zlib compression method/flags code 1 byte
Additional flags/check bits 1 byte
Compressed data blocks n bytes
Check value 4 bytes

解壓縮時去掉前2個位元組

var deCompressedDat = MicrosoftDecompress(compressedDat.ToArray()[2..]).AsSpan();
public static byte[] MicrosoftDecompress(byte[] data)
{
    MemoryStream compressed = new MemoryStream(data);
    MemoryStream decompressed = new MemoryStream();
    DeflateStream deflateStream = new DeflateStream(compressed, CompressionMode.Decompress);
    deflateStream.CopyTo(decompressed);
    byte[] result = decompressed.ToArray();
    return result;
}

5. 重建原始數據

PNG的IDAT數據流在壓縮前會通過過濾演算法將原始數據進行過濾來提高壓縮率,這裡需要將過濾後的數據進行重建

有關過濾和重建可以參考W3組織的文檔

這裡定義了一個類來輔助重建

    public class PngFilterByte
    {
        public PngFilterByte(int filterType, int row, int col)
        {
            FilterType = filterType;
            Row = row;
            Column = col;
        }

        public int Row { get; set; }

        public int Column { get; set; }

        public int FilterType { get; set; }

        public PngFilterByte C { get; set; }

        public PngFilterByte B { get; set; }

        public PngFilterByte A { get; set; }

        public int X { get; set; }

        private bool _isTop;

        public bool IsTop
        {
            get => _isTop;
            init
            {
                _isTop = value;
                if (!_isTop) return;
                B = Zero;
            }
        }

        private bool _isLeft;

        public bool IsLeft
        {
            get => _isLeft;
            init
            {
                _isLeft = value;
                if (!_isLeft) return;
                A = Zero;
            }
        }

        public int _filt;

        public int Filt
        {
            get => IsFiltered ? _filt : DoFilter();
            init
            {
                _filt = value;
            }
        }

        public bool IsFiltered { get; set; } = false;

        public int DoFilter()
        {
            _filt = FilterType switch
            {
                0 => X,
                1 => X - A.X,
                2 => X - B.X,
                3 => X - (int)Math.Floor((A.X + B.X) / 2.0M),
                4 => X - Paeth(A.X, B.X, C.X),
                _ => X
            };
            if (_filt > 255) _filt %= 256;
            IsFiltered = true;
            return _filt;
        }

        private int _recon;

        public int Recon
        {
            get => IsReconstructed ? _recon : DoReconstruction();
            init
            {
                _filt = value;
            }
        }

        public bool IsReconstructed { get; set; } = false;

        public int DoReconstruction()
        {
            _recon = FilterType switch
            {
                0 => Filt,
                1 => Filt + A.Recon,
                2 => Filt + B.Recon,
                3 => Filt + (int)Math.Floor((A.Recon + B.Recon) / 2.0M),
                4 => Filt + Paeth(A.Recon, B.Recon, C.Recon),
                _ => Filt
            };
            if (_recon > 255) _recon %= 256;
            X = _recon;
            IsReconstructed = true;
            return _recon;
        }

        private int Paeth(int a, int b, int c)
        {
            var p = a + b - c;
            var pa = Math.Abs(p - a);
            var pb = Math.Abs(p - b);
            var pc = Math.Abs(p - c);
            if (pa <= pb && pa <= pc)
            {
                return a;
            }
            else if (pb <= pc)
            {
                return b;
            }
            else
            {
                return c;
            }
        }

        public static PngFilterByte Zero = new PngFilterByte(0, -1, -1)
        {
            IsFiltered = true,
            IsReconstructed = true,
            X = 0,
            Filt = 0,
            Recon = 0
        };
    }

下麵獲取重建的數據

首先從IHDR獲取寬高

var width = ihdrData.ReadBinaryInt(0, 4);
var height = ihdrData.ReadBinaryInt(4, 4);

按行處理

var filtRowDic = new Dictionary<int, byte[]>();
for (int i = 0; i < height; i++)
{
    var rowData = deCompressedDat.Slice(i * (width + 1), (width + 1));
    filtRowDic.Add(i, rowData.ToArray());
}

var rowColDic = new Dictionary<(int, int), PngFilterByte>();

for (int i = 0; i < height; i++)
{
    var row = filtRowDic[i];
    var filterType = row[0];
    for (int j = 1; j <= width; j++)
    {
        var bt = new PngFilterByte(filterType, i, j - 1)
        {
            Filt = Convert.ToInt32(row[j]),
            IsFiltered = true,
            IsTop = i == 0,
            IsLeft = j == 1
        };
        if (bt.IsTop && bt.IsLeft)
        {
            bt.C=PngFilterByte.Zero;
        }
        if (!bt.IsTop)
        {
            bt.B = rowColDic[(bt.Row - 1, bt.Column)];
        }

        if (!bt.IsLeft)
        {
            bt.A = rowColDic[(bt.Row, bt.Column - 1)];
        }
        rowColDic.Add((bt.Row, bt.Column), bt);
    }
}

var realImageData = new byte[rowColDic.Count];
foreach (var bt in rowColDic.Values)
{
    realImageData[bt.Row * width + bt.Column] = Convert.ToByte(bt.Recon);
}

6. 最後構建灰度Bitmap並賦予數據

using var bitmap = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
ColorPalette cp = bitmap.Palette;
for (int i = 0; i < 256; i++)
{
    cp.Entries[i] = Color.FromArgb(i, i, i);
}
bitmap.Palette = cp;
var bmpData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed);
Marshal.Copy(realImageData, 0, bmpData.Scan0, realImageData.Length);
bitmap.UnlockBits(bmpData);

return bitmap;

完整代碼

Github Gist


參考:

1. PNG文件格式詳解
2. Png的數據解析
3. How to read 8-bit PNG image as 8-bit PNG image only?
4. Portable Network Graphics (PNG) Specification (Second Edition)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 第一題 下列程式輸出啥? public class StringDemo{ private static final String MESSAGE="taobao"; public static void main(String [] args) { String a ="tao"+"bao"; S ...
  • 基本介紹 MyBatis-Plus (opens new window)(簡稱 MP)是一個 MyBatis (opens new window)的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。 MyBatis-Plus特性 無侵入:只做增強不做改變,引入它不會對 ...
  • 在Java中,Serializable是一個標記介面(marker interface),用於指示一個類的對象可以被序列化。序列化是將對象轉換為位元組流的過程,可以將對象保存到文件、在網路上傳輸或在記憶體中傳遞。 當一個類實現了Serializable介面時,它表示該類的對象可以被序列化和反序列化。 序 ...
  • 在Java 21中,引入了虛擬線程(Virtual Threads)來簡化和增強併發性,這使得在Java中編程併發程式更容易、更高效。 虛擬線程,也稱為“用戶模式線程(user-mode threads)”或“纖程(fibers)”。該功能旨在簡化併發編程並提供更好的可擴展性。虛擬線程是輕量級的,這 ...
  • 折線圖是一種用於可視化數據變化趨勢的圖表,它可以用於表示任何數值隨著時間或類別的變化。 折線圖由折線段和折線交點組成,折線段表示數值隨時間或類別的變化趨勢,折線交點表示數據的轉折點。 折線圖的方向表示數據的變化方向,即正變化還是負變化,折線的斜率表示數據的變化程度。 1. 主要元素 折線圖主要由以下 ...
  • 1、概述 GEBCO(General Bathymetric Chart of the Oceans)全球 DEM數據集(Geo-Engineering Digital Savage)是基於“全球地球系統計劃”(Global Earth System Project)的最新數據集。 GEBCO 數據 ...
  • Question Description 使用JAVA語言的若依框架的時候,發現只要使用了startPage()函數, 並不需要前端傳遞分頁的數據,也不需要註解,就能完成分頁功能。預判他應該是使用類似攔截器的機制,但還是感覺很神奇,感覺知道個大概不過癮,還是要更細緻的瞭解才能滿足,就想研究一下並記錄 ...
  • 歡迎訪問我的GitHub 這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos 本篇概覽 本文是《Strimzi Kafka Bridge(橋接)實戰之》系列的第二篇,咱們直奔bridge的重點:常用介面,用實際操作體驗如何用brid ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...