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
  • 通過WPF的按鈕、文本輸入框實現了一個簡單的SpinBox數字輸入用戶組件並可以通過數據綁定數值和步長。本文中介紹了通過Xaml代碼實現自定義組件的佈局,依賴屬性的定義和使用等知識點。 ...
  • 以前,我看到一個朋友在對一個系統做初始化的時候,通過一組魔幻般的按鍵,調出來一個隱藏的系統設置界面,這個界面在常規的菜單或者工具欄是看不到的,因為它是一個後臺設置的關鍵界面,不公開,同時避免常規用戶的誤操作,它是作為一個超級管理員的入口功能,這個是很不錯的思路。其實Winform做這樣的處理也是很容... ...
  • 一:背景 1. 講故事 前些天有位朋友找到我,說他的程式每次關閉時就會自動崩潰,一直找不到原因讓我幫忙看一下怎麼回事,這位朋友應該是第二次找我了,分析了下 dump 還是挺經典的,拿出來給大家分享一下吧。 二:WinDbg 分析 1. 為什麼會崩潰 找崩潰原因比較簡單,用 !analyze -v 命 ...
  • 在一些報表模塊中,需要我們根據用戶操作的名稱,來動態根據人員姓名,更新報表的簽名圖片,也就是電子手寫簽名效果,本篇隨筆介紹一下使用FastReport報表動態更新人員簽名圖片。 ...
  • 最新內容優先發佈於個人博客:小虎技術分享站,隨後逐步搬運到博客園。 創作不易,如果覺得有用請在Github上為博主點亮一顆小星星吧! 博主開始學習編程於11年前,年少時還只會使用cin 和cout ,給單片機點點燈。那時候,類似async/await 和future/promise 模型的認知還不是 ...
  • 之前在阿裡雲ECS 99元/年的活動實例上搭建了一個測試用的MINIO服務,以前都是直接當基礎設施來使用的,這次準備自己學一下S3相容API相關的對象存儲開發,因此有了這個小工具。目前僅包含上傳功能,後續計劃開發一個類似圖床的對象存儲應用。 ...
  • 目錄簡介快速入門安裝 NuGet 包實體類User資料庫類DbFactory增刪改查InsertSelectUpdateDelete總結 簡介 NPoco 是 PetaPoco 的一個分支,具有一些額外的功能,截至現在 github 星數 839。NPoco 中文資料沒多少,我是被博客園群友推薦的, ...
  • 前言 前面使用 Admin.Core 的代碼生成器生成了通用代碼生成器的基礎模塊 分組,模板,項目,項目模型,項目欄位的基礎功能,本篇繼續完善,實現最核心的模板生成功能,並提供生成預覽及代碼文件壓縮下載 準備 首先清楚幾個模塊的關係,如何使用,簡單畫一個流程圖 前面完成了基礎的模板組,模板管理,項目 ...
  • 假設需要實現一個圖標和文本結合的按鈕 ,普通做法是 直接重寫該按鈕的模板; 如果想作為通用的呢? 兩種做法: 附加屬性 自定義控制項 推薦使用附加屬性的形式 第一種:附加屬性 創建Button的附加屬性 ButtonExtensions 1 public static class ButtonExte ...
  • 在C#中,委托是一種引用類型的數據類型,允許我們封裝方法的引用。通過使用委托,我們可以將方法作為參數傳遞給其他方法,或者將多個方法組合在一起,從而實現更靈活的編程模式。委托類似於函數指針,但提供了類型安全和垃圾回收等現代語言特性。 基本概念 定義委托 定義委托需要指定它所代表的方法的原型,包括返回類 ...