- 投稿日:2022-03-23T22:19:01+09:00
【Unity】Editor拡張でメニューからHLSLを作成できるようにした
ソースコード こんな感じで作れるようになります。 作ったわけ HLSLをUnityで作るには右クリックしてShow in Explorer テキストを作成して 名前を変更して拡張子をhlslに書き換えれば完成! しかしこの一連の動作が面倒くさいのでEditor拡張して簡単に作れるようにしようというわけです。 解説 [MenuItem("Assets/Create/Shader/HLSLScript")] これでUnityの上のメニューバーのAssetsボタンやProjectウィンドウで右クリックしたときにメニューを追加します。 foreach (Object obj in Selection.GetFiltered(typeof(DefaultAsset), SelectionMode.DeepAssets)) { if (obj is DefaultAsset) { path = AssetDatabase.GetAssetPath(obj); } } これで右クリックした場所のパスを取得します。 path = EditorUtility.SaveFilePanelInProject("CreatHLSLScript", "NewShader", "hlsl", "", path); EditorUtility.SaveFilePanelInProject()を使うことでファイルを生成するウィンドウを出すことができます。 参考 : https://docs.unity3d.com/ja/current/ScriptReference/EditorUtility.SaveFilePanelInProject.html File.WriteAllText(path,""); File.WriteAllText()で新しいファイルを作成します。 参考 : https://docs.microsoft.com/ja-jp/dotnet/api/system.io.file.writealltext?view=net-6.0 AssetDatabase.Refresh(); 最後にAssetDatabase.Refresh()で更新を行います。
- 投稿日:2022-03-23T21:21:54+09:00
ImageSharpのバージョンアップでマークシートシステム Mark2のマーク認識が高速化
マークシートシステム Mark2の近況 Blazor WebAssemblyを利用して開発しているマークシートシステム Mark2はまわりの人に使ってもらう機会も増えてきて、先日は地方自治体向けにマークシートとWebアンケートに対応したハイブリッドアンケートシステムのプレスリリースを配信しました。利用してもらうといろいろなフィードバックをいただき、やはり一番多いリクエストは処理速度を速くできないかというもので、引き続き改善を進めています。 処理速度のボトルネックになっているのは主に画像処理で、Blazor WebAssemblyのAOTコンパイルによって高速化されているのですが、やはりデスクトップアプリと比較すると動作が遅く感じる部分もあるのかもしれません。 ImageSharpのバージョンアップ 2022年2月7日にImageSharp 2.0.0がリリースされ、パフォーマンスの向上に関する記載がありました。 いくつかの破壊的な変更があっためにMark2のプログラムを修正しているうちに、ImageSharp 2.1.0がリリースされていました。せっかくなのでこれまで利用していたImageSharp 1.0.4と2.1.0で処理速度にどのくらいの変化があったのか計測してみることにしました。 マークシートのマーク認識の処理速度を比較 800枚のマークシートをマーク認識した処理時間は以下のようになりました。 ImageSharp 処理時間(秒) 1.0.4 276.448 2.1.0 210.254 ImageSharp 1.0.4から2.1.0にバージョンアップすることで約1.3倍の高速化ができました。ここまで速くなると思っていなかったので、期待以上でした。
- 投稿日:2022-03-23T21:05:07+09:00
ImageSharpのバージョンアップでマークシートシステム Mark2のマーク認識が高速化
マークシートシステム Mark2の近況 Blazor WebAssemblyを利用して開発しているマークシートシステム Mark2はまわりの人に使ってもらう機会も増えてきて、それと同時にいろいろな要望をいただくようになりました。やはり一番多いリクエストは処理速度を速くできないかというもので、引き続き改善を進めています。 処理速度のボトルネックになっているのは主に画像処理で、Blazor WebAssemblyのAOTコンパイルによって高速化されているのですが、やはりデスクトップアプリと比較すると動作が遅く感じる部分もあるのかもしれません。単純に画像処理の高速化を頑張るだけでなく、ユーザー体験のデザインが重要になっていると感じます。 ImageSharpのバージョンアップ 2022年2月7日にImageSharp 2.0.0がリリースされ、パフォーマンスの向上に関する記載がありました。 いくつかの破壊的な変更があっためにMark2のプログラムを修正しているうちに、ImageSharp 2.1.0がリリースされていました。せっかくなのでこれまで利用していたImageSharp 1.0.4と2.1.0で処理速度にどのくらいの変化があったのか計測してみることにしました。 マークシートのマーク認識の処理速度を比較 800枚のマークシートをマーク認識した処理時間は以下のようになりました。 ImageSharp 処理時間(秒) 1.0.4 276.448 2.1.0 210.254 マークシートのマーク認識ではImageSharp 1.0.4から2.1.0にバージョンアップすることで約1.3倍の高速化ができました。ここまで速くなると思っていなかったので、期待以上でした。 参考:マークシートシステム Mark2に関するプレスリリース 地方自治体向けにマークシートとWebアンケートに対応したハイブリッドアンケートシステムのプレスリリースを配信しました。
- 投稿日:2022-03-23T19:55:30+09:00
C# ユーザー定義型データの深掘り「構造体」
おはようございますこんにちはこんばんは ユーザー定義型データということで、この記事の続きです。 おさらい ユーザー定義型データにはこんな種類がありました。 データ型 型の種類 構造体 値型 列挙体 値型 リスト 参照型 レコード 参照型 配列 参照型 クラス 参照型 デリゲート 参照型 インターフェイス 参照型 さらにメモリ、値型、参照型についてお話しした記事もありましたね。 この記事は関係のないこと(PCの主要パーツ)にもちらっと触れているので、興味があればご覧ください。 ユーザー定義型データをそれぞれ見ていこう 多いなぁ長くなりそう笑 できればパパッといきたいですね! ということで早速上から順に見ていきましょうか! 長期連載になる可能性… 構造体 構造体はクラスとよく似た機能を持っている値型のデータです。 構造体の例 using System; var point = new Point(10, 40); //魔法のキーワードを使ってPoint構造体のインスタンスをx = 10, y = 40で生成 point.Main(); //出力は「X = 10, Y = 40」 //構造体の定義↓ public readonly struct Point //読み取り専用のPoint構造体を定義する { public Point(int x, int y) { X = x; //Pointメソッドのint型変数xの値をXフィールドに格納する Y = y; //Pointメソッドのint型変数yの値をYフィールドに格納する } public int X { get; } // Xは読み取り専用ですよ(この時点では変数) public int Y { get; } // Yは読み取り専用ですよ(この時点では変数) public void Main() //コンソールに表示するだけのメソッド { Console.WriteLine("X = {0}, Y = {1}", X, Y); } } これ合ってるかなぁって感じで書きました。(コメントアウトの内容も心配です〜) レシピ本見て改変しています。 ちなみにレシピ本にはMainメソッドはなく、overrideしたToStringメソッドで値を返し、Console.WriteLine(point.ToString())で値を出力しています。 何言ってるんだって感じですよね。とりあえず頭の中に入力された情報を何かしらの形で出力することが目的なので、コードを改変してますよということです。 なんてったって備忘録ですから。(しかもレシピ本はバージョンがちょっと古いやつね) 一応コンソールアプリで実行してみてエラーはなかったので、出力自体は可能です。 ところで構造体の定義でMainメソッドがあって良いものなのかという疑問があるのですがどうなんでしょ? 構造体の定義に必要なこと structキーワード structのMS公式ドキュメント 構造体を作りますよという宣言です。structure(構造)のことですね。 newキーワード newのMS公式ドキュメント 構造体を定義した上に変数を宣言、newキーワードでインスタンスを生成します。 以前書いた記事でもご紹介してますね。 なるべく読み取り専用で! でもネット記事によってはそこまで重要ではなさそうな印象なので、意外と必要ではないかも? 必須ではないが新しく出たキーワード public publicのMS公式ドキュメント 割と必須かもしれないと思いつつこちらに書いています。(もしかしたら以前書いた記事で出てきているかも?) 今回はpublicキーワードやprivateキーワードなどがメインの記事ではないので。 このキーワードはアクセス修飾子というものになります。 これはプログラムないだろうが別のソースファイルだろうが同一プロジェクト内であればこの変数やメソッドにアクセスできますよという、スコープ範囲を明示するものです。(僕の解釈です。) 一応スコープ範囲について補足すると、「あなた(変数やメソッド)の適用範囲はここからここまでね、これ以上超えたら使えないよ」みたいな感じ。 スコープについては別記事で詳しく解説します。 readonly readonlyのMS公式ドキュメント 読み取り専用であることを明示するキーワードです。 構造体の定義の際はできるだけreadonlyキーワードを使用することが推奨されています。(レシピ本が言ってた) get getのMS公式ドキュメント アクセサーと呼ばれるキーワードです。 getなので、値を受け取りますよという意味です。 今回のコードではreadonlyキーワードを使っているため、フィールドの値は読み取り専用である必要があります。 反対にreadonlyを使っていない場合は、getキーワードの他にsetキーワードというものも使えます。 setキーワードはgetキーワードの逆で、値を書き込みますよみたいな感じです。(若干違いますがわかりやすくするためですご了承を) getキーワード、setキーワード共にさまざまな書き方があるので、いずれアクセサーについても投稿しますね。 構造体でできないこと 以前の記事でクラスについて少し触れましたが、なんとなく似ている気がしますよね。 冒頭でもクラスとよく似た〜なんて言っているので実際似ているんです。 そんなクラスと構造体の違いなのですが、クラスと違ってできないことがあります。 クラスの継承ができない(そもそもクラスではないし、継承元にはなれない) インスタンスフィールドの初期化ができない インスタンスプロパティの初期化ができない 引数なしコンストラクターの定義ができない(引数ありコンストラクタは可能で、クラスは引数の有無に関わらず可能) 静的なクラス/構造体の生成ができない 長期連載確定ですね ソースコード載せてさらにキーワードの解説となると、やっぱりちょっと長くなりますね。 読みやすいように一つ一つ解説する感じにしてみましょう。(何度も言いますが備忘録です) という訳で今回はここまで! お疲れ様でした!
- 投稿日:2022-03-23T18:42:47+09:00
【C#】Waveファイルを自力で読み書きし音声を自動分割するアプリケーションを書いた話
はじめに 今回は、音声ファイルの無音を検知して自動で分割するアプリケーションを書いたのでその技術的な話を記録として書いておきます。開発記シリーズです。そしてまたもマルチメディア関係のアプリケーションです。 後述しますが、1~2年前に書いたコードをそのまま使いまわしているので書き方が最適ではない、あるいは古臭い部分がありますが、温かい目で見守ってくれると嬉しいです。 ソースコード全文は以下のリポジトリにあります。 ベータ版のビルド済みバイナリは以下にあります。 Wave入出力 今回はwaveファイルをライブラリの力を借りずに自力で読み書きしてみようと思います。Waveファイル以外の読み取りには以前書いたffmpegの記事が参考になるかもしれません(宣伝)。 Waveヘッダーの構造 まず、Waveのヘッダーの構造を理解する必要があります。適宜バイナリエディタを併用するなどすると理解が早いでしょう。 Waveファイルの最初の12バイトは以下のような構造になっています。 開始バイト 終了バイト 内容 0 3 "RIFF"という文字列 4 7 全体のファイルサイズ[bytes] 8 11 "WAVE"という文字列 文字列はASCII(またはUTF-8などのASCII互換なcharset)で読み書きします。 最初の12バイトがこのように読めない場合は、別のフォーマットであるので例外を投げるなど解釈をやめるようにしてください。 また、ファイルサイズはバイト単位で書かれており、フォーマット上4GBまで想定する必要があります(32ビット符号 なし 整数またはそれ以上の型で表現してください)。 以下に参考実装を示します。実際には unsafe なコードを書いたほうがパフォーマンスはよいです。 private static readonly string _RIFF = "RIFF"; private static readonly byte[] RIFF = Encoding.UTF8.GetBytes(_RIFF); private static readonly string _WAVE = "WAVE"; private static readonly byte[] WAVE = Encoding.UTF8.GetBytes(_WAVE); /// <summary> /// Waveのヘッダ部を読み取ります。 /// </summary> /// <param name="stream">読み取る対象。ヘッダ部の最初を示している必要があります。終了時はデータの先頭にまで移動します。</param> /// <returns></returns> public static WaveHeader Read(Stream stream) { var buffer = new byte[4]; uint fileSize; stream.Read(buffer, 0, 4); if (!buffer.SequenceEqual(RIFF)) { throw new InvalidHeaderException(nameof(RIFF)); } stream.Read(buffer, 0, 4); fileSize = BitConverter.ToUInt32(buffer, 0); stream.Read(buffer, 0, 4); if (!buffer.SequenceEqual(WAVE)) { throw new InvalidHeaderException(nameof(WAVE)); } } チャンク 一番最初以外は、チャンクという単位に分かれています。チャンクは以下のような構造をしています。 開始バイト 終了バイト 内容 0 3 チャンクの名前 4 7 チャンクのデータのサイズ[bytes] 8 ... チャンクのデータ(可変長) 注意事項などはヘッダーの最初の部分と同じです。 主なチャンクの種類を以下に示します。 チャンク名 必須? 内容 fmt true 音声フォーマットを記載 data true 音声データを記載 list false メタデータを記載 最低限、fmtチャンクとdataチャンクさえ読み書きできればOKです。 それ以外のチャンクは読み飛ばすことにします。 fmtチャンク ファイルのフォーマットを教えてくれるのがfmtチャンクです。 ヘッダーに実際に書かれるチャンクの名前は、fmt です。(スペースに注意) 最初にfmt の4byteが書かれ、それ以降は以下のような構造をしています。 /// <summary> /// Waveヘッダのフォーマットチャンク部に対応するデータを表現します。 /// </summary> /// <remarks>メモリレイアウトが一致するように設定されています。</remarks> [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 20)] public struct WaveHeaderFormatChunk { [FieldOffset(0)] public int FormatSize; [FieldOffset(4)] public short FormatCode; [FieldOffset(6)] public short ChannelCount; [FieldOffset(8)] public int SampleRate; [FieldOffset(12)] public int ByteRate; [FieldOffset(16)] public short BlockSize; [FieldOffset(18)] public short BitPerSample; } こういう構造体を書いておいて Unsafe.As メソッドで再解釈するのが結構楽です。 フィールド名はある程度適当です。専門用語とかではないのですいません。 ヘッダーの意味をかんたんに説明すると 1 2 FormatSize このチャンクの長さ。通常16。 FormatCode フォーマットの種類。通常1。 ChannelCount チャンネル数。ステレオなら2。 SampleRate 一秒あたりのサンプル数。CDなら44100。 ByteRate 一秒あたりのバイト数。ビットレートではなくバイトレートなので注意。 BlockSize 1ブロックのサイズ。つまり、全チャンネルぶん合計の1サンプルのサイズ。 BitPerSample サンプルのビットの深さ。CDなら16bit。 となります。上記の構造体を介した読み取りのサンプルコードは以下。"fmt " が見つかるまで stream を読み進め、見つかったら以下のメソッドを呼びます。 private static WaveHeaderFormatChunk ReadFormatChunk(Stream stream) { const int size = 20; var buffer = new byte[size]; stream.Read(buffer, 0, size); ref WaveHeaderFormatChunk result = ref Unsafe.As<byte, WaveHeaderFormatChunk>(ref buffer[0]); return result; } dataチャンク 音声データが書き込まれているのがこのdataチャンクです。やはり最初の4byteが"data"で、次の4byteがdataチャンク本体のサイズです。 44.1Khz/16bitでステレオの音声ファイルを例に取ると、dataチャンクには、右チャンネルの1サンプル目(2byte)、左チャンネルの1サンプル目(2byte)、右チャンネルの2サンプル目(2byte)...と続きます。 チャンネルごとに分けてデータを持ちたい場合、以下のようなコードで読めます。 public static WaveAudio Read(Stream stream) { WaveHeader header = WaveHeaderReader.Read(stream); WaveHeaderFormatChunk format = header.WaveHeaderFormatChunk; // PCMかどうか確認。PCM以外はサポートする気はない。 if (format.FormatCode != 1 || format.FormatSize != 16) { throw new NotSupportedException(); } // 16ビットのステレオか確認、それ以外も本当はサポートしたい。 if (format.ChannelCount != 2 || format.BitPerSample != 16) { throw new NotSupportedException(); } var length = header.AudioDataSize / (format.ChannelCount * sizeof(short)); var right = new short[length]; var left = new short[length]; var buffer0 = new byte[format.BlockSize / format.ChannelCount]; var buffer1 = new byte[format.BlockSize / format.ChannelCount]; for (var i = 0; i < length; i++) { if (stream.Read(buffer0, 0, buffer0.Length) + stream.Read(buffer1, 0, buffer1.Length) != 0) { right[i] = BitConverter.ToInt16(buffer0, 0); left[i] = BitConverter.ToInt16(buffer1, 0); } else { break; } } } 出力は入力の逆操作をします。出力のほうが楽です。 オーディオを抽象化(ArraySegment like) 今回は読み取ったデータをガンガン分割したいのですが、メモリ上で実際にコピーすると辛いので ArraySegment のようなイメージで元データの区間を参照できるクラスを実装していきましょう。 interface を定義して... オーディオデータを以下のような interface で定義します。 /// <summary> /// 読み取り専用のオーディオデータを表現します。 /// </summary> public interface IReadOnlyWaveAudioSource { IReadOnlyList<short> RightData { get; } IReadOnlyList<short> LeftData { get; } int BitDipth { get; } double Length { get; } int SampleRate { get; } } メモリ上の実体で実装するクラスを書く そして、Waveをちゃんと読み取ったデータでこの interface を実装するクラスを定義します。 普通の実装なので省略します。 部分参照にも interface を継承させる そして、これに対する部分参照を表すクラスを書きます( ArraySegment のイメージです) offset と length を持っておき、さきほどのメモリ実体での実装の一部分を参照できるようにします。 これで、メモリ上で実際にコピーすることなく、あたかもオーディオデータが分割されたかのように見える設計をすることができるようになりました。 パフォーマンスはちょっと悪いです。Span<T> を活用すればよかった。ref struct 制約結構面倒なんですよね。 簡易編集 指定したしきい値よりも信号強度が小さいサンプルを「無音」とみなし、この無音でオーディオを分割できるメソッドを定義します。 無音とみなすしきい値、分割する最低の無音長さの 2 つのパラメータを受け取って分割することとします。 分割したあとに前後の「無音」をトリムする必要があるのでそのメソッドも実装します。 どちらのメソッドも、さきほど実装した「参照」をもつのみで、データをコピーするわけではないのがポイントです。 自動分割は難しい ということで実際に試してみたのですが、人間の意図通りに分割するのは難しいです。喋っている人が詰まったときの微妙な間、音楽のシーンチェンジの微妙な間、こういった区間でも分割されてしまったり、逆に分割してほしいところで分割されなかったりと、結局人間が多分に介入することになりあまり実用的ではありませんでした。 結果的に、勉強にはなりましたがアプリケーション自体はボツになってしまったのです。 そして、1年後 しかし、約1年後(今記事を書いている時空です)、新しいアイディアがありました。 分割した結果どのくらいの長さになるべきかを考慮したらどうだろうか 分割したいソースが動画の場合、そのおおよその分割位置は概要欄やコメント欄から取得できるケースが多いでしょう。 例えば以下のように... #01. 稲妻 - 00:00 #02. 落葉風波 - 05:30 #03. 羈留の客 - 07:26 (以下略) 出典 こういったデータは、単独で分割の指標にするには精度不足ですが、さきほどの音声解析による分割を最適化する指標として用いるには十分です。 分割されるべき長さというメタデータを文字列解析により受け取り、分割の指標にすることで正しく分割できるアルゴリズムを実装します。 アルゴリズム 戦略は以下の通りです。 外部メタデータとして、分割する目標の長さを得る ゆるいパラメータで分割し、過剰分割する 分割しすぎたものを結合していき、目標長さとの誤差を最小化する 競プロで培ったアルゴリズム力(?)を駆使し、このロジックを実装します。すごそうなことを言っていますが頭から順に見てそれなりに最小化するだけです。 結果 依然、分割ターゲットの状態によっては無音検出のしきい値などのパラメータを人力で詰める必要がありますが、少なくとも人間が波形編集ソフトで手直しすることは不要になり十分実用的な品質になりました。結構きれいに分割できるので作者としては気持ちいいです。
- 投稿日:2022-03-23T10:27:15+09:00
C#を使用してWord画像の自動番号付け機能を実現する方法
けっこうサイズの大きいドキュメントを作成すると、多数の画像を使用する必要がある場合、通常、人々は画像に番号を付けます。現時点では、手動で画像に番号を付ける方法を使用すると、時間がかかり、手間がかかり、エラーが発生しやすくなります。実際、Wordのキャプション機能を使用すると、写真に自動的に番号を付けることが簡単にできます。この記事では、Spire.DocコンポーネントとC#プログラミングを使用して、Word画像の自動番号付け機能を実装する方法を紹介します。 次のコードを使用する前に、Visual StudioでC#アプリケーションを作成し、Spire.Doc.dllをプロジェクトに参照する必要があります。 使用する名前空間: using System.Drawing; using Spire.Doc; using Spire.Doc.Documents; using Spire.Doc.Fields; コード一覧 //Documentオブジェクトをインスタンス化する Document document = new Document(); //Sectionを追加する Section s = document.AddSection(); //段落を追加し、その中に画像を追加する Paragraph par1 = s.AddParagraph(); par1.Format.AfterSpacing = 10; DocPicture pic1 = par1.AppendPicture(Image.FromFile(@"C:\Users\Administrator\Desktop\1.jpg")); pic1.Height = 100; pic1.Width = 120; //画像にキャプションを追加する CaptionNumberingFormat format = CaptionNumberingFormat.Number; pic1.AddCaption("画像", format, CaptionPosition.AfterImage); //別の段落を追加して同じことを行う Paragraph par2 = s.AddParagraph(); DocPicture pic2 = par2.AppendPicture(Image.FromFile(@"C:\Users\Administrator\Desktop\2.jpg")); pic2.Height = 100; pic2.Width = 120; pic2.AddCaption("画像", format, CaptionPosition.AfterImage); //ドメインを更新してドキュメントを保存する document.IsUpdateFields = true; document.SaveToFile("AddCaption.docx", FileFormat.Docx); キャプションを追加した効果: 今回のWord画像の自動番号付け機能を実現する方法は以上でした、最後まで読んでいただきありがとうございます。