はじまり
なんだこれ生成AIに聞いても全然わからないよぉぉ!??
初見殺し的なところはあるな。
身に覚えのない型によるエラー
こんな感じのコードを実行した時に、最終行でとあるエラーが発生しました。
$srcBmp = New-Object System.Drawing.Bitmap($srcBmpAbsName);
$width = $srcBmp.Width;
$heightUnit = [Math]::Round($width * $AspectRatioOfLegal, 0, [MidpointRounding]::AwayFromZero);
$dstRect = New-Object System.Drawing.Rectangle (0, (($increment - 1) * $heightUnit), $width, $heightUnit);
エラーメッセージ
"System.Object[]" の値を "System.Object[]" 型から "System.UInt32" 型に変換できません。
はて?
System.Drawing.Rectangle
の引数には数値型の変数しか入れていません。なのに「System.Object[]」?
それともたった今New-Object
で変数を代入して出来上がったものに対してなんか言ってる? 果たしてどの部分が問題なのか全然わからない・・・。あー、PowerShell終わったなーと思いました。
よく分からないので、とりあえずキャストして再び実行してみます。
$dstRect = New-Object System.Drawing.Rectangle ([int]0, [int](($increment - 1) * $heightUnit), [int]$width, [int]$heightUnit);
エラーメッセージ
"System.Object[]" の値を "System.Object[]" 型から "System.UInt32" 型に変換できません。
はぁぁ・・・?
解決法
今回のバグは、「.NET Framework」という外部ライブラリの作法に則っていないことが原因でした。このライブラリのSystem.Drawing.Rectangle
構造体は、引数にSystem.UInt32
の型の数値を渡さなければなりません。
なので、引数を渡す時にこのようにキャストすれば直りました。
$srcRect = New-Object System.Drawing.Rectangle ([System.UInt32]0, [System.UInt32]0, [System.UInt32]$srcBmp.Width, [System.UInt32]$srcBmp.Height);
上記のように、外部モジュールの作法に則ってデータを整形して渡すことを、「マーシャリング」と呼ぶそうです。
マーシャリングは、「整形、整頓」といった意味を持つ単語です。(そういえば以前にGoを触っていた時に、json.Marshal
とかjson.Unmarshal
とかいうメソッドがあったから、JSONを「シリアライズ」するのと似たような感じなのだろうか。)
あー、良かったよぉぉぉ。
ということで、トリミング処理を実装する。
ここを乗り越えればとりあえずは実装できそう。
と思ったんですけど、今回初めてPowerShellで画像のトリミング処理を実装するので、その後の処理もそれなりに調べて実装していきます。(以前にPythonのPillowライブラリで、画像を生成する処理を書いた時はこんなに考えなかったような・・・。)
今回は以下のような、画像のトリミングツールを構成するコードが出来ました。この処理は、縦にとても長~い画像ファイルを任意の長さに切り分けるものになっています。イメージとしては、かまぼことか伊達巻を輪切りにしていくみたいな感じです。
# Load assembly of .NET Framework
Add-Type -AssemblyName System.Drawing;
$srcBmpAbsName = $Args[0];
$srcFile = Get-Item -Path $srcBmpAbsName;
$srcBmp = New-Object System.Drawing.Bitmap($srcBmpAbsName);
$srcRect = New-Object System.Drawing.Rectangle ([System.UInt32]0, [System.UInt32]0, [System.UInt32]$srcBmp.Width, [System.UInt32]$srcBmp.Height);
$srcBmpData = $srcBmp.LockBits($srcRect, [Drawing.Imaging.ImageLockMode]::ReadOnly, $srcBmp.PixelFormat);
try {
$isLast = $false;
$width = $srcBmpData.Width;
$AspectRatioOfLegal = 356 / 216;
$heightUnit = [Math]::Round($width * $AspectRatioOfLegal, 0, [MidpointRounding]::AwayFromZero);
$clippingHeight = $heightUnit;
Write-Host ("{0}: aaaaaaaaaaaaaaaaaaaaaaaaaaaaa`n" -f $MyInvocation.MyCommand.Name);
Write-Output $srcBmpAbsName;
Write-Host ("{0}: bbbbbbbbbbbbbbbbbbbbbbbbbbbbb`n" -f $MyInvocation.MyCommand.Name);
Write-Output $srcBmp;
Write-Host ("{0}: cccccccccccccccccccccccccccccc`n" -f $MyInvocation.MyCommand.Name);
Write-Output (Get-Location).Path;
Write-Output $srcFile.BaseName;
Write-Output $srcFile.Extension;
for ($increment = 0; $increment -lt 100; $increment++) {
if (($increment) * $heightUnit + $heightUnit -gt $srcBmp.Height) {
$clippingHeight = $srcBmp.Height - ($increment) * $heightUnit;
$isLast = $true;
}
Write-Output $srcBmp.Height;
Write-Host ("{0}: iiiiiiiiiiiiiiiiiiiiiiiiiiiii`n" -f $MyInvocation.MyCommand.Name);
Write-Output ((($increment) * $heightUnit).GetType().FullName)
Write-Output ($width.GetType().FullName)
Write-Output ($heightUnit.GetType().FullName)
Write-Output ($heightUnit)
# Set an area to trim and clone an image.
# $dstRect = New-Object System.Drawing.Rectangle ([int]0, [int]($increment) * $heightUnit, [int]$width, [int]$heightUnit);
$dstRect = New-Object System.Drawing.Rectangle ([System.UInt32]0, [System.UInt32]($increment * $heightUnit), [System.UInt32]$width, [System.UInt32]$clippingHeight);
# Trim an image and save
$dstBmpName = "{0}\{1}_{2:D2}{3}" -f (Get-Location).Path, $srcFile.BaseName, [int]($increment + 1), $srcFile.Extension;
$dstBmp.Save($dstBmpName, [System.Drawing.Imaging.ImageFormat]::Png);
Write-Host ("{0}: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm`n" -f $MyInvocation.MyCommand.Name);
Write-Output $dstBmpName;
Write-Host ("{0}: nnnnnnnnnnnnnnnnnnnnnnnnnnnnnn`n" -f $MyInvocation.MyCommand.Name);
Write-Output $dstBmp;
# $dstRect.Dispose();
$dstBmp.Dispose();
if ($isLast -eq $true) {
break;
}
}
}
catch {
Write-Host "Error occured.....";
Write-Host $_ -BackgroundColor DarkRed;
$tmp = Read-Host "Input 'y' to close this process......";
}
finally {
$srcBmp.UnlockBits($srcBmpData);
$srcBmp.Dispose();
}
$tmp = Read-Host "Input 'y' if you wanna move original images......";
BitmapをDisposeする
BitmapのDisposeを忘れるべからず
今回、画像のトリミング処理をするに当たって、.NET FrameworkのSystem.Drawing.Bitmap
クラスを使っていきます。
さて、このクラスを使うに当たって、このクラスで新しいオブジェクトを作って何らかの処理で使った後にそのままスクリプトを終わらせてはいけません。なぜなら、このSystem.Drawing.Bitmap
によって出現したオブジェクト(GDI+)はプロセス終了後にもメモリの中に残ってしまうためです。
# これでBitmapオブジェクトを作ったら、
$srcBmp = New-Object System.Drawing.Bitmap($srcBmpAbsName);
# 破棄しなければならない。
$srcBmp.Dispose();
ちなみにこの「GDI+」は、「グラフィックス デバイス インターフェイス」というヤツらしく、アプリとデバイスドライバーの間を取り次ぐ仕事をしてくれるそうです。しかし、トリミング処理が終わったら君には退散してもらわなければならない・・・。働かざるもの食うべからず。
GDI+ で汎用エラーが発生しました。?
先程のGDI+オブジェクトの破棄を忘れたり、実行中に割り込み終了をしたりすると、次回に同処理を実行した時にこんな感じのエラーメッセージが表示されるようになってしまいます。
“1” 個の引数を指定して “UnlockBits” を呼び出し中に例外が発生しました: “GDI+ で汎用エラーが発生しました。”
このエラーメッセージは、GDIオブジェクトの数がシステム上限の10,000個を超えたときに発生するものらしいです。一度、Dispose()
を忘れるとこれが表示され続けるのは煩わしいですね・・・。
じゃあ、破棄されなかったGDIオブジェクトがどのプロセスで使われているかを確認したくなってきました。
そのために、タスクマネージャーを起動して、「詳細」→ヘッダーをクリック。→「列の選択」→「GDIオブジェクト」を選択してOK。の流れで・・・・・・確認できると思ったんですけど・・・、
と言うのも、タスクマネージャーを見ただけでは、PowerShellで起動したプロセスのどれが当てはまるのかを区別することが出来ませんでした。既にプロセスが存在しないとも言えそう・・・。(じゃあ、裏でGDIオブジェクトが10,000個以上あるってこと・・・? ちょっと分からないなあ。)
そういえば話は変わりますが、以前にPythonで書いた時はimg.save()
みたいな感じで関数を書き終えた覚えがある・・・。その画像もちゃんとBitmap形式だったから破棄する必要が・・・うーんまあいいか・・・。一応ガベージコレクションあるし・・・。まあ、良くはなかったか・・・。
エラーハンドリングで、Disposeを忘れない。
なんだか、Bitmap
クラスのオブジェクトの破棄を忘れると厭わしいことになる事が分かったので、途中で処理に失敗しても間違いなくDispose
するようにしたいと思います。
そこで、try-catch-finally
ステートメントをちゃんと用意して処理を入れ込みます。
# Load assembly of .NET Framework
Add-Type -AssemblyName System.Drawing;
$srcBmpAbsName = $Args[0];
$srcFile = Get-Item -Path $srcBmpAbsName;
$srcBmp = New-Object System.Drawing.Bitmap($srcBmpAbsName);
$srcRect = New-Object System.Drawing.Rectangle ([System.UInt32]0, [System.UInt32]0, [System.UInt32]$srcBmp.Width, [System.UInt32]$srcBmp.Height);
$srcBmpData = $srcBmp.LockBits($srcRect, [Drawing.Imaging.ImageLockMode]::ReadOnly, $srcBmp.PixelFormat);
try {
# トリミング処理
}
catch {
Write-Host "Error occured.....";
Write-Host $_ -BackgroundColor DarkRed;
$tmp = Read-Host "Input 'y' to close this process......";
}
finally {
$srcBmp.UnlockBits($srcBmpData);
$srcBmp.Dispose();
}
色々書きましたが、結局のところは.NET FrameworkのSystem.Drawing.Bitmap
クラスをDispose()
するのを忘れないように気を付けるということです。忘れたらPCを再起動でGDIオブジェクトは消せるでしょう。
RectangleはDispose出来ない
なので、同じく.NET FrameworkのSystem.Drawing
名前空間にいるSystem.Drawing.Rectangle
構造体もDispose()
する必要があるのかと言うと、おそらく必要なさそう。
なぜなら、System.Drawing.Rectangle
構造体を作る時には、GDI+を作らないからです。たぶん。以下のRectangle構造体に関するドキュメントには、「GDI」の文字は存在しなかった。
ちなみにDispose()
しようとすると、こんな感じのエラーメッセージが表示されます。
[System.Drawing.Rectangle] に ‘Dispose’ という名前のメソッドが含まれないため、メソッドの呼び出しに失敗しました。
Bitmap.LockBitsで処理を軽くする。
それから、System.Drawing.Bitmap
クラスのオブジェクトをいちいち参照すると、処理が重くなるようです。こちらが参考記事です。
どうやら、Bitmap
オブジェクトのGetPixel()
メソッドに紐づく処理を実行すると、動作が遅くなってしまうみたいなのです。そこで、このBitmap.LockBits()
メソッドで、Bitmapのピクセルデータをバッファに一時保存して、そのデータから情報を参照することで処理が軽くなるみたいです。
$srcBmp = New-Object System.Drawing.Bitmap($srcBmpAbsName);
$srcRect = New-Object System.Drawing.Rectangle ([System.UInt32]0, [System.UInt32]0, [System.UInt32]$srcBmp.Width, [System.UInt32]$srcBmp.Height);
$srcBmpData = $srcBmp.LockBits($srcRect, [Drawing.Imaging.ImageLockMode]::ReadOnly, $srcBmp.PixelFormat);
try {
$width = $srcBmpData.Width;
$heightUnit = [Math]::Round($width * $AspectRatioOfLegal, 0, [MidpointRounding]::AwayFromZero);
$clippingHeight = $heightUnit;
for ($increment = 0; $increment -lt 100; $increment++) {
if (($increment) * $heightUnit + $heightUnit -gt $srcBmp.Height) {
$clippingHeight = $srcBmp.Height - ($increment) * $heightUnit;
$isLast = $true;
}
$dstRect = New-Object System.Drawing.Rectangle ([System.UInt32]0, [System.UInt32]($increment * $heightUnit), [System.UInt32]$width, [System.UInt32]$clippingHeight);
$dstBmp = $srcBmp.Clone($dstRect, $srcBmpData.PixelFormat);
$dstBmpName = "{0}\{1}_{2:D2}{3}" -f (Get-Location).Path, $srcFile.BaseName, [int]($increment + 1), $srcFile.Extension;
$dstBmp.Save($dstBmpName, [System.Drawing.Imaging.ImageFormat]::Png);
$dstBmp.Dispose();
if ($isLast -eq $true) {
break;
}
}
}
そして、Bitmap.LockBits()
メソッドを使ったプロセスでは、Bitmap.UnlockBits()
メソッドで、メモリ上にロックされていたビットマップのロックを解除する必要があります。Dispose()
と同時期に書いておきます。
finally {
$srcBmp.UnlockBits($srcBmpData);
$srcBmp.Dispose();
}
実際にツールを実行してみますよ。
実際にこのトリミングツールを使用すると、この縦に長~い画像が・・・、
このように輪切りされました。この画像の場合は5枚に分かれました。
まとめ
PowerShellを使って、大きな画像を小分けにトリミングするツールを、実装する方法を紹介しました。
- .NET Frameworkの
System.Drawing.Rectangle
構造体の引数には、System.UInt32の型の数値を渡さなければならない。 - 外部ライブラリ、ランタイム等に対して渡すデータを整形する作業を、「マーシャリング」と呼んだりする。
System.Drawing.Bitmap
クラスには、Dispose()
を忘れるべからず。忘れると、GDI+というオブジェクトがメモリに残る。Bitmap.LockBits()
メソッドで、バッファにデータを一時保存することで、Bitmap
への処理が軽くなる。
PowerShellはWindows上にプリインストールされているので、誰でもこの画像トリミングの処理は実行できます。よければ試してみて下さい。
PowerShell関連記事
その他のPowerShell関連の記事を貼っておきます。
おしまい
よし、これで画像を整理できる!
もっと手早く終わらせるつもりだったのになあ・・・。
以上になります!
コメント