Delphi」カテゴリーアーカイブ

プログラミング言語Delphi関連のコーディングTips

マークシートリーダー

自分的に必要と思った機能は全部搭載しました・・・が、
プロが作った有償販売できるレベルのソフトウェアではありません。
見た目も、使い勝手も、よくないと思います。
もちろん、無料でお使いただけますが、サポートも、動作保証もありません。
ダウンロードから設定まで、ALL自己責任でお願いします。

快適と感じる速度で動作させるには、かなり高性能なCPU搭載のマシンが必要です。
私のプログラミング技術が足りない部分を、CPUパワーでカバーしてもらってます。
マシンによっては、読み取り結果のチェック等がかなりトロいかもですが・・・

それでも、もし、よかったら使ってください。
Delphiで作ったマークシートリーダーです。

【ご案内】追記(20240929)

当Blogで紹介してきた自作のデジタル採点プログラムを一つにまとめました。次のリンク先にその紹介とダウンロードリンクがあります。

当Blogで紹介したデジタル採点プログラムのすべてをまとめました!

【使い方のご案内】

1.デスクトップにMS_Reader.zipを展開(解凍)
2.高解像度ディスプレイへの対応
3.マークシート画像の読み取り準備
4.テンプレートを作成
5.マークの読み取りを実行
6.読み取り結果のチェック
7.CSVファイルへの書き出し
8.Excel Book の準備作業
9.Excel Book への書き出し
10.マークシート印刷用紙について
11.まとめ
12.お願いとお断り

どんな環境でも、100%動作する保証はできません・・・が、
私と同じ環境・条件を揃えていただければ、きっと動くと思います。

使用したPC及びOS、開発環境は、次の通りです。

・プロセッサ 11th Gen Intel(R) Core(TM) i7-1185G7 3.00 GHz
・実装 RAM 32.0 GB

・Windows 11 Pro 64ビット版
・バージョン 23H2
・OS ビルド 22631.2861

・Embarcadero® Delphi 12 バージョン 29.0.50491.5718

・設計時の画面解像度は「1366 × 768」です。これ以上の解像度でお使いください。

使い方をなるべく丁寧に説明しますので(マニュアルも同梱してありますが)、まず、ここに書かれている順番で、一通り操作してみていただけたら幸いです。

1.デスクトップにMS_Reader.zipを展開(解凍)

ダウンロードした MS_Reader.zip をお使いのPCのデスクトップにコピペして右クリックするとサブメニューが表示されます。この中の「すべて展開」をクリックしてください。

デスクトップに MS_Reader.zip を展開します。


無事、展開に成功したら、MS_Readerフォルダをダブルクリックして開きます。

フォルダ内に展開されたファイルの中に MS_Reader.exe があります。これをダブルクリックしてマークシートリーダーを起動します。


次のメッセージが表示された場合は、「詳細情報」(画像中、赤い枠で囲んで示した部分)をクリックします(プログラムの発行元が不明である場合に、Windows のDefender機能である SmartScreen がこの表示を出すそうです。自分の責任で実行すれば、次回からこのメッセージは表示されなくなります)。

「詳細情報」をクリックします。


すると、次の画面が表示されます。「実行」(画像中、赤い枠で囲んで示した部分)をクリックしてMS_Readerを起動してください。

「実行」をクリックします。

2.高解像度ディスプレイへの対応

高解像度ディスプレイをお使いの場合の対応方法です。高解像度ディスプレイをお使いの場合、設定から「システム」⇨「ディスプレイ」と順にクリックすると、次のように表示されると思います。

拡大縮小に「150~200」という値が設定されていれば、高解像度ディスプレイです。


この場合、起動したマークシートリーダーの画面が小さくて見えにくいと感じることがあるかもしれません。その場合は、次のように操作してください。

MS_Reader.exe を右クリックして、表示されるサブメニューのプロパティをクリックします。

MS_Reader.exe のプロパティを表示します。


「互換性」タブをクリックします。


高DPI設定の変更をクリックします。


「高いDPIスケールの動作を上書きします。」にチェックを入れて、「拡大縮小の実行元:」は「システム」をComboBoxの選択肢から選択して指定。OKボタンをクリックします。


続けて「適用」⇨「OK」とクリックして設定は終了です。これで画面が見やすい大きさで表示されるようになります。

3.マークシート画像の読み取り準備

デスクトップに展開した MS_Reader フォルダ内に「ScanData」フォルダがあります。この中に練習用のサンプル画像が2種類(解像度150dpiと200dpi)入っています。この画像を用いて説明します。

重要 マークシートは、解像度150~200dpiでスキャンしてください。

重要 1回の操作で読み取り可能な枚数は最大99枚です。

MS_Reader.exe をダブルクリックしてマークシートリーダーを起動したら、画面左上の「画像変換」をクリックし、表示されるメニューの「専用画像を作成」をクリックします。


画像変換用のWindowが表示されたら、画面右上の「選択」ボタンをクリックします。


「フォルダの選択」ダイアログが表示されます。ここでは「Scanner_A」フォルダを選択します。フォルダ名をクリックして、下のFolder欄に「Scanner_A」と表示されたことを確認し、「OK」をクリックします。

スキャンしたマークシート画像は「ScanData」内に適切な名前を付けたフォルダを作成し、必ずその中に保存してください!

重要 フォルダ名にハイフン(-)を使わないでください。

参考 フォルダ名には、文字の他、アンダースコア(_)が使用できます。

注意してください。選択するのは「フォルダ」で、「ファイル」ではありません。
(Scanner_Aをダブルクリックして開いても何も表示されません)


画面は、次のようになります。赤い枠で囲んだ部分にマークシート画像のサムネイルが表示されます。回転の必要性の有無と回転方向を確認してください。


この場合は、回転の必要性「有り」で、回転方向は左90°です。これを「画像の回転」のオプションボタンをクリックして指定します。

左90°のオプションボタンをクリックします。


必要であれば、次に画像のリサイズ指定を行います。リサイズを指定「する・しない」の判定基準は、スキャナーでマークシート画像をスキャンした際の解像度の数値で判断してください。

「Scanner_A」フォルダ内のマークシート画像は、ScanSnap iX1500 のノーマルモードでスキャンした画像で、その解像度は 150dpi です。この場合は、ちょうどよい大きさでマークシート画像が表示されますので、画像をリサイズする必要はありません。

重要 解像度150dpi ・A4横置きの場合、リサイズは必要ありません!

重要 解像度200dpi ・A4横置き・解答マーク欄4列の場合、80%の大きさにリサイズしてください。読み取り後のチェックまで含めて、作業しやすくなります。

マークシート画像の読み取り解像度が 200dpi でも、マークシートがA4横置き、解答マーク欄の列数が3列の場合、リサイズは必要ありません

また、A4以外の大きさのマークシートは使ったことがありません!
(用紙の左上にマーカー画像■■■を入れ、その他はここでダウンロードできるサンプルと同様に作成していただければ、用紙サイズに関係なく動作すると思いますが、試行したことがありませんので確かなことは言えません。ただ、画像のサイズが大きくなればなるほど、動作速度は間違いなく低下します。また、複合機のスキャナーを用いて、マークシートを画像化する際も、B4やA3の大きさだと私が使用している機材ではメモリがいっぱいになるのでしょうか? 30枚程度読み込んだあたりで一旦動作が停止します。数百枚単位での読み取りにはそれなりに時間がかかります。そのような理由から、マークシートに使う紙の大きさはA4サイズ以下が適切だと思います。)

参考:プログラムを書いた本人が言うのもナンですが、自動でのリサイズはおまけ程度にお考えください。
ScanDataフォルダのScanner_Bフォルダに保存されたサンプル画像の大きさは、2338 × 1653
これを自動リサイズオプションボタンを指定して、変換してみます。
ProcDataフォルダのScanner_Bフォルダに保存されたサンプル画像の大きさは、1760 × 1248
いちおう、これでマークシート画像が横方向のはみ出し「なし」で表示されました。

重要 画像のリサイズの有無を必ずメモ(記録)してください!

⇨ 複数クラスのマークシート読み取り時に、同じ設定を適用する必要があります。

重要 大きな解像度の画像を扱う場合、動作速度が大幅に低下します!

回転の有無と方向、リサイズの有無を指定したら、画面中央右にある「参照」ボタンをクリックして、保存先のフォルダを選択します。


「フォルダの選択」ダイアログが開きます。Pathを見ると「ProcData」フォルダが指定されていることがわかると思います。なお、Procは「Processed(加工済み)」という意味です。

プログラムは「ScanData」フォルダで指定したフォルダと同名のフォルダを「ProcData」フォルダに自動作成します。この自動作成されたフォルダをクリックして選択します(しつこいようですが、選択するのは「フォルダ」で、「ファイル」ではありません)。下のFolder欄に「Scanner_A」と表示されたことを確認し、「OK」をクリックします。

読み取り用のマークシート画像は、必ず「ProcData」内の自動作成されたフォルダに保存してください!

重要 ProcData以外のフォルダには画像を保存しないでください。

読み取り用画像を保存するフォルダは自動で作成されます!
(自動作成されたフォルダをクリックして選択してください)


「変換実行」をクリックします。

回転とリサイズの有無を指定して「変換実行」をクリック!


次に表示される案内メッセージには「いいえ」を選択してください。


このマークシートリーダーとは別に、手書き答案の採点プログラムを作成しました(準備が整い次第、公開する予定です)。このマークシートリーダーは、そちらと連動しての動作も可能な設計にしてあるため、このメッセージが表示されます。

画像の変換が完了すると、メッセージが表示されますので、OKをクリックします。


変換された読み取り専用画像のサムネイルが表示されます。作成された読み取り用の画像ファイルには連番の名前が自動的に付きます(自動生成されたファイル名は変更しないでください)。

重要 Python環境を利用する場合はファイル名は必ず連番にしてください。

画像処理のアルゴリズムは、GDI+を利用しています。画像の回転とリサイズが伴う場合は、変換に時間がかかります。処理が完了するまでお待ちください。

(後日、別途ご案内する予定の)手書き答案の採点プログラムと併用する場合は、採点やり直しのために必要な画像もここで作成します(Loopが二重にまわり、時間も2倍かかります)。

クラス別に処理する場合は、「画面の初期化」ボタンをクリックします。
変換元フォルダの選択から、画像の変換処理を再実行できます。

画像の変換処理が完了したら、「終了」ボタンをクリックして、この画面を閉じます。

参考:画像を変換する理由は以下の3つです!
(1)Jpeg画像のサイズを最適化するため(全体が画面内に収まるようリサイズしてください)。
(2)画像の名前が連番になるよう、自動的にリネームするため。
(3)証拠画像としてのオリジナルを残したまま、読み取りに最適な大きさの画像を生成するため。

4.テンプレートを作成

次に、マークシートの情報を記録した読み取り用のテンプレートを作成します。これを作成することにより、同じ採点を複数クラスに対して実行したり、設定(縮小処理の有無を含む)が同じマークシートを異なる考査での使いまわしが可能となる・・・

・・・ように設計したのですが、実際には使いまわしがなんとなく不安なので、考査毎にテンプレートを再生成して運用しています。ですので、同じ設定(大きさ)のマークシート画像の情報を記録したテンプレートの使いまわしが可能か・どうか、これについては未確認です。

「確実なマークシート読み取りを実行する」ためには、お手数をおかけしますが、試験ごとに使用したマークシートのテンプレートを作成していただくのが最良の方法であると思います。

メニューの「2 テンプレート」をクリックして表示されるサブメニューの「テンプレートの新規登録」をクリックしてください。別のWindowが開きます。


画面右上の「取得」ボタンをクリックします。


今度は「ファイルを選択」するダイアログが表示されます。任意のマークシート画像を選んでください(1番のファイルを選ぶ方が多いのではないでしょうか?)。ファイルをクリックしてファイル名を取得し、「開く」をクリックします。

マークシート画像のサムネイルをクリックするとファイル名が取得できます。


画面は次のようになります。

このプログラムでは、マーカー(特徴点)画像を利用してマークシートのマーク位置を計算しています。ですので、このプログラムで処理するマークシートには必ずマーカー(特徴点)画像が必要です。

重要 マークシート左上にマーカー画像(■■■)を必ず用意します。

重要 マーカー画像は、マークシート1枚に1つだけ用意します。

画面右の操作パネル上段にある「マーカー」オプションボタンをクリックして選択状態にします。

「選択対象」の「マーカー」オプションボタンをクリックしてください。


マークシートの画像が拡大表示され、マウスのカーソルが大きな「+」になります。

マーカー画像の「左上」をクリックし、ボタンを押したまま「右下」へドラッグしてください。画像上には点線のラバーバンドが表示されます。

マーカー画像の左上を左クリックして、マウスの左ボタンを押したまま、マーカー画像の右下へドラッグ。
点線のラバーバンドでマーカー画像が囲まれます。


ドラッグ中の画像です(わかりやすさのため、マーカー画像より大きめにドラッグしています)。

黒点線がラバーバンドを示します。


マーカー画像の座標を正しく取得できる例です。

マーカー画像とラバーバンドがぴったり重なるようドラッグしてください。


マウスの左ボタンから指を離すと、取得できたマーカー画像が画面右側に表示されます。

数値は、画像左上からの距離です。

マークの読み取り時、プログラムは、コンピュータの眼である「OpenCV」のテンプレートマッチングの機能を利用して、まず、最初にマークシート画像中にあるこのマーカー(特徴点)画像を探し出します(これはマークシート画像1枚1枚について必ず行います)。

次に、マーカー(特徴点)画像左上隅を原点(0,0)として、テンプレートに記録されたマーク欄の座標からマーク一つ一つの位置を割り出して、これを切り抜いて画像化(正確に言うと、マークの切り抜き処理前に、ボカシ・二値化・白黒反転の各処理を行い、マークの切り抜き後に白面積計算処理を行って)、マークの有無を判定しています。

この方式の利点は、印刷そのものが左右にズレでも、マーカー画像と解答欄の相対的な位置関係は一定で変わりませんから、印刷がズレすぎて解答欄が印刷されなかった場合以外は、必ずマークの位置を探し出せる(=マークの有無を判定できる)ことです。

事実、輪転機で印刷(非推奨ですが!)して、チェックから漏れた(チェックしなかった?)、正しい位置から印刷が5cmくらいズレたマークシートも、このプログラムでなんの問題もなく読み取れました・・・。印刷のズレを申告せず、そのまま解答して提出する受験者も受験者ですが・・・。A4横・4列のシートで、解答には3列めまでしか使わなかったから「4列めはなくてもOK! 大丈夫」と思ったのでしょうか? それともただ単にめんどくさかったのでしょうか? たぶん、後者だと思いますが・・・

次は、そのテンプレートマッチングの機能をテストします。画面右にある「マーカー画像の読み取りテスト」ボタンをクリックしてください。テンプレートマッチングが正しく実行されると、マーカー(特徴点)画像が太い赤枠で囲まれます。


表示されるメッセージをお読みいただき、「OK」をクリックしてメッセージを閉じてください。


結果が良好であれば「選択対象」グループの「解答欄」をクリックします。


次に、マークシートのマーク(解答)欄の「行数」と「列数」及び「選択肢の数」を指定します。


マークシートの列数・行数・選択肢数の数え方は次の通りです(Scanner_Aフォルダにあるマークシート画像は、A4横置き・3列・25行・8選択肢の形式です)。


ですので、これを次のように設定します。


ComboBox に正しく設定を入力したら、その下の「採点方法の設定」の座標「1列」のオプションボタンをクリックして選択状態にします。マウスのカーソルが大きな「+」になります。


第1列目のマーク(解答)欄の座標を取得します。マーカー(特徴点)画像の時と同様、第1列の枠のうち、設問番号欄の矩形を除いた選択肢のマークが印刷されている欄の矩形の左上隅を(左)クリックして、そのままボタンを離さずに、枠の右下隅へドラッグします。この作業は正確に、慎重に行ってください。この作業の良し悪しでマークの読み取りの可否が決まります。

極めて重要 設問番号欄を含めて指定してはいけません!

極めて重要 指定するのはマーク欄のみ!

プログラムは、ここで取得した座標値(矩形の高さ)を行数で割り算して列を設問毎1行ずつに切り出し、さらに切り出した1行を選択肢数で割って1つ1つのマークを切り出し、その塗りつぶし面積を計算して、マークの有無を判定しています。

マーク(解答)欄の枠線と、表示されるラバーバンドがぴったり重なるようにドラッグしてください。

※ 下図は2つともドラッグ直後の結果を示しています(〇はドラッグ開始点と終了点です)。

マーク欄第1列めの左上隅を(左)クリックしてそのまま指を離さずに右下隅へドラッグ


ドラッグ中は、黒点線のラバーバンドが表示されます。これを目安に位置決めを行ってください、

画像中の〇印の位置までドラッグします。


指を離すと、ドラッグした範囲が赤い矩形で囲まれます。画面右側に取得できた座標が表示されます。


「再範囲選択」ボタンをクリックして、座標の取得をやり直すこともできます。


1列目が済んだら、同様にして2列目の座標を取得します。この作業を「マークシートの列数」分だけ繰り返します。

すべての列の座標を取得できたら、「保存」ボタンをクリックして取得した座標を保存します。


「保存」処理が完了すると、次のメッセージが表示されます。

参考:テンプレートの名前について
例 N_R25C03S08
N:ノーマル(通常の大きさ:解像度150~200dpi)
R:Row(行数)は25行
C:Col(列数)は3列
S:Selection(選択)は8個


「二値化テストの実行」ボタンをクリックすると、第1列めを「平滑化(ぼかし)処理&白黒反転して二値化」した画像の状態が確認できます。「マークあり」の部分が白く表示されていればOKです!
(プログラムは、この白部分の面積を計算して、マークの有無を判定しています)

「終了」ボタンをクリックして画面を閉じ、マーク(解答)欄座標の取得作業を終了します。

二値化テストを実行した場合は、終了ボタンをクリックする前に、保存ボタンをクリックすることを忘れないでください!

二値化の閾値と平滑化(ぼかし処理)のパラメータは、まずデフォルト設定でお試しください。

5.マークの読み取りを実行

これでマークを読む準備ができました。メニューの「テンプレート」をクリックし、表示されるサブメニューの「テンプレートの選択」をクリックします。


次のように、テンプレートを選択するWindowが表示されます。マークシートの形式に合ったテンプレートをクリックして選択し、決定をクリックします。

シングル/ダブルとあるのは、数学や教科「情報」のテストで、マークシート2枚1セットの採点を行うための設定です。選択肢数が16のマークシートを選ぶと、この設定も選択できるようになります(選択肢数が16未満のマークシートでは、この設定は利用できません)。

数学及び教科「情報」用の設定は、後日別記事として掲載する予定です。


次のメッセージが表示されます。「はい」をクリックしてください。


マークの読み取りを実行したいマークシート画像のあるフォルダを選択し、「Ok」ボタンをクリックしてください。


保存してあるマーカー(特徴点)画像をもとに、自動的にテンプレートマッチングが行われ、見つかったマーカー(特徴点)画像から、マークシートのマーク(解答)欄第1列第1行目の座標が計算され、それぞれが赤い矩形で囲まれて表示されることを確認してください。

Python環境を利用する場合(ここでワンクッション置くような感じで)テンプレートマッチングにしばらく時間がかかることがあります。同じプログラムを走らせているのですが、PCにより、このフリーズしたような時間の長さが極端に違うようです・・・、その辺の理由が私にはさっぱりわかりませんが・・・。

Python環境利用時に、この画面が表示されるまで、フリーズしたようになることがあります!


ここまでの設定操作が順調に進行していれば(抜け・落ち・欠けがなければ)、間違いなくテンプレートマッチングが成功し、マーカーと1列1行目が赤い矩形で囲まれるはずです。次のメッセージが表示されますので、お読みになったら「OK」ボタンをクリックしてください。


「読む」ボタンをクリックすると、マークシートの読み取りがスタートします。


画面下部の StringGrid に読み取り結果がリアルタイムで表示されます。また、読み取り完了後、処理にかかった時間が画面左下に表示されます。


8選択肢・25行・3列だから、合計600マーク ×3枚=1800マークの読み取りで、早ければ2013ミリ秒、遅くて2467ミリ秒で読んでます(PCの性能により、この値は変わります)。


遅かった方で1マークあたりの読み取り時間を計算すると、

2.467秒 ÷3≒ 0.82秒/枚
0.82 ÷ 600 ≒ 1.4ミリ秒/1マーク

そう書くと、すごく早いような気がしますが・・・

600マーク3枚で2.5秒だから、30枚ならその10倍で25秒かかります。平均的な高校の1学年分の生徒数を1学年8クラス320名とすると、さらに10倍で280秒程度、約5分処理時間が必要です。

300名分、5分だと慣れてくるとちょっと遅く感じてしまうかな? みたいな気が・・・

このプログラムには、内部的にPython環境を組み込んで高速動作させるモードがあります。数学用途の16選択肢・25行・3列で1200マーク/枚のマークシートで処理速度を計算・比較してみます。
(組み込みPythonの利用方法は後日ご案内します)

まず、Python環境を利用しない場合、1200マーク×40枚=1クラス分の48000マークを読むのにかかった時間が・・・


約78秒です。2枚1セットのダブルモードならその倍になります。
1枚(1200マーク)読むのに1.95秒かかってます。

次に、Python環境を利用した場合です。同じ読み取り条件で実験すると・・・


約11.5秒。8クラスあっても2分かかりません。ダブルモードでも4分未満。
1枚0.3秒未満で読み取ってます。

何やってもダメな自分にしては、よく頑張ったって正直、思います・・・。
よほど、びみょーなマークでない限り、期待した通り、ほぼ正しく読み取ってるし・・・。
かあさん、オレ、がんばったよ☆☆☆

まぁ このプログラム作成そのものに50万枚くらい採点できる時間をかけてますから・・・

それと合算すれば、
たぶん、プラマイ0ですー!!

6.読み取り結果のチェック

マークシートリーダーで最も重要な部分は、マーク読み取りの正確さであることは言うまでもありませんが、読み取り結果のチェック機能も非常に重要であると考えます。

人によってマークの濃さや大きさは少しずつ異なり、また、マークを訂正した箇所に残る消し跡も判定に少なからぬ影響を及ぼします。常に100%正しい読み取り結果が保証されないのが現実ですから、如何に効率よく、読み取り結果をチェックできるかで、プログラムの使用感はずいぶん変わってくると思います(CPUパワーにかなり依存したプログラムを書いておいて、そう言うのもナンですが・・・)。

自分自身の書いたものがベストだなんて、到底、思えませんが、このプログラムを書くにあたり、マークの読み取り部分と同等か、それ以上に頑張って書いたのが、この読み取り結果のチェック部分です。

機械との協働。機械との融和。これをテーマに、ヒトと機械とが一体化しての「快適なチェック作業」の実現を目指しました。

・・・が、プログラミング技術の未熟。自分自身の勉強不足。見い出した妥協点。等々の理由により、視覚による機械と協働してのチェックも、聴覚(音声出力)による機械と協働してのチェックも、いずれも全面的にマシンのCPUパワーに依存した、もっさりした感のある処理となってしまいました・・・。

処理性能の高いマシンなら、それなりに快適に作業できると思うのですが。以下、チェック機能の使い方です。

マーク読み取り結果のチェックを実行。


上の図の左のGUIから説明します。

白紙にチェックすると、マークがひとつもないシートのチェックは行わない(飛ばす)設定で動作します。この機能はデフォルトでON(チェックあり)です。

マーク(解答)がなかった場合の読み取り結果の表示が「999」です(デフォルトOFFです。このプログラムでは、「空欄」のフラグを「999」としています。マークの番号にも、得点にも「999」は通常ないことがその理由です。ちなみに複数マークは「99」と表示しています。色は「999」が「青」、「99」が「赤」です。少しでも視覚に訴えた方がチェックしやすいと考えました)。

ごく薄い色でマークされた答案が混じっていないことが大前提ですが、答案全体(1クラス分!)のマークの濃さが十分「濃い」と保証されていれば、チェック開始時のみ「999」のチェックを外してチェック(機械がきちんと空欄を識別していることをヒトが目視して確認)、で、確実に空欄を見分けていることが確認できたら、「999」にチェックして続行。こうすれば大変スムーズな確認作業を実現することができます。あくまでもごく薄くマークされたシートがないことが大前提ですが・・・

いずれにしても「Check!」ボタンをクリックすると、プログラムは次の「空欄(999)」もしくは「複数マーク(99)」を探し、それが見つかった場合は該当箇所を赤い矩形で囲んで表示します。処理性能の高い(CPUパワーのある)マシンであれば、それなりに快適に動作しますが、そうでない場合は、かなり「もっさり」した動作になりますので、イライラするかも知れません。ごめんなさい。

【空欄と判定した場合】

マークがない場合の表示例(設問番号25が空欄であった場合)

【複数マークありと判定した場合】

複数マークと判定した場合の表示例(設問番号43が複数マークであると判定)


複数マークの判定はパラメータ設定を厳し目にしてあります(上の図はそれがわかるよう、大きめに表示しました)。ごく小さなシミは「平滑化(ぼかし)」処理である程度消えますが、ある程度の面積があるシミや汚れは上のように複数マークと判定されます。

いずれの場合も、ヒトの眼で確認して、訂正の必要がなければ「Check!」ボタンをクリックしてチェックを続行。読み取り結果の訂正が必要な場合は、正しい値を直接入力します(上の場合であれば「2」と入力してください)。


【処理をスキップして次のシートへ】

「Skip」ボタンをクリックすると、現在チェックしているシートの残りの部分のチェックを省略し、次のシートのチェックへ移動します。チェック対象シートの残りの行が全部空欄であった場合などに利用してください。


【チェックの再実行】

「ReDo」をチェックすると、初めからチェックを再実行できます。

ReDoにチェックすると表示されるメッセージ①
ReDoにチェックすると表示されるメッセージ②


【音声読み上げ】

読み取り結果が表示されているStringGridの任意の行をクリックして、「▶」ボタンを押すとWindowsに標準搭載されている日本語の音声合成エンジン(Microsoft Haruka Desktop)の音声で読み取り結果をアナウンスしてくれます。

マークの読み取りが正しく行われているか・どうか、少しでもラクに確認できないかと考え、この機能を搭載しました。処理性能の高いマシンでないと快適な動作は期待できませんが、CPUパワーのあるマシンであればそれなりに使えると思います。

「▶」ボタンの下にある「×」ボタンをクリックすると、音声読み上げを途中で中止することができます。


【列を指定して、任意の行からその列の最後の行までのチェックをスキップ】

数学用のマークシート等で、第1問の解答をシート第1列にマーク、第2問の解答をシート第2列にマーク、第3問の解答を・・・というような設定にしたい場合、「指定列の任意の行から最後の行までをチェックの対象から外す」ことができます。以下、その方法です。

任意の行を指定して、その行以降のチェックをスキップできます。


図のいちばん左にある「Skip」にチェックすると、この機能が有効になり、続けて「Check!」ボタンをクリックすると、ここでの設定に基づいたチェックを実行できます。

上の例であれば、1列目25設問あるうちの20設問目以降25設問目までのチェックをスキップ(チェックは19設問まで実行)、2列目は設問番号26から始まるので34設問目以降50設問目までを、3列目は設問番号51から始まるので70設問目以降75設問目までのチェックをそれぞれスキップします。スキップの設定はComboBoxへ入力した指定値「以降」であることにご注意ください。

また、シートの型式により、列の指定の可否をプログラムが自動的に判断し、ComboBoxのEnabled プロパティが設定されます(上の例では4列目は指定不可)。

「覚」ボタンをクリックすると、現在の設定を ini ファイルに書き込んで記憶します。「消」ボタンをクリックすると「設定なし」の状態に初期化できます。

数学用途等で2枚1セットの処理を実行する場合は、1枚目と2枚目を分けてスキップ処理の設定を行うことができます(数学用途の処理方法は後日掲載します)。

7.CSVファイルへの書き出し

マークの有無の読み取り結果は、CSVファイルとExcel Book への書き出しが可能です。

【CSVファイルへの書き出し】


「ファイルへの出力」にある「CSV」をクリックして選択し、「書き出し」ボタンをクリックしてください。


上記の場所にCSV形式で、読み取り結果が出力されます。

フィールド名として1行目に「設問番号」、レコード名としてA列に「マークシート番号」が書き込まれます。

8.Excel Book の準備作業

【Excel Bookへの書き出し準備】

Excel Book への読み取り結果の書き出しは、自分用に(あれば便利かなー☆)と思って作成したものです。ですので、式の入ったセルを保護する等、第三者が使うことへの配慮は何一つ行っていません。セルに入力された式やVBAの内容をご自身でメンテナンスできる方なら、お使いいだけるかな? という程度のシロモノです。

添付した Excel Book はこれまでに何度も「実際に使用して動作に誤りがないことを確認済み」ですが、誤って式を削除したりした場合は(当然ですが)意図した通りに動作しません。ですので、こちらも動作保証は一切ありません。ご使用はあくまでも自己責任でお願いします。この Excel Book に対しても、このプログラムの使用要件にあります免責事項がそのまま適用されますことを申し添えます。

以下、試験実施前に行っておくとよい採点準備作業です。

eFile フォルダに「一般用マークと手書き併用採点シート.xltm」というマクロ有効テンプレートがあります。これをダブルクリックすると「一般用マークと手書き併用採点シート1.xlsx」という名前で新しい Excel Book が作られます。拡張子に注意してください。「.xlsx」です。このままでは期待通りに動作しませんので、適切な名前を付け、拡張子を「.xlsm」(マクロが有効な Excel Book )に変更して eFile フォルダ(必ずこのフォルダに保存してください!)に保存します。

ここでは test.xlsm という名前で保存したことにして説明を続けます。

「コンテンツの有効化」をクリックしてマクロが実行できるようにしてください。


【インターネットからダウンロードしたマクロ有効 Excel Book の取り扱い】

いつからこうなったのか、わかりませんが、インターネットからダウンロードした拡張子 xlsm の Excel Book をダブルクリックして開くと、次のメッセージが表示されるようになりました。

「編集を有効にする」をクリックすると・・・
マクロを動かすことができません!


こうなった時は、いったん Book を閉じて、その Excel ファイルを右クリックして表示されるサブメニューのプロパティをクリックして、全般タブのいちばん下にある「セキュリティ:」の「許可する」にチェックします(チェックする=マクロの実行をご自身の責任で行うことになります。どうか、ご注意ください)。

全般タブの下の方にあるセキュリティの設定。
マクロの実行をご自身の責任で行う場合は、「許可する」にチェックしてください。


「許可する」にチェックした状態で、「適用」をクリックすると「セキュリティ」の表示そのものが消えます。「あなたの責任でマクロの実行が可能になりました」ということなのでしょう。「OK」をクリックしてプロパティの設定画面を閉じます。


これでマクロが実行できるようになります。


【欠席者がいた場合】

Excel Book を利用して採点する場合、大変重要な注意事項があります。それは欠席者がいた場合の処理です。該当試験に欠席者がいる場合は、その欠席者の出席番号位置に未使用のマークシートを挿入し、シートが確実に出席番号順に並んでいることを確認してから、スキャナーでスキャンしてください。
※ 可能であれば、この用途専用に未使用のマークシートを複数枚、最初から手元に準備しておくとよいと思います。

重要 未使用のマークシートを欠席者の出席番号位置に挿入しておく!

これを忘れると、あとから「すーぱーめんどくさい」コトになります(もし、忘れたらマークシートのスキャンからもう一度、採点をやり直した方が効率がいいかもしれません)。


【受験者の氏名データを準備する】

test.xlsm をダブルクリックして開き、「コンテンツの有効化」を行ったら、いちばん最初に「名票への貼付元名票」シートをクリックして開き、ここに「採点対象者全員分の氏名」を準備してください。

もっとわかりやすく言うと、採点したいテストを受験した生徒全員の「クラス・出席番号・氏名・ふりがな・性別」データを「クラスごと」に「出席番号順」で、「名票への貼付元名票」シートに用意します。なんで「ふりがな」まで必要なのか? 疑問に思う方もいらっしゃるかもしれませんが、最近の若い方々のお名前は難読である場合が多く、採点結果を個票でお知らせする際に、個票の氏名欄のところに「ふりがな」も印刷しておくとスムーズに答案返却が行えます。そのための「ふりがな」準備です。

また、テストの受験者全員分の氏名データを1シートに準備する理由は、次のような使い方を想定しているからです。

(1)同じテストを受験 ⇨ クラス毎に採点用 Excel Book を用意するのは非効率的。
(2)採点用 Excel Book は1個だけ作成し、これをコピーして全クラス分を作る。

具体的には、eFile フォルダの Excel Book(test.xlsm)をコピーして、クラス別(AHR.xlsm)に名前を変えて MS_Reader.exe がある場所に保存。採点結果もコピーした Excel Book(AHR.xlsm) に書き込みます。さらに、この作業はすべてプログラムから自動実行します。

採点者は、採点結果が書き込まれた Excel Book(AHR.xlsm)を開いて、「名票への貼付元名票」シートに用意した氏名データから「A組の受験者の氏名データ(クラス・出席番号・氏名・ふりがな・性別)を範囲選択してコピーし、「名票」シートに値のみ貼り付けます。

こうすることで同じ内容のファイルを複数個準備することなく、言わば「採点原本」として利用する Excel Book を1つ作成するだけで、試験を実施した全クラス分の採点が可能となります。

ここでは「クラス」と表現しましたが、用意する氏名データを適宜変更すれば「講座」等の採点もまったく同じように行えます。※ プログラムの仕様としては、1回の採点作業で採点する人数を100名以下と想定していますが、実際の採点作業は1採点40名程度で行っています。ですので、40名程度を1つのまとまりとして採点していただく方向でお考えください。


【正解を入力】

氏名データの準備が完了したら、「正解」シートをクリックして表示し、設問毎に「正解」の選択肢の番号を入力します。設問がない場合(無解答でよい設問番号の欄)は空欄のままにしておきます。入力したら、入力内容に間違いがないか、よく確認し、上書き保存してください。

正解の入力を間違えるとたいへんなコトになります!
慎重に入力し、最低2回は間違いがないことを確認してください。


【配点を入力】

次に、「マークシート配点」シートをクリックして「配点」を入力します。入力と同時に合計が自動的に計算されます。入力が完了したら上書き保存してください。なお、この配点表の下には観点別評価の表もありますが、この表には一切入力しないでください(観点別評価の表は入力禁止です)。

配点を入力すると「合計」が自動計算されます。
確認作業にお役立てください。


【観点別評価の区分を入力】

次に、「マーク&手書き観点別評価」シートをクリックして「観点別評価の区分」を入力します。
「知識・技能 ⇨ 1」、「思考・判断・表現 ⇨ 2」として設問毎に、半角数字で入力してください。デフォルト設定では、すべての設問に「1」が入っています。解答を要しない設問は「空欄」にしてください。入力したら上書き保存します。


以上で、試験実施前の準備は終了です!

9.Excel Book への書き出し

重要 すべての Excel Book を閉じてから実行してください!

危険 Excelが起動した状態で実行すると重大なエラーが発生します!

Excel へデータを書き込む際は、上記注意事項を必ずお守りください。この注意を忘れて Excel が起動したまま、Excel Book への書き込みを実行すると最悪の場合、Excel のプロセスが幽霊のように残り、これを終了することが出来なくなって、復旧するには、システムの再起動しかない状態になります。未保存の重要なデータがあるような場合、当然そのデータは失われます。Excel Book へのデータ書き込み時は、Excel が起動していないことを(タスクバーに眠っている Excel Book がないことも含めて)十分確認した上で、書き込み作業を行ってください。


【書き出し処理】

マークシートを読み取り後、読み取り結果のチェックまで完了したら、Excel Book への読み取り結果の書き出しが可能となります。次のようにマークシートリーダーを操作してください。

最初に、ファイルへ出力の Excel のオプションボタンをクリックして選択します。すると、その右側にある「選択」ボタンがクリックできるようになりますから、このボタンをクリックしてください。


ファイル選択のダイアログが表示されますので、読み取り結果を書き込む Excel Book をクリックして選択し、その後、下にある「開く」ボタンをクリックします。Pathの指定は、デフォルトで eFile フォルダになっています。準備作業で作成した test.xlsm を eFile フォルダに保存したのは、この読み取り結果を書き込む Excel Book を選択する作業を円滑に実行するためです。


次のメッセージが表示されます。

重要 ここで Excel が起動していないことを必ず確認してください!

選択した Excel Book が書き込み先として表示されていることを確認し、「書き出し」ボタンをクリックします。


書き込みには、しばらく時間がかかります。次のメッセージが表示されるまでお待ちください。


すぐに書き込み結果を確認する場合は、「はい」をクリックします(ここでは「はい」をクリックしたものとして説明を続けます)。

「はい」をクリックした場合は、エクスプローラーが自動的に開きます。先ほど選択した「test.xlsm」のコピーが「Scanner_A.xlsm」として、eFile フォルダではなく、MS_Reader.exe のあるフォルダに生成されています。


ファイル名がなぜ「Scanner_A.xlsm」になったかというと、マークシートの読み取り元フォルダとして選択したのが、ProcData\Scanner_A であったためです。プログラムは、マークシートの読み取り元フォルダの名称をそのまま、原本「test.xlsm」をコピーして生成する読み取り結果書き込み先 Excel Book の名称として利用します。

マークシートの読み取り元フォルダの名称が、Excel Book の名称になります!


マークシートの読み取り元フォルダの名称が「R05_情報Ⅰ_1A」であれば、MS_Reader.exe のあるフォルダに「R05_情報Ⅰ_1A.xlsm」が生成されます。

ここは、この仕様に慣れるまで混乱が生じやすいところと思われます。しかし、この仕様(仕組み)を十分に理解して、マークシートリーダーを使いこなしている職場の同僚からは「よく考えられた採点システムだと思います」と言ってもらえました。うれしかったなー!!


【成績一覧表の印刷】

生成された Excel Book をダブルクリックして起動します。起動したら「名票への貼付元名票」タブをクリックして開き、採点対象クラス(等)の氏名データを範囲選択してコピーし、「名票」タブをクリックして B3 セルに値のみ貼り付けます。次に「採点」タブをクリックしてください。次のような画面が表示されます。「氏名がある場合のみチェックする」ボタンをクリックしてください。画面上方に表示されている平均点が正しく再計算されます。なお、欠席者の得点は「0」と表示されていますので、この場合は手動で「受験確認」のチェックを外し、平均点の計算対象から除外してください。

「氏名がある場合のみチェックする」をクリックしてください。


このシートは通常の印刷操作で印刷できます。ただし、デフォルト設定で100名分を2枚に分けて印刷する仕様となっているため、成績一覧表が1枚でよい場合は、次のように指定して1ページ分のみ印刷を実行してください。

成績一覧表を1枚だけ印刷したい場合の設定


【観点別評価を行う場合】

観点別評価を行う場合は、「正答率」タブをクリックして、上と同様の操作を行ってください。欠席者がいた場合の処理も同じです(このシートは印刷しません)。


【個票の印刷】

最後に、試験の採点結果を受験者に知らせる成績個票を印刷します。よー書いた。さすがに私も疲れました。あと、もぉちょっとです!

「個人表」タブをクリックします。次のような画面が表示されます(表示倍率は異なります)。まず、考査名と科目名を入力してください(忘れやすい部分です! ご注意願います)。印刷はVBAでマクロを組んであります。設問数に合った「印刷(QXX)」ボタンをクリックしてください。

重要 セルを保護していません。誤って式を消さないでください!


次の印刷フォームが表示されます。開始番号と終了番号を入力し、「印刷実行」をクリックします。

重要 印刷は途中で中止できません!

VBAではプログラム書いてない!のに、Engterキー押し下げでフォーカスが移動します・・・

この印刷は Excel の仕様上、印刷データをためてからイッキにプリンタへ送信という方法が取れません。1枚ずつ送信しますので、ちょっとギクシャクした感じで印刷が実行されます。プリンタが壊れているわけではありません。


【個票を個別に確認したい場合は?】

受験者個々の個票を確認したい場合は、A2 セルに「採点」シートの通番を入力します。いろいろなクラスの生徒が混在した講座の処理に対応するため、入力値は「出席番号とは異なる」ことにご注意願います。

採点シートの通番を入力します!


個票を確認したい受験者の通番は「採点」シートを表示して確認してください。

受験者の通番を確認します。


【壊しちゃったときは?】

個人表シートを壊してしまった時は、次のようにすれば直せます。「個人票_Back」タブをクリックします(このシートは絶対に非表示にしないでください)。A 列の左、1行めの上(図の〇印を付けた部分)を右クリックしてシート全体を選択し、表示されるサブメニューのコピーをクリックします。

A 列の左・1行めの上を右クリック!
シート全部をコピーします。


個人表シートに戻って、先ほどと同じ A 列の左・1行目の上を右クリックして表示されるサブメニューの「数式fx」をクリックします(罫線データ等を壊してしまった場合は、すべてを貼り付けます)。

数式が壊れた場合は数式を貼り付けます。
面倒な場合は、いちばん左の全部「貼り付け」でもOK!

10.マークシート印刷用紙について

紙の「白さ」の度合いを「白色度」というそうです。このマークシートリーダーで読み取りに使用したマークシートはすべて「再生紙 or 再生コピー用紙」と呼ばれる紙に印刷したものです。

ですから、ここで紹介したマークシートの読み取り結果は、すべて「白色度70%」前後の「再生紙」に印刷してのもので、ホームセンターで一般的に販売されているような「白色度」が「再生紙」よりはるかに高い「真っ白に見える」用紙を用いての読み取り結果ではないことに、十分ご注意願います。

マークシートの印刷に使用する紙の「白色度」によっては、読み取りパラメータ設定の見直しが必要になるかもしれません(私自身は、実験・試行していませんので正確なことはわかりませんが)。入手可能なすべての紙について、実験することは現実的に無理でありますので、マークシートを印刷する用紙については、本ソフトウェア使用者の責任で十分な試行を行い、確実に動作するパラメータ設定を行った上で、このプログラムをお使いいただけますよう、お願いいたします。

印刷はインクジェットプリンタで行うことを推奨しましたが、長期にわたって使用していない(メンテナンスもしていない)インクジェットプリンタ(複合機)では、インクの吸い込みに問題が生じ、「いくら調整しても・何度クリーニングを行っても」期待した濃度での印刷ができないということも経験しました。サービスマンの方に伺ったところ、「こういう状態になると通常のクリーニングではなかなか復旧しない」と教えていただき、あらためて日常的に使用してインクを動かすことと、不具合が見えたらすぐにメンテナンスをお願いすることの大切さに気づいたこともあります。

そのサービスマンの方からは、マークシートに付着していた消しゴムの「屑」がスキャナーのローラー等可動部の動きを悪くして、マークシートがやや斜めにスキャンされたりする原因となり得ることも教えていただきました。実際に大量のマークシートを読み取ってきた複合機のスキャナー部分からは、かなりの量の消しゴム屑が・・・。受験者には消しゴム屑をよーく落としてから答案(マークシート)を提出するよう注意しておく必要があります。まさに塵も積もればなんとやら・・・です。

また、ご使用のスキャナーの読み取り設定によっては(デフォルトの読み取り設定が)0~255段階のグレースケールでなく、カラーであったり、ある閾値で白黒二値化しての読み取りであったりという、私の想定外の設定であることも、当然のようにあり得ると思います。それがカラー画像であった場合の影響はほとんどないと思われますが、ある閾値での白黒二値化画像であった場合は、判定に重大な影響を及ぼす可能性があります。ですので、マークシートの読み取りに、使用されるスキャナーの読み取り設定に関して、予め、使用者様の責任で十分ご確認いただけますよう、併せてお願い申し上げます。

11.まとめ

このマークシートリーダーで出来ること、出来ないことをまとめました。

【出来ること】

・マークシートのJpeg画像を回転&適切なサイズに縮小
・マークシート画像のマーク読み取り(1設問当たり最大16選択肢まで対応)
・読み取り結果の確認(GUI & 音声出力)
・読み取り結果のCSVファイル出力
・読み取り結果を採点結果通知用Excel Bookへ出力(新教育課程に対応)
・共通テスト形式の数学試験に対応(選択肢:-、±、0-9、記号:a~d)※ 後日掲載します。
・共通テスト形式の情報Ⅰ試験に対応(選択肢:0始まりの設定も可能)※ 後日掲載します。
・使用環境に合わせて各種パラメータ設定を変更可能
 ⇨ ScanSnap iX1500のノーマルモード(解像度150dpi相当?)、もしくはEPSON PX-M7110F(解像度200dpi)でスキャンしたJpeg画像のマーク読み取りに最適化した値をデフォルト値に設定済み。

【出来ないこと】

・1設問について、複数の解答が設定された採点
・前問の解答内容に応じて、次の問いの解答が変わる採点
・その他、答案1枚のみの採点等、このプログラムで想定外の採点全て
・1回の読み取り操作で処理できるJpeg画像は99枚までで、100枚を超える枚数は処理できません。

【その他の使用方法】

MS_Reader.exe の「ヘルプ」にある「PDFを表示」をクリックすると利用方法の手引きがお使いのPDFリーダーで表示されます。マークシートの作り方等、このブログの記事にないことも書いてありますので、必要に応じてこちらも併せてご参照いただけますよう、お願いいたします。

12.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容及びダウンロードしたプログラムを利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

VCLのMessageBox

Delphi 12.0を使い始めて、最初に気づいたことは、メッセージダイアログの異変だ。

( なんだか、シンプルになったなー )

Delphi 12.0 のメッセージダイアログ


そう。MessageDlgからアイコンが消えちゃった!

【もくじ】

1.Delphi 12.0 のメッセージダイアログ
2.MessageBoxがあった!
3.MessageBoxの使い方を学ぶ
4.MessageBoxの使用例
5.ダイアログからの戻り値
6.まとめ
7.お願いとお断り

1.Delphi 12.0 のメッセージダイアログ

今まで( Delphi 11.2 )なら・・・

i マークのアイコンがあるメッセージダイアログを表示するコード


実行すると・・・

表示されたメッセージダイアログ


System.UITypes を uses してないと・・・

「System.UITypesをusesしなさい」とアドバイスされる(これは 12.0 も同じ)。

Delphi 12.0 では・・・、System.UITypes は、もちろん uses して、

Delphi 12.0 でメッセージダイアログを表示


11.2 と同じコードを実行すると・・・

青地に白の i マークのアイコンが「ない」


それに、なんかエラーも。

僕には理解不能


( どうする? )

取り敢えず、Help でVCLのライブラリ・リファレンスを調べてみた。

キーワードを指定してEnterキーを押すと・・・


表示されたリファレンスの下の方に、次の一文を発見!

mtInformation じゃなくて、mtConfirmation だけど「Microsoftは、・・・削除してしまいました。」とある・・・


( これかー T_T )

mtInformation ではなく、mtConfirmation で試してみると・・・

引数を mtConfirmation に変更


実行すると・・・

アイコンは表示されないが、タイトルは「確認」に変わる


タイトルが「情報」から「確認」に変わるから、mtConfirmation は確かに効いてる。

「このアイコンがメッセージの具体的な種類を明確に表さない」って、んじゃもっと良いアイコンに変更するとか・・・、元々、アイコンはキャプションの補佐的役割を果たしているだけだから、それが「ない」よりは「あった」方が親切だと思うんだけれど・・・。

リファレンスによれば、第2引数に指定可能な値とその意味は次の通り。

Delphi 12.0 の Help の VCL のライブラリ・リファレンスを引用


リファレンスには「以前のダイアログ ボックスの概観を使用するには、Vcl.Dialogs ユニットの UseLatestCommonDialogs 変数を False に設定しなければなりません。」と但し書きがある。

以前の・・・って、XPの時代のことかな?・・・なんて思いながら、次のように指定して、

UseLatestCommonDialogs 変数を False に設定


実行すると・・・

さらにシンプルに・・・T_T


僕的には「シンプルじゃないのがよかった」ので、
とても「Simple is Best 」的な気持ちにはなれない!

あぁアイコンが欲しい!

もう、二度と・・・


きみに会えないかと思うと。

魂が折れそうなくらいの、さみしさがこみあげて・・・くる。

なんでだよー!

そう思いながら、ついでに mtError も試してみる。すると・・・

エラーのアイコンは表示される


「 × 」マークはMicrosoftさん的には、全世界共通「ダメです」って通じるってこと?


mtWarning もやってみた!

警告もアイコンは表示された


黄色の三角形に「i」が表示されたら、世界中の人が「警告」だって思うのかなー☆
Microsoftさん的には、「思う」ってことなんだろうなー。


ともあれ、これで確認できた。「情報」と「確認」がダメなんだ・・・。

でも、ない袖は振れない。

自分専用のダイアログを作るという、奥の手がないわけじゃないけど、それは最終手段。

MessageDlg のかわりになるものは・・・、ShowMessage 以外になんか、なかったっけ?

2.MessageBoxがあった!

ものごころついた時から・・・ってわけじゃないけど、僕はずっと MessageDlg関数を使ってきた。
ダイアログで使用する Font の大きさや色を変えたいなど、余程の理由がない限り、ユーザーに何かを伝えるいちばんの方法は、僕にとっては MessageDlg関数 で表示するメッセージだった。

それが ShowMessage ではない最大の理由は、テキストの他にアイコンも表示できるから。

だから、階層化テキストエディターの NaNaTree に書き溜めてきた、さまざまなメッセージの表示方法に関する覚書も、そのほとんどが MessageDlg関数 についてのもので、これにかわる代替手段など、これまで僕は考えたこともなかった。

( バージョンアップとかすると、毎回、いろんな落とし穴があるけど・・・ )

( 今回のも、これまで普通に使ってきたダイアログの仕様変更だもんなー )

( すーぱー困るけど、元に戻してくれるわけないし・・・ )

( なんか、代替手段、なかったっけ? )

そう思いながら、NaNaTree に保存した「メッセージ」に関する記録を下のほうへスクロールしてみる。

すると・・・

MessageBoxなる文字が・・・


あんまり使ったことのない、未整理の項目が下の方にあって、その中にMessageBoxという文字を発見。

( ほぼ使った記憶がないけれど、そんな関数もそう言えばあった・・・ )

さっそく、Helpを読んで、書き方を確認し、次のコードを実行してみた。

MessageBox関数を試す


はたして、アイコンは表示されるか?

祈るような気持ちで実行すると・・・

アイコン付きのダイアログが表示された!

何にも(追加で) uses しなくてイイし、いきなり書いて、すぐ動く!

コレだ! コレ!!

今度からコレで行こう☆

自分でもあきれるくらいの変わり身の早さ・・・

MessageDlg が泣いてるぞ。

愛してたんじゃなかったのかい・・・?

3.MessageBoxの使い方を学ぶ

VCL ライブラリの Help を読んでわかったことは、「MessageBox は、Windows API MessageBox 関数をカプセル化したものである」ということ。

このカプセル化で「具体的に何がどうなったか」と言うと、Windows.MessageBox とした場合には必要であったウィンドウ ハンドル パラメータが欠けていても、自動的に補完される、つまり、所有者ウィンドウへのハンドルは引数内で指定しなくてもよいということ。

ありがたき しあわせ。

ちなみに、Win32APIのリファレンス(C++)では、次のようになっているが、

int MessageBox(
  [in, optional] HWND    hWnd,
  [in, optional] LPCTSTR lpText,
  [in, optional] LPCTSTR lpCaption,
  [in]           UINT    uType
);

Delphi の Application.MessageBox のリファレンスでは、次のように

function MessageBox(const Text, Caption: PChar; Flags: Longint = MB_OK): Integer;

所有者ウィンドウへのハンドルが確かに省略されている。

実際のコードで、Windows.MessageBox とした場合には・・・

procedure TForm1.Button4Click(Sender: TObject);
begin
  Winapi.Windows.MessageBox(Handle, PChar('Do you know Delphi?'), PChar('情報'), MB_OK or MB_ICONINFORMATION);
end;

だったのが、第一引数のHandle は必要なくなり、( OK ボタンのみの表示でよければ)MB_OK も省略できるようなので、次のように

procedure TForm1.Button3Click(Sender: TObject);
begin
  Application.MessageBox(PChar('Do you know Delphi?'), PChar('情報'), MB_ICONINFORMATION);
end;

・・・とずい分、短くなる。それどころか、PChar型への型変換も省略可能なようで・・・

procedure TForm1.Button5Click(Sender: TObject);
begin
  Application.MessageBox('Do you know Delphi?', '情報', MB_ICONINFORMATION);
end;

型変換も内部で自動的に処理してくれるようだ(このまま実行可能。エラーも、警告も、ヒントも表示されない)。

Good! Gooder! Goodest!

これからは MessageBox で行こう!

4.MessageBoxの使用例

「 i 」マークのアイコンで、OK ボタンのみ表示する場合は、

Application.MessageBox(PChar('メッセージ'), PChar('情報'), MB_ICONINFORMATION);


警告マーク(黄)

Application.MessageBox(PChar('メッセージ'), PChar('警告'), MB_ICONWARNING);


禁止・エラー・停止マーク(赤地に白)

× の意味は、第3引数にSTOPとあるから「停止」が正しいのかな?

Application.MessageBox(PChar('メッセージ'), PChar('禁止'), MB_ICONSTOP);
Application.MessageBox(PChar('メッセージ'), PChar('エラー'), MB_ICONSTOP);


?マーク(青地に白)

Application.MessageBox(PChar('メッセージ'), PChar('質問'), MB_ICONQUESTION);

文字列型の変数を用意して、

var
  strMsg:string;
begin
  strMsg:='メッセージ';
  Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
end;


別の文字列型変数をさらに代入したり、また、改行を含む表示も、

procedure TForm1.Button2Click(Sender: TObject);
var
  strMsg, strPath:string;
begin
  strPath:='C:\abc\def';
  strMsg:='出力先は次の場所です。' + #13#10 + #13#10 + strPath;
  Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
end;

複数のボタンを表示。例えば、「はい」・「いいえ」の二択なら、

procedure TForm1.Button3Click(Sender: TObject);
begin
  //Information
  if Application.MessageBox(PChar('Do you know Delphi?'), PChar('情報'), MB_YESNO or MB_ICONINFORMATION) = mrYes then
  begin
    //[はい]が選ばれた時
    Application.MessageBox(PChar('Gooooooooooooood!'), PChar('情報'), MB_ICONINFORMATION);
  end else begin
    //[いいえ]が選ばれた時
    Application.MessageBox(PChar('No!'), PChar('情報'), MB_ICONINFORMATION);
  end;
end;
二択だから「キャンセルはない」
(閉じるボタンは自動的に無効になる)

ユーザーに「キャンセル」も許可するなら、

procedure TForm1.Button4Click(Sender: TObject);
var
  StrMsg: String;
  intRet: Integer;
begin
  StrMsg := 'Do you know Delphi?';
  intRet := Application.MessageBox(PChar(StrMsg), PChar('情報'),
                         MB_YESNOCANCEL or MB_ICONQUESTION);
  if intRet = mrYes then begin
    //[はい]を選択した時の処理

  end else
  if intRet = mrNo then begin
    //[いいえ]を選択した時の処理

  end else
  if intRet = mrCancel then begin
    //[キャンセル]を選択した時の処理
    Application.MessageBox(PChar('ユーザーによる処理のキャンセル'), PChar('情報'), MB_ICONINFORMATION);
  end;
end;
閉じるボタンは自動的に有効化されている


ちなみに「キャンセル」ボタンではなく、ダイアログ右上の「閉じる」ボタンをクリックすると・・・

閉じるボタンをクリックした場合は、キャンセル扱いになるようだ。

デフォルトでフォーカスを与えるボタンの指定方法は、第3引数の中で MB_DEFBUTTON3 のように、MB_DEFBUTTON の後ろにフォーカスを与えるボタンの番号を付けて指定する。番号はダイアログに表示するボタンの左から順番に1、2、3、4となるようだ。

第3引数に MB_YESNOCANCEL or MB_DEFBUTTON3 or MB_ICONQUESTION を指定すると、

左から三つめの「キャンセル」ボタンにフォーカスされた状態でダイアログが表示される


数字を付けない MB_DEFBUTTON や、数字を付けても MB_DEFBUTTON0(ゼロ)や MB_DEFBUTTON5 は未定義の識別子エラーになることから、指定可能なボタンの数は1~4の範囲内と決まっているようだ。

MB_DEFBUTTON5 は未定義の識別子エラーになる


「はい・いいえ・キャンセル」の三つボタンを表示する設定で MB_DEFBUTTON4 を指定しても未定義の識別子エラーにはならないし、実行時コンパイラはヒントも警告も表示しないが、フォーカスは最も左のボタンに当たるようだ。

intRet := Application.MessageBox(PChar(StrMsg), PChar('情報'),
                         MB_YESNOCANCEL or MB_DEFBUTTON4 or MB_ICONQUESTION);
MB_DEFBUTTON4 を指定してもエラーにはならない。
MB_DEFBUTTON4 を指定したにもかかわらず、
4つめのボタンがない場合には、最も左のボタンにフォーカスされる。

ってか、四つ目のボタンでナニ?

想像したけど、ちょっと思いつかない。表示できるボタンの種類を調べてみた。

MessageBox に表示できるボタンの種類は全部で七つあるようで、Flags パラメータで指定可能な値は、次の通り(Delphi 12.0 の VCL リファレンスより引用)

意味
MB_ABORTRETRYIGNORE メッセージ ボックスには、次の 3 つのボタンが配置されます: 中止、再試行、無視。
MB_OK メッセージ ボックスには、次のボタンが配置されます: OK。 これがデフォルトの設定です。
MB_OKCANCEL メッセージ ボックスには、次の 2 つのボタンが配置されます: OK、キャンセル。
MB_RETRYCANCEL メッセージ ボックスには、次の 2 つのボタンが配置されます: 再試行、キャンセル。
MB_YESNO メッセージ ボックスには、次の 2 つのボタンが配置されます: はい、いいえ。
MB_YESNOCANCEL メッセージ ボックスには、次の 3 つのボタンが配置されます: はい、いいえ、キャンセル。
Flags パラメータで指定可能な値

リファレンスには、さらに「これらの値は、希望する効果を得るため、組み合わせて使うこともできます。」とある。組み合わせるって、どういうこと?

ちなみに、MB_OK と MB_RETRYCANCEL を組み合わせてみると・・・

procedure TForm1.Button5Click(Sender: TObject);
begin
  Application.MessageBox('Do you know Delphi?', '情報', MB_OK or MB_RETRYCANCEL or MB_ICONINFORMATION);
end;

OK と 再試行、キャンセル 三つのボタンが表示されるのかなー? って思ったけど・・・

OK ボタンは表示されませんでした!


これは MB_OK と MB_ABORTRETRYIGNORE を組み合わせても同じで、MB_OK はやはり表示されない。MB_DEFBUTTON4 の役割は何なんだろう???

Web上の資料を調べてみた!

MessageBox 関数 (winuser.h)

https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-messagebox

上記リンク先の説明によれば MB_HELP もあるらしい。

Application.MessageBox('Do you know Delphi?', '情報', MB_OK or MB_HELP or MB_ICONINFORMATION);

実行してみると・・・

OK とヘルプは共存できるようだ

ただ、ヘルプをクリックしても、なんにも起きなかったが・・・
(処理を書いてないんだから、当然と言えば、当然)

しかし、戻り値の中に「ヘルプ」に相当する値がない・・・ような気が。上記の資料には「ユーザーが [ヘルプ ] ボタンをクリックするか F1 キーを押すと、 WM_HELPメッセージが 所有者に送信されます。」とあったけど、これ以上、追いかけると深みにはまりそうな気がするので、ヘルプに関する勉強は後日それが必要となった時にあらためてすることにして、ここではお茶を濁す。

結局、MB_DEFBUTTON4 の果たす役割はわからずじまい。残念!

それから、MessageBox関数には「すべてはい・すべていいえ」のボタンがないようだ。これもまぁ、それが必要になった時、考えることにする。僕が書くプログラムでそれが必要になることは、まず、ないだろう・・・。

あとわかったことは、「Text パラメータの値はメッセージで、必要なら 255 文字以上になっても構いません。 長文メッセージは、メッセージ ボックスでは自動的に改行されます。」ふむふむ、なるほど。

さらに「Caption パラメータの値はキャプションで、ダイアログボックスのタイトル バー上に表示されます。 Captions は 225 文字より長く指定できますが、改行されません。長文キャプションの場合、メッセージ ボックスの幅が広げられます。」とのこと。

5.ダイアログからの戻り値

ダイアログからの戻り値は、次の通り(Delphi 12.0 の VCL リファレンスより引用)

定数意味
mrNone0 なしユーザーが終了する前にデフォルト値として使用される
mrOkidOK ユーザーが[OK]ボタンで終了した
mrCancelidCancelユーザーが[キャンセル]ボタンで終了した
mrAbortidAbortユーザーが[中止]ボタンで終了した
mrRetryidRetryユーザーが[再試行]ボタンで終了した
mrIgnoreidIgnoreユーザーが[無視]ボタンで終了した
mrYesidYesユーザーが[はい]ボタンで終了した
mrNoidNoユーザーが[いいえ]ボタンで終了した
ダイアログからの戻り値

6.まとめ

プログラムからユーザーに対してメッセージを表示する方法は、他にもいろいろあるみたいだけれど、MessageBox関数最大の強みは「すべての OS で利用可能」なこと。

OK と、はい・いいえ、キャンセル の各ボタンが利用できれば十分というのであれば、アイコンが表示されなくなった MessageDlg関数のかわりに MessageBox関数が使える。もちろん、ダイアログにアイコンも表示される。

ただ、これまでに書いてきたプログラムを Delphi 12.0 で修正・更新する際にはMessageDlg関数を、MessageBox関数に変更するという、地道な作業が待ってることだけがちょっと気になるが。

まっ ヒマだから、いいか!

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

範囲チェックエラーが出た時は?

{$R-}で範囲チェックさせない!

Delphiで、画像をグレースケール変換するプログラムを作成。実行すると、

表示されたエラーメッセージ

プログラムのコードは、次の通り。
Image1に表示した画像をグレースケールに変換してImage2に表示するというモノ。

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Jpeg,
  Vcl.ComCtrls;

type
  TForm1 = class(TForm)
    Image1: TImage;
    Image2: TImage;
    Button1: TButton;
    StatusBar1: TStatusBar;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

function CreateGrayScalePalette(Tone:Byte): HPALETTE;
var
  Palette: ^TLogPalette;
  i: Integer;
begin
  GetMem(Palette, SizeOf(TLogPalette) + SizeOf(TPaletteEntry) * Tone );
  Palette^.palNumEntries:=Tone+1;
  Palette^.palVersion:=$0300;
  for i := 0 to Tone - 1 do begin
    Palette^.palPalEntry[i].peRed:= Tone - i;
    Palette^.palPalEntry[i].peGreen:= Tone - i;
    Palette^.palPalEntry[i].peBlue:= Tone - i;
    Palette^.palPalEntry[i].peFlags:= 0;
  end;
  Result:=CreatePalette(Palette^);
  FreeMem(Palette);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  X, Y: Integer;
  Bmp: TBitmap;
  P: PByte;
begin
  Bmp := TBitmap.Create;
  try
   Bmp.Assign(Image1.Picture.Bitmap);
   Bmp.PixelFormat := pf8bit;
   Bmp.Palette := CreateGrayScalePalette(255);
   Image2.Picture.Bitmap := Bmp;
  finally
   Bmp.Free;
  end;
  Image2.Width:=Image2.Picture.Bitmap.Width;
  Image2.Height:=Image2.Picture.Bitmap.Height;
  Image2.Visible:=True;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  jpg: TJPEGImage;
begin
  StatusBar1.SimplePanel:=true;
  // TJPEGImageオブジェクトをインスタンス化
  jpg := TJPEGImage.Create;
  try
    // Jpegファイル読み込み
    jpg.LoadFromFile('Image.jpg');
    // Image1に割り当てる
    Image1.Picture.Bitmap.Assign(jpg);
    Image1.Width:=Image1.Picture.Bitmap.Width;
    Image1.Height:=Image1.Picture.Bitmap.Height;
    //StatusBar1.SimpleText:=IntToStr(Image1.Width)+'/'+IntToStr(Image1.Height);
  finally
    // TJPEGImageオブジェクトを破棄
    jpg.Free;
  end;
end;

end.

グレースケール変換実行のボタン(Button1)をクリックすると・・・
このButton1Click手続き内で呼び出しているCreateGrayScalePalette関数でエラーが発生。

ブレークして確認すると、エラーになるのはココ。

でも、なんでエラーになるのか、わからない・・・

Google先生に訊くと、次のような情報を発見。

[Delphi?][ネタ]透明に見えるパターンを描く

https://qiita.com/pik/items/25276e49fb131425db07

早速、範囲チェックさせないコンパイラ指令 {$R-} を追加。

ナニがどうして、そうなるのか?
原因も、理由も、皆目わからないけれど・・・

範囲チェックエラーは出なくなりました!

範囲チェックを実行しないというコンパイラ指令 {$R-} は知りませんでした。
同じ理由で困ってる方もいるかもしれないと思い、記録だけUpしました。

なお、画像のグレースケール化にあたっては、次のWebサイト様にあった情報を使わせていただきました。24bitのフルカラー画像を256階調のモノクロ画像に変換(グレースケール変換)する処理を行う際に役立つ情報が、そのアルゴリズムも含めて、多数紹介されています。

カラー画像をモノクロ画像に変換

http://rakasaka.fc2web.com/delphi/grayscale.html

また、上のWebサイト様で紹介されている配列要素を動的に確保する必要のない、Delphiが独自に定義しているTMaxLogPalette構造体を使用したCreateGrayScalePalette関数を利用した場合は、範囲チェックエラーは発生しませんでした。

お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

This updated is support for use with high resolution devices

高DPIに対応しました!

これまでずっとPC画面の解像度は1366×768に固定して、この解像度での使用のみを前提に、僕はプログラムを書いてきた・・・。僕のプログラムが走るマシンは全部、この解像度だったから、それで何も問題は起きなかったのだけれど。

【今回の記事】

1.2880×1920の世界を知る
2.Formの表示がたいへんなコトに・・・
3.問題点を続々と発見!
4.exeを高DPI対応に設定
5.VCLの幅や高さを自動調整
6.まとめ
7.お願いとお断り

1.2880×1920の世界を知る

新しく支給されたPCの画面解像度は2880×1920で、拡大縮小率は150%に設定されてた。持ち運ぶことを考えると、ノートPCの画面サイズはそうそう大きく出来ないから、画面サイズが変わらないまま、解像度だけ上がってしまうと、相対的にアプリや文字の見た目はどんどん小さくなって、目にとてもやさしくない画面になる。だから拡大150%や拡大200%って設定が必要なんだと思うけど・・・。

取り敢えず、この環境で僕のプログラムを動かすとどうなるか、実験してみた。

1366×768、拡大率100%で表示した場合(開発時の設定)
2880×1920、拡大率100%で表示した場合

高解像度画面では、ボタンのCaptionが読めない・・・。

2.Formの表示がたいへんなコトに・・・

しかも、このプログラムから別のFormを呼び出すと・・・

たいへんなコトに・・・

自分的には、こう表示されてほしいのですが・・・
(今までは何の問題もなく、こう表示されていた)

この(今までの)ように表示するには、どうしたらイイ?

やばい。何としても高DPIに対応させないと、職場のみんなにプログラムを使ってもらうどころか、自分ひとりですら使えない。画面が高解像度になっただけで、こんな問題が生まれるなんて・・・。これまで考えたこともなかった。

けっこうショックが大きくて、心がまた折れかけたけど、この問題をクリアすれば、プログラムも、僕も、もっとよくなれるんだって、必死で自分に言い聞かせる。

3.問題点を続々と発見!

高解像度画面で一通り、プログラムの動作検証を行ってみると、見つけられただけで次のような問題が発生することがわかった。

① Formが設計時とは異なる大きさで表示される。
② 画面表示の拡大設定を行わないと、字が読めないくらい小さくなる。
③ 拡大設定時には、VCLコントロール(Toolbar)の幅や高さが意図しないものになる。

まず、①の問題の解決にチャレンジ。

FormCreate手続きでFormの幅を指定しても無駄。
まるで言うことを聞いてくれない。
いったいナニがどうなると、この問題が発生するのか?
これまで、こんな問題に出会ったこと、ないぞ・・・。

そう思いつつ、いろいろ調べてみると、次の情報を発見。

フォームを新規作成したらまずやる事 (Delphi)

https://ht-deko.com/ft1004.html#100408_02

明らかな既視感があったので、以前、どこかで見た情報に間違いないと思うのだけれど、知識として使ったことがなかったので、情報の有用性に気づいてなかった・・・。

この中に、Scaledプロパティに関して、次の記述が・・・

Scaled
常に False。True にすると OS の DPI (ユーザが指定した DPI) によってフォームサイズやコントロールサイズが勝手に変更されてしまいます。

(たぶん、コレだ・・・)

早速、すべてのFormのScaledプロパティをFalseに変更。なんでこんな問題を起こすような設定がデフォルトでTrueなんだ?・・・何か、大切な理由でもあるんだろうか?

動かして確認。

直った!(・・・というか、壊れなくなった)

これで①の問題は解決。思ったより簡単に解決できて、よかった!

4.exeを高DPI対応に設定

②の文字の大きさについて、Google先生にいろいろきいた結果、こちらもベストと思われる対応方法を発見。

Windows11でアプリやメニューが小さい時に行う高DPI設定

https://win11lab.info/win11-high-dpi/

設定方法は、次の通り。

exeを右クリックして表示されるサブメニューの「プロパティ」をクリック
「高DPI設定の変更」をクリック
「高い DPI スケールの動作を上書きします」のチェックボックスをチェックして、
拡大縮小の実行元は「システム」を選択。

で、OK → 適用 → OK と順にボタンを押して画面を閉じ、アプリを再起動すると、Formが適正な倍率で表示されてアプリやメニューが見やすくなった。ちなみに「システム」ではなく、「アプリケーション」では表示に変化がなく、「システム(拡張)」ではFontが高解像度化された感じに。

いちどexeにこの設定を実行しておけば、画面の解像度をいろいろ変更しても常にFormは適正な大きさで表示されるようになり、たいへん便利!

Windowsには、ほんとうにいろんな画面解像度の設定があるから、exeに対するこのおまじないは必須なのかもしれない・・・。これで②の問題も無事解決。

結局、②の問題は、プログラムではなく、OS側の設定の問題だった。

5.VCLの幅や高さを自動調整

最後に残った③の問題に取り組む。まず、これがどういう現象かと言うと・・・

手書き答案採点プログラムで、画面を横にスクロールさせるために作ったToolbarコントロールが、本来なら次のように表示されるはずなのに・・・

ToolButton1,2,3とBevel1の4つのコントロールの幅の合計値がToolbar1の表示サイズの幅となるはず

上の4の設定を行わず、かつ、画面の拡大縮小が100%でない場合には・・・

表示そのものが崩れてしまう・・・

職場のマシンたちは全部!デフォルト設定が「高解像度」で、画面の拡大率150%だから、何にもしないで僕のプログラムを配布されたままの状態で動かしたら、間違いなく、この問題が発生してしまう・・・。

マジ、困った・・・。

すがるような思いでGoogle先生に援けを乞う。すると・・・

03_高 DPI における画像の描画サイズ調整

http://mrxray.on.coocan.jp/Delphi/Others/DisplayDPI_Image.htm#03

またしても、Mr.XRAYさんのサイトに救いとなる情報を発見!

職場では、僕のことを「困った時の〇〇さん・・・」と呼ぶ人がいるけど、
僕にとってMr.XRAYさんは、「本当に困った時のMr.XRAYさん」です。

これまでにいったい何度、僕の窮地を救ってくださったことか・・・。
あらためてMr.XRAYさんに、心から感謝のありがとうです。

Mr.XRAYさんのホームページにあった情報をもとにプログラムを次のように修正。

procedure TFormCollaboration.btnSelectClick(Sender: TObject);

  //--------------------------------------------------------------------------
  //  ディスプレイの拡大縮小の比率を取得
  //  100% の時は 1.0.150% の時は 1.5 を返す
  //--------------------------------------------------------------------------
  function GetDpiRatio: Extended;
  var
    LXDpi : Integer;
  begin
    LXDpi := GetDeviceCaps(GetDC(0), LOGPIXELSX);
    Result := LXDpi / USER_DEFAULT_SCREEN_DPI;
  end;

var
  ・・・
  //高DPIに対応する
  VCL_Width:Extended;
  VCL_Height:Extended;

begin

  ・・・

  //解像度が変わると不具合がでる
  //r.Right := r.Left+ToolBar1.Width;
  //r.Bottom := r.Top+ToolBar1.Height;

  //解像度の変更に対応
  //幅
  VCL_Width := (ToolButton1.Width + 
    ToolButton2.Width + ToolButton3.Width + Bevel1.Width) * GetDpiRatio;
  r.Right := r.Left + Trunc(VCL_Width);
  //高さ
  VCL_Height := ToolBar1.Height * GetDpiRatio;
  r.Bottom := r.Top + Trunc(VCL_Height);

  ・・・

end;

GetDpiRatio関数を使ってディスプレイの拡大・縮小の比率を計算し、これをVCLコントロールの幅と高さに掛けて、コントロールが適切に描画されるように設定。

上記設定を行った後、実行中のToolbar
(フローティング状態で画面の任意の位置に埋め込む)

こうしてプログラム側でも、VCLコントロールの幅や高さを画面の拡大縮小に合わせるように設定しておけば、exeそのものに「高 DPI 設定の変更」を設定しなくても・・・

ちょっとカタチは崩れるけど、使えないレベルではない。
exeに「高 DPI 設定の変更」を設定せず、
拡大150%で実行してみた場合

これで③の問題も無事解決できた!

プログラムも、僕も、よくなれた☆

それは間違いない・・・から、いいんだけれど。
ひとりでも、戦えるかな・・・

Let me see you through.

空を見上げて・・・

I’m missing you.

そう思えてならない時が、あるんだ。

6.まとめ

(1)様々な画面解像度に対応するには、FormのScaledプロパティをFalseに設定。
(2)画面の拡大縮小に対応するにはプロパティの「高 DPI 設定の変更」を利用。
(3)画面の拡大縮小にプログラムコードでも対応可能。

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Recognize handwritten katakana characters No,4

手書きカタカナ文字をPCに認識させる(その④)

前回の記事で作成した手書きカタカナ文字「アイウエオ」の学習モデルを、My手書き答案採点プログラムで利用できるようにした。自動採点用のGUIを作成して、実際の手書き文字をどの程度正しく認識できるか検証。ついでに、ふと思い立って、「〇」記号と「×」記号の学習モデルも作成。こちらについても、正しく認識できるかどうか、実験してみた。結果は「アイウエオ」、「〇×」とも100%正しく認識することはできなかったが、よく考えれば、リアルな文字認識にチャレンジするのは今回が初めて。ここまでが長かったので、自分的には終了感満載だったけど、ここからが本当のチャレンジの始まりなんだ・・・と気づく。これまでにやってきたことは、言わば準備作業。現段階で、僕の「自動採点」は、採点作業の「補助」くらいには、使えるんじゃないか・・・と。

1.それは「イ」じゃないんですけど・・・問題への対応を考える
2.プログラムに自動採点のGUIを追加
3.自動採点を実行!(その1)
4.自動採点を実行!(その2)
5.〇×記号の学習モデルを作成
6.〇×記号の解答も自動採点
7.FormCreateでPythonEngineを初期化
8.まとめ
9.お願いとお断り

1.それは「イ」じゃないんですけど・・・問題への対応を考える

まずは、前回の記事で最後に紹介した「問題」への対応から。

前回は、学習モデルの性能を確認するため、PCの画面にマウスで描いたカタカナ文字をLobeで作成したMy学習モデルが「どの程度正しく認識できるか」を試すプログラムをDelphiで作成して検証(文字認識部分は内部に埋め込んだPythonスクリプトで実行)。

My学習モデルは、上の文字すべてを正しく認識してみせた

あまりにもGoooooooooooooooooooooooooooooooooood!な結果に、この結果にたどり着くまでの長かった道のりを思い出し、本人涙ぐむシーンもあったが・・・、スキャナーでスキャンした画像にみられるシミや汚れへの反応をみるため、試しに画面をワンクリックして「点」を入力し、それを認識させてみたところ・・・

信頼度は99.9%・・・でもLobeさん、それ、「イ」じゃないと思うんですけど・・・。

このあまりにも楽しい結果に、今度は涙ぐむほど大笑い。さすがMy学習モデル。夏休みの自由研究レベルをしっかりと維持しています・・・。

で、どう対策したか?

さすがにこのままでは実戦に投入できないので、文字画像に「大津の二値化」を適用した後、OpenCVのcountNonZero()関数を利用して、全ピクセルのうち、値が0(=黒)でないピクセルの合計を求め、画像中の白黒の面積を計算。イロイロ、テストした結果、上記の画像で白面積(=文字面積)が1.5%より大きい画像を「文字情報あり」と判断して、輪郭検出するようスクリプトを修正。これで、この問題は無事クリア☆

# 読み込んだイメージにOpenCVのcountNonZero関数を適用、白面積を計算。
wPixels = cv2.countNonZero(img)

※ 上の画像では、文字が「白」なので白面積を計算している。

2.プログラムに自動採点のGUIを追加

My手書き答案採点プログラムに自動採点のGUIを付け加えるにあたり、プログラムの64ビット化(プログラムに同梱したembeddable PythonにインストールしたTensorFlowは64ビット版しか存在しないため)と、解答欄矩形の自動検出機能の実装で不要になったGUIの整理を行った。で、空いたスペースに自動採点のGUIを作成。

TensorFlowに合わせ、プログラムは64ビット化☆
My手書き答案採点プログラムを実行中の画面

操作パネルのGUIを32ビットバージョンから、次のように変更。準備段階でしか使わなかった部品があらかた消えて、(自分的には)画面がかなり「すっきり」した気が。

解答欄矩形の手動設定関連のGUIを削除して、空いたスペースに自動採点のGUIを作成

3.自動採点を実行!(その1)

(1)学習モデルを指定

学習モデル「ア行」を選択する

選択肢だけは、たくさん用意してあるけど、現在利用できるのは「○×」と「ア行」のみ。(「カ行」以降は、もしかしたら永遠に利用できないカモ・・・)

自前で機械学習の訓練用データを作成するのは、本当に、本当に、本当に、すーぱーたいへん! 答案をスキャンした画像から、文字画像の切り抜き&クリーニング作業を、またン千枚もやるかと思うと・・・。

ポキッ あっ! 心の折れた音が。

(2)正解ラベルを指定

正解ラベルを選択

設問ごとに、正解ラベルを選択。学習モデルの識別結果と、ここで選択指定した正解ラベルを比較して、〇・× を判定。で、得点欄に入力(選択)した値を採点記号とともに解答欄の指定位置に表示する。プログラム起動後、初回の実行時にはPython Engineの初期化に数秒かかるが、2回目以降、採点自体は35枚を1秒程度で処理できた☆ だから処理時間に起因するストレスはまったく感じない。Python Engineの初期化だけ、あとで何とかしよう・・・。

(3)自動採点を実行

解答用紙のサンプル(これを35枚書いた☆)

「アイウエオ」の文字データは、集めたサンプルに似せて全部自分で手書きしたもの。文字の大小、濃淡、線の太さ等なるべく不揃いになるようにした(つもり)。解答用紙は新品はもったいないので、職場にあった反故紙の裏面に解答欄を印刷して利用。ホントは、もっとたくさん作成するつもりだったんだけど、35枚書いたところでなんか用事が入り、もうその後は作業を再開する気が失せて、作業を放棄。そのような理由から、とりあえず35枚で実験することに。

ウソ偽りのない採点結果の一例は、次の通り(「ア」を正解とした場合)。

サンプルを真似たアイウエオを書いて、My手書き答案採点プログラムで自動採点した結果

自動採点へのチャレンジを始めたのは2022年の12月下旬だから、ここにたどり着くまでに2ヵ月かかっている・・・。途中、(もはや、これまで)みたいなシーンも何度かあったけど、そのたびに『誰も待ってないけど、オレはやるぞ』と自分自身を叱咤激励。

「オレはやるぞ」と言えば・・・

高校生だった頃、芸術選択はめったにない「工芸」で、すごく楽しくて・・・。焼き物の時間に、みんなは指示された通り、湯飲みとか作ってたけど、僕は「オレはやるぞ!」って文字を刻んだ粘土板(看板)を岩石風の土台に張り付けた、何の役にも立たないモニュメントを製作して、大満足。先生は笑いながらも、僕の作品(?)を炉のすみっこに入れて焼いてくださった。高校生活、最高だったなー☆

解答欄画像の切り抜きとは別に、プログラム内部では(罫線の影響を排除して)、個々の解答欄画像中の文字をOpenCVの輪郭検出で探し出し、幅64×高さ63で切り抜いて、次に示すような画像データを作成している。

解答欄画像から輪郭検出で切り抜いた文字画像

なんで「イ」だけ「字の一部分だけが取得」されてるのか、そこは???なんだけど、その他の文字は、比較的よく検出できているのではないか・・・と思うのですが、いかがでしょう?

輪郭検出のスクリプトは、次のサイトに紹介されていたものを参考に、罫線が入らないようにするなど、様々に工夫を加えて作成。(このスクリプトの作者の方に、心から厚く御礼申し上げます)

[AIOCR]手書き日本語OCRデータセットを自動生成する[etlcdb]

https://www.12-technology.com/2021/11/aiocrocretlcdb.html

実際にキカイがどんな画像を見ているのか、気になったので調べてみると・・・

切り出し処理の途中の画像を保存してみた

そのうちの1枚を拡大してみたところ。

けっこう汚れている・・・

この二値化の処理には、また別のWebサイトにあった次のコードを当てたんだけど・・・

thresh = 
cv2.adaptiveThreshold(blur,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,11,2)

これは「濃淡の大きな画像に対しては大変有効な処理」のようだけれど、僕の用意した文字画像の処理には向かなかったようで、そこで、ここは思い切って次のように変更。

threshold = 220
ret, thresh = cv2.threshold(blur, threshold, 255, cv2.THRESH_BINARY)

上記のように変更した結果、キカイが処理の途中で見ている画像は・・・

かなりキレイになった☆

さっき拡大した画像は・・・

おー!キレイになった。実にイイ感じ!

左の方に、小さなシミがまだ残っているけど、これは次のようにして輪郭として検出しないように設定。

contours = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[0]
num = len(contours)
mylist = np.zeros((num, 4))
i = 0
# red = (0, 0, 255)
for cnt in contours:
    x, y, w, h = cv2.boundingRect(cnt)
    # 高さが小さい場合は無視(ここを調整すれば設問番号を無視できる)
    #if h < '+cmbStrHeight.Text+': <- Delphi埋め込み用
    if h < 30:
        mylist[i][0] = 0
        mylist[i][1] = 0
        mylist[i][2] = 0
        mylist[i][3] = 0
    else:
        mylist[i][0] = x
        mylist[i][1] = y
        mylist[i][2] = x + w
        mylist[i][3] = y + h
        #cv2.rectangle(img, (x, y), (x+w, y+h), red, 2)

    i += 1

まとめとしては(自分的には)、「ア」のみについて見れば、この設問20問のうち、15問正解で正解率は75%と決して高くはないけれど、「ア」以外のデータはちゃんと見分けているから、ほんとに満足。悔しい気持ちとか、全然、湧いてこない。2022年末のチャレンジで正解率91%だった時は、もう口惜しさの塊みたいになってたのに。なんで全然悔しくないんだろー? 人間ってほんと不思議。

まぁ、これに「自動採点」と銘打って、誰かに販売してお金もらったら完全な詐欺だと思うけど、『発展途上の自動採点モード付き手書き答案採点補助プログラムです。こんなんでも、もし、よかったら、使ってくださいねー! 』・・・というスタンスで仲間にタダでプレゼントする分には(合計点自動計算機能や返却用答案印刷機能等、採点プログラムとしての必須機能が完全に動作すれば)何の問題もないかと・・・。

さらに自動採点と言いながらも、採点の最後にヒトのチェックが必ず必要なのは言うまでもないので、その時、キカイが間違えた5問については、ヒトが「違うよー☆」ってやさしく訂正してあげれば、それこそヒトとキカイの美しい協働・・・じゃないのかなー☆☆☆

いいえ。
そういうのを世間一般には
「言い訳」と言います。

ってか、ここまでは全部、自動採点の準備作業で、ここからが本質的には「始まり」・・・なんだけど、自分的には、かなりヘトヘトになって終了感満載・・・

もしかして、ぼくは、とほーもないことにチャレンジしているのではないか? と、コトここに至って初めて気づく・・・

だって、「アイウエオ」と「〇×」のたった7つPCに教えるのに2ヵ月かかったんだよ。「点くのが遅い蛍光灯のようなお子さんですね」と担任の先生に評された(母親談)という、小学校低学年の児童生徒だったぼくでも、アイウエオくらいは半日で覚えたぞ・・・。

あぁ カー カー キクケコ
サシスセソー

まだ いっぱい あるー☆

4.自動採点を実行!(その2)

文字や記号が印刷された解答欄への対応も、実際問題としては必須。
例えば、次のような画像。

上に示したスクリプトがうまく動作してくれるとイイのだけれど。そう思いながら祈るような気持ちで、上の画像の設問に対して自動採点を実行・・・(正解ラベルは「エ」)。

一部、ヘンなところもあるけど、だいたいうまく切り出せた☆

で、結果は?
なんと100%正解。もしかして、夏休みの自由研究レベルじゃなかった?
予想外の成果に、僕はもう、大満足☆

設問番号「(4)」が解答欄にあっても自動採点可能でした!

スキャナーで読み込む際の縮小率とかの問題は未検証だけど、9ポイント程度の大きさで設問番号等は印刷してもらえば、だいたいOKのようだ。手書き文字が小さすぎる場合はどうしようもないけれど、それは事前に「ちいさな文字で解答してはいけません!」と案内しておけば、ある程度は防げるハズ。それでも、ちいさな文字で書くヒトは「チャレンジャー」と見なして・・・

5.〇×記号の学習モデルを作成

2月末、自動採点のGUIを作成しようと、いつもの通り、午前2時に起きて(ジジィは朝が好き / でも出勤はいちばん遅い)「さぁ、やるか」と思った時、なぜか前の晩、眠るときにふと、〇×記号の自動採点用の学習モデルならすぐ作れるんじゃないか・・・と思ったことを思い出し、GUI作りは後回しにして、朝までの4時間で〇×記号の学習モデルを作成することに、当日第1部の予定を変更。

「〇」記号は、ETLデータベースにあったような気がしたので、まずはこちらから。

ETL1の「48」フォルダに1423枚のお宝画像が入っていた!

解凍? してあったETL文字データベースの文字・記号が入ったフォルダを一つずつ開けて内容を確認。「48」のフォルダ内に目的の画像を発見。これが1423枚もあれば、訓練用データとしては十分だろうと思い、このデータを機械学習用に加工。

まず、すべてのファイルが連番になるよう、リネーム。

import os
import glob

path = r".\(Pathを指定)\maru"
files = glob.glob(path + '/*')

files = glob.glob(path + '/*')

for i, f in enumerate(files):
    # すべてのファイルを連番でリネームする
    os.rename(f, os.path.join(path, "maru"+'{0:04d}'.format(i) + '.png'))
ファイル名が連番になるようリネーム

次に「輝度反転」。

# 輝度反転
from PIL import Image
import numpy as np
from matplotlib import pylab as plt

for i in range(1423):

    # 画像の読み込み
    im = np.array(Image.open(r".\(Pathを指定)\maru"+r"\maru"+"{0:04d}".format(i) + ".png").convert("L"))

    # 読み込んだ画像は、uint8型なので 0~255 の値をとる
    # 輝度反転するためには、入力画像の画素値を 255 から引く
    im = 255 - im[:,:]

    print(im.shape, im.dtype)

    #保存
    Image.fromarray(im).save(r".\(Pathを指定)\maru"+r"\r_maru"+"{0:04d}".format(i) + ".png")
輝度を反転

さらに、二値化する。
もしかしたら、上の輝度を反転させた画像のまま、機械学習を実行してもいいのかも? とチラっと思ったが、一度、最も極端な方向(=二値化で白黒にする)に振ってみて実験し、その結果を見てから判断することに決めて、二値化を実行。

import cv2
import os
import glob

path = r".\(Pathを指定)\maru_nichika"
files = glob.glob(path + '/*')

for f in files:
    # 読み込み
    im = cv2.imread(f)

    # グレースケールに変換
    im_gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)

    # 大津の二値化
    th, im_gray_th_otsu = cv2.threshold(im_gray, 0, 255, cv2.THRESH_OTSU)

    # 書き込み
    cv2.imwrite(f, im_gray_th_otsu)
二値化

二値化した画像中に訓練用データとして不適切な画像がないか、念のため、チェックしたところ、いくつかの不適切なデータを発見したため、それらは削除した。

訓練用データとして、不適切と思われる画像その①(いちばん左の画像は複数枚存在する)
訓練用データとして、不適切と思われる画像その②

これで「〇」記号の訓練用データは完成。次は「×」記号。

残念ながら、「×」記号のデータはETL文字データベースにはないようだ・・・。しかし、代替できそうなデータを「43」のフォルダに発見。それは「+」記号。これを45度ほど右か左へ回転させてあげれば、「×」に見えるんじゃないか? と・・・。

「+」記号を1444枚発見!

画像の回転スクリプトは・・・

from PIL import Image
import os
import glob

path = r".\(Pathを指定)\batsu"
files = glob.glob(path + '/*')

for f in files:
    # ファイルを開く
    im = Image.open(f)

    # 回転
    im_rotate = im.rotate(45)

    # グレースケールへ変換
    img_gray = im_rotate.convert("L")

    # 画像のファイル保存
    img_gray.save(f)
「×」記号ではあるけど、倒れかかった十字架のようで、なんとなく違和感がある・・・。

普通の「×」記号は、「\」が短くて、「/」が長い。上の画像は、ことごとくそれが逆だから違和感を覚えるんだと気づき、さらに90度回転させる。

イイ感じ!

で、「〇」記号と同様に、リネーム & 輝度反転させて、二値化。

八角形になっちゃったデータが複数あるので、これは全部削除した。

次は、Lobeで機械学習を実行。「〇:maru」と「×:batsu」だから「mb」という名前のフォルダを作成。「〇」記号はフォルダ名を半角数字の「0:ゼロ」、「×」記号はフォルダ名を半角数字の「1」に設定(認識結果の正解ラベルが 0 or 1 で返るようにするため)。

正解ラベル名のフォルダを作成して、訓練データをその中へコピー。

データが準備できたので、Lobeを起動。機械学習を実行。最終的に用意できた訓練データは「〇」記号が「1406」、「×」記号が「1323」。ここまで、なんだ・かんだで3時間半。さらに待つこと30分。東の空が明るくなる頃、ついに「〇×」記号の学習モデルが完成した。シャワーを浴びて出勤。さぁ 今日も第2部の始まりだー☆

6.〇×記号の解答も自動採点

プログラムの中では、次のようにして、採点対象を切り替えている。

  strScrList.Add('    if 黒の面積 > 1.5:');  # 白->黒へ訂正(20230306)
                          ・・・画像ファイルへのPathを設定等・・・
  strScrList.Add('        if os.path.isfile(img):');
                              ・・・画像ファイルを開く・・・
  if cmbAS.Text='○×' then
  begin
    strScrList.Add('            if outputs["label"] == "0":');
    strScrList.Add('                var1.Value = str("○") + "," + ・・・ 
    strScrList.Add('            elif outputs["label"] == "1":');
    strScrList.Add('                var1.Value = str("×") + "," + ・・・ 
    strScrList.Add('            else:');
    strScrList.Add('                var1.Value = str("Unrecognizable")');
    strScrList.Add('        else:');
    strScrList.Add('            var1.Value = str("Could not find image file")');
    strScrList.Add('    else:');
    strScrList.Add('        var1.Value = str("XXX")');
  end;

  if cmbAS.Text='ア行' then
  begin
    strScrList.Add('            if outputs["label"] == "0":');
    strScrList.Add('                var1.Value = str("ア") + "," + ・・・
    strScrList.Add('            elif outputs["label"] == "1":');
    strScrList.Add('                var1.Value = str("イ") + "," + ・・・
    strScrList.Add('            elif outputs["label"] == "2":');
    strScrList.Add('                var1.Value = str("ウ") + "," + ・・・
    strScrList.Add('            elif outputs["label"] == "3":');
    strScrList.Add('                var1.Value = str("エ") + "," + ・・・
    strScrList.Add('            elif outputs["label"] == "4":');
    strScrList.Add('                var1.Value = str("オ") + "," + ・・・
    strScrList.Add('            else:');
    strScrList.Add('                var1.Value = str("Unrecognizable")');
    strScrList.Add('        else:');
    strScrList.Add('            var1.Value = str("Could not find image file")');
    strScrList.Add('    else:');
    strScrList.Add('        var1.Value = str("XXX")');
  end;

正解を「〇」記号として、自動採点してみた結果は・・・

何とも理解に苦しむ摩訶不思議な採点結果が2個あるが、その他は良好と言っていい結果になった。

空欄であるにもかかわらず、正解となっている画像をよく調べてみると・・・

画像の中に小さなL字型のシミを発見

高さが30未満である場合は、輪郭検出しない設定のはずなんだが・・・。他には何にも見つけられないので、原因はコレしか考えられない。いったいナニがどうなっているんだろう??? 結局、コレは謎のままに。

同じデータに対して、正解を「×」記号として自動採点すると・・・

10個めのデータが呪われている気が・・・

10個目のデータの切り抜き画像を調べてみると・・・

微妙なトコロで、画像が欠けている・・・

どうやら元画像の「色が薄い」 or 「画像の線が太い」と問題が発生する傾向が強い気がしてきた。僕はこの実験に「えんぴつ」を使ったが、普通、試験時解答に使うのはシャーペンだから線が太くなることはあまり考えられない、むしろ、なるべく濃く書くことを注意事項に入れるべきかもしれない。なお、幅が狭くなっているように見えるのは、画像を強制的に幅64×高さ63にリサイズしているためだ。

「アイウエオ」同様、「〇×」記号の自動採点も残念ながらヒトの最終チェックがどうしても必要だという結果になった。が、こちらも「採点補助」程度には使えるぞ。

7.FormCreateでPythonEngineを初期化

何度も実験していると、プログラム起動後、初回の自動採点実行時、Python Engineの初期化に数秒を要するところを何とかしたくなってきた。これは起動後、毎回必ず発生する現象なので、マウスカーソルを待機状態にするとか、そういうレベルで誤魔化せる話ではない。なるべくユーザーの気づかないところで(ソッと)初期化してしまわなくてはならない。

いちばんイイのはプログラム起動時だ。マークシートリーダーを作った時にもこのことが気になったため、スプラッシュ画面を表示して(画像は自前で準備した画像ではなく、Webで販売している画像を購入して使用するという暴挙に出た)、その裏側で初期化作業を行うよう設定。今回も、このやり方を踏襲。

(1)初期化に使う画像をリソースに準備

Python Engineを初期化するには画像が必要なので、専用画像をリソースに準備。

心をこめて製作したmaru.png
マークシートリーダー用のPython Engine初期化用画像もまだ残ってた!

(2)初期化処理を実行

プログラム起動時、FormCreate手続きの中で、次のように初期化処理を実行。

まず、リソースに埋め込んだ初期化用画像ファイルを再生。

    //リソースに読み込んだ初期化用ファイルを再生

    //ファイルの位置を指定
    strFileName:=ExtractFilePath(Application.ExeName)+'imgAuto\tmp\maru.png';

    //ファイルの存在を確認
    if not FileExists(strFilename) then
    begin
      //リソースを再生
      with TResourceStream.Create(hInstance, 'pngImage_1', RT_RCDATA) do
      begin
        try
          SaveToFile(strFileName);
        finally
          Free;
        end;
      end;
    end;

次に、Python Engineそのものを初期化。

    //embPythonの存在の有無を調査
    AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-64';

    if DirectoryExists(AppDataDir) then
    begin
      //フォルダが存在したときの処理
      PythonEngine1.AutoLoad := True;
      PythonEngine1.IO := PythonGUIInputOutput1;
      PythonEngine1.DllPath := AppDataDir;
      PythonEngine1.SetPythonHome(PythonEngine1.DllPath);
      PythonEngine1.LoadDll;
      //PythonDelphiVar1のOnSeDataイベントを利用する
      PythonDelphiVar1.Engine := PythonEngine1;
      PythonDelphiVar1.VarName := AnsiString('var1');
      //初期化
      PythonEngine1.Py_Initialize;
    end else begin
      //MessageDlg('Python実行環境が見つかりません!',mtInformation,[mbOk], 0);
      PythonEngine1.AutoLoad := False;
    end;

最後に初期化用画像を読み込んで、1回だけ自動採点を実行する。

    //スプラッシュ画面を表示してPython Engineを初期化
    try
      theSplashForm.Show;
      theSplashForm.Refresh

      //Scriptを入れるStringList
      strScrList := TStringList.Create;
      //結果を保存するStringList
      strAnsList := TStringList.Create;

      try
        strScrList.Add('import json');
        ・・・略(自動採点用のPythonスクリプトをStringListに作成)・・・

        //0による浮動小数除算の例外をマスクする
        MaskFPUExceptions(True);
        //Execute
        PythonEngine1.ExecStrings(strScrList);
        
        //先頭に認識した文字が入っている
        if GetTokenIndex(strAnsList[0],',',0)='○' then
        begin
          //ShowMessage('The Python engine is now on standby!');
          theSplashForm.StandbyLabel.Font.Color:=clBlue;
          theSplashForm.StandbyLabel.Caption:='The P_Engine is now on standby!';
          theSplashForm.StandbyLabel.Visible:=True;
          Application.ProcessMessages;
          //カウントダウン
          for j:= 2 downto 1 do
          begin
            theSplashForm.TimeLabel.Caption:=Format('起動まであと%d秒', [j]);
            Application.ProcessMessages;
            Sleep(1000);
          end;
        end else begin
          ShowMessage('Unable to initialize python engine!');
          MessageDlg('Auto-scoring is not available!'+#13#10+
          'Please contact your system administrator.',mtInformation,[mbOk],0);
        end;

      finally
        //StringListの解放
        strAnsList.Free;
        strScrList.Free;
      end;

    finally
      theSplashForm.Close;
      theSplashForm.Destroy;
    end;

これで「自動採点GroupBox」内の「実行」ボタンをクリックした際の処理が、ほぼ待ち時間なしで行われるようになった。これをやっておくのと、おかないのとでは、プログラムの使用感がまったく異なってくる・・・。上記のプログラムの for j := 2 downto 1 do 部分を「ムダ」だと思う方もいらっしゃるかもしれませんが、「画像の使用権を購入」してまで表示したスプラッシュ画面なので、せめて2秒間だけ!必要以上に長く表示させてください・・・。

8.まとめ

準備に2ヵ月を要したが、なんとか手書きカタカナ文字の自動採点まで到達。結果は自分的には概ね満足できるものであったが、「実用に適するか」という点では、まだまだブラッシュアップが必要。今回の実験で得たことは、学習モデルを適用する「文字画像の切り抜き精度」の重要性。Lobeで作成した学習モデルは間違いなく優秀。その性能を遺憾なく発揮させる「場」を、僕は準備・提供しなければならない。これこそが今後の課題。

あいん つばい どらい
唯 歩めば至る・・・

コトここに至ってようやく・・・
これは、とほーもないチャレンジだと気づいたけれど。

もう行くしか ない 。
僕も、プログラムも、きっともっとよくなれる。

よくなるんだ!

9.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

本記事内で紹介させていただいた実験結果は、あくまでも私自身が用意した文字データに対してのものであり、別データで実験した場合、同様の結果が得られることを保証するものではありません。

Recognize handwritten katakana characters No,2

手書きカタカナ文字をPCに認識させる(その②)

前回の記事では、ETL文字データベースのカタカナ5文字(アイウエオ)+独自に収集した手書きカタカナ文字(アイウエオ各450~650文字)を元に機械学習で作成した学習モデルを用いて、答案の解答欄に書かれた手書きカタカナ1文字(ア~オのいずれか)の識別に挑戦。自分なりに最善を尽くしたと判断した段階での実際の正解率は91%・・・。

今回は、前回とは別の方法で再チャレンジ。前回と同じデータでテストして、正解率95%を達成。自分で言うのもなんだけど、これなら自動採点も可能なんじゃないか・・・と。

【今回の記事の内容】

1.分類器を検索
2.Lobeを使う
3.tflite形式で書きだす
4.書きだしたtfliteファイルをDelphiで・・・(泣)
5.Pythonで再チャレンジ
6.正解率95%!
7.まとめ
8.お願いとお断り

1.分類器を検索

どうしても手書き答案の自動採点をあきらめきれない僕は、昨日も「Delphi 分類器」をキーワードにGoogle先生にお伺いをたてた。実は「分類器」なる言葉を知ったのは最近のことで、これまで機械学習関連の検索キーワードとしてこの言葉を使ったことがなく、これでヒットするページは「ほぼ既読のリンク表示にならない」ので、当分の間、このキーワードで情報を得ようと考えたのだ。

また、Pythonではなく、Delphiとしたのは、2022年の年末からほぼ1か月間、Python関連の機械学習(ライブラリはTensolflowとkerasを使用・言うまでもなくスクリプトはもちろん、ほぼ全部写経!)で手書き文字の自動採点を実現しようと試みたが、どう頑張っても期待したような結果が出せず、とりあえずPythonスクリプト以外の「新しい情報」が「もしあれば」そちらも探してみようと思ったのだ。新しい情報があって、それがDelphi関連なら、すごく・・・ うれしいから。

で、検索すると次のページがトップに表示された。

Tensorflowで数字の分類器を構築する方法

https://blogs.embarcadero.com/ja/how-to-build-a-digit-classifier-in-tensorflow-ja/

「著者: Embarcadero Japan Support  2021年11月09日」ってコトは ・・・

(へぇー! DelphiでもMNISTできるんだ。知らなかったー!!)

(しかも、日付がどちらかと言えば 最近!)

急に興味関心が湧いて、しばらく記事を読んでみる。記事によれば、現在 TensorFlow LiteがDelphiで利用可能とのこと(気分的には TensorFlow Super Heavy の方がマッチするんだけど、残念ながらそれはないようだ)。4年前にPythonでやったのと同じ、マウスで画面に数字を書いて、それが0~9の何なのかを判定するプログラムの画像が掲載され、「プロジェクト全体をダウンロードしてテストすることができます。」とある。

( なつかしいなー あの時はGUI作りにPyQtを使って・・・ 動かすのに苦労したなー )

( 今は数字じゃなくて、オレ、「ア」 って書きたいんだけど・・・ )

( ・・・てか、肝心の学習モデルはどうやって作ってるんだろう? )

なんだかドキドキしてきた!
ページのいちばん下には「下記のリンクにアクセスしてサンプルコードをダウンロードし、実際に試してみてください。」という、うれしい案内が。

思わず、小学生のように、笑顔で、元気よく、「はぁーい」と答えたくなる。

( MNISTは別にして、学習モデルだけでもどうなってるか、確認してみよう・・・ )

結果的には、これが大正解。その存在すら知らなかった「Lobe(ローブ)」に巡り合うきっかけになろうとは・・・。

早速、リンク先からサンプルコードをダウンロード・・・

できませんでした(号泣)

404 This is not the web page you are looking for.

僕の人生は七転八倒。こんなコトには慣れっこさぁ T_T

こんなときのラッキー キーワードはもちろん「 TensorFlow-Lite-Delphi 」
Google先生、たすけてー!

今度は見事にヒット!

Embarcadero / TensorFlow-Lite-Delphi Public archive

https://github.com/Embarcadero/TensorFlow-Lite-Delphi

DLも無事成功! プログラムソースの学習モデルの指定部分を探すと・・・

//DCUnit1.pasより一部を引用

procedure TForm1.Recognize;
var
  i, X, Y: DWORD;
・・・
begin
  try
    var fModelFile := 'mnist3.tflite';
    case rdModel.ItemIndex of
      0: fModelFile := 'mnist.tflite';
      1: fModelFile := 'mnist1.tflite';
      2: fModelFile := 'mnist2.tflite';
      3: fModelFile := 'mnist3.tflite';
    end;

fModelFileとあるから、まずコレが学習モデルの代入先で・・・
んで、入れてるのが『mnistX.tflite』??? あんだ? コレあ?

拡張子 tflite から想像して、学習モデルは TensorFlow Lite で作成したモノのようだ・・・
Pythonの機械学習では見たことない気がするけど。

もし、この tflite 形式で、手書きカタカナ文字の学習モデルが作成できれば・・・
お絵描き部分は、共用可能だから・・・
マウスで「ア」って書いて、PCに「これ、なぁーに?」って!! きけるカモ ↑

コレだ。コレだ。コレだ。コレだ。コレだ。僕は、コレを待っていたんだ!!

即、Google先生にお伺いをたてる。

「Delphi tflite 書き込み」 ポチ!

すると、検索結果の上から3番目くらいに、

[Lobe] Lobeで作成したモデルをTensorflow Lite形式で …

というリンクを発見。

( Lobeってナンだか知らんけど、これで学習モデルが作れる・・・ のかな? )

( で、Tensorflow Lite形式でモデルの保存ができるの・・・ かな? )

とりあえず、クリックだぁ!!

*(^_^)*♪

2.Lobeを使う

リンク先の記事・その他を読んでわかったことは、まず、Lobeは「Microsoftによって公開されている機械学習ツール」であるということ。

Lobe公式サイト

https://www.lobe.ai/

さらにそれは、無料でダウンロードでき、しかも完全にローカルな環境で、コードを1行も書かずに機械学習を実現する、夢のような分類器(ツール)らしい。

(こんなのがあったのか! まるで知らなかった・・・)

Lobeの使い方を紹介したWebサイトの記事を片っ端から読んで、だいたいの作業の流れを理解。次に示すような感じで、自分でも実際にやってみた。

Lobeを起動して、まず、タイトルを設定。

タイトルは、これしかないでしょう!

importするのは、もちろん画像なんだけど・・・

僕の場合は、Datasetがよさげです!

Datasetの説明に Import a structured folder of images. とあるから、これはつまり、「ア」の画像データは、それだけをひとつのフォルダにまとめておけば、フォルダ名でラベル付けして、それをひとつのデータセットとして読み込んでくれるってコト?

「ア」を入れるフォルダ名は、最初「a」にしようかとちょっと思ったけれど、そうすると昇順の並びが「aeiou」になることを思い出し、躊躇。

やっぱり、ここは、後できっとLoopを廻すであろうことを予想して、

「ア」→フォルダ名「0」
「イ」→フォルダ名「1」
「ウ」→フォルダ名「2」
「エ」→フォルダ名「3」
「オ」→フォルダ名「4」

とすることに決定。

モノは試し、最初は様子だけ見てみようということで、データセットはとりあえず自分で集めた手書き文字だけにして実験することに決めたんだけど、ここでデータセットの整理を思い立つ。

・・・というのは、オリジナル手書き文字画像データを作る際に、元の画像から切り抜いたカタカナ文字画像をETL文字データベースのETL1及びETL6の画像サイズに合わせ、幅64・高さ63に成形するPythonのスクリプトを書いたんだけど、その中にはガウシアンフィルタをかけても取り切れないようなシミや黒点がある画像がかなりあること(汚れがすごく目立つ画像は、ひとりで大我慢大会を開催し、「これは修行なんだ」と自分に言い聞かせて、1枚ずつペイントでそれなりにキレイにしたんだけど、それでもまだ汚れの目立つ画像がいくつも残っていた)。

ただ、文字情報とは関係のない黒い点なんかは、多少あった方が過学習を防止するのに役立つカモ(?)という観点から、小さなシミのある画像は敢えてそのままにしたものもそれなりにある。このへんは画像を見た感じでテキトーに判断(理論的なコトは勉強していないので、まったくわかりません)。

それと最初に書いた画像成形のスクリプトが不完全で文字の一部が欠けてしまった画像も若干含まれていることなど、今、ちょっと冷静になって振り返ってみると、自分では慎重に処理を進めてきたつもりでも、やはり無我夢中でやってると、その時々は気づかなかった「さらに良くすることができた・より良くすべきだった」見落としがポロポロあり、ここで、ようやく僕は、見落とし箇所の改善を思い立ったのだ。

例えば・・・シミや汚れ取りは(ある文字の一部を部分的に拡大)

左がシミと汚れがある画像 / 右がクリーニング(手作業)後の画像

※ 実際には、この程度のシミと汚れは過学習防止用に敢えて残した画像も多数あり。

機械学習の事前準備処理の中で、学習用データとして使用する画像に、ガウスぼかしをかけたり、二値化したりして、上の左の画像に見られるようなごく薄い汚れは自動的に消えるから、すべての画像について徹底的にクリーニングする必要はないと思うのだけれど、ただ、明らかに濃度の高いシミ等は、なるべく消しておいた方が真っ白な気持ちで学びを開始するキカイに、少しはやさしいかな・・・みたいな気もするし *^_^*

反面、学習させたいデータ(例えば「ア」)とは直接関係のない黒いシミがある画像が多少は混じっていたほうが、過学習が起こりにくいのかなー? みたいな気もするし・・・

パラメータが利用できる場合は、ドロップアウトを何%にするかで、そういうことも含めて調整できるのかなー? みたいな気もするし・・・

ちゃんと勉強しなさい!という
神さまのドデカイ声が、力いっぱい
聴こえるような気もするケド・・・

『いったい、何が幸いするのか』まったくわからん。機械学習はほんとに難しい・・・などと、イロイロ考え(自分を誤魔化し)ながら、極端に大きなシミがある画像はとりあえずクリーニングし、また、これは不要と思われる文字(例:崩しすぎた文字、極端に小さな文字、ぼやけ・かすみの激しい文字等)を「テキトー」に削除した結果、ア~オ各文字のデータ数にばらつきが生じてしまった・・・。

たしか、MNISTだって各数字の総数はそろってなかったような気がするけど、今、僕が用意できたデータはMNISTの約1/100しかないから、質はともかく量的には明らかに不足しているはず・・・

今、手元にあるデータの数は・・・

ア:641
イ:653
ウ:652
エ:459
オ:575

「エ」がちょっと少ないのが気になると言えば、気になるけど、これしか集められなかったんだから仕方ない。せっかく集めたデータを利用しないのは嫌だし・・・。とりあえず、各文字の個数を水増しスクリプトで700に統一しようか・・・

データの水増しに使用するPythonスクリプトは、次のWebサイト様の情報を参照して作成じゃなくてほぼ写経(Pythonスクリプトの全容は、引用させていただいたWebサイト様の情報をご参照ください)。

ImageDataGenerator

https://keras.io/ja/preprocessing/image/#imagedatagenerator

薬剤師のプログラミング学習日記

https://www.yakupro.info/entry/digit-dataset
# ライブラリのインポートとパラメータの設定部分のみ
# Error
# from keras.preprocessing.image import ImageDataGenerator, load_img, img_to_array
# OK!
from keras_preprocessing.image import ImageDataGenerator, load_img, img_to_array

if __name__ == '__main__':
    generator = ImageDataGenerator(
        rotation_range=3,  # ランダムに回転する回転範囲
        width_shift_range=0.1,  # 水平方向にランダムでシフト(横幅に対する割合)
        height_shift_range=0.1,  # 垂直方向にランダムでシフト(縦幅に対する割合)
        #zoom_range=0.1,  # 文字のハミ出しを防止するため設定せず
        shear_range=0.5,  # 斜め方向に引っ張る
        fill_mode='nearest',  # デフォルト設定(入力画像の境界周りを埋める)
    )

    sample_num = 700   # 各ラベルの画像がこの数になるよう拡張する

これでア~オの各文字約700個ずつのデータセットができた!

Lobeを起動し、ImportからDatasetを選び、0(アが入っている)~4(オが入っている)のフォルダの親フォルダを指定する。

Choose Datasetをクリックするとフォルダ選択ダイアログが表示される

で、画像を Import すれば、あとは何にもしなくても、勝手に学習が始まるようだ。わずか(?)3500個のデータであるが、それなりに処理時間は必要(読ませた画像のサイズは幅64×高さ63で統一)。他のことをしながら処理が終わるのを待ったので、実際に何分かかったのか、定かではない(20~30分くらいか)。気がついたら終わっていた感じ。

表示が Training ⇨ Train になったら、終了 らしい。

学習結果は、次の通り。

自動で分類できなかったデータはわずか1%!

自動で分類できなかった文字は、ラベル付けして再学習も可能なようだが、今回はLobeが自動認識できなかった文字はそのままにして先へ進むことにした。

・・・と言うか、自動認識できなかった文字をクリックしてなんかテキトーにいじったら、その文字だけでなく、他の文字の処理も再び始まり(Train ⇨ Training に変化)、99%だった進行状況を表す数値がいきなりガクンと低下して、84%とかになってしまった。

自動認識できなかった文字は1%と言っても、数にすれば約30個あるから、その全てをこんなふうに再学習させたら、間違いなく日が暮れてしまう・・・。中には「コレが ア なら、7も1」、「雰囲気が『ア』ですー」みたいな文字も混じっているから、先へ急ぎたい僕は(本格的な再学習は次の機会に行うことにして)学習モデルの書き出し処理を優先することにしたのだ。

3.tflite形式で書きだす

Training が完了したら、次のように操作して学習モデルを tflite 形式で書き出し。

Useをクリック!
Export ⇨ TensorFlow Lite をクリック
任意のフォルダを指定して、Exportをクリック
普通は右側を選ぶのかなー?(右を選んだ場合、最適化に結構時間がかかります)
書出し処理中の画面(Just Exportを選択した場合)
書出し終了の画面
指定したフォルダ内に書き出されたファイル
exampleフォルダ内に書き出されたファイル

なんか、ものすごくかんたんに、tflite 形式で学習モデルができちゃったけど。
これでイイのかなー???

それから、ちょっと気になったので、tflite_example.py の内容をエディタでチラ見。

# tflite_example.pyの一部を引用

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Predict a label for an image.")
    parser.add_argument("image", help="Path to your image file.")
    args = parser.parse_args()
    dir_path = os.getcwd()

    if os.path.isfile(args.image):
        image = Image.open(args.image)
        model = TFLiteModel(dir_path=dir_path)
        model.load()
        outputs = model.predict(image)
        print(f"Predicted: {outputs}")
    else:
        print(f"Couldn't find image file {args.image}")

outputs = model.predict(image) ・・・ってコトは、学習モデルにイメージ(=画像)を predict(=予測)させ、outputs に代入(=出力)してるから、

うわー すごいおまけがついてる!

コレもあとから試してみよう!

※ 自分の中では、tflite形式で出力された学習モデルをDelphiから直接呼び出して文字認識を実行する方が、この時はあくまでも優先でした。

4.書き出したtfliteファイルをDelphiで・・・(泣)

早速、出来上がった(書き出された) saved_model.tflite ファイルを、DelphiのDebugフォルダにコピー。

saved_model.tfliteファイルの表示部分までの切り抜き(この他にもファイルあり)

んで、コードを書き替えて・・・

//コードを書き換えた部分
procedure TForm1.Recognize;
var
  ・・・
  //fOutput: array [0 .. 10 - 1] of Float32;
  fOutput: array [0 .. 5 - 1] of Float32;
  ・・・
begin
  ・・・
  try
    {var fModelFile := 'mnist3.tflite';
    case rdModel.ItemIndex of
      0: fModelFile := 'mnist.tflite';
      1: fModelFile := 'mnist1.tflite';
      2: fModelFile := 'mnist2.tflite';
      3: fModelFile := 'mnist3.tflite';
    end;}
    var fModelFile := 'saved_model.tflite';
    case rdModel.ItemIndex of
      0: fModelFile := 'saved_model.tflite';
      1: fModelFile := 'saved_model.tflite';
      2: fModelFile := 'saved_model.tflite';
      3: fModelFile := 'saved_model.tflite';
    end;

この他には、関係ありそうな箇所は見当たらないから、きっとこれで準備OK! *^_^*

実行して、「ア」の上の「つ」部分を描いたところでマウスの左ボタンを離す・・・と、

うわーん!( T_T )

君の見ている風景は・・・
どこまでも すべてが 涙色

君を悲しませるもの
その理由は もう 聞かないよ・・・

それでも僕は・・・
Delphi きみが大好きだ!

※ ファイルどうしの依存関係とか、よくわからんけど、もしかしたらそれがあるカモと考え、saved_model.tflite と同じフォルダに書き出されてた labels.txt や signature.json もDebugフォルダ内へ全部コピーして実行しても同じ結果でした。

イロイロ調べてみると、tflite 形式のファイルにもイロイロあるようで・・・

同じ .tflite のファイルでも違いがいろいろ:メタデータまわりについてTeachable Machine、Lobe、TensorFlow Hub等で出力した画像分類用のものを例に

https://qiita.com/youtoy/items/e58c02c1e32c56358d03

たぶん、saved_model.tflite と同じフォルダに書き出されてた labels.txt や signature.json の内容が tflite ファイル内に必要なんじゃないかなー。よくわかんないけど。

tfliteファイルを編集する知識なんて、僕にあるわけないし・・・
(ちょっと調べてみたら、PythonでTF Lite SupportのAPIを利用して、ラベル等の情報をtfliteファイル内に追加することができるようなんだけど、回り道が長すぎる気が・・・)

いずれにしても、どこかしら邪な、このチャレンジは失敗。

 ちなみに labels.txt の内容は・・・

0
1
2
3
4
ちなみに signature.json の内容は

{
  "doc_id": "9276cb74-46aa-435d-9edd-c0dcfa978a77", 
  "doc_name": "aiueo", 
  "doc_version": "fb48bd091c2950039e3841e1204230f2", 
  "format": "tf_lite", 
  "version": 45, 
  "inputs": {
    "Image": {
      "dtype": "float32", 
      "shape": [null, 224, 224, 3], 
      "name": "Image"
  }
} みたいな感じで、よくわかりません(以下、略)

5.Pythonで再チャレンジ

Delphiで tflite ファイルを直接読み込んでの文字認識に失敗して、すぐに思い出したのは先に見た『 tflite_example.py 』

PythonのスクリプトをDelphiのObject Pascal に埋め込んで実行することなら僕にもできるから、tflite_example.py で学習モデルがまだ見たことのないカタカナ文字画像の認識に成功すれば、最終的な目標の実現は可能だ。

すごいまわり道になりそうだけど、Delphiから直接読み込めるように tflite ファイルを編集する方法だってまだ残されている。ただ、僕はものごとを理解するのが遅く、学習には普通のヒトの何倍もの時間が必要だから、これはいよいよとなった時の最終手段だ。

SDカードに入れたWinPythonとAtomエディタで、僕は持ち運べるPython実行環境を作っている。Atomが開発中止になってしまったのはちょっとイタいけど、取り敢えずPythonスクリプトを書いて、実行するのに今のところ何ひとつ不自由はない。そのSDカードへLobeが書き出したファイルをフォルダごとコピーする。

Atomを起動。今、コピーしたexampleフォルダを開く。オリジナルの tflite_example.py をコピーして tflite_example2.py を作成。Atomに入れたパッケージ「script」から実行できるようにスクリプトを少し変更。exampleフォルダ内に次の画像を用意して・・・

用意した手書きの「ア」画像(Wordで作成)

スクリプトをクリックしてアクティブにしておいて、Shift + Ctrl + B で実行・・・

# 実行結果

Predicted: {'predictions': [
{'label': '0', 'confidence': 0.9855427742004395}, 
{'label': '3', 'confidence': 0.008924075402319431}, 
{'label': '1', 'confidence': 0.005291116423904896}, 
{'label': '4', 'confidence': 0.00012611295096576214}, 
{'label': '2', 'confidence': 0.0001159566527348943}]}
[Finished in 14.759s]

学習モデルが予測したラベルは「0」つまり「ア」、信頼性は99%!
やった。成功だ。待ちに待った瞬間が、ついに訪れた・・・ ありがとう Lobe!

次々に画像を変えて実験。

# 実行結果

Predicted: {'predictions': [
{'label': '1', 'confidence': 0.8973742723464966}, 
{'label': '4', 'confidence': 0.10129619389772415}, 
{'label': '2', 'confidence': 0.0012468534987419844}, 
{'label': '3', 'confidence': 4.6186032705008984e-05}, 
{'label': '0', 'confidence': 3.642921365099028e-05}]}
[Finished in 3.313s]
# 実行結果

Predicted: {'predictions': [
{'label': '2', 'confidence': 0.9924760460853577}, 
{'label': '1', 'confidence': 0.0038044601678848267}, 
{'label': '0', 'confidence': 0.0017367065884172916}, 
{'label': '3', 'confidence': 0.0010746866464614868}, 
{'label': '4', 'confidence': 0.0009080663439817727}]}
[Finished in 13.591s]
# 実行結果

Predicted: {'predictions': [
{'label': '3', 'confidence': 0.9999231100082397}, 
{'label': '1', 'confidence': 7.657476089661941e-05}, 
{'label': '4', 'confidence': 2.250336166298439e-07}, 
{'label': '0', 'confidence': 7.755971154210783e-08}, 
{'label': '2', 'confidence': 6.385280215681632e-08}]}
[Finished in 15.323s]
# 実行結果

Predicted: {'predictions': [
{'label': '4', 'confidence': 1.0}, 
{'label': '3', 'confidence': 1.7214372288743007e-11}, 
{'label': '1', 'confidence': 4.185582436200264e-12}, 
{'label': '0', 'confidence': 8.478809288784556e-14}, 
{'label': '2', 'confidence': 4.801435060631208e-14}]}
[Finished in 13.506s]

すべて、正解 ・・・

なんだか、こころがカラッポになった。
そう、夢が叶う瞬間は、いつも・・・

6.正解率95%!

チャレンジの総仕上げとして、前回の実験では正解率91%だった手書きカタカナ「アイウエオ」画像37セットをLoopで判定し、認識結果を「アイウエオ」で出力できるよう、スクリプトを準備。

結果を信じて、実行。

# 実行結果

ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
イ:×
イ:〇
エ:×
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
イ:×
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
イ:×
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
イ:×
エ:〇
オ:〇
ア:〇
イ:〇
エ:×
エ:〇
オ:〇
エ:×
イ:〇
ア:×
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
ア:〇
イ:〇
エ:×
エ:〇
オ:〇
ア:〇
イ:〇
ウ:〇
エ:〇
オ:〇
[Finished in 18.27s]

正解率をExcelで計算。

全体の95%を正しく認識できた!

手書きアイウエオ画像185枚のうち、176枚を正しく認識できた。
これなら、自動採点に使える。
とうとう、やった。

夢の実現へ、大きく一歩を踏み出せた!

7.まとめ

無料で利用できるOCR技術を利用した手書き文字認識は、自分が試した範囲では、現状まだ実用には程遠い感触であった。

そこで、次に、手書き文字の座標を輪郭検出で取得し、文字を矩形選択して、解答欄のスキャン画像中から切り抜いて画像データ化、Web上に大量に情報が溢れているPythonライブラリ(TensorFlow+keras)を使用した機械学習で処理して学習モデルを作成(読み取り対象文字はアイウエオの5文字に限定)、この学習モデルを使っての文字認識にチャレンジしたが、学習データ数及びパラメータ設定を様々に工夫しても実際の検証データに対する正解率は91%より上昇することはなく、最終的な目標としていた自動採点に繋げることはできなかった。

そこで、今回は情報の収集範囲を広げて再チャレンジ。Lobeという分類器の存在を知る。このLobeに約3500枚(総文字数)の手書きカタカナ画像を読ませて tflite 形式の学習モデルを作成。これに前回の実験で使用したのと同じ手書きカタカナ文字(アイウエオの5文字 × 37セット=185枚)を見せたところ、95%の文字を正しく認識することができた。

この結果より、今後、より多くの良質な機械学習用手書き文字画像を集め、Lobeを利用して全自動で学習モデルを生成、さらにLobeが自動分類できなかった画像の質をチェックし、もし必要と判断される場合はラベル付けして追加学習を行い、より良くトレーニングされた学習モデルを準備できれば、最終的に人が必ずチェックするという条件の元で、機械と協働しての答案の自動採点は十分可能であると、僕は感じた。

なぜ、それを実現したいのか?
僕の中で、その理由はひとつしか、ない。
地位も、名誉も、富も、その前で、輝きを失う言葉に、僕は巡り合えたからだ。

The purpose of life is to contribute in some way to making things better.
人生の目的は、ものごとを良くすることに対して何らかの貢献をすることだ。

Robert F. Kennedy

DelphiやPythonと力を合わせれば、夢見たことを実現できる。そして、僕がひとりで夢見たことが、本当に、本当になったとして、それが僕自身だけでなく、偶然でもかまわないから・・・、知らない人でもいい、僕でない、他の誰かのために、もし、役立ったなら・・・、その時、こんな僕の拙く幼い学びにも、そこに、初めて「意味」や「価値」が生まれるんだ、と・・・。僕は本気で、そう信じている。

採点プログラムへのチャレンジは、再生紙に印刷したマークシートを、複合機のスキャナーでスキャンして電子データ化、このスキャン画像から、ほぼ100%正しくマークを読み取れるマークシートリーダーを作ることから始まった(完成したプログラムは任意の選択肢数を設定可能な一般用の他、選択肢の最大数を記号-、±、数字0~9、文字A~Dの計16 として「読み取り結果を抱き合わせての採点も可能」な数学採点用途にも対応)。開発当初は読み取りパラメータの最適な設定がわからず、読み取り解像度も高くする必要があったが、最終的にはパラメータ設定を工夫し、職場内の複数個所に設置されている複合機のスキャナーのデフォルト設定である200dpiの解像度で読み取ったJpeg画像でエラーなく稼働するものを実用化できた。これを職場のみんなに提供できた時、僕は本当に、心からうれしい気持ちになれた。人生の師と仰ぐ、ロバート・フランシス・ケネディの言葉をほんの少しだけ、僕にも実践できたかもしれない・・・と、そう本気で思えたからだ。

次に、マークシートとも併用可能な、手書き答案の採点ソフト作りにチャレンジした。最初は横書きの答案から始め、最終的には国語の縦書き答案も採点できるものに仕上げた。採点記号も 〇 や × だけでなく、負の数をフラグに使うことで、部分点ありの△も利用可能とし、コメント挿入機能や、現在採点している解答を書いた児童生徒の氏名も解答欄画像の左(or 右)に表示できるように工夫した(横書き答案のみ)。もちろん、合計点は自動計算。返却用の答案画像の印刷機能も必要十分なものを実装できた。採点作業が最も大変な、国語科7クラス分の答案を、午後の勤務時間内で全部採点出来たと聞いた時は、胸がたまらなく熱くなった・・・。

その次のチャレンジは、解答用紙の解答欄の自動認識機能の搭載だった。手書き答案採点プログラムを使うユーザーを見ていて、いちばん強く感じたことは、PCに解答欄の位置座標を教えるため、解答欄の数だけ矩形選択を繰り返さなくてはならない採点準備作業を何とかして低減、せめて半自動化できないか・・・ということだった。ここではOpenCVの優秀な輪郭検出器に巡り合い、点線を活用するなど解答欄の作成方法を工夫することで、全自動とまではいかないが、取り敢えず解答用紙中の全矩形の位置座標を自動取得し、必要な解答欄矩形の座標のみ、ユーザーが取捨選択できるプログラムを作成・提供できた。

そして、今、僕は、手書き・カタカナ1文字(アイウエオ限定)の自動採点にチャレンジしている・・・。

Ask and it will be given to you.

この言葉を信じ、失敗の山を築きながら、

次はきっと・・・

誓って自分に言い続けて。

生きるちからを失くしたときが、このチャレンジの終わり。
でも、僕は、僕がこの世から消えたあとも、
動くプログラムを作るんだ・・・

正直、今回、最終的に自分でやったことはアイウエオの画像データの準備だけ・・・みたいな感じになっちゃったけど、結局、Lobeとの出会いがすべてだったけれど、もし、途中で夢をあきらめていたら、絶対にLobeには出会えなかった・・・。

さぁ 次はアイウエオ限定の自動採点機能の実装だ。
Delphiが笑顔で、僕を待ってる・・・

8.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。また、本記事内で紹介させていただいた実験結果は、あくまでも私自身が用意した文字データに対してのものであり、別データで実験した場合、同様の結果が得られることを保証するものではありません。

Rectangle Detector

矩形検出器

手書き答案をスキャナーで画像化して採点するソフトを書いた。概ね、思った通りにカタチになったが、解答欄の位置座標を取得するのに、解答欄の数だけ、その左上隅から右下隅へマウスでドラッグする作業を繰り返さなくてはならない。(もし、これが自動化できたら・・・) そう思って書いたのが、このプログラム。

1.矩形の検出方法
2.字数制限のある解答欄の作り方
3.GUIはDelphiで作成
4.矩形検出器の使い方
5.まとめ
6.お願いとお断り

1.矩形の検出方法

キーワードを『矩形 検出』にしてGoogle先生にお伺いをたてると、思った通りOpenCVを活用する方法がいくつもヒットする。しかも、そのほとんどすべてがPythonでの活用方法だ。Delphi用のOpenCVもあるようだけれど、次の理由から矩形の検出はPython用のOpenCVで行うことにした。

Pythonを使う利点は、まず、何と言っても、情報が豊富なことだ。マイ・プログラミング環境では、わからないことはすべてGoogle先生に教えてもらうしかないので、情報が入手しやすいことは、他のすべてに優先する。

(メインの開発環境がDelphiなのは、上記の内容と大いに矛盾しますが・・・)

さらに、手書き答案の採点ソフトより前に、マークシートリーダーを作った時、マーク欄の座標を得るために、やはりPythonとOpenCVのお世話になった。マークシートリーダーも、手書き答案の採点ソフトも、embeddable pythonに入れたOpenCVと一緒のフォルダに詰め込んでユーザーに配布しているから、Pythonを内包して使う環境は既に完成済み。PythonのスクリプトをDelphiのコードに埋め込んで、PythonForDelphiを使って実行する方法は勉強済みだから安心。Delphi用のOpenCVは、情報も少ないし、何よりその使い方がわからない・・・。

他人様に使っていただくプログラムはDelphiで書くけれど、自分専用のToolはPython環境を利用して作ることが多い。ちょっと特別なことをしたい時、Pythonはとても便利だ。いろいろ紆余曲折はあったけれど、現在はSDカードにWinPythonとAtomエディタを入れて持ち運べるPython環境を作っている。

そのSDカードに入れたPython環境で、いつものようにAtomを起動し、Web上にあったいくつものScriptをコピペして試してみる。

まず、OpenCVで「ハフ変換」なるものを利用する例だが、ハフ変換はノイズの除去で苦労しそうだ。ノイズの発生源が多数存在する解答用紙の矩形検出でパラメータを適切に設定することが果たしてできるだろうか? 経験がない自分にはちょっと厳しそうだ。

次に、LSD(Line Segment Detectorの略とのこと)という直線検出器を試した。試した瞬間、(もう、これしかない!)と思うほど、これは凄かった。使い方も超カンタンで、LSDをこれでもか!とばかりに並べるだけ。

from pylsd.lsd import lsd
Mylines = lsd(picture)

【検出結果】

LSDで検出できた矩形の例

さらに驚くべきことに、こういう作業には付き物の引数も一切ない。つまり、パラメータを調整する必要など『ない』ということなのだろう・・・。ただ、LSDはそのライセンス形態がAGPLであると知り、使用を断念。MITやBSDでないと自分的にはやはり困る・・・。

最後に試したのが、OpenCVのfindContours関数。これを使うには前処理として、まず、画像をグレースケールに変換し、さらに白黒反転させて二値化しなければならない。

import cv2
import numpy as np
from PIL import Image

# Pillowで画像ファイルを開く(全角文字対応の確認用にファイル名は「ひらがな」)
pil_img = Image.open("./img/さんぷる.jpg")
# PillowからNumPyへ変換
img = np.array(pil_img)

# グレースケールに変換する
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 白黒を反転
gray = 255 - gray
# 2値化する
ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)

Pillowで画像ファイルを開いているのは、OpenCVのimread関数が日本語(全角文字)に対して拒絶反応を示すので、これを回避するため。もし、ファイル名とそこまでのPathに全角文字が含まれないという確実な保証があるなら、次のようにしてもいいようだ。これなら1行で済む。

# 8ビット1チャンネルのグレースケールとして画像を読み込む
img = cv2.imread("全角文字のないPathと画像ファイル名", cv2.IMREAD_GRAYSCALE) 

で、準備が出来たらfindContours関数を使って輪郭を検出する。

# すべての輪郭を同じ階層として取得する
contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

解答欄には、その性格上、小さな矩形が多く使われることが多いので、閾値以下の面積の矩形は削除する。※ 閾値は整数型の数値で指定する。

# 閾値以下の面積の矩形(小さい輪郭)は削除
contours = list(filter(lambda x: cv2.contourArea(x) > 閾値, contours))

よりスムーズに作業するためには、予め、小さな矩形を消去した機械読み取り用の解答欄(解答用紙)をヒト用の解答用紙のコピーから作成し、これを用いて解答欄座標を取得した方がよい(国語の縦書き解答用紙は、ワープロソフトではなく、表計算ソフトで作成する方法が業界では一般的らしいので、機械読み取り用の解答用紙はそれほど手間をかけなくても、カンタンに作成できる・・・はず)。

解答欄矩形をちゃんと認識できているか・どうかを確認するため、検出した輪郭を描画する。このPythonのスクリプトをDelphiのObject Pascalに埋め込んで実行する際は、ここが最大の「見せ場」になる。検出した矩形をグラブハンドル付きのラバーバンドで表示する方法は後述。

# 検出した輪郭を描画する
cv2.drawContours(img, contours, -1, color=(0, 0, 255), thickness=2)

最後に解答欄矩形の座標を取得する(これが最終的な目標)。取得した座標は、採点順になるよう、並べ替えて表示する(並べ替え方法は後述)。

# 矩形の座標を表示(左上の座標, 右下の座標)
for i in range(len(contours)):
    x, y, w, h = cv2.boundingRect(contours[i])
    print(str(x)+','+str(y)+','+str(x+w)+','+str(y+h))

数値より、画像(絵)で見た方がわかりやすいのは言うまでもない。

# 保存
cv2.imwrite('./img/lined.jpg', img)
# 画像を表示
cv2.imshow("Image", img)
# キー入力で終了
cv2.waitKey()
画像を表示して、解答欄矩形の取得状況を確認

ここまでの Python Script をまとめて示せば、次の通り。

import cv2
import numpy as np
from PIL import Image

# Pillowで画像ファイルを開く
pil_img = Image.open("./img/さんぷる.jpg")
# PillowからNumPyへ変換
img = np.array(pil_img)

# グレースケールに変換する
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 白黒を反転
gray = 255 - gray
# 2値化する
ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)

# すべての輪郭を同じ階層として取得する
contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

# 閾値以下の面積の矩形(小さい輪郭)は削除
contours = list(filter(lambda x: cv2.contourArea(x) > 数値, contours))

# 検出した輪郭を描画する
cv2.drawContours(img, contours, -1, color=(0, 0, 255), thickness=2)

# 矩形の座標を表示(左上の座標, 右下の座標)
for i in range(len(contours)):
    x, y, w, h = cv2.boundingRect(contours[i])
    print(str(x)+','+str(y)+','+str(x+w)+','+str(y+h))

# 保存
cv2.imwrite('./img/lined.jpg', img)
# 画像を表示
cv2.imshow("Image", img)
# キー入力で終了
cv2.waitKey()

OpenCVのfindContours関数を使って検出した輪郭(=解答欄の矩形)の例。
(解答用紙画像はLSDを試した時と同じものを使用)

矩形を検出しやすいように作った解答用紙なら、この結果はまさに『ブラボー!』

解答用紙中の ■ や □ を検出しないよう、検出下限の閾値を設定したこともあり、期待した通りの満足できる結果が得られた。OpenCVのハフ変換や、LSDでは日本語に対する反応が見られたが、findContours関数は(適切な閾値を設定してあげれば)日本語に反応しないようだ。

答案の「答」には「口」、問にも「口」、漢字にはたくさんの矩形が使われている。適切な閾値を設定することで、誤認識を減らせることも理想的。

【実験してみた!】

閾値を「700」として、□ に対する反応を実験して確認した。結果は次の通り。

26×26=676、28×28=784 だから・・・機械は正確に反応している

28ポイントの「□」から反応するが、40ポイントの「問」には無反応。通常使用される解答用紙であれば、フォントの大きさに制限を設ける必要性はなさそう。

もう少し細かい矩形を使った解答用紙で、閾値700で実験すると・・・

解答欄の矩形をさらに細かく分割したサンプルを作成してテスト
解答欄の番号の矩形に反応してしまう・・・

閾値1400までは・・・

解答欄の番号の矩形に反応するが

閾値を1500にすると・・・

解答欄の番号の矩形には反応しなくなる☆

少し、細かい矩形を用いた解答用紙であれば、閾値1500くらいから試せば狙った通りに解答欄の座標だけを取得することができそうだ。

閾値に上限を設定すれば、さらに良い結果を得られるかも・・・と思ったが、数学の解答用紙には他の教科ではあり得ない巨大な矩形が普通に使用される。矩形を取得できなければ、検出器とは言えない。さらに、解答欄全体を一つの大きな矩形として認識してしまうのはプログラムの性格上、絶対に回避できないから、閾値の上限は設けずに、むしろ、不要な矩形の座標を削除しやすいプログラム(GUIを作成)を書けばいいと気づく。

さらに、ユーザーが矩形座標の編集(修正)を自由にできるようにプログラムを工夫すれば、理想的な矩形検出器ができるはず。

これでDelphiでGUIを作成する際の方向性も見えてきた。

2.字数制限のある解答欄の作り方

解答欄の矩形を検出する上で、大きなハードルになるだろうと予想していたのが『字数制限が設定された解答欄』。

機械読み取り用に作成した解答用紙であっても・・・

上の解答用紙は、ヒト用の解答用紙の問題番号部分にあった小さな矩形を消去して、機械読み取り処理用に作成した解答用紙。この状態で矩形を検出(閾値1500)すると・・・

それでも削除しなければならない矩形座標が多すぎ・・・

閾値を「3100」に設定して、ようやく・・・

閾値をどんどん大きくすれば、何とかなることはわかった!

閾値を大きく設定すれば、何とかなることは上の例でわかったが、閾値を大きくすれば当然必要な解答欄の座標を取得できなくなる可能性も生じてくるわけで・・・。

ところが別の国語用解答用紙を処理している際に、閾値を気にせずに字数制限のある解答欄を作成する良い方法があることを偶然発見。それは・・・

罫線に「点線」を利用した解答用紙

字数制限を設定したり、完全解答で正解としたい解答欄は内側の罫線を点線にする!

閾値「700」で実験した結果

これなら問題2の(1)・(2)が作る大きな矩形の座標のみ削除すればOK!
点線を活用することで、一番大きな問題を難なくクリアできることが判明。
やったー☆

【embeddable Pythonのバージョンとインストールしたライブラリの一覧】

Python 3.9.9

Package Version
numpy 1.21.5
opencv-python 4.5.4.60
Pillow 9.3.0
pip 22.3.1
setuptools 60.1.0
wheel 0.37.1

3.GUIはDelphiで作成

取得した解答欄の座標を編集するGUIはDelphiで作成。最終的にはこうなった。

検出した矩形の確認と編集を行うGUIはDelphiで作成

画面下の「操作」グループ内のVCLを左から右へ順にクリックして行けば、解答用紙画像から解答欄の矩形が取得・表示できる仕組み。

左から右へ順に操作して解答欄矩形の座標を取得する。

取得した解答欄矩形の座標は、画面右上に一覧形式で採点順に表示されるようにプログラミングした。

取得した座標の一覧を表示

横書き答案が指定された場合は、y座標の値が昇順になるよう並べ替え(y座標が同じなら、x座標でさらに昇順に並べ替え)。

縦書き答案が指定された場合は、x座標の値が降順になるよう並べ替え(x座標が同じなら、y座標でさらに昇順に並べ替え)。

こうすれば、座標の並び方が「ほぼ採点する順番になる」はず。なお、並べ替えはカンマで区切った解答欄矩形の座標を入れたStringListを対象として実行(解答欄数は多くても100未満のはず・・・だから、並べ替えの速度はまったく考えていない)。そのアルゴリズムは次の通り。まず、グローバルに使う変数、ソート用のプロパティと関数を準備。

  private
    { Private 宣言 }
    x1,x2:integer;
    y1,y2:integer;
    //Pythonから送られたデータを保存する
    strAnsList:TStringList;

var
  Form1: TForm1;

type TSStyle = (ssText,ssInteger);
var
  //ソート用のプロパティ
  fAscending : Boolean;
  fIndex : Integer; //項目番号
  fStyle : TSStyle; //テキストか整数か

implementation

uses
  System.UITypes;
function GetCommaText(aStr:String; aIndex:Integer):string;
  var
    subList:TStringList;
begin
  subList := TStringList.Create;
  subList.Delimiter := ',';
  subList.DelimitedText := aStr;
  Result := subList.Strings[aIndex];
  subList.Free;
end;
function MyCustomSort(List: TStringList; Index1, Index2: Integer): Integer;
begin
  case fStyle of
    ssText:begin
      Result:=CompareText(GetCommaText(List.Strings[Index1],
      fIndex),
      GetCommaText(List.Strings[Index2],fIndex));
    end;
    ssInteger:begin
      //一重ソート
      //Result:=StrToInt(GetCommaText(List.Strings[Index1],fIndex))
      //          -StrToInt(GetCommaText(List.Strings[Index2],fIndex));
      //二重ソート
      Result:=StrToInt(GetCommaText(List.Strings[Index1],fIndex))
                -StrToInt(GetCommaText(List.Strings[Index2],fIndex));
      if Result=0 then
        //-1することで1番目の項目がソートキーになる
        Result:=StrToInt(GetCommaText(List.Strings[Index1],fIndex-1))  
                  -StrToInt(GetCommaText(List.Strings[Index2],fIndex-1));
      if fAscending then
      begin
        Result:=Result*-1;
      end else begin
        Result:=Result*1;
      end;
    end;
  else
    //これを入れておかないとコンパイラが警告を表示する
    Result:=0;
  end;
end;

で、「解答欄座標を取得」ボタンがクリックされたら、PythonForDelphiを通じてPythonのScriptを内部的に実行して座標を取得し、上記関数を呼び出して並べ替えを実行、結果をMemo2に表示する。

procedure TForm1.btnGetSquareClick(Sender: TObject);
var
  //PythonのScriptを入れる
  strScrList:TStringList;
  //Pythonから送られたデータを保存する -> グローバル変数化
  //strAnsList:TStringList;
  //Sort
  i:integer;
  strFileName:string;
  strList:TStringList;
begin
  //初期化
  Memo1.Clear;
  //Scriptを入れるStringList
  strScrList:=TStringList.Create;
  //結果を保存するStringList
  strAnsList:=TStringList.Create;

  try
    //Python Script
    strScrList.Add('import cv2');
    strScrList.Add('import numpy as np');
    //strScrList.Add('img = cv2.imread("./ProcData/sample2.jpg")');
    strScrList.Add('img = cv2.imread(r"./ProcData/'+ExtractFileName(StatusBar1.SimpleText)+'")');
    strScrList.Add('gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)');
    strScrList.Add('gray = 255 - gray');
    strScrList.Add('ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)');
    strScrList.Add('contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)');
    strScrList.Add('contours = list(filter(lambda x: cv2.contourArea(x) > '+cmbThreshold.Text+', contours))');
    strScrList.Add('for i in range(len(contours)):');
    strScrList.Add('    im_con = img.copy()');
    strScrList.Add('    x, y, w, h = cv2.boundingRect(contours[i])');
    strScrList.Add('    var1.Value =str(x)+","+str(y)+","+str(x+w)+","+str(y+h)');
    //Scriptを表示
    Memo1.Lines.Assign(strScrList);
    //Execute
    PythonEngine1.ExecStrings(Memo1.Lines);
    //結果を表示
    Memo2.Lines.Assign(strAnsList);
  finally
    //StringListの解放
    strAnsList.Free;
    strScrList.Free;
  end;

  strFileName:=ExtractFilePath(StatusBar1.SimpleText)+'Temp.csv';
  Memo2.Lines.SaveToFile(strFileName);

  strList := TStringList.Create;
  try
    for i := 0 to Memo2.Lines.Count-1 do
    begin
      strList.Add(Memo2.Lines[i]);
    end;
    //fAscending := True; //昇順で
    fAscending := False;
    fIndex := 1; //2番目の項目を
    fStyle := ssInteger; //整数型でソート
    strList.CustomSort(MyCustomSort); //ソート
    //データ抽出
    Memo2.Clear;
    for i := 0 to strList.Count - 1 do
    begin
      //Memo2.Lines.Add(GetCommaText(strList.Strings[i],fIndex));
      Memo2.Lines.Add(strList[i]);
    end;
  finally
    strList.Free;
  end;

end;

上記のアルゴリズムは、次のWebサイトに紹介されていた情報を元に作成。
カンマ区切りのデータの並べ替えは初めて行った。採点順に座標を並べたかったので、プログラムコードをよく読んで、二重ソートになるよう工夫した。
貴重な情報を投稿してくださった方に心から感謝申し上げます。

[delphi-users:1175] カンマ区切りのデータの並べ替え

https://groups.google.com/g/delphi-users/c/Ck2mQXNFTvw

4.矩形検出器の使い方

ここまでの操作で解答欄の座標はすべて取得できたはずなので、不要な矩形のデータをいかに効率よく削除するかを主眼に、GUIの操作方法を考えた。

まず、取得できた座標データの先頭にセットフォーカスし、そのデータが示す矩形を赤いラバーバンドで囲んで表示する。ユーザーは、ラバーバンドで囲まれた矩形を見て、その要・不要を判断。

この矩形は不要

不要な矩形であった場合は、「編集」ボタンをクリック。不要なデータを自動で選択状態に設定。

Memoの一行全部を選択状態に設定

手続きは次の通り。

procedure TForm1.BitBtn1Click(Sender: TObject);
var
  i:integer;
begin

  //行番号をLines[i]で取得
  i:=StrToInt(LBRow.Caption)-1;

  EditTF:= not EditTF;
  if EditTF then
  begin
    BitBtn1.Caption:='編集中';
    BitBtn1.Font.Color:=clRed;
    Memo2.ReadOnly:=False;
    btnSave.Enabled:=False;

    //i行目の文字全てを選択状態にしたい場合
    //先頭にカーソルをセット
    Memo2.SelStart:=Memo2.Perform(EM_LINEINDEX, i, 0);
    //全ての文字を選択
    Memo2.SelLength:=Length(WideString(Memo2.Lines[i]));
    //Memo2.Perform(WM_VSCROLL,SB_TOP,0); //先頭にスクロール

  end else begin

    BitBtn1.Caption:='編 集';
    BitBtn1.Font.Color:=clBlack;
    Memo2.ReadOnly:=True;
    Memo2.SelStart:=SendMessage(Memo2.Handle,EM_LineIndex,i,0);
    btnSave.Enabled:=True;
    Memo2Click(Sender);

  end;

  //SetFocus
  Memo2.SetFocus;

end;

Delete or Backspaceキーで不要なデータを削除すると同時に、Memoの行も削除する。で、ボタンを「編集」(=意味的には「編集したい場合はクリックせよ」)に戻す。次のデータをラバーバンドで囲む。この一連の動作がすべて自動的に流れ作業で行われるように手続きを作成。

コードは次の通り。

procedure TForm1.Memo2KeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
var
  LineNo:integer;
begin
  //現在、カーソルがある行を取得
  LineNo:=Memo2.Perform(EM_LINEFROMCHAR, UINT(-1), 0);
  //空欄なら行を削除
  if Memo2.Lines[LineNo]='' then
  begin
    Memo2.Lines.Delete(LineNo);
  end;
  //表示
  GetLinePos;
  if not EditTF then
  begin
    Memo2Click(Sender);
  end else begin
    BitBtn1Click(Sender);
  end;
end;
procedure TForm1.GetLinePos;
var
  CurPos,Line:Integer;
begin
  with Memo2 do
  begin
    CurPos:=SelStart;
    Line:=Perform(EM_LINEFROMCHAR, CurPos, 0);
    //LBRowは現在フォーカスがある行番号を表示するラベル
    LBRow.Caption:=Format('%d', [Line+1]);
    LBRow2.Left:=LBRow.Left+LBRow.Width;
    LBRow2.Caption:='行目';
  end;
end;
procedure TForm1.Memo2Click(Sender: TObject);
var
  i:integer;
  p1,p2:TPoint;

  function RemoveToken(var s:string;delimiter:string):string;
  var
    p:Integer;
  begin
    p:=Pos(delimiter,s);
    if p=0 then Result:=s
    else Result:=Copy(s,1,p-1);
    s:=Copy(s,Length(Result)+Length(delimiter)+1,Length(s));
  end;

  function GetTokenIndex(s:string;delimiter:string;index:Integer):string;
  var
    i:Integer;
  begin
    Result:='';
    for i:=0 to index do
      Result:=RemoveToken(s,delimiter);
  end;

begin

  if not EditTF then
  begin

    //座標を取得
    i:=Memo2.Perform(EM_LINEFROMCHAR, Memo2.SelStart, 0);

    //エラー対策
    if Memo2.Lines[i]='' then Exit;

    x1:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',0));
    y1:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',1));
    x2:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',2));
    y2:=StrToInt(GetTokenIndex(Memo2.Lines[i],',',3));

    if Assigned(plImage1) then begin
      FreeAndNil(plImage1);
    end;

    //コンポーネントを生成し,イベントを定義し,位置を指定して画像を表示
    plImage1:=TplResizeImage.Create(Self);
    plImage1.Parent:=ScrollBox1;
    plImage1.TransEvent:=True;
    //クライアント座標をスクリーン座標へ変換
    //GetSystemMetrics(SM_CYCAPTION) -> タイトルバーの高さ
    //GetSystemMetrics(SM_CYFRAME) -> ウィンドウの枠幅
    p1.X:=x1-(GetSystemMetrics(SM_CYFRAME) div 2);
    p1.Y:=y1-GetSystemMetrics(SM_CYCAPTION)-(GetSystemMetrics(SM_CYFRAME) div 2);
    p2.X:=x2-(GetSystemMetrics(SM_CYFRAME) div 2);
    p2.Y:=y2-GetSystemMetrics(SM_CYCAPTION)-(GetSystemMetrics(SM_CYFRAME) div 2);
    p1:=Image1.ClientToScreen(p1);
    p2:=Image1.ClientToScreen(p2);
    plImage1.SetBounds(p1.X, p1.Y, p2.X-p1.X, p2.Y-p1.Y);

    //SelectedプロパティをTrueにするとラバーバンドとグラブハンドルが表示される
    plImage1.Selected := True;
    plImage1.BringToFront;

  end;

end;

ラバーバンドはMr.XRAYさんのWebサイトにあったplResizeImageを使わせていただいて作成。これまでにもどれだけ助けていただいたことか・・・。このような素晴らしい素材を提供し続けてくださっているMr.XRAYさんに今回も心から感謝申し上げます。

157_移動リサイズ可能な TImage   ラバーバンドとグラブハンドル

http://mrxray.on.coocan.jp/Delphi/plSamples/157_MoveResize_GrabHandle.htm

ラバーバンドで囲まれた矩形が必要な矩形であった場合は、下のMemo3へ「移動」ボタンをクリックしてデータを移す。で、次の矩形をラバーバンドで囲んで表示する。

次の矩形の要・不要を判断
必要な矩形であれば下のMemo3へ移動する

この作業を順次繰り返すと、最終的に必要な矩形の座標のみがMemo3に移動。不要な矩形の座標はすべて削除されることになる。

必要な矩形の座標のみ、採点順に取得できた!

最終的に過不足がないか・どうか、Memo3の先頭座標データをクリック、ラバーバンドで該当矩形を囲んで表示、下向きの矢印キーを次へ次へと押して、フォーカスを下の座標データへ移動、ラバーバンドを表示して確認、これを最後の座標データまで繰り返し。

採点順を含めて、必要な座標データがすべて揃っていることを先頭データから順に確認する。

必要な座標がすべて取得できていることを確認したら、「保存」ボタンをクリックして手書き答案採点ソフトが実行時に読み込む、様々な採点設定を記録するための iniファイルに解答欄の座標データを保存する。

データの保存

【任意の範囲を指定したい場合】

複数の解答欄を抱き合わせて、完全解答で正解としたい場合などに対応するため、任意の範囲を矩形選択できるようにした。

画面中央左の追加ボタンをクリックすると、画面の中央にラバーバンドが表示される。これを任意の位置へドラッグする。

追加ボタンをクリックしてラバーバンドを表示
画面の中央にラバーバンドを表示、これを任意の位置へドラッグ。

ボタンのCaptionは、自動で「取得」に変更。

ボタンのCaptionを変更

任意の範囲をラバーバンドで囲んだら(=範囲指定完了)、「取得」ボタンをクリック。取得された座標がボタンの右のEditに表示され、同時にクリップボードへ送られる。

任意の範囲を指定して座標を取得

Memo3上の「追加」ボタンをクリックすると、Memo3が編集可能になるので、採点順を確認して、適切な行に座標のデータを追加(クリップボードから貼り付けても、データを見ながら手動入力してもよい)。

適切な位置に座標のデータを入力する

ラバーバンドを使わなくても、解答欄の左上と右下を、それぞれポイントすればその座標をラベルに表示する機能も追加したので、上の図のように、Memo3を編集モードにして、座標を任意の行へ直接入力することも可能。

マウスでポイントした場所の座標をリアルタイムで表示する

クライアント座標の取得と表示を行う手続きは、次の通り。

procedure TForm1.Image1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  PtInput:TPoint;
begin
  //スクリーン座標を取得
  GetCursorPos(PtInput);
  //で、そのコントロールのクライアント領域に対するカーソルの座標を取得
  PtInput := Image1.ScreenToClient(PtInput);

  //補正する必要はない(確認済み)
  //表示
  Label2.Caption:=
    Format(' クライアント座標  '+'X : %d, Y : %d', [PtInput.X, PtInput.Y]);
end;

【矢印キーの押し下げを拾う】

最も難しかったのが、フォーカスが「どこにあるか」で矢印キーの挙動を制御すること。以前にStringGridのセルのフォーカスの移動を制限した時に学んだ内容が今回も役に立った。

今回は、Memoにフォーカスがある場合と、ラバーバンドにフォーカスがある場合、さらにラバーバンドにフォーカスがある場合のうち、Shiftキーと同時に矢印キーが押し下げられているのか(=ラバーバンドの大きさの変更)、それとも矢印キーが単独で押し下げられているのか(=ラバーバンドの表示位置の移動)、この3パターンを見分けてそれぞれにあった動作を行わせたいと考えた。最終的には次のコードで対応。

  private
    { Private 宣言 }

    //ある(矢印他)キーが押されたことを知る
    procedure AppMessage(var Msg: TMsg; var Handled: Boolean);

上のように手続きを宣言して、Shift+Ctrl+Cで手続きを生成。

procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean);
var
  StrText: string;
begin
  //何かキーが押し下げられたら
  if Msg.message = WM_KEYDOWN then
  begin
    try
      if ActiveControl is TMemo then
      begin
        //キー操作を「通常動作」にするおまじない
        case Msg.Message of
          WM_USER + $0500:
          Handled := True;
        end;
      end else begin
        //上位ビットが1ならShiftキーが押されている
        if GetKeyState(VK_SHIFT) and $8000 <> 0 then
        begin
          if plImage1.Visible then
          begin
            //右矢印キー
            if Msg.wParam=VK_RIGHT then
            begin
              plImage1.Width := plImage1.Width + 1;
              Msg.wParam:=0;
            end;
            //左矢印キー
            if Msg.wParam=VK_LEFT then
            begin
              plImage1.Width := plImage1.Width - 1;
              Msg.wParam:=0;
            end;
            //上矢印キー
            if Msg.wParam=VK_UP then
            begin
              plImage1.Height := plImage1.Height - 1;
              Msg.wParam:=0;
            end;
            //下矢印キー
            if Msg.wParam=VK_DOWN then
            begin
              plImage1.Height := plImage1.Height + 1;
              Msg.wParam:=0;
            end;
          end;
        end else begin
          //Shiftキーは押されていない
          //対象を限定(どちらでも動いた)
          //if TplResizeImage(ActiveControl).Visible then
          if plImage1.Visible then
          begin
            //右矢印キー
            if Msg.wParam=VK_RIGHT then
            begin
              plImage1.Left := plImage1.Left +1;
              Msg.wParam:=0;
            end;
            //左矢印キー
            if Msg.wParam=VK_LEFT then
            begin
              plImage1.Left := plImage1.Left -1;
              Msg.wParam:=0;
            end;
            //上矢印キー
            if Msg.wParam=VK_UP then
            begin
              plImage1.Top := plImage1.Top - 1;
              Msg.wParam:=0;
            end;
            //下矢印キー
            if Msg.wParam=VK_DOWN then
            begin
              plImage1.Top := plImage1.Top + 1;
              Msg.wParam:=0;
            end;
            //Deleteキー
            if Msg.wParam=VK_DELETE then
            begin
              //plImage1を解放
              if Assigned(plImage1) then begin
                FreeAndNil(plImage1);
              end;
              Msg.wParam:=0;
            end;
          end;
        end;
      end;
    except
      on E: Exception do
      begin
        StrText := E.ClassName + sLineBreak + E.Message;
        Application.MessageBox(PChar(StrText), '情報', MB_ICONINFORMATION);
      end;
    end;
  end;
end;

plImage1が生成されないうちに上の手続きが呼ばれると、当然、一般保護違反のエラーが発生するので、FormCreate時にplImage1を生成しておく。

procedure TForm1.FormCreate(Sender: TObject);
var
  //Python39-32へのPath
  AppDataDir:string;
  i:integer;
begin

  //メモリーリークがあれば検出
  ReportMemoryLeaksOnShutdown:=True;

  //有効にする(忘れないこと!)
  Application.OnMessage := AppMessage;

  //[Enter]でコントロールを移動させるために、Form上のコンポーネント
  //より先にFormがキーボードイベントを取得する。
  KeyPreview:=True;

  //コンポーネントを生成 -> インスタンス(実体)をつくる
  // = 一般保護違反エラーの防止
  //plImage1はグローバル変数として宣言しているから未定義の識別子エラーは発生しない
  //でも、Create(生成)してからでなければ使えない!
  plImage1:=TplResizeImage.Create(Self);

  //編集フラグ(編集中ではない)
  EditTF:=False;
  PlusTF:=False;
  Memo2.ReadOnly:=True;

  //StatusBar1の設定
  StatusBar1.SimplePanel:=True;

  //Formを最大化して表示(幅も最大化される)
  Form1.WindowState:=wsMaximized;

  //Embeddable Pythonの存在の有無を調査
  AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-32';
  if DirectoryExists(AppDataDir) then
  begin
    //フォルダが存在したときの処理    
    PythonEngine1.AutoLoad:=True;
    PythonEngine1.IO:=PythonGUIInputOutput1;
    PythonEngine1.DllPath:=AppDataDir;
    PythonEngine1.SetPythonHome(PythonEngine1.DllPath);
    PythonEngine1.LoadDll;
    //PythonDelphiVar1のOnSeDataイベントを利用する
    PythonDelphiVar1.Engine:=PythonEngine1;
    PythonDelphiVar1.VarName:=AnsiString('var1');  //プロパティで直接指定済み
    //初期化
    PythonEngine1.Py_Initialize;
  end else begin    
    PythonEngine1.AutoLoad:=False;
  end;

  //面積の閾値の選択肢を設定
  for i := 1 to 200 do
  begin
    cmbThreshold.Items.Add(IntToStr(i*100));
  end;

  //画面のちらつきを防止する
  DoubleBuffered := True;

end;

で、メモリーリーク発生の原因とならないよう、アプリの終了時に忘れずに解放。

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  //メモリーリークを防止する
  PythonEngine1.Py_Finalize;
  PythonDelphiVar1.Finalize;
  FreeAndNil(plImage1);
end;

5.まとめ

(1)矩形の検出は、OpenCVのfindContours関数を利用する。
(2)矩形の検出を回避するには「点線」を利用する。
(3)GUIはDelphiで作成し、必要な座標だけ保存できるように工夫。
(4)「フォーカスがどこにあるか」で矢印キーの動作を制御。
(5)コントロール生成のタイミングと確実な破棄にも注意する。

6.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Link Image click position with Grid control

「画像のクリック位置とグリッドコントロールの連動」

画像を表示したTImageの任意の1点をクリックしたら、そのY座標に応じてStringGridコントロールの適切なセルを選択するようなプログラムが書けないかなー。・・・と思って、実際に書いてみたら、あまりにも簡単に実現できてしまったお話。

1.やりたかったこと
2.作成したプログラム
3.まとめ
4.お願いとお断り

1.やりたかったこと

次のようなGUIのある採点プログラムを作成した。プログラムはスキャナーで読み込んだ複数枚の答案画像から、設問ごとに解答欄をかき集めて表示するもの(答案画像への書き戻しも可能)。で、採点作業をより一層効率的に行うには、各々の解答欄画像をクリックした際、自動的に採点結果を入力するGridコントロールのセルが選択されたらイイなーと思ったことが、この連動プログラムを書こうとしたきっかけです。

画像をクリックした時に得られるY座標を元に、Gridコントロールの最適なセルを選択したい!

2.作成したプログラム

まず、思ったことはTImageのOnMouseDownイベントを利用すればイイ(画像上でクリックした位置のX, Y座標が取得できる)ということです。早速、imgAnswerという名前をつけたTImageをクリックして選択し、OnMouseDownイベントの右側をダブルクリックして、imgAnswerMouseDown手続きを作成。

次のようなコードを書いてみました!

procedure TFormCollaboration.imgAnswerMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  //imgAnswerの高さは、解答欄1つの高さ × 解答用紙数 だから
  //クリックした場所の(Y座標 div 解答欄1つの高さ) + 1 が解答用紙の番号になるはず
  OutPutDebugString(PChar(IntToStr((Y div 解答欄1つの高さ) + 1)));   
end;

実行(F9)して、確認。

No,5の解答欄をクリックすると、確かに「5」と表示されている! やった☆

必要と思われる変数 i, j を宣言して・・・
あとは Gridコントロールへ SetFocus するコードを書くだけ!

procedure TFormCollaboration.imgAnswerMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  i,j:integer;
begin
  //imgAnswerの高さは、解答欄1つの高さ × 解答用紙数 だから
  //クリックした場所の(Y座標 div 解答欄1つの高さ) + 1 が解答用紙の番号になるはず
  OutPutDebugString(PChar(IntToStr((Y div 解答欄1つの高さ) + 1)));
  i:= (Y div 解答欄1つの高さ) + 1;
  //SetFocus
  j:= StrToInt(現在選択している解答欄の番号);
  StringGrid1.Col:= j;
  StringGrid1.Row:= i;
  StringGrid1.SetFocus;   
end;

上のコードでは、わかりやすさのため変数(記号)ではなく、そのかわりに「解答欄1つの高さ」等のように説明の文字列で記述しています。また、採点ミスを防止する観点から、Gridコントロールに表示する入力可能な列は、現在選択(=画像として表示)している解答欄に対応する1列のみとしています。

これで理想としていたカタチの採点プログラムに一歩近づけたように思います。ユーザーに案内したい採点方法は、サっと見て全員が良く出来ている設問であれば、最初に全員正解として得点を一括自動入力し、誤りの解答のみ、解答欄画像を見ながらチェックしてクリック、自動選択された採点欄のセルにゼロ(得点)を入力する方法です。

3.まとめ

案ずるよりナントカといいますが、ほんとうにその通りで、内心、ずっとできるかなー? なんて思っていたことが、案外、基本的な知識だけで簡単に実現できてしまいました!

//Integer 型で、割り算をしたいときは、整数除算の演算を行う div を使う
A div B  //A ÷ B の商が取得できる

あとは、やりたいことを実現するアルゴリズムを考えるだけですね!
プログラミングしていて、いちばんしあわせで、楽しい時間を過ごせました☆

【注意!】
画像の表示倍率の変更が伴う場合には、表示倍率に合わせてプログラムの変更が必要
です。完璧には動作しませんが、ある程度使える(と思われる)表示倍率の変更に対応するコードは次の通りです(クリック位置が画像の下へ行くほど、誤差が大きくなります)。

procedure TFormCollaboration.imgAnswerMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  i,j,w:integer;
  k:double;
begin
  //imgAnswerの高さは、解答欄1つの高さ × 解答用紙数 だから
  //クリックした場所の(Y座標 div 解答欄1つの高さ) + 1 が解答用紙の番号になるはず
  OutPutDebugString(PChar(IntToStr((Y div 解答欄1つの高さ) + 1)));
  i:= (Y div 解答欄1つの高さ) + 1;
  //表示倍率が100%の時
  if Edit1.Text='100' then
  begin
    i:=(Y div 解答欄1つの高さ)+1;  //このアルゴリズムは記録として残したい
  end else begin
    //表示倍率が100%ではない時
    k:= 解答欄1つの高さ * (StrToFloat(Edit1.Text)/100);
    i:=Ceil(Float(Y)/k);  // <- とりあえずこれで使えるカモ?
  end;

4.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

Mark Sheet Reader (Basic version)

「マークシートリーダーをつくる(基礎編)」

DelphiでGUIを作成、マークシート画像はPythonにインストールしたOpenCVとNumpyで読み取り&計算処理して、結果をMemoに表示するマークシートリーダーの練習プログラム。

0.準備
1.使用するプログラムとマークシート画像について
2.マークシート画像を読み込む
3.マークシート読み取り処理のアルゴリズム
4.マークシート読み取り処理の実際(Object Pascalのコード)
5.さらに進化
6.著作権表示の記載方法
7.お願いとお断り

ここで紹介している練習用プログラムを、実際の採点業務で使用できるようにした拙作マークシートリーダーです。

0.準備

マークシートリーダー作成にあたって、以下の事前準備が必要です。

・PythonForDelphiのインストール
・Embeddable Pythonのダウンロードと必要なライブラリのインストール
(作業後、このプログラムへの埋め込み用にフォルダ名を「Python39-32」に変えて、このプログラム(マークシートリーダー)のexeがある場所へコピーする)
・アプリケーションの表示画面のリサイズ対応(縦編)

(いずれも、当Blogの記事で過去に紹介)

重要 上の記事の手順で、OpenCVとNumpyをインストールしたEmbeddable Pythonが入ったフォルダを「Python39-32」という名前で、以下のフォルダ内にコピーする。

C:\Users\ xxx \ Project1.dprojファイルのあるフォルダ \Win32\Debug\

1.使用するプログラムとマークシート画像について

当Blogの過去記事『~主として「高さ」の変更に関する覚書~』で作成したDelphiのGUIをそのまま使用します。

必要なVCLとその構造(親子関係)

画面サイズの変更に対応できるよう、以下のコードを記述。

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.ExtCtrls, Vcl.Grids, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    Panel3: TPanel;
    Splitter1: TSplitter;
    ScrollBox1: TScrollBox;
    Image1: TImage;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Splitter1Moved(Sender: TObject);
  private
    { Private 宣言 }
    //Panel1の幅とFormの高さを記憶する変数
    intPH, intFH:integer;
    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TForm1 }

procedure TForm1.CMShowingChanged(var Msg: TMessage);
begin
  inherited; {通常の CMShowingChagenedをまず実行}
  if Visible then
  begin
    Update; {完全に描画}
    //Formの表示終了時に以下を実行
    Panel1.Height:=intPH;
    intPH:=Panel1.Height;
    intFH:=Form1.Height;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  //Panel1とFormの高さを記憶する変数を初期化
  intPH:=200;
  intFH:=480;
end;

procedure TForm1.FormResize(Sender: TObject);
begin
  //比率を維持してPanel1の高さを変更
  Panel1.Height:=Trunc(Form1.Height * intPH/intFH);
end;

procedure TForm1.Splitter1Moved(Sender: TObject);
begin
  //Panel1とFormの高さを取得
  intPH:=Panel1.Height;
  intFH:=Form1.Height;
end;

end.

マークシート画像は、以下の画像を使用。

「ms01.Jpg」

マークシート画像は、以下の場所に「MarkSheet」という名前のフォルダを作成して、その中に保存。

C:\Users\ xxx \ Project1.dprojファイルのあるフォルダ \Win32\Debug\Marksheet

2.マークシート画像を読み込む

Delphiを起動して、Project1.dproj(マークシート読み取り用GUIの保存してあるフォルダ内のDelphiのプロジェクトファイル)を開き、Panel3をクリックして選択しておいて、Panel3上にButton1を作成。Button1のNameプロパティはButton1のまま、Captionプロパティを「画像を表示」に変更。Button1の位置は下図を参照。

Captionプロパティを「画像を表示」に変更
Button1の位置は画面下・Panel3の左に寄せる

OpenDialog1をForm上に置く。

OpenDialogをダブルクリック
Form上のOpenDialog1

次に、Form上のButton1をダブルクリックして、procedure TForm1.Button1Click(Sender: TObject);を作成。

procedure TForm1.Button1Click(Sender: TObject);
begin

end;

作成した手続きではJpeg画像を扱うので、画面を上にスクロールして、implementation部の下に Vcl.Imaging.Jpeg を uses する。

implementation

uses
  Vcl.Imaging.Jpeg; //Jpeg画像を読み込む

{$R *.dfm}

Button1Clickプロシージャにvar宣言を追加して、Jpeg画像読み込み用の変数jpgを宣言。

procedure TForm1.Button1Click(Sender: TObject);
var
  jpg: TJPEGImage;
begin

end;

beginとend;の間に、以下のコードを記述。

  //OpenDialogのプロパティはExecuteする前に設定
  With OpenDialog1 do begin
    //表示するファイルの種類を設定
    Filter:='JPEG Files (*.jpg, *.jpeg)|*.jpg;*.jpeg';
    //データの読込先フォルダを指定
    InitialDir:=ExtractFilePath(Application.ExeName)+'MarkSheet';
  end;

  if not OpenDialog1.Execute then Exit;  //キャンセルに対応
  //オブジェクトを生成
  jpg := TJPEGImage.Create;
  try
    //読み込み
    jpg.LoadFromFile(OpenDialog1.FileName);
    //Image1に表示
    Image1.Picture.Assign(jpg);
  finally
    //オブジェクトを破棄
    jpg.Free;
  end;

上書き保存(Ctrl+S)して、実行(F9)。データの読み込み先を指定しておくと、目的のフォルダが一発で開くので便利。

マークシート画像が表示される。が、ごく一部しか見えない。

これはImage1のAutoSizeプロパティがデフォルトFalseに設定されているため。 Image1 のAutoSizeプロパティをTrueにするコードを追加(オブジェクトインスペクタで Image1 のAutoSizeプロパティを 直接指定してもOK)。

  try

    //読み込み
    jpg.LoadFromFile(OpenDialog1.FileName);
    //Image1に表示
    Image1.Picture.Assign(jpg);

    //追加
    Image1.AutoSize:=True;

  finally

上書き保存(Ctrl+S)して、実行(F9) 。画像の表示を確認する。

うまくいったように見える。Formを最大化してSplitterを下げて、さらに確認。
画像の表示位置を修正する必要がありそうだ

画像が表示される位置を、画面の左側へ移動するコードを手続きの先頭に追加する。

begin

  //Imageの表示位置を指定
  Image1.Top := 25;
  Image1.Left := 40;

  //OpenDialogのプロパティはExecuteする前に設定しておくこと
  With OpenDialog1 do begin

上書き保存(Ctrl+S)して、実行(F9) 。画像の表示を再度確認する。

ほぼイメージに近い出来栄え?

参考:画像読み込みのコード(全体)

implementation

uses
  Vcl.Imaging.Jpeg; //Jpeg画像を読み込む

{$R *.dfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
var
  jpg: TJPEGImage;
begin

  //Imageの表示位置を指定
  Image1.Top := 25;
  Image1.Left := 40;

  //OpenDialogのプロパティはExecuteする前に設定しておく
  With OpenDialog1 do begin
    //表示するファイルの種類を設定
    Filter:='JPEG Files (*.jpg, *.jpeg)|*.jpg;*.jpeg';
    //データの読込先フォルダを指定
    InitialDir:=ExtractFilePath(Application.ExeName)+'MarkSheet';
  end;

  if not OpenDialog1.Execute then Exit;  //キャンセルに対応
  //オブジェクトを生成
  jpg := TJPEGImage.Create;
  try

    //読み込み
    jpg.LoadFromFile(OpenDialog1.FileName);
    //Image1に表示
    Image1.Picture.Assign(jpg);

    //追加
    Image1.AutoSize:=True;

  finally
    //オブジェクトを破棄
    jpg.Free;
  end;

end;

3.マークシート読み取り処理のアルゴリズム

まず最初にマークシートの左上にある特徴点(マーカー)画像: ■■■(トリプルドット)をOpenCVのテンプレートマッチングで探す。

特徴点(マーカー)画像が見つかったら、 特徴点(マーカー)画像左上位置を基準にして、「マークシートの周囲の枠部分のみ」を矩形選択して切り出し。

参考①:あらかじめ測定しておいた特徴点(マーカー)画像の位置(単位はピクセル)
左上のX座標=65
左上のY座標=28
右下のX座標=121(マークシート矩形の座標計算には使用しない)
右下のY座標=43(マークシート矩形の座標計算には使用しない)

参考②:あらかじめ測定しておいたマークシート矩形の座標 (単位はピクセル)
左上の X座標=65
左上の Y座標=61
右下の X 座標=419
右下の Y 座標=497

参考 上記の各座標をマークシート画像から計測し、テンプレートとして用意したマークシートごとに登録(座標値を保存)するプログラムを別途作成した。なお、座標原点(0,0)は画像の左上である(使い慣れた数学の座標系とちょっと違うことに注意!)。

赤が左上、青が右下の座標で、緑がマークシート枠の矩形

この座標を元にして、 特徴点(マーカー)画像からの距離で、マークシート矩形を切り出す。

マークシート矩形において、(W1、H1)が左上位置を、(W2、H2)が右下位置を示す座標となる。

上の例では、マークシートの列数は「1」、行数は「10」と数えることにする。列数が「1」の場合、W1は「ほぼ0(ゼロ)」になり、値としての意味がないように思われるが、このプログラムを実用化した場合は、下の例のように、複数の列があるマークシートを用いることになるので、2列めのマークシート矩形の座標は、左上が(W3,H3)、右下が(W4,H4)、3列めのマークシート矩形の座標は左上が (W5,H5)、右下が(W6,H6)のように指定でき、W値が0ではない場合が生じる。

マークシート用紙の作成に、私はWordを用いたが、Wordのバージョンによっては、あろうことか、上書き保存時に、マーカー画像(■■■)の位置が数ミリ程度、勝手に左へ移動するという予期しないトラブル(Wordの仕様?)が発生。このような点も考慮して、W1の座標は敢えて(0として)定数化していない。

マークシートの作成例(実験用に使用)
列数3、1列あたりの行数25、1行あたりの選択肢の数は16
この用紙の場合、総マーク数は3×25×16=1200個/枚となる
つまり用紙1枚につき、1200回マークの有無の判定が必要

実際の作業では、マークシート画像をスキャナーで読み取って、グレースケールのJpeg画像としてデータ化するので、マークシート(用紙)に「しわ」があったり、状況によっては「折られ」ていたりする関係上、読み取り画像を1枚ずつ比較すると、その上下・左右にどうしても微妙なブレ・ズレが生じてしまう。しかし、同じ印刷機で、同時に印刷したマークシートであれば、特徴点(マーカー)画像とマークシートの行列位置の関係は絶対であり、これが1枚ごとに変化することはありえない。つまり、スキャンした画像が余程大きく傾きでもしていない限り、テンプレートマッチングで、特徴点(マーカー)画像さえ発見できれば、予め測定・記録しておいた座標の相対的位置関係からマークシート矩形は容易に切り出せる。

次の画像は、別データとして保存してある特徴点(マーカー)画像を元に、OpenCVのテンプレートマッチングをマークシート画像に対して行ったもの。類似度の高い部分を赤枠で囲んで示すようプログラミングしている。

マーカー
テンプレートマッチングを行った画像

次に、上に述べた方法で計算したマークシート矩形を列単位で切り出す。切り出した画像は、マークの(=列)数・行数の整数倍のサイズになるようリサイズする(これは、このあと画像を細かく分割して処理するので、切り出す行や列の計算を簡単にするための工夫 → 整数倍にリサイズすれば、列数分&行数分廻すLoop処理の中で処理しやすい)。

列単位で切り出したマークシート矩形

マークシート用紙は、一般的なマークシート用紙のような厚みのある(高級感あふれる)専用紙でなく、ホームセンターでも「売ってない!」ような見た目が灰色の再生紙を用いている。このためか、あちらこちらにゴミのような黒い点や、細いすじが入っていることがある。これらの黒点やすじを判定プログラムが「マークあり」と誤認しないようにするため、次に「平滑化(ボカシ)処理」を行う。

平滑化(ボカシ)処理には「ガウシアンフィルタ」を用いた。これは、正規(ガウス)分布を利用して「注目画素からの距離に応じて近傍の画素値に重みをかける」という処理を行うもので、自然な平滑化が実現できるとのこと。次の画像は、上の切り出したマークシート矩形に対して、この平滑化処理を行ったもの。

img = cv2.GaussianBlur(img,(35,35),0) ※引数は奇数を指定する必要がある

引数の値が大きいほど正規分布のピークが低く、広がりは広くなる(=より均一に、より全体にボカシがかかる)。ここでは引数をかなり大きめにとり「35」としている。こうすることで、ゴミやシミを画像からほぼ完全に除去できる。

ガウシアンフィルタ処理を行い、ゴミやシミを除去する

さらに、この画像を「ある閾値」を元に白と黒に二値化処理する。この処理で枠線やマークされていないマーク部分が「すべて白」になり、鉛筆で濃くマークされている部分だけが「黒」になった白黒画像が得られる。当初は、以下のように引数を指定して二値化画像を作成した。

ret, img = cv2.threshold(img, 140, 255, cv2.THRESH_BINARY)

現在は、次のように閾値の設定を自動で行う「大津の二値化」を利用している。

ret, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

式中の第2引数は閾値だが、大津の二値化では自動計算させるので0(ゼロ)を指定。第3引数は0-255の256段階でグレースケール化しているから、最大値の255を指定する。これによって、次の画像が得られる。

大津の二値化で作成した白黒画像

さらに、これを白黒反転させた画像を作成する。式は以下の通り。

img = 255 - img

これにより、次の画像が得られる。

マーク部分を「白」に変換した画像

次に、この画像を「行」単位に分割して切り出す。

1行目を切り出した画像

次に、選択肢の数で、均等に分割する。ここでは選択肢の数が「8」なので、上の画像を等幅で8個に分割する。下は、その1個目の切り出し画像である。

このように細かく分割して切り出した画像1つ1つについて、画素が白なら値を255・黒なら0として面積あたりの合計値を計算し、マークされている部分の面積の中央値を算出、これを閾値として、下の式では、マークされている(白い部分の)面積が他より3倍以上あるものを「マークあり!」と判定している。この数値が大きいほど、判定はきびしくなる。

result.append(area_sum > np.median(area_sum) * 3)

このマークシート読み取り処理のアルゴリズムの主要部分は全て、GitHubの次の記事に紹介されていたものです。素晴らしい記事を投稿してくださった作成者の方に、心から感謝申し上げます。

PythonとOpenCVで簡易OMR(マークシートリーダ)を作る

URL:https://qiita.com/sbtseiji/items/6438ec2bf970d63817b8

参考 列が複数あるマークシートの読み取り処理について

上記記事では、特徴点(マーカー)画像をマークシートの上下に複数個用意し、テンプレートマッチングを行っています。確かに、マークシートの左上と右下に特徴点(マーカー)画像を用意すれば、より簡単にマークシート矩形の切り出しが可能でした。これは素晴らしいアイデアです。

私も当初は特徴点(マーカー)画像を複数個用意してマークシートを作成していたのですが、列数を2列、3列と増やすと、さまざまな問題が生じることに気が付きました。

第一に、特徴点(マーカー)画像を変えないと、列ごとの切り出しが困難だということです。つまり、3列あるマークシートでは、最も左の列用の特徴点を■■■、真ん中の列用の特徴点を■□■、最も右側の列用の特徴点を■□□として、Loop処理の中でテンプレートマッチングに使用する特徴点(マーカー)画像を切り替えて、目的とするマークシート矩形を切り出せるようにしてみた(□□■や□□□も含めればさらに多くの列が作成可能)のですが、この方法では、うまく特徴点(マーカー)画像を認識してくれないことがあり、安定感に欠ける気がしました。

第二に、万一、回答者が特徴点(マーカー)画像に意図的に変更を加える(例: ■□□ → ■■□)等の暴挙に出た場合、対応が難しいこと。

第三に、マーカー画像が多いと、マークシートの見た目もなんだか騒がしくて、個人的にはマーカー画像を複数個用意する方法はなるべく避けたいと考えたこと。

これらの理由から、「なんとか特徴点(マーカー)画像が1個で済まないか」と、私なりに工夫して、当ブログで紹介した方法を考えました。

創意工夫の過程で一時は、回答者が意図的に変更できるようなマーカー(例: □ )がなければOKかとも思い、別の特徴点(マーカー)画像も使ってみたのですが、それはそれでまた別の問題を起こすことがわかりました。

例えば、下のように、ヒトなら簡単に両者の違いを判別できる画像を用意します。

用意した特徴点(マーカー)画像

これに対して、左側の画像でテンプレートマッチングを行うと・・・

機械はヒトと違うモノの見方をしていることが、大変良くわかりました。

4.マークシート読み取り処理の実際(Object Pascalのコード)

Form上に、Buttonを1つ、PythonForDelphi関連のVCLコンポーネントを3つ配置する。Button2は、Panel3の中央付近に置き、Nameプロパティはそのまま、Captionプロパティを「読み取り」に変更する。PythonForDelphi関連のVCLコンポーネントは、すべて非ビジュアルコンポーネントなので、位置はどこでもよく、Nameプロパティもデフォルトのままとする。 PythonForDelphi関連で配置するコンポーネントは以下の通り。

以下のように、PythonForDelphi関連のコンポーネントのプロパティとイベントを設定

・PythonEngine1のAutoLoadプロパティはFalseに設定。

・PythonEngine1のDllNameプロパティはpython39.dllを指定(埋め込みPythonのバージョンに合わせて設定する)。ここでは3.9.9以下のバージョンのPythonでないとNumpyが非対応(2021年12月現在)であり、用意した埋め込みPythonのバージョンは3.9.9なのでpython39.dllに変更する。

・PythonEngine1のIOにはPythonGUIInputOutput1を指定。

・PythonGUIInputOutput1は他で利用するならプロパティのOutPutに「Memo1」などとするところだけれど、ここでは何も設定しない。

・PythonDelphiVar1のVarNameはプログラムコードの記述に合わせて「var1」とする。var1と入力後、Enterで確定すること!(青く反転表示されるのを確認する)

Formが生成される時、PythonEngine1を初期化する。Formのタイトルバーの上をクリックして選択し、オブジェクトインスペクタのイベントタブをクリックしてOnCreateイベントの右に表示されている「FormCreate」をダブルクリックして、コードの入力に切り替える。

参考:エラー対応方法(20220724追加)

P4D使用時にImageコントロールの bsClear を使うとエラーが発生します。

[dcc32 エラー] Unit02_MSReader.pas(1199): E2010 'TBrushStyle' と 'Enumeration' には互換性がありません

これはPythonEngine.pasの中で bsClear が定義(使用)されているためです。次に示す例のように、Image1の方のbsClearを明示的に Vcl.Graphics.bsClear として対応します。

  //矩形を描画
  with Image1 do
  begin
    //Canvas.Brush.Style:=bsClear;
    Canvas.Brush.Style:=Vcl.Graphics.bsClear;
  end;

以上、エラー対応でした。解説を続けます。

表示は次のようになっている(はず)。ここにコードを追加する。

procedure TForm1.FormCreate(Sender: TObject);
begin

  //Panel1とFormの高さを記憶する変数を初期化
  intPH:=200;
  intFH:=480;

end;

追加するコード

procedure TForm1.FormCreate(Sender: TObject);
var
  //Python39-32へのPath(追加)
  AppDataDir:string;
begin

  //Panel1とFormの高さを記憶する変数を初期化
  intPH:=200;
  intFH:=480;

  //以下のコードを追加
  //embPythonの存在の有無を調査
  AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-32';

  if DirectoryExists(AppDataDir) then
  begin
    //フォルダが存在したときの処理
    MessageDlg('Embeddable Pythonが利用可能です。',
      mtInformation, [mbOk] , 0);
    PythonEngine1.AutoLoad:=True;
    PythonEngine1.IO:=PythonGUIInputOutput1;
    PythonEngine1.DllPath:=AppDataDir;
    PythonEngine1.SetPythonHome(PythonEngine1.DllPath);
    PythonEngine1.LoadDll;
    //PythonDelphiVar1のOnSeDataイベントを利用する
    PythonDelphiVar1.Engine:=PythonEngine1;
    PythonDelphiVar1.VarName:=AnsiString('var1');  //プロパティで直接指定済み
    //初期化
    PythonEngine1.Py_Initialize;
  end else begin
    MessageDlg('Embeddable Pythonが見つかりません!',
      mtInformation, [mbOk] , 0);
    PythonEngine1.AutoLoad:=False;
  end;

end;

ここでMessageDlgを使用しているので、以下のように System.UITypes を uses に追加する。

implementation

uses
  Vcl.Imaging.Jpeg, System.UITypes;  // <-追加

  //Jpeg:Jpeg画像を読み込む
  //System.UITypesはMessageDlgの表示に必要

{$R *.dfm}

プライベートメンバー変数 intCnt(カウンタとして利用する)と strAnsList(Pythonから返された計算結果を保存する) を2つ、Private宣言で新しく宣言する。

  private
    { Private 宣言 }

    //for Python(追加)
    //Counter
    intCnt:integer;
    //Pythonから送られたデータを保存
    strAnsList:TStringList;

    //Panel1の幅とFormの高さを記憶する変数
    intPH, intFH:integer;
    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;

  public
    { Public 宣言 }
  end;

Form上のButton2(読み取りボタン)をダブルクリックして、手続きを作成し、以下の内容を入力する。

procedure TForm1.Button2Click(Sender: TObject);
var
  StrList:TStringList;
  strJCnt,strColCnt,strRowCnt,strSelCnt:String;
  TopLX, TopLY, TLX1, TLY1, BRX1, BRY1:integer;
  strPicName:string;
begin

  //初期化
  Memo1.Clear;
  intCnt:=1;

  //座標
  TopLX:=65;
  TopLY:=28;
  //BtmRX:=121;
  //BtmRY:=43;
  TLX1:=65;
  TLY1:=61;
  BRX1:=419;
  BRY1:=497;

  //マークシート数Check(+1することを忘れない)
  strJCnt:=IntToStr(2);

  //列数Check(+1することを忘れない)
  strColCnt:=IntToStr(2);

  //1列あたりの行数Check
  strRowCnt:=IntToStr(10);

  //選択肢数Check
  strSelCnt:=IntToStr(8);

  //マークシート名
  strPicName:='ms';

  //結果を保存するStringList
  strAnsList := TStringList.Create;

  //Scriptを入れるStringList
  StrList := TStringList.Create;

  try

    //Python Script
    StrList.Add('import cv2');
    StrList.Add('import numpy as np');

    //for JPN(日本語に対応)
    StrList.Add('def imread(filename, flags=cv2.IMREAD_GRAYSCALE, dtype=np.uint8):');
    StrList.Add('    try:');
    StrList.Add('        n = np.fromfile(filename, dtype)');
    StrList.Add('        img = cv2.imdecode(n, flags)');
    StrList.Add('        return img');
    StrList.Add('    except Exception as e:');
    StrList.Add('        return None');

    //マーカー画像を読み込む
    StrList.Add('template = imread("marker.png", cv2.IMREAD_GRAYSCALE)');

    //マークシートの枚数
    StrList.Add('for j in range(1,'+strJCnt+'):');

    //列数
    StrList.Add('    for i in range(1,'+strColCnt+'):');

    //マークシートへのパスを取得
    StrList.Add('        if j < 10:');
    StrList.Add('            MS_Name = r".\Marksheet\'+ strPicName +'0"+ str(j) +".jpg"');
    StrList.Add('        else:');
    StrList.Add('            MS_Name = r".\Marksheet\'+ strPicName +'"+ str(j) +".jpg"');

    //画像を読み込む
    StrList.Add('        img = imread(MS_Name)');
    //画像をグレースケールで読み込む
    StrList.Add('        img_gray = imread(MS_Name, 0)');

    //テンプレートマッチングの実行(比較方法cv2.TM_CCORR_NORMED)
    StrList.Add('        result = cv2.matchTemplate(img, template, cv2.TM_CCORR_NORMED)');

    //類似度が最小,最大となる画素の類似度、位置を調べ代入する
    StrList.Add('        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)');
    //最も似ている領域の左上の座標を取得
    StrList.Add('        top_left = max_loc');
    StrList.Add('        if i == 1:');

    //補正値を取得(高さ)
    StrList.Add('            h1 = ' + IntToStr(TLY1 - TopLY));
    StrList.Add('            h2 = ' + IntToStr(BRY1 - TopLY));
    //補正値を取得(幅)
    StrList.Add('            w1 = ' + IntToStr(TLX1 - TopLX));
    StrList.Add('            w2 = ' + IntToStr(BRX1 - TopLX));

    //矩形の左上の座標を計算 [0]-> X, [1]-> Y
    StrList.Add('        TL = (top_left[0] + w1, top_left[1] + h1)');
    //矩形の右下の座標を計算
    StrList.Add('        BR = (top_left[0] + w2, top_left[1] + h2)');
    //画像を切り出し img[top_Y : bottom_Y, left_X : right_X]
    StrList.Add('        img = img_gray[TL[1] : BR[1], TL[0] : BR[0]]');

    //選択肢数
    StrList.Add('        n_col = '+ strSelCnt);

    //解答欄1列あたりの行数
    StrList.Add('        n_row = '+ strRowCnt);
    StrList.Add('        margin_top = 0');
    StrList.Add('        margin_bottom = 0');
    StrList.Add('        n_row = n_row + margin_top + margin_bottom');

    //マークの列数・行数の整数倍のサイズになるようリサイズ
    StrList.Add('        img = cv2.resize(img, (n_col*100, n_row*100))');

    //保存して確認
    //StrList.Add('        cv2.imwrite("01_ReSize.png", img)');

    //平滑化の度合い
    StrList.Add('        img = cv2.GaussianBlur(img,(35,35),0)');

    //保存して確認
    //StrList.Add('        cv2.imwrite("02_GaussianBlur.png", img)');

    //二値化の閾値
    //50を閾値として2値化
    //imgはグレースケール画像でなければならない
    //第2引数はしきい値で,
    //画素値を識別するために使用(指定)
    //第3引数は最大値でしきい値以上
    //(指定するフラグ次第では以下)の値を持つ
    //画素に対して割り当てられる値
    //StrList.Add('        ret, img = cv2.threshold(img, 140, 255, cv2.THRESH_BINARY)');

    //大津の二値化で閾値の設定を自動化
    //第1引数には画像データを設定
    //(グレースケール画像でなければならない)
    //第2引数はしきいだが自動計算させるので0(ゼロ)を指定
    //第3引数は0-255の256段階でグレースケール化しているから
    //最大値の255を指定
    StrList.Add('        ret, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)');

    //保存して確認
    //StrList.Add('        cv2.imwrite("03_threshold.png", img)');

    //白黒を反転
    StrList.Add('        img = 255 - img');

    //保存して確認(追加)
    StrList.Add('        cv2.imwrite("04_threshold.png", img)');

    //全マークを判定
    StrList.Add('        result = []');
    StrList.Add('        for row in range(margin_top, n_row - margin_bottom):');
    StrList.Add('            tmp_img = img [row*100:(row+1)*100,]');
    StrList.Add('            area_sum = []');
    StrList.Add('            for col in range(n_col):');
    StrList.Add('                area_sum.append(np.sum(tmp_img[:,col*100:(col+1)*100]))');
    StrList.Add('            result.append(area_sum > np.median(area_sum) * 3)');

    //判定結果を出力
    StrList.Add('        for x in range(len(result)):');
    StrList.Add('            res = np.where(result[x]==True)[0]+1');
    StrList.Add('            if len(res)>1:');
    StrList.Add('                var1.Value = "99"');
    StrList.Add('            elif len(res)==1:');
    StrList.Add('                s = str(res)');
    StrList.Add('                var1.Value = s[1]');
    StrList.Add('            else:');
    StrList.Add('                var1.Value = "999"');

    //Execute
    PythonEngine1.ExecStrings(StrList);

    //結果を表示
    Memo1.Lines.Assign(strAnsList);

    //Userへ案内
    MessageDlg('読み取り完了!', mtInformation, [mbOk] , 0);

  finally
    //解放
    StrList.Free;
    strAnsList.Free;
  end;

end;

Pythonから返された計算結果を受け取るため、PythonDelphiVar1のOnSetDataイベントの手続きを作成する。Form上のPythonDelphiVar1をクリックして選択し、オブジェクトインスペクタのOnSetDataイベントの右側をダブルクリックして、コード入力画面で以下の内容を入力する。

procedure TForm1.PythonDelphiVar1SetData(Sender: TObject; Data: Variant);
begin
  //値がセットされたら動的配列に値を追加
  strAnsList.Add(Data);
  intCnt:=intCnt+1;
  Application.ProcessMessages;
end;
表示の「999」は空欄、「99」は複数マークであることを意味する。

上書き保存(Ctrl+S)して、実行(F9)。次の画像のように、マークシートが正しく読み取り処理されることを確認する。

複数マークを許可する場合には、判定結果を出力する部分のコードを次のように変更する。マークシートの読み取り結果をCSVファイルに出力したり、Excelに書き出したりして利用する場合には、複数回答は99、未回答は999のように処理した方が、後々の処理がラクになる(・・・と思う)。

    //判定結果を出力(複数回答は99、未回答は999で表示)
    {コメント化ここから
    StrList.Add('        for x in range(len(result)):');
    StrList.Add('            res = np.where(result[x]==True)[0]+1');
    StrList.Add('            if len(res)>1:');
    StrList.Add('                var1.Value = "99"');
    StrList.Add('            elif len(res)==1:');
    StrList.Add('                s = str(res)');
    StrList.Add('                var1.Value = s[1]');
    StrList.Add('            else:');
    StrList.Add('                var1.Value = "999"');
    ここまで}

    //判定結果を出力(複数回答の詳細を表示)
    StrList.Add('        for x in range(len(result)):');
    StrList.Add('            res = np.where(result[x]==True)[0]+1');
    StrList.Add('            if len(res)>1:');
    StrList.Add('                var1.Value = str(res)+ '+'"!複数回答!"');
    StrList.Add('            elif len(res)==1:');
    StrList.Add('                s = str(res)');
    StrList.Add('                var1.Value = s[1]');
    StrList.Add('            else:');
    StrList.Add('                var1.Value = " *未回答*"');

PythonEngineが正しく初期化され、Embeddable Pythonが利用できることが確認できたら、このメッセージは必要ないのでコメント化しておく。

procedure TForm1.FormCreate(Sender: TObject);
var
  //Python39-32へのPath
  AppDataDir:string;
begin
  ・・・
  if DirectoryExists(AppDataDir) then
  begin
    //フォルダが存在したときの処理(コメント化)
    //MessageDlg('Embeddable Pythonが利用可能です。',
    //  mtInformation, [mbOk] , 0);
    PythonEngine1.AutoLoad:=True;

5.さらに進化

さまざまな機能を追加したマークシートリーダー
(ファイルの名称を連番で変更/画像の回転/グリッド指示位置と画像の連動/グリッド指示位置を画像上で矩形選択/閾値等各種パラメータの調整と保存機能/音声読み上げ関連機能の搭載/回答チェック機能(空欄&複数回答対応)/CSV形式でのデータ出力/ExcelBookへのデータ出力/様式の異なるマークシートをテンプレートとして登録して利用可能/抱き合わせ採点の実施機能/共通テスト(数学の様式)に対応等、考えつく限りの機能を搭載/さらに進化します!)

このプログラムでは、「マークシート画像の表示」と、「読み取り処理」の間に何も関連がないが、このプログラムをさらに発展させて、複数枚数の処理を可能にし、読み取り結果を画面上で確認するような機能を追加する際には、マークシート画像の表示はどうしても必要な機能になる。

さらに、画面の左側などに読み込んだマークシートがリスト形式で表示されるようにして、ここから任意のマークシート画像を選んで表示できるような機能も追加するとよいと思う。

読み取り結果も、ここではMemoに表示しているが、CSVやExcelへ出力して利用することを考えると、ここはGridコントロールに変更したい。

Gridコントロール上で選択したデータの該当回答欄に相当する画像が自動的に画面上に表示され、かつ、表示されたマークシート画像上の該当回答欄が矩形で選択され、ユーザーがチェックしやすいGUIにするとなお良いだろう。

また、チェック時にはユーザーがマークシート画像を見ながら確認作業が行えるよう、Gridコントロールの数字をアナウンスしてくれる音声読み上げ機能があると大変便利だ。それから、回答の必要がない、全マークシートが空欄となっている部分は、予め指定することで、チェックから除外できる機能も欲しい。

さらに、スキャナーから読み込んだ画像データを回転させたり、連番で扱いやすい名前に変更したり、様式の異なるマークシートをテンプレートとして登録できるような機能も搭載したい。

より一層ユーザーに優しい、夢に見たようなマークシートリーダーを開発したい。この希望の実現に向けて、日々努力する私でありたい。

Web上に貴重な資料を公開してくださった多くの皆さまに心より深く御礼申し上げます。ほんとうにありがとうございました。

6.著作権表示の記載方法

参考:Python4DelphiのLicenseについて

GitHubのPython4Delphiのダウンロードページには「The project is licensed under the MIT License.」とある。これは「改変・再配布・商用利用・有料販売すべてが自由かつ無料」であること、及び使用するにあたっての必須条件はPython4Delphiの「著作権を表示すること」と「MITライセンスの全文」or 「 MITライセンス全文へのLink」をソフトウェアに記載する、もしくは、別ファイルとして同梱しなさい・・・ということを意味する。

したがってPython4Delphiを利用したプログラムの配布にあたっては、ソフトウェアの中で、次のような著作権表示を行うか、もしくは P4DフォルダのルートにあるLicenseフォルダをプログラムに同梱して配布すればよいことになる。

Python4Delphiを利用した場合の著作権表示の記載例:

Copyright (c) 2018 Dietmar Budelsky, Morgan Martinet, Kiriakos Vlahos
Released under the MIT license
https://opensource.org/licenses/mit-license.php

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】

Installing The Splitter & Resizing Height of the VCL Components

「~ 主として「高さ」の変更に関する覚書 ~」

0.準備
1.最も簡単なリサイズ対応(高さの変更)・・・ AlignプロパティとSplitterの利用法
2.さまざまなVCLコンポーネントを追加する
3.画面サイズの変更に追随(主として高さ)
4.まとめ
5.ご案内
6.お願いとお断り

DelphiのVCLコンポーネントTSplitterの使い方と画面のリサイズ対応の覚書Part2。
ここでは、主として「高さ」に関する設定を取り上げる。

0.準備

Delphiを起動して、新規プロジェクトを作成後、任意のフォルダに「プロジェクトに名前を付けて保存」する。※ 同じフォルダにプロジェクトとは別名で、Unitファイルも保存する(Unitが1つしかないプログラムでも、プロジェクトとは別名でUnitファイルを保存する必要がある)。ここではデフォルト設定の名称をそのまま利用する。

・プロジェクトファイル名:Project1.dproj
・ユニットファイル名:Unit1.pas

1.最も簡単なリサイズ対応(高さの変更)・・・ AlignプロパティとSplitterの利用法

FormにPanelを3つドラッグ&ドロップする。もし、 ドラッグ&ドロップ ではなく、PanelをダブルクリックしてFormに置く場合は、操作手順に要注意。パレットのPanelを連続してダブルクリックすると、Formではなく、Panel1の上に次々と新しいPanelが乗っかってしまう。Panel1が出現したら、いったん(Panel2の親となる)Formをクリックして選択してから、パレットのPanelを再度ダブルクリックする。

PanelはStandardに入っている
Formへドラッグ&ドロップする

画面は、次のようになる。

Panel1をクリックして選択したら、次の図のように操作してPanel1のAlignプロパティをalTopに設定する。

画面は次のようになる。

ここでFormをクリックして選択し、Form(親)がアクティブな状態で、このForm(親)に対して、Splitterコンポーネントを(子として)設置する。この「何が親で、何が子なのか」をまず明確にして、かつ、「それぞれの子の状態は、親に対してどうなのか=どんなGUIにするのか」を考えながら作業すると混乱を防げる。

構造をみれば親子関係がわかる

下の図はSplitterを置いたところ。Alignプロパティのデフォルト設定が「alLeft」なのでSplitterはFormの左端に貼りついている。ここで、Splitterを選択したまま、SplitterのAlignプロパティを 「alTop」 に設定すると、Panel1の下に貼り付くように、Splitterの位置が変化して、それと同時に、SplitterのCursorプロパティが上下分割カーソルを意味する「crVSplit」に自動的に変更される。このようにして「親」に対する、「子」の状態を適切に決めて行く。

この状態で SplitterのAlignプロパティを 「alTop」 に設定する
SplitterのAlignプロパティをalTopに設定すると、Cursorプロパティも連動して「crVSplit」に変化する

さらに、実行時のSplitterの動作をわかりやすくするため、SplitterのAutoSnapプロパティをFalseに設定し、MinSizeを「30(デフォルト設定値)」にする。実際の操作としては、SplitterのAutoSnapプロパティをFalseに設定し、下方へスクロールすれば、MinSizeは30になっている(はず)。

AutoSnapプロパティをFalseに設定

次に、Panel3をクリックして選択し、 次の図のように操作してPanel3のAlignプロパティをalBottomに設定する。

画面は次のようになる。

次に、Panel2をクリックして選択し、 次の図のように操作してPanel2のAlignプロパティをalClientに設定する。

Panel2のAlignプロパティをalClientに設定

次にPanel1をクリックして選択し、下のハンドルをドラッグして(Panel1の)高さを少し大きくして下の図のようにする。この状態で上書き保存(Ctrl+S)して実行(F9)し、Splitterが意図した通りに動作することを確かめる。

実行(F9)して、Splitterの動作を確認する

2.さまざまなVCLコンポーネントを追加する

ここで、Panel1~3のCaptionプロパティを「空欄」にして、Panelの名前が表示されないように設定する。さらに、Panel1をクリックして選択し(親にして)、Panel1の上にScrollBoxを載せ、ScrollBoxのAlignプロパティをalClientに設定する。さらに、その上にImageを1つ載せる。ImageのAlignプロパティは「None」のままでよい。

次に、Panel2をクリックして選択し、Memoを1つ載せ、MemoのAlignプロパティをal Clientに設定する。

VCLコンポーネントの配置について慣れないうちは、かなり混乱するが、何が親で、どれが子になって、どういう状況で仕事をさせたいか(このVCLは常に画面の下方に固定で・・・とか、親の残りのスペース全部=alClientで・・・など)を、「よーく考えながら」作業すると、必要なコンポーネントだけでなく、それを設置する順番も見えてくる。

必要な各VCLコンポーネントがパレットのどこにあるのか? もし、場所を忘れてしまっていても、 コンポーネントの名前で検索すれば、検索窓に3~4文字入れた時点で、ほぼ見つかるので、設置したいVCLコンポーネント の機能と名前さえ思い出せれば、そのパレット内の配置に関しては、まったく覚えていなくても、何とかなる。

むしろ、このようなシーンで重要なのは、「実現したい処理にはどんなVCLコンポーネントが最適なのか?」そして「どのコンポーネントを、どう配置すれば、ユーザーに最も使いやすいGUI環境を提供できるのか?」の2点だと思う。

GUI作成に関しては、プログラマ個々のデザインのセンスの良し悪しも当然あると思うが、これに加えて、そのプログラマが「どれだけ修羅場を経験したか・・・」というような、個々のバックグラウンドにある経験も、もしかしたら重要な要素のひとつかもしれない・・・。

VCLコンポーネントの検索例

各VCLコンポーネントの親子関係を「構造」で確認。

VCLコンポーネントの親子関係がよくわかる

いちばん下の階層にあるPanelは、画面では他のコントロールに隠されて見えない。

画面を見ただけでは、各コンポーネントの階層構造はわからない

上書き保存(Ctrl+S)して実行(F9)し、Splitterの動作やFormを最大化した際の各コントロールの見え方等を確認する。

3.画面サイズの変更に追随(主として高さ)

さらに、Formの大きさが変わっても、その時点でのPanel1とFormの高さの比率が維持されるようにプログラミングしてみた。 まず、Private宣言部で整数型の変数2つと、Formが完全に表示された時点で実行される表示終了イベントを取得する手続き procedure CMShowingChanged を宣言。

Formの 表示終了イベントを取得するprocedureの実現部は、以下に記載したコードをミスのないように入力し(文法的に誤りのない状態で)、procedure CMSShowingChanged~行のどこか(付近でも可)にフォーカスがある(=カーソルがある)状態で、Shift+Ctrl+C操作を行うと、手続きが自動的に生成される。

Shift+Ctrl+C:キーボード左側のShiftキーとCtrlキーを左手で同時に押して、さらに右手でCキーを押す

また、宣言の順番も大切。プライベートメンバー変数と手続き(procedure)の宣言の順番が逆になってはいけない。 プライベートメンバー変数の宣言を、手続きの宣言より必ず先に行う必要がある。

  private
    { Private 宣言 }
    //Panel1の幅とFormの高さを記憶する変数
    intPH, intFH:integer;
    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;
  public
    { Public 宣言 }
  end;

Formの表示終了イベントを取得して、その時点でのPanel1とFormの高さを記憶する。

フォームの表示完了時に処理する(くろねこ研究所さん)

URL:https://www.blackcat.xyz/article.php/ProgramingFAQ_del0049より引用
procedure TForm1.CMShowingChanged(var Msg: TMessage);
begin
  inherited; {通常の CMShowingChagenedをまず実行}
  if Visible then
  begin
    Update; {完全に描画}
    //Formの表示終了時に以下を実行
    Panel1.Height:=intPH;
    intPH:=Panel1.Height;
    intFH:=Form1.Height;
  end;
end;

Formが生成される際に、Panel1とFormの高さをプログラムから指示して決定。

procedure TForm1.FormCreate(Sender: TObject);
begin
  //Panel1とFormの高さを記憶する変数を初期化
  intPH:=200;
  intFH:=480;
end;

Formの大きさの変更イベントに合わせて、Panel1の高さを計算して決定。

procedure TForm1.FormResize(Sender: TObject);
begin
  //比率を維持してPanel1の高さを変更
  Panel1.Height:=Trunc(Form1.Height * intPH/intFH);
end;

ここがいちばん重要か? Splitterが動かされたら(=Movedイベントが発生)、それが動かされた時点(=動かされる直前)でのFormとPanel1の高さを取得。この値をもとにしてFormとPanel1の高さの比率を計算し、さらに、この比率をもとにFormのResize時に Panel1 の高さを計算、その計算結果の小数点以下を切り捨てた整数値を Panel1.Heightプロパティに設定している。※ Heightは整数値で、小数点以下の値にこだわる必要はまったくないから。

procedure TForm1.Splitter1Moved(Sender: TObject);
begin
  //Panel1とFormの高さを取得
  intPH:=Panel1.Height;
  intFH:=Form1.Height;
end;

上書き保存(Ctrl+S)し、実行(F9)して、Panel1の高さを変更し、Formの大きさを最大化して、Formと Panel1の高さの比率が維持されることを確認する。

プログラム起動時の画面
最大化した状態(縦の比率が維持されていることを確認)

4.まとめ

Formに置いたVCLコンポーネントの高さを実行時に調整できるようにするには、Splitterを利用する。手順は以下の通り。

(1)Panelを3つ、Formに設置。上位のPanel1のAlignプロパティを alTop に設定。
(2)Form(親)をクリックして選択後、Splitter(子)を設置。
(3)Panel3のAlignプロパティを alBottomに設定(=Panel3は固定する)。
(4)Panel2の AlignプロパティをalClientに設定。

5.ご案内

今回作成したプログラムを利用して、次回、マークシートリーダー作成の練習プログラムを紹介します。プログラムのGUIはDelphiで今回作成したものをそのまま使い、マークシート読み取りと計算処理は、このBlogでこれまでに紹介してきた PythonForDelphi と Embeddable Python を用いて行います。練習用なので、マークシートの読み取り枚数は1枚で、読み取り結果の表示にはMemoを利用します。

実用化するには、複数シートを読み取れるよう、さらにLoop処理を加えたり、読み取り結果のCSVファイル等への出力も考慮して、結果表示用にMemoではなく、Gridコントロールを用いる等、さらなる工夫が必要ですが、最も重要な「マークシートを読み取る」というプログラムの核心部分を丁寧に紹介します。興味のある方はぜひ、ご覧ください。

6.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】

Installing the Splitter & Resizing Width of the VCL Components

「~ 主として「幅」の変更に関する覚書 ~」

0.準備
1.最も簡単なリサイズ対応(幅の変更)・・・ AlignプロパティとSplitterの利用法
2.最も簡単なリサイズ対応(幅の変更)・・・ Anchorsプロパティの利用法
3.画面サイズの変更に追随(主として幅)
4.まとめ
5.お願いとお断り

DelphiのVCLコンポーネントTSplitterの使い方と画面のリサイズ対応の覚書。

Formの大きさは決め打ちで、決してResizeしない前提でプログラミングできれば、ある意味、それがいちばんラクなんだけど・・・。百歩譲って、Formの大きさはResizeしなくても、Formの中にあるVclコンポーネントの幅・高さは変更できた方がうれしい場合は多い。

例えば、 マークシート画像を読み取って処理する場合、マークシート画像と読み取り結果を比較(チェック)するには、画像と読み取り結果の両方が1つの画面上に表示されていることが望ましい。このような場合に備えて、必要に応じて「画像」と「データ」の表示領域を思いのまま手動で広げたり、狭めたり出来る機能をユーザーに提供したいと考えるのは、全プログラマに共通の思いだろう・・・。

そのような私自身の経験を基に、何に使うかはアイデア次第として、画面の左右や上下にさまざまなVCLコンポーネントが置かれている時、 Splitterを使ってその幅や高さを自由に変える方法をここでは取り上げた。

さらに、Formの大きさの変化に合わせ、Form上に置いた各コンポーネントの大きさ(主として幅)が追随して変化するようなプログラミングにも(自我流で恥ずかしい限りだが)チャレンジしてみた。

主として「幅」の変更に関する覚書

0.準備
1.最も簡単なリサイズ対応(幅の変更)・・・ AlignプロパティとSplitterの利用法
2.最も簡単なリサイズ対応(幅の変更)・・・ Anchorsプロパティの利用法
3.画面サイズの変更に追随(主として幅)
4.まとめ
5.お願いとお断り

0.準備

Delphiを起動して、新規プロジェクトを作成後、任意のフォルダに「プロジェクトに名前を付けて保存」する。※ 同じフォルダにプロジェクトとは別名で、Unitファイルも保存する(Unitが1つしかないプログラムでも、プロジェクトとは別名でUnitファイルを保存する必要がある)。ここではデフォルト設定の名称をそのまま利用する。

・プロジェクトファイル名:Project1.dproj
・ユニットファイル名:Unit1.pas

1.最も簡単なリサイズ対応(幅の変更)・・・ AlignプロパティとSplitterの利用法

最も簡単なリサイズ対応は、Formに置いたPanelなどのAlignプロパティをalNoneからalClientに変更することだ。これでFormの大きさの変更に合わせて(幅・高さ共に)、Form上のPanelの大きさも、親Formの大きさの変更に追随して変わるようになる。
もし、Panel上にButtonを1つだけ置いて使用するのであれば、Buttonの Anchorsプロパティを適切に指定するだけで、画面のリサイズに対応したGUIが完成する。

何に使うかはアイデア次第として、ここではサンプルとして画面の左にMemo、右にPanel(いろいろなコンポーネントを置くベース)がある条件で、Memoの幅を自由に変える方法を取り上げる。さらに、Formの大きさが変わっても、その時点でのMemoとPanelの幅の比率が維持されるような(自我流の)プログラミング例も掲載した。

新規作成したプロジェクトのForm上に、MemoとPanelを1つずつ置く。
MemoもPanelもパレットのStandardにあるので、まずStandardを開く。

>をクリックして開く
MemoとPanelをそれぞれFormへドラッグ&ドロップ
(それぞれをダブルクリックしてもよい)
Form上にMemoとPanelがのる

Memo1をクリックして選択し、下図のように操作してMemoのAlignプロパティをalLeftに設定する。

画面は次のようになる。

重要 ここでFormをクリックして選択する(Formをアクティブにする)。

ハンドル(水色の枠)がMemoからFormに移動し、Formの方がアクティブになる。

Formをアクティブにした状態で、パレットでsplitterを検索する。splitterはAdditionalにあるコントロールで、これを2つのコントロールの間に追加すると、ユーザーが実行時にそのコントロールのサイズを変更できるようになる。

「split」未満の入力で見つかるはず

見つけたらTSplitterをダブルクリックしてFormに配置する。

Memoの右側にSplitterが配置される。
デフォルトでは「幅」の変更用になっている

Cursorプロパティに設定された「crHSplit」は「左右分割カーソル」を意味する。ちなみに、「crVSplit」を指定すると「上下分割カーソル」になる。

最後に、PanelコンポーネントのAlignプロパティを変更する。Panelをクリックして選択し、オブジェクトインスペクタのAlignプロパティを alClient に設定する。

これでMemoの幅の大きさを自由に変更できるはず

Ctrl+Sで上書き保存、F9を押して実行。意図した通りに操作できるか、確認する。

設計時にMemoの幅を適切に指定することで、実行時の初期画面を意図した通りに作成できる。

参考:SplitterのAutoSnapプロパティをFalseすると、幅を小さくしたとき、 Splitterの MinSizeプロパティで設定した値以下に変更されなくなる。これを用いると、MemoやPanelが完全に隠されてしまう事態を予め防止できる。

SplitterのAutoSnapをFalse、MinSizeを30に設定
MemoもPanelも幅がMinSize以下にならない

2.最も簡単なリサイズ対応(幅の変更)・・・ Anchorsプロパティの利用法

では、Panelの上にButtonをのせたら、画面サイズの変更に合わせてButtonの位置はどのように変わるのだろうか?

これを検証してみる。Panel1をクリックして選択し、その上にButtonを1つ設置する。

Panel1が選択されている状態でダブルクリック

設置したButtonの位置を変更する(下図のようにFormの右下隅の方へドラッグして移動)。Buttonのプロパティはデフォルト設定のままにしておく。

この状態で上書き保存(Ctrl+S)し、実行(F9)して、Formの大きさを最大化してみる。

右上の × をクリックして画面を閉じる

Button1の位置を常識的な位置へ自動的に変化させる最も簡単な方法は、「Anchorsプロパティ」の利用である。Button1をクリックして選択し、オブジェクトインスペクタのAnchorsプロパティの設定を次のように変更する。

上書き保存(Ctrl+S)し、実行(F9)して、Formの大きさを最大化して確認する。

右上の × をクリックして画面を閉じる

ちなみに下図のように設定すると・・・

AnchorsプロパティをすべてTrueに設定
もしかしたら、場合によってはアリかも・・・。 右上の × をクリックして画面を閉じる

ちなみに、コレだと・・・

akTopのみFalseに設定
「年越しそば」的な挙動を見せました。ほそーく、ながーく なりました。ある意味これがベスト?

3.画面サイズの変更に追随(主として幅)

さらに、Formの大きさが変わっても、その時点でのMemoとPanelの幅の比率が維持されるようにプログラミングしてみた。 まず、Private宣言部で整数型の変数2つと、Formが完全に表示された時点で実行される表示終了イベントを取得する手続き procedure CMShowingChangedを宣言。

Formの 表示終了イベントを取得するprocedureの実現部は、以下に記載したコードをミスのないように入力し(文法的に誤りのない状態で)、procedure CMSShowingChanged ~行のどこか(付近でも可)にフォーカスがある(=カーソルがある)状態で、Shift+Ctrl+C 操作を行うと、手続きが自動的に生成される。

Shift+Ctrl+C:キーボード左側のShiftキーとCtrlキーを左手で同時に押して、さらに右手でCキーを押す

また、宣言の順番も大切。プライベートメンバー変数と手続き(procedure)の宣言の順番が逆になってはいけない。 プライベートメンバー変数の宣言を、手続きの宣言より必ず先に行う必要がある。

  private
    { Private 宣言 }
    //Memoの幅とFormの幅を記憶する変数
    intMW, intFW:integer;
    //Formの表示終了イベントを取得
    procedure CMShowingChanged(var Msg:TMessage); message CM_SHOWINGCHANGED;
  public
    { Public 宣言 }
  end;

Formの表示終了イベントを取得して、その時点でのMemoとFormの幅を記憶する。

フォームの表示完了時に処理する(くろねこ研究所さん)

URL:https://www.blackcat.xyz/article.php/ProgramingFAQ_del0049より引用
procedure TForm1.CMShowingChanged(var Msg: TMessage);
begin
  inherited; {通常の CMShowingChagenedをまず実行}
  if Visible then
  begin
    Update; {完全に描画}
    //Formの表示終了時に以下を実行
    Memo1.Width:=intMW;
    intMW:=Memo1.Width;
    intFW:=Form1.Width;
  end;
end;

Formが生成される際に、MemoとFormの大きさをプログラムから指示して決定。

procedure TForm1.FormCreate(Sender: TObject);
begin
  //MemoとFormの幅を記憶する変数を初期化
  intMW:=480;
  intFW:=640;
end;

Formの大きさの変更イベントに合わせて、Memoの幅を計算して決定。

procedure TForm1.FormResize(Sender: TObject);
begin
  //比率を維持してMemoの幅を変更
  Memo1.Width:=Trunc(Form1.Width*intMW/intFW);
end;

ここがいちばん重要か? Splitterが動かされたら(=Movedイベントが発生)、それが動かされた時点(=動かされる直前)でのFormとMemoの幅を取得。この値をもとにしてFormとMemoの幅の比率を計算し、さらに、この比率をもとにFormのResize時にMemoの幅を計算、その計算結果の小数点以下を切り捨てた整数値をMemo1.Widthプロパティに設定している。※ Widthは整数値で、小数点以下の値にこだわる必要はまったくないから。

procedure TForm1.Splitter1Moved(Sender: TObject);
begin
  //MemoとFormの幅を取得
  intMW:=Memo1.Width;
  intFW:=Form1.Width;
end;

上書き保存(Ctrl+S)し、実行(F9)して、Memoの幅を変更し、Formの大きさを最大化して、FormとMemoの幅の比率が維持されることを確認する。

Buttonコントロールの幅の違いに注目(上と下の画像は、同じ画像ではありません)

Formの幅は640ピクセル、Memoの幅は480ピクセルで表示
画面を最大化してみた。Formの幅とMemoの幅の比率は保たれているように見える。

数値的にはどうかと思い、FResize前のFormとMemoの幅を表示してみた。

procedure TForm1.FormResize(Sender: TObject);
begin
  //比率を維持してMemoの幅を変更
  Memo1.Width:=Trunc(Form1.Width*intMW/intFW);
  ShowMessage('Memoの幅 / Formの幅:'+IntToStr(intMW)+
    ' / '+IntToStr(intFW));
end;

画面を最大化してから、元の大きさに戻した時のようす。

Formの幅は1382ピクセルと表示されている

参考:私のPCは、画面の解像度を1366×768に設定している(Formが表示される際、Screen.Widthを調査すると1366と表示された)。そこで、FormのWidthを1366に設定すると、ClientWidthはそれより小さくなり、設計時に画面の横幅いっぱいに配置したVCLコンポーネントの右側に実行時余白が生まれる。
FormのClientWidthを1366に設定すると、Widthは1382となり、画面の解像度より大きくなるが、VCLコンポーネントの位置は設計時も実行時も同じになった。
この経験から、画面の解像度はClientWidth×ClientHeightを意味するものと、私は理解しているのだが、これでいいのだろうか?

4.まとめ

Formに置いたVCLコンポーネント(例:Memo)の幅を実行時に調整できるようにするには、Splitterを利用する。手順は以下の通り。

(1)MemoコンポーネントのAlignプロパティを alLeft に設定。
(2)Formを選択後、Splitterを設置。
(3)Panelコンポーネントを置いて、Alignプロパティを alClient に設定。

Panelの上に乗せたButtonなどのコンポーネントは、 Anchorsプロパティを適切に設定することでFormのリサイズに合わせて、その表示位置を変更できる。

5.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】

How to use Python4Delphi

「PythonForDelphiの使い方(Delphiのプログラム内でPythonを動かす)」

1.Delphiで埋め込みPythonを使う
2.準備
3.ノートPCの電池残量を表示するプログラムを作成
4.PythonEngineのメモリリーク
5.Delphi11のIDEが真っ白になってしまう問題への対応方法
6.著作権表示の記載方法
7.お願いとお断り

こちらで紹介した方法の応用版として、自作のマークシートリーダーの読み取り速度をPython4Delphiで高速化。プログラムのダウンロード(無料)も可能です。もし、よかったら次のリンク先記事もご参照ください。

1.Delphiで埋め込みPythonを使う

ノートPCの電池残量を表示する練習プログラムを、埋め込みPythonを使ってDelphiで書いてみる。
埋め込み用途のembeddable pythonをDelphiで使うには? というテーマで悩んでいらっしゃる方の参考になれば、望外の喜びです。なお、以下の内容はDelphiで開発経験のある方を対象としています。IDEの基本的な操作方法等は省略していますので、予めご了承ください。

2.準備

(1)DelphiにPython4Delphi(P4D)のパッケージを予めインストールしておく。

(2)埋め込み用のEmbeddable Pythonをダウンロードし、各種ライブラリをインストール(下記リンク先ではNumpyとOpenCVライブラリをインストール)。

(3)Embeddable PythonにノートPCの電池残量を表示するため、psutilライブラリをインストール( Embeddable Python のダウンロードと設定方法は上の(2)を参照してください)。

「python -m pip install psutil」と入力してEnterキーを押す

(4)Delphiを起動して「ファイル」→「新規作成」→「Windows VCL アプリケーション」の順にクリックして新しいプロジェクトを準備する。

VCLアプリケーションの新規作成(Delphi11の場合)

3.ノートPCの電池残量を表示するプログラムを作成

(1)プロジェクトに名前を付けて保存する
(2)GUIを作成
(3)コンパイル & Python環境をコピー
(4)Python関連のVCLコンポーネントを配置
(5)Python関連のVCLコンポーネントのプロパティを設定
(6)エラー対応(ライブラリパスの確認)
(7)閉じるボタンのコードを書く
(8)FormのCreateでPython39-32の有無を確認する
(9)Messageダイアログを使う
(10)埋め込みPythonと接続する
(11)OnSetDataイベントを利用する
(12)プログラムの完成と動作確認

(1)プロジェクトに名前を付けて保存する

新しいフォルダを作成(名称は任意:ここではBTRC_byP4Dとしている)し、Unit1.pasを保存(Unit1を別名にしてもよいが、名称をメモしておく)。

参考 BTR:Battery(電池) / C:Charging(充電)/ P4D:PythonForDelphi

つづけて、プロジェクトファイル(Project1.dproj)を同じフォルダに保存。
Project1は別名にしてもよいが、上のpasファイルと同じ名称にしないこと。
また、別名にした場合は、名称を忘れないようにメモしておく。

(2)GUIを作成

画面にVCLコンポーネントを配置してGUIを作る。
Memoを2つ(Memo1とMemo2)、
Buttonを2つ(Button1とButton2)が最低限必要。

パレットのTMemoとTButtonをそれぞれ2つずつ、FormへD&Dする。

DelphiのIDEの基本的な操作方法や、VCLコンポーネントの配置方法は、次のリンク先の解説がわかりやすい。

はじめてのDelphiアプリケーション (VCL Form編) (Delphi プログラミング)

URL:https://www.ipentec.com/document/delphi-first-application-vcl-form-application



※ Formの大きさの変更にMemoの大きさやButtonの表示位置を追随させる方法は、別途解説する予定。

各VCLコンポーネントの名称はデフォルト設定のまま

Button1のCaptionプロパティを「実行」に変更。
Button2のCaptionプロパティを「終了」に変更。

Button1のCaptionプロパティを「実行」に変更。 Button2も同様にして「終了」に変更する。
ボタンのCaptionプロパティを変更

(3)コンパイル & Python環境をコピー

ビルド構成(Debug)のまま、ここで1回コンパイルしてexeを生成。

Ctrl+F9(Ctrlキーを押しながらF9キーを押す)でもOK!
コンパイル成功を確認→OKをクリック

※ ツールバーの実行(F9)をクリックして実行した場合は、生成されたexeが実行されてFormが表示されるので、表示されたFormを右上の閉じるボタンをクリックして閉じる。

ツールバーの実行(F9)から実行する場合
右上の「閉じる」ボタンでFormを閉じる

コンパイルに成功すると、BTRC_byP4Dフォルダの中にWin32フォルダが、さらにその下にDebugフォルダがそれぞれ自動的に作成される。このDebugフォルダを開き、別途作成しておいたEnbeddable Pythonの入ったフォルダをコピーして、貼り付ける(下の例では Enbeddable Pythonの入ったフォルダ名をpython39-32としている)。

Enbeddable Pythonの入ったフォルダを
ここへ貼り付ける。
フォルダとファイルの構造はこうなる。

Embeddable Pythonのダウンロードと各種ライブラリのインストール方法は以下のリンク先を参照してください。

(4)Python関連のVCLコンポーネントを配置

DelphiにPythonのスクリプトを埋め込んで実行するには、PythonForDelphiが必要。
PythonForDelphi(またはPython4Delphi さらに略すと P4D)をDelphiにセットアップする方法は以下のリンク先で解説。

(Python4Delphiのパッケージがインストールされた)Delphiのパレットのいちばん下にPython4Delphiの非ビジュアルコンポーネントがあるので、この中から次の3つのコンポーネント

「PythonEngine、PythonGUIInputOutput、PythonDelphiVar」

をForm上にドラッグ&ドロップ(各非ビジュアルコンポーネントをダブルクリックしてもよい)。

※ 非ビジュアルとは、「実行時に見えなくなる」コンポーネントを意味する。

Python4Delphiの非ビジュアルコンポーネント
非ビジュアルコンポーネントなので画面の任意の位置へD&DすればOK!
非ビジュアルコンポーネントは表示しない設定にすることも出来る(忘れっぽい私は常に表示している)。

(5) Python関連のVCLコンポーネントのプロパティを設定

・PythonEngine1のAutoLoadプロパティをFalseに設定

Form上にパレットからPythonEngineコンポーネントをドラッグ&ドロップすると、名称は自動的に PythonEngine1になる。上の図のようにこれをクリックして選択すると、オブジェクトインスペクタにPythonEngine1のプロパティが表示されるので、その中のAutoLoadプロパティをFalseに変更する(デフォルトTrueに設定されているので、チェックボックスのチェックを外す)。

AutoLoadプロパティをFalseに変更

練習ではなく、本格的にプログラミングする際、私はビジュアルコンポーネントについては、その名称を必ず変更するようにしている。理由はButtonコントールなどは使用数が多く、わかりやすい名前を付けておいた方がプログラミングしやすいからだ。

 例:OKボタンなら、そのNameプロパティを button1→btnOK へ変更

しかし、非ビジュアルコンポーネントの場合は、同じコンポーネントを複数配置することは稀なので、Delphiが自動的に割り振った名前をそのまま利用している。ここでもその例にならって、非ビジュアルコンポーネントの名称は Delphiが自動的に割り振った名前をそのまま利用することにする。

・PythonEngine1のDllNameプロパティは、python39.dllを予め指定(組み込み用のPythonのバージョンに合わせて設定する)。

最新版のPython4Delphiでは「python310.dll」がデフォルト値になっていた。

python39.dllは、上でDebugフォルダ内に張り付けたPython39-32フォルダ内にある。

・PythonEngine1のIOプロパティにはPythonGUIInputOutput1を指定する。

IOプロパティのデフォルト設定は「空欄」になっていた。

・PythonGUIInputOutput1のOutPutプロパティに「Memo2」のように出力先を指定したくなるが、ここでは敢えて何も設定しない。

・PythonDelphiVar1のVarNameプロパティは、プログラムコードの記述に合わせるため「var1」とする。※var1と入力後、Enterで確定すること!(青く反転表示されるのを確認する)

「var1」と入力後、Enterキーを押さないと変更が反映されない。

・この状態で実行(F9)した際に「Python Engineが見つかりません」というようなエラーメッセージが表示される場合は、P4Dのパッケージをインストールした際のライブラリパス設定に誤りがないか、確認する

画面下のメッセージ欄の表示:[dcc32 致命的エラー] Unit1.pas(7): F2613 ユニット ‘PythonEngine’ が見つかりません。
コンパイルエラー発生時のUnit1画面

(6)エラー対応(ライブラリパスの確認)

GitHubから入手したPython4DelphiのフォルダのSourceフォルダ以下にある、このプログラムの動作に必要なファイルへのライブラリパスが正しく設定されていることを確認する。設定されていない場合は、(灰色で表示されている誤ったパスを削除して)ライブラリパスを再設定する。

「ツール」→「オプション」の順にクリックして、次の画面を表示する。

「言語」→「Delphi」→「ライブラリ」とクリックして、赤枠囲みの中をクリック。

ライブラリパスを正しく設定する。

PCを新しくした場合等、再設定する必要があるかもしれないので、
設定内容をメモしておく。

ライブラリパスの設定が完了したら、再度コンパイル(実行:F9)してエラーが発生しないことを確認する。

(右上の閉じるボタンで終了)

参考:コンパイルとビルドの違い

・メニューの「プロジェクト」 →「Project1をコンパイル」
 (ショートカットは「Ctrl+F9」)

前回のビルド以降に変更されたファイルと、それに依存するファイルのみをコンパイルして EXE を生成するが、アプリケーションは起動しない。

・メニューの「プロジェクト」 →「Project1をビルド」
 (ショートカットは「Shift+F9」)

変更の有無に関わらず、全てのユニットを再コンパイルして EXE を生成するが、アプリケーションは起動しない。ユニット数が多ければ当然それだけ遅くなる。

・実行(ショートカットはF9)

変更されたソースコードをすべてコンパイルする。コンパイルが成功した場合は、アプリケーションを実行するので、そのアプリケーションを IDE でテストできるようになる。

・デバッガを使わずに実行。(ショートカットは「Shift+Ctrl+F9」)

変更があったユニットだけをコンパイルしてexeを生成し、 アプリケーションを起動する(exe単体での起動と同じ)。

(7)閉じるボタンのコードを書く

Formの「終了」ボタンをダブルクリックすると画面は次のようになる。ここに終了ボタン(Button2)がクリックされた時のProcedure(手続き)を記述する。

procedure TForm1.Button2Click(Sender: TObject);
begin

end;

beginとend;の間に次のように記入する。

procedure TForm1.Button2Click(Sender: TObject);
begin
  //プログラムの終了
  Close;
end;

//は1行をコメント化(コンパイラはコメント部分を無視する)

Closeは、Formを閉じる命令(正確にはメソッドだから方法?)。アプリケーションのメインフォームを閉じると、そのアプリケーションは終了する。
(ここはApplication.TerminateでもOKだが、 Windowsでは、Application.Terminate でアプリケーションを強制終了させた場合には、OnCloseQueryイベントが実行されない仕様になっているとのこと)。← これは不具合ではなく、Windowsの仕様。

もし、アプリケーション終了時(Windowsの終了やログアウト時も含む)に、何らかの終了処理(中止を含む)を行いたい場合は、OnCloseQueryイベントが実行されるCloseを使用する。(今回は行わないがForm生成時に、例えばTStringListをCreateしてプログラム内で利用するような場合には、CreateしてTry文で使用(~Finally ここで解放 End;)の一般的流れが使えないので、 OnCloseQueryイベントもしくはOnDestroyイベントで、TStringList.Freeのようにして確実に解放しなければならない。)

実行(F9)してFormが表示されたら、「終了」ボタンでアプリケーションを終了できることを確認する。

(8)FormのCreateでPython39-32の有無を確認する

FormがCreateされる時に、Embeddable Python(Python39-32 フォルダ)があることを確認し、必要な諸設定を行う。F12を押すとFormとUnitの表示を交互に切り替えることができる。画面をFormに切り替え、アクティブ(Formのどこかをシングルクリック)にし、オブジェクトインスペクタのイベントタブをクリックして、下にスクロールさせ、OnCreateイベントの右の空白部分をダブルクリックする。自動的にUnit画面に表示が切り替わり、下のようにForm.Create手続き部が生成される。

procedure TForm1.FormCreate(Sender: TObject);
begin

end;

Python39-32フォルダのパスを入れる変数を宣言する。procedureとbeginの間にvar(宣言)を入力して、改行&字下げを行い、文字列型変数AppDataDirを宣言する。必要であればコメントで変数の用途を書いておく。

procedure TForm1.FormCreate(Sender: TObject);
var
  //Python39-32へのPath
  AppDataDir:string;
begin

end;

次に、beginとend;の間にForm.Create手続きで行いたい内容を記述する。

begin

  //Embeddable Pythonの存在の有無を調査
  AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-32';

  if DirectoryExists(AppDataDir) then
  begin
    //フォルダが存在したときの処理
    MessageDlg('Embeddable Pythonが利用可能です。',
      mtInformation, [mbOk] , 0);
    PythonEngine1.AutoLoad:=True;
    PythonEngine1.IO:=PythonGUIInputOutput1;
    PythonEngine1.DllPath:=AppDataDir;
    PythonEngine1.SetPythonHome(PythonEngine1.DllPath);
    PythonEngine1.LoadDll;
    //PythonDelphiVar1のOnSeDataイベントを利用する
    PythonDelphiVar1.Engine:=PythonEngine1;
    PythonDelphiVar1.VarName:=AnsiString('var1');  //プロパティで直接指定済み
    //初期化
    PythonEngine1.Py_Initialize;
  end else begin
    MessageDlg('Embeddable Pythonが見つかりません!',
      mtInformation, [mbOk] , 0);
    PythonEngine1.AutoLoad:=False;
  end;

end;

Ctrl+Sでコードを上書き保存。保存したら実行(F9)。
ここまでの操作にミスがなければ次のメッセージが表示される。

「OK」をクリックして閉じる

続けてFormが表示されるので、終了ボタンをクリックして閉じる。
画面下のメッセージ欄に次のヒントが表示されることを確認する。

(9) Messageダイアログを使う

[dcc32 ヒント] Unit1.pas(118): H2443 インライン関数 ‘MessageDlg’ はユニット ‘System.UITypes’ が USES リストで指定されていないため展開されません

ヒントの言う通り、 ‘System.UITypes’ を USES リストで指定する。以下のように、30行目付近の implementation (実装・実現部)宣言と、その下の コンパイラ指令 {$R *.dfm}の間が空白行になっているので、ここに「uses」と「 System.UITypes ;」を記述。なお、System.UITypes の後ろには行末を意味するセミコロン;を半角で入力する。

implementation

{$R *.dfm}

implementation の下に「uses」と入力してEnter & 字下げ(TABキー)、
で、次の行に「System.UITypes;」を記述。

implementation

uses
  System.UITypes;  // <-入力する

{$R *.dfm}

{$R *.dfm} はコメントではなく、dfmファイルを見つけて 実行ファイルにリンクさせるコンパイラ指令(命令)。「不要なコメントである」と勘違いして、消してはいけない。

以上が入力した状態。上書き保存(Ctrl+S)して、実行(F9)。メッセージにヒントが表示されないことを確認。 表示されたらメッセージ欄を確認。確認後、Formを閉じる。

警告もヒントも表示されない

(10) 埋め込みPythonと接続する

次に、いよいよ埋め込みPythonと接続する。Unitが表示されている場合はF12キーを押してFormの画面に切り替え、左下の「実行」ボタンをダブルクリックする。表示は自動的に以下のように、Button1Click手続きに切り替わる。

procedure TForm1.Button1Click(Sender: TObject);
begin

end;

初めにPythonのスクリプトを入れる文字列型リストと、Pythonから送られたデータを保存する文字列型リストをローカル変数として、以下のように宣言する。

procedure TForm1.Button1Click(Sender: TObject);
var
  //PythonのScriptを入れる
  strScrList:TStringList;
  //Pythonから送られたデータを保存する
  strAnsList:TStringList;
begin

end;

最初に、Memo1を初期化し、データの入れ物をそれぞれ準備する。

begin

  //初期化
  Memo1.Clear;

  //Scriptを入れるStringList
  strScrList:=TStringList.Create;
  //結果を保存するStringList
  strAnsList:=TStringList.Create;

end;

準備したStringListが処理の最後にきちんと解放されるよう、try文を用いて処理する。
tryと入力してEnterキーを押すと、次の画面のようにfinallyとend;が自動入力される。

begin

  //初期化
  Memo1.Clear;

  //Scriptを入れるStringList
  strScrList:=TStringList.Create;
  //結果を保存するStringList
  strAnsList:=TStringList.Create;

  try

  finally

  end;

end;

StringListの解放処理を先に書いてしまう。これで万一、トラブルが発生しても必ずStringListは処理の最後に解放(メモリが空く)される。

  //Scriptを入れるStringList
  strScrList:=TStringList.Create;
  //結果を保存するStringList
  strAnsList:=TStringList.Create;

  try

  finally
    //StringListの解放
    strAnsList.Free;
    strScrList.Free;
  end;

最後に、バッテリー残量を取得するPython Scriptを文字列型リストへ、1行ずつ書き込んで、Memo1に表示、Python側でMemo1に表示されたスクリプトを実行し、返ってきた結果を文字列型リストに読み込んで、Memo2に表示するコードを記述する。

  try
    //バッテリー残量を取得するPython Script
    strScrList.Add('import psutil');
    //バッテリー残量
    strScrList.Add('btr = psutil.sensors_battery()');
    //バッテリー残量を表示
    strScrList.Add('var1.Value = str("残量:") 
      + str(btr.percent) + str("%")');
    //Scriptを表示
    Memo1.Lines.Assign(strScrList);
    //Execute
    PythonEngine1.ExecStrings(Memo1.Lines);
    //結果を表示
    Memo2.Lines.Assign(strAnsList);
  finally
    //StringListの解放
    strAnsList.Free;
    strScrList.Free;
  end;

入力したら上書き保存(Ctrl+S)して、実行(F9)する。Formが表示されたら、Form上の「実行」ボタンをクリックする。結果は次のようになる。

Memo1には、意図した通り、StringListに入れたPythonのScritが表示されているが、
Memo2は空欄のままである。

Object Pascalのコードをよく読むとPythonEngineをExecuteしてPythonに電池残量を計算させるところまではOKだが、Pythonが計算した結果を「Delphi側が受け取れていない」ことがわかる。

    //Execute
    PythonEngine1.ExecStrings(Memo1.Lines);

    { ここでPythonからの結果通知を受け取る必要がある }

    //結果を表示
    Memo2.Lines.Assign(strAnsList);

(11) OnSetDataイベントを利用する

では、Pythonからの結果通知を受け取るにはどうしたらいいかというと、残念ながらその処理はこのprocedure内には書けない。

結論から言うと、Pythonの返した結果は、Formに配置したPythonDelphiVar1コンポーネントのOnSetDataイベントで受け取ることができる。その処理を実現するため、プログラムに必要な変更を加える。

まず、実行ボタンがクリックされた時の手続きの冒頭で、「結果を保存するStringList」として「strAnsList」というローカル変数を宣言したが、今、結果は「PythonDelphiVar1のOnSetDataイベントで受け取る」ことにした=つまり「別の手続きの中で受け取る」ことになるから、この変数をプログラムのあちこちから使える(見える)プライベートメンバー変数(クラス内部でのみ利用可能な変数) に変更することにする。以下、その処理を示す。

まず、 Button1Click手続きでローカル変数として宣言したstrAnsList変数をコメント化する。

procedure TForm1.Button1Click(Sender: TObject);
var
  //PythonのScriptを入れる
  strScrList:TStringList;
  //Pythonから送られたデータを保存する
  //strAnsList:TStringList;  //コメント化してしまう
begin

22行目付近のprivate部に、このクラス内部でのみ利用可能な プライベートメンバー変数として、strAnsList変数を再宣言する。

  private
    { Private 宣言 }
    //Pythonから送られたデータを保存する
    strAnsList:TStringList;
  public
    { Public 宣言 }
  end;

これでstrAnsList変数は、プライベートメンバー(クラス内部でのみ利用)化され、異なる手続きの中でアクセスできるようになった。

続けて、PythonDelphiVar1のOnSetDataイベントの処理を実装する。F12を押して画面をFormの方に切り替えて、PythonDelphiVar1をクリックして選択する。

選択する

画面左下のオブジェクトインスペクタにPythonDelphiVar1が表示されていることを確認して、イベントタブをクリックし、下にスクロールしてOnSetDataイベント部分の右の空白をダブルクリックする。

OnSetDataの右の空白をダブルクリック

PythonDelphiVar1SetData手続きが自動的に生成されるので、次のコードを記述する。

procedure TForm1.PythonDelphiVar1SetData(Sender: TObject; Data: Variant);
begin
  //値がセットされたら文字列リストに値を追加
  strAnsList.Add(Data);
  Application.ProcessMessages;
end;

これでPython側からDelphi側へ、計算結果を渡せるようになった。ここでは単純な処理しかしていないので実質不要であるが、例えばループ処理を行って何度も結果が返るなど、より複雑な計算処理をPython側で行わせる場合に、確実に結果を受け取れるよう、 Application.ProcessMessagesを「おまじない」として入れてある。

Application.ProcessMessages メソッドは、「Windows がイベントに応答できるようアプリケーションの実行を一時的に停止」する命令であるとのこと。このメソッドについては下記リンク先の説明が詳しい。

Article: 待ち関数の必要性

URL:http://gumina.sakura.ne.jp/CREATION/OLD/COLUMN/CD1MATI.htm

(12)プログラムの完成と動作確認

これで、最低限の機能だけは組み込んだノートPCの電池の残容量を表示するプログラムの完成である。上書き保存(Ctrl+S)して、実行(F9)し、結果を確認する。

電池の残量が表示された

4. PythonEngineのメモリリーク

参考 PythonEngineのメモリリークが起きた時は・・・

別のプログラムでPythonEngineがメモリリークを起こしたことがある。この問題について、次のようにFormのOnDestroyイベントでFinalize処理を行うよう対応したところ、メモリリークは解消された。備忘録として記しておく。

procedure TFormZZZ.FormDestroy(Sender: TObject);
begin
  //これでメモリーリークは発生しなくなった
  //PythonDLLによって割り当てられたすべてのメモリが解放される
  //旧バージョンのPythonEngineの場合
  //PythonEngine1.Finalize;
  //最新バージョン(2021年12月現在)のPythonEngineの場合
  PythonEngine1.Py_Finalize;
  PythonDelphiVar1.Finalize;
end;

5. Delphi11のIDEが真っ白になってしまう問題への対応方法

参考リンク Delphi11のIDEが真っ白になってしまう問題への対応方法

RAD Studio 11のプロジェクトファイル(.dproj、.cbproj)をダブルクリックしてIDEを起動し、デバッグ実行すると、IDEの各ウィンドウが白く表示される

URL:上のLinkをクリックしてください。

6.著作権表示の記載方法

参考:Python4DelphiのLicenseについて

GitHubのPython4Delphiのダウンロードページには「The project is licensed under the MIT License.」とある。これは「改変・再配布・商用利用・有料販売すべてが自由かつ無料」であること、及び使用するにあたっての必須条件はPython4Delphiの「著作権を表示すること」と「MITライセンスの全文」or 「 MITライセンス全文へのLink」をソフトウェアに記載する、もしくは、別ファイルとして同梱しなさい・・・ということを意味する。

したがってPython4Delphiを利用したプログラムの配布にあたっては、ソフトウェアの中で、次のような著作権表示を行うか、もしくは P4DフォルダのルートにあるLicenseフォルダをプログラムに同梱して配布すればよいことになる。

Python4Delphiを利用した場合の著作権表示の記載例:

Copyright (c) 2018 Dietmar Budelsky, Morgan Martinet, Kiriakos Vlahos
Released under the MIT license
https://opensource.org/licenses/mit-license.php

7.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】

Download Embeddable Python and Install the library

「埋め込み用Pythonのダウンロードとライブラリのインストール方法」

1.始めに
2.Embeddable Python をダウンロード
3.必要なライブラリをインストールする準備
4.Numpyのインストール
5.OpenCVのインストール
6.単体で動作確認(検証)
7.まとめ
8.お願いとお断り

1.始めに

なぜ、Embeddable(埋め込み用)なのかというと、内部的なデータ処理にPythonのOpenCV & Numpyライブラリを使うと、アプリケーションをより一層高速化できることがわかったから。
それから、Python環境のアップデートとは関係なく、安定動作する実行環境を、PCの操作にあまり詳しくないユーザーに提供できるから。

重要

このような特殊な目的ではなく、学習用にPythonを導入したい場合は、埋め込み用途に配布されている Embeddable Python はお勧めできません! 普通にインストーラを使用して、普通のPython環境をPCにセットアップしてください。

もし、PC環境を変更せずに、(持ち運びも可能な)Pythonが実行できる環境を作りたい場合は、WinPythonが便利! WinPythonならUSBメモリやSDカードにセットアップして、PC環境に変更を加えずに利用可能。なお、この場合は・・・

スタートボタン → 設定 → アプリ → アプリと機能 → その他の設定 → アプリ実行エイリアス → アプリ インストーラー(項目のいちばん下)のPythonとPython3をオフ

・・・にしてから、外部メディアにセットアップしたWinPythonを実行。

WinPythonのDL先URL:https://winpython.github.io/

WinPythonを外部メディアに入れて利用する場合

2.Embeddable Python をダウンロード

Embeddable Python は https://www.python.org/downloads/windows/ からダウンロード可能。

上記のサイトに行くと、古い2.X.Xから最新版の3.11.0(テスト用)まで、これまでにリリースされた Embeddable Python すべてがある。どれを選んでよいか、困ってしまう(実際、困った)。だから、使用目的(& 条件)に合わせてダウンロードする Embeddable Python を選択しなければならない。

私の場合、まず Stable Release (安定動作版:様々な動作検証がそれなりに行われたバージョンってこと?)であること。さらに、数値演算用のNumpyライブラリと、コンピュータの眼として利用する画像処理用のOpenCVがインストールできること。最低限、この3つを満たしていればOK!だ。

それから、32 or 64bitバージョンのどちらを選択するか、ちょっと迷ったが、よく考えたら(私が)、Delphi11で設定しているVCLのターゲットプラットフォームは32bitアプリケーション。だから32bitバージョンを選択すべきだと気付く。

ターゲットプラットフォームの設定はWindows32ビット(私の場合)

あとは・・・新しいのか、ちょっと前のか、すごく古いのか、どれを選べばいいんだろー??? 2.X.Xはもう既にサポートがないから、3.X.X なのは絶対だけど。。。3.6.X? 3.7.X? 3.8.X? それとも3.9.X? 最新版は3.10.1があるけどー。

うー。うーー。うーーー。(悩む私)

※ 実はマイナーバージョンごとの違いすらまったくわかってない。

たぶん(根拠無し)、最新版でいいだろー☆(←完全な思い込み)

単純極まりない私は、Stable Release のいちばん上にある

「Win7より前のOSには使えません」・・・って注意書きしかないし、この時点での私はNumpyが3.10.1に非対応(2021年12月現在)だということを、誰も教えてくれないから当然知らない(調べろ!)し、なにより、普通の人(?)は、最新版が取り敢えず良さそうに思えちゃうものじゃないですか。

3.10.1のダウンロード&解凍作業完了! 続いてライブラリのインストール。

コマンドプロンプトを開いて・・・。解凍先フォルダへ行って・・・。ラッタッタッタ。

python -m pip install numpy で、ポチ!

ERROR: Could not build wheels for numpy, which is required to install pyproject.toml-based projects

・・・と、表示され、あっけなく阻止される。なんでー

エラーメッセージの内容をよく読んでみると・・・

setup.py:63: RuntimeWarning: NumPy 1.21.5 may not yet support Python 3.10.

確かに。たいへんよくわかりました。はい。

インストールするライブラリが、どのPythonのマイナーバージョンに対応しているか? なんて、対応状況をあらかじめ調査するなんてこと、まずやるわけない私のようなド素人が(無茶を承知で) Python3.10.1 にNumpyライブラリを強制インストールする凶行に及んでも、ちゃんと阻止してくれるんですね。

できればこういう大事なことは、N○Kの朝と晩の7時の全国放送で毎日しつこくアナウンスするとか、誰もがTopページにしているであろう某サイトのいちばん見やすい場所に広告として日々表示してほしい☆・・・と夜空の星に願いつつ、

「使いたいライブラリがどのバージョンに対応しているか、ダウンロード前にきちんと調べる」という貴重な教訓を得て、ここで初めて検索キーワード「numpy python 対応バージョン」でGoogle先生にお伺いをたてると、以下の情報がヒット!

Python向け科学計算パッケージNumPyの開発チームは、最新版となる「NumPy 1.20.0」を1月30日(現地時間)にリリースした。
「NumPy 1.20.0」はこれまでで最大となるアップデートで、Python 3.7~3.9をサポートし、Python 3.6のサポートは終了している。

1月30日とあるのは2021年のこと。この記事は https://codezine.jp/article/detail/13574 より引用

わかった☆OK これでバージョン3.10.1は除外。とりあえず3.9.Xのどれかにしよう。

もうひとつ、どうしても入れたいのがコンピュータの眼「OpenCV」ライブラリ。そこで、PythonとNumpyとOpenCVの関係について調べてみると・・・

opencv-python 4.5.1.48が最新です。
pythonのバージョンは3.6以上とされていますが、numpyについては特に指定はありません。
pipのバージョンは19.3以上

teratailのPythonに関する質問(https://teratail.com/questions/323063)より引用

わかった☆OK これを近所の3歳児でもわかるように言い換えてみよう。

OpenCVとNumpyは仲がイイ。

ダウンロードするPythonのバージョンは、この情報をもとに 3.9.X の中でいちばん新しい 3.9.9 に決定。

理由は次の通り。

Pythonのバージョンを意味する番号は前から順に、メジャー.マイナー.マイクロのそれぞれを意味するそうで、Pythonのメジャーバージョンは2or3。サポート状況から、これは当然「3」を選択。マイナーバージョンは、これもやはりサポート期限を考えるといちばん長いのは3.9.Xで「2025年10月」までだから、これを根拠に「3.9.X」に決定。で、さらにマイクロバージョンは「バグ修正リリース」に相当し、マイクロバージョン間については、互換性が保証されるとのこと。ならば最もバグが消えているのは「3.9.9」なのかなー。みたいな・・・

Pythonのバージョンによる違いについては、次のサイトの解説が詳しい。

Pythonの複数バージョンの扱い方(Windowsの場合)

URL:https://gammasoft.jp/python/python-version-management/

あらためて気合を入れなおし Embeddable Python3.9.9 のダウンロードを持てる全力を挙げて決行!

(正直 ポチ!するだけだけど)

控えめに言えば、Python3.9.9-32bitのEmbeddable Packageを選択してダウンロード。

3.必要なライブラリをインストールする準備

ダウンロードした Package を任意のフォルダに解凍し、ライブラリのインストールに pip が使えるよう、設定を変更( pythonNN._pthファイルを修正 )する。

デスクトップに新しいフォルダーを作成して、そこにDLしたPackageを保存(Zipファイルの大きさはたったの7.3MB!)。

これを解凍すると、

python-3.9.9-embed-win32ができる(大きさは14.0MBとかなり小さい)

python-3.9.9-embed-win32 フォルダを開き、pythonNN._pthファイルを見つけて修正を加える(NNはPythonのバージョンを示す数字)。その方法は下記の通り。

→ バージョン3.9.9をダウンロードしたから、修正するファイルは python39._pth。見つけたらテキストエディタで開いて、いちばん下の行・・・

このナンバーを削除する→ # import site

を、

import site

と コメント解除 する。(※ 正確には、削除するのは#とその後ろの半角スペース)

【補足】
3.9.10では「#import site」となっており、ナンバー#の後ろには「半角スペースがありません」でした!(20220822追記)

コメント解除したら、上書き保存(Ctrl+S)する。

※ 以前、こんな場面で「上書き保存」ではなく「名前を付けて保存」し、あろうことか、ファイル名が「例:XXXXX._pth.txt」になってしまったコトが・・・

次に、ライブラリのインストールに必要な pip を実行するためのScriptファイル get-pip.py を入手する。get-pip.py は次のリンクからダウンロードできる。ちなみにダウンロードした get-pip.py をテキストエディタで開いたら、内容が知らない言語(もしかして、コレが宇宙語?)で書かれており、驚愕。びっくり。もうあけない。

get-pip.py の入手先はこちら(https://bootstrap.pypa.io/get-pip.py

で、ダウンロードした get-pip.py を python-3.9.9-embed-win32 フォルダへコピー。これで get-pip.py が使えるので、次に説明する方法で、まずpipをインストール。

ここからはコマンドプロンプトで作業する(PowerShellでは、モジュールエラーとなり、実行出来ないようだ:情報のみ、未検証です)。

スタートボタンを右クリック→ファイル名を指定して実行→「cmd」と入力して「OK」をクリック→コマンドプロンプトが起動→「cd」+半角スペースを入力→エクスプローラーから「 python-3.9.9-embed-win32 フォルダ」をドラッグ&ドロップしてEnterキーを押す。

で、画面に表示されている > の後ろに「python get-pip.py」と入力してEnterキーを押す(下図赤のアンダーライン部分)。正しく操作が行われていれば、下の画面のようにpipのダウンロードとインストールが自動的に行われる。

pipをインストール(この時点でフォルダ全体の大きさは29.7MB)

Consider adding this directory to PATH(このディレクトリをPATHに追加することを検討してください)と警告されるが、これは気にしない。Embeddable Python を使う目的そのものが、PATHなんかどこにも通さずに

「好き勝手にPythonを使う」

ことだから。

参考:もし、ここで「’python’ は、内部コマンドまたは外部コマンド、操作可能なプログラムまたはバッチ ファイルとして認識されていません。」というエラーが出る場合は、コマンドプロンプトの現在位置(カレントディレクトリ)をよく確認すること。Python.exeがある(見える)フォルダじゃないと、>python ~ コマンドは使えない。

pipがきちんとインストールされたことを、ここで確認しておく。

python -m pip list と入力してEnter

問題がなければ、インストールされたpip他のバージョンが表示される。
「python -m pip list」で「python.exe: No module named pip」が返る場合は、 pythonNN._pthファイルの修正(# import siteの前にある記号#(ナンバー)とその後ろの半角スペースを削除して import site だけにするコメント化の解除手続き)が正しく行われていない可能性が高い。
また、複数のライブラリのインストールを行うと、 pythonNN._pthファイル が修正前の状態に戻されてしまうこともあるようだ。要確認。

4.Numpyのインストール

続いて「愛しのNumpy」をインストール。

>python -m pip install numpy と入力してEnter!

「生きていてよかった」と思える至福の一瞬がここに。

警告:Consider adding this directory to PATH (このディレクトリをPATHに追加することを検討してください) は、まったく気にしない。Numpyが入ればいいのだ。わはは*(^_^)*♪

5.OpenCVのインストール

さらに、視力0.01かつ老眼&緑内障の恐れありと診断(2万ン千円も払ったのにイタいことばかり言いやがって:チ○ショー!「我が愛と哀しみの人間ドック2021年の記録」より抜粋)された私の眼に代わるSecret Weapon、目にも止まらぬ 走召 高速!でマークシートを読んでくれる機械の眼という意味がほぼない長い前置きを乗り越え、今、怒涛のクライマックス。「OpenCV」ライブラリがいよいよ My PC へ!

サぁイレント ナァイ~ ホぉリィ ナァイ~(さらに意味なし)

>python -m pip install opencv-python と入力してEnter!

注意:「opencv」に続けて「-python」が必要。

念願のOpenCVのインストールについに成功した・・・その日、彼は狂喜乱舞して泣き崩れたという。彼の日記の末尾には「OpenCVよ。永遠なれー」の文字が。

ちなみに、この時点で「Numpy」と「OpenCV」を入れた「python-3.9.9-embed-win32」フォルダの内容は152MB!と他を圧する勢いで巨大化していた。最初は15MB程度しかなかったのに10倍に膨れ上がっている・・・。

なんということか。すでに語るべき言葉を私は持たない。大きな広い美しい心で、この変化をありのままに・・・、そうだ、謙虚に受け止めよう。さぁ深呼吸だ。おぉ空気がうまい。生きてるってことは素晴らしい。

そう言えば、私が書いたDelphiのプログラムをことごとく「ウイルス扱い」して「隔離」しやがる某有名ウイルス対策ソフトも、今日は静かにしてるじゃないか。人間、すべからく、受容することが肝心だ。別にPCの重さがいつの間にか10倍になって、持ち運び困難になったわけではないのだから。

6.単体で動作確認(検証)

作成したEmbeddable Pythonのフォルダ「python-3.9.9-embed-win32」は名前が長く、ちょっと扱いにくいので、フォルダ名をもう少し短く、わかりやすい名前に変更してから、動作検証を行う。

変更前: python-3.9.9-embed-win32 → 変更後:python39-32

フォルダ名の意味:前から順に「Pythonが入っているフォルダで、そのメジャーバージョンは「3」、マイナーバージョンは「9」で、ターゲットプラットフォームは32ビット版だよ」と、全世界のユーザーにやさしくPR(どこかのサイトでこの表記法を見て感動!)。

【動作検証の準備】

上で作成した「python39-32」フォルダと同じ階層に、新しく「psf」という名前のフォルダを作成する。ここにテスト用のScriptファイルや画像データを保存する。

【説明】psf:「P」ythonの「S」criptが入っている「F」older ・・・ という意味。

データ保存用の psf フォルダを作成

【動作検証用の環境変数設定バッチファイルを作成】

最終的にはDelphiから操作する予定のEmbeddable Pythonだが、ここでは動作検証用のバッチファイルを作成し、これを起動してテスト用のScriptを走らせる。

最初に環境変数をセットするバッチファイルを作成する(バッチファイルの作成に関しては、下記参考リンク先:「Windowsでpythonを使う/配布する時に便利!Python embeddable package使い方」に大変詳しい解説があります。作成した方に心から感謝 m(__)m )。

以下の3行をテキストエディタに入力(コピペ)し、文字コードはUTF-8を指定して「setmyenv.bat」という名前を付けて、上の図の「新しいフォルダー」に保存する。

SET DP0=%~dp0
SET PATH=%DP0%\python39-32;%PATH%
SET PYTHON_PATH=%PYTHON_PATH%;%PYTHON_PATH%\Scripts

1行目で、バッチファイルのあるフォルダをカレントディレクトリに指定
2行目で、PATHにEmbeddable Pythonを入れたフォルダへのパスを設定
3行目で、Python.exeとpip.exeへのパスを設定

【動作検証用のスクリプト実行バッチファイルを作成】

続いてScriptを実行するためのバッチファイルを作成する。 以下の5行をテキストエディタに入力(コピペ)し、文字コードはUTF-8を指定して「python_script.bat」という名前を付けて「新しいフォルダー」に保存する。

@echo off
cd /D %~dp0
call setmyenv.bat
cd psf
cmd

1行目は、コマンドプロンプトの画面表示を抑制して見やすくする
2行目は、 バッチファイルのあるフォルダをカレントディレクトリに指定
3行目は、環境変数設定用バッチファイルを内部的に呼び出して実行
4行目で、画面に表示するディレクトリへ移動
5行目は、コマンドプロンプトを表示する

フォルダとファイル構成

【検証用スクリプトを作成】

Embeddable PythonにインストールしたNumpyとOpenCVをインポートして動作する検証用のScriptを作成する。 以下の内容をテキストエディタに入力(コピペ)し、文字コードはUTF-8を指定して「test.py」という名前を付けて「psf」フォルダーに保存する。

import numpy as np
import cv2

img = cv2.imread("test.jpg")
print(type(img))   # Numpy配列に画像データが読み込まれたことを確認
print(img.shape)   # OpenCVが読んだ画像情報(縦横画素数他)を表示

【検証用画像を用意】

任意のJpeg形式の画像を「test.jpg」という名前で「psf」フォルダーに用意する。画像ファイル名に日本語は使えないことに注意する(OpenCVの読み書きコマンドは日本語に対応していないため、日本語が混じっているとエラーになる)。この問題への対応方法は下記参考リンクをご参照ください。

psf フォルダの内容

【検証】

(1)「python_script.bat」 をダブルクリックしてコマンドプロンプトを起動。

コマンドプロンプトを起動したところ

(2)赤で示した下線部に「python test.py」と入力してEnterキーを押す。

黄色の枠内に結果より正しく動作したことがわかる。
<class ‘numpy.ndarray’>:データ形式はNumpyの配列、
(284, 283, 3)は、縦・横の画素数とチャンネル数を示す。

【参考URL】

Windowsでpythonを使う/配布する時に便利!Python embeddable package使い方

URL:https://hituji-ws.com/code/python/python-emb-usage/

Python OpenCV の cv2.imread 及び cv2.imwrite で日本語を含むファイルパスを取り扱う際の問題への対処について

URL:https://qiita.com/SKYS/items/cbde3775e2143cad7455

WindowsでPython3.7の実行環境を手早く作る方法

URL:https://qiita.com/hirohiro77/items/377dfc0a264acb3db222

7.まとめ

(1)使用目的や使用条件、必要なライブラリのインストール上の制約(どのバージョンのPythonに対応しているか)、何bitのアプリケーションに埋め込むのか等、事前に必要事項を十分調査した上でダウンロードするEmbeddable Pythonのバージョンを決める。

(2)ライブラリのインストールは必ず「Python -m」を付ける。→ 付けないとモジュール参照パスの指定等に問題が発生(構成を壊してしまうとの情報あり:参考リンク「WindowsでPython3.7の実行環境を手早く作る方法」を参照)するようだ。

Python -m pip install (ライブラリ名)

(3)必要なライブラリをインストール後、実際にそれらをimportして動くPython Script をEmbeddable Pythonで動かし、確実に動作することを確認する。Delphiに埋め込んでから余計なトラブルに悩まされないよう、ここで必ず単体で動作することを確かめておく。

8.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】

Delphi & Embeddable Python

「なぜ Delphi & Embeddable Python なのか?」

自分ひとりで使うにはPythonはとても便利だ。カプセル化してある高機能なライブラリのおかげで、わずか数行Scriptを書くだけで、とんでもない処理が誰にでも簡単に実現できる。

必要な大抵の処理は、自分で書かなくても、どこかの優秀な方が作ったサンプルが、Web上のあちこちで公開されているから、ほとんどすべてそれで間に合ってしまう。だから、Pythonに関する限り、自分で書くというよりは、誰かが書いたものを探している時間の方が多い・・・というのは、私だけではないだろう。

それらを写経して、切ったり、貼ったりして業務をこなす。便利であること、この上ない。ラクをしたその分だけ、プログラミングする楽しさや喜びが失われたような、そんな気がすることもあるが・・・。

ただ、他人様に使っていただくモノについては、これが当てはまらない。

「マニュアルを読まなければ使えないようなプログラムは、ダメなプログラムだ。」・・・という、もはや信念と化した、狂気に近い思い込みが私にはある。

「マニュアルを読まなくても使えるプログラム」

それを実現するのがGUIなのだが、簡単・高速に、そのインターフェイスを作る機能は残念ながらPythonにはない。tkinterやPyQtを試したこともあったけど、Delphiのようにはいかなかった。直感的な操作という点で、どうしてもPythonで使えるGUI環境作成ツールはどれもこれもDelphiのそれに見劣りする(・・・と私は思う)。

唯一、2018年から開発が始まったというPySimpleGUIだけは、ちょっと違ったが。

さらに、実行形式のexeファイルにする作業もPythonだと困ることが多い。以前、業務で使用するプログラムをPythonで書き、exe化したら何と300MBを超える巨大なexeができちゃった・・・ことがある。ちゃんと動いたけど。必要なライブラリを全部!詰め込んだから、おなかいっぱいになっちゃった・・・んだろう。たぶん。

ところで逆に、Delphiで業務で使用するマークシートリーダーを開発した際、Delphiから利用できるOpenCVライブラリを使ったのだが、100枚読み取るのに4~5分を要した。読み取るA4横のマークシートは1枚が「1行あたりマーク数16個×25行×3列」という仕様(これは必須)なので、1枚あたり判定必要数はなんと1200! で、これが100枚あるとすると合計12万!

PCは、マークされている場所だけ読み取る・・・なんてヒト並みの芸当は絶対にできないから、白紙のマークシートであっても地道に1個1個・・・1枚についてきちんと1200回、白・黒の判定を繰り返す(実際の処理は、スキャナーで読み取ったマークシート画像にゴミ取り用のガウシアンぼかしをかけてから、ある閾値で二値化して、白黒反転させ、1行ずつ元画像から切り出して、さらにその画像を1行あたりのマーク数で細かく均等に分割して、1枚について1200個生成される画像1つ1つについて画素が白の部分の面積を計算し、白面積が最も大きい画像をマークありぃ!と判定している)。

私なら、1枚でやめます。・・・ってか、1行分でも多分無理です。

読み取りに「5分」かかったとすると、5分は300秒。12万個のマークを300秒で読むから、1秒あたりの読み取りマーク数は400個。1枚に3列(1200個)あるから1列1秒、1枚3秒で読んでおり、ヒトがそれをやるのに比べれば、これでも十分に高速なのだが・・・。

ところがPythonで同じ処理を書いてみたら、速いのだ。コレが・・・。

1枚250ms以下で読み取ってしまう。処理の流れはどちらも同じ(どちらも書いたのは私)だから、Python環境での処理速度は、Delphi環境のそれの12倍も速いことになる・・・。100枚を30秒未満で処理できる実力。これをどうにかして生かしたい。

そんな時、Embeddable Python というモノが存在することを、私は知ってしまったのだ。

Python Embeddableとは、超軽量なPythonの実行環境でファイルサイズがとても小さく、Windowsのシステムを汚さずに環境構築ができ、配布するのも簡単という特徴があります。

Webエンジニアの仕事見聞録(https://engineer-milione.com/programming/python-embeddable.html)より引用

Delphiで創ったコレが・・・

拙作Delphi製マークシートリーダー(テスト用サンプルを読み込んだところ)
拙作マークシートリーダーは上記リンク先ページからダウンロードできます。

PythonのOpenCVという視力を得たなら・・・どういうコトになるか?と思うと・・・

年甲斐もなく、ドキドキしてくるじゃありませんか! 皆さん

まとめ

(1)DelphiはGUI環境を簡単・高速に作成できる。

(2)Pythonには強力無比の数値演算ライブラリがある。

(3)DelphiでGUIを作成し、内部的な演算処理はPythonで実行。

(4)それを可能にするのがEmbeddable Python

(5)誰が言ったか知らんけど、

為せば成る!

俺はやるぞ!

お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】

Setup Old Python4Delphi

「Delphiで古いPythonForDelphiを使う(おすすめしません)」

OpenCVとNumpyをインストールしたembeddable pythonをDelphiから利用できるようにした。これはその覚書その2。タイトルにあるように古いPython4Delphiをセットアップした時の記録。

1.どなたにもおすすめしません(最新版が便利です)
2.旧バージョンのインストール方法
3.まとめ
4.著作権表示の記載方法
5.お願いとお断り

1.どなたにもおすすめしません(最新版が便利です)

今はどこを探しても、この古いPython4Delphiはダウンロードできないが、もし、それが入手できて、使わなければならなくなった時には参考になる(カモ)。

ちなみに、ずっと愛用していた(10年以上前のバージョン?の)Python4Delphiは最新のDelphi11に、ここに記載した方法でほぼ問題なくインストールでき、かつ、期待通りに(VCLコンポーネントとして)動作した。←が、どなたにもおすすめしません。

最新のPython4DelphiをDelphi10.3以降のバージョンにインストールする方法は・・・

2.旧バージョンのインストール方法

以下の内容を利用される場合は、自己責任でお願いします。以下に記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

まず、困ったことに、ここで取り上げているPython4Delphiのバージョンがいくつなのか、どれくらい前にリリースされたものなのか、いつ、どこから入手したものなのか、いずれもわからない。

気が付いた時には、My PCの中にいた・・・。そんな存在である。

python4delphi-master\PythonForDelphiにあるDeployment.txtには、See document “Deploying P4D.PDF” first.・・・とあるので、これを読むとドキュメントの日付は「5/1/2005」となっている。もしかしたら、それくらい前のものかもしれない。

fmxには非対応のようで、vcl関連のファイルのみで構成されている。Readme.txtで紹介されているファイルとフォルダの構成は以下の通り。

FILES:
Readme.txt This file.
Python.txt Infos about Python, and further references.
Changes.txt List of all changes since the first release.
Tutorial.txt A simple tutorial to use the PythonEngine
To do.txt A to do list.
Deploying P4D.pdf Notes on the Deployment of your applications using Python for Delphi.
C++ Builder Notes.txt Notes on using C++Builder with the Python for Delphi components.
PythonAtom.hlp A help file explaining the use of TPythonAtom
Demos A folder containing several demos of Python for Delphi.
Components\Python.* The “Python for Delphi” packages.
Components\Sources\Core The source folder of the core “Python for Delphi”.
Lib Library of Python modules.
PythonIDE A Python developpment environment written in Delphi.
See PythonIDE\Readme.txt for the required components.
Modules Contains the Delphi\Delphi.dpr project that creates the Modules\Delphi.pyd Python module
that allows you to interact with Delphi VCL objects from Python.

同じく Readme.txt にあるインストール方法は、次の通り。この手順でDelphi10.4にインストール。

INSTALLATION:
install the Python for Windows distribution (http://www.python.org/).

1) Install the core components
For recent versions of Delphi, install the “Python_d” package located in the
Components folder and add the folder “…\Components\Sources\Core” to the library path.

1) コアコンポーネントのインストール

Components フォルダにある “Python_d” パッケージをインストールし、ライブラリパスに “…\Components\Sources\Core” フォルダを追加してください。

注意:異なるバージョンのDelphiがインストールされている環境では、Python_D.dpkをダブルクリックすると拡張子dpkに関連付けされたバージョンのDelphiが起動してしまう(あたりまえ)。このような場合は、P4D環境をインストールしたいDelphiを起動し、ファイルメニューの「開く」からPython_D.dpkを指定してパッケージをインストールする。

また、「開く」のは「Python_D.dpk」で、「Python_D.dproj」ではないことにも注意する。で、「Python_D.dpk」を開いたら・・・

プロジェクトマネージャーに表示されたPython_D.bplを右クリックして、表示されたサブメニューの「インストール」をクリック。

【Delphi10.4の場合】

この方法でエラーなくインストールできた。(・・・気がするだけかもしれない)

【Delphi11の場合】

次のエラーが発生!

[dcc32 エラー] PythonEngine.pas(63): E2029 ‘INTERFACE’ が必要な場所に 識別子 ‘Error’ があります。

エラーが起きている場所を確認すると・・・

unit PythonEngine;

{ TODO -oMMM : implement tp_as_buffer slot }
{ TODO -oMMM : implement Attribute descriptor and subclassing stuff }

{$IFNDEF FPC}
{$IFNDEF DELPHI2010_OR_HIGHER}
  Error! Delphi 2010 or higher is required! ←ここでエラーが発生!
{$ENDIF}
{$ENDIF}

とりあえず、この1行をコメント化して再実行。

{$IFNDEF FPC}
{$IFNDEF DELPHI2010_OR_HIGHER}
  //Error! Delphi 2010 or higher is required!
{$ENDIF}
{$ENDIF}

エラーは発生せず。表示されたメッセージを読み、インストールの成功を確認。

もう一度Python_D.bplを右クリックして、表示されたサブメニューの「上書き保存」をクリック。これでパッケージのインストールは完了。

「ライブラリパスに “…\Components\Sources\Core” フォルダを追加・・・」とあるが、パスを追加しなくてもプログラムの動作に必要な.pasファイルをプロジェクトファイルのあるフォルダにコピーすれば動くから、ここでは「追加しない」ことを選択。

重要 特別な理由のない限り、最新版のPython4Delphiを選択することをお勧めします。
(最新版のP4Dパッケージを登録する場合は、ライブラリパスをきちんと設定しましょう)

2) Install the VCL components (this is optional)

For recent versions of Delphi, install the “PythonVCL_d” package located in the Components folder and add the folder “…\Components\Sources\Core” to the library path.

2) this is optional ・・・とあるので、オプションならやらなくてもいいか!ということで実行しない。

3) Build Modules\Delphi\Delphi.dpr (This is optional and unsupported)

Once the project is build you can either extend the Python path with ..\Modules or copy ..Modules\Delphi.pyd to C:\Python24\DLLs, to be able to import the Delphi module from Python.

Note that you can try this module by invoking the ..\Modules\TestApp.py script.

3) This is optional and unsupported ・・・とあり、オプションである上にサポートなしとあるので、これも実行しない。

3.まとめ

(1) Readme.txt の INSTALLATION の手順1)のみ実行すればOKだった。

(2)DelphiのXXX.dprojファイルのあるフォルダへコピーするPython関係のファイルは以下の通り。他のプロジェクトでも利用する場合は、ライブラリパスへ登録した方が使いやすくなるが、このP4Dは最新版ではないので、このようにして利用した(←過去形であることに注意)。

動作に必要なファイル

4. 著作権表示の記載例

参考:Python4DelphiのLicenseについて

GitHubのPython4Delphiのダウンロードページには「The project is licensed under the MIT License.」とある。これは「改変・再配布・商用利用・有料販売すべてが自由かつ無料」であること、及び使用するにあたっての必須条件はPython4Delphiの「著作権を表示すること」と「MITライセンスの全文」or 「 MITライセンス全文へのLink」をソフトウェアに記載する、もしくは、別ファイルとして同梱しなさい・・・ということを意味する。

したがってPython4Delphiを利用したプログラムの配布にあたっては、ソフトウェアの中で、次のような著作権表示を行うか、もしくは P4DフォルダのルートにあるLicenseフォルダをプログラムに同梱して配布すればよいことになる。

Python4Delphiを利用した場合の著作権表示の記載例:

Copyright (c) 2018 Dietmar Budelsky, Morgan Martinet, Kiriakos Vlahos
Released under the MIT license
https://opensource.org/licenses/mit-license.php

5.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】

Setup Python4Delphi

「DelphiからPythonを使えるようにする」

追々記(20231208)

さらにカンタンな方法がありました!

以下、上記の方法にたどり着くまでの、長い長い歩みの記録です。

追記(20231126)

RAD Studio 12.0(Delphi 12.0 Athens)のリリースに合わせ、Python4Delphi も更新されました。
RAD Studio 12.0(Delphi 12.0 Athens)へのインストールに対応した Python4Delphi (20231109版)のインストール記事は、以下のリンク先にあります。

RAD Studio 12.0(Delphi 12.0 Athens)に Python4Delphi をインストールされる場合は、こちらをご参照ください。

以下、2021年12月31日に掲載した記事です(内容は当時のままです)。

OpenCVとNumpyをインストールしたembeddable pythonをDelphiから利用できるようにした。これはその覚書。

1.Python4Delphiのダウンロード
2.P4DパッケージをDelphiにインストール
3.ライブラリパスの設定
4.P4Dの著作権表示の記載例
5.お願いとお断り

1.Python4Delphiのダウンロード

まず最初に、Python for Delphi(P4D)をGitHubから入手してDelphiにインストール。

P4Dの入手先URL https://github.com/pyscripter/python4delphi

Git Bashがない場合は、Codeをクリックすると表示されるサブメニューのいちばん下にDownLoad ZIPがあるので、これをクリックしてZIPファイルをダウンロードし、任意の場所(フォルダ)に解凍する(ここではダウンロードするフォルダの名前を「P4D」として説明)。

Download ZIPをクリック

Git Bashがある場合は・・・

Codeをクリック → 表示されるサブメニューからURLをコピー

で、Git Bashがあれば開く。

Git Bashは公式サイト https://gitforwindows.org/ から入手可能。

Git Bashでは、ls(エルエス)コマンドで今いる場所が表示され、cdコマンドでディレクトリの移動ができる。今、いる場所の直下のフォルダに移動するのであれば、Git Bashの画面に直接「cd フォルダ名」と入力してEnterキーを押す。

今、いる場所の直下に新しくフォルダを作成する場合は「mkdir フォルダ名」と入力してEnterキーを押す。

1階層上に移動したい場合は、「cd ../」と入力してEnterキーを押す。

階層の深いフォルダへ移動したい場合は、「cd」+半角スペースを入力後、そのフォルダをGit Bashの画面上へドラッグ&ドロップすればOK!

ここでは、Git Hubのリポジトリをクローンする「中が空の任意のフォルダ」を選ぶ(上の図ではあらかじめ作成しておいたP4Dフォルダを選んでいる)。

フォルダの内容が空であれば、フォルダの名前は何でもOKだが、後のPython4Delphiのインストール時に、「インストール元フォルダとして選択するフォルダとなる」ことに十分注意(フォルダ名を忘れないように)する(ここではフォルダ名を「P4D」としている)。

Git cloneと入力し、半角スペースを入れ、画面を右クリックして表示されるサブメニューのPasteをクリック。Enterキーを押すとダウンロードが始まる。

Enterキーを押してダウンロード開始。
ダウンロード終了時の画面

※ Git Bashがない場合は、Codeをクリックすると表示されるサブメニューのいちばん下にDownLoad ZIPがあるので、これをクリックしてZIPファイルをダウンロードし、任意の場所(フォルダ)に解凍する。(再掲)

2.P4DパッケージをDelphiにインストール

ここで超重要ポイントがひとつ

ダウンロードが無事完了すると、P4Dフォルダの中には「python4delphi」フォルダが出来ている。

(ZIPファイルを解凍した場合は「python4delphi-master」フォルダが出来る)

このフォルダの名前を手動で「P4D」に変更(リネーム)する。

「P4D」フォルダの中(1階層下)に「P4D」フォルダがあることになるが、これでOK! 理由は以下の通り。

C:\XXX\P4D\P4D\Install\Readme.mdには、以下の記述が・・・

P4D Installation using MultiInstaller

Use for Delphi Seattle (10.4) or later to install all packages in one step.

  1. Clone or copy the Python4Delphi git repository to a folder of your choice. The setup.ini file assumes that the folder is called “P4D”. If you chose to name your folder differently then modify the “Folder” option in setup.ini.
  2. Close all Delphi IDEs running.
  3. Run MultiInstaller.exe
  4. Select the packages you want and press Next
  5. In the dialog box specify the parent folder of “P4D” (i.e. the folder containing the directory to which you have copied Python4Delphi) and the Delphi target version. Then press Next to install the components

Google先生曰く・・・

MultiInstallerを使用したP4Dインストール

Delphi Seattle(10.4)以降で使用して、すべてのパッケージを1つのステップでインストールします。

  1. Python4Delphigitリポジトリのクローンを作成するか選択したフォルダーにコピーします。 setup.iniファイルは、フォルダが「P4D」と呼ばれることを前提としています。フォルダに別の名前を付けることを選択した場合は、setup.iniの[フォルダ]オプションを変更します。
  2. 実行中のすべてのDelphiIDEを閉じます。
  3. MultiInstaller.exeを実行します。
  4. 必要なパッケージを選択して、[次へ]を押します。
  5. ダイアログボックスで、「P4D」の親フォルダ(つまり、Python4Delphiをコピーしたディレクトリを含むフォルダ)とDelphiのターゲットバージョンを指定します。次に、[次へ]を押してコンポーネントをインストールします。

Python4Delphiをコピーしたディレクトリを「P4D」にリネームして、さらに、インストール時に表示されるダイアログボックスでは・・・

「その親フォルダを指定せよ」

と言っている・・・。

C:\XXX\P4D\P4D\Installにある「MultiInstaller.exe」を起動
デフォルトですべてにチェックが入っている。そのままNextをクリック。
親の方のP4Dフォルダを選択し、OKをクリック
Compile packages and install on IDE にチェック(My環境ではDelphi11しかないので、チェックを入れると自動的にRAD Studio 11 Alexandriaのオプションが選択された)。

複数バージョンのDelphiがインストールされている環境であれば、インストールしたいバージョンを選択することになるはず。

Nextをクリックして続行。無事終了すれば下のような画面が表示される。

Delphi11にも無事インストールできた(あらかじめInstallフォルダ内にあったSetup.iniを確認したところ、Pathの設定に10.4+とあるから大丈夫と思ったが)。Finishをクリックしてインストール終了。

DelphiのIDEを起動して確認。

パレットに7匹のヘビを無事発見。

3.ライブラリパスの設定(確認)

追加したP4Dのパッケージを使用する場合、パッケージをインストールした後で、
「ツール」→「オプション」→「言語」→「Delphiオプション」→「ライブラリ」の順にクリックして下の画面を表示する(Delphi11の場合)。

「ツール」→「オプション」で上の画面が表示されるので、左ペインでさらに「言語」→「Delphi」→「ライブラリ」と進み、次に右ペインのライブラリパス(B)の赤枠囲みの…をクリックする。

表示される画面で、ライブラリのSourceファイルがあるフォルダのパスを登録する。ライブラリのパスの設定はターゲットにするそれらのプラットフォームごとに設定する必要がある。上の画面では 「Windows 32ビット」 のプラットフォームに対して設定している。 必要であれば、「Windows 64ビット」 のプラットフォームに対しても設定する。

ライブラリのSourceファイルは、PCを変更した場合でも容易に参照できるよう、
絶対に忘れない場所に置くようにしている。
また、上の例では最新版のP4DのSourceが階層構造を持っているため、共通利用するものとそうでないもの(vcl/fmx)を、それぞれ分けて登録している。

コンパイルを実行すると、Delphiはいちばん最初にプロジェクトファイル(.dproj)のあるフォルダ(ここはパスが通っているから登録は不要)を検索し、必要なユニットファイル等の有無を確認。もし、そこに必要なファイルがなければ、この画面に登録したライブラリパスを検索するようだ。

まとめ

(1)Python4Delphiをダウンロードするフォルダの名称は任意だが、そこに作られるフォルダ「 python4delphi 」は「P4D」にリネームする。

(2)MultiInstaller.exeを実行してインストール先フォルダを指定する際には、上でリネームした「P4D」フォルダの1階層上のフォルダを指定する。

(3)パッケージのインストール後、コンパイル時に必要なSourceファイルのある場所をライブラリパスに登録する。

4.P4Dの 著作権表示の記載例

参考:Python4DelphiのLicenseについて

GitHubのPython4Delphiのダウンロードページには「The project is licensed under the MIT License.」とある。これは「改変・再配布・商用利用・有料販売すべてが自由かつ無料」であること、及び使用するにあたっての必須条件はPython4Delphiの「著作権を表示すること」と「MITライセンスの全文」or 「 MITライセンス全文へのLink」をソフトウェアに記載する、もしくは、別ファイルとして同梱しなさい・・・ということを意味する。

したがってPython4Delphiを利用したプログラムの配布にあたっては、ソフトウェアの中で、次のような著作権表示を行うか、もしくは P4DフォルダのルートにあるLicenseファイルをプログラムに同梱して配布すればよいことになる。

Python4Delphiを利用した場合の著作権表示の記載例:

Copyright (c) 2018 Dietmar Budelsky, Morgan Martinet, Kiriakos Vlahos
Released under the MIT license
https://opensource.org/licenses/mit-license.php

5.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。ここに記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

【関連記事】