検索
タグ
ASP.NET
.NET
ASP.NET MVC
Visual Studio
F#
Azure
ASP.NET Core
ライトニングトーク
Plone
Selenium
AJAX
C#
jQuery
SQL Server
JavaScript
ADO.NET Entity Framework
EFCore
WebMatrix
LINQ
Fizz-Buzz
カテゴリ
最新の記事
最新のコメント
記事ランキング
最新のトラックバック
以前の記事
2024年 11月 2024年 10月 2024年 09月 2024年 08月 2024年 04月 2024年 03月 2024年 02月 2024年 01月 2023年 12月 2023年 11月 2023年 10月 2023年 09月 2023年 08月 2023年 07月 2023年 06月 2023年 05月 2023年 04月 2023年 03月 2023年 02月 2023年 01月 2022年 12月 2022年 11月 2022年 10月 2022年 09月 2022年 08月 2022年 07月 2022年 06月 2022年 05月 2022年 04月 2022年 03月 2022年 02月 2022年 01月 2021年 12月 2021年 11月 2021年 10月 2021年 09月 2021年 08月 2021年 07月 2021年 06月 2021年 05月 2021年 04月 2021年 03月 2021年 02月 2021年 01月 2020年 12月 2020年 11月 2020年 10月 2020年 09月 2020年 08月 2020年 07月 2020年 06月 2020年 05月 2020年 04月 2020年 03月 2020年 02月 2020年 01月 2019年 12月 2019年 11月 2019年 10月 2019年 09月 2019年 08月 2019年 07月 2019年 06月 2019年 05月 2019年 04月 2019年 03月 2019年 02月 2019年 01月 2018年 12月 2018年 11月 2018年 10月 2018年 09月 2018年 08月 2018年 07月 2018年 06月 2018年 05月 2018年 04月 2018年 03月 2018年 02月 2018年 01月 2017年 12月 2017年 11月 2017年 10月 2017年 09月 2017年 08月 2017年 07月 2017年 06月 2017年 05月 2017年 04月 2017年 02月 2017年 01月 2016年 12月 2016年 11月 2016年 10月 2016年 09月 2016年 08月 2016年 07月 2016年 06月 2016年 05月 2016年 04月 2016年 03月 2016年 02月 2016年 01月 2015年 12月 2015年 11月 2015年 10月 2015年 09月 2015年 08月 2015年 07月 2015年 05月 2015年 04月 2015年 03月 2015年 02月 2015年 01月 2014年 12月 2014年 11月 2014年 10月 2014年 09月 2014年 08月 2014年 06月 2014年 04月 2014年 03月 2014年 02月 2014年 01月 2013年 12月 2013年 10月 2013年 09月 2013年 08月 2013年 07月 2013年 06月 2013年 05月 2013年 04月 2013年 03月 2013年 02月 2013年 01月 2012年 12月 2012年 11月 2012年 10月 2012年 09月 2012年 08月 2012年 07月 2012年 06月 2012年 05月 2012年 04月 2012年 03月 2012年 02月 2012年 01月 2011年 12月 2011年 11月 2011年 10月 2011年 09月 2011年 08月 2011年 07月 2011年 06月 2011年 05月 2011年 04月 2011年 03月 2011年 02月 2011年 01月 2010年 12月 2010年 11月 2010年 10月 2010年 09月 2010年 08月 2010年 07月 2010年 06月 2010年 05月 2010年 04月 2010年 03月 2010年 02月 2010年 01月 2009年 12月 2009年 10月 2009年 09月 2009年 07月 2009年 06月 2009年 05月 2009年 04月 2009年 03月 2009年 02月 2009年 01月 2008年 12月 2008年 11月 2008年 10月 2008年 09月 2008年 08月 2008年 07月 2008年 06月 2008年 05月 2008年 04月 2008年 03月 2008年 02月 2008年 01月 2007年 12月 2007年 11月 2007年 04月 2007年 03月 2007年 02月 2007年 01月 2006年 11月 2006年 10月 2006年 09月 2006年 08月 2006年 07月 |
2024年 03月 24日
C# プログラミングでの話。 先日、C# の日本語 Discord サーバーにて、以下のような C# プログラミングのお題が出ていた (出典はこちら)。 まず以下のような、机の画像データと、 以下のような背景透過なリンゴの画像データがあって、 この 2 つの画像データを重ね合わせた、つまり、机の上にリンゴが載っている新たな画像データを C# プログラム上で生成する、というものだ (下図)。 そのスレッド上では、WindowsForms や WPF などのプラットフォーム依存の解決策の他に、以下の 3 つのライブラリを使った選択肢が話題にあがっていた。
そこで自分でも試しに、上記 3 種のライブラリを使って、先のお題を .NET 8 の C#コンソールアプリケーション x 3 本として実装してみた。ちなみに画像データのフォーマットは、いずれも PNG で試してみた。 その他の前提条件・仕様としては、いずれのプログラムにおいても、元画像となる机とリンゴの PNG ファイルは、出力フォルダ以下にそれぞれ ./assets/table.png および ./assets/apple.png として配置しておくものとし、および、合成後の画像データは、出力フォルダ直下に image.png というファイル名で保存することとした。 また上記グラフィクスライブラリ x 3種においては、画像ファイルからの読み込みと書き込みについては、それぞれ直接にファイルパスを指定するだけでよいメソッド類が用意されていたりする。しかし今回はちょっと訳ありで、画像ファイルの読み書きは、System.IO 名前空間の File クラスを使って byte[] として読み書きすることとし、上記グラフィクスライブラリは、画像データは byte[] で入出力するよう、いずれのプログラム例も統一した。 OpenCVSharpまず OpenCVSharp 版。実はこれはかなり難儀した。 何はともあれまずは、プロジェクトファイル (*.csproj) に、OpenCVSharp の NuGet パッケージを以下のように追加しておく。 <ItemGroup> <PackageReference Include="OpenCvSharp4" Version="4.9.0.20240103" /> <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.9.0.20240103" /> </ItemGroup> 今回自分は Windows 上で作業していたので、Windows 向けランタイムの NuGet パッケージも併せて追加しておく必要があった。 そして、Program.cs に実装していくのだが、これが一苦労。自分の作例としては最終的に以下のようになった。 using OpenCvSharp; // Create an empty image canvas that is 256 x 256 px by OpenCVSharp. var canvas = new Mat(256, 256, MatType.CV_8UC3, Scalar.Black); // Load the table image and draw it on the canvas var tableImage = await LoadImage("./assets/table.png"); tableImage.CopyTo(canvas);// [new Rect(0, 0, tableImage.Width, tableImage.Height)]); // Load the apple image with alpha channel var appleImage = await LoadImage("./assets/apple.png"); // Create a mask from the alpha channel of the apple image var mask = new Mat(); Cv2.ExtractChannel(appleImage, mask, 3); Cv2.CvtColor(appleImage, appleImage, ColorConversionCodes.BGRA2BGR); Cv2.CvtColor(mask, mask, ColorConversionCodes.GRAY2BGR); // Create a masked version of the apple image Cv2.BitwiseAnd(appleImage, mask, appleImage); // Blend the masked apple image with the masked tableImage var appleRect = new Rect(96, 50, appleImage.Width, appleImage.Height); var blendedImage = tableImage[appleRect]; Cv2.BitwiseNot(mask, mask); Cv2.BitwiseAnd(blendedImage, mask, blendedImage); Cv2.BitwiseOr(blendedImage, appleImage, blendedImage); // Draw back the belnded image to the canvae blendedImage.CopyTo(canvas[appleRect]); // Get the canvas image as a byte array. var imageBytes = canvas.ToBytes(".png"); // Save it. await SaveImage("image.png", imageBytes); Console.WriteLine("The image was saved."); Console.ReadLine(); async ValueTask<Mat> LoadImage(string imageName) { var imageBytes = await File.ReadAllBytesAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, imageName)); // Create a Mat from the byte array. return Cv2.ImDecode(imageBytes, ImreadModes.Unchanged); } async ValueTask SaveImage(string imageName, byte[] imageBytes) { await File.WriteAllBytesAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, imageName), imageBytes); } 何が大変だったかというと、自分ができた範囲では、どうやら OpenCVSharp (というか OpenCV) では、「背景透過な画像の描画」という機能が直接的には提供されていないようなのだ。ではどうするかというと、元の PNG 画像データからアルファチャネル部分を抜き出した "マスク" 画像データを生成し、そのマスク画像データと AND および OR の論理ビット演算で画像を合成していく必要があった。実のところ上記作例は少々手抜きをしており、透明度は正確には反映しておらず「まったく透明かそうでないか」の二択を前提として実装してある。もしかすると自分が間抜けなことをやっているだけで、もっとスマートな解法があるのかもしれないが、もしその場合はコメント欄や SNS 上で知らせていただきたい。 ImageSharpついで ImageSharp 版。 プロジェクトファイル (*.csproj) には、以下のように ImageSharp の NuGet パッケージを以下のように追加しておく。 <ItemGroup> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" /> </ItemGroup> ImageSharp はマネージドコードだけでできているのか (間違ってたらすみません)、特段の実行時プラットフォームを気にすることなく、単一 NuGet パッケージを指定するだけでよいのは、使い勝手がよさそう。 続いて Program.cs は以下のとおり。 using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; // Create an empty image canvas that is 256 x 256 px by ImageSharp. using var canvas = new Image<Rgba32>(256, 256); // Load the table image and draw it on the canvas using var tableImage = await LoadImage("./assets/table.png"); canvas.Mutate(ctx => ctx.DrawImage(tableImage, 1f)); // Load the apple image and draw it on the canvas using var appleImage = await LoadImage("./assets/apple.png"); canvas.Mutate(ctx => ctx.DrawImage(appleImage, new Point(96, 50), 1f)); // Get the canvas image as a byte array. var memoryStream = new MemoryStream(); await canvas.SaveAsPngAsync(memoryStream); var imageBytes = memoryStream.ToArray(); // Save it. await SaveImage("image.png", imageBytes); Console.WriteLine("The image was saved."); Console.ReadLine(); async ValueTask<Image> LoadImage(string imageName) { var imageBytes = await File.ReadAllBytesAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, imageName)); // Create a ImageSharp.Image from the byte array. return Image.Load(imageBytes); } async ValueTask SaveImage(string imageName, byte[] imageBytes) { await File.WriteAllBytesAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, imageName), imageBytes); } 極めて直感的に、「画像データを読み込んで、キャンバスに描き込むだけ」で背景透過処理もやってくれる。 SkiaSharp最後に SkiaSharp 版。 プロジェクトファイル (*.csproj) に、SkiaSharp の NuGet パッケージを以下のように追加しておく。 <ItemGroup> <PackageReference Include="SkiaSharp" Version="2.88.7" /> </ItemGroup> 実は SkiaSharp は、実行時プラットフォームに応じて、各プラットフォームのランタイムが必要である。しかしどうやら NuGet パッケージシステムの依存性解決の仕組みを使って、必要な実行時プラットフォーム向けランタイムを自動的に参照するようだ (間違ってたらすみません)。つまり、アプリケーションプロジェクトのレベルでは、難しいことを考えずに、上記のように単一の NuGet パッケージを参照するだけでよいらしい。 そして Program.cs は以下のとおり。 using SkiaSharp; // Create an empty image canvas that is 256 x 256 px by SkiaSharp. using var surface = SKSurface.Create(new SKImageInfo(width: 256, height: 256)); var canvas = surface.Canvas; // Load the table image and draw it on the canvas. using var tableImage = await LoadImage("./assets/table.png"); canvas.DrawImage(tableImage, 0, 0); // Load the apple image and draw it on the canvas. using var appleImage = await LoadImage("./assets/apple.png"); canvas.DrawImage(appleImage, 96, 50); // Get the canvas image as a byte array. var imageBytes = surface .Snapshot() .Encode(SKEncodedImageFormat.Png, quality: 100) .ToArray(); // Save it. await SaveImage("image.png", imageBytes); Console.WriteLine("The image was saved."); Console.ReadLine(); async ValueTask<SKImage> LoadImage(string imageName) { var imageBytes = await File.ReadAllBytesAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, imageName)); // Create a SKImage from the byte array. return SKImage.FromEncodedData(imageBytes); } async ValueTask SaveImage(string imageName, byte[] imageBytes) { await File.WriteAllBytesAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, imageName), imageBytes); } こちらも ImageSharp 同様、極めて直感的でシンプル。「画像データを読み込んで、キャンバスに描画する」だけだ。ちゃんと背景透過 PNG を考慮して描画してくれる。 実装してみてどうだったか以上の実装例を以下の GitHub リポジトリに公開しておく。 ひととおり実装してみたが、ソースコード上の書き味に関しては、自分は ImageSharp 版より SkiaSharp 版が好み。 OpenCVSharp は自前でマスク処理をしなければならず、このような用途では面倒くさくて採用しないと思う。実行時プラットフォームにあわせたランタイム NuGet パッケージを別途参照追加しなくてはならないのも厄介に感じた。もっとも OpenCVSharp はどちらかといえばこのような 2D グラフィクス描画というよりはエッジ抽出や顔認識などといった "画像処理" 向けだと自分は思っているので、今回のお題で比較するのは筋が違うのだろうな、とは思った。 メモリ消費量について、雑に Visual Studio 上でデバッグ実行した際に、その診断ツールウィンドウに表示されていたヒープサイズ、および、リリースビルドを Windows 上で直接実行したときのタスクマネージャーに表示されていたメモリ消費量を以下に記しておく。
ヒープサイズが最大で、ImageSharp と SkiaSharp との間で 500 KB 前後ほどの差異があるようではあるが、この数字がどの程度信頼できるものか少々怪しい気もするし、500 KB 程のサイズ差を大きいとみるかどうかはケースバイケースだと思うので、ここではそれ以上のことはコメントしないでおく。 Blazor WebAssembly ではどうか実は先のお題は、このような画像合成処理を行なう C# プログラムを、Blazor WebAssembly として実装・ブラウザ上で実行する、という、プラットフォーム要件があった。本記事ではコンソールアプリケーションとして作成したのだが、もちろんこれらコンソールアプリケーションは、ほぼ同じ実装で、そのまま Blazor WebAssembly プログラムとして実現可能なはずだ。 ということで、Blazor WebAssembly 版も試作した。しかし実は OpenCVSharp 版だけがどうも画像データの読み込みが機能せず、実現に至っていない。ImageSharp 版および SkiaSharp 版はさくっと Blazor WebAssembly 版に移植でき、動作確認できた。 Blazor WebAssembly 版についてはまた日を改めて公開したい。
by developer-adjust
| 2024-03-24 20:56
| .NET
|
ファン申請 |
||