トップ回答者
PictureBoxにBitmap表示する際に、例外OutOfMemoryExceptionが発生する

質問
-
お世話になっています
VisualStudio2008でC++/CLIでGUI開発をしています。
PictureBoxに大きなサイズのBitmapを表示しようとすると、例外OutOfMemoryExceptionが発生してしまいます
Bitmapクラスに画像を読み込み、
下記のように、PictureBoxのペイントイベントでDrawImageを使ってPictureBoxに表示しています
private: System::Void pictureBox1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { if(Bmp!=nullptr) { e->Graphics->DrawImage(this->Bmp, 0, 0, this->Bmp->Width, this->Bmp->Height); } }
この処理は、画像サイズが500x500のBitmapではうまく動いています。
ターゲットのアプリケーションでは、Bitmapを徐々に2倍、4倍、8倍・・・と拡大する機能を持たせています
8倍まではうまく動きますが、16倍まで拡大したあたりでOutOfMemoryExceptionが発生します。
このとき、画像サイズは元が500x500なので、16倍すると8000x8000となり
画像サイズは、8000 * 8000 * 3byte(24bitフルカラー) = 192MBとなると思います。
物理メモリは、1.5GBの空きがあるので、まだメモリ不足にはならないはずですが、
もしかして、連続したメモリが取れないといったことでこのような例外が発生しているのでしょうか?
また、この例外発生を抑える解決策をどなかた教えて頂けないでしょうか?
回答
-
DrawImage は経験上、もう 1 ~ 2 枚分の連続メモリが空いていないと失敗することが多いです。
特に巨大な画像(8000*8000 は十分大きい)は、x86 プロセスの DrawImage で扱えるものではありません。
(物理メモリの空きではなく、プロセス内のメモリ空間の連続した空き領域が必要なので、メモリ搭載量を変えても効果はありません)ところで、本当に 8000*8000 で持ち続けないといけないのでしょうか?
現在表示している箇所周辺のみ拡大した状態で持っておくなど、メモリを浮かせる工夫はできないものでしょうか。
巨大画像を x86 プロセスで DrawImage を普通に使っていくなら、かなり厳しいと思いますので、工夫するか、別のソリューションを探すかしないといけないでしょう。// 3bytes per pixel なのかな?Bitmap クラスは細かく指定しなかったら
// 4bytes per pixel になってたりすることもあるので。
質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
- 編集済み AzuleanMVP, Moderator 2012年5月10日 15:44
- 回答としてマーク BB-X LARISSA 2012年5月11日 0:13
-
.NETはまったくわからないのですが、
> このとき、画像サイズは元が500x500なので、16倍すると8000x8000となり
> 画像サイズは、8000 * 8000 * 3byte(24bitフルカラー) = 192MBとなると思います。
ここに引っかかりました。
ひょっとしたら、DDBのサイズ制限かもしれません。
xpだと、CreateCompatibleBitmapの最大値は 192MBでした(実測値)。
( ただし、大きなDDBを複数作り、削除した後だと、制限は少し拡大されます)
.NETには SDKのDIBSectionに相当するものは無いのでしょうか。
- 回答としてマーク BB-X LARISSA 2012年5月11日 0:28
-
.NETには SDKのDIBSectionに相当するものは無いのでしょうか。
.NET Framework の System.Drawing 名前空間は基本的に GDI+ のラッパーです。(例)
GDI+ はその目的から、device-independent なので DIB と考えて差し支えないと思っています。DIBSection を使おうが、192MB(per 24bit) とか 256MB(per 32bit) はとれる数は有限ですし、環境によって増減します。これは前述したようにプロセス内のメモリ空間で連続的にとれることが必要だからです。
GDI+ はうまく使わないと無駄にメモリを消費する傾向にあると思っています。
今回の事例でうまく使えば何とかなるのかと問われると、正直、よくわかっていません。「うまく動く事例を見つけて実装する」(環境依存の不確実なやり方)というよりは、安全なやり方(必要以上に持たない)に倒す方が、トータルの工数としては少なく済むと思っているので細かいところの切り分けはしていません。質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
- 回答としてマーク BB-X LARISSA 2012年5月18日 1:40
-
Microsoft Win32 と Microsoft .NET Framework API との対応
に
CreateBitmap ビットマップを作成します。 System.Drawing.Bitmap constructor メモ DDB ではなく DIB を作成します。
って書いてあり、Azuleanさんご提示のページに
って書いてあるので、DIBであることは間違いなさそうですね。
ただ、「メインとしてはそうだけども、実は内部でなんかごにょごにょやってる可能性はないか?」とか、DIBやDDBの性質のおさらいとかも兼ねて
C++/CLIのBitmapクラスの「扱い」自体は
ネイティブのAPI直触りと比べて、デフォルト指定で作るだけなら遥かに楽なのと
自分用のGDI関連のAPIサイドのコードは以前既に作ってあったので、XP・VisualStudio2008で検証してみました。
一回につき24MB程度の、CreateCompatibleBitmapのDDBの消耗量を追加したネイティブオンリーのアプリを
多重起動していくと、8回あたりで作成できなくなりました。(つまりプロセスが別でもXPの限界で192MB、かな?諸事情により別途VC++を2つ起動中は、やはりいくらか限界数が減少)
CreateCompatibleBitmapでDDBとして作成した際、表示されているメモリ使用量や仮想メモリサイズには変化がないので、こちらのXPの環境においてはビデオメモリに確保されている可能性が高いと思われますが
ビデオメモリは700MBぐらいあったと思うので、これはsnaoさんの仰るように純粋にXPの192MBという限界だと考えられます。
そこで、上記ネイティブアプリを7回など、ぎりぎりそれに達しないところまで起動してDDB用の領域を枯渇寸前にしておいた状態で
新規に作ったマネージアプリにピクチャーボックスを配置し
ほにゃらら = gcnew Bitmap(幅,高さ);
といった、幅と高さのみの引数のSystem::Drawing::Bitmapのコンストラクタをもちいて
Bmp = gcnew Bitmap(8000,8000);
という巨大ビットマップを作成してみると
成功してしまいました。
なお、プロセス内のメモリの使用量もトータルのメモリの使用量も大丈夫であれば
Bmp = gcnew Bitmap(9000,9000);
ぐらいまでは出来ました。
といってもこれは、「一応不可能ではない状況があっただけ」ということですね。
詳しくは後述します。
さらに BB-X LARISSAさんご提示の
private: System::Void pictureBox1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { if(Bmp!=nullptr) { e->Graphics->DrawImage(this->Bmp, 0, 0, this->Bmp->Width, this->Bmp->Height); } }
このコードそのままで、意図的にPaintイベントを呼び出した時
PF使用量が大幅に増加したのですが
これは、ふたを開けてみるとPaintイベント中メモリ使用量(及び仮想メモリ サイズの項目)が同じように大幅に増加していました。
以上より
やはり少なくとも
Azuleanさんの予想通り、C++/CLIのBitmapは(画面描画時の最小限の・・・とかまではどうかは分かりませんが、少なくとも大量確保という意味での)DDBは使っていない可能性が高いと考えられます。
次に
プロセスの仮想メモリ サイズだけだなく、メモリ使用量がだいぶ増加しているというところに目を付けてみました。
通常CreateDIBSectionでDIBSectionを作っただけでは仮想メモリ サイズは増えましたが、『メモリ使用量』の項目は増えませんでした。
なので、あくまで単純にバイト列を内部で確保している、DIBの方法まんまと言う印象を受けます。
ただ、それだけでは到底プロセスのメモリ使用量の限界はほど遠かったので、もう少し観察してみましたが
生成時と描画時にさらにメモリ使用量が増えますね。
Azuleanさんの仰る通り、倍以上は余分に連続領域が要る感じです。
つまり、より正確には
状況によっては上記9000*9000ぐらいも可能な状況もある、ただし環境によってぎりぎりすぎるので、実用性はあくまで皆無ということですね。
ここまでは確認できました。
そこで、仮に速度が全く問われないアプリで
.NET FrameworkのBitmapクラスを使って、ネイティブ抜きで簡単にやりたい場合は
方向性としてはBB-X LARISSAさんの現在の案のように
それは出来る限り小さいものにして、スクロールや拡大貼り付けは冒頭のリンク先の
System.Drawing.Graphics.DrawImageがStretchBlt
に対応してるとの事なので、そこだけ必要事項を計算してやってしまえば、簡単ではあるかもしれません。
(Win32 APIは準備や解放などが、マネージコードと比べてそもそも結構面倒な上に、ネイティブとマネージの連携となるとより一層面倒な可能性があります。ただ、もう少し速度が必要なのであれば必要な分だけネイティブコードを混在させることになるかなとは思います。)
なお、巨大DIBSectionを作った場合でも、マッピング自体は仮想アドレス割り当て作業なので、先に計算しておくということはやっておいて必要が生じたところを小出しにマップ・・・とかいう方法を採用しない限りは、結局膨大な連続アドレスが必要になってしまう事にかわりはないはずです。
こういう切り出しをやろうとすると、「マップ出来るアラインメント(64KBとか)に対するアドレス調整」とかがまたかなり面倒です。
※(追記) ↑よく考えてみると、DIBでなくDIBSectionとして使う分なら、そこの調整については不要かもしれません。CreateDIBSectionにファイルマッピングオブジェクトのハンドルを渡して試したことが以前にチョロっとしかなく(いつもはNULL)、DIBSectionとしてつかいながら、しかも途中から切り出すということは試したことがないうえに、コードが残っていなかったので、オフセットとして指定できる値に制限があるのかないのか、また直感的でない裏仕様がなかったか、その辺までは曖昧です。
ここまでの情報をほとんどご存知で、コードが既に手元にあるならまだしも
(高さをマイナスにしないと普通の座標と同じように出来ないとか、1ライン4の倍数バイトに切り上げないと・・・などと言った微妙なところも含め)
リソースの解放タイミング・管理方法も踏まえてコードを書きまくって、ようやくスタートライン、という感じになるので
全部知ってても現在コードがなかったら、びっくりするほど面倒です。
なので、現状の情報の範囲では
そこまでするより、単に必要箇所を切り出して拡大貼り付けするだけの方が得策に思えます。
ただし、もっと気のきいた様々な描画処理を先に済ませた状態を保存しておいて、かつめちゃめちゃメモリ使用量を少なくしたい
かつDirectXを使わない
のであれば、最終奥義的な感じで採用する・・・というぐらいの方が良いと思います。
(あるいは、単に元の大きさの画像の全体を、ファイルマッピングなDIBSectionとして・・・という方法も、状況次第でありかもとは思いますが、それはまずファイルの共有について考えないと行けなくて
そして、その場合はしかもなんと読み取り専用のオープンを出来ない仕様になっていたので、正確にコードを書けば何も問題はないですが、ちょっと間違うと元画像がめちゃめちゃになる可能性もあるのでご注意ください。)
ただし、今後複雑な描画処理をしつつ描画が高速である必要が出てくることがはっきりしているなら、DIBやDIBSectionなどを使いつつCPUに演算させるのではなく、DirectXなどを使ってGPUに計算してもらった方が良いと思います。
そこまでする必要がなければ、あくまでここでは
.NET Frameworkだけ、あるいは僅かにGDIのAPIを絡めながら、単に位置計算して拡大貼り付け・・・で問題ないと思います。
- 編集済み mr.setup 2012年5月14日 20:14
- 回答としてマーク BB-X LARISSA 2012年5月18日 1:41
すべての返信
-
回答ありがとうございます
質問に記載したDrawImageの行で例外発生します
e->Graphics->DrawImage(this->Bmp, 0, 0, this->Bmp->Width, this->Bmp->Height);
例外の内容は下記のとおりです
System.OutOfMemoryException' のハンドルされていない例外が System.Drawing.dll で発生しました。追加情報: メモリが不足しています。
プロセスのメモリ使用量をタスクマネージャで確認したところ、
1倍 49,088K
2倍 52,521K
4倍 64,896K
8倍 111,768K
16倍 303,172K ←ここで例外
となっていました。実際は16倍時点で303MB使っているようです。
例外発生前、発生後ともに、PCの動作が重くなったりといった兆候は特にありません。
-
DrawImage は経験上、もう 1 ~ 2 枚分の連続メモリが空いていないと失敗することが多いです。
特に巨大な画像(8000*8000 は十分大きい)は、x86 プロセスの DrawImage で扱えるものではありません。
(物理メモリの空きではなく、プロセス内のメモリ空間の連続した空き領域が必要なので、メモリ搭載量を変えても効果はありません)ところで、本当に 8000*8000 で持ち続けないといけないのでしょうか?
現在表示している箇所周辺のみ拡大した状態で持っておくなど、メモリを浮かせる工夫はできないものでしょうか。
巨大画像を x86 プロセスで DrawImage を普通に使っていくなら、かなり厳しいと思いますので、工夫するか、別のソリューションを探すかしないといけないでしょう。// 3bytes per pixel なのかな?Bitmap クラスは細かく指定しなかったら
// 4bytes per pixel になってたりすることもあるので。
質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
- 編集済み AzuleanMVP, Moderator 2012年5月10日 15:44
- 回答としてマーク BB-X LARISSA 2012年5月11日 0:13
-
.NETはまったくわからないのですが、
> このとき、画像サイズは元が500x500なので、16倍すると8000x8000となり
> 画像サイズは、8000 * 8000 * 3byte(24bitフルカラー) = 192MBとなると思います。
ここに引っかかりました。
ひょっとしたら、DDBのサイズ制限かもしれません。
xpだと、CreateCompatibleBitmapの最大値は 192MBでした(実測値)。
( ただし、大きなDDBを複数作り、削除した後だと、制限は少し拡大されます)
.NETには SDKのDIBSectionに相当するものは無いのでしょうか。
- 回答としてマーク BB-X LARISSA 2012年5月11日 0:28
-
.NETには SDKのDIBSectionに相当するものは無いのでしょうか。
.NET Framework の System.Drawing 名前空間は基本的に GDI+ のラッパーです。(例)
GDI+ はその目的から、device-independent なので DIB と考えて差し支えないと思っています。DIBSection を使おうが、192MB(per 24bit) とか 256MB(per 32bit) はとれる数は有限ですし、環境によって増減します。これは前述したようにプロセス内のメモリ空間で連続的にとれることが必要だからです。
GDI+ はうまく使わないと無駄にメモリを消費する傾向にあると思っています。
今回の事例でうまく使えば何とかなるのかと問われると、正直、よくわかっていません。「うまく動く事例を見つけて実装する」(環境依存の不確実なやり方)というよりは、安全なやり方(必要以上に持たない)に倒す方が、トータルの工数としては少なく済むと思っているので細かいところの切り分けはしていません。質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
- 回答としてマーク BB-X LARISSA 2012年5月18日 1:40
-
すみません、そっち側使ったことがなかったので変に断定していましたので、フォロー助かります。
だとすると、DIBSeciton と BitBlt とかその手の Win32API をごりごり使うという選択肢はあり得ますね。
API を直接使うことになるので Graphics クラスとうまくつきあわせないといけないので、ハードルは上がるかな。
(巨大な画像を扱う時点で十分ハードル高いので、API ぐらいは何とでもなるかもしれませんが)質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
-
Microsoft Win32 と Microsoft .NET Framework API との対応
に
CreateBitmap ビットマップを作成します。 System.Drawing.Bitmap constructor メモ DDB ではなく DIB を作成します。
って書いてあり、Azuleanさんご提示のページに
って書いてあるので、DIBであることは間違いなさそうですね。
ただ、「メインとしてはそうだけども、実は内部でなんかごにょごにょやってる可能性はないか?」とか、DIBやDDBの性質のおさらいとかも兼ねて
C++/CLIのBitmapクラスの「扱い」自体は
ネイティブのAPI直触りと比べて、デフォルト指定で作るだけなら遥かに楽なのと
自分用のGDI関連のAPIサイドのコードは以前既に作ってあったので、XP・VisualStudio2008で検証してみました。
一回につき24MB程度の、CreateCompatibleBitmapのDDBの消耗量を追加したネイティブオンリーのアプリを
多重起動していくと、8回あたりで作成できなくなりました。(つまりプロセスが別でもXPの限界で192MB、かな?諸事情により別途VC++を2つ起動中は、やはりいくらか限界数が減少)
CreateCompatibleBitmapでDDBとして作成した際、表示されているメモリ使用量や仮想メモリサイズには変化がないので、こちらのXPの環境においてはビデオメモリに確保されている可能性が高いと思われますが
ビデオメモリは700MBぐらいあったと思うので、これはsnaoさんの仰るように純粋にXPの192MBという限界だと考えられます。
そこで、上記ネイティブアプリを7回など、ぎりぎりそれに達しないところまで起動してDDB用の領域を枯渇寸前にしておいた状態で
新規に作ったマネージアプリにピクチャーボックスを配置し
ほにゃらら = gcnew Bitmap(幅,高さ);
といった、幅と高さのみの引数のSystem::Drawing::Bitmapのコンストラクタをもちいて
Bmp = gcnew Bitmap(8000,8000);
という巨大ビットマップを作成してみると
成功してしまいました。
なお、プロセス内のメモリの使用量もトータルのメモリの使用量も大丈夫であれば
Bmp = gcnew Bitmap(9000,9000);
ぐらいまでは出来ました。
といってもこれは、「一応不可能ではない状況があっただけ」ということですね。
詳しくは後述します。
さらに BB-X LARISSAさんご提示の
private: System::Void pictureBox1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { if(Bmp!=nullptr) { e->Graphics->DrawImage(this->Bmp, 0, 0, this->Bmp->Width, this->Bmp->Height); } }
このコードそのままで、意図的にPaintイベントを呼び出した時
PF使用量が大幅に増加したのですが
これは、ふたを開けてみるとPaintイベント中メモリ使用量(及び仮想メモリ サイズの項目)が同じように大幅に増加していました。
以上より
やはり少なくとも
Azuleanさんの予想通り、C++/CLIのBitmapは(画面描画時の最小限の・・・とかまではどうかは分かりませんが、少なくとも大量確保という意味での)DDBは使っていない可能性が高いと考えられます。
次に
プロセスの仮想メモリ サイズだけだなく、メモリ使用量がだいぶ増加しているというところに目を付けてみました。
通常CreateDIBSectionでDIBSectionを作っただけでは仮想メモリ サイズは増えましたが、『メモリ使用量』の項目は増えませんでした。
なので、あくまで単純にバイト列を内部で確保している、DIBの方法まんまと言う印象を受けます。
ただ、それだけでは到底プロセスのメモリ使用量の限界はほど遠かったので、もう少し観察してみましたが
生成時と描画時にさらにメモリ使用量が増えますね。
Azuleanさんの仰る通り、倍以上は余分に連続領域が要る感じです。
つまり、より正確には
状況によっては上記9000*9000ぐらいも可能な状況もある、ただし環境によってぎりぎりすぎるので、実用性はあくまで皆無ということですね。
ここまでは確認できました。
そこで、仮に速度が全く問われないアプリで
.NET FrameworkのBitmapクラスを使って、ネイティブ抜きで簡単にやりたい場合は
方向性としてはBB-X LARISSAさんの現在の案のように
それは出来る限り小さいものにして、スクロールや拡大貼り付けは冒頭のリンク先の
System.Drawing.Graphics.DrawImageがStretchBlt
に対応してるとの事なので、そこだけ必要事項を計算してやってしまえば、簡単ではあるかもしれません。
(Win32 APIは準備や解放などが、マネージコードと比べてそもそも結構面倒な上に、ネイティブとマネージの連携となるとより一層面倒な可能性があります。ただ、もう少し速度が必要なのであれば必要な分だけネイティブコードを混在させることになるかなとは思います。)
なお、巨大DIBSectionを作った場合でも、マッピング自体は仮想アドレス割り当て作業なので、先に計算しておくということはやっておいて必要が生じたところを小出しにマップ・・・とかいう方法を採用しない限りは、結局膨大な連続アドレスが必要になってしまう事にかわりはないはずです。
こういう切り出しをやろうとすると、「マップ出来るアラインメント(64KBとか)に対するアドレス調整」とかがまたかなり面倒です。
※(追記) ↑よく考えてみると、DIBでなくDIBSectionとして使う分なら、そこの調整については不要かもしれません。CreateDIBSectionにファイルマッピングオブジェクトのハンドルを渡して試したことが以前にチョロっとしかなく(いつもはNULL)、DIBSectionとしてつかいながら、しかも途中から切り出すということは試したことがないうえに、コードが残っていなかったので、オフセットとして指定できる値に制限があるのかないのか、また直感的でない裏仕様がなかったか、その辺までは曖昧です。
ここまでの情報をほとんどご存知で、コードが既に手元にあるならまだしも
(高さをマイナスにしないと普通の座標と同じように出来ないとか、1ライン4の倍数バイトに切り上げないと・・・などと言った微妙なところも含め)
リソースの解放タイミング・管理方法も踏まえてコードを書きまくって、ようやくスタートライン、という感じになるので
全部知ってても現在コードがなかったら、びっくりするほど面倒です。
なので、現状の情報の範囲では
そこまでするより、単に必要箇所を切り出して拡大貼り付けするだけの方が得策に思えます。
ただし、もっと気のきいた様々な描画処理を先に済ませた状態を保存しておいて、かつめちゃめちゃメモリ使用量を少なくしたい
かつDirectXを使わない
のであれば、最終奥義的な感じで採用する・・・というぐらいの方が良いと思います。
(あるいは、単に元の大きさの画像の全体を、ファイルマッピングなDIBSectionとして・・・という方法も、状況次第でありかもとは思いますが、それはまずファイルの共有について考えないと行けなくて
そして、その場合はしかもなんと読み取り専用のオープンを出来ない仕様になっていたので、正確にコードを書けば何も問題はないですが、ちょっと間違うと元画像がめちゃめちゃになる可能性もあるのでご注意ください。)
ただし、今後複雑な描画処理をしつつ描画が高速である必要が出てくることがはっきりしているなら、DIBやDIBSectionなどを使いつつCPUに演算させるのではなく、DirectXなどを使ってGPUに計算してもらった方が良いと思います。
そこまでする必要がなければ、あくまでここでは
.NET Frameworkだけ、あるいは僅かにGDIのAPIを絡めながら、単に位置計算して拡大貼り付け・・・で問題ないと思います。
- 編集済み mr.setup 2012年5月14日 20:14
- 回答としてマーク BB-X LARISSA 2012年5月18日 1:41