月別アーカイブ: 2023年3月

自己一致

幼い頃は、欲しかったおもちゃを買ってもらえたら、もうそれだけで幸せでした。
ひとは成長するにつれて、欲しいものを手に入れたり、内なる希望を実現するには、苦労を伴う努力が必要なことを知ります。

ひとがなぜそのように成長するのか?
あくまでも「僕にとって」という但し書き付きなのですが、その答えを知る「きっかけ」となったのがマズローの心理学との出会いです。

Abraham Harold Maslow(アメリカ合衆国の心理学者、1908 – 1970)

また、すべての物事には、始まりに必ず、何らかの「きっかけ」があり、「きっかけ」を得て初めて、ひとの「心」に変化が起きるわけですから・・・このプロセスが非常に重要で、ひとの想いと行動を考える上では、物事の始まりとなる「きっかけ」は、絶対に見落としてはならない、重要な要素であると思います。

マズローは自身の発表した欲求5段階説で、人間の欲求を「生理的欲求」「安全の欲求」「社会的欲求」「承認の欲求」「自己実現の欲求」の5つの階層に分類し(後に、自己実現の欲求の、さらに1階層上に「自己超越の欲求」を付け加えますが)、人はより低次元の欲求が満たされて初めて、より高い次元の欲求の実現を求めるようになると、定義しました。

【マズローの欲求5段階説】

6.自己超越の欲求・・・自らを犠牲にしてもよいから、貢献する/理念の実現を図る。
5.自己実現の欲求・・・自分らしい自分を生きるために満たしたい欲求。
4.承認の欲求・・・他者から認められたいという欲求。
3.社会的欲求・・・人間社会の集団に所属したい欲求(所属と愛情の欲求とも)。
2.安全の欲求・・・安心かつ安全な環境の中で暮らしたいという欲求。
1.生理的欲求・・・睡眠等、生きるために最低限満たされる必要がある欲求。

ひとの成長期の前半から後半にかけて、このマズローの欲求5段階説の3階層目及び4階層目である「社会的欲求」や「承認の欲求」が現れ、ひとはその欲求を満たすため、様々に努力します。

社会の仕組みそのものが、人間の発達段階に合わせて作られているから、ちょうどこの時期に入試や入社・採用試験が集中するのは、当然です。

現実社会には様々な矛盾があり、現実が理想に一致しない状態・・・そう、私たちが「ふしあわせ」と呼ぶ状態が存在することも、私たちはこの時期に(否応なく)知ることになります。もちろん、それとは逆に、自らが理想とする状態と、現実が合致している状態が「しあわせ」であることも・・・。

そのような意味での「しあわせ・ふしあわせ」は、「周囲から見て、ある基準と比較して相対的に判断される」ものではなく、あくまでも本人の中での「理想」と「現実」の一致、いわゆる「自己一致」の有無のみによって決まるものと言えます。

だから、常人には耐え難い苦痛を伴うことが明白なオリンピックのマラソン競技でも、それに出場することを目指した選手にとっては、オリンピックに出て金メダルを取ること(自らの理想)と、それを実現するために苦しい練習が必要なことは当然(現実)だから、彼らが日々行う「一般人から見れば苦痛以外の何ものでもない過酷な練習」も、本人的には、その必要性において完全に「自己一致」しています。この「自己一致」があるから、マラソンの選手は、普通の人ならば確実に死んでしまうような練習を毎日普通に行い、しかも、その苦痛に満ちた状態を「しあわせだ!」と表現できてしまったりするわけです。

10代後半における、所属と承認の欲求の実現の具体例をひとつ挙げよと問われたら、多くの人が、その最たるものは「大学入試である」と答えるのではないでしょうか?

次に示すのは、ある教員が書いた先輩教師への手紙です。

最近、こんな出来事がありましたので、お伝えしたく、筆を取りました。

昨年、秋が深まった頃、模擬試験の問題がわからないと、質問に来てくれた子がいました。

出来る限り、丁寧に解説して、理解が深まるようアドバイスしたのですが・・・、解説の最後に「わかったかい?」と、そう訊ねると、その子は俯いたまま、涙をポロポロこぼして言いました。

(どんなに努力しても、模試を何回受けても、毎回同じ点数で成績が全然上がらない)

(いったい、どうすれば応用問題が解けるようになるんですか?)

あぁ・・・ 僕でよければ、代わりに試験を受けてあげたい。
そう、思ったのですが、それはできません。

(このまま、この子を帰すわけにはいかない。今、自己一致させるには、どうすればいい・・・)

僕は、死に物狂いで考えました。
そして、こう、言いました。

「わからない問題があったから、もっとよくなりたくて、きみは質問にきた。
 今、きみのやってることは、間違いか。それとも、正しいことか、どっちだい?」

その子は、泣きながら、でも、はっきり答えました。

「正しいことです。」

「ならば、それを続けるしかないじゃないか。
 わからないことがあれば、悩んでる暇なんか、ない。すぐにおいで。
 きみがわかったと言うまで、きみにつきあうぞ。
 僕もわからない問題なら、わかるまでいっしょに考える。
 それだけは、絶対に、約束する。」

「もし、それでダメなら、僕を一生恨んだらいいじゃないか。」


今年度の大学入学共通テストが行われた翌週の月曜、放課後、
午後5時を過ぎてあたりがすっかり暗くなった頃、
その子が僕の準備室をたずねてくれました。


(できたかい?)

ほんとうは、そう言いたかったのですが、この場合、そうは言えません。

「全力を出し切れたかい?」

僕は、そう訊ねました。

「はい。47点取れました。先生のおかげです。」
(この科目の満点は50点です)

(僕は、何もしていない・・・)

僕は、本当に、何もしていない。ただ、できる限り、丁寧に、その子の質問に答えただけなのですが、人は本当に不思議な生き物です。

どうしてこんなにも、胸が熱くなるのか、その理由を、僕は、今も、言葉にできません。

手紙の中の彼女が「ある教員」のところへ質問に来た時、彼女の中に「自己一致」がありませんでした。つまり、応用問題が解ける状態(=理想)と、応用問題が解けない状態(=現実)の不一致が、彼女の「不幸な状態」を作っていたわけです。

「ある教員」は、彼自身のこれまでの経験から、彼女の中で「自己一致が失われている」ことが苦しみの最大の原因であることに気づき、短時間で、何とかして、それを作り出そうとします。

現実の中で「よくなりたい」から質問に来た。それを確認することで、引き続き「よくなる」ためにはそれが最善の方法だと再確認がとれ、自らの行いの正しさを再認識することで、現実的には「まだ応用問題が解けるようにはなっていない」にもかかわらず、彼女の中に「この正しいことを繰り返して行けば応用問題が解けるようになるかもしれない」という「希望」が生まれます。すると現実的には「何ひとつ変わっていない」のに、「不幸せ」だった状態が「自分は正しい」という「幸福」な状態に変化します。

これが「自己一致」です。

さらに「ある教員」は、彼女の特別な味方であることを強調しています。相手の弱さや苦しみを自分の中に見て、相手の行動が正しい場合には、肩を並べ、ともに生きる姿勢を示すことで、相手の中に「生きるちから」を再生することができます。

最後の「一生恨んだらいい」には、カウンセリング的な意味はありません。
「ある教員」独特の個性でしょう。

相手への最大限の誠意の上に、カウンセリングの知識と技法を乗せれば、相手に「しあわせ」をプレゼントすることができます。これが「言葉の魔法」です。

ここに書いたことに加えて「共感的理解」・「受容と許容」について学べば、より一層知識を深めることができます。

この物語の彼女は、卒業式当日、「ある教員」へ手紙を書いてきてくれました。

 あの日、私の中で何かガラっと変わった気がします。

たったひとつ、自己一致が生まれた、それだけでひとはこんなにも強くなれるのですね。

現在、長時間労働等の問題がクローズアップされ、教員志望者が減少し、教員不足が深刻化していると聞きました。教員の仕事は、日々、自らの学びを深め、技能を向上させることで、児童・生徒のみなさんのためだけでなく、職場の同僚に対しても貢献でき、人間として感じられるこれ以上ない「よろこび」と「感動」に出会える仕事だと思います。

教員希望者のみなさん! がんばってください。
世はきみが立つのを待っています!!

【お断り】

この物語に登場した「彼女」は実在する、とてもすてきな女の子です。
本人の掲載許可を得た上で、ここに書かせていただきました。
また、このエピソードはある「公」の場で語られたものであり、個人を特定してその秘密を公開する意図はなく、法的な守秘義務にも反しないものと判断して掲載しています。

私にとって、愛とは・・・

なぜ、ひとに「悲しみ」という感情があるのか・・・
僕はその理由を知りません。

悲しみなんて、すべてなくなればいいのに・・・ と、そう願いながらも、
悲しみのない世界がどんな世界なのか、
僕には、その世界を想像することができません。

もし、悲しみがなくなれば・・・
ひとは、ほんとうにしあわせになれるのでしょうか・・・

この問いかけが許されるなら、神なる存在に、そう尋ねてみたい気がします。

「愛」もそうです。

ただ、「悲しみ」とは違い、「愛のない世界」を知ることはできます。
さまざまな作家が「愛のない世界」を描いていますが、僕の心に最も残った作品は、遠藤周作さんが「女の一生 二部・サチ子の場合」で描いた、第二次世界大戦中、ポーランドの古都クラクフ郊外にあった「アウシュビッツ強制収容所」の風景です。

「ここに・・・・・・愛があるのかね、神父さん」
ひきつった声でその囚人は笑った。
「あんた、本当にそんなことを信じているのか」

「ここに愛がないのなら・・・・・・」
と神父はかすれた声で言った。
「我々が愛をつくらねば・・・・・・」

新潮文庫「女の一生 二部・サチ子の場合」 遠藤周作 著より引用

なぜ、ナチス・ドイツはその「収容所」を作ったのか。
ユダヤの人々に対する、ヒトラーの「歪んだ思い」はどこから生まれたのか。
そんな彼を、ドイツの人々は、なぜ「支持」したのか。
また、彼は実際に「どのような政治」を行ったのか。

もしかしたら、そのような(教科書に書かれていないことも含めて)歴史の背景を学んでから、この作品を読んだ方がいい・・・のかもしれませんが、たとえ、そうでなくても・・・

ここに登場する・・・マキシミリアン・コルベ神父の言う「愛」が容易くないことは明らかです。容易い「愛」などないと、今は思いますが、自らの内なるものの何が「愛」なのか、僕はずっと理解していませんでした。自らの内なるものの何が「愛」であるのかを知り、その重さを初めて理解したのは、二十歳を過ぎてからのことでした。

人生では、縁も、所縁もない人と、偶然同じ場所で、同じ時を生きることがあります。そして、その隣人と生涯をともにするわけでも、なんでもないのに、その人の「現在」を例えようもなく、重たく感じることがあります。

例えば、学校の先生になって・・・
クラスの子どもたちに対して感じる気持ちが、そうです。

まだ、若かった頃のことです。

僕は、他者への気持ちが、際限なく重たくなる理由がわかりませんでした。
例えようもなく、他者(クラスの子どもたち)への気持ちが重たくなるのを感じたある夜、郷里の父に電話して、その重さの理由を尋ねたことがありました。

どんなに心を尽くしても、こちらのしてほしくないことばかりする、クラスの子どもたちを『なんで心配してしまう』のか、その本当の理由が知りたかったのです。

郷里の父の部屋の机の上には、十字架があり、聖書と祈祷書が常にきちんと置かれていました。部屋の壁には作り付けの本棚があり、青年期に入った僕はそこから遠藤周作さんの「沈黙」や「海と毒薬」、八木重吉さんの「貧しき信徒」などを手にすることになります。父に連れられて教会へ行くことも、子ども心には不思議でしたが、僕の家では年中行事のひとつでした。父にとってイエス・キリストは、とても大切な存在であったようです。僕は、そんな家庭で育ちました。

その晩、電話の向こう側で、父は、ただ黙って、僕の話をきいていました。
なぜ、彼がずっと黙っていたのか、今はその理由がわかるような気がしますが・・・。
父がいつまでも黙っていることに耐えかねて、僕は彼にこう尋ねました。

「お父さんの信じる神なら、こんな時は何て言うの?」

「救い・・・って、何」

それまで黙っていた父が、即答しました。

「救いなんて、ない」

「それなら、なんなの。遠藤(周作)さんの『沈黙』と同じじゃないか。
 やっぱり、お父さんの神も黙ってるの?」

僕は、いちばん知りたかった疑問を重ねて彼にぶつけました。

「なんで、こんなに、赤の他人の人生が重いの?」

それから、父がゆっくりと、まるでひとりごとを言うかのように、答えてくれた言葉を、僕は今でも覚えています。それが、次の言葉でした。

「(私の名前)・・・な、ちんけな、安っぽい言葉に、聞こえるかもしれん。
 ちんけな、安っぽい言葉に、聞こえるかもしれんけど・・・」

「私は、それが愛だと思う。」

電話はそこで切れました。

内なる悲しみを分かち合うことは困難ですが、内なるこの愛は共有できます。

「女の一生 二部・サチ子の場合」 遠藤周作 著

青年期に読むべき、価値ある一冊だと思います。
初版は昭和61年ですが、現在でも重版されており、新しい本が入手できます。

ぜひ、手に取ってみてください。

花が咲いたよ

朝、仕事に行こうとして家の玄関を出たら・・・

スイセンの花が咲いていた。

僕は、大好きな、八木重吉さんの詩を思い出した・・・

 花

花はなぜ美しいか
ひとすじの気持ちで咲いているからだ

僕は、悲しみの塊のような人間だから、
どうしても過ちを繰り返してしまう・・・

たとえ、ひとすじの気持ちであっても・・・
それが自分にとって、どんなに美しいものであっても・・・
だれかを傷つけるなら、それは間違いだ。

空を見上げて、きみに心から謝りたいです。
ほんとうに、ほんとうに、ごめんなさい。

そう思ったら・・・
大好きな、八木さんの詩を、もうひとつ、思い出せた。

 ひかる人

私をぬぐうてしまい
そこのとこへひかるような人をたたせたい

ほんとうに、その通りだ。
それができたら、どんなにか、こころが明るく、軽く、なるだろう・・・

こんな くだらない僕を・・・
ひと思いに、ぬぐいさってしまえたら。

それが僕の ほんとう であるのに・・・
それが僕の 偽りなき ほんとう であるのに・・・

昨日の報道発表を聞いて
たくさんのひとが・・・僕に会いに来てくれて
美しい涙を流してくださった・・・

ぬぐいさってしまいたいような・・・こんな 僕のために。

わたしのまちがいだった
わたしのまちがいだった
こうして草にすわれば
それがわかる

あと何度、混乱を繰り返したら、美しい気持ちになれるだろう・・・
どれだけ思い悩んだら、この僕を卒業できるだろう・・・

どこに向かって歩けば、いちばん僕らしく、歩けるだろう

誰ひとり、傷つけずに・・・
誰ひとり、困らせずに・・・
静かに美しく咲いていた・・・

今朝、見た あのスイセンの花のように。

花が咲いたよ
花が咲いたよ
幼かった僕なら、きっと・・・笑顔で
そう、今の僕に言ったに違いないけど。

Begin

今日は、出張だった。

遅れてはならない大切な打ち合わせだったから、時間には十分余裕を持って出た。
昼を食べていなかったので、途中、コンビニでパンを買って・・・
僕は、なつかしい公園の駐車場にクルマを止めた。

最初から、そうしようと決めていたんだ。
数分間でもいい。思い切り、思い出に浸りたかった・・・。
新しい風景を見る、その前に。

(街路樹が・・・なくなってる?)

たしか、あそこには、ポプラ並木があった・・・はずだ。
僕だけじゃなくて、公園もいつしか年齢を重ねてた。

今の職場に6年・・・
その前の職場には4年・・・
この公園によく立ち寄ったのは、さらにその前の職場にいた頃だから、もう10年以上前のことだ。

あの頃、言葉にできないことがあると・・・
帰り道、ここでよく空を見上げた。
空は青かったこともあるし、茜色だったことも、星が瞬いていたこともあった。

樹があった場所に、10年の歳月と、今がある気がする・・・

(空港を出発したばかりの飛行機だ・・・)

そうだ・・・みんなを乗せて・・・ 遠い、とおい、ところへ・・・

優しかった微笑み
あの日の拍手
それから・・・ それから・・・

思い出せることすべてが泣きたくなるほどに、美しいのはなぜ・・・

思えば、胸いっぱいに広がるさみしさと、別れのかなしみを感じながら
大切に思えてならない多くのひとのしあわせを祈り続けてきた・・・たくさんの三月。

これが・・・ 僕の仕事なんだ。

今は、まだ、どうしても、うつむきたくなるけれど・・・
きっと思い出せるはずだ。

Begin

そう、Begin

美しい思い出とともに、また、きっと・・・歩き出せる。

Object Pascalと、僕と。

Conditional Compilation

条件付きコンパイル

Delphiからembeddable Pythonを利用するプログラムを書いた。これまでは32bit環境しか利用(作成)しなかったので、条件付きコンパイル(条件コンパイル)には縁がなかったが、64bit環境を利用しなければならない事情ができたので、初めてこれに挑戦。

結論だけ言うと、すごぉーく、カンタンだったー☆

【今回の記事】

1.ターゲットプラットフォームを切り替える
2.条件付きコンパイルやってみた
3.まとめ
4.お願いとお断り

1.ターゲットプラットフォームを切り替える

以前、スキャナーで読み込んで画像データ化した答案用紙の解答欄座標を、自動的に取得できるプログラムを書いた。もちろん、GUIはDelphiで作成。・・・とある事情から、コレを64bit化しなければならなくなった(「・・・とある事情」は次の記事以降のMyBlogに掲載)。

簡単に言うと、「手書きカタカナ文字の自動採点」をTensorFlowを使って実現しようとしたためです。正解率がどうがんばっても100%にならず、手書き答案の採点ソフトへの実装は見送りましたが・・・

このプログラムの核となる矩形(輪郭)検出部分は、OpenCVのfindContours関数を使い、座標取得部分は同じくOpenCVのboundingRect関数を使っている。OpenCV自体は、Delphi用ではなく、Python用のライブラリをembeddable Pythonにインストール。で、DelphiのObject PascalにPythonのScriptを埋め込み、必要な時にGUIの向こう側で走らせて、矩形検出を行っている。

32bitバージョンのembeddable Pythonを入れたフォルダの名称は「Python39-32」、64bitバージョンのembeddable Pythonを入れたフォルダの名称は「Python39-64」としてこのフォルダの名称以外はまったく同じコードでWin32版とWin64版の解答欄矩形検出プログラムとしてそれぞれコンパイル、同じくDelphiで書いた手書き答案採点プログラムから、採点準備の際に呼び出して利用している。フォルダ名の「39」だが、これはPythonのメジャーバージョンが「3」、マイナーバージョンが「9」であることを表しているのは言うまでもない。

Python環境を入れたフォルダの名称が異なるだけで、その他は32bit版も64bit版も一字一句まったく同じプログラム。これを別個のプロジェクトとして保存して、改良のための変更やバグの修正があった場合に、同じ内容を二度書くのはどう考えても効率が悪い。何かしら上手い方法はないのか・・・。

そこで、最も簡単にターゲットプラットフォームを切り替えるにはどうしたらいいかを調べたところ、条件コンパイル(条件付きコンパイル)という方法があることを知った。早速、見様見真似でやってみた☆

2.条件付きコンパイルやってみた

「Delphi ターゲットプラットフォーム 取得」をキーワードにして、Google先生に質問してみた。実は、この時点では、僕はまだ「条件付きコンパイル」なるものの存在を知らない。現在選択しているターゲットプラットフォームをプログラムコードから取得し、条件分岐でembeddable PythonへのPathを指定しようと思ったのだ。

  //手動でEmbeddable PythonへのPathを切り替え(存在の有無を調査)
  AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-32';
  //AppDataDir:=ExtractFilePath(Application.ExeName)+'Python39-64';
  if DirectoryExists(AppDataDir) then
  begin

  end;

Google先生が教えてくれたWebサイトへ行ってみると・・・

異なるバージョンの Delphi でソースを共有する

https://ht-deko.com/tech001.html

記事に『条件コンパイルを設定すればバージョンで異なるソースを記述できます。』と書かれている。

そんなの、あったんだー☆

で、今度は「Delphi 条件コンパイル」をキーワードにして再度、検索。新しいターゲットプラットフォームの追加方法と条件付きコンパイル(条件コンパイル)の記述方法を学ぶ。

ターゲットプラットフォーム(64ビットWindows)を追加するには、プロジェクトマネージャの「ターゲットプラットフォーム(Win32)」部分をポイントして右クリック、表示されたサブメニューの「プラットフォームの追加」をクリックすると次のWindowが表示されるので、追加したいターゲットプラットフォームを選択(されているけど)して、「OK」をクリックする。

「プラットフォームの選択」画面

すると、プロジェクトマネージャの表示は・・・

「Windows64ビット」が追加された!

条件付きコンパイルの記述方法は、処理を切り替えたいところで・・・

  {$IFDEF WIN32}
  //32bit環境での処理

  {$ELSE}
  //64bit環境での処理
  
  {$ENDIF}

・・・とすればいいだけ!とのこと。走召カンタン

$IFDEF に続く WIN32 部分を「シンボル」というらしい。もちろん、WIN32はターゲットプラットフォームが32ビット環境であることを意味する。他にもたくさんのシンボルが指定でき、例えばここを IOS とすれば、ターゲットプラットフォームが iOS であるかを判断できるとのこと。まぁ、僕がiOS用のプログラムを書くことはないだろうけれど・・・。

うれしい気持ちで、書いてみた!

うわー! 色が変わったぁー☆

どうやら、現在選択されていないプラットフォーム用のコードは色が薄くなるしくみらしい。DelphiのIDEの評価ってどうなのか、よく知らないけれど、コレはイイ!

ターゲットプラットフォームを切り替えると、コードの色も変わる

うれしくなって、何度も無意味に切り替えてしまう・・・

むかし、上野動物園でデカい白くまが「プールに落ちる」動作(飛び込みではない・単に落下するだけ!)を何度も何度も繰り返すのを見たけど、今、ようやく、あの時の白くまの気持ちがわかった気がする・・・。上野動物園、行きたいなー☆

うわー 壊れたー T_T

状況によっては、ターゲットプラットフォームを切り替えても、コードエディタの文字の色が切り替えに追随しなくなることがあるようだ。あわててDelphiのIDEを再起動。ターゲットプラットフォームを切り替えて直ったことを確認。よかったぁ・・・

これで別プロジェクトに分けなくても、プロジェクトマネージャーでターゲットプラットフォームを切り替えてコンパイルするだけで、32bit/64bitいずれの実行ファイルも生成できるようになった!

今回は、近所の小学生でもわかるような内容だったけれど・・・

すごくよくなれた気がする*(^_^)*♪

3.まとめ

(1)条件付きコンパイルを使えばターゲットプラットフォームの切り替えは簡単。
(2)コンパイラ指令は、コメント開始区切り記号{の後に $を先頭文字として記述。
(3){$IFDEF WIN32} //32bit環境での処理 {$ELSE} //64bit環境での処理 {$ENDIF}

4.お願いとお断り

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

I did it again!

また、やっちゃった!

2022年が終わる頃から、およそ2ヵ月半を費やして(カタカナのア~オと、記号の〇と×限定ではあるが)手書き文字(答案)の自動採点プログラムを、GUIはDelphi・自動採点部分はPythonで作成。職場の仲間たちみんなにプレゼントしたのはいいんだけれど・・・。

なんで、なんで、いつも最後に、思ってもみなかったコトが起きるんだろー T_T
Python ・・・ きみと仲良くしたい時は、いつも・・・

【今回の記事】

1.ついにできた!!!
2.なんで起動しない?
3.また、やっちゃった!
4.それでも動かしたい僕は・・・
5.全角文字を探し出すには?
6.まとめ
7.お願いとお断り

1.ついにできた!!!

夢にまでは出てこなかったケド、手書き答案の自動採点プログラムがついに完成( した・・・ と自分的には思っている)。

プログラム添付の取り扱い説明用PDF文書の一部を抜粋

パラメータの設定さえ上手く行けば、上の画像のように、解答欄中の設問番号などには目もくれず、手書きのカタカナ文字だけを識別してほぼ期待通りに動作( してほしい くれる ・・・と自分的には思っている)。

しかも、このプログラムは、自分史上初となる64ビットバージョン。

まぁコレは内部的に呼び出して使用しているTensorFlowに32ビット版がないから、仕方なく64ビット化した・・・というのが、ほんとうだけど。

自動採点部分は「オマケ」程度に考えてもらえれば、採点記号&得点の入力や合計点の自動計算、返却用答案画像の印刷、さらに(こちらも自作の)マークシートリーダーとの併用等、採点プログラムとして必須の機能は間違いなく動作するから、職場の仲間たちによろこんで使ってもらえるはず・・・

みんなが、寄って集って(しかも、あろうことか、実戦で)動作検証してくれるから、ある意味でプログラマーにとってこれ以上しあわせな環境は「ない」カモしれない・・・

(僕がプログラマーと言えるか、どうか、それはまた別の問題として)

仲間たちの信頼を裏切らないための、自分に出来得る限りの動作検証は、もちろん必要だけど・・・

これで、またひとつ、夢をカタチにできた!
でも、うれしいより、なんだか カラッポ になっちゃった感じ。
(がんばった後は、いつもそうなんだけど)

すごーく高い山の頂上にたどり着いて、上を見上げたら
そこには、もっと高い、きれいな青い空があることに気づいたような・・・

確かに積み上げたと思ったもの、すべてが消えて
胸の中がカラッポになってしまったような・・・

でも、僕は、この感覚がとても好き。

My自動採点プログラムの実行結果
ここでの正解は「エ」に設定。ここでは1つも間違っていない。
(画面は合成です)

パラメータ設定さえ、決まれば・・・上の画像のように非常に良好な結果が出せる。
ただ、どんな答案に対しても「常に」良好な結果が出せるような・・・共通して利用できるパラメータ設定は、現在のところ、まだ見いだせない。

マークシートリーダーを作った時も、最後の最後で、この最適なパラメータ設定を見つけるという問題の解決のために、本番と同時進行で、ドキドキしながら試行錯誤を繰り返したんだ・・・。

プログラム添付の取り扱い説明用PDF文書の一部を抜粋

いろんな解答欄に、共通して利用できる設定さえ、見つけることができれば・・・

さらに答案画像の解像度や縮小率も関係するので・・・パラメータの最適な組合せがまだ見えない!

まぁ、ここまでくれば・・・
最初に夢見た自動採点を、「実現できた!」と言っていい はずだ。

そう、はずだった・・・んだ、ケド。

2.なんで起動しない?

採点プログラムの動作に必要なモノすべてをReleaseフォルダに詰め込んだら、Releaseフォルダごと、職場のファイルサーバーの公開フォルダにコピーする。

で、フォルダ名が “Release” のままでは内容がわからないから、フォルダ名を「採点プログラム」のような、誰が見てもわかりやすい名前に変更する。

あとは使いたい人が自分のPCのデスクトップ等に、そのフォルダごとコピペして、使いたい時にフォルダ内にあるAC_Reader.exeをダブルクリックして採点プログラムを起動してもらえば、それでOK!

(ACは “Answer column” の略)

・・・だったはず、なんだけど。。。

起動時にいきなり「学習モデルが開けない!」エラーが発生

エラーメッセージは、「値のエラー:採点プログラムフォルダ内のsaved_model_mb.tfliteファイルを開くことができません」と訴えている。

僕はFormCreate時にPythonEngineを初期化し、自動採点機能を1回だけ動かしている。これはユーザーがFormの自動採点ボタンを押した時に「動作が重たい」と感じないようにするために行っていることだ。同じ内容を同じようにembeddable Python を組み込んだマークシートリーダー(Win32版)でも行って、これまで一度も問題は起きなかった・・・。

PythonEngineの初期化手続きは・・・

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

  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;

ここでエラーは起きていないようだ。次が自動採点の実行。最初の1回目だけはそれなりに時間(数秒だけど)がかかるから、FormCreate時に1回だけ実行しておけば、ユーザーにとっての1回目の自動採点の実行は、実は2回目になり、待ち時間ほぼゼロで動くようになる。そのために、どうしても必要な部分なんだけれど・・・

メッセージから考えて、エラーはStringListを作成して実行しているPythonスクリプトの “model.load()” おそらくここで起こってる・・・

if os.path.isfile(img):
    image = Image.open(img)
    model = TFLiteModel(dir_path=dir_path)
    model.load()
    outputs = model.predict(image)

プログラムでは、起動時にリソースに埋め込んだ〇記号の画像を再生し、これを読み込んで機械学習モデルと照合する「誰にもわからない」自動採点を一度だけ行っている。

“model.load()” 時に呼び出される〇×記号の学習モデルが saved_model_mb.tflite だ・・・。
このtflite形式の学習モデルはLobeが生成したjsonファイル内で、次のように・・・

"filename": "saved_model_mb.tflite"

filename変数に代入されて参照できるように設定され、このjsonファイルをPythonスクリプトではTFLiteModelクラスの中で開いている・・・

with open(os.path.join(model_dir, "signature_mb.json"), "r") as f:

なぜ、ここで saved_model_mb.tflite が開けないんだ?
My PC では、同じプログラムが「何の問題もなく動作する」のに・・・

ナニが違う・・・?

3.また、やっちゃった!

クライアントPCで起動時に表示されるエラーメッセージを、もう一度見つめて考える・・・

「E:¥採点プログラム」 僕のPCとの違いはここだけだ・・・

ここで、ようやく僕は気づいた・・・。そうだ。exeへのPathが違うんだ・・・
Pathに全角文字が・・・、日本語が含まれていると・・・OpenCVでもファイルの読み込みに失敗してた・・・だからPillowを使ってファイル読み込み時のエラーを回避したじゃないか。

わかった。これがエラーの原因だ。では、どう対応したらいい?

Delphiに埋め込む前のPythonスクリプトの状態で、自動採点部分のみを動かして対応策を考える。

スクリプトを入れたフォルダの名前を「採点プログラム」に変更

スクリプトを走らせる。すると「期待通り」にエラーが発生。

Traceback (most recent call last):
  File "E:\採点プログラム\xxx.py", line 138, in <module>
    model.load()
  File "E:\採点プログラム\xxx.py", line 58, in load
    self.interpreter = tflite.Interpreter(model_path=self.model_file)
  File "E:\WPy64-3980\python-3.9.8.amd64\lib\site-packages\tensorflow\lite\python\interpreter.py", line 455, in __init__
    _interpreter_wrapper.CreateWrapperFromFile(
ValueError: Could not open 'E:\採点プログラム\saved_model.tflite'.
[Finished in 5.174s]

まずい・・・。エラーはTensorFlowの内部で起きている・・・
この壁は、今の僕の実力では越えられない・・・。

また、やっちゃった・・・
Python環境と全角文字の組み合わせは、要注意!だったはずなんだ・・・
すっかり、そのことを忘れてた・・・

2ヵ月半もかけて、ここまでたどり着いたのに
まさか、最後に、こんな・・・

もしかしたら・・・と思い、読み込み時の文字コードをUTF-8に指定して見たけれど。

with open(os.path.join(model_dir, "signature.json"), "r", encoding="utf-8") as f:

状況は変わらず・・・。つまり・・・

Pathに日本語があると
僕のプログラムは起動しない・・・

4.それでも動かしたい僕は・・・

現在の僕の実力では、この問題に対応できないことはわかった。もっとよくなるためには勉強しなければいけないけれど、それには時間が必要だ。

とりあえず、今、できることは何か?

答えはわかっている。

Pathに日本語(全角文字)が
なければイイ!

この世から日本語を消す・・・

壮大すぎる・・・、実現不可能な挑戦だ。
第一、僕は日本語を愛している。それを消し去るなんて出来るわけがない。
そんなことをするくらいなら、僕が死んだ方がいい。

でも、この前、とても大切に思う人から「長生きしてください」って、言ってもらえた
今すぐ死んでもいいと思うほどうれしかった。だから、僕は、まだ死ねない。

そう、生きる「ちから」の、ある限り・・・

では、どうする?

そうだ。プログラム起動時にPathをチェックするんだ・・・。
Pathをチェックして全角文字が含まれていないことを確認できたら、PythonEngineを初期化する。もし、Pathのどこかに全角文字があれば、メッセージを表示して「Pathから全角文字を取り除いてもらえるよう」ユーザーにお願いすればいい。

ひとに何かをお願いするのは、大嫌いだけれど・・・これだけは仕方ない。

もっとよくなって、TensorFlowの内部をなんとかできるようになるまでは・・・

5.全角文字を探し出すには?

文字列に含まれる全角文字を探すには、どうしたらいいか?

これまでの学びの中でByteType関数を使う方法を勉強済みだ。早速、僕はFormCreate手続きのいちばん最初に、Pathに全角文字が含まれているかどうか、確認するプログラムを追加した。

implementation

uses
  System.AnsiStrings;
  //System.AnsiStringsは、起動Path中の全角文字の有無を調査するために追加

procedure TFormCollaboration.FormCreate(Sender: TObject);
var
  i,j:integer;
  ・・・ 略 ・・・

  //引数に指定した文字列が半角か全角かチェックする
  function OnlySingleByte(const S: AnsiString): Boolean;
  var
    i: Integer;
  begin
    for i:=1 to Length(S) do
      //System.SysUtilsのByteType関数(非推奨)が呼び出される
      //if ByteType(S, i) <> mbSingleByte then
      //usesにSystem.AnsiStringsが必要
      if System.AnsiStrings.ByteType(S, i) <> mbSingleByte then
      begin
        Result := False;
        Exit;
      end;
      Result := True;
  end;

begin

  //起動Path中の全角文字の有無を調査
  //[dcc64 警告]データ損失の可能性がある文字列の暗黙のキャスト ('string' から 
  //'AnsiString')を表示しないようにAnsiString()で明示的に型キャストした
  if not OnlySingleByte(AnsiString(Application.ExeName)) then
  begin
    MessageDlg('AC_Reader.exeへのPath中に全角文字が含まれていないか、'+
    '確認してください。'+
    '全角文字が含まれているとPythonEngineの初期化作業を行うことができません。'+#13#10+#13#10+
    '全角文字を含まないPathに変更後、再度実行してください。',mtError,[mbOk],0);
    //プログラムの終了
    //Close;  //止まらない!
    Application.ShowMainForm:=False;
    Application.Terminate;  //停止するが、エラーが発生する
    //halt;  //停止するが、エラーが発生する
  end else begin
    //カーソルを待機状態にする
    Screen.Cursor := crHourGlass;

    ・・・ 略 ・・・

  end;
end;

テスト用にReleaseフォルダの名前を変更する。

「あ」をフォルダ名の末尾に追加

で、フォルダを開けて、exeを直接ダブルクリックして実行。

意図した通り、Path中の全角文字「あ」をつかまえた!

しかし、そのあとがよくない。Close命令では、なぜか止まらず、Application.Terminateやhaltではプログラムは停止するが、次のように実行違反のエラーが発生する。

原因のわからないエラーが発生!

これには、ちょっと困った・・・。どう処理しようか、しばし考える。で、確か、エラーメッセージの表示を抑制する方法があったことを思い出す。階層化テキストエディターのNanaTreeに記録してある方法で、エラーメッセージの表示を代替するメッセージボックスを表示し、エラーメッセージの表示を出さないように設定すればいいと気づく。

重要! エラーの原因がわからないので、処理としては正しくないと思います!

NanaTreeに記録してあるTipsは、次のWebサイトにあった記事を参考にして作成したもの。このTipsを参照しながら、コードを次のように変更。

エラーメッセージを示すダイアログの表示の制御

https://www.gesource.jp/weblog/?p=7116
  private
    //エラーメッセージを表示しない
    procedure ExceptionEvent(Sender: TObject; E: Exception);

implementation

uses
  System.AnsiStrings;
  //System.AnsiStringsは、起動Path中の全角文字の有無を調査するために追加

procedure TFormCollaboration.ExceptionEvent(Sender: TObject; E: Exception);
begin
  //ShowMessage('Event OnException: ' + E.Message);  //<-このかわりに下を表示
  MessageDlg('AC_Reader.exeへのPath中に全角文字が含まれていないか、確認してください。'+'全角文字が含まれているとPythonEngineの初期化作業を行うことができません。'+#13#10+#13#10+
  '全角文字を含まないPathに変更後、再度実行してください。', mtError, [mbOk] , 0);
end;

procedure TFormCollaboration.FormCreate(Sender: TObject);
var
  i,j:integer;

  //半角か全角かチェックするプログラム
  function OnlySingleByte(const S: AnsiString): Boolean;
  var
    i: Integer;
  begin
    for i:=1 to Length(S) do
      //System.SysUtilsのByteType関数(非推奨)が呼び出される
      //if ByteType(S, i) <> mbSingleByte then
      //usesにSystem.AnsiStringsが必要
      if System.AnsiStrings.ByteType(S, i) <> mbSingleByte then
      begin
        Result := False;
        Exit;
      end;
      Result := True;
  end;

begin

  //エラーメッセージを示すダイアログの表示の制御
  Application.OnException := ExceptionEvent;

  //起動Path中の全角文字の有無を調査
  //[dcc64 警告]データ損失の可能性がある文字列の暗黙のキャスト ('string' から 'AnsiString')を
  //表示しないようにAnsiString()で明示的に型キャストした
  if not OnlySingleByte(AnsiString(Application.ExeName)) then
  begin
    //ExceptionEvent手続きへ、そのままコピペ
    {
    MessageDlg('AC_Reader.exeへのPath中に全角文字が含まれていないか、確認してください。'+'全角文字が含まれているとPythonEngineの初期化作業を行うことができません。'+#13#10+#13#10+
    '全角文字を含まないPathに変更後、再度実行してください。',mtError,[mbOk],0);
    }
    //プログラムの終了
    Application.ShowMainForm:=False;
    Application.Terminate;
  end else begin
    ・・・ 略 ・・・
  end;
end;

これでエラーメッセージは表示されなくなる・・・

6.まとめ

(1)TensorFlowを利用する際は、exeへのPathに全角文字があってはいけない。
(2)自力でエラーを回避できないので、Path中の全角文字の有無を調査して対応。
(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.お願いとお断り

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

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