none
Tiff画像ファイルのピクセル深度を複製したい RRS feed

  • 質問

  • いつも、お世話になっています。

    Visual C# 2010、Windows7でTiff画像ファイルに文字情報を追記して、ファイルに書き出すプログラムを

    作成し、ファイル出力は出来るのですが、入力ファイルのビットの深さが32ビットになってしまいます。

    (入力は二値画像です)入力ファイルのビットの深さを複製するには、どうすれば良いのか、お教えください。

    念のため、現時点のソースコードを添付します。

            private void button1_Click(object sender, EventArgs e)
            {
                //ファイルから画像をビットマップに展開
                Bitmap src = new Bitmap("C:\\test\\test.tif");
    
                // 画像解像度を取得する
                float hRes = src.HorizontalResolution;
                float vRes = src.VerticalResolution;
    
                //Rect/Depthを取得する。
                System.Drawing.Imaging.PixelFormat format = src.PixelFormat;
                Rectangle rect = new Rectangle(0, 0, src.Width, src.Height);
    
                //出力用ビットマップを作成する。
                //Bitmap dest = new Bitmap(src.Width, src.Height);
                Bitmap dest = new Bitmap(src);
                dest.SetResolution(hRes, vRes);
                Graphics g = Graphics.FromImage(dest);
    
                //画像を描画する
                g.DrawImage(src, 0, 0, src.Width, src.Height);
    
                //追記の文字情報を描画する
                Font font = new Font("HGP明朝E", 22,FontStyle.Bold);
                string ctlnum = "AAAAA  BBBBBB  CCCCCCCCCC";
                g.DrawString(ctlnum, font, Brushes.Black, 170, 0);
                
                //ファイルに書き出す。
                dest.Save("C:\\test\\out.tif");
                MessageBox.Show("Width=" + src.Width + ",Height=" + src.Height+",DPI="+hRes + "Depth=");
    
            }
    

    2013年1月12日 14:09

回答

  • そのメッセージの通りでしょう。
    Graphics クラスで扱うために一度、RGB24 などでビットマップを作り、その結果を読み込みながら、2 色のインデックス Bitmap に対して LockBits で書き込んでいくことになるかと思います。
    (Graphics クラスのために増色し、文字列を描き込み、2 色に減色する)

    • 回答としてマーク MitsuoTAKEI 2013年1月13日 15:21
    2013年1月13日 2:14
    モデレータ

すべての返信

  • Bitmap dest = new Bitmap(src); 実はこの部分で32bitに変換されています。この処理は必要なのでしょうか? 不要なら削除しsrcをそのまま使用してください。メモリに読み込んでいるだけなので編集しても元ファイルが変更されることはありません。必要なら同等な

    var dest = (Bitmap)src.Clone();

    にしてみてください。こちらは元の形式を維持したまま複製します。

    2013年1月12日 21:29
  • 佐祐理さま

    いつも、お世話になります。ご回答ありがとうございました。

    dest = (Bitmap)src.Clone();

    でやってみましたが、以下の行で、「インデックス付きのピクセル形式をもつイメージからグラフィックス オブジェクトを作成することはできません。」の例外となってしまいます。

    Graphicは画像と追記文字の描画に必要なので、削除はできません。

    何か、いい方法はないでしょうか?

    Graphics g = Graphics.FromImage(dest);

    2013年1月13日 1:12
  • そのメッセージの通りでしょう。
    Graphics クラスで扱うために一度、RGB24 などでビットマップを作り、その結果を読み込みながら、2 色のインデックス Bitmap に対して LockBits で書き込んでいくことになるかと思います。
    (Graphics クラスのために増色し、文字列を描き込み、2 色に減色する)

    • 回答としてマーク MitsuoTAKEI 2013年1月13日 15:21
    2013年1月13日 2:14
    モデレータ
  • Azuleanさま

    ご回答ありがとうございました。

    二値化したBitmapを作成し、

    g.DrawImage,g.DrawStringしてsave

    するようなことは、できないのでしょうか?

    2013年1月13日 4:19
  • Azuleanさま

    ご回答ありがとうございました。

    二値化したBitMapでは、描画出来ないことが、わかりました。

    (Graphicsが作成できない)

    32ビットを二値化する処理を組み込むことにします。

    2013年1月13日 6:00
  • Azuleanさま

    ご回答ありがとうございました。

    ご指示どおり、実現できました。

    深さ32bitから1bitへの変換が

    非常に時間が掛かり、Cloneして

    DrawStringの実現をしたほうが

    速いかも知れません。

    悩んでいます。

    2013年1月13日 15:31
  • その減色処理はどんなコードでしょうか。
    もし、GetPixel を使っているのなら LockBits にした方が速いですよ。

    あと考えられるとしたら、空の Bitmap に DrawString して、DrawString で必要だった矩形の部分で色が変わったところだけ上書きするという形にするとか。

    // Clone して DrawString はできないと言ったことは先に知っているはずですよね?
    // Graphics がサポートしないのですから、普通にはできません。

    2013年1月14日 0:35
    モデレータ
  • Azuleanさま

    ご回答ありがとうございます。

    減色のコードは、以下のとおりです。

    //CloneしてDrawStringというのは、

    二値化した画像上で、DrawStringに

    相当する機能を自前で実現したら

    どうかなと、考えた次第です。

                //新しい画像のピクセルデータを作成する
                byte[] pixels = new byte[bmpData.Stride * bmpData.Height];
                for (int y = 0; y < dest.Height; y++)
                {
                    for (int x = 0; x < dest.Width; x++)
                    {
                        //明るさが0.5以上の時は白くする
                        if (0.5f <= dest.GetPixel(x, y).GetBrightness())
                        {
                            //ピクセルデータの位置
                            int pos = (x >> 3) + bmpData.Stride * y;
                            //白くする
                            pixels[pos] |= (byte)(0x80 >> (x & 0x7));
                        }
                    }
                }
                //作成したピクセルデータをコピーする
                IntPtr ptr = bmpData.Scan0;
                System.Runtime.InteropServices.Marshal.Copy(pixels, 0, ptr, pixels.Length);
    
                //ファイルに書き出す。
                newImg.Save("C:\\test\\out.tif");
                MessageBox.Show("Width=" + bmpData.Width + ",Height=" + bmpData.Height + ",DPI=" + hRes + ",Stride=" + bmpData.Stride);
    

    2013年1月14日 1:54
  • 二値化した画像上で、DrawStringに相当する機能を自前で実現したらどうかなと、考えた次第です。

    難しそうな印象です。
    何らかの方法で文字を描いた結果のピクセルごとの 0 or 1 を得ないといけませんが、自前で全部書くのは必要となる知識が多くなりそうなだと思っています。
    近いことをやるのであれば、私が先に書いたように DrawString だけ別に実行してマージすることでしょうね。

    減色のコードは、以下のとおりです。

    GetPixel は基本的に遅いので、LockBits で Scan0 から読み込むことを考えてください。
    BGR24 なのか、RGB24 なのか、ARGB32 なのか、わかりませんが、そのピクセルフォーマットによる並び順で読み込むとマシになる…はず。
    GetBrightness の実行効率は調べていませんが、まずは GetPixel を廃してさらに手を打たないといけないかを見るところからでしょうか。

    方法がわからない場合は「GetPixel LockBits」で調べるとそういったページが見つかるかと思います。

    2013年1月14日 2:01
    モデレータ
  • Azuleanさま

    ご回答ありがとうございました。

    遅くなりましてすみませんでした。

    あれから、悪戦苦闘しまして、

    なんとかGetPixel()なしで、

    実現しましたが、まだ20秒近く

    かかります。

    やはり、1ピクセルずつの処理を

    いかに効率よくするかだとわかり

    ました。

    良いアイデアがございましたら、

    お教えください。

    念のため、ソースコードを提示

    させていただきます。

    アドバイスありがとうございました。

                //処理時間計測開始
                Stopwatch sw = new Stopwatch();
                sw.Start();
    
                //画像をアンロック
                dest.UnlockBits(bmpData);
                byte[] pixels = new byte[(bmpData.Stride * bmpData.Height)/8];
                int pv, dv;
                int SIx = 0;
                int DIx = 0;
                int RGB = 0;
                int threshold = 500;
    
                //32bpp画像の二値化
                for (int y = 0; y < dest.Height; y++)
                {
                    dv = 0;
                    pv = 0x80;
                    //全ピクセル繰り返し
                    for (int x = 0; x < dest.Width; x++)
                    {
                        //32bppのRGB値を取得
                        RGB = srcBuf[SIx + 1] + srcBuf[SIx + 2] + srcBuf[SIx + 3];
                        if (RGB > threshold)    dv += (byte)pv;
                        if (pv == 1)
                        {
                            pixels[DIx++] = (byte)dv;
                            dv = 0;
                            pv = 0x80;
                        }
                        else  pv >>= 1;
                        
                        //次のピクセルへ
                        SIx += 4;
                        if (pv != 0x80)  pixels[DIx] = (byte)dv;
                    }
                }
    
                //作成したピクセルデータをコピーする
                int numByte = (newImg.Width * newImg.Height) / 8; 
                Marshal.Copy(pixels,0,bmpOut.Scan0,numByte);
                newImg.UnlockBits(bmpOut);
    
                //処理時間計測終了
                sw.Stop();
                long milsec = sw.ElapsedMilliseconds;
    

    2013年1月16日 5:54
  • どれが遅いかは、実験してみないとわからないのですが、考えるべきポイントとしてはループ内で何度も参照・書き換えをするような処理を軽量化することです。
    たとえば、for ループで Height/Width プロパティと比較する終了条件になっていますが一時変数に入れておくとか、配列ベースのアクセスは遅いので unsafe コードを用いたポインタベースのアクセスにするとかはよく知られているところでしょうか。
    2013年1月16日 13:43
    モデレータ
  • 変数名が汚くて読みづらいです。
    Bitmap: dest → newImg
    BitmapData: bmpData → bmpOut
    byte[]: srcBuf → pixels

    BitmapData.Strideがほとんど考慮されていません。
    pixelsのサイズ計算では使われていますが間違っています。bmpDataではなくbmpOutを参照する必要がありますし、1bppではストライドは入らないでしょう。逆にSIxはストライドを補正していません。(無駄にもpixelsはストライドを考慮しましたが)Marshal.Copy()で考慮されていないので、もしストライドがあればずれます。

    本題。見た感じ20秒もかからないと思うのですが。

    私もunsafeを使いますが、使わない場合の別の方法として、srcBufをbyte[]ではなくint[]にします。そうすると配列アクセス・メモリアクセスが1/3になります。

    その他、今回のパターンならloop unrollingでしょうか。

    2013年1月16日 15:07
  • Azuleanさま

    ご回答ありがとうございました。

    Height/Widthをローカル変数化

    したところ、0.061秒、約328倍

    高速化しました。

    これまで、安易にプロパティを

    演算に使っていましたが、

    オーバーヘッドが巨大な事が

    身に沁みました。

    ありがとうございました。

    2013年1月17日 2:09
  • 佐祐理さま

    ご回答、ご指摘ありがとうございました。

    pixelsのサイズ計算間違っていました。

    ストライド、ポインターを使用しなくても、十分なパフォーマンスが得られましたので、

    (0.061秒)これで、良しとします。

    ありがとうございました。

    最終のコードを添付します。

                int width = dest.Width;
                int height = dest.Height;
    
                //32bpp画像の二値化
                for (int y = 0; y < height; y++)
                {
                    dv = 0;
                    pv = 0x80;
                    //全ピクセル繰り返し
                    for (int x = 0; x < width; x++)
                    {
                        //32bppのRGB値を取得
                        RGB = srcBuf[SIx + 1] + srcBuf[SIx + 2] + srcBuf[SIx + 3];
                        if (RGB > threshold)    dv += (byte)pv;
                        if (pv == 1)
                        {
                            pixels[DIx++] = (byte)dv;
                            dv = 0;
                            pv = 0x80;
                        }
                        else  pv >>= 1;
                        
                        //次のピクセルへ
                        SIx += 4;
                        if (pv != 0x80)  pixels[DIx] = (byte)dv;
                    }
                }
    

    2013年1月17日 2:43