C# はグレースケール PNG イメージを手動で解析してビットマップに変換します

質問:

ファイル パスを直接使用して 8 ビット グレースケール PNG 画像をビットマップとして読み込む場合、ビットマップの形式は Format8bppIndexed ではなく Format32bppArgb になります。これは一部の判定に影響するため、PNG データを手動で解析してビットマップを構築する必要があります

ステップ

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 バイトで構成されます。

ドメイン名 データバイト数 説明する
4バイト 画像の幅(ピクセル単位)
身長 4バイト 画像の高さ (ピクセル単位)
ビット深度 1バイト 画像深度: インデックス付きカラー イメージ: 1、2、4、または 8、グレースケール イメージ: 1、2、4、8 または 16、トゥルー カラー イメージ: 8 または 16
色の種類 1バイト カラー タイプ: 0: グレースケール イメージ、1、2、4、8 または 16; 2: トゥルー カラー イメージ、8 または 16; 3: インデックス付きカラー イメージ、1、2、4 または 84: アルファ チャネル データ イメージ付きのグレースケール、8または 16; 6: アルファ チャネル データを含むトゥルー カラー イメージ、8 または 16
圧縮方式 1バイト 圧縮方式(LZ77派生アルゴリズム)
フィルタ方式 1バイト フィルタ方式
インターレース方式 1バイト インターレース方式: 0: ノンインターレース; 1: Adam7 (アダム M. コステロによって開発された 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]);

ここでdataPNG ファイルを表すバイト配列を示します。+8 は、PNG ファイルの各データ ブロックが 4 バイトのデータ フィールド長と、データ フィールドの前に 4 バイトのデータ ブロック タイプ (名前) を持つためです。

3. すべての画像データ ブロックを取得する

PNG ファイルの画像データは 1 つ以上の画像データ ブロックで構成され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圧縮方法/フラグコード 1バイト
追加のフラグ/チェックビット 1バイト
圧縮されたデータブロック nバイト
チェック値 4バイト

解凍時に最初の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. 元のデータを再構築する

圧縮前に、 PNGIDATデータ ストリームは圧縮率を高めるためにフィルタリング アルゴリズムを通じて元のデータをフィルタリングしますが、ここでフィルタリングされたデータを再構築する必要があります。

フィルタリングと再構築の詳細については、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. 最後にグレースケール ビットマップを構築し、データを割り当てます。

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 の要点

おすすめ

転載: blog.csdn.net/lwf3115841/article/details/133548634