Bitmap Conversion

「ビットマップ変換」

TImageに画像を読み込んで、変更を加え、その画像をGDI+で高速に保存しようとした。
しかし、VCLのTBitmapをGDI+のBitmapへ変換する方法がわからず、半日以上試行錯誤。これはその時の記録。

0.Bitmapを変換したい理由
1.わかったこと
2.まとめ
3.お願いとお断り

0.Bitmapを変換したい理由

GDI+を使うようになる前は、次のようなコードでJpegファイルを保存していた。

var
  Jpeg: TJPEGImage;
  S:string;
begin
  S := ChangeFileExt(画像ファイルへのPath, '.jpg');
  Jpeg := TJPEGImage.Create;
  try
    Jpeg.Assign(Image1.Picture.Bitmap);
    Jpeg.Compress;
    Jpeg.SaveToFile(S);
  finally
    Screen.Cursor := crDefault;
    Jpeg.Free;
  end;
end;

1枚とか、ごくわずかな枚数の画像を保存するだけなら、この方法でも特に問題はないんだが、何十枚もの画像を頻繁に処理することを繰り返すような場合には、やはりGDI+を使って高速に処理したくなる。例えば、次のような場合だ。

マークシートと手書きの解答がセットになった解答用紙をスキャナーで読み取り、その手書きの解答欄を矩形選択して切り取り、(1)なら(1)だけ!以下の画像のように人数分集めて表示 & 採点するプログラムを作りたいとき(実際に今、作っている)。

矩形で切り抜いて表示、採点までは無事にプログラミングできた☆

手書き答案をPCと協働で採点するプロジェクト

ハマったのは、ココからだ。採点したら、各々の解答欄を、解答用紙の元の場所に戻したい。矩形で切り取って集めて1枚の画像に仕立てたアルゴリズムを再利用し、読み込み元Rectと書き込み先Rect、及び読み取り元Imageと書き込み先Imageをそれぞれ逆にすれば、各々の解答欄は「ばっちり元の場所に戻る」ハズだ。・・・と考え、実際にその処理を書いてみた。

ところが赤い丸印(採点済みのマーク)を付加して変更を加えたTImage(VCL)のTBitmapをGDI+のBitmapへ変換する方法がどぉーしてもわからない。走召!困った。。。

        //GDI+で元の答案画像を読み込んでおく
        bmp:=TGPBitmap.Create(答案の画像ファイル);
        Image1.Picture.Bitmap.Width:=bmp.GetWidth;
        Image1.Picture.Bitmap.Height:=bmp.GetHeight;
        Image1.Picture.Bitmap.PixelFormat:=pf24bit;
        graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);

        try  
          //イメージを表示
          graphics.DrawImage(bmp, 0, 0, bmp.GetWidth, bmp.GetHeight);

            ・・・ 採点結果の書き戻し準備処理 ・・・

          //採点した解答欄の矩形を元の答案画像へ書き戻し(これは成功する)
          Image1.Canvas.CopyRect(DestRect, Image2.canvas, SrcRect);
        finally
          bmp.Free;
          graphics.Free;
        end;

        //書き戻した答案画像はできてるけど、まだファイルになってない!
        //・・・ってか、それをファイルにする処理をここに書きたい・・・んだけど
        bmp:=TGPBitmap.Create(ココの書き方がわからない);
        //Graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);
        Graphics:=TGPGraphics.Create(Image1.Canvas.Handle);
        try
          Graphics.DrawImage(bmp,0,0);
          ・・・

上のコードで、TGPBitmap.Create( )の( )の中にナニを入れたらいいのか、この部分がどうしてもわからず、かなり苦しんだ。仕方がないからGDI+での保存はいったんあきらめて、Jpeg.Assign(Image1.Picture.Bitmap)の方法を試してみることにする。すると、思っていた通り、たった10枚の答案でも とぉーっても 遅いのだ。

数十枚(最大100枚程度を想定している)の答案の解答欄一つひとつについて、採点後に元の答案画像への書き戻しを行うことを考えると、Jpeg.SaveToFileメソッドはどうしても使う気になれない。保存方法は何がナンでも高速なGDI+を使いたい。

長くなったけど、これがビットマップを変換したい理由なのです。

1.わかったこと

いろいろ調べたり、思い出したりして、試行錯誤。

Image1.Picture.SaveToFile(画像の保存先Path);

GDI+とは何の関係もないけれど、上のコードも試したが、これはエラーに。

bmp:=TGPBitmap.Create(ココの書き方がわからない);
                  ↓
bmp:=TGPBitmap.Create(画像の保存先Pathを入れてみた);

エラーにはならないけど、変更したImageは当然保存されない。
(あたりまえです。元画像を読み込んで、そのまま上書きコピーしてるだけだもん。)

時間だけがゆっくりと過ぎて行く。
このプログラムの完成を待ってる人もいないけど、教えてくれる人もいない。

Google先生も調子悪そうだ。
何を尋ねても、わかってることしか表示してくれない。

僕は、DelphiのIDEではなく、NanaTreeの画面を「ぼー」っと見つめていた。

NanaTreeは、オープンソースのフリーソフト(階層化テキストエディター)で、僕はこのNanaTreeに、これまでに調べたことや、学んだことを備忘録として記録、資料化している。10年以上前から書き溜めてきた、お金では買えない、僕のたからものだ。

僕とDelphi(Object Pascal)の、これまでのすべてが、ここに詰まっている・・・。

ふと、下から3番目のノードが目に留まった。

VCL TBitmap から

なんだって、その先は? あわててノードをクリックする。
すると、目に飛び込んできたのは・・・

VCL TBitmap から GDI+ Bitmap へ

そのものズバリがあったー!!

「灯台元暗し」とはまさにこのこと。

オレ、調べてたんだ・・・。これまで使ったことがなかっただけで・・・。
少し前の記事になりますが、NanaTreeに記録した資料の元の記事がこちらです。

めもニャンだむ

junkiの日常雑感・日記・プログラミングのランダムメモ

VCL TBitmap から GDI+ Bitmap への転送を試した。

TGPBitmap クラスのコンストラクタのうち
constructor Create(stream:IStream;..);reintroduce;overload;
を使う。

まず、VCL TBitmap オブジェクトの内容を SaveToStream() によって、TMemoryStream にセーブする。次に、TStreamAdapter クラスのインスタンスを作成して、TMemoryStream オブジェクトを IStream に変換し、上記のコンストラクタで作成する。

URL:http://blog.livedoor.jp/junki560/archives/21910595.htmlより引用

junkiさん、ほんとうにありがとうございます。
TMemoryStreamを介してVCLビットマップのデータをGDI+に渡せるのですね!

さっそく書いたのが次のコード。

implementation

uses
  System.IOUtils,
  Winapi.GDIPAPI, Winapi.GDIPOBJ, Winapi.GDIPUTIL;

  //System.IOUtilsはPathから拡張子を取得するTPath.GetExtensionを使うために追加
  //GDIPAPI, GDIPOBJ はGDI+を利用した描画に資料するために必要
  //GDIPUTILを宣言すればGetEncoderClsid関数を利用してGUIDを取得できる

{$R *.dfm}

var
  ・・・
  //VCL TBitmapからGDI+ Bitmapへ変換
  Graphics:TGPGraphics;
  srcBMP:TBitmap;
  dstBMP:TGPBitmap;
  stream:TMemoryStream;
  //拡張子を取得する
  dotExt, strExt:string;
  //GetEncoderClsid関数の利用とTGUIDを使用するには、usesにWinapi.GDIPUTILが必要
  ImgGUID:TGUID;
begin

  //解答欄の矩形を元の答案画像へ書き戻し(内容が変更されたImageができる)
  Image1.Canvas.CopyRect(DestRect, Image2.canvas, SrcRect);

  //保存(VCL TBitmap -> GDI+ Bitmap)
  srcBMP:=TBitmap.Create;
  srcBMP.Width:=Image1.Width;
  srcBMP.Height:=Image1.Height;
  srcBMP.Assign(Image1.Picture.graphic);
  //データ受け渡し用のストリームを生成して保存
  stream:=TMemoryStream.Create;
  srcbmp.SaveToStream(stream);
  //保存GDI+のBMPを生成
  dstbmp:=TGPBitmap.Create(TStreamAdapter.Create(stream));
  //引数の指定をImage1.Picture.Bitmap.Canvas.Handleに変更
  //Graphics:=TGPGraphics.Create(Image1.Canvas.Handle);
  Graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);
  try
    Graphics.DrawImage(dstbmp,0,0);
    //拡張子を小文字に変換して取得(.XXX形式:Dotが付いている)
    dotExt:=LowerCase(TPath.GetExtension(画像の保存先Path));
    //JPEG & TIFFに対応する
    if dotExt='.jpg' then begin
      strExt:='jpeg';
    end else begin
      if dotExt='.tif' then begin
        strExt:='tiff';
      end else begin
        strExt:=StringReplace(dotExt,'.','',[rfReplaceAll, rfIgnoreCase]);
      end;
    end;
    //指定された拡張子を付けて保存
    if GetEncoderClsid('image/'+strExt, ImgGUID) >= 0 then
    begin
      //20220930訂正 bmp -> dstbmp
      //bmp.Save(ChangeFileExt(画像の保存先Path, dotExt), ImgGUID);
      dstbmp.Save(ChangeFileExt(画像の保存先Path, dotExt), ImgGUID);
    end;
  finally
    Graphics.Free;
    srcbmp.Free;
    dstBMP.Free;
    stream.Free;
  end;

end;

ちなみに uses は・・・(参考まで)

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.ExtCtrls, Vcl.StdCtrls, Vcl.Imaging.Jpeg, Vcl.Grids, Vcl.ComCtrls,
  System.IOUtils, System.Types, Winapi.ShellAPI, System.UITypes,
  System.IniFiles, System.Masks, Vcl.Filectrl, System.StrUtils,
  Ipl, OpenCV, Winapi.GDIPAPI, Winapi.GDIPOBJ, Winapi.GDIPUTIL,
  System.Win.ComObj, PythonEngine, Vcl.PythonGUIInputOutput, System.ImageList,
  Vcl.ImgList;

  //GDIPAPI, GDIPOBJ はGDI+を利用した描画に資料するために必要
  //usesにWinapi.GDIPUTILを宣言すればGetEncoderClsid関数を利用してGUIDを取得できる
  //System.Win.ComObjはExcelのxlsmファイルの読み書きに必要

TMemoryStreamを使えば、間に「ファイル」というかたちを入れないでBMPデータをGDI+のBitmapに渡すことができるなんて知りませんでした。これで扱う画像の数が何十枚あっても超高速!に保存処理を実行できます。

GDI+ の bitmap クラス(TGPBitmap)と graphics クラスを使って、画像ファイルから VCL の TBitmap へイメージを読み込むことはもうできてるから、これで画像ファイルからGDI+を使って高速にデータを読み取り、VCL Imageに表示し、必要な変更を加えたりした後、VCL TBitmapからGDI+のTGPBitmapにデータを移して高速に保存処理を行う、GDI+の利点を最大限に活用したプログラムが書けるようになった。

2.まとめ

(1)VCLのTBitmapのデータをまずTMemoryStreamに保存する。

var
  //VCL TBitmapからGDI+ Bitmapへ変換
  srcBMP:TBitmap;
  dstBMP:TGPBitmap;
  stream:TMemoryStream;
begin
  //保存(VCL TBitmap -> GDI+ Bitmap)
  srcBMP:=TBitmap.Create;
  srcBMP.Width:=Image1.Width;
  srcBMP.Height:=Image1.Height;
  srcBMP.Assign(Image1.Picture.graphic);
  //データ受け渡し用のストリームを生成して保存
  stream:=TMemoryStream.Create;
  srcbmp.SaveToStream(stream);
  ・・・
end;

(2)GDI+のTGPBitmapに入れるには TStreamAdapter.Create(stream) を使う。

  //GDI+のBMPを生成
  dstbmp:=TGPBitmap.Create(TStreamAdapter.Create(stream));
  //Graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);
  Graphics:=TGPGraphics.Create(Image1.Canvas.Handle);
  try
    Graphics.DrawImage(dstbmp,0,0);

(3)最後に使用したBitmapとMemoryStreamのオブジェクトを解放(Free)する。

  finally
    Graphics.Free;
    srcbmp.Free;
    dstBMP.Free;
    stream.Free;
  end;

3.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。