C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

ZIPファイルからBitmapImageの高速読み取り

[トピック内 8 記事 (1 - 8 表示)]  << 0 >>

■103107 / inTopicNo.1)  ZIPファイルからBitmapImageの高速読み取り
  
□投稿者/ 紅 (1回)-(2024/05/12(Sun) 12:55:07)

分類:[.NET 全般] 

開発環境:VS2022の.NetFramework4.8
言語:C#(WPF)

ZIPファイルにあるエントリーから直接BitmapImageの読み取りを行うメソッドを作りました。
このメソッドは画像1枚あたり1〜2MB程度なら、体感気にならない速度で実行できます。
ただ8MB以上の画像を読み込んだ場合に、BeginInitメソッドからEndinitメソッドで0.5〜1秒ほどの時間がかかります。

実行PCのCPUなどのスペックを上げれば問題を解消できそうですが、それ以外の方法で実行速度を早くする手段はありますか?
8MBなど以上の画像ファイルを高速で読み込む方法があったらアドバイス頂ければ幸いです。
最終的にBitmapImageに変換できれば、unsafeやWin32APIを使った手法でも構いません。


>サンプル
/// <summary>ZIPファイルのストリームからビットマップイメージを取得する。</summary>
/// <param name="zipPath">ZIPパス</param>
/// <param name="fileName">ファイルパス</param>
/// <returns>ビットマップイメージ</returns>
/// <exception cref="Exception">ZIPファイルの破損などによりストリームからBitmapイメージの変換に失敗した場合</exception>
public static BitmapImage ReadStreamBitmapImage(string zipPath, string fileName)
{
// ストリームから取得したビットマップイメージ
BitmapImage bi = new BitmapImage();

using (ZipArchive archive = ZipFile.Open(zipPath, ZipArchiveMode.Read))
{
// 処理対象のエントリー
ZipArchiveEntry entry = archive.GetEntry(fileName);

try
{
using (BufferedStream bs = new BufferedStream(entry.Open()))
using (MemoryStream ms = new MemoryStream())
{
int data = -1;
while ((data = bs.ReadByte()) != -1)
{
ms.WriteByte((byte)data);
}

ms.Seek(0, SeekOrigin.Begin);

bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.StreamSource = ms;
bi.EndInit();
}

if (bi.CanFreeze)
{
bi.Freeze();
}
}
catch (InvalidDataException ex)
{
// 例外処理
}
}

return bi;
}
引用返信 編集キー/
■103108 / inTopicNo.2)  Re[1]: ZIPファイルからBitmapImageの高速読み取り
□投稿者/ とっちゃん (824回)-(2024/05/12(Sun) 14:44:31)
No103107 (紅 さん) に返信
> 開発環境:VS2022の.NetFramework4.8
> 言語:C#(WPF)
>
> ZIPファイルにあるエントリーから直接BitmapImageの読み取りを行うメソッドを作りました。
> このメソッドは画像1枚あたり1〜2MB程度なら、体感気にならない速度で実行できます。
> ただ8MB以上の画像を読み込んだ場合に、BeginInitメソッドからEndinitメソッドで0.5〜1秒ほどの時間がかかります。
>
> 実行PCのCPUなどのスペックを上げれば問題を解消できそうですが、それ以外の方法で実行速度を早くする手段はありますか?
> 8MBなど以上の画像ファイルを高速で読み込む方法があったらアドバイス頂ければ幸いです。
> 最終的にBitmapImageに変換できれば、unsafeやWin32APIを使った手法でも構いません。
>
zip の展開自体は、Win32 の API はありませんので(zlibを使うか、別のライブラリを使うかの2択)
C# で出来る範囲(unsafe も Win32API にも頼る必要なし)でどうやれば速度向上できるか?を
検討する必要があります。

ひとまず今のソースの範疇での最適化ポイントとしては

1. ストレージへのアクセスを物理的に減らすにはどうするか?
2. ループ処理を極力減らすにはどうするか?

の2点があります。

1は、メモリ使用量との関係もあるため、必ずしも有利とは言えません。
2については、対zipからの処理という点ではほぼ確実に効率が上がります。

ということで改善点としては

1. ZipOpen.OpenRead() ではなく ReadAllBytes() でバイト配列に読み込んで MemoryStream を介して ZipArchive を作る
2. entory.Open()が返すストリームから、一括でバイト配列に読み取って MemoryStream を介して BitmapImage を作る

の2点になると思います。

もしこれらの改善だけでは速度的に難しいとなるともう少し大掛かりなテコ入れが必要になります。

具体的には、
1. BitmapImageの作成をバックグラウンドで事前に処理する
2. 物理を改善する(ストレージを爆速SSDにする)
というあたりになると思います。

2はすでにSSD環境だとそれほど大きく変わらない可能性もありますので
必ずしも投資に見合うか?といわれると微妙です。

引用返信 編集キー/
■103109 / inTopicNo.3)  Re[2]: ZIPファイルからBitmapImageの高速読み取り
□投稿者/ 紅 (2回)-(2024/05/12(Sun) 18:33:14)
ご回答ありがとうございます。

速度の部分は一度どこに問題があるか調査した経緯があります。
BufferedStreamからMemoryStream辺りの部分はたいした時間はかかりませんでした。
サイズが8MB以上の画像を使った場合に200ミリ秒前後だった記憶があります。

時間がかかっているのは、次の部分です。画像サイズによっては1秒以上かかる場合があります。
bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.StreamSource = ms;
bi.EndInit();

>1. BitmapImageの作成をバックグラウンドで事前に処理する
前後の画像を事前に読み取っておいて、それを表示するのもいいかもしれないですね。
バックグラウンド案を考えてみようと思います。
引用返信 編集キー/
■103110 / inTopicNo.4)  Re[3]: ZIPファイルからBitmapImageの高速読み取り
□投稿者/ とっちゃん (825回)-(2024/05/12(Sun) 21:12:27)
No103109 (紅 さん) に返信
> 速度の部分は一度どこに問題があるか調査した経緯があります。
> BufferedStreamからMemoryStream辺りの部分はたいした時間はかかりませんでした。
> サイズが8MB以上の画像を使った場合に200ミリ秒前後だった記憶があります。
>
200ミリ秒ということは、0.2秒です。
0.5〜1秒かかる処理にさらに0.2秒乗っかるってことですよね?
全体の割合からすると2〜3割くらいの割合を占めているように思うのですが?


> 時間がかかっているのは、次の部分です。画像サイズによっては1秒以上かかる場合があります。
> bi.BeginInit();
> bi.CacheOption = BitmapCacheOption.OnLoad;
> bi.StreamSource = ms;
> bi.EndInit();
>
画像の種類はなんでしょうか?
それらは変更できるのでしょうか?

例えば、BMP形式に変えたらどうなりますか?

png/jpeg と違って圧縮の展開が無い分速度の向上が期待できます。
(期待できるだけで保証はありませんが)

BMPにすれば速度が向上できるのであれば変更してしまうというのも選択肢だと思います。

また、WPFは画像ファイルの読み取りにWIC(Windows Imaging Component)を使っているはずなので
これをバイパスさせてしまえば高速化が期待できます(コンポーネントを使うことで
汎用性が上がる分、大きなオーバーヘッドを伴うため)。

といっても、画像の代わりに任意のバイナリデータとして保存しておけるなら…という前提条件が
つくので必ずしも可能とは言えませんが。


> >1. BitmapImageの作成をバックグラウンドで事前に処理する
> 前後の画像を事前に読み取っておいて、それを表示するのもいいかもしれないですね。
> バックグラウンド案を考えてみようと思います。

もちろんこちらのパターンもありです。
勧めておいてなんですが、非同期処理は思ってもみないようなところに落とし穴があったり
余計なボトルネックになってしまうなどもあるので慎重に対応を進める必要があります。

既存の実装によっては、結局設計レベルで作り直ししないと対応できないなども出てくることがあります。

また、逆に十分非同期にできるような状況の場合 ReadStreamBitmapImage の中身だけを非同期にするだけで
十分な場合もあります(やってみないとわからない)。

非同期処理はこうすればいい感じになるという指針がないので難しいんですよね。

引用返信 編集キー/
■103111 / inTopicNo.5)  Re[4]: ZIPファイルからBitmapImageの高速読み取り
□投稿者/ 紅 (3回)-(2024/05/12(Sun) 23:35:17)
更に明確な助言ありがとうございます。

体感、画像の切り替えに1秒未満だとあまり気にならない感じです。
ただ問題個所だけで1秒以上かかると、さすがに遅いと感じるので問題にしています。

GIFやWebPファイルもたまに使うのでWICの機能は必要なんですが、基本的に読み込む画像はJPEG形式が多いです。
たとえばJPEG形式の画像をBitmapやImage型に変えて読み込んだ方が早いということですよね。
その認識で合っているなら同じWindowでできれば、ファイル形式の違う画像を表示させたいので、その方法は難しいかもしれないです。
(元々、WindowFormsで作っていたビューワーを、GIFやWebPファイルを高速に読み込ませるためにWPFに切り替えた経緯があります)
WICのボトルネックなど助言の内容を知らなかったので、今後の参考にします。

>勧めておいてなんですが、非同期処理は思ってもみないようなところに〜
デグレしている箇所がありますのでテスト中ですが、コードも思ったより汚くならずに書けたので、一応こちらで考えています。
業務だったら慎重にならないといけないと思いますが、趣味のツールなので気楽にやっています。

引用返信 編集キー/
■103112 / inTopicNo.6)  Re[5]: ZIPファイルからBitmapImageの高速読み取り
□投稿者/ とっちゃん (826回)-(2024/05/13(Mon) 01:03:48)
No103111 (紅 さん) に返信
> 更に明確な助言ありがとうございます。
>
> 体感、画像の切り替えに1秒未満だとあまり気にならない感じです。
> ただ問題個所だけで1秒以上かかると、さすがに遅いと感じるので問題にしています。
>
zip から MemoryStream を作る部分も BufferedStream を使うとそこでメモリをとりますし
MemoryStream に1バイトずつ書き込んでいくとメモリ伸長を繰り返すので
メモリ利用状態に無駄が生じやすくなります。

微々たるもとおっしゃっていましたが、1秒かかる処理で200ミリ秒は、2割を
占める処理時間です。
その上で、メモリ分断も発生しているであろうことを想像すると
目に見えない無駄が多くなりがちではないかな?と思います。
(あとある程度以上の大きさだと、ラージメモリ固有の問題も出てくる)


> GIFやWebPファイルもたまに使うのでWICの機能は必要なんですが、基本的に読み込む画像はJPEG形式が多いです。
> たとえばJPEG形式の画像をBitmapやImage型に変えて読み込んだ方が早いということですよね。

いえ、WriteableBitmap に WritePixels できるような形で画像の内容を直接持たせて
独自に画像化処理を行うので、WIC(GDI+でも同様)の持つどのデコーダを利用するか?や
実際にデコードするという処理(どんなに簡素な構造でも相応の時間がかかる)を
丸ごとバイパスしてしまうというものです。


> その認識で合っているなら同じWindowでできれば、ファイル形式の違う画像を表示させたいので、その方法は難しいかもしれないです。
> (元々、WindowFormsで作っていたビューワーを、GIFやWebPファイルを高速に読み込ませるためにWPFに切り替えた経緯があります)
> WICのボトルネックなど助言の内容を知らなかったので、今後の参考にします。
>
任意のファイルがzip内に格納されているということですね。
とすると、独自形式にしておくのは無理なので、画像でコード部分は今まで通りとするしか
無いと思います(結論から言えば、BeginInit()〜EndInit()の間の時間は短縮できない)。


> >勧めておいてなんですが、非同期処理は思ってもみないようなところに〜
> デグレしている箇所がありますのでテスト中ですが、コードも思ったより汚くならずに書けたので、一応こちらで考えています。
> 業務だったら慎重にならないといけないと思いますが、趣味のツールなので気楽にやっています。
>
極論すれば、レスポンスを落とさず、パフォーマンスを上げるにはどうするか?
というところになります。

業務かどうかにかかわりなく、戻れるようにしておいてから冒険することをお勧めします。
いざやってみたら途中で動かなくなってしまったなんてことになって
後戻りできないとなると、もう作り直すしかないという悲しい結果しかありませんのでw

ま、最近は GitHub という開発者の強い味方がいるので
ソース管理の導入も容易なので影響は少ないと思いますが。
引用返信 編集キー/
■103114 / inTopicNo.7)  Re[1]: ZIPファイルからBitmapImageの高速読み取り
□投稿者/ radian (158回)-(2024/05/13(Mon) 10:37:30)
2024/05/13(Mon) 10:57:14 編集(投稿者)

No103107 (紅 さん) に返信

> 開発環境:VS2022の.NetFramework4.8

ランタイムは新しい方が最適化されるので、
.NET8以上に乗り換えるだけでも多少は高速化が期待出来るかもしれません。


> ZIPファイルにあるエントリーから直接BitmapImageの読み取りを行うメソッドを作りました。
> このメソッドは画像1枚あたり1〜2MB程度なら、体感気にならない速度で実行できます。
> ただ8MB以上の画像を読み込んだ場合に、BeginInitメソッドからEndinitメソッドで0.5〜1秒ほどの時間がかかります。
>
> 実行PCのCPUなどのスペックを上げれば問題を解消できそうですが、それ以外の方法で実行速度を早くする手段はありますか?
> 8MBなど以上の画像ファイルを高速で読み込む方法があったらアドバイス頂ければ幸いです。
> 最終的にBitmapImageに変換できれば、unsafeやWin32APIを使った手法でも構いません。

オフィシャルの実装がそこまで遅いとも思えませんが、
C++等の高速な実装のラッパーや、より新しいランタイム用に最適化された画像ライブラリがnugetにあるなら、
そういったものを用いると高速化できるかもしれません。

徹底的にやるなら、高速化はまずどの処理がボトルネックになっているかを調査し、
一番早くする余地のある部分から手を入れていくのが基本になります。
画像の読み込みだと、

・ZIPのメモリ展開(ディスクアクセス速度、ZIP展開速度)
・画像のデコード(デコーダーのデコード速度)
・メモリアロケーションの量と回数(少ないほどよい、領域を解放せず再利用出来ると尚良い)

辺りに分けられると思いますが、各処理における実行時間を調査した後に、
現在最速の実装と比較して、どの程度早く出来る余地があるのかを
地道に調べる必要があると思います。

画像を順に読んでいくようなアプリなら、
次に開く画像を別スレッドで先読みしてキャッシュしたりすれば、
空き時間を有効利用出来るので、体感速度は上がりそうな気がします。
(メモリの使用量は増えるでしょうけど)
引用返信 編集キー/
■103116 / inTopicNo.8)  Re[2]: ZIPファイルからBitmapImageの高速読み取り
□投稿者/ 紅 (4回)-(2024/05/13(Mon) 13:15:06)
お二方、ご回答ありがとうございます。
改善点の調査などの話、参考にさせて頂きます。
一応問題点は先に書いた通り、BeginInitの部分になります。
その部分の改善はできないという有用な回答も頂きましたので、別の手段で行いました。

検証はしていないので推測になりますが、恐らく.NET8に移行した場合も今回の問題は発生します。
(他のアプリで.NET6〜8を使ったアプリをいくつか作っていますが、とくに大きな違いはなさそうです)
WPFの場合はDirect3Dを利用して描画を行っているので、GIFやWebPなどのアニメーションで大きな違いを感じています。

事前にビットマップイメージを読み込む方法で一定の効果がありました。
ただ画像を即時に切り替えるといった場合は、まだ体感遅く感じますが。。。改善は見られました。
今後CPUを変えれば、この問題は解決しそうなので解決とします。
貴重な意見の数々、今後の参考にします。ありがとうございました。
引用返信 編集キー/

このトピックをツリーで一括表示


トピック内ページ移動 / << 0 >>

このトピックに書きこむ