2017年9月4日月曜日

C#で64bitPNG

 C#で64bitPNGを出したかったので、やってみた。

using (Bitmap bmp = new Bitmap(256, 256, PixelFormat.Format64bppArgb))
{
    BitmapData bmd = bmp.LockBits(
        new Rectangle(Point.Empty, bmp.Size),
        ImageLockMode.WriteOnly,
        PixelFormat.Format64bppArgb);

    for (int y = 0; y < bmp.Height; y++)
    {
        for (int x = 0; x < bmp.Width; x++)
        {
            int l = y + x;
            byte[] data = BitConverter.GetBytes((UInt16)l);

            int pos = x * 8 + bmd.Stride * y;

            Marshal.WriteByte(bmd.Scan0, pos + 0, data[0]);
            Marshal.WriteByte(bmd.Scan0, pos + 1, data[1]);
            Marshal.WriteByte(bmd.Scan0, pos + 2, data[0]);
            Marshal.WriteByte(bmd.Scan0, pos + 3, data[1]);
            Marshal.WriteByte(bmd.Scan0, pos + 4, data[0]);
            Marshal.WriteByte(bmd.Scan0, pos + 5, data[1]);
            Marshal.WriteByte(bmd.Scan0, pos + 6, 255);
            Marshal.WriteByte(bmd.Scan0, pos + 7, 255);
        }
    }

    bmp.UnlockBits(bmd);

    bmp.Save("./log.png", ImageFormat.Png);
}

 まずBitmapのコンストラクタにPixelFormat.Format64bppArgbを指定する。これで64bitの画像が作れる。
 あとは色を載せるだけだが、今回は直接値を書き込んでいる。
 試してないが、たぶんSetPixelはダメだと思う。Color.FromArgbだと256以上は例外が投げられるはずなので、16bit画像は作れないはず(未確認)。
 画像を保存する際は、ImageFormat.Pngで保存すれば、16bitのPNG(16bit*RGBA =64bit)で保存される。

 ちなみに、このサンプルは下位9bitだけを使ってグレースケール画像を生成している。8bit換算で下位約2bitしか操作していないから、普通に表示しても何も見えない。

 これをpaint.netで輝度をいじると以下のような感じ。
下位8bitが切り捨てられて、上位8bitのみが使用されている。

 Lr5で輝度をいじると以下のような感じ。
てきとーに調整したのでガタガタだが、ちゃんとグレースケールで表示されてる。つまり、少なくとも下位8bitの内の数bitは使用されている。


 ということで、天体写真合成で16bitの出力ができる目処が立ったので、あとから輝度を調整しやすくなった。
 もともと自前で48bitPNGを出すライブラリを作ろうと思ってて、PNGって面倒くさいな~と思ってたところなので、.Net単体で簡単に64bitが出せたので楽になった。
 BitmapDataを使わなきゃいけないのは面倒だけど、そもそもBitmap.SetPixelはクッソ遅いので、どっちにしろ自前でBitmapDataの処理は作らにゃいかん。

 僕としては96bitPNGが欲しいところw。 16bitだと256枚加算まではロスレスだが、それ以上の枚数を重ねると情報が劣化してしまう。96bitだと8bit画像を16777216枚まで重ねられるので、当面は心配いらない。いちおうPNGのヘッダは1024bppまで対応しているっぽいけど、.Netは64bppが上限のはず。
 一般的なユーザーでは32bppPNGで充分だろうけども、データ処理の中間データとして使うならやはり128bppPNGくらいあると心強い。あとはIntだけじゃなくてFloatを持てたりとか、1/3/4ch以外にも、任意のch数を持てたりとかすると、学術データの標準フォーマットとして使えると思うんだけど。
 APNGのフォーマットは調べたことがないが、このコンテナに入れれば2次元画像をもう1次元増やせるので、3次元空間に4次元のベクトルをもたせることができる。現状だと、3次元空間に16bitの4次元ベクトルを入れるのが限界かな。それでも大したもんだが。16bit整数から浮動小数点に変換する処理とかを入れればさらに情報量は増やせる。
 APNGだとWebブラウザでも表示できるから、3次元空間の3次元ベクトルデータをざっくりと確認するのにWebブラウザで開けばいい、という利点もあるかも?
 PNG、クッソ古い規格なのに、拡張性やばい。

5 件のコメント:

  1. 本ブログで、PixelFormat64bppARGB 画像の作成は
    ネット上でも扱っているブログなどあまりなく、
    大変参考になりました。ありがとうございます。

    さて、本ブログをを参考に自分でも
    PixelFormat32bppARGB の画像を読み込み、そのビットマップデータをbmd1としました。

    別途 PixelFormat64bppARGB 空画像を作成して、ビットマップデータbmd2に対して、
    そこに、RGB各画素を6ビットシフトして与えました。

    6ビットシフトは、.netマニュアルに、
    「各16ビットカラーチャネルは、0 ~ 2 ^ 13 の範囲の値を保持できます。」と記載があり、
    0001-1111-1111-1111 13ビットまでしか画素の保持ができないようだからです。
    (それ以上の画素値を与えると実験的には画像がおかしくなる)
    また、α値は実験的には上限が0x7FFFのようです。

    short red = Marshal.ReadByte(bmd1.Scan0, pos1 + 0);
    short green = Marshal.ReadByte(bmd1.Scan0, pos1 + 1);
    short blue = Marshal.ReadByte(bmd1.Scan0, pos1 + 2);

    int shiftVal = 6; // 8bit ==> 14bit(使っているのは13bit)
    red = (short)(red << shiftVal);
    green = (short)(green << shiftVal);
    blue = (short)(blue << shiftVal);

    int pos2 = x * 8 + bmd2.Stride * y;

    Marshal.WriteInt16(bmd2.Scan0, pos2 + 0, red);
    Marshal.WriteInt16(bmd2.Scan0, pos2 + 2, green);
    Marshal.WriteInt16(bmd2.Scan0, pos2 + 4, blue);
    Marshal.WriteInt16(bmd2.Scan0, pos2 + 6, 0x7FFF);


    ここで、とある問題がおきました。
    下図のように、32bit 64bitのグレイスケールカーブを調べてみますと
    PixelFormat64bppARGB側にはトーンカーブがかかっているのです。

    http://issin.es.land.to/test/bit32_64.jpg

    この点、貴殿が何かのご知見があれば、共有いただけると
    大変幸いと思い、コメントいたしました。

    返信削除
  2. C#(.NET Core 3.1)で確認してみましたが、各チャンネル共に16bitフルスケールが使用できているはずです。トーンカーブに関しても特に違和感はありませんでした。

    参考までに、画像を読み込んで64bitでコピーして書き出すコードです(RGB下位8bitゼロ埋め、アルファは固定値)。
    位置の計算やエンディアンが面倒なので、一旦int配列に1行分を読み出して、uintにキャストしてからulongにパックし、long配列に入れてから1行分を書き込む、というような動作です(1色あたり16bitを端から端まで使うので、符号なしの型で処理したほうが安全です)。

    var bmp1 = LoadBitmap("src.jpg");
    var bmp2 = new Bitmap(bmp1.Width, bmp1.Height, PixelFormat.Format64bppArgb);

    var data1 = bmp1.LockBits(new Rectangle { Size = bmp1.Size }, ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb);
    var data2 = bmp2.LockBits(new Rectangle { Size = bmp2.Size }, ImageLockMode.WriteOnly, PixelFormat.Format64bppArgb);

    var line1 = new int[data1.Width];
    var line2 = new long[data2.Width];

    for (int y = 0; y < data2.Height; y++)
    {
    Marshal.Copy(data1.Scan0 + data1.Stride * y, line1, 0, line1.Length);

    for (int x = 0; x < data2.Width; x++)
    {
    uint a = (uint)line1[x];
    ulong b = 0;

    b |= 0xFFFFLU << 48; // アルファ
    b |= (ulong)(a >> 16 & 0xFF) << (32 + 8); // 赤
    b |= (ulong)(a >> 8 & 0xFF) << (16 + 8); // 緑
    b |= (ulong)(a >> 0 & 0xFF) << (0 + 8); // 青

    line2[x] = (long)b;
    }

    Marshal.Copy(line2, 0, data2.Scan0 + data2.Stride * y, line2.Length);
    }

    bmp1.UnlockBits(data1);
    bmp2.UnlockBits(data2);

    bmp2.Save("dst.png", ImageFormat.Png);

    /*
    static Bitmap LoadBitmap(string path)
    {
    using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
    using (var img = Image.FromStream(fs))
    {
    return new Bitmap(img);
    }
    }
    */

    返信削除
  3. 早速ご検討していただき、大変ありがとうございました。

    判明したことを報告します。

    私も、RGB各チャンネル 16bitフルα値0xFFFFにて
    Bitmapオブジェクトをを作りました。
    そして、png画像として保存して、
    Windows10(21H1)にて標準のフォトビューワやペイントアプリにて表示したところ、
    トーンカーブもかからず、正しく表示できました。

    一方、私が当初試した方法は、、.NET Framework4.6のWindowsForms
    にて、bmpオブジェクトを

    private void panelTest_Paint(object sender, PaintEventArgs e)
    {
    if (bmp != null) e.Graphics.DrawImage(bmp, 10, 10, bmp.Width, bmp.Height);
    }

    のように直接自分のテストアプリで描出する方法でした。

    これですと、実験的結果ではありますが、各チャンネル 13ビットまで
    α値は上限が0x7FFFまでしか正しく描写できず、正しくとはいってもトーンカーブがかかるようです。

    すなわち、.NET FrameworkのWindowsFormsのレンダリング側の問題のようでした。

    これをオフする方法をgoogleで漁ったのですが、
    ちょっと判できませんでした。

    ご報告まで、コメントさせていただきます。

    返信削除
  4. 基本的に32bppを超える画像が使われることはまれなので、サポートは環境を問わずあまり強くないようですね。
    大抵の画像ビューアやペイントソフトでも(正しく表示できるように見えても、)実際には下位8bitを切り捨てているだけの場合が多いです。例えば下位9bit分だけを使う非常に暗い画像を作って、ペイントソフトのカーブで明るくしてみると、1bit分しか情報が残っていない、ということがままあります。

    C#もBitmapで64bpp画像を作る(&PNGで保存する)ことは可能ですが、普通に読み込むと32bppに切り捨てられてしまうようですし(もしかしたら方法があるのかもしれませんが)。むしろWindows Formがなんで下位ビット切り捨てじゃないのかが不思議ですね。


    興味本位ですが、フォームの中で64bit画像を表示したい用途ってどういう使い方なんでしょうか?

    返信削除
  5. > 大抵の画像ビューアやペイントソフトでも(正しく表示できるように見えても、
    > )実際には下位8bitを切り捨てているだけの場合が多いです。

    最近のことは知りませんが、
    グラボが歴史的には32bitRGBとして発展している経緯があるんでしょうね。


    > フォームの中で64bit画像を表示したい用途

    医用画像の処理に活用できないかと考えていたところです。
    医用画像は12~16Bit画像となりますし、
    それをBitmapで直接表示したり、補間モード指定して簡単に拡縮できるかと思って検討した次第です。
    補間モード指定拡縮は、Graphics.DrawImageを経由するので無理っぽいところですね。


    また、表示時に最終グラボ上はRGB8bitでしょうから、
    下位8bit切り捨てはしょうがないかなという思いに至りました。

    つまるところ、結局は自分で必要なブラコン調整自分で行って8bitBitmap化することになりそうです。

    このブログで、天体写真合成というのもそういう用途があると気が付きました。
    デジカメRAW出力なんか、各色8bit超えとかありそうでしょうね。

    返信削除