#author("2025-12-04T15:10:21+09:00","default:kouzouken","kouzouken")
#author("2025-12-04T15:10:55+09:00","default:kouzouken","kouzouken")
&aname(Top);
#contents


*卒論 [#v045a40a]

**4月 [#j417c400]
***4月第4週(4/23〜4/30) 手書き数字の判別 [#j417c423]
卒論テーマが決まった。
 耐候性鋼橋の耐候性鋼材の錆の現地調査(今年度から)(日本鉄鋼連盟、土木研究センター、東北の大学や高専の土木構造系研究室の共同研究)に参加しながら、撮影データに対して機械学習(AI)を用いた外観評価を行う。最終的にドローンで撮影した耐候性鋼橋の画像データに対して外観評点を行う方法も検討する(特定の距離や照明で撮影するといった制御ができるか)。

-jpg, ppmファイルから1点ごとのRGB値を確認する方法
--1,写真フィルを選択して"gimpで開く"をクリックする。
--2,gimpが開けたら上にあるバーから "ウィンドウ" → "ドッキング可能なダイアログ" → "ピクセル情報" と進んでいくと要素ごとにRGB値を確認できる。

-MATE端末のコマンド
--1, locate (ファイル名等) → (ファイル名)がどこにあるのかリストアップしてくれる
--2, gfortran -o (ファイル名) (ファイル名.拡張子) → あるファイルを変換する(gfortran -o)
--3, history → MATE端末内で過去にどんなコマンドを用いたか確認できる。
--4, hg (ファイル名等) → MATE端末内でどの部分に(ファイル名)が用いられているかを確認できる。(検索エンジン内での Ctrl + Fキー のようなもの)

-明日までにやっておくべきこと
 --佐藤さんの卒論日誌をはじめ耐候性鋼材にまつわるウィキを読んで分からなかった部分をリストアップする。ただ何が分かっていないか分からない 来週のゼミ報告に向けてなにか目標をたてなければ


-これからしばらくはpythonになれることが必要 Python上で佐藤さんが作成したプログラムが正常に動作するか確認

-【質問内容】
--最初に取り組むべきステップは何か?
機械学習を使うにはどんなデータ整理や前処理が必要かがわかっていない。例えば、「画像をどう分類するか」「どんなラベルをつければいいか」など、考えるべきことを教えていただけるとありがたいです。

--過去の先輩方の研究でどの部分を参考にするのがよいか?
佐藤卒論日誌やRGBメモの中で、現在の作業と関連が深い部分があれば教えていただきたい。

--MATE端末からpythonを開くには → python と入力すれば入ることができる
--MATE端末から1つ上のディレクトリに上がりたい(戻りたい)場合 → cd .. と打つ (cdとコンマの間には半角スペースをいれること)

-python上で ホーム / hikitugi 内に保存された.pyファイルを動かすためには
--MATE端末にもどり( python の状態から脱出する) "python ファイル名.py" と打つと実行することができる。
--hikitugi内には RGBrironti.py , edge.py , henkan.py , matrix.py , svm.py の計5つのpyファイルがあった。それぞれ何を入力したら先に進むのか考えていく

-RGBrironti.py について (コマンド)
--1, MATE端末を開き py ファイルのある hikitugi に移動する (cd hikitugi)
--2, py ファイルを呼び出す (python3 RGByomitori.py) [3は ver のことであり入力しなくても呼び出せる]
--3, フォルダーのパス入力が求められるので錆の写真があるフォルダーを入力する → Enterキー  (一例: /home/kouzou/sato24/gr/data/1_ppm)
--4, 保存するファイル名を入力する → Enterキー (一例: RGBsyuturyoku0423 )
--5, その後ウィンドウのどこかしらに錆の画像が表示されるので、その写真のどこか適当な所をクリックするとMATE端末上に
 画像: ファイル名(.ppm) | クリック位置: (x座標, y座標), RGB値: (R(0~255), G(0~255), B(0~255),)
と表示される。またフォルダーパスの入力をしたファイル内に4で保存したファイルが保存されている(今回はhome/kouzou/sato24/gr/data/1_ppm に RGBsyuturyoku0423 というの文書ファイルが保存されている)
--6, 文書ファイルを開くと写真のようにMATE端末に表示されているのと同じ情報が文書ファイル内に保存される。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250424-1.png,1000x);

--7, 写真の左下に写っている錆の写真は矢印キー(←, →)でフォルダー内の写真を切り替えることができる。
--補足:保存するファイル名を前に作成したファイル名と同じにすると新たに上書きされるのではなく、既存のデータに加えられる形で保存される。


-edge.py について(コマンド)
--1, MATE端末を開き py ファイルのある hikitugi に移動する (cd hikitugi)
--2, py ファイルを呼び出す (python3 edge.py) [3は ver のことであり入力しなくても呼び出せる]
--3, 入力ディレクトリパスの入力が求められる。なお edge.py はpgmファイルのみ対応しているのでpgmファイルで保存されていいる写真のあるフォルダーを入力する → Enterキー (一例: /home/kouzou/sato24/soturon/resize_pgm/k_data_ppm_resize/r1_pgm)
--4, 出力ディレクトリパスの入力が求められる。ここでは3で入力したファイルをエッジ処理したものをどこに保存するかを設定する。ここでは一例として(/home/kouzou/edge-syuturyoku0424)と入力しておく。
---/edge-syuturyoku0424はエッジ処理したものを入れておくファイル名であり、もしファイル名を入れずにEnterキーを押してしまうとこの場合kouzouのフォルダー内に保存されてしまうので注意すること。
--5, エッジ処理の閾値1, 2を入力してくださいと表示される
---エッジ処理の閾値1は "Cannyエッジ検出の「低い方のしきい値」" のことである。画像の中で「ここはエッジ」と判断するための数値で、5〜100くらいに設定しておくとよい。
---エッジ処理の閾値2は "Cannyエッジ検出の「高い方のしきい値」" であり、150〜200くらい に設定しておくとよい。
--6, 閾値1, 2 を入力して Enterキー を押してしばらくするとkouzouにedge-syuturyoku0424というフォルダーが作成される。そのフォルダーを開いて錆の写真が左下から右下のような感じに変換されていれば成功。

http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250424-2.png → http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250424-3.png

-明日までにやっておくべきこと
 --残りのPythonファイルについて使い方を学ぶ



-svm.py について (コマンド)
--0, はじめに
--- svm = "s"upport-"v"ector-"m"achine のこと
--1, py ファイルを呼び出す前に svm.py ファイルを開く

--2, ファイルを開いたら13行目に書いてある data_dir = 〜 の部分を確認する
---デフォルトでは data_dir = "edge/250_300_edge" となっていると思う。基本的にこのsvm.pyと15行目に書いてあるフォルダー(の中身 = "mk1_pgm" , "mk2_pgm" , ...)は下の写真のように同じ階層で保存しておく必要がある。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250425-1.png,600x);

この写真の場合 "mk1_pgm"等が入っている1番左にある "250_300_edge" フォルダーと1番右にある "svm-kaizou.py" が同じ階層にある。

-henkan.py について (コマンド)
--1, MATE端末を開き py ファイルのある hikitugi に移動する (cd hikitugi)
--2, py ファイルを呼び出す (python3 henkan.py) [3は ver のことであり入力しなくても呼び出せる]
--3, 変換行列ファイルのパスを入力してください と表示されるので入力する(一例: /home/kouzou/sato24/gr/henkanm/1_henkanm/m_rating1_1.txt ) → Enterキー
--- 変換行列ファイルは3 ☓ 3行列の .txtファイルで表されており、/home/kouzou/sato24/gr/henkanm にある。
---henkanm内にある 1_henkanm ~ 4_henkanmの .txt の名前と /home/kouzou/sato24/gr/data にある各ファイル[1_ppm~4_ppm]の .ppmの名前と対応している。

--4, 補正したいPPM画像のパスを入力してください と表示されるのでppm画像のパス入力する。(例: /home/kouzou/sato24/gr/data/hg_k2_5_5.ppm) → Enterキー
---基本的に/home/kouzou/sato24/gr/data内にある ○_ppm や hg_○_ppm と書かれたファイルから選択するはず (例: /home/kouzou/sato24/gr/data/1_ppm/rating1_1.ppm)

--5, 補正後の画像を保存するパスを入力してください と表示されるので変換後のppm画像をどのパスに保存したいか入力する (例: /home/kouzou/henkan-after.ppm と入力するとkouzouのフォルダー内に henkan-after というppmファイルが保存される) → Enterキー

 ---変換行列ファイルはかなり細かい値まで書かれているためなぜそのような値に設定したのか一度聞いてみる必要がありそう

-matrix.py について (コマンド)
--0, はじめに
 このpythonファイルは henkan.py で必要になる変換行列ファイルを作り出すためのものであり、henkan.pyよりも先にやっておく必要がある。
--1, MATE端末を開き py ファイルのある hikitugi に移動する (cd hikitugi)
--2, py ファイルを呼び出す (python3 matrix.py) → Enterキー
--3, 結果を書き込むファイルを入力してください と表示されるので入力する。ここでは保存する場所に加えて結果ファイルの名前も”名前 + .txt ”と入力する必要がある。(例: /home/kouzou/matrix-kekka.txt) → Enterキー
--4, シアン・イエロー・マゼンタの各RGB値を入力してください と表示されるので入力する。(0~255 の間)

 ---各RGB値を入力するにあたりなにか根拠となるものがあるはず(ppm画像とか) → 探しておく必要がある

-来週中にやること
--サポートベクターマシーン(優先)と畳込みニューラルネットワークについて調べる
--このページをまとめる(5つのpythonファイルをどういう順序で使っていくのか示す)
--佐藤さんへの質問をまとめておく



-5つあるpythonファイルの使い方についてとそれぞれの役割
-錆の写真を撮影してそれをサポートベクターマシンを用いるまでの手順を知りたい
-SVMについて
-佐藤さんが研究していたファイル達(sato2024)は各フォルダーごとにどんな意味を持っているのか。

-佐藤さんから錆の写真からサポートベクターマシンを用いて用いて機械学習を行うまでの流れを教えて頂いた。
--耐候性鋼材のページに上記の事について詳しくまとめてある。
--機械学習のページは何か発見があったら随時更新していく必要あり。

-来週までにやること
--畳み込みニューラルネットワークについて調べる
--佐藤さんのファイル内にある手書き数字を使って機械学習
--それに加え動物の顔(一例)が判別できるかについても機械学習を行う。


-畳込みニューラルネットワークを用いて手書き数字の機械学習
--手書き文字の機械学習は mnist という手書き数字を認識するために用いられる画像データセットで行った。
--1, MATE端末で "pip install tensorflow" と入力しEnterキーを押す。
--2, エラーが出なかったらmnistの画像データをCNNに読み込ませるのと同時にmnist_model.h5ファイルを保存するスクリプトを書いて実行する。(/home/kouzou/Morii25/hikitugi 内にある mnist.py)
 Pythonコード
--3, しばらくすると正答率が表示される。だいたい99%前後の正答率が得られると思う。畳み込みとプーリング層を2層にした時点で100%に近い正答率が得られているため、これらの層を多くしてもさらに100%に正答率が近づくことはないだろう。

-ある手書き文字を読み込ませて判別させる
--1, mnist等から28×28ピクセルで書いた数字のpngやjpgファイルを持ってきて以下のPythonコードを書く。ただしmnistに保存されているファイルは28×28ピクセルしかないため基本的にそれらの画像を拡張する方法を取らない限りPythonコードは28×28で書いておいたほうが良さそう。(/home/kouzou/Morii25/hikitugi 内にある suuji-hanbetsu.py)
 Pythonコード
--2, Enterキーを押せば判別してもらえる。

-複数枚の手書き文字を一度に判別させたい場合
--1, 複数枚ある手書き数字をどこか1つのフォルダーにまとめておき、以下のスクリプトを書いて実行する。(/home/kouzou/Morii25/hikitugi 内にある Hukusuu-hanbetsu.py)
--2, Enterキーを押せば一度に複数枚判別してもらえる。
 Pythonコード
---11行目で予め画像を保存していたフォルダー名を入力する
---12行目で画像ファイル形式を入力する

---7と書いた画像(右の写真)を読み込ませたところ0と出力されてしまった。もう少しピクセルの大きさを大きくする必要がありそう。
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/Slide7.png,150x);

-畳込みニューラルネットワーク(CNN)について
--特に画像認識や画像分類で優れた性能を発揮するディープニューラルネットワークの一種である。ディープニューラルネットワークとは通常のニューラルネットワークは層の数が3なのに対し、さらに畳込み層やプーリング層等多くの層から構成されているため層の深さによって"ディープ"がつくかどうか決まる。

-層の用語について
--畳み込み層 … 画像におけるエッジや色の変化といった局所的な特徴を抽出することで、空間構造を把握しながら高度な特徴の抽出が可能となる。
--プーリング層 … 画像の特徴と位置を紐付けてしまうとCNNの場合同じ特徴が画像の別の部分に現れたとしてもそれを抽出することができない。なので特徴から位置情報を削ぎ落とすことで畳み込み層で抽出された特徴が他の画像内のどこに移動(上下・左右)としても影響を受けないようにする。
--全結合層 … 畳み込み層・プーリング層で抽出された特徴から最終的にクラスの分類や予測を行う層のこと。


,手順 ,実行内容
,1,画像を入力する(ピクセルで) 
,2,畳み込み層で画像の特徴を検出する
,3,プーリング層で重要な情報を抽出する
,4,2と3を繰り返す
,5,全結合層で画像の特徴をもとに分類を行う
,6,出力する

-CNNとSVMの違いについて

, 比較項目 , SVM , CNN 
, モデル , すでに用意された特徴(数値)をもとにクラス分けする数式モデル ,画像の中から特徴を自動で学習して分類まで行うニューラルネットワーク
, 入力 , 数値データ (特徴量ベクトル) → 画像を数値に変換する必要あり ,生の画像そのまま(ピクセルデータ)
, 特徴の抽出方法 , 手動(人があらかじめ決めた特徴) ,自動(エッジ・色・形などを学習)
, モデル構造 , 数学的な式(境界面) ,多層のニューラルネットワーク
, 適用例 , 小規模なデータで分類(例:文字分類、小さな画像) ,大規模な画像分類・認識(例:顔認識、医療画像、サビ分類など)
, 学習の難易度 , 比較的簡単(実装も軽い) ,やや難しい(GPUや深層学習の知識が必要)
, 処理速度 , 小規模なら早い ,訓練に時間がかかる(推論は速い)

--SVM → 与えられた特徴を元に最適な境界線で分類する方法。この方法は手軽だが特徴量の選び方次第
--CNN → 画像の中から重要な特徴を自分で見つけて分類してくれる方法。学習に時間がかかるが精度が高く応用範囲が広い


**5月 [#j417c500]

***5月第1週(5/1〜5/7) [#j417c502]
-手書き数字の機械学習(CNN, 判別)について
--mnistのテストデータに入っている数字を判別させたところ正答率が100%であったのに対し、mnistには保存されていない数字ファイルを判別させたところ正答率は67%となった。
--mnist以外の数字画像はまだ正確に判別できているとはいえず、これから判別制度の向上と様々な数字画像を学習させていく必要がありそう。
--また mnist.py または mnist-hosei.py を何度もMATE端末上で動かしてmy_mnist_model.h5ファイルを更新し続けると前のモデルにさらに学習を重ねることでモデルが過剰に記憶し汎化性能(正答率)が落ちてしまう(これを過学習と呼ぶ)。なので定期的にmy_mnist_model.h5ファイルを削除して更新する必要がある。または2つのPythonファイルで保存場所を変えるのも良いのではないか。

-mnistの画像を少し補正
--今までは画像の中心に数字を書いて機械学習を行ってきたが、home/kouzou/Morii25/hikitugi 内にある mnist-hosei.py ファイルでは画像の見た目を変えるため 回転・移動・拡大・変形 を行って機械学習させた。その結果以前よりも精度は上がった。

,学習方法 ,テストデータ正答率(%) ,判別精度(%)
,そのままの状態で画像を学習 ,99.14 ,44 (4/9)
,画像を回転・移動・拡大・変形させて学習 ,98.49 ,67 (6/9)

-エポック数を増やして学習させてみた

---そのままの状態で画像を学習させた時
,エポック数 ,テストデータ正答率(%) ,判別精度(%)
,10 ,99.20 ,67 (6/9)
,12 ,99.13 ,56 (5/9)
,13 ,99.26 ,89 (8/9)
,14 ,99.19 ,89 (8/9)
,15 ,99.16 ,89 (8/9)
,16 ,99.08 ,89 (8/9)

--エポック数が13を超えると判別精度が向上したがmnistにはない数字の画像を判別させると4の画像だけどうしても4と認識されない。エポック数によって4の画像のときだけ"1"や"9"と認識されてしまう。
--4の画像を識別させたときにどれくらいの確率でどの数字であるかをPythonで作成してみるのも1つの手かも

---画像を回転・移動・拡大・変形させて学習させた時
,エポック数 ,テストデータ正答率(%) ,判別精度(%)
,10 ,95.47 ,56 (5/9)
,12 ,87.01 ,56 (5/9)
,13 ,90.55 ,56 (5/9)
,14 ,96.15 ,56 (5/9)
,15 ,86.66 ,56 (5/9)
,16 ,74.12 ,56 (5/9)

--エポック数の大小に関わらず判別精度は変化せず、特に5, 7, 9の画像が"3"と認識されてしまった。これだけで比べるとテストデータ正答率, 判別精度ともにそのままの状態で画像を学習させた時の方が良い判別結果が得られそうだ。

-来週にやること
--GW中に手書き数字の機械学習は終わらせたい
---自身で手書き数字を書いてみてそれがどのくらいの精度であるか確かめる
--動物の顔(仮)が機械学習を通じて判別できるようになるスクリプトを書き始める
---とりあえず多くの動物の顔の写真が載っているサイトを探す。


-MNISTについて
--MNISTを用いた手書き文字の画像判別はどのサイト・ホームページ (MNIST 判別 正解率と検索すれば確認できる) を見ても9割以上、特にCNNで学習させた場合99%前後まで精度が向上する。
--限りなく100%の正解率に持っていく手法はあるだろうが正解率100%になることはない。

-手書き数字の機械学習 (続き)
--とりあえず自身で作成した手書き数字を機械学習に判別させてみた

,数字 ,モデル数 ,正答率
,0 ,3 ,3/3
,1 ,5 ,5/5
,2 ,6 ,6/6
,3 ,6 ,6/6
,4 ,2 ,2/2
,5 ,10 ,9/10
,6 ,2 ,1/2
,7 ,3 ,2/3
,8 ,2 ,1/2
,9 ,13 ,9/13

本来は各数字のモデル数が揃っていることが望ましいが、こちらの諸事情により用意することができなかった。モデル数が 2 や 3 としかない部分は正確さに欠ける所があるが、1 ~ 3 はかなり正確性があると言って良いのではないか。特に 9 は 7 と間違えて識別するケースが多く見受けられた。


-来週までにやること
--手書き数字の画像に点や線、枠を入れても正確に判別できるのか
--自動的に画像内のどこか適当なところに点や図形を自動的に入れることができるプログラムを組む
--動物の顔を使った機械学習(できたら)

--自分の研究についてスライドを3枚程度作成する

-明日にやること
--数字に点や線, 図形を加えた結果の表を作成
--使用した画像を載せる
--色を白ではなく他の色にするとどうなるか (エラーを吐き出される可能性あり)


***5月第2週(5/8〜5/14) [#j417c508]
-画像に数字に点や線, 図形を加えてみた。これを判別してみる

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/0_0.png,150x); ー画像加工→
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/modified_0_0.png,150x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/5_0.png,150x); ー画像加工→
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/modified_5_0.png,150x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/7_2.png,150x); ー画像加工→
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/modified_7_2.png,150x);

 Pythonコード (Gazou-Kakou.py)



-- 0 ~ 9 の画像数をそれぞれ9枚に統一させた。表の真ん中の列は画像に何も加工せず数字ごとにおける正答率を表したものである。(上の画像の右側)
--表の右側の列は数字に点や線, 図形のすべてを画像に加え数字ごとにおける正答率を表したものである。(上の画像の左側)(Gazou-Kakou.py を使用)
,数字 ,正答率 ,正答率
,0 ,9/9 ,9/9
,1 ,8/9 ,1/9
,2 ,9/9 ,6/9
,3 ,9/9 ,6/9
,4 ,9/9 ,0/9
,5 ,9/9 ,5/9
,6 ,5/9 ,2/9
,7 ,8/9 ,4/9
,8 ,6/9 ,6/9
,9 ,6/9 ,0/9

---数字は太くはっきり書いてあること且つ線や図形と被っていなければ判別することはできそう
---ただ要素を加えすぎたせいか無加工と比べてかなり判別精度が低下してしまっている。なのでこれから点, 線, 図形のうちどれか1つか2つの要素だけを画像に取り込んでみて精度がどう変化するのか見ていく。

-改良版
--画像を10枚に統一させてフォントを太くした。以下の表はその画像に何も加工せず数字ごとにおける正答率を表したものである。(KGPrimaryPenmanship.ttfとSuuji-Gazou-Seisei.py を使用)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/0_100.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/5_100.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/7_100.png,150x);

,数字 ,正答率
,0 ,10/10
,1 ,10/10
,2 ,10/10
,3 ,10/10
,4 ,10/10
,5 ,10/10
,6 ,10/10
,7 ,10/10
,8 ,10/10
,9 ,10/10

--数字を中心に持ってきたからなのかまたはほとんど形が変わらないからなのか、それともその両方なのか分からないが全て正確に判別することができた。

--次に点, 線, 図形のうち1つの要素だけを画像に加え数字ごとにおける正答率を表にしたものである。左の表から順に点, 線, 図形のみを加えた。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/0_101.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/5_101.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/7_101.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/0-8.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/5-8.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/7-2.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/0_201.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/5_201.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/7_201.png,150x);
,数字 ,点 正答率 ,線 正答率 ,図形 正答率(リスタート前) ,図形 正答率(リスタート後)
,0 ,10/10 ,9/10  ,8/10  ,10/10
,1 ,10/10 ,4/10  ,5/10  ,6/10
,2 ,10/10 ,9/10  ,8/10  ,10/10
,3 ,10/10 ,7/10  ,9/10  ,10/10
,4 ,10/10 ,9/10  ,8/10  ,8/10
,5 ,10/10 ,9/10  ,10/10 ,10/10
,6 ,8/10  ,3/10  ,1/10  ,3/10
,7 ,10/10 ,7/10  ,7/10  ,8/10
,8 ,10/10 ,10/10 ,10/10 ,10/10
,9 ,10/10 ,7/10  ,7/10  ,6/10

--点は1画像あたり50個加えた(Gazou-Kakou-Ten_only.py を使用)
 Pythonコード (Gazou-Kakou-Ten_only.py)
--線は1画像あたり5本加えた(Gazou-Kakou-Sen_only.py を使用)
 Pythonコード (Gazou-Kakou-Sen_only.py)
--図形は1画像あたり三角形から十角形までの多角形を1〜3個ランダムに設置した。図形のリスタート前・後とは過学習状態をリセットした前後の時を指す。(Gazou-Kakou-Zukei_only.py を使用)
 Pythonコード (Gazou-Kakou-Zukei_only.py)

-判別させたい手書き文字を作成するには
--① GIMPで直接書きたい場合 (推奨)
---1, GIMPを開く
---2, 上のツールバーから ファイル → 新しい画像 の順にクリックする。そうするとピクセルのサイズを指定できるので両方に28と記入する。
---3, 左側に下のようなアイコンが現れたと思うがこれは背景色と描画色を表しており、右下の色アイコンが背景色, 左上が描画色である。それぞれクリックすると自身で色を選択できるようになり、基本的に背景色は黒, 描画色は白に 描画ツールは鉛筆に設定する。(壁画ツールは上のツールバーから "ツール → 描画ツール → 鉛筆で描画" で設定できる)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/aikon.png,150x);

---4, アイコンの下にはサイズ, 縦横比, 角度, .... と大きさや強弱を設定できるパラメータが縦に並んでおり、サイズは必ず1.00(ピクセル)に設定しておくこと。そうしないと上手く文字を書くことができなくなる。
---5, 文字を書き直したいときはアイコンの上にある消しゴムを使って文字を消すことができる。
---6, 書き終えたら保存し、その際にどこに保存するかを指定する。続けて文字を書きたい場合は上のツールバーから ファイル → 新しい画像 の順に選択
---7, 保存はxcfファイルでされるため一度pngに変換する必要があるため変換してから判別してもらうようにする。
---※スクリプトでxcfファイルを直接判別してもらえるような文言を書いてみてもいいかも

--② AIに書かせる場合 (ChatGPT)
--1, ChatGPTに"黒背景に白の線で 0 ~ 9 の数字を各5枚、合計50枚の手書き数字を書いてください” という旨のメッセージを送る。
--2, 画像が作成されるまで1, 2時間かかる場合があるためその間は待機 (この時間がもったいないため非推奨)
--3, 生成された画像は複数の文字(数字)が1枚の画像となって出力されるのでこれをGIMPで分割させる。詳しくは機械学習のWikiページを参照 (https://www.str.ce.akita-u.ac.jp/cgi-bin/pukiwiki/?%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92)
--4, 分割できたら各画像を保存 → png に変換 → 28×28ピクセルに変換


-画像に点, 線, 図形をそれぞれ加えたときの正答率について

--点を加えたときに関してはかなり精度の高い判別ができたのではないかと思う。加工した後の画像を見るともう少し点の数を増やしてみても判別精度は維持できそうだ。
--線を加えると判別精度は点の時と比較してやや劣るが、その中でも"6"における精度の劣り方が良くない。ランダムに5本の線を引くようにプログラムしているので、再び画像に加工処理を行って判別させると精度は変化するのだろうか。下にある7に線を引いた画像を見てもらえると分かるがこれは文字の太さから辛うじて"7"と認識できる。機械でこの画像を"7"と判別させるにはやはりMNISTのデータ量以上の数字データが欲しい。
--最後に図形を加えた場合数字によって精度のばらつきがある。特に"1", "6", "9"は他の数字と比較して精度が低く、1の場合周囲に6角形以上の多角形が加わると"6", "8", "9"のように見えてしまい精度に影響が出たのではないか。"6"と"9"も同様で数字に重なるように図形が描かれることで"8"と判別されてしまったのではないか。また図形同士が上下に描かれることで"8"と認識される場合もありそうだ。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/6_201.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/9_201.png,150x);

-次回やること
--MNISTで学習させてからある数字の画像を何と判別したかをフルオート化できるようなスクリプトを組みたい
  →大まかな内容は完了しこれから肉付け(5/11)
--動物の顔を使った機械学習(できたら)


-とりあえずmnistで学習させた後 "判別したい画像を1つのフォルダーにまとめて画像加工, 判別(○ or ×), 数字ごとの判別率" までの " " で囲った一連の作業を全自動で行えるようにスクリプトを組んだ。画像加工については大きく分けて点, 線, 図形の3種類があり加工の有無(画像に入れるかどうか)と数を任意に設定できるようになっている。
-全自動スクリプトにmnistで学習させる作業を入れなかった理由としては常に新しい h5 ファイルを取り入れたいためである。Python上で何度もMnistファイルを実行してしまうと過学習が発生してしまい判別率が低下してしまうことがある。

 Pythonスクリプト (Hanbetsu-Full_auto.py)

-上記のスクリプトを実行すると "既存の画像を加工したフォルダー" と "判別結果のテキストファイル" が保存されていると思う。
--テキストファイルの中身
,0_7.png 予測: 0 正解: 0 一致: 〇
,1_5.png 予測: 8 正解: 1 一致: ×
,3_5.png 予測: 3 正解: 3 一致: 〇
,8_2.png 予測: 8 正解: 8 一致: 〇
,…
,--- 数字ごとの正答率 ---
,0: 正解 ○ / □ 枚 → 正答率 △ %
,1: 正解 △ / ○ 枚 → 正答率 □ %
, …

--以下の表は全自動スクリプトを3回用いて判別させた数字ごとの正答率である。(その都度mnistのh5ファイルは新しくしている)
,数字  ,正答率  ,正解率  ,-正答率-  ,-正解率-  ,--正答率--  ,--正解率-- 
,0 ,10/10 ,100.00%         ,10/10 ,100.00%          ,10/10 ,100.00%
,1 ,0/10 ,0.00%                ,8/10 ,80.00%                ,5/10 ,50.00%
,2 ,10/10 ,100.00%         ,5/10 ,50.00%                ,10/10 ,100.00%
,3 ,10/10 ,100.00%         ,10/10 ,100.00%            ,10/10 ,100.00%
,4 ,6/10 ,60.00%              ,4/10 ,40.00%                 ,5/10 ,50.00%
,5 ,10/10 ,100.00%         ,10/10 ,100.00%            ,10/10 ,100.00%
,6 ,0/10 ,0.00%                ,0/10 ,0.00%                    ,3/10 ,30.00%
,7 ,10/10 ,100.00%         ,1/10 ,10.00%                 ,9/10 ,90.00%
,8 ,10/10 ,100.00%         ,10/10 ,100.00%            ,10/10 ,100.00%
,9 ,10/10 ,100.00%         ,10/10 ,100.00%            ,10/10 ,100.00%


 Pythonスクリプト (Mnist-Hanbetsu-Full_auto.py)

-上記のスクリプトで出来ること
--mnistの数字画像を用いて学習し、その結果をh5ファイルに保存。 (もし前回のh5ファイルが保存されていたら上書きするのではなく削除して新規作成するようにした)
--判別させたい数字画像に図形を加える等の加工をする。
--加工した画像を見られるように別のフォルダーを作成して保存
--加工した図形を判別させて合っていたか、また数字別の正解率をテキストファイルで表示。
--→判別させたい画像をフォルダーに入れておき Mnist-Hanbetsu-Full_auto.py を実行するだけで正解率まで表示されるようにした。

--以下の表は"シン"・全自動スクリプトを3回用いて判別させた数字ごとの正答率である。
,数字  ,正答率  ,正解率  ,-正答率-  ,-正解率-  ,--正答率--  ,--正解率--        
,0 ,10/10 ,100.00%       ,10/10  ,100.00%       ,10/10  ,100.00%
,1 ,0/10 ,0.00%               ,0/10  ,0.00%               ,2/10  ,20.00%
,2 ,8/10 ,80.00%            ,9/10  ,90.00%             ,10/10  ,100.00%
,3 ,10/10 ,100.00%        ,7/10  ,70.00%            ,8/10  ,80.00%
,4 ,8/10 ,80.00%             ,9/10  ,90.00%            ,1/10  ,10.00%
,5 ,10/10 ,100.00%        ,10/10  ,100.00%       ,10/10  ,100.00%
,6 ,1/10 ,10.00%             ,4/10  ,40.00%            ,0/10  ,0.00%
,7 ,2/10 ,20.00%             ,7/10  ,70.00%             ,2/10  ,20.00%
,8 ,10/10 ,100.00%        ,10/10  ,100.00%        ,1/10  ,10.00%
,9 ,10/10 ,100.00%        ,9/10  ,90.00%             ,10/10  ,100.00%

--3回とも正答率が100%である数字もあれば判別を1回行うごとに正答率の差が激しいものも存在する。特に"7"はその差が顕著に表れており、常に100%に近い正答率を得るには何か別のアプローチから試してみる必要がある。

-2種類の全自動スクリプトで判別に使用した画像(画像加工前)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/0_100.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/5_100.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/7_100.png,150x);


 Pythonスクリプト (Mnist-Hanbetsu-Full_auto2.py)

-上記のスクリプトの違い
--mnistの中にある画像を加工して学習させた  (加工方法と内容は判別画像と同様) → 判別率を上げるため
--以下の表は Mnist-Hanbetsu-Full_auto2.py を用いて判別させた数字ごとの正答率である。

,数字  ,正答率  ,正解率  ,-正答率-  ,-正解率-  ,--正答率--  ,--正解率--      
,0 ,15/15  ,100.00%  ,15/15  ,100.00%  ,15/15  ,100.00%
,1 ,15/16  ,93.75%     ,15/16  ,93.75%    ,16/16  ,100.00%
,2 ,15/16  ,93.75%     ,16/16  ,100.00%  ,15/16  ,93.75%
,3 ,16/16  ,100.00%  ,16/16  ,100.00%   ,16/16  ,100.00%
,4 ,15/16  ,93.75%     ,15/16  ,93.75%     ,15/16  ,93.75%
,5 ,15/16  ,93.75%     ,15/16  ,93.75%     ,15/16  ,93.75%
,6 ,12/16  ,75.00%     ,6/16  ,37.50%       ,8/16  ,50.00%
,7 ,15/16  ,93.75%     ,14/16  ,87.50%     ,16/16  ,100.00%
,8 ,16/16  ,100.00%   ,15/16  ,93.75%     ,15/16  ,93.75%
,9 ,15/16  ,93.75%     ,16/16  ,100.00%   ,15/16  ,93.75%

--今までと比べて作業効率も識別の精度も上がっていると思う。mnistのデータ数でも十分学習できるのだが、やはりもっと多くの手書き数字の画像データが欲しい。→ Pythonスクリプトを考えているが手書き文字に寄せるところに難あり
--どのスクリプトを実行しても"6"も識別が上手くいかない。明日は判別させたいデータを全てmnistから無作為に抽出したものに置き換えてやってみる。(今までは手書きとは言えない画像データで判別させていた)

-明日やること
--日誌に画像を貼り付ける
--mnistから抽出したデータで判別させる
--明後日に向けて自身の研究内容を英語で書いておく


-mnistから抽出したデータで判別
--判別する画像の対象を以下の画像に変更した。(今までよりも手書き感を出した)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/0_0.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/5_0.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/7_2.png,150x);

--以下の表は全自動スクリプトを用いて判別させた数字ごとの正答率である。
,数字  ,正答率  ,正解率  ,正答率  ,正解率  ,正答率  ,正解率  ,正答率  ,正解率  ,正答率  ,正解率
,0 ,20/20 ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%
,1 ,20/20 ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%
,2 ,20/20 ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,19/20  ,95.00%
,3 ,20/20 ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%
,4 ,20/20 ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%
,5 ,19/20 ,95.00%     ,20/20  ,100.00%  ,18/20  ,90.00%    ,20/20  ,100.00%  ,20/20  ,100.00%
,6 ,20/20 ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%
,7 ,17/20 ,85.00%     ,20/20  ,100.00%  ,19/20  ,95.00%    ,19/20  ,95.00%     ,19/20  ,95.00%
,8 ,20/20 ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%  ,20/20  ,100.00%
,9 ,19/20 ,95.00%     ,18/20  ,90.00%    ,19/20  ,95.00%     ,18/20  ,90.00%    ,20/20  ,100.00%

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/20250513-1.png,600x);

--全体的に高い判別精度が得られ0〜4と 6, 8はすべての回で100%近い判別率を達成しており、画像処理を行ってから機械学習させるとより良い結果が得られることが分かった。一方で5, 7, 9は精度にばらつきが生じており、これは処理の影響が強く出てしまったことで形が似ている数字(例:7と1、9と4など)との誤認識が起きやすい状況が発生している。

--精度をさらに上げるためには
---ノイズの位置をランダムではなく重要領域を避けるように設計する。ここでは数字が書かれている部分のことを指す = 画像中央付近へのノイズを減らす
---Dropoutの実装 (過学習の防止とアンサンブル効果を得るため)
---混同行列を作成しどの数字が判別に弱いのかを確認する

-アンサンブル効果とは
個々の学習モデルにはそれぞれ異なる「誤り」や「予測の癖」を持っており、それらをうまく組み合わせることで単体のモデルよりも高い性能(精度向上, 過学習のリスク減少)を得られる効果のこと。

 Pythonスクリプト (Hanbetsu-Full_auto3.py) 

--画像中央付近への点の描画を減らした。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_1_0.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_4_0.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_9_0.png,150x); → 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_1_1.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_4_1.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_9_1.png,150x);


--混同行列ををpng形式で表示・保存できるように設定した。
--以下の表はそのスクリプトを用いて判別させた数字ごとの正答率である。

,数字  ,正答率  ,正解率  ,正答率  ,正解率  ,正答率  ,正解率 
,0  ,20/20  ,100.00%         ,20/20  ,100.00%         ,20/20  ,100.00%
,1  ,20/20  ,100.00%         ,19/20  ,95.00%           ,19/20  ,95.00%
,2  ,20/20  ,100.00%         ,20/20  ,100.00%         ,19/20  ,95.00%
,3  ,20/20  ,100.00%         ,20/20  ,100.00%         ,20/20  ,100.00%
,4  ,20/20  ,100.00%         ,20/20  ,100.00%         ,20/20  ,100.00%
,5  ,20/20  ,100.00%         ,19/20  ,95.00%           ,19/20  ,95.00%
,6  ,20/20  ,100.00%         ,20/20  ,100.00%         ,20/20  ,100.00%
,7  ,19/20  ,95.00%            ,19/20  ,95.00%           ,20/20  ,100.00%
,8  ,20/20  ,100.00%         ,20/20  ,100.00%         ,20/20  ,100.00%
,9  ,20/20  ,100.00%         ,19/20  ,95.00%            ,20/20  ,100.00%

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/Figure_1.png,500x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/Figure_2.png,500x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/Figure_4.png,500x);

-上記の行列の見方
--縦軸は True Label (画像に書いてある実際の数字 = 答え), 横軸は Predicted Label (機械によって判別された数字) であり、濃い青で塗りつぶされている部分は実際に書いてある数字を機械が正確に判別できた数を表している。
--濃い青で塗りつぶされていない部分は機械が実際の数字とは異なる数字を判別した数を表している。
--1番右の Recall は数字別の判別率を表している。


自身の研究内容について

研究テーマ : 耐候性鋼橋の耐候性鋼材の錆の現地調査に参加しながら、撮影データに対して機械学習(AI)を用いた外観評価を行う。最終的にドローンで撮影した耐候性鋼橋の画像データに対して外観評点を行う方法も検討する

About his own research 
Research theme : While participating in a field survey of rust on weathering steel materials of weather-resistant steel bridges, he will evaluate the appearance using machine learning (AI) on the photographed data. Finally, a method to evaluate the appearance of weather-resistant steel bridges using image data taken by a drone will also be studied.

耐候性鋼材の錆の調査は来週の水曜日に行く予定であり、この研究に関しては錆のデータをもらわないと何もすることができないので現在は錆の画像をAIで外観評価する前段階として0~9までの手書き数字(mnist)をCNNを用いて学習させ、正確に判別できるかどうかを行っている。

The investigation of rust on weather resistant steel is scheduled to go next Wednesday, and since we cannot do anything about this research without receiving rust data, we are currently learning handwritten numbers (mnist) from 0 to 9 using CNN as a preliminary step to evaluate the appearance of rust images using AI to see if we can accurately identify the rust. We are now trying to see if it is possible to accurately discriminate the rust images.


1, 画像処理を加えてCNNで学習させたもの

1, The images of mnist, a data set containing nearly 60,000 handwritten digit images, were trained by CNN, and the correct answer rate was obtained by adding points, lines, circles, hexagons, and other shapes to the image to be discriminated.

使用したデータ Data used(example) &ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/Slide7.png,150x);

,数字  ,正答率  ,正解率 
,number ,correct response rate ,correct response rate
,0 ,9/9 ,9/9
,1 ,8/9 ,1/9
,2 ,9/9 ,6/9
,3 ,9/9 ,6/9
,4 ,9/9 ,0/9
,5 ,9/9 ,5/9
,6 ,5/9 ,2/9
,7 ,8/9 ,4/9
,8 ,6/9 ,6/9
,9 ,6/9 ,0/9


2, 判別したい画像を以下のように変更し 判別する画像の加工, 判別の正誤をテキストファイルに保存するまでを1つのスクリプトにまとめた。

2, Change the image to be discriminated as follows, process the image to be discriminated, and save the correct and incorrect discriminions to a text file, all in one script.

,数字  ,正答率  ,正解率 
,number ,correct response rate ,correct response rate
,0 ,10/10 ,100.00%   
,1 ,0/10 ,0.00%             
,2 ,10/10 ,100.00%      
,3 ,10/10 ,100.00%       
,4 ,6/10 ,60.00%      
,5 ,10/10 ,100.00%  
,6 ,0/10 ,0.00%    
,7 ,10/10 ,100.00%    
,8 ,10/10 ,100.00%    
,9 ,10/10 ,100.00%   


3, 2で用いたスクリプトはmnistをCNNで用いて学習させる機能を持っていなかった。そのため2のスクリプトにmnistをCNNで用いて学習させるスクリプトを追加した。それに加えて判別の正誤だけでなく数字別の正答率も出すようにした。

3, The script used in 2, did not have the ability to train mnist with CNN. Therefore, we added a script for training mnist using CNN to the script used in 2. In addition, we added the ability to produce not only the correctness of the discriminant but also the percentage of correct answers for each number.

,数字  ,正答率  ,正解率 
,number ,correct answer rate ,correct answer rate
,1 ,0/10 ,0.00%          
,2 ,8/10 ,80.00%         
,3 ,10/10 ,100.00%      
,4 ,8/10 ,80.00%         
,5 ,10/10 ,100.00%      
,6 ,1/10 ,10.00%         
,7 ,10/10 ,100.00%
,8 ,10/10 ,100.00%      
,9 ,10/10 ,100.00%      


4, 判別したい画像に加工処理を施すだけでは正答率を100%に近づけるのに限界があると考え、学習データにも加工処理を施した。(mnistの画像にも点や直線, 円や六角形等の図形を加えた)

4, Since there is a limit to achieving a correct response rate close to 100% only by processing the images to be discriminated, we also processed the training data.  (Points, lines, circles, hexagons, and other shapes were added to the mnist images.) 

,数字  ,正答率  ,正解率   
,number ,correct answer rate ,correct answer rate
,0 ,15/15  ,100.00% 
,1 ,15/16  ,93.75%   
,2 ,15/16  ,93.75%    
,3 ,16/16  ,100.00%  
,4 ,15/16  ,93.75%  
,5 ,15/16  ,93.75%  
,6 ,12/16  ,75.00%  
,7 ,15/16  ,93.75% 
,8 ,16/16  ,100.00%  
,9 ,15/16  ,93.75%  

Data used in 2-4 (example)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/0_100.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/5_100.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/7_100.png,150x);


5, 判別したい画像をmnistから抽出したデータに変更した。2~4で使っていた数字の画像と比較して精度に差が出るのか試してみたところmnistから抽出したもののほうが精度が高かったためこちらを使うことにした。

5, The image to be discriminated was changed to the data extracted from mnist, and the accuracy of the image extracted from mnist was higher than that of the image used in steps 2~4.

,数字  ,正答率  ,正解率
,number ,correct answer rate ,correct answer rate
,0 ,20/20  ,100.00%  
,1 ,20/20  ,100.00% 
,2 ,20/20  ,100.00%  
,3 ,20/20  ,100.00%  
,4 ,20/20  ,100.00%  
,5 ,20/20  ,100.00%
,6 ,20/20  ,100.00%  
,7 ,20/20  ,100.00%  
,8 ,20/20  ,100.00%  
,9 ,18/20  ,90.00%    

Data used(example)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/0_0.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/5_0.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250508/7_2.png,150x);


6, 数字の書いてある部分(画像の中心部分)になるべく白い点を入れないようにした

6, I tried to avoid white dots in the numbered area (center of the image) as much as possible.

,数字  ,正答率  ,正解率 
,number ,correct answer rate ,correct answer rate
,0  ,20/20  ,100.00%  
,1  ,20/20  ,100.00%     
,2  ,20/20  ,100.00%   
,3  ,20/20  ,100.00%   
,4  ,20/20  ,100.00%    
,5  ,20/20  ,100.00%     
,6  ,20/20  ,100.00%  
,7  ,19/20  ,95.00%       
,8  ,20/20  ,100.00%  
,9  ,20/20  ,100.00%   

Data used(example)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_1_0.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_9_0.png,150x); → 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_1_1.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250513/noised_9_1.png,150x);


***5月第3週(5/15〜21) 物体分類判別 [#j417c515]
-判別をするにあたって以下の要素を加えた
--mnistの画像を加工した状態で機械学習を行う
--学習の偏りを避けるため複数の学習モデルを構築 ( h5 → keras ファイルを3つ作成) ※kerasファイルはTensorFlowの標準形式をベースに構築された新しいファイル形式であり、h5ファイルは古い保存形式であるため場合によってPython上で実行するとエラーが出る可能性あり。
--過学習抑制のためDropout関数の実装
--エポック数を増やす
--機械学習によてどの数字が判別に弱いのかを知るために混同行列を作成し可視化
--判別させる画像にも加工を加える
--最良のモデルを保存するためにEarlyStoppingとModelCheckpointを実装

これら全てを取り込んだのが以下の Ver5.0 = Mnist-Hanbetsu-Full_auto5.py である。

 Pythonコード (Mnist-Hanbetsu-Full_auto5.py)

,数字  ,正答率  ,正解率  ,正答率  ,正解率  ,正答率  ,正解率 
,0 ,19/20 ,95.00%    ,19/20 ,95.00%    ,18/20 ,90.00% 
,1 ,8/20 ,40.00%      ,19/20 ,95.00%    ,6/20 ,30.00% 
,2 ,19/20 ,95.00%    ,19/20 ,95.00%    ,19/20 ,95.00% 
,3 ,20/20 ,100.00% ,18/20 ,90.00%    ,19/20 ,95.00% 
,4 ,18/20 ,90.00%    ,18/20 ,90.00%    ,20/20 ,100.00% 
,5 ,19/20 ,95.00%    ,18/20 ,90.00%    ,20/20 ,100.00% 
,6 ,20/20 ,100.00% ,20/20 ,95.00%    ,20/20 ,100.00% 
,7 ,18/20 ,90.00%    ,15/20 ,75.00%    ,19/20 ,95.00% 
,8 ,17/20 ,85.00%    ,20/20 ,100.00%  ,17/20 ,85.00% 
,9 ,18/20 ,90.00%    ,19/20 ,95.00%    ,18/20 ,90.00% 

Mnist-Hanbetsu-Full_auto4.py を実行したときと比較して明らかに判別精度が落ちてしまっている。しかし下に示した混同行列をみると訓練モデルは正確に判別できている。(左側1枚がVer4.0のとき 右側3枚がVer5.0)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/Ver4.0.png,400x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/Ver5.0.png,400x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/Ver5.0_2.png,400x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/Ver5.0_4.png,400x);

下記の内容が判別精度低下の主な原因ではないかと考えている。
--学習方法を同一のCNNモデルから3種類のCNNモデルへ変更(アンサンブル学習) → 小さめ(層の薄い)のCNNモデル①と大きめ(層の薄い)のCNNモデル②, モデルの汎化性能を向上させる正則化を強化したCNNモデル③
--間違えて判別した画像を再学習させて精度の向上を図る

---1, 複数の学習方法の統合によるモデルの一貫性の欠如
→アンサンブル学習を行なうため3つの学習モデルで判別を行っているが、それぞれの学習結果や特性がうまく調和していないと予測の信頼性が落ちる可能性あり。

---2, 誤判別画像の再学習による過学習や偏り
→誤判別された画像のみを再学習させることで過剰に最適化され、汎化性能が落ちることがあるうえ再学習データが少数であったり偏っていると全体のバランスが崩れてしまうことがある。

---3, スクリプトの構成が複雑化したことでデータ処理や前処理の不整合が発生している可能性
→スクリプトの行が多くなるとどうしても学習データと推論データで画像サイズや正規化, ノイズの種類や強度といった前処理の定義が微妙に異なってしまうことがある。モデルが期待するデータと異なるものを判別してしまい精度が下がることがある。

-使用したCNNモデルについて
,要素 ,モデル① ,モデル② ,モデル③
,畳み込み層数 ,1 ,2 ,2
,カーネルサイズ ,3×3 ,3×3(2回) ,5×5 → 3×3
,プーリング ,あり(1回) ,あり(1回) ,あり(1回)
,Dropout ,なし ,あり(0.3) ,あり(0.25・0.5)
,BatchNorm ,なし ,あり(1回) ,あり(1回)
,全結合層 ,64ユニット ,128ユニット ,64ユニット

以下は全5バージョンのMnist-Hanbetsu-Full_autoを実行したときの判別率である。左から順にVer1.0(Mnist-Hanbetsu-Full_auto.py), Ver2.0(Mnist-Hanbetsu-Full_auto2.py), ... ,Ver5.0(Mnist-Hanbetsu-Full_auto5.py)と並んでいる。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/Ver1.0.png,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/Ver2.0.png,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/Ver3.0.png,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/Ver4.0_1.png,330x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/Ver5.0_1.png,330x);

以上より手書き数字の画像判別はあまり要素を盛り込みすぎることなく判別させることが重要であるのではないか。 (Ver4.0が数字の画像判別に1番適していると考える)


物体分類の判別
-以下のサイト (CIFAR-10) にあるデータを用いて画像識別を行った。
 https://www.cs.toronto.edu/~kriz/cifar.html
-CIFAR-10には飛行機, 自動車, 鳥, 猫, 鹿, 犬, カエル, 馬, 船, トラック の10種類の画像が訓練データ(約5万枚) テストデータ(約1万枚)合わせておよそ6万枚の画像が保存されている。ここからMnistの時と同様にCNNを用いて機械学習を行っていく。

-Pythonスクリプトの実装
--とりあえずCIFAR-10上にある画像で学習を行い混同行列を作成してみた。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/00.png,400x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250516/01.png,400x);

左側がエポック数20, 右側がエポック数50としたがあまり精度は向上しなかった。

-来週やること
--Animal.pyを改良して精度が向上するか確かめる
--今日はただCIFAR-10上にある画像を学習させただけなのでテストデータから画像を抽出して実際に判別させてみる。


-物体分類の判別について最終的な目標として

--1, 学習する画像には特に加工を加えることなく
--2, テストデータから何枚か画像を抽出して判別をさせて
--3, 10種類ごとに正答率の割合を算出することを目的とする。

とりあえず
-① 学習画像は無加工とし混同行列を表示させるスクリプトと
-② テストデータから画像を抽出するスクリプト, 
-③ 学習データを用いてテストデータを判別させるスクリプト
を組んでみた。3つとも独立(手動)で実行させてみて特に気になる箇所が無ければスクリプトをまとめる。

--③について①で作成された学習データをもとに判別した結果を以下の表に示す。(抽出したデータは各20枚)

,タグ ,正答率
,airplane   ,90.00 % 
,automobile ,100.00 % 
,bird       ,85.00 % 
,cat        ,80.00 % 
,deer       ,95.00 % 
,dog        ,55.00 % 
,frog       ,95.00 % 
,horse      ,100.00 % 
,ship       ,100.00 % 
,truck      ,95.00 % 

 Pythonコード① (Animal.py)

 Pythonコード② (Yobidashi.py)

 Pythonコード③ (Buttai-Bunrui.py)


-1回の学習 (.py を実行させてから全て出力し終えるまで) で15時間近くかかるようになってきたため、下手な鉄砲も数打てば当たるという考え方が通用しなくなった。これからはスクリプトに間違いがないか慎重に確かめる必要がある。
--エポック数を1にして学習させれば20分ぐらいで結果が出力される事が分かった(5/23追記)
-その間に明日から本格的に始動する研究に向けて耐候性鋼材の錆をCNNを用いてどう判別するか考えていた。

目標に向けて大まかなステップ

--1, 100 枚/クラス を目標にデータ収集&CSV ラベリング
--2, .img .jpg形式で整理
--3, 整理した画像を 転移学習+EarlyStopping で試走
--4, 混同行列 と Grad‑CAM で失敗要因を可視化
--5, クラスあたりの枚数を増やし誤判定画像を追加収集 → 再学習

目標を達成するために
-1. データ収集 … 構造物の部位や照明条件, 距離を揃えて撮影し1 枚ごとに 1〜5 のラベル を付与させておく。同じ鋼材でも光の当たり方で見え方が変わる場合があるため、昼夜, 曇天, 逆光等といった様々な条件下を想定した撮影をする。
-- → 作業現場でスマホ等で撮影 → PCに画像データを移行すると同時に一括ラベリング(5段階でラベル付け  CSV に filename,label)表計算ソフト+Python で CSV 書き出し

-2. 前処理 … 画像を 224×224〜299×299 にリサイズまたは正規化(0‑1 もしくは ‑1〜1)する。
-- torchvision.transforms (PyTorch をインストールした場合 - 推奨) 
-- tf.keras.preprocessing (TensorFlow をインストールした場合) のどちらかを使用

-3. データ分割 … Train(訓練用) : Val(検証用) : Test(評価用) に用いるデータの比率を 70 : 15 : 15 としその際撮影順ではなくシャッフルして分割するようにする。また撮影した橋ごとに分割すれば「未知の橋写真に強いか」も検証することができるのでは。
-- → sklearn.model_selection.train_test_split を用いて分割させる。

-4. データ拡張 … ランダム左右反転・回転 ±15° 
-- → ColorJitter(彩度・明度)や Cutout or RandomErasing を用いて錆パターンを“水増し”し少ないデータでも CNN が汎化するようにする。始めは torchvision で画像の変換を行い、さらに穴あけやランダムぼかし等といった高度な加工を行いたい場合は Albumentations を使うようにする。
 
-5. モデル … 転移学習が最速 
-- 転移学習とは … 1から CNN を学習させるには何千〜何万枚の画像が必要となるため「一般画像(ImageNet)で事前に学習済みのモデル」を用いて学習時間の短縮を図り、最後の分類部分だけ自分のデータに合わせて学習することが目的の学習である。
-- ResNet‑50 / EfficientNet‑B0 (事前学習済みのCNN)などを ImageNet 重みで初期化し最終層を 5 ユニット(5段階評価)+softmax に置換する。そして学習しないように層を凍結を最後の数ブロックだけ微調整する。
--- → PyTorch Lightning / Keras を使用

-6.  学習 … EarlyStopping & ModelCheckpoint を用いる

-7.  評価 … 従来どおり混同行列を表示し学習は上手くいったのかと判別に弱い等級を確認
-- 以下のツールで判断根拠を可視化	
---① scikit‑learn … Pythonの定番機械学習ライブラリであり、分類, 回帰, クラスタリング(学習データを基に画像をグループに分ける), 評価指標など多数搭載している。深層学習では PyTorch と組み合わせて「評価や前処理」に使うことが多い。

---② torchmetrics … PyTorch 専用の 評価指標ライブラリであり、エポックごとにおける学習中/検証中にF1スコア, 精度, 平均絶対誤差などを自動で計算できることに加えてGPU 対応&バッチ間の統計も管理してくれる。

---③ grad‑cam … CNNが画像のどこを見てその点数であると判断したのかを可視化する手法で、モデルが「赤サビの剥がれ」を見て5点にしたのか?それとも「背景の空」を見て間違ったのか?を確認できる。

- 8. 推論 … 重みを .keras (推奨) で保存
-- CLIツールをPython上で作成して判別させたい画像のラベル(評点)を予測する。 

より高精度・運用向けの改善アイデア
--クラス間で枚数が偏ると “稀な酷い錆(5点)” を取りこぼすので意識的に集めるかクラス重みを掛ける。
-- 回帰併用 … 5 段階を「1〜5 の連続値」として MSELoss も同時に学習(マルチタスク)。誤差 ±0.5 を許容した評価がしやすい。
-- アンサンブル学習 … EfficientNet + ConvNeXt など 2–3 種を学習。また混同行列で間違えやすい写真だけ再収集/追加学習。


***5月第4週(5/22〜5/28) 錆画像判別  [#j417c521]
-Animal-2.pyを実行して結果が出力されたので下の表に示す。

,評価項目 ,判別率(学習回数1回) ,判別率(学習回数60回)
,airplane   ,74.20% ,86.80%
,automobile ,77.90% ,83.40%
,bird       ,43.00% ,80.00%
,cat        ,55.10% ,62.80%
,deer       ,67.60% ,85.30%
,dog        ,45.90% ,76.90%
,frog       ,66.40% ,89.40%
,horse      ,74.90% ,88.80%
,ship       ,76.00% ,92.50%
,truck      ,78.70% ,92.00%
,平均       ,65.97% ,83.79%

自作したスクリプトが正常に動作するかどうかを調べたかったため、学習させる層を1層として実行させた。問題なく実行できた後は学習回数を60回にしてみてどのくらい精度が変化するのか見た。全10項目において判別精度は平均して凡そ18ポイント上昇したが、特に bird, cat, dog はもう少し精度を上げることはできないだろうか。また21日の午後は耐候性鋼材の講習会に参加した。

学習させたデータをもとに cifar10 上にあるテスト用画像を各項目100枚ずつ無作為に抽出してどのくらいの精度が得られるか検証した。以下はエポック数(= 学習回数)を変えながら行った項目ごとの判別率と判別に用いた画像である(抜粋)。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/1.png,163x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/2.png,163x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/3.png,163x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/4.png,163x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/5.png,163x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/6.png,163x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/7.png,163x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/8.png,163x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/9.png,163x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250522/10.png,163x);


,評価項目 ,判別率(学習回数1回) ,判別率(学習回数10回) ,判別率(学習回数30回) ,判別率(学習回数60回)
,airplane   ,73.00 %  ,100.00 % ,100.00 % ,88.20 %
,automobile ,84.00 %  ,100.00 % ,100.00 % ,74.50 %
,bird       ,53.00 %  ,100.00 % ,100.00 % ,76.50 %
,cat        ,68.10 %  ,100.00 % ,100.00 % ,58.90 %
,deer       ,71.60 %  ,100.00 % ,100.00 % ,86.70 %
,dog        ,44.00 %  ,100.00 % ,99.00 % ,75.80 %
,frog       ,69.00 %  ,100.00 % ,100.00 % ,91.10 %
,horse      ,79.00 %  ,100.00 % ,100.00 % ,90.10 %
,ship       ,83.00 %  ,100.00 % ,100.00 % ,93.40 %
,truck      ,89.70 %  ,100.00 % ,100.00 % ,91.10 %
,平均       ,71.30%   ,100.00 % ,99.90% ,82.63 %

5/23現在エポック数を60にして判別した結果は記載していないが、エポック数が10でもかなり良い精度が出ているのでこれから学習をさせるには10とか12で良さそう。


-午前中に高専の中島さん達と今後の動きについて話し合った。耐候性鋼材の調査は主に県内になりそうだが、もしかしたら庄内・最上にも行く可能性あり。
-東北各地から錆のデータが集まるまでまだ時間がかかりそうなので、それまでは過去の先輩方が行っていた耐候性鋼材の研究データから錆の画像を拝借してそれを機械学習させる。
--はじめに目視で評点を画像に入力するなどした後に機械がどう評価をするのかを確かめる。


錆の一例

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250526/1.jpg,400x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250526/2.jpg,400x);


過去の先輩が残してくださったセロハン試験の画像データがあったので、400ピクセル四方に切り取って学習させてみる。
以下の画像はUMAPという高次元データを低次元空間に圧縮し、データの構造や関係性を可視化するために用いられる次元削減手法を用いて錆の画像にパターンがあるかどうかを示したものである。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250527-1.png,1000x);

この図から分かること
-1. 似た錆画像は近くに配置されている
--UMAPは元の画像の見た目の特徴の類似度を保持したまま、2次元(ある特徴の方向性 = 模様・粒子の粗さ・色味など)に圧縮しているため、図の中で近くに並んでいる点同士は錆の見た目(模様・粒子の粗さ・色味など)が似ていることを意味する。

-2. KMeansクラスタリングにより錆画像が5つのグループに分かれた
--色が異なる5つの「クラスタ (= 点)」は特徴的に似た錆画像同士が集まっているグループで、例えば緑色のクラスタは比較的上の方に固まっており同様に他のクラスタも明確に分かれている。茶色や青のクラスタは左側に密集していてそれぞれ緑色とは異なる特徴を持っている可能性がある。これは、「肉眼で似ている錆画像」がグループ化された結果であり以下のような傾向を含んでいる可能性がある。

--- 微細な粉状の錆
--- 鱗片状に剥がれた錆
--- 密集した粗い錆
--- 色調の異なる錆(赤錆、黒錆など)

-3. UMAPと2つの軸(UMAP-1とUMAP-2)の意味
--UMAP(Uniform Manifold Approximation and Projection)とはレーダーチャートのような高次元のデータを2次元等の低次元のデータに変換することを指し、データ間の関係性を分かりやすく可視化することが可能である。変換前においてある程度データが似ていたものは実際に変換後でも2次元平面内では距離的に近くなる(同じ位置に集まりやすい)ため実際に2つのデータが近い値を取っている事を視覚的に捉えることが出来る。逆に2つのデータが似ていない(別々の特徴を持っている)場合は2次元に変換しても互いに離れた位置に点がプロットされる。

--両軸は「人間が理解できる物理的意味」は持っておらずただし「見た目の特徴の次元」を2軸に圧縮したものなので、相対的な位置関係には意味がある。 ⇒   X軸・Y軸が近い点ほど見た目が似ている

 UMAPは錆の画像に評点をつけてから使うこととする。評点をつけ終えたらクラスタ別にフォルダーを作成してどのような分布になっているのかを確かめる。

5/29 やること
-評点を判別させるスクリプトの構築

**6月 [#j417c600]

***6月第1週(5/29〜6/4) [#j417c529]
昨日UMAPによって分類された錆の画像からグループ毎に10枚 合計50枚の画像に評点を書き加えた。判断方法は画像と錆の見本写真・サンプルを見比べたため正確にできているかどうか怪しいが、とりあえず作成したスクリプトが実行されるかどうかを試したいため正確さは後回し。評点ごとの枚数は以下の通り

,評点 ,枚数
,1 ,6枚
,2 ,12枚
,3 ,15枚
,4 ,9枚
,5 ,8枚

-学習1回目
--錆の画像の一部にラベルをつけて機械学習させた後その結果からラベル付けしていない画像を評点1~5のフォルダーに保存するようスクリプトを作成した。以下に評点ごとに分類した画像をUMAPできれいにグループ分けできたかどうかを示す。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250529/1.png,1000x);

---紫色 … 評点1   紺色 … 評点2   翠玉色 … 評点3   緑色 … 評点4   黄色 … 評点5

--評点2と3はある程度グループとして固まっているもののラベル付けに精彩を欠いてしまったのかどの評点も散らばってしまっている。これから評点ごとに保存された画像から再度ラベル付けを行って上記に示したUMAP図がどのくらい変化するのか見ていく。理想はここから2枚上の画像

-学習2回目

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250529/2.png,1000x);

---紫色 … 評点1   紺色 … 評点2   翠玉色 … 評点3   緑色 … 評点4   黄色 … 評点5

--1回目と比較してバラつきは少なくなったものの左上に固まっているグループをもっときれいに色分けしたい。


-明日やること
--学習の繰り返し

-3回目と4回目

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250529/3.png,800x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250529/4.png,800x);



評点付きの錆画像データを見つけたのでこれをUMAPを用いて分類した結果を以下に示す。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/1.png,1000x);

-1. 点(クラスタ)の存在
--縦軸と横軸にそれぞれ注目するとUMAP-1(縦軸)で見ると広く分布しており、UMAP-2(縦軸) で見た場合は低い領域(0〜5周辺)に点が密集していることが見て取れる。
--画像の中央から左側は縦方向に伸びていることが確認できる。→ 様々な特徴量を考慮した結果縦に展開された可能性
--左上には4~5の評点が密に集まる領域があり比較的状態が良好な錆画像はそのあたりに分類される。

-2. UMAPとスコア(≠評点)について
スコアは完全分離しているわけではないが、スコアごとにある程度のパターンが見られる。
--スコア1 : 縦軸に注目すると殆どの点がUMAP2の1〜5に集中している。
--スコア2 : UMAP2の -1〜3に集中している。
--スコア3, 4 : どちらも画像の左側に密集しているもののスコア3の点が真ん中に多くある2方向のUMAPはそれぞれ何を基に分類を行っているか確認してみた。以下の画像群はUMAPと aspect_ratio,  sobel_x,  sobel_y,  orientation_std,  num_blobs,  roughness との間に相関関係があるかどうかを表したものである。右側がUMAP1, 左側がUMAP2となっている。一方でスコア4の点はその外側(上下)に分散してある。
--スコア5 : スコア4と同様な分布となっているが左上にかなり密集している箇所が見受けられる。

2方向のUMAPはそれぞれ何を基に分類を行っているか確認してみた。以下の画像群はUMAPと aspect_ratio, sobel_x, sobel_y, orientation_std, num_blobs, roughness との間に相関関係があるかどうかを表したものである。左側がUMAP1, 右側がUMAP2となっている。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/11.png,800x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/21.png,800x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/12.png,800x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/22.png,800x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/13.png,800x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/23.png,800x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/14.png,800x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/24.png,800x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/15.png,800x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/25.png,800x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/16.png,800x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250603/26.png,800x);

-錆画像から抽出した各特徴量の意味
, 特徴量名 , 説明 , 数字の大小で何が分かるか 
, aspect_ratio , 縦方向エッジ量 ÷ 横方向エッジ量(= Sobel Y / Sobel X) , 値が大きいほど縦方向模様が強い(縦筋・縦のび)傾向にある 
, sobel_x , 横方向(左右)の輪郭強度(エッジ) , 値が大きいほど横縞や横模様が多い 
, sobel_y , 縦方向(上下)の輪郭強度(エッジ) , 値が大きいほど縦縞や縦模様が多い 
, orientation_std , 画像内の輪郭や模様の方向性のばらつき(標準偏差) , 値が小さいほど方向が揃っており、値が大きいほどランダム・雑多な模様 
, num_blobs , 二値化画像から検出された「斑点」や「模様のかたまり」の数 , 多いと細かい斑点が密集しており、少ないと大きな斑模様 
, roughness , 周辺の濃淡変化の激しさや表面のざらつき度(局所コントラストなどで算出) , 値が大きいと表面が粗くざらついた錆であり、小さいと滑らかで均一 

-UMAP1は sobel_x, sobel_y, roughness,  に強い負の相関が見られ「輪郭の強さ(エッジの濃さ)」と「粗さ」によって分類がされているのではないか。一方でUMAP2は orientation_std に正の相関が見られ「模様の方向性のバラつき(雑さ)」に影響されているのではないか。

-よってUMAP1の場合横・縦方向のエッジ(sobel_x・sobel_y)が強くなることに加えて表面がざらざらしている(roughness)とUMAP1は小さくなる。 → 全体として、エッジが強くざらついた錆は UMAP1 が小さく、方向性が乱れた模様では UMAP1 が大きい傾向にある。
-UMAP2の場合は模様の方向(orientation_std)がバラバラだとUMAP2が大きくなる。 → UMAP2は主に「方向性のバラつき」と「表面の粗さ」によって決まっている。

--UMAPの座標が意味するもの
, 軸 , 表すものの傾向 
, UMAP1 , 主に「模様の強さ(エッジ強度)」+「表面の粗さ(roughness)」 
, UMAP2 , 主に「模様の方向性のバラバラ度合い(orientation_std)」 

このことから、UMAPプロット上で:

--左下あたりにいる画像:鋭いエッジがあり、ざらざらした錆(典型的な斑模様)
--右上あたりにいる画像:模様がバラバラ・薄く滑らかな錆(方向性のない軽度な腐食)

といった分布傾向が読み取れる。


1回の学習ごとに手動で評点を入力してはの繰り返しだと終わりが見えない&精度を上げるのに限界を感じたため別のアプローチで判別することを試みた。

評点付きの錆画像は何枚か同じ画像が存在したため重複していたものを削除したら全74枚あった錆画像が24枚(評点1から順番に 3 , 7 , 6 , 6 , 2枚)となってしまった。この枚数で機械学習させるには無理があるため、評点付きの錆画像に様々な処理を加えることで機械学習させる画像の枚数を増やしていくことにした。以下に具体的な処理とその内容について表形式で示しておく。

, 処理 , 内容 
, 画像の反転 , ただ画像を鏡写しにするだけ 
, 画像の回転 , ランダムで ±10°~±30°、±90°、±180°のいずれかを行なう 
, 類似画像の削除 , 画像処理分野で画像の類似度を数値化する際に用いられるSSIMを使用 

 MMLI (=More Machine Learning Images).py … 既存の画像を反転・回転等で学習させる画像枚数を増やすスクリプトのこと。

 Hyoutenhuriwake+UMAP2.py … ラベル付きの画像を学習させた結果をもとに画像の評点をつける。 あとUMAP画像の表示


上記のPythonスクリプトを実行させると24枚しかなかった画像を970枚近くまで増やすことができた。(SSIM=0.99のとき)このデータをUMAPを用いてグループ分けすると以下のようになった。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250604-1.png,1000x);

以前よりもクラスタリング精度が向上したのではないか。

-1. 評点ごとの分離性
--この画像ではグループ5(黄色)が密集しており、他の評点と明確に分離されている。
--以前のUMAP図では評点ごとの混在が見られたのに対し今回はグループ1〜5が比較的分離かつ密集していると考えられ、UMAPの特徴抽出がより一致している可能性あり。

-2. 色の連続性
--カラーバーは評点1〜5の連続スケールとなっているが評点をつける関係上5段階スケールとなってしまっている。例としてUMAP上ではグループ2とグループ3が混在している箇所があるが、これは連続スケールに書き換えるとグラデーションが連続的に変化している部分であると考える。 → モデルが評点を「離散的なラベル」としてではなく「連続的なラベル」として学習できていることを示している。

-3. まとめ
--このUMAP可視化では以前よりも評点ごとのクラスタリングが明確になっており、モデル精度の向上が確認できた。特に評点5におけるクラス分離は顕著でありCNN+UMAPによる分類・可視化の成功例と言って良いのではないか。


-次回やること : CNN×スクリプトの作成
--augmented_imagesをCNNで機械学習させる (3層×3回 や 3通りの学習方法の構築 その結果は.kerasファイルで保存されるのが理想)
--評点付き教師画像はラベルの先頭が評点となっている。 例) 1-1_flipped_rot6_(-11°).png → 先頭の1が評点を表しているので、この場合は評点1のpng画像であると分かる。
--上記の学習結果を基に判別させたい画像(Fuji2として保存) に1点から5点の計5段階で評点をつけたい。またその判別した結果を評点ごとにフォルダーとして合計5つ保存したい。
--また判別した結果を評点別にUMAP + KMeans で可視化 & 伝播してほしい。UMAPは画像として保存。
--画像における2方向(x, y軸)のUMAPはそれぞれ何を基に分類を行っているか確認してほしい。UMAPごとに aspect_ratio, sobel_x, sobel_y, orientation_std, num_blobs, roughness との間に相関関係があるかどうかを確かめてもらいたい。画像は1つのフォルダーにまとめて保存してほしく、1つの画像でUMAPと何かしらの要素1つとすること。

***6月第2週(6/5〜6/11) [#j417c606]
-CNNを用いて錆画像を判別させるスクリプトを作成中


 Main.py ーー軽量化ー> Main2.py
-とりあえず評点付き錆画像を3タイプのCNNで判別させた学習結果をそれぞれ .keras ファイルとして保存する。さらに評点を付けたい画像が複数枚保存されているフォルダーごとその学習結果に基づいて評点を付け、点数ごとにフォルダーを作成して保存されるようなスクリプトを作成した。(Main2.py)

3パターンのCNNは以下の通り

 # === CNN構造定義(バリエーションを3通り用意) ===
 def create_model(version):
     model = models.Sequential()                                                  # 順次積み重ねるタイプのモデル(Sequential)を作成。
     model.add(layers.Input(shape=(*IMAGE_SIZE, 3)))                              #入力層を定義。IMAGE_SIZE = (64, 64) なら、入力は 64x64x3 のカラー画像(RGB)。
 
     if version == 0: (パターン1)
         model.add(layers.Conv2D(64, (3,3), activation='relu'))                   #64個の3×3フィルターで特徴抽出。ReLUで非線形性を付加。
         model.add(layers.BatchNormalization())                                   #出力を正規化し、学習の安定化と高速化を図る。
         model.add(layers.Conv2D(64, (3,3), activation='relu'))                   #さらに抽出を深める。
         model.add(layers.MaxPooling2D((2,2)))                                    #空間サイズを半分に圧縮(特徴量の要約+計算コスト削減)
         model.add(layers.Dropout(0.3))                                           #学習時にランダムに30%のノードを無効化(過学習防止)
         model.add(layers.Conv2D(128, (3,3), activation='relu'))
         model.add(layers.BatchNormalization())
         model.add(layers.Conv2D(128, (3,3), activation='relu'))                  #フィルター数を128に増やして、抽出する特徴の数を増加 (サイズは減っているが、意味的に濃い特徴を取る)

     elif version == 1: (パターン2)
         model.add(layers.Conv2D(64, (5,5), activation='relu'))                   #5×5の大きめフィルターで広い範囲の特徴を捉える
         model.add(layers.BatchNormalization())
         model.add(layers.MaxPooling2D((2,2)))
         model.add(layers.Dropout(0.3))                                           #正規化 → 縮小 → Dropout(典型的な畳み込みブロック)
         model.add(layers.Conv2D(128, (3,3), activation='relu'))
         model.add(layers.BatchNormalization())
         model.add(layers.Conv2D(128, (3,3), activation='relu'))
         model.add(layers.MaxPooling2D((2,2)))
         model.add(layers.Dropout(0.3))                                           #より深い特徴を抽出し、再び圧縮。Dropoutを通して過学習に強い深層CNNに。

     elif version == 2: (パターン3)
         model.add(layers.Conv2D(64, (3,3), activation='relu'))
         model.add(layers.Conv2D(64, (3,3), activation='relu'))                   #同じフィルター数で畳み込みを2回繰り返すことで、非線形表現力を高める
         model.add(layers.BatchNormalization())
         model.add(layers.MaxPooling2D((2,2)))
         model.add(layers.Dropout(0.3))                                           #出力を安定させて縮小し、汎化能力を保つ。
         model.add(layers.Conv2D(128, (3,3), activation='relu'))
         model.add(layers.Conv2D(128, (3,3), activation='relu'))
         model.add(layers.BatchNormalization())                                   #より抽象度の高い情報を取り出す2段構成。
 
     model.add(layers.GlobalAveragePooling2D())                                   #各チャンネルの特徴マップを平均化して、全結合層の前処理を簡潔に行う。Flattenより軽量。
     model.add(layers.Dense(128, activation='relu'))                              #最終的な判断のための全結合層
     model.add(layers.Dropout(0.5))                                               #強めのDropoutで過学習防止
     model.add(layers.Dense(NUM_CLASSES, activation='softmax'))                   #クラス数(評点1〜5)に対応した出力層。確率で予測を出す。
 
     model.compile(optimizer='adam',                                              #Adam:最もよく使われる最適化手法
                   loss='sparse_categorical_crossentropy',                        #sparse_categorical_crossentropy:ラベルが数値 (0〜4) のときの多クラス分類用損失関数
                   metrics=['accuracy'])                                          #accuracy:精度(正答率)を指標として追跡
 
     return model
 
-上記のCNNで学習させると特に評点1と評点2, 評点5の判別が上手く行かない。
-評点2には人間の目で評点1と捉えられてもおかしくない画像が混じっていたり、評点5に保存されている画像が1枚しかなかったりと判別精度の面からまだまだ不十分である。錆の判断方法について一度確認してみる必要がありそうだ。
--評点2に保存されていた評点1だと思われる画像(評点2の錆画像である可能性も0では無いがどこで線引きするか難しい)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250608/1.jpg,270x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250608/2.jpg,270x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250608/3.jpg,270x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250608/4.jpg,270x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250608/5.jpg,270x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250608/6.jpg,270x);


従来はラベルの付いた画像をそのまま学習させていたが、判別が上手く行かないため何か特徴量で分類したほうが良いのではないかと考えた。

 RSRC.py (rust_score_regression_cluster.py)

→ 錆画像にラベルをつける際に判断材料となるであろう錆の粒径, 色の濃さ, 錆が画像に占める割合, 錆の数, 画像内にある1番大きな錆と1番小さな錆の面積差の計5つの項目について、ある特徴量(前に示した5つの判断材料とは異なる)によって5グループに分けられたクラスタ(縦軸)と、実際の評点(横軸)との対応関係を可視化するスクリプト。以下に結果(各クラスタは評点1~5の画像でそれぞれどのくらい構成されているか)を示す。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250609/1.png,810x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250609/2.png,810x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250609/3.png,810x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250609/4.png,810x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250609/5.png,810x);

-1, Mennsekisa:錆の大きさのバラつき(面積差)
クラスタ2が評点2と完全一致しており非常に良い識別精度といえる。他クラスタはスコアがバラけているものの各クラスタごとに特徴が見受けられ(0は評点2〜4, 1は評点2と3, 3は評点3と4, 4は評点1と2)、クラスタ0を除けば中程度の分類力を持っている。

-2, Color_kosa:色の濃さ(グレースケール)
クラスタ2が評点2と完全一致している。クラスタ0と4も比較的明確な傾向を示しているがクラスタ1と3はバラけてしまっている。 評点2や4を分離するのに非常に有効な方法である。

-3, Mensekihi:錆面積比
錆を黒, それ以外の部分を白と区別させて面積比を求めた。クラスタ2が評点2に0.99、クラスタ4が評点2に0.66と集中。しかしクラスタ0, 1はすべての評点が分散しておりクラスタ3は真っ二つに分かれている。

-4, Sabi_kazu:錆の数
どのクラスタも比較的均等に分散しているため、錆の数と錆の面積差では評点の識別に弱くCNNに組み込んで学習には適していない。

-5, Ryukei:錆の大きさ・粒径
クラスタ2が評点2に1.00、クラスタ3が評点2に0.96  評点2, 3が分離できているが他のスコアがランダムに分布 → 評点2, 3以外では効果が限定的

以上よりクラスタ2のように明確に分類できているものもあったが全体的に見ると少数派で、クラスタが様々な評点で構成されている方が多くやはり錆に現れる特徴でラベル分けを行なうのは現時点で難しいことが分かった。正確かつ膨大なデータ数があれば別だと思うが。


-特徴量付き CNN(ハイブリッドモデル)
--画像はCNNで処理しつつ、錆の形状・数などの特徴量を別入力としてモデルに加える。
--画像から抽出される“感性的”なパターンと、手動で抽出した“定量的”な情報を両方使う。

--CNN単体だと分類が不安定(特に評点1と2)
--面積・数・濃淡などの情報が評価に明らかに効いている場合

 rating_cnntrainer.py

上記のスクリプトで処理していることについて

, 項目 , 説明 
, 画像入力 , CNNで64×64の画像から特徴を抽出 
, 数値特徴量入力 , .npyファイルから読み込まれた錆特徴量(以下の表に記載) 
, ハイブリッド構造 , CNN特徴と手作り特徴を Concatenateで融合 
, モデルバージョン , 3種類のCNN構造を用意したアンサンブル 学習
, 予測 , 3モデルの出力平均で最終スコアを判定 1〜5の評点に分類して保存 



, 錆特徴量名 , 内容説明 , 特徴量例 
, rust_ratio , 錆の占有面積(全体に対する割合) , 平均輝度、標準偏差、2値化による錆割合 
, num_blobs , 錆のかたまり数(connected components) , 形状(個数) 
, np.mean(gray) , グレースケールの平均(明るさ) , GLCM(テクスチャ)	コントラスト・相関・エネルギー・同質性 
, np.std(gray) , グレースケールの標準偏差(濃淡の変動) , GLCM(テクスチャ)	コントラスト・相関・エネルギー・同質性 
, np.mean(sobel_edges) , エッジ強度(輪郭の激しさ) , Sobelエッジの平均 
, area_diff , 最大面積 - 最小面積の錆領域の差 , 形状(面積差) 


ラベル付き画像 (947枚) を学習させてラベルを付けていない画像(665枚)を分類させた結果 Score1から順に10, 73, 538, 1, 43枚となった。Score4はたった1枚しか保存されておらずしかもその1枚が評点1相当の殆ど錆の画像であった。
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250609/6.jpg,100x);


 CNN+binning.py

-CNN回帰モデル + binning
--評点(1〜5)を離散ラベルではなく連続的な“点数”と捉えて学習させて予測後に5段階に「bin(分割)」して最終的な評点を決める。
--各評点の境界が曖昧で連続的なスコアで表現した方が自然
--評価者の意識が「点数感覚」に近い(例:2.3点 ≒ 評点2)

-構成内容
--1. 目的
画像を入力しCNNで連続的なスコア(評点1.0〜5.0の0.1点刻み)を予測。
そのスコアをbinning(1点〜5点)に変換して評点分類として扱う

--2. モデル構成(従来のCNN分類との違い)
, 項目 , CNN分類 , CNN回帰 + binning 
, 出力層 , Dense(NUM_CLASSES. activation='softmax') , Dense(1)(活性化なし or ReLU) 
, 損失関数 , sparse_categorical_crossentropy , mean_squared_error (MSE) 
, 出力形式 , クラス確率(0〜1) , 連続値スカラー(例:3.72) 
, 評点の扱い , クラス分類(0〜4) , binning により 1〜5 に変換 

---出力層・形式 , 評点の扱い

今までのCNNは NUM_CLASSES(出力次元数 = 何クラスに分類するか) , softmax(確率的なベクトル出力をする際に用いられる関数) を用いて例えば錆の画像を評点1~5点で付けたい場合
 model.add(Dense(5, activation='softmax')) → この場合NUM_CLASSES = 5
とスクリプトに入力すれば [0.1, 0.1, 0.7, 0.05, 0.05] のようなクラス(評点)ごとの確率(配列)で出力され、 argmax関数を用いてその配列の中で最大値のインデックスを返す。この配列の場合だと最大値は0.7になるので評点は3となる。このように錆画像の評点を1, 2, 3, 4, 5点と他クラスとの相対的な区別を学習するため人によって判断基準が異なる場合はこの分類方法はあまり適さない場合がある。一方でCNN回帰 + binningでは
 model.add(Dense(1))
と入力すれば錆画像の評点は3.27点というように連続スカラー値で出力される。評点の決め方はラベル付きデータをCNNで学習し画像のパターン(色、濃淡、エッジ、形など)から点数を最適化。3.27という値は"学習データの中で、最も似ている例が3点と4点の中間"であることを意味する。これを binning 関数で1点刻みの評点に変換する。その変換方法は2.5点から3.5点の間に出力された数値は評点3というような感じで評点±0.5「評点のスケール」を意識して学習する。

---損失関数
分類の場合
 loss='sparse_categorical_crossentropy'
で正解クラスだけ1、それ以外0のone-hotラベル (異なるグループに分けられたデータを数値として表現するための手法) を元に確率分布を学習。

回帰の場合
 loss='mean_squared_error' 
で予測値 3.27 とラベル 3.0 との差 0.27 を最小化し「正解と近いスコアなら許容」という発想になる。

--3. 学習データ(ラベル)の準備
.png などの画像を  Image.open → resize → 正規化 の順に変換した後、ラベル(fname[0])を int で読み取り、float型のスカラーにする。 [ 例:"3_xyz.png" → label = 3.0 ]

--4. binning(スコア→評点)
学習後の推論時に以下のような関数で連続値を離散化する (一例)

, def binning(score): 
, if score < 1.5: return 1 
, elif score < 2.5: return 2 
, elif score < 3.5: return 3 
, elif score < 4.5: return 4 
, else: return 5 

--5. 評価方法

学習時は回帰(MSE = 平均二乗誤差)で最適化されるが、推論後は binned 値に変換して accuracy_score (予測結果の正確さ) や confusion matrix (混同行列の可視化) を使って評価

 from sklearn.metrics import accuracy_score, confusion_matrix

 y_pred = model.predict(x_val).flatten()
 y_binned = [binning(p) for p in y_pred]
 acc = accuracy_score(y_val, y_binned)

--6. CNN回帰モデル + binning に追加で工夫できること
工夫	内容
出力に ReLU + offset	出力が1〜5に制限されるようにする(例:Dense(1) → ReLU() → +1)
Huber loss に変更	MSEよりも外れ値に強く安定
binning間隔を柔軟化	ヒストグラムを見て等間隔でない分割にも対応可


                       CNN Backbone
             ┌───────────┐
             │ Conv2D → Pool → ...│
             └─────┬─────┘
                     ▼
             ┌───────────┐
             │ Flatten → Dense()   │
             └─────┬─────┘
                        ▼
         分類モデル            回帰モデル
      ┌─────────┐      ┌─────┐
      │ Dense(5, softmax)│      │ Dense(1) │
      └─────────┘       └─────┘
             ↓                     ↓
              argmax           float値 → binning → int


***6月第3週(6/12〜6/18) [#j417c612]
これまでの結果から錆画像を識別するのはCNN回帰 + binning が精度の観点から1番可能性がありそうなので現段階ではこのモデルを軸に画像分類を進めていく。行き詰まってしまう, これよりももっと優れたモデルが見つかれば乗り換える可能性あり。

現時点(6/12現在)で使用している錆画像フォルダーとPythonスクリプト
-錆画像フォルダー
--1, Fuji2 … ラベル付けを行っておらず学習結果を基にどのくらいの精度で評点1~5に分類できるのかを試すためのフォルダー
--2, train_images … ラベル付けされた画像のあるフォルダー
--3, augmented_images … train_imagesに入っているラベル付き画像は全評点合わせて24枚しかなくこの状態で機械学習させるにはデータが少なすぎるため MMLI.py を用いて画像を増やしたものが保存されているフォルダー
--4, Kekka_Reg … 下記の CNN + binning.py を実行した後に出力される評点別に分類されたフォルダー

-Pythonスクリプト
--1, MMLI.py …  train_images 内に保存されている画像に指定サイズでの切り出し, 左右反転や回転(任意の角度で設定可能)を加えて枚数を水増しするスクリプト。1枚あたり水増しする画像を設定することが可能であり似通った画像は自動的に削除してくれるシステム付き。例えば下の1番左側に示した画像を左右反転して時計回りに34°の回転を加えようとするとその右側に示した画像のように余白が表れてしまい、錆の判定・学習に影響を及ぼす可能性がある。画像を回転する以上どうしても余白が表れてしまうのでその部分には画像をさらに反転(シンメトリー 赤の線を軸に)させたものを付け加えることで錆の判定・学習に影響を及ぼすことなく画像を増やしている。

                     &ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/1.png,1080x); 

--2, CNN+binning.py … この学習で核となるスクリプトであり詳しくは6/11に記載。

回帰スコアから評点の分類を行っていた。スクリプトのデフォルトでは回帰スコア1.00〜1.50までを評点1, 1.51〜2.50までを評点2, 2.51〜3.50までを評点3, 3.51〜4.50までを評点4, 4.51〜5.00までを評点5としていたが、それでは上手いこと分類ができなかったので目視で錆画像を見ながら0.01点単位で評点の範囲を決めていた。その結果以下の表のように評点の分類を行なうこととした。またCNNの層を厚くしても精度は変わらないことが分かった。

, 評点 , 回帰スコア 
, 1 , 2.30点以下 
, 2 , 2.31点以上3.00点以下 
, 3 , 3.01点以上3.40点以下 
, 4 , 3.41点以上3.64点以下 
, 5 , 3.65点以上 

以下回帰スコア別に保存された錆画像を貼っておく
-評点1
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/11.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/12.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/13.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/14.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/15.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/16.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/17.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/18.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/19.jpg,150x);

-評点2
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/21.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/22.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/23.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/24.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/25.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/26.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/27.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/28.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/29.jpg,150x);

-評点3
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/31.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/32.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/33.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/34.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/35.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/36.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/37.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/38.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/39.jpg,150x);

-評点4
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/41.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/42.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/43.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/44.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/45.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/46.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/47.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/48.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/49.jpg,150x);

-評点5
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/51.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/52.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/53.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/54.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/55.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/56.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/57.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/58.jpg,150x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250612/59.jpg,150x);

-来週の外国語文献までに原稿と発表スライドを書く。


-CNN+binning.py内のアンサンブル学習に少し手を加えた
  CNN+binning3.py


CNN+binning.py内のアンサンブル学習についての変更点(CNN+binning3.pyについて)
-CNNモデルの変更, 今までは名前のない簡単な構造の自作CNNモデルを構築して学習ごとに層の厚さ, 全結合層のユニット数を変えながら3通りのモデルを作成していた。これからは3種類のCNNモデルを用いて学習させることにする。

--1, EfficientNetV2S... 高精度なのに学習が早い・モデルが軽い。錆の「濃さ」「粒度」「面積」など多様な特徴 に対応できる階層性を持つ。
--2, DenseNet121... ResNetよりモデルは軽く錆やひび割れと言った微細な特徴の抽出に強い一方で学習速度が遅い。
--3, MobileNetV3Large... 特徴の抽出力はそこまで高くないもののモデルサイズは軽く学習速度は非常に高速。

 def create_model(version):                                                                       #モデルを作成する関数の定義。引数 version によって使用するベースモデル(CNN)が変わる。
     input_img = Input(shape=(*IMAGE_SIZE, 3))                                                    #入力層の定義 IMAGE_SIZE は画像サイズ(例:224×224)を意味し、3 はRGB画像を意味する。
     if version == 0:  # EfficientNetV2S                                                          #version == 0 のとき軽量・高精度な EfficientNetV2S をベースに採用。
         base = EfficientNetV2S(include_top=False, weights="imagenet", input_tensor=input_img)    #include_top=False → 最終の分類層は除外。
                                                                                                  #weights="imagenet" → ImageNetで事前学習された重みを使用。
                                                                                                  #input_tensor=input_img → 上で定義した入力をモデルに接続。
    elif version == 1:                                                                            #version == 1 のとき DenseNet121 をベースに採用。
        base = DenseNet121(include_top=False, weights="imagenet", input_tensor=input_img)
    elif version == 2:                                                                            #version == 2 のとき MobileNetV3Large をベースに採用。
        base = MobileNetV3Large(include_top=False, weights="imagenet", input_tensor=input_img)
     base.trainable = False                                                                       #転移学習の初期段階ではベースモデルの重みを凍結(学習しない)。
     x = base.output                                                                              #ベースモデルの出力を取り出す(特徴マップ)。
     x = layers.GlobalAveragePooling2D()(x)                                                       #2次元の特徴マップから平均値を取って1次元に圧縮。Flattenより計算効率が良く過学習を起こしにくい。
     x = layers.Dense(512, activation='relu')(x)                                                  #512ユニットの全結合層。非線形変換(ReLU)で特徴を抽出。
     x = layers.BatchNormalization()(x)                                                           #バッチごとに入力を標準化し学習安定化(精度や速度向上)。
     x = layers.Dropout(0.5)(x)                                                                   #ニューロンを50%の確率で無効化し、過学習を防ぐ。
     x = layers.Dense(256, activation='relu')(x)                                                  #次の中間層  ユニット数を減らしながら段階的に抽象化しDropout率は少し下げている。
     x = layers.BatchNormalization()(x)
     x = layers.Dropout(0.3)(x)
     x = layers.Dense(128, activation='relu')(x)                                                  #より小さな次元にして最終出力に近づける。Dropoutは軽めにして出力を安定させる。
     x = layers.Dropout(0.2)(x)
     output = layers.Dense(1)(x)                                                                  #出力層:1つの連続値(回帰値)を出力し評点を推定する回帰タスクに対応。
     model = Model(inputs=input_img, outputs=output)                                              #全てをまとめて Keras の Model として定義。
     model.compile(optimizer='adam', loss=custom_penalizing_loss, metrics=['mae'])                #Adam最適化を使用(自動調整が得意)。
                                                                                                  #損失関数は custom_penalizing_loss(大きな誤差を強くペナルティ)。
                                                                                                  #評価指標として mae(平均絶対誤差) も計算。
     return model                                                                                 #構築したモデルを呼び出し元へ返す。


明日やること
-CNN+binning3.pyの回帰スコア調節

***6月第4週(6/19〜6/25) [#j417c619]
CNN+binning2.py と CNN+binning3.py の比較
, 諸条件 , CNN+binning2.py , CNN+binning3.py 
, CNNモデル , 自作CNNモデル3パターン , EfficientNetV2S、DenseNet121、MobileNetV3Large の3種類 
--2つのPythonスクリプトで異なる部分はCNNモデルぐらいであり、機械学習・評価する画像, 回帰スコア範囲(下に記載), エポック数(学習回数)等の学習条件は統一させた。

-評点と回帰スコアの対応表
, 評点 , 回帰スコア 
, 1 , 0.00点以上2.30点未満 
, 2 , 2.30点以上3.10点未満 
, 3 , 3.10点以上3.40点未満 
, 4 , 3.40点以上3.70点未満 
, 5 , 3.70点以上5.00点未満 

上記の条件でそれぞれ学習させると評点ごとに保存された枚数は以下の表に示す。
-評点ごとに保存された枚数
, 評点 , CNN+binning2.py , CNN+binning3.py 
, 1 , 24 , 13 
, 2 , 110 , 119 
, 3 , 54 , 123 
, 4 , 59 , 146 
, 5 , 418 , 264 

評点が1, 2 の場合は判別に大きな差は見られなかったが、逆に評点3〜5は保存枚数に大きな差が見られCNN+binning2に関しては評点が3であろう錆画像が4や5に点在していたためスクリプトを改良する必要がありそうだ。CNN+binning3は評点5の画像が減ったものの評点3 ,4が混在してしまっている気がする。これらのグループは再度錆の大きさや面積に占める割合で判別するスクリプトを組み込む必要があるかもしれない。


CNN+binning3.pyに

-面積・粒径・Sobelエッジ量などの数値特徴を別途抽出
-CNN出力 + 特徴量ベクトル を結合してDense層に入力     →CNN+binning4.pyとした。

を加えたところ回帰スコアが負の値や2桁になってしまったため、スコアの上振れ or 下振れするのを防ぐ目的で出力された回帰スコアにシグモイド関数をかけて0.00~5.00点以内になるよう調整した所スクリプトの構成が上手く行かない。(=エラーが頻発) また上手くいったとしても回帰スコアが全て同じ値になる。これはシグモイド関数の傾きを緩やかにすれば解決する可能性あり。来週には実行できる状態にさせたい。


CNN+binning4.py にUMAPを画像で出力することに加えて評点も出力されるようにした。ただ評点はシグモイド関数でつけているため出力された評点とどう組み合わせていくかを考えている必要があり、現在模索中。

-シグモイド関数 パラメータ
--SIGMOID_SCALE ...
通常の sigmoid(x) は0〜1の値を出力するがそれを 5.0 * sigmoid(x) にすることで出力スコアが 0.00〜5.00 に正規化される。例えばSIGMOID_SCALEを10.0にすると出力は 0〜10 になる(評価スケールが10段階に)。

--SIGMOID_K ... 
シグモイド関数の傾きを調整する係数でありあいまいな場合小さい値(例:0.1)でゆるやかに変化するシグモイドカーブを作成し評点の境界が明確な場合は大きい値(例:1.5)で S字の中央で急激に変化するカーブを作成する。

--SIGMOID_X0 ... 
シグモイド関数の 中央(変曲点)をどこに置くかを決めるシフト量つまり関数を左右どちらに移動させたいかを調節することが出来る。SIGMOID_X0が大きいとシグモイド全体が右にシフト(高めのスコアを中心に)し、逆に小さくすると左にシフト(低めのスコアが中心に)する。学習後に全体的な予測スコアが低すぎる・高すぎる傾向にある場合微調整する必要がある。


 CNN+binning5.py

CNN+binning5.pyの内容
-1. CNNを用いた錆画像の評点回帰モデルの構築

EfficientNetV2S / DenseNet121 / MobileNetV3Large の3つの深層学習モデルを活用したアンサンブル学習を実装し、出力は 回帰スコア(0~5.0)+ Sigmoid関数 + binning処理(1〜5点に丸める) により、評点分類を実現。

-2. 画像+数値特徴のハイブリッドモデル構築

画像からのCNN出力に加え、以下の 数値特徴量(4種類)を自動抽出してモデルに結合した。CNNベクトルと数値特徴をConcatenate層で結合し、Dense層により回帰スコアを出力。
 -錆面積割合(rust_ratio)
 -粒径(平均・最大)
 -Sobelエッジ量
 -錆の個数

-3. Optunaによるしきい値の自動最適化

特徴量(粒径, Sobel量など)に基づき評点を補正するためのしきい値を自動探索。評価指標 (accuracy_score) を最大化するように調整。

今まではCNNで評点付き錆画像を学習させた後、判別させたい錆画像の回帰スコアを出力し評点を決めていた。
 例) 錆画像の回帰スコアが3.45点であり、評点と回帰スコアの対応が上記に示した表の通りであったとすると錆画像の評点は4となる
例を読んで理解できただろうか。今回はこの判別方法を行った後に画像の錆の数, 大きさ, 面積割合の特徴量(自動的に最適値を探すようにしている)を読み取って最終的な評点を決めるスクリプトを付け加えた。さらにシグモイド関数 パラメータを組み込むことで回帰スコアが0.00〜5.00の間になるよう調節できる。

ただUMAPでの分類はそれなりであるものの評点ごとに保存されている画像の枚数を確認すると評点1から順に 121, 168, 129, 1, 246枚となっており特に評点1と4が極端な枚数になっている。保存されている枚数が極端であるということは評点の付き方または特徴量の付け方に問題があるということなので明日以降に確認してみる。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250624-1.png,1000x);

ちなみにUMAPの理想は同じ色の点が密集しているのが理想である。点が固まっているところに複数色混在しているのは良くない。


***6月第5週(6/26〜7/2) [#j417c626]
評点を1.0~5.0点の間に変換する方法として シグモイド関数, 線形変換, 活性化関数がある。

1, シグモイド関数(+スケーリング)  
シグモイド関数 ''sigmoid(x) = 1 / (1 + e^(-x))'' は、出力を [0,1] の間に制限する関数であり、それに4を掛けて1を加えれば(スケーリング)評点を1.0~5.0点の間に変換できる。

 ・実装方法
 output = Dense(1, activation='sigmoid')(x)             # → [0.0, 1.0]       (シグモイド変換)
 output = Lambda(lambda z: 1.0 + 4.0 * z)(raw_output)   # → [1.0, 5.0]       (スケーリング)
 ※PCやバージョンによっては "Lambda" 関数がインストールされておらずエラーが出てしまう可能性がある。以下はLambdaを使わずに書いたスクリプト
 SIGMOID_SCALE = 5.0  # 出力を0〜5にスケーリング
 SIGMOID_K = 1                # 傾き係数(大きくするとS字が急)
 SIGMOID_X0 = 0               # 切片(出力の中心)
 --------------------------------------------------------------------------------------------------
 output = layers.Dense(1)(x)
 output = ScaledSigmoid(SIGMOID_SCALE, SIGMOID_K, SIGMOID_X0)(x)

メリットとして上記のスクリプトを組み込むだけで1.0~5.0点の間に変換が可能であること (ワンライナー) 、出力が滑らか・連続的であるため学習しやすい事が挙げられる。デメリットは非線形変換のため中央(関数の変曲点=評点3.0付近)に予測が集中しやすく極端なスコア(1点や5点)が出力されにくくなることで分布が歪む可能性がある。また出力分布のコントロールが難しい (傾きや切片を任意で設定できるようなスクリプトを組むのはいいが自身で最適な値を見つけることは困難) 。

2, 線形変換(+出力範囲制限) 
出力層は線形のまま Dense(1) を用いて範囲外の値(~1.0, 5.0~)をあとから切り落とす方法。例えば負の値や5.0を超えるような点が出力されてしまった場合前者は1.0に後者は5.0に丸められる。

 ・実装方法

 output = layers.Dense(1)(x)
 output = layers.Lambda(lambda z: tf.clip_by_value(z, 1.0, 5.0))(output)
 
 ※Lambdaを実装しない場合(一例)

 # === ClipLayer の定義 ===
 @register_keras_serializable()
 class ClipLayer(Layer):
     def __init__(self, min_val=1.0, max_val=5.0, **kwargs):
         super().__init__(**kwargs)
         self.min_val = min_val
         self.max_val = max_val
 
     def call(self, inputs):
         return tf.clip_by_value(inputs, self.min_val, self.max_val)
 
     def get_config(self):
         config = super().get_config()
         config.update({
             'min_val': self.min_val,
             'max_val': self.max_val
         })
         return config
 ---------------------------------------------------------------------------------------------------------
 output = layers.Dense(1)(x)
 output = ClipLayer()(output)

メリットとしてシグモイド関数と同様に実装が簡単であり、ネットワーク出力の自由度が高く極端なスコアにも対応しやすいうえ線形なので分布の偏りが出にくい。デメリットとして範囲外に出た値はclip_by_valueによって強制的に最小or最大点に丸められることで、例えばある錆画像を学習結果をもとに判別した結果6.0と出力されたとき5.0に丸められてしまう。しかし機械学習モデルの予測値と実際の正解値との誤差を数値化する損失関数は "5.0" で計算されるため、「6.0と5.0の差」の情報を勾配として受け取ることで学習が進まないことがある。範囲外に出た値は学習に使わないことも出来るが極端な錆画像を学習する場合はどうなるだろうか。

3, 活性化関数 [tanh(x)] ( + スケーリング)
シグモイド関数と異なる箇所は用いる関数のみで、活性化関数 ''tanh(x)'' は、出力を [-1, 1] の間に制限する関数であり、それに2を掛けて3を加えれば(スケーリング)評点を1.0~5.0点の間に変換できる。

 ・実装方法
 output =  Dense(1, activation='tanh')(x)               # → [-1.0, 1.0]      (シグモイド変換)
 output =  Lambda(lambda z: 3.0 + 2.0 * z)(raw_output)  # → [1.0, 5.0]        (スケーリング)
 ※PCやバージョンによっては "Lambda" 関数がインストールされておらずエラーが出てしまう可能性がある。

メリットとして出力が中央(3.0)に対して対称でありシグモイド関数よりも中心から離れた値を出力しやすい(評点が1や5の場合でも分かりやすい)。デメリットとしてこれはシグモイドにも同じことが言えるがあまりにも極端すぎるものは出力の変化が鈍くなり学習が止まりやすい。

これらをまとめると
, 方法 , 評点の保証 , 出力の自由度 , 実装難易度 , 学習安定性 , おすすめ度 
, 1. Sigmoid + スケーリング , ◎ 常に1~5 , △ 中央に寄る , ◎ 簡単 , ◎ 高い , 4 
, 2. Clipによる制限 , ◯ 制限あり , ◎ 高い , ◎ 簡単 , △ 勾配停止あり , 2 
, 3. Tanh + スケーリング , ◎ 常に1~5 , ◯ 対称性あり , ◯ 普通 , ◯ 中央寄り , 3 
, 制限なし(今まで) , × 保証なし , ◎ 最大自由度 , ◎ 簡単 , × 不安定 , 1 

どれも課題があるもののその中で学習安定性を重視したいと考えてシグモイド関数で評点を出している。ただ線形変換で評点を出して1.0~5.0に収まらなかった画像は学習に使わないというスクリプトも実装してみて精度とかを見てみる。


これから5つのアンサンブル学習(計15のCNNモデル)で精度・時間の比較を行っていく。学習条件(学習させる層, 学習回数など)は統一してある スクリプト(binning+CNN6.py)参照。

画像から評点を出すまでの流れ

1. 前処理・画像とスコアの準備
-指定したフォルダから画像ファイルを読み込み、画像ファイル名の先頭の数字を「元スコア」として取得した後ランダムノイズを加えて「ソフトラベリング」する(例:3.0 → 3.13など)。画像は指定サイズにリサイズし、正規化(0〜1にスケーリング)して配列化。

2. モデル構築・学習
-複数のCNNモデル(アンサンブル)を構築させ各モデルはImageNetの重みを使った転移学習で特徴抽出部は凍結(trainable=False)し、全結合層で回帰出力(1〜5点)を学習。出力値はカスタム層(ClipLayer)で1.0〜5.0にクリップされる。損失関数は「予測値と正解値の差が大きいほど重くペナルティを与える」独自設計である。学習データを訓練・検証に分割し、EarlyStoppingとModelCheckpointで最適モデルを保存。

--この損失関数(カスタム損失関数)では予測値と正解値の絶対誤差(diff)をまず計算し、その二乗(tf.square(diff))にさらに「1 + diffの1.5乗」(1.0 + tf.pow(diff, 1.5))を掛けている。誤差が小さい場合は通常の二乗誤差に近い値になる一方で誤差が大きい場合は、diff の1.5乗が効いてきて損失値が急激に大きくなります。したがって誤差が大きいサンプルほど損失関数の値が急増し「大きな外れ値」をモデルが特に嫌うようになる。

3. 評点の予測
-テスト用画像(未知画像)を同じく前処理し各モデルで回帰スコア(1.0〜5.0)を予測した後、モデルごとのスコアを平均し最終的な「連続スコア」とする。この連続スコアをbinning関数で1〜5の整数評点に変換し、画像を「score1」〜「score5」フォルダに振り分けて保存。

4. モデル評価・可視化
-検証データに対しても同様に予測し回帰スコアと評点(整数)を算出。特徴空間の分布をUMAPで2次元に可視化し評点ごとのクラスタリング傾向を確認。

-まとめ
--1, 画像を前処理して読み込み、元スコアを取得(前処理→特徴抽出)
--2, 複数のCNNモデルで回帰学習(1〜5点)(学習)
--3, テスト画像を各モデルで予測し、平均スコアをbinningで1〜5点に変換(予測)
--4, 評点ごとに画像を分類・保存し、検証データで精度評価や可視化も実施(評価)


結果
-モデルA ["EfficientNetV2M", "InceptionResNetV2", "DenseNet121", "MobileNetV3Large"]
--学習時間:4時間15分, 分類:評点1から順番に[2, 32, 365, 240, 26]
-UMAP
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250630/1.png,800x);


-モデルB["EfficientNetB3", "ResNet50", "MobileNetV3Small", "NASNetMobile"]
--学習時間:1時間36分 分類:評点1から順番に[0, 0, 5, 153, 507]
-UMAP
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250630/2.png,800x);

-モデルC["InceptionResNetV2", "ResNet101", "EfficientNetV2S", "NASNetLarge"]
--学習時間:7時間32分 分類:評点1から順番に[0, 11, 73, 336, 245]
-UMAP
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250630/3.png,800x);

-モデルD["DenseNet201", "EfficientNetV2M", "EfficientNetV2L", "MobileNetV3Large"]
--学習時間:6時間48分 分類:評点1から順番に[0, 15, 84, 462, 104]
-UMAP
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250630/4.png,800x);

-モデルE["EfficientNetV2S", "EfficientNetV2M", "EfficientNetB7", "InceptionResNetV2"]
--学習時間:4時間33分 分類:評点1から順番に[0, 4, 90, 409, 162]
-UMAP
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250630/5.png,800x);

**7月 [#j417c700]
***7月第1週(7/3〜7/9) [#j417c701]

5タイプのCNNモデルの結果
-パターンA
クラスタの分離が良好であり特にグループ1・2が図の右側に、グループ4・5が中央付近から左側にかけてなんとなくではあるがまとまっている。評点3は他の評点と比較してやや混在しているように思える。→ 高精度かつ安定性があり分類性能がバランス良い構成

-パターンB
グループ5は分離はしているものの何箇所にも散らばっている上にグループ1〜4はクラスタ内で混在しておりパターンAに比べて明確に分離できていない。密集度が高く情報の圧縮が強すぎる可能性あり。→ 推論が高速な一方で精度は犠牲になっている印象を受けた

-パターンC
グループ1, 2, 5の分類は明瞭であるもののグループ3, 4がクラスタ内で混在(中間層の表現が曖昧)→ 深層かつ多様な構造による表現力が強く精度・再現性の観点ではAと並ぶ

-パターンD
全体的に「偏り」が見られ、クラスタ構造自体は悪くないがグループ3〜4あたりが不明瞭。MobileNetV3Largeの影響か細部抽出よりも全体傾向に寄っている可能性。(MobileNetV3Largeは画像分類・認識には適しているものの錆の大きさや形状といったローカルな特徴の識別は得意としない)→ MobileNetV3Large以外のCNNで錆の微細模様検出を行なうのは良いが、スコア付けにはやや不安定

-パターンE
グループ1, 2は明瞭な分類ができているものの3, 4は分類が上手くできていない。5は図の右側に散らばっておりクラスタごとの距離も大きい。
→ パターンAに劣らない分類性能は持っているものの複数のクラスタが散らばってしまっている。実装するのであればもっと大きなまとまりを作らなければ

-まとめ
, パターン , 分類精度 , 分離度 , 安定性 , 処理速度 , コメント 
, A , ◎ , ◎ , ◎ , ○ , 実用・研究の両面でバランス型  安定+高精度
, B , △ , △ , △ , ◎ , 軽量だが精度不足   軽量で高速な実験をしたい
, C , ◎ , ○ , ◎ , △ , 深層学習として高性能、やや重い  安定+高精度
, D , ○ , ○ , △ , ○ , 特徴は良いが評点にはやや曖昧さ 
, E , ◎ , ◎ , ◎ , △ , 最強構成、実装コスト高め 最大性能を目指す

学習時間, 精度の面から元々行っていた CNN+binning3.py を改良して判別させたほうが良いという結論に至った。評点の変換についてこれからは評点に関数を掛けて1.0〜5.0点に全画像を収めるのではなくこの範囲に評点が収まらなかった画像は学習の対象外&結果を出力・保存しないこととした。


来週までにやること

-評点ごとの平均絶対誤差を算出する
-混同行列を表示する(分類的な観点)
-誤差の大きい画像を抽出する

Pythonスクリプトに 平均絶対誤差 と 混同行列の可視化 を追加した。⇒ CNN+binning3-UMAP.py

平均絶対誤差 MAE は1枚毎に予測値と正解値のズレの大きさを表し、例えば3枚の錆画像A, B, Cがありそれぞれ評点が3点, 5点, 2点であったとする。ラベル付されていない画像を学習した結果をもとにA, B, Cに再度評点(予測・回帰スコア)を付けてそれぞれ2.8, 4.3, 4.0 だった場合誤差は0.2, 0.7, 2.0 となる。MAEは0.3以内に収まれば正確な部類に入るのだが今回のMAEはおよそ0.967であるため正確性に欠ける結果となってしまった。混同行列は判別精度をを可視化するために導入した。

-augmented_imagesに保存されているラベル付き錆画像 
評点1:144枚  評点2:1,131枚  評点3:1,004枚  評点4:519枚  評点5:838枚    Fuji2に保存されている画像:665枚

CNN+binning3-UMAP.pyで学習させてみた
, -項目- , -条件- 
, 評点を付ける画像サイズ(Fuji2) , 334×334ピクセル 
, 使用するCNNモデル , EfficientNetV2S / DenseNet121 / MobileNetV3Large 
, 全結合層 , 6層 
, Dropout関数の実装 , あり 
, 活性化関数 , なし(出力された予測・回帰スコアをbinningスコアに変換) 
, 学習中に精度が良くならない状態が続いたら学習をやめるエポック数 , 10回(10回学習して改善されない場合は終了) 
, エポック数(学習する最大の回数) , 50回 

※binningスコアと予測・回帰スコアの違いについて
-予測・回帰スコアとはCNNモデルが出力する ”連続的な数値(小数)”のことで、例えばモデルが錆画像を見て 3.74 や 2.91 と予測するような値である。予測スコアと回帰スコアは同じ意味を持つ。

-binningスコアとは連続的な回帰スコアを "評点1〜5のカテゴリ(整数値)"に変換したもので、これを "ビニング(binning)"と呼ぶ。求めるにはあらかじめ予測・回帰スコアを出力しておく必要がある。以下に示す変換例のようにbinningスコアを求める。

 # === binning: 予測・回帰スコア → 評点1〜5 ===
 def binning(score):
     if score < 2.00: return 1     #予測・回帰スコアが2.00点未満の場合は評点が1と出力
     elif score < 2.50: return 2   #予測・回帰スコアが2.00点以上2.50点未満の場合は評点が2と出力
     elif score < 3.00: return 3   #予測・回帰スコアが2.50点以上3.00点未満の場合は評点が3と出力
     elif score < 3.50: return 4   #予測・回帰スコアが3.00点以上3.50点未満の場合は評点が4と出力
     else: return 5                #予測・回帰スコアが3.50点以上の場合は評点が5と出力

上記の条件に当てはめると予測・回帰スコアは以下のように変換することが出来る
, 予測・回帰スコア , → binningスコア 
, 1.96 , → 1 
, 2.23 , → 2 
, 2.86 , → 3 
, 3.48 , → 4 
, 4.01 , → 5 


-7/4
特徴量を実装してみた
--錆の占める割合…白黒画像の場合 mask(cv2.threshold)というツールで錆とみなされる部分を白(255)にそれ以外を黒(0)とピクセルごとに塗り分ける。画像内にある錆の部分もとい白いピクセルの数を足し合わせてそれを全体ピクセル数で割ることで、「錆が画像のどれくらいを占めているか」を計算している。
--錆の数… cv2.findContours というツールを用いて錆部分の輪郭を抽出して数える。そうすると"○"のような形状が画像に表示され見つかった"○"(輪郭)の個数を錆の斑点の数=錆の「数」と見なす。
--最大錆サイズ…cv2.contourAreaというツールを用いて各輪郭の面積をピクセルで算出しその中で最も大きな錆を抽出して結果に出力している。
--※錆の部分とそうでない部分の色が逆転してしまっているが画像から錆を検出するにあたりmaskやcv2.〜というツールは Open CV というオープンソースを用いており、このソースは精度や学習の安定性, ノイズ耐性に対して高くかつ強くするために白い領域を物体=錆 と捉えるようになっている。元画像は錆の部分が黒になっているため白黒反転させてから特徴量を抽出している。

-7/7 
引き続き特徴量の補正関数について試行錯誤を重ねていく 重回帰分析を取り入れてみるのも一つの考えだがとりあえず納得するような補正関数を見つけてから導入するか考えることにする。

-7/8 CNNに限らず機械学習(アンサンブル学習)を行なうと学習した結果が .keras ファイルとして保存される。今までは学習するたびに .keras ファイルを削除して学習が終わるまで待機していたがどうやら .keras が残っていれば再学習することなくすぐに評点の出力とUMAPの可視化ができそうだ。これが実現すればPCの前に座っている時間の大幅な短縮が見込める。

--今まで:スタート ⇒既存の .keras ファイルを削除 ⇒ アンサンブル学習開始(3セット分 1〜2時間) ⇒ 評点の出力・UMAP可視化 ⇒ ゴール
--これから:スタート ⇒既存の .keras ファイルを呼び出す ⇒ 評点の出力・UMAP可視化 ⇒ ゴール

-7/9
 CNN+binning5.py
上記のスクリプトで行われていること
--ラベルの付いていない錆画像から画像の面積(ピクセル)に対する錆の割合・錆の総数・最大サイズの各特徴量を抽出する。抽出方法については一番最後にに示した図を参照。
--3つのCNNモデル(EfficientNetV2S / DenseNet121 / MobileNetV3Large)でラベル付きの錆画像を学習させる。
--学習を繰り返している中で最も結果・精度が良くなったところで学習を止める。(ModelCheckpoint)一方で何回学習しても結果・精度が向上しない場合が n 回続いた時点で学習を中止する。( n は任意で設定可能 EarlyStopping )
--学習結果を保存するファイルとして .keras ファイルというものが作成されるが、もし予め存在していればその結果をもとにターミナル上で学習することなく評点の出力とcsvファイルでの保存・UMAPと混同行列の可視化・平均絶対誤差の表示が行われる ←「"新規"」

 例)📂 1_001.jpg → 回帰: 2.62 → 補正後: 2.52 → 評点: 2 | 錆割合: 0.253, 錆数: 375, 最大錆: 470
     📂 1_002.jpg → 回帰: 3.14 → 補正後: 3.14 → 評点: 3 | 錆割合: 0.145, 錆数: 807, 最大錆: 158
     📂 1_003.jpg → 回帰: 2.71 → 補正後: 2.71 → 評点: 3 | 錆割合: 0.237, 錆数: 611, 最大錆: 1188
     📂 1_004.jpg → 回帰: 3.37 → 補正後: 3.67 → 評点: 4 | 錆割合: 0.086, 錆数: 728, 最大錆: 106
     〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜 省略 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
     📂 4_094.jpg → 回帰: 2.58 → 補正後: 2.48 → 評点: 2 | 錆割合: 0.253, 錆数: 410, 最大錆: 638
     〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜 省略 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
     平均絶対誤差:0.11544688538164781(0.3を下回っていれば精度的に"良")

--評点の出力について まず3つのCNNモデルで学習した結果をソフトアンサンブル(平均法)で回帰スコアを出力する。その後抽出された各特徴量の補正係数をもとに補正したスコアを出力し binning で評点に変換する。
--※1 補正したスコアが0.00〜5.00の範囲から外れてしまった場合は学習結果に保存・出力しないようにした。ただし範囲から外れるケースは極めて稀。
--※2 補正係数については以下のスクリプトで設定している。
 # 特徴量によるスコア補正関数(抜粋)
 def adjust_score_by_features(score, rust_ratio, rust_count, max_blob_area):
     correction = 0.0
     #錆面積割合
     if rust_ratio >= 0.70:
         correction -= 0.4         #錆面積割合が0.7(70%)を超えていたら回帰スコアから0.4点マイナスする。
 
     #elif 0.50<= rust_ratio < 0.70:  #評点の結果に応じてこのように範囲, 値を自由に設定可能。
     #    correction -= 0.3        
 
     elif rust_ratio < 0.10:
         correction += 0.2 
    
     #錆の数
     if rust_count > 800:
         correction -= 0.1
 〜〜〜〜〜〜 省略 〜〜〜〜〜〜   
     #最大錆サイズ
     if max_blob_area >= 10000:
         correction -= 0.3
 〜〜〜〜〜〜 省略 〜〜〜〜〜〜    
     return score + correction

--※3 特徴量の抽出方法について

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250709/1.png,750x); ⇒ 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250709/2.png,750x);

***7月第2週(7/10〜16) [#j417c710]
-7/10
特徴量補正の有無による評点結果の比較検証

CNNによる回帰スコアに対して画像特徴量(錆割合、錆数、最大錆サイズなど)を加味することで、最終的な評点結果にどのような影響が現れるのかを検証した。そのために2種類のPythonスクリプト(CNN+binning4.py、CNN+binning5.py)を使い分けて比較を行った。ちなみに2つのスクリプトの違いは学習と判別に特徴量を入れているかどうかである。
-手順
--1,  CNN+binning4.py を実行しCNNモデルの学習を行った。このスクリプトは学習後に .keras ファイルを保存すると同時に、特徴量を考慮しない状態で回帰スコアおよび評点(binning)を算出・CSV, 平均絶対誤差を保存する構成となっている。
--2, 続いてCNN+binning5.py を実行。先ほど CNN+binning4.py で保存した .keras ファイル(学習済みモデル)を読み込んで、特徴量を考慮した補正後スコアおよび最終評点を算出・CSVに保存した。
--3, このとき学習モデルは同一であるため、元の「回帰スコア」は binning4.py と binning5.py のどちこのようにして、「特徴量を考慮するかどうか」の一点のみを変数として固定した上で、最終的な評点の変化や傾向を比較するためのデータセットを得ることができたらでも全く同じ値になる。違いが生じるのはそこから先の「特徴量補正の有無」による最終的な評点のみである。
--4, 次にCNN+binning5.py 側で新たに .keras ファイルを出力(学習)し特徴量補正を行った状態での評点を同時に算出した。
--5, 最後にその CNN+binning5.py で出力した .keras モデルを CNN+binning4.py 側で読み込み、特徴量を考慮しない評点を再度算出することで特徴量補正の影響だけを純粋に比較できる状態を作った。

以下に結果を示す
-① 学習に "特徴量を使い(青)、使わず(朱)" 評点を算出するにあたり特徴量は考慮しなかった場合の評点別に分類された枚数の分布図とUMAP(左下が青 右下が朱)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250710/2.png,750x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250710/452.png,750x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250710/442.png,750x);

-② 学習に "特徴量を使わず(緑)、使った(黃)" 評点を算出するにあたり特徴量を考慮した場合の評点別に分類された枚数の分布図とUMAP(左下が緑 右下が黃)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250710/3.png,750x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250710/552.png,750x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250710/542.png,750x);

-③ ①と②の分布図をひとまとめにしたグラフを以下に示す

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250710/1.png,750x);


, グラフの色 , 学習に用いたpython , 評点算出の際に用いたpython , 特徴量 
, 青 , CNN+binning4.py , CNN+binning5.py , 0.1783 
, 朱 , CNN+binning4.py , CNN+binning4.py , 0.1756 
, 緑 , CNN+binning5.py , CNN+binning5.py , 0.1496 
, 黃 , CNN+binning5.py , CNN+binning4.py , 0.1510 

-7/11
--UMAPの画像を見比べてみると変化があったのは一部の点の色が変わっているくらいで分布や散らばり具合に違いは見られなかった。これは評点別に保存された画像の分布が変動したことによるものだと見ている。

--上記のグラフからわずか4通りの組み合わせで学習を行ったにも関わらず評点ごとに保存された錆画像の枚数が大きいところでは100枚上の差があることを視覚的に捉えることができた。しかしグラフを見ただけではどの組み合わせが正確性に欠けて・精度が良いかを判断することは出来ないため、ここで平均絶対誤差(MAE)の値が必要となってくる。MAEは0に近いほど正確と言えるので今回の場合は "学習に特徴量を使い評点を算出するにあたり特徴量を考慮した" 学習が1番精度が高かったと言える。とはいえ他の組み合わせもMAEが0.3を切っておりそれなりに正確性を備えている。特徴量を考慮した学習を行なうことで考慮しない場合と比較して0.25程差が出ることが今回行ったことでの1番の収穫であったと思っている。今後は特徴量を学習に組み込んでMAEをより0に近づけられるように尽力する。

-7/14〜16
O.Cの準備及び外国語文献の発表スライド作成に注力したため進捗なし

***7月第3週(7/17〜23) [#j417c717]
-7/17
今週はMAEに注目して精度向上を狙っていきたい。現時点では学習が何回停滞したら終了するかを任意で設定可能なEarlyStoppingのpatienceを5にしたときがMAEが最小となった(0.11ぐらい)。ただ検証データ(ラベル付き錆画像の20%にあたる)の精度が上がらず、特に評点4と5が正答率50%前後をうろついておりほかは90%台後半をコンスタントに出し続けている。評点4と5は錆の中でも程度が軽い方であるため早急に改善を行う必要性は高くはないと思っているが、今年中には各評点の精度が100%に限りなく近づけなければ。

***7月第4週(7/24〜30) [#j417c724]
-7/28〜30
 CNN+binning6.py

binning5.pyには実装しなかった重回帰分析がメインとなる。今までは各特徴量(錆割合・面積比・錆の数)の大きさに応じて回帰スコアから点数を加減していたが、重回帰を入れることで .pkl ファイルと .csv ファイルから特徴量の重みを自動で最適化してもらうようにした。しかしMAEがCNN+binning5.pyより大きくなってしまっているため出来が良くないかそれともまだ改善の余地があるのだろうか。

-ここ最近はMAEをなるべく0に近づけるようにしている(CNN+binning5.pyで実行)

, 回 , 錆面積範囲 , 錆数 , 最大錆サイズ , MAE , 評点別画像分布(左側から順に評点1、2 …)
, 1 , 定義しない , 定義しない , 定義しない , 0.129 , 13-133-420-78-21 
, 2 , 0.5以上:-0.4 0.35以上0.5未満:-0.15 0.1以下:+0.15 , 800以上:+0.1 ,  10000以上:-0.3 250未満:+0.1 , 0.135 , 22-124-374-118-27 
, 3 , 0.5以上:-0.4 0.35以上0.5未満:-0.15 0.05以下:+0.15 , 550以下:+0.2 , 10000以上:-0.2 5000以上10000未満:-0.1 50以上150未満:+0.1 50未満:+0.2 , 0.133 , 17-72-432-107-37 
, 4 , 2と変化なし , 削除(定義しない) , 2と変化なし , 0.127 , 22-126-406-85-26 
, 5 , 0.5以上:-0.4 0.33以上0.5未満:-0.15 0.05以上0.1未満:+0.1 0.05未満:+0.2 , 削除(定義しない) , 10000以上:-0.3 5000以上10000未満:-0.2 50以上200未満:+0.1 50未満:+0.2 , 0.127 , 27-121-406-99-32 
, 6 , 0.6以上:-0.5 0.45以上0.6未満:-0.3 0.25以上0.45未満:-0.1 0.05未満:+0.3 , 1000以上:-0.2 500以上1000未満:-0.1 100以下:+0.1 , 15000以上:-0.4 8000以上15000未満:-0.2 100以下:+0.2 , 0.124 , 24-139-397-71-33 
, 7(重回帰) , ーーー , ーーー , ーーー , 0.126 , 13-125-375-115-27 

特徴量に応じて回帰スコアから加減点を行うことは、定義しないときと比較してMAEは小さくなっているものの正確性は僅か1%ほどしか変わらず必ずしも特徴量を考慮する必要性は無いように思える。結局は学習させる画像が少ないのかはたまたラベル付けが下手なのか色々要因は考えられそうだが、どちらかと言えばラベル付き画像が少ない方に原因があってほしい。とりあえずデータを4倍とかに増やして後日学習させたい。

データ数を増やすのはもう少し時間がかかりそう。重回帰で学習させてみたところ重回帰なしの場合と比較してMAEにそこまで違いが見られなかったので

-① 特徴付き画像フォルダを読み込む(学習画像 + ラベル = 回帰ターゲット)
-② 特徴量(錆割合・数・最大サイズなど)を抽出
-③ CSVに保存(訓練データ・予測用データ兼用)
-④ 特徴量と評点(ラベル)で重回帰学習 → .pkl保存
-⑤ 同じ特徴量データに対して、重回帰を適用 → 評点を補正
-⑥ CNN3種(EffNetV2S/DenseNet/MobileNet)で学習 → アンサンブル
-⑦ 評点の最終出力(重回帰補正後の回帰スコア + CNN予測スコアの平均 or組み合わせ)
-⑧ 結果保存(CSV・UMAP・混同行列など)

を1つのスクリプトにまとめたものをこれから1周間で作成できればと思っている。現地調査で得られる錆画像の学習を考慮して重回帰を軸にスクリプト作成をこれからは行っていきそうだ。

**8月 [#j417c800]
***8月第1週(7/31〜8/6) [#j417c731]

-8/3
1日から県内に架けられている耐候性鋼橋約90箇所の中から調査が行えそうな箇所かつ市内から近いところを中心に調査していたがとりあえず近場の19橋だけを見てみた。調査できそうな箇所のみ以下のような感じで耐候性鋼橋のページに記載する。

[["大学"を Google Map で開く>https://www.google.com/maps/place/39.7271952,140.1345854]]

-8/4
耐候性鋼橋約90箇所が調査可能かどうかを Google map で見ながら 可, 保留, 不可 の3種類に分類し、"可"の中でも確実に調査できるであろう橋梁をピックアップして耐候性鋼橋のページに記載させていただいた。

機械学習についてはやはり学習元となるセロハン試験で採取されたラベル付き錆のモノクロ画像が絶対数足りないためこの新たなデータが待たれる。現在はラベル付き画像を約10,000枚に増やして学習をさせているがMAEは変化しない。
-MAE向上へ
--1, データ量を無意味に水増しするのではなく意味的な水増しを行う…画像パッチ単位(タイル切り出し)で訓練データを別々に生成し認識系タスクで実績ある augmentation を導入
--2, Soft label にラベル品質スコア導入…手動で「このラベルは信頼度が高い(1.0)」「これは曖昧(0.6)」と設定。自分自身でラベル付けした画像は信頼度1.0, 拡張画像は0.85としておく(拡張画像に対して直接的にラベルを付けていないため)。
--3, 高次元な非線形補正…重回帰の代わりに LightGBM, XGBoostによる補正モデルを用いる → 特徴量の組み合わせに対する非線形学習を適用
--4, スコア帯(4, 5)の混同行列精度や Confusion Matrix上の改善率を指標に追加


-8/5, 6

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250805/1.png,800x);

上記の画像(混同行列)は10,000枚近くあるラベル付きの画像の中から20%にあたる2,000枚弱をテスト用データとして残り80%の画像を3つの学習モデルで学習、その結果を .keras ファイルで保存して正確に判別できるかどうか試したものである。縦軸 True label が実際の評点 横軸の Predicted label が .keras ファイルをもとに予測した評点をそれぞれ表しており、左上から右下への対角線上が青くなっているほど正確に予測できたと言えるのだがご覧の通り対角線以外の部分が青くなっている。各評点ごとの判別率は評点1から順に 81%, 85%, 100%(に限りなく近い), 34%, 84% となっており、特に評点4の精度が低くその原因として
--評点4画像の枚数・バリエーション不足 → 水増しによる拡張はしているものの "見た目の多様性" が不足しているのでは
--上記の原因によりモデルが評点4の特徴を学習できていない → 評点4特有のパターンが訓練データに弱く学習困難
が考えられる。精度向上のため
-- 評点4の「代表的な画像」を新たに5〜10枚ほど追加 → 判別させたい錆画像の中から評点4っぽい画像を数枚ピックアップして目視でラベル付け
--追学習 → 評点4に関して予測誤差が大きい画像だけを再学習させるスクリプトを実装
を行うことで正答率34%からどれほど上げられるか。とりあえず評点4のみで試してみて良い結果が得られたら他の評点にも導入していく予定。

結果

-評点4のラベル付画像を新たに6枚追加した場合のみの混同行列(生成画像枚数 全1,406枚)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250805/2.png,800x);

生成された画像が少ないが評点4の正答率が大幅に向上した。(58 / 185 → 57 / 58)他の評点においてもラベル付き画像を10枚に増やして学習を行うことにする。評点5は元々の画像が少ないので10枚にする必要はないかも

***8月第2週(8/7〜8/13) [#j417c807]
-8/7
--全評点のラベル付画像を10枚(評点5だけ9枚)にした場合のみの混同行列(生成画像枚数 全8,956枚)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250805/3.png,800x);

画像を水増ししすぎてしまったのか綺麗な対角線を描けなかった。上手く言ったときは1枚あたり60枚水増し、今回は1枚あたり3倍の約180枚水増し 過学習を引き起こしたことで思った結果が得られなかったのか明日検証してみる。

-8/8
--1枚あたりの水増しを約60枚にして学習させてみた。

***8月第3, 4週(8/14〜8/27) [#j417c814]
-8/14
一度に複数の条件で学習を行えるようにしたため、学習条件を変えながら全24通りのモデルを試してみる。全24通りの条件内容は以下の表の通り(水増しする画像の枚数, 水平・鉛直方向への移動,  学習回数-エポック数, バッチサイズ)※バッチサイズ(batch_size)とは学習の1回のパラメータ更新で何枚の画像を使うかを決める値のこと

, モデルNo. , 水増し画像枚数 , 水平・鉛直方向への移動 , 学習回数 , バッチサイズ 
, 1 , 50 , あり(水平・垂直ともに最大10%移動 他のモデルも同様) , 30回 , 32枚 
, 2 , 50 , なし , 30回 , 32枚 
, 3 , 50 , あり , 50回 , 32枚 
, 4 , 50 , なし , 50回 , 32枚 
, 5 , 50 , あり , 30回 , 64枚 
, 6 , 50 , なし , 30回 , 64枚 
, 7 , 50 , あり , 50回 , 64枚 
, 8 , 50 , なし , 50回 , 64枚 
, 9 , 100 , あり , 30回 , 32枚 
, 10 , 100 , なし , 30回 , 32枚 
, 11 , 100 , あり , 50回 , 32枚 
, 12 , 100 , なし , 50回 , 32枚 
, 13 , 100 , あり , 30回 , 64枚 
, 14 , 100 , なし , 30回 , 64枚 
, 15 , 100 , あり , 50回 , 64枚 
, 16 , 100 , なし , 50回 , 64枚 
, 17 , 150 , あり , 30回 , 32枚 
, 18 , 150 , なし , 30回 , 32枚 
, 19 , 150 , あり , 50回 , 32枚 
, 20 , 150 , なし , 50回 , 32枚 
, 21 , 150 , あり , 30回 , 64枚 
, 22 , 150 , なし , 30回 , 64枚 
, 23 , 150 , あり , 50回 , 64枚 
, 24 , 150 , なし , 50回 , 64枚 

-統一した諸条件

, 条件内容 , 条件 
, ランダム回転(画像を 最大 ±(記入された数字)° の範囲でランダム回転) , 最大±15°回転 
, 画像を反転させるか , ON 
, 画像の明るさ変更(明るさをA倍(暗く)〜B倍(明るく) の範囲でランダム変更) , 0.8〜1.2倍に設定 
, 使用するCNNモデル , EfficientNetV2S、DenseNet121、MobileNetV3Large 

-8/14
--全24パターンのMAEが出力されたのでその結果を貼り付ける。

, モデルNo. , MAE 
, 1 , 0.196729463582136 
, 2 , 0.243831214856128 
, 3 , 0.195313368524824 
, 4 , 0.144703118168578 
, 5 , 0.205965055494892 
, 6 , 0.289931784357343 
, 7 , 0.17892047945334 
, 8 , 0.230305907920915 
, 9 , 0.126362581399022 
, 10 , 0.0984963168903273 
, 11 , 0.132567102081922 
, 12 , 0.0933165281402821 
, 13 , 0.162534093126959 
, 14 , 0.183336140306628 
, 15 , 0.173845792789849 
, 16 , 0.0904155796279713 
, 17 , 0.108065831782867 
, 18 , 0.095051087976313 
, 19 , 0.104002416782639 
, 20 , 0.0796793378534771 
, 21 , 0.204868344141513 
, 22 , 0.206347949407539 
, 23 , 0.092119121267682 
, 24 , 0.256283822351573 

--MAEが0.11を切ったモデルのみ抽出(昇順に並び替え)
, モデルNo. , 水増し画像枚数 , 水平・鉛直方向への移動 , 学習回数 , バッチサイズ , MAE 
, 20 , 150 , なし , 50回 , 32枚 , 0.0796793378534771 
, 16 , 100 , なし , 50回 , 64枚 , 0.0904155796279713 
, 23 , 150 , あり , 50回 , 64枚 , 0.092119121267682 
, 12 , 100 , なし , 50回 , 32枚 , 0.0933165281402821 
, 18 , 150 , なし , 30回 , 32枚 , 0.095051087976313 
, 10 , 100 , なし , 30回 , 32枚 , 0.0984963168903273 
, 19 , 150 , あり , 50回 , 32枚 , 0.104002416782639 
, 17 , 150 , あり , 30回 , 32枚 , 0.108065831782867 

上記の結果より学習回数(の上限)は50回, 画像の水増し枚数は150枚/1枚あたりで学習を行ったほうがMAEは良くなることが分かった。一方で画像の加工についてだが移動に関しては動かさないほうがより良い結果に結びつくのではないかと推測し、バッチサイズの大きさはそこまで学習に影響を及ぼさない事が収穫か。次回は学習回数, バッチサイズを統一して1枚あたりの水増し枚数, 画像加工に関するパラメーターを弄りながらMAEの変化を見ていくことにする。

-8/18

 CNN+binning5-1.py
 CNN+binning7.py

今までは画像の水増しと学習を別々に行っていた(始めにMMLI.pyなどでラベル付きの画像を水増しした後CNN+binning5.py等で学習をさせていた)が、CNN+binning5-1.pyより1つのモデルに対する画像生成・学習を一括で行えるようにした。またCNN+binning7.pyは5-1の拡張版のようなもので画像を拡張するにあたり回転の大きさ, 明るさ, 画像の水増し枚数等の要素や学習方法(学習回数, バッチサイズ)を任意で設定可能にした且つ全通りの学習を一度に行えるようになった。つまり学習が1回完了するごとにMAEや行列を確認する必要がなくなり複数の学習モデルを比較できるようになったわけである。

-- CNN+binning5-1.py → .keras ファイルが作成されるように書き換えて その .keras ファイルで思うような結果が出力されるか
-- CNN+binning7.py → 明日までに27通りの学習モデルを学習させられるようにしたい

-8/19
--CNN+binning5-1.py …  .keras ファイルの出力完了
--CNN+binning7.py … MAE等結果の確認は24日以降 今日中に回して明日の朝確認できたら

---上記のスクリプトは画像を生成する際に元画像(長方形)から正方形にリサイズしていたため画像が横に伸び錆画像が魚群探知機みたいになってしまう現象が発生した。なのでそれが起こらないように長方形の錆画像を正方形にリサイズする場合、左右にできる空白は画像を反転することで埋め合わせすることで解決した。

CNN+binning5-1.py で出力された .keras ファイル(MAEは0.07)を CNN+binning5.py に適応したところMAEが0.14になった。既存の .keras がある状態でラベル付き画像を学習することはないので根本的な原因は双方の学習方法にありそうだがどうだろうか。来週には調べられたらと思っている。

-8/25
20日の朝に学習を開始させたところ学習開始約60時間で強制終了されていた。原因として考えられるのがパラメータの組み合わせの多さで多分729通りとか学習させようとしていたと思う。その後24日の午前中に組み合わせの数を減らして学習させたところその日の夜に強制終了していた。どちらも37通り目で強制終了していたので組み合わせの総数が37未満であれば回るのではないかと思い、水増し枚数, 回転角度, 明るさ, 横移動, 縦移動の各パラメータを2通りづつ計2^5 = 32 通りの学習を行わせた。結果は明日にでも出ると思う。

-8/26
午前中の時点で12パターンの途中まで学習が行われていなかったため強制終了が入らなければ明日の午後にずれ込むと思う。ターミナルの学習履歴を見ていると各学習パターンでスコアが負の値になっていたり5.5を超えたりしている錆画像が数枚見つかった。特徴量を抽出してその程度で加減の大きさを変える必要がありそうだ。

--学習の流れについて

今現在の学習方法を以下に示す

 1, オリジナル画像の読み込み
 2, 画像をアスペクト比を維持しながらリサイズ・パディング
 3, 回転・明るさ調整・シフトなどを加えた画像を複数枚生成してデータを水増し
 4, 水増し済み画像の読み込んでラベル(スコア)を抽出
 5, 学習用と検証用にそれぞれ総画像枚数 8:2 に分割
 6, EfficientNetV2S / DenseNet121 / MobileNetV3Large の 3種類をベースにした回帰モデルを作成
 7, モデル( .keras ファイル)が保存済みなら再利用なければ学習して生成
 8, 検証用データの結果をもとにMAE(平均絶対誤差), 混同行列(binning で1〜5に丸めた結果を利用), UMAP を保存
 9, ラベルがついていない画像を読み込んで推論値(回帰スコア)を算出し、錆割合・錆の数・最大錆サイズ等の特徴量を抽出し予め設定しておいた補正値を加算
 10, 各画像ごとの推論結果, MAE, データ数, タイムスタンプ等を csv ファイルとして保存

--補正関数について(再掲)

現在実装している補正関数は以下の通り ペナルティ関数と補正関数の2種類が実装されている

---1, ペナルティ関数(学習時の損失関数)

 @register_keras_serializable()
 def custom_penalizing_loss(y_true, y_pred):          #pred = prediction(予測)
     diff = tf.abs(y_true - y_pred)
     return tf.reduce_mean(tf.square(diff) * (1.0 + tf.pow(diff, 1.5)))

誤差(diff) = |y_true - y_pred| を二乗しつつ(1 + diff^1.5) で大きな誤差を強くペナルティする → 外れ値や大きな誤差を強く抑える

---2, 補正関数(特徴量で補正)

 def adjust_score_by_features(score, rust_ratio, rust_count, max_blob_area):
     correction = 0.0      #リセット
     # 錆面積割合
     if rust_ratio >= 0.60:
         correction -= 0.5
 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
     # 錆の数
     if rust_count >= 1000:
         correction -= 0.2
  〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
     # 最大錆サイズ
     if max_blob_area >= 15000:
  〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
     return score + correction  #補正前評点+補正関数

錆割合・錆の数・最大錆サイズに基づいてスコアを ±0.1〜0.5 程度補正し、小さな錆 → 加点、大きな錆 → 減点というような感じで付けている。


現在使用しているモデル(CNN+binning7.py)と以前使用していたモデル(CNN+binning5.py)の各スコア・評点の比較(各特徴量はどちらも同じ値をとっていたため省略) 画像はどちらもランダムに15枚抽出した同じものを使用した。

--1, 補正前スコア(回帰スコア)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250806/1.png,750x);

--2, 補正後スコア

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250806/3.png,750x);

--3, 評点

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250806/2.png,750x);


CNN+binning.pyのVerが上がるごとに学習方法も変化してくるので前モデルと現モデルの比較を行ってみた。全画像を比較していないので断言は出来ないが、1番誤差があると考えられる補正後スコアの2番は0.2ポイントほどでありどの要素もそこまで大きく乖離している部分は見受けられなかった。

-8/27

昨日の夜に学習が強制終了されていた。どうやら学習時にメモリを消費しすぎて落ちてしまうらしい。そこで少しでも使用するメモリを抑えるため学習時に numpy 配列をそのまま渡していたのをやめて tf.data.Dataset を使用するように変更し、学習ごとにどのような条件で行ったのかがわかるように出力されるログの部分に ”水増し枚数”, ”回転角度”, ”明るさ”, ”縦移動”, ”横移動”, ”エポック数”の各要素を記載するようにした。今現在組み合わせをかなり減らして学習を行わせているのでなんとか学習を終えてほしい。

***8月第5週(8/28〜9/3) [#j417c828]
-8/28

 また強制終了していた。学習の組み合わせを2パターンにして回してみるがこれで回らなかったらスクリプト全体を見直す必要がありそうだ。主な原因としてバッチサイズが大きすぎる(現在32で学習させている=1グループあたり何枚にするかを決める)ことによるGPUメモリの使い切りが考えられる。バッチサイズを小さくすることでトータルで学習に時間がかかってしまうが、一度に使用するメモリを抑えることができるのでバッチサイズを16や8にして学習させてみたい。だが今までの学習ではバッチサイズを32で統一してきたので、より小さくすることで結果に影響が出ないのかを見ておく必要がありそうだ。

-8/31

 CNN+binning7-1.py

スクリプトの簡略化と一度に複数回学習を行えるようにした機能を追加したところ最後まで学習が行われない問題が発生したので(CNN+binning7.py)、簡略化を断念し各学習ごとにパラーメータを記載するようにした。試しに2通りの学習モデルを実装して回した。

--学習結果

エポック数を1にして回してみたところ終わったので構造に問題はなさそう。学習中にモニターの動作が遅くなるようなことはなし。

-9/2

 現在全64パターンを学習中 画面が固まるようなことなく無事に学習が続いている。31日から始めてまる2日ほど経過しただろうか 27パターン目まで学習完了しているので単純計算でもう2, 3日は掛かりそうだ。現在精度の良いモデルを見つけるにあたってMAEという指標を用いて探しているのだが、どうやら他にも混同行列, Macro F1, 単純精度-Accuracyも精度の良し悪しに関して判断材料となりそうだ。

--混同行列…何度も出てきているので説明は割愛 対角線上に数値が集まっているほど理想的な学習モデル
--Macro F1…再現率と適合率の調和平均 0.6以上あればモデルとしては良 [[Macro F1の算出方法について:https://zenn.dev/hellorusk/articles/46734584386c49057e1b]]
--単純精度…全体で「正しいクラス(評点1〜5)」に分類できた割合(正解枚数/予測枚数)75%は欲しい

**9月 [#j417c900]
***9月第1週(9/4〜9/10) [#j417c904]
-9/4
 39パターン目の学習が終了した時点で強制終了となっていた。ターミナル上で学習が1回終わる毎にPCに積んであるメモリの何%を使用しているか確認できるようにしたことに加えてメモリの開放を実装した。もし開放していても使用メモリが徐々に上がっていたらまた何か対策を考えなければ。とりあえず今(9:30 a.m.)は全16パターンの学習を始めたところで上手く動くか様子を見ていくつもり 明日の朝来たときには終わっていてほしい。

-9/5

中間発表に向けて準備を進めているが現地調査の結果次第で書く内容を修正する必要性があるので、概要に関しては背景〜方法までを書き上げたもののスライドに関しては何もやっていない。とりあえず8日までにできれば終われせておきたいことリストを以下に示しておく。

・各CNNモデルごと(単体)で学習させた結果とアンサンブル学習の結果比較

・概要の見直し

・概要を書き上げた部分までに該当するスライド作成

-9/7

1枚あたりの生成枚数を200枚固定, 画像の加工処理範囲を変化させながら学習させてみたところ以下のような結果が得られた。

, 明るさ(通常は1.0以下だと暗、1.0以上だと明) , 縦の移動(百分率) , 横の移動(同左) , MAE , 正答率(百分率) , macrof1 
, (0.8、1.2) , (-0.05、0.05) , (-0.05、0.05) , 0.123 , 0.841 , 0.793 
, (0.8、1.2) , (-0.05、0.05) , (-0.1、0.1) , 0.124 , 0.824 , 0.755 
, (0.8、1.2) , (-0.1、0.1) , (-0.05、0.05) , 0.131 , 0.818 , 0.753 
, (0.8、1.2) , (-0.1、0.1) , (-0.1、0.1) , 0.128 , 0.815 , 0.762 
, (0.7、1.3) , (-0.05、0.05) , (-0.05、0.05) , 0.134 , 0.818 , 0.773 
, (0.7、1.3) , (-0.05、0.05) , (-0.1、0.1) , 0.133 , 0.814 , 0.759 
, (0.7、1.3) , (-0.1、0.1) , (-0.05、0.05) , 0.126 , 0.816 , 0.769 
, (0.7、1.3) , (-0.1、0.1) , (-0.1、0.1) , 0.134 , 0.817 , 0.786 

結果として水増しする枚数を多くしたりパラメータをいじったりしても精度にはほどんど影響されないことが分かった。逆に学習は明るさに左右されないということもここから読み取れる。これは既存の錆画像を水増ししただけでは精度は向上しないと暗に示しているのではないかと思われ、明日, 明々後日の調査で得られる錆画像を学習に取り入れて結果に変化が出るか見る必要がありそうだ。ちなみに結果の上段から順に1, 3, 5, 7段目の混同行列の結果を貼り付けておく。大して結果が変わっていない以上各行列にも大きな変化は見当たらないが、どれも評点4の判別に苦戦しているようだった。(ラベル付き画像を増やせば改善されるかも)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250907/9.png,700x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250907/11.png,700x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250907/13.png,700x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250907/15.png,700x);


各CNNモデル単体とアンサンブル学習の結果比較は9日の午後時点で進行中 今日中に終えられるのかは厳しいところ

-9/8
CNNの単一モデルとアンサンブル学習の結果を比較したところ以下のような結果になった。

, Model , Accuracy , MAE 
, EfficientNetV2S , 0.807 , 0.211 
, DenseNet121 , 0.864 , 0.112 
, MobileNetV3Large , 0.858 , 0.138 
, Ensemble , 0.849 , 0.124 

どのモデルも精度には申し分ないのだが、アンサンブルの結果が一番だと思っていた予想を裏切られた。他のモデルも試してみる必要があるか それともDenseNetだけで学習させる必要があるのか。

--どうやらモデル間の相関が高すぎるとアンサンブルの結果がかえって低下を招く恐れがあるらしい。3つのモデルはどれも ImageNet 事前学習済みの CNNであるため、学習データが同じで特徴も似てしまい異なる視点で補完できずなかった可能性がある。アンサンブルは「誤りが補完し合う」ことで精度が上がるため誤差が似ていると効果が薄い。今回は "DenseNet121", "InceptionResNetV2", "Xception" の3モデルとアンサンブルを試してみる 明日の朝には結果が出る予定。

-9/9
--昨日の現地調査で撮影した写真を橋梁ごとにフォルダを作成してK2にアップした 報告書はまだ作成していないので画像の名前は書き換えていない。

"DenseNet121", "InceptionResNetV2", "Xception" の3モデルとアンサンブルのMAE・正答率を比較したところ以下のような結果になった。

, Model , Accuracy , MAE 
, DenseNet121 , 0.863 , 0.109 
, InceptionResNetV2 , 0.862 , 0.109 
, Xception , 0.857 , 0.110 
, Ensemble , 0.861 , 0.103 

***9月第2, 3週(9/11〜9/24) [#j417c911]
-9/11
水増し枚数を50, 100, 150, 200枚にして学習させた結果(混同行列)を載せておく 前にも言ったがやはり50枚のときが一番精度的に良さそうだと思える。

--混同行列(左:50枚 右:100枚)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250911/50.png,700x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250911/100.png,700x);

--混同行列(左:150枚 右:200枚)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250911/150.png,700x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250911/200.png,700x);

新CNNモデルの組み合わせでそれぞれ学習, アンサンブルを行ったところ以下のような混同行列が得られた。因みに水増し枚数は50枚で統一している。

--混同行列(左:DenseNet121 右:InceptionResNetV2)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250911/confusion_matrix_DenseNet121.png,700x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250911/confusion_matrix_InceptionResNetV2.png,700x);

--混同行列(左:Xception 右:アンサンブル)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250911/confusion_matrix_Xception.png,700x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20250911/confusion_matrix_Ensemble.png,700x);

-9/17

画像の水増し枚数は50枚, 水増しの際に画像にする加工処理はなるべく弱めにすることが精度向上のコツなのだが正答率85%よりも上を目指すにはどうしたら良いか考える必要がある。


-9/18
 CNN+binning7-2.py

CNN+binning7-1.py に 誤差解析ツール(MAE, Accuracy, Macro-F1, クラス別MAE, ±1精度, 残差・誤差相関プロット)を組み込み、評価結果が保存されるようにした。現在アンサンブルの精度が伸び悩んだ原因を分析中で組み込んだ誤差解析ツールの結果待ち。

-9/22

新旧の両モデルを学習させて結果の比較を行っている 新モデルの学習は完了し今日の夜ごろには旧モデルが終わる見込み

-9/24

今月上旬に行った現地調査で採取した錆付きテープをスキャナーで線画・カラーの両方でスキャンして学習に取り入れた。年末の発表までに良い結果が得られればよいのだが、学習させてみて精度に影響を及ぼすか見てみる MAEが0.10を下回ればラベル付き画像を導入した甲斐があったといえるか。CNN+binning7-21.py をいじっていたら学習に水増しした画像にさらに水増しを行った画像で行っていることが発覚し、元画像から1回だけ水増しした画像たちで学習させたところMAEやF1スコアがとんでもなく悪化した。ただ収穫もあり、37万枚程学習させないと精度は良くならないことが分かった。

***9月第4週(9/25〜10/1) [#j417c925]


調査で採取した錆画像を取り入れたことでラベル付き画像が既存の枚数から2倍以上に増えた。水増しする枚数を変化させながら(元画像✕500, 1000, 1500, 2000, 3000枚)結果がどうなるかを見てみようと思う。以下に各枚数ごとの精度指標を載せておく。


-元画像→水増し1回→学習

, 精度指標 , 水増し500枚 , 水増し1000枚 , 水増し1500枚 , 水増し2000枚 , 水増し3000枚 
, MAE(Ensemble) , 2.576 , 2.364 , 1.931 , 3.012 , 3.15 
, 正答率 , 0.304 , 0.261 , 0.217 , 0.261 , 0.217 
, F1スコア , 0.093 , 0.083 , 0.071 , 0.083 , 0.071 



, 各評点ごとのMAE , 水増し500枚 , 水増し1000枚 , 水増し1500枚 , 水増し2000枚 , 水増し3000枚 			
, 評点1 , 1.573 , 1.183 , 0.647 , 1.364 , 1.594 
, 評点2 , 2.352 , 2.111 , 1.683 , 2.496 , 2.589 
, 評点3 , 2.516 , 2.456 , 1.985 , 3.244 , 3.399 
, 評点4 , 3.302 , 3.211 , 2.691 , 4.090 , 4.057 
, 評点5 , 4.003 , 3.531 , 3.263 , 4.658 , 4.537 

元画像114枚から水増しを1回だけ行った結果、以前に言った通り精度が低下してしまった。114枚 → 約36万枚だと画像レパートリーが不十分なのか納得のいく結果が得られない。元々は水増しした画像をさらに水増しするとMAEが0.10あたりになる。今回の場合114枚 → 5700枚 → 約36万枚で増やすと上手くいく やはり元画像は6000枚はないと厳しいか。


**10月 [#j417c1000]
***10月第1, 2週(10/2〜10/15) [#j417c1002]

-10/2

学習が強制終了していたためもう一度回しているが、無事学習し終えるかどうかは五分五分といったところ

-10/9

中間発表が終わってやること
--現地調査報告書作成
--県管理の橋梁で調査できそうな耐候性鋼橋の選定
--中間発表で用いた機械学習の中身
--耐候性鋼橋のページに写真を貼り付ける

-10/13
--機械学習の中身についての説明を明日

-10/14
--中間発表で使用したモデル

 import os                                                                                                     # ファイルパスやフォルダ操作のための標準ライブラリ
 import cv2                                                                                                    # 画像処理用ライブラリ(OpenCV)
 import shutil                                                                                                 # フォルダ削除やコピー操作に使用
 import numpy as np                                                                                            # 数値計算ライブラリ(行列演算などに必須)
 import seaborn as sns                                                                                         # グラフ描画を綺麗にするための補助ライブラリ
 import tensorflow as tf                                                                                       # Deep Learning フレームワーク(Kerasを内包)
 from PIL import Image                                                                                         # 画像の入出力・変換処理用
 from tensorflow.keras import layers, models, Input, Model                                                     # Keras モデル構築系の基本構成要素
 from tensorflow.keras.models import load_model                                                                # 保存済みモデルの読み込み用
 from tensorflow.keras.layers import Layer                                                                     # カスタムレイヤーを定義するために使用
 from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint                                         # 学習制御用のコールバック
 from sklearn.model_selection import train_test_split                                                          # 学習データと検証データを分割する関数
 from sklearn.metrics import mean_squared_error, confusion_matrix, classification_report, mean_absolute_error  # モデル評価のための関数群
 from tensorflow.keras.applications import DenseNet121, InceptionResNetV2, Xception                            # 転移学習で利用するCNNモデル3種類の定義
 import matplotlib.pyplot as plt                                                                               # グラフや画像を描画するためのライブラリ
 import pandas as pd                                                                                           # データフレーム操作ライブラリ(結果集計などで使用)
 import psutil                                                                                                 # メモリ使用量確認用ライブラリ
 import datetime                                                                                               # 実験時間や日付の記録用
 import gc                                                                                                     # メモリ開放(ガーベジコレクション)を強制的に行う
 
 # === 基本設定 ===
 NUM_MODELS = 3                                                                                                # アンサンブルに使用するモデルの数(今回は3種類のCNNモデルを使用)
 TRAIN_FOLDER = "train_images"                                                                                 # 学習用に用いるデータを保存しておくフォルダ
 TEST_FOLDER = "Fuji2"                                                                                         # 評価用データフォルダ
 BASE_RESULTS_DIR = "results_patterns1-2"                                                                      # 学習結果を保存するディレクトリ名
 IMAGE_SIZE = (256, 256)                                                                                       # すべての画像を256×256にリサイズしてから学習を行う
 USE_COLOR = False                                                                                             # False → 白黒で処理(True の場合はカラーで処理される)
 MODEL_PATHS = [f"ensemble_model_{i+1}.keras" for i in range(NUM_MODELS)]                                      # モデル保存用ファイル名(.keras ファイル)を自動生成
 
 # === フォルダの削除・作成 ===
 if os.path.exists(BASE_RESULTS_DIR):                                                                          # 既に結果フォルダがある場合
     shutil.rmtree(BASE_RESULTS_DIR)                                                                           # そのフォルダを丸ごと削除して
     print(f"既存のフォルダ '{BASE_RESULTS_DIR}' を削除しました。")                                            # 削除完了を通知
 
 os.makedirs(BASE_RESULTS_DIR, exist_ok=True)                                                                  # 新しいフォルダを作成
 print(f"新しいフォルダ '{BASE_RESULTS_DIR}' を作成しました。")  
 
 # === 学習パターンの定義 ===
 patterns = [
     {'name': 'pattern_1', 'epochs': 50, 'batch_size': 32, 'num_aug':40, 'rotation': (-100, 100), 'brightness': (0.7, 1.3), 'width_shift': (-0.5, 0.5), 'height_shift': (-0.5, 0.5)},
     {'name': 'pattern_2', ...
 ]
 #ここでパターンごとに各パラメータを設定する(name:学習結果を保存するディレクトリ名, epochs:学習回数, batch_size:バッチサイズ設定, num_aug:1枚の画像から水増しする枚数, rotation:回転角度(定義された数値内でランダム), brightness:明るさ(定義された数値内でランダム), width_shift:横移動(定義された数値内でランダム), height_shift:縦移動(定義された数値内でランダム))
 
 # === 学習画像とスコアの読み込み関数 ===
 def load_images_and_scores(folder):
    images, scores = [], []                                                                                    # 画像データとスコアを格納するリストを初期化
    for fname in sorted(os.listdir(folder)):                                                                   # 指定フォルダ内のファイルをソートして順に処理
        if fname.endswith(".png") or fname.endswith(".jpg"):                                                   # 対象はPNGまたはJPEG画像
            try:
                score = float(fname[0])                                                                        # ファイル名の先頭文字(例:3_001.jpg → 3)をスコア(ラベル)として取得
                score += np.random.uniform(-0.2, 0.2)                                                          # ±0.2 の乱数を加えて Soft Label 化(過学習防止)
                if score < 0.00 or score > 5.50:                                                               # 学習をもとに出力されたスコアが0点以下または5.50点以上の場合はスキップし、結果に反映させない
                    print(f"⚠️ スコア範囲外: {fname} → {score:.2f} → スキップ")
                    continue
                path = os.path.join(folder, fname)                                                             # ファイルパスを生成
                img = Image.open(path).convert("RGB").resize(IMAGE_SIZE)                                       # 画像をRGB化&256×256に統一
                images.append(np.array(img) / 255.0)                                                           # ピクセル値を0〜1に正規化してリストに追加
                scores.append(score)                                                                           # スコアを追加
            except Exception as e:                                                                             # エラーが起きた場合に備えて例外処理
                print(f"⚠️ 読み込み失敗: {fname}, 理由: {e}")
    return np.array(images), np.array(scores)                                                                  # Numpy配列として返す
 
 # === カスタム損失関数 ===
 @register_keras_serializable()
 def custom_penalizing_loss(y_true, y_pred):                                                                   # 予測値と正解値の差に基づく損失を定義
     diff = tf.abs(y_true - y_pred)                                                                            # 絶対誤差を計算
     return tf.reduce_mean(tf.square(diff) * (1.0 + tf.pow(diff, 1.5)))                                        # 誤差が大きいほどペナルティを強くする(差が大きいサンプルを重点的に修正)
                                                                                                               # 通常のMSEよりも大誤差を強調し、外れ値を抑制する効果
 
 # === CNNモデル構築関数 ===
 def create_model(version):
     input_img = Input(shape=(*IMAGE_SIZE, 3))                                                                 # 入力画像サイズを定義(256×256×3)
 
     # CNNモデルごとに異なるベースアーキテクチャを使用
     if version == 0:
         base = DenseNet121(include_top=False, weights="imagenet", input_tensor=input_img)                     # DenseNet121を利用
     elif version == 1:
         base = InceptionResNetV2(include_top=False, weights="imagenet", input_tensor=input_img)               # InceptionResNetV2を利用
     elif version == 2:
         base = Xception(include_top=False, weights="imagenet", input_tensor=input_img)                        # Xceptionを利用
                                                                                                               
     base.trainable = False                                                                                    # 転移学習:ベースモデルの重みを固定して学習させない
 
     # グローバルプーリングで特徴マップを1次元に圧縮
     x = base.output
     x = layers.GlobalAveragePooling2D()(x)
 
     # 全結合層(Dense)を追加して非線形特徴を学習
     for units, dr in [(2048, 0.5), (1024, 0.4), (512, 0.3), (256, 0.2), (128, 0.15), (64, 0.1)]:
         x = layers.Dense(units, activation="relu")(x)
         x = layers.BatchNormalization()(x)
         x = layers.Dropout(dr)(x)
     output = layers.Dense(1)(x)
 #-----------------------展開すると-----------------------↓
    x = layers.Dense(2048, activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Dropout(0.5)(x)  # 過学習防止
 
    x = layers.Dense(1024, activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Dropout(0.4)(x)
 
    x = layers.Dense(512, activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Dropout(0.3)(x)
 
    x = layers.Dense(256, activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Dropout(0.2)(x)
 
    x = layers.Dense(128, activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Dropout(0.15)(x)
 
    x = layers.Dense(64, activation='relu')(x)
    x = layers.Dropout(0.1)(x)
 
    output = layers.Dense(1)(x)  # 最終出力は1次元の連続値(錆のスコア)
 #---------------------------------------------------------------
    model = Model(inputs=input_img, outputs=output)                                                             # モデルを構築
    model.compile(optimizer='adam', loss=custom_penalizing_loss, metrics=['mae'])                               # 学習設定
    return model                                                                                                # 完成したモデルを返す
 
 # === 錆特徴量抽出関数 ===
 def extract_features(image, use_color=False):                                                                  # 錆の特徴量を抽出(面積・数・最大錆サイズなど)
     gray = np.array(image.convert("L"))                                                                        # 画像をグレースケールに変換
 
     if use_color: #錆画像をカラーで学習させた場合
         hsv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2HSV)                                                 # HSV空間に変換(錆の色抽出用)
         lower_rust = np.array([5, 50, 50])                                                                     # 錆の色の下限値(橙〜茶)
         upper_rust = np.array([25, 255, 255])                                                                  # 錆の色の上限値
         mask = cv2.inRange(hsv, lower_rust, upper_rust)                                                        # 錆領域のマスク生成
     else: #錆画像を白黒で学習させた場合
         _, mask = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV)                                         # 白黒画像の場合、128を閾値にして錆部分(暗い部分)を抽出
 
     rust_ratio = np.sum(mask > 0) / mask.size                                                                  # 錆が占める割合(面積比)
     contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)                           # 輪郭を抽出(錆の個数などを把握)
     rust_count = len(contours)  # 錆の数
     max_blob_area = max([cv2.contourArea(c) for c in contours], default=0)                                     # 最大の錆面積
 
     return rust_ratio, rust_count, max_blob_area                                                               # 錆割合・錆数・最大錆サイズを返す
 
 # === CNNの回帰スコアに基づく補正関数 ===(今回は未使用)
 def adjust_score_by_features(score, rust_ratio, rust_count, max_blob_area):  
     correction = 0.0                                                                                           # 補正値の初期化
 
     if score >= 4.5:
         return score                                                                                           # 高スコア(重度錆)は補正しない
 
     # 錆面積割合に基づく補正
     if rust_ratio >= 0.60:                                                                                     # 錆の割合が60%以上だった時
         correction -= 0.5                                                                                      # 錆が非常に多い → 学習によって出力された評点から0.5点マイナス
     elif 0.45 <= rust_ratio < 0.60:
         correction -= 0.3
     elif 0.25 <= rust_ratio < 0.45:
         correction -= 0.1
     elif rust_ratio < 0.05:
         correction += 0.3                                                                                      # 錆が少ない → 学習によって出力された評点から0.3点プラス
 
     # 錆の個数による補正
     if rust_count >= 1000:                                                                                     #錆の数が1000個以上の場合
         correction -= 0.2                                                                                      #学習によって出力された評点から0.2点マイナス
     elif 500 <= rust_count < 1000:
         correction -= 0.1
     elif rust_count < 100:
         correction += 0.1
 
     # 最大錆サイズによる補正
     if max_blob_area >= 15000:                                                                                 #最大錆の大きさが15000ピクセル以上の場合
         correction -= 0.4                                                                                      #学習によって出力された評点から0.4点マイナス
     elif 8000 <= max_blob_area < 15000:
         correction -= 0.2
     elif max_blob_area < 100:
         correction += 0.2
 
     return score + correction                                                                                  # 補正後スコアを返す
 
 # === スコアを1〜5の離散値に変換 ===(binning関数の設定)
 def binning(score):
     if score < 1.9: return 1
     elif score < 2.6: return 2
     elif score < 3.3: return 3
     elif score < 4.0: return 4
     else: return 5                                                                                             # CNNの出力は連続値(回帰)だが、最終的に1〜5の評点に変換して分類評価する
                                                                                                                
 # === 実験を実行するメイン関数 ===
 def run_experiment(pattern, run_id):                                                                           # pattern: 各学習条件の設定情報(辞書形式)
     print(f"\n===== 実験 {run_id}: {pattern['name']} 開始 =====")
 
     # 出力ディレクトリを作成
     output_dir = os.path.join(BASE_RESULTS_DIR, pattern["name"])
     if os.path.exists(output_dir):
         shutil.rmtree(output_dir)
     os.makedirs(output_dir, exist_ok=True)
     for i in range(1, 6):
         os.makedirs(os.path.join(output_dir, f"score{i}"), exist_ok=True)
 
     # 学習データの読み込み
     x, y = load_images_and_scores(TRAIN_FOLDER)
     total_labeled = len(y)                                                                                      # ラベル付き画像の総数
     x_train, x_val, y_train, y_val = train_test_split(x, y, test_size=0.2, random_state=42)                     # ラベル付き錆画像を学習用80%、検証用20%に分割
 
     # === モデルの学習 or 読み込み ===
     models_ensemble = []                                                                                        # アンサンブルモデルを格納するリスト
     for i in range(NUM_MODELS):
         model_path = os.path.join(output_dir, f"ensemble_model_{i+1}.keras")
         if os.path.exists(model_path):                                                                          # 既に学習済みモデルがあれば読み込み
             model = tf.keras.models.load_model(model_path, custom_objects={"custom_penalizing_loss": custom_penalizing_loss})
 
         else:# 学習済みモデルが無ければ新規作成
             model = create_model(i)                                                                             # 新規モデル作成
             callbacks = [
                 EarlyStopping(patience=5, restore_best_weights=True),                                           # 5エポック改善がなければ早期終了
                 ModelCheckpoint(model_path, save_best_only=True)                                                # 最良モデルを保存
             ]
             model.fit(                                                                                          # 学習の実行 予め設定しておいたパラメータを呼び出す
                 x_train, y_train,
                 epochs=pattern["epochs"],
                 batch_size=pattern["batch_size"],
                 validation_data=(x_val, y_val),
                 callbacks=callbacks,
                 verbose=2
             )
             model.save(model_path)
         models_ensemble.append(model)                                                                           # アンサンブルリストに追加
 
     # === 評価対象(テスト画像)を処理 ===
     test_fnames = sorted([f for f in os.listdir(TEST_FOLDER) if f.endswith(".png") or f.endswith(".jpg")])
     results = []                                                                                                # 結果を格納するリスト
 
     for fname in test_fnames:
         path = os.path.join(TEST_FOLDER, fname)
         img = Image.open(path).convert("RGB").resize(IMAGE_SIZE)
         x_input = np.array(img) / 255.0
         x_input = x_input.reshape(1, *IMAGE_SIZE, 3)
 
         # 各モデルの予測を平均化(=アンサンブル)
         preds = [m.predict(x_input, verbose=0)[0][0] for m in models_ensemble]
         avg_score = np.mean(preds)
 
         # 錆特徴量で補正
         rust_ratio, rust_count, max_blob_area = extract_features(img, use_color=USE_COLOR)
         adjusted_score = adjust_score_by_features(avg_score, rust_ratio, rust_count, max_blob_area)
 
         # 範囲外スコアはスキップ
         if adjusted_score < 0.00 or adjusted_score > 5.50:
             print(f"⚠️ スコア範囲外: {fname} → {adjusted_score:.2f} → スキップ")
             continue
 
         final_score = binning(adjusted_score)                                                                    # 回帰値を評点(1〜5)に変換
         save_path = os.path.join(output_dir, f"score{final_score}", fname)
         img.save(save_path)                                                                                      # 評点ごとのフォルダに分類して保存
 
         # 結果を記録(CSV出力用)
         results.append({
             "ファイル名": fname,
             "回帰スコア": round(avg_score, 3),
             "補正後スコア": round(adjusted_score, 3),
             "評点": final_score,
             "錆割合": round(rust_ratio, 3),
             "錆の数": rust_count,
             "錆の最大サイズ": round(max_blob_area, 1),
         })
 
     # === 予測結果CSVを保存 ===
     pd.DataFrame(results).to_csv(os.path.join(output_dir, "rust_score_with_features.csv"), index=False, encoding="utf-8-sig")
                                                                                                                   
    # === 評価指標の算出 ===
    val_preds = [m.predict(x_val, verbose=0).flatten() for m in models_ensemble]                                   # 検証データに対して、全モデルの出力をそれぞれ取得
    y_val_pred = np.mean(val_preds, axis=0)                                                                        # アンサンブル:3つのモデルの出力を平均化して最終予測値とする
 
    # 予測スコアを1〜5のクラスラベルに変換(分類評価用)
    y_val_bin = np.array([binning(s) for s in y_val_pred])  
    y_true_bin = np.array([binning(s) for s in y_val])
 
    # === MAE(平均絶対誤差) ===
    mae_score = mean_absolute_error(y_val, y_val_pred)
    print(f"MAE:{mae_score}")                                                                                      # 数値予測の精度指標。小さいほど予測が正確。
    
    # === Accuracy(単純精度) ===
    from sklearn.metrics import accuracy_score, f1_score                                                           # ここで明示的に追加
    acc_score = accuracy_score(y_true_bin, y_val_bin)                                                              # クラス分類として正解率を算出
    
    # === Macro平均F1スコア ===
    macro_f1 = f1_score(y_true_bin, y_val_bin, average="macro")                                                    # 各クラスのF1スコアを平均した値(不均衡データへの耐性あり)
    
    # === 混同行列の保存(どのクラスを誤分類したか可視化) ===
    pattern_dir = os.path.join("results_patterns", pattern["name"])                                                # パターンごとの保存フォルダを指定
    os.makedirs(pattern_dir, exist_ok=True)                                                                        # フォルダがなければ作成
 
    cm = confusion_matrix(y_true_bin, y_val_bin, labels=[1, 2, 3, 4, 5])                                           # 1〜5評点ごとの混同行列を生成
    plt.figure(figsize=(10.8, 7.2))                                                                                # 図のサイズを設定
    disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=[1, 2, 3, 4, 5])
    disp.plot(cmap="Blues", values_format="d")                                                                     # 青色系で可視化
    plt.title("Confusion Matrix (Validation Set)")                                                                 # タイトル
    plt.savefig(os.path.join(output_dir, "confusion_matrix.png"))                                                  # 画像として保存
    plt.close()
 
    # === UMAP可視化 ===
    feature_model = Model(inputs=models_ensemble[0].input, outputs=models_ensemble[0].get_layer(index=-3).output) # 高次元特徴を2次元に圧縮して分布を可視化
    features_val = feature_model.predict(x_val, verbose=0)                                                         # 検証データの特徴を抽出 
                                                                                                                   # CNNモデルの最終層手前の特徴ベクトルを抽出
    umap = UMAP(n_neighbors=15, min_dist=0.1, random_state=42)                                                     # 近傍15点を基に2次元に埋め込み。min_dist=0.1で局所構造を保つ
    embedding = umap.fit_transform(features_val)                                                                   # UMAP変換実行
 
    plt.figure(figsize=(10.8, 7.2))
    scatter = plt.scatter(embedding[:, 0], embedding[:, 1], c=y_true_bin, cmap="coolwarm", s=30, alpha=0.8)
    plt.colorbar(scatter, ticks=[1, 2, 3, 4, 5])                                                                   # カラーバーを追加
    plt.title("UMAP Visualization of Validation Features")                                                         # タイトル
    plt.savefig(os.path.join(output_dir, "umap_val.png"))                                                          # 図を保存
    plt.close()
 
    # === メモリ使用量の可視化(強制終了の原因調査用) ===
    vm = psutil.virtual_memory()                                                                                   # システム全体のメモリ情報を取得
    total_gb = vm.total / (1024 ** 3)                                                                              # 総メモリ容量(GB単位)
    used_gb = (vm.total - vm.available) / (1024 ** 3)                                                              # 使用中メモリ量(GB単位)
    percent = vm.percent                                                                                           # 使用率(%)
    print(f"💾 System Memory usage after {pattern['name']}: {used_gb:.2f} GB / {total_gb:.2f} GB ({percent}%)")    # ターミナル上でメモリ状況を表示(メモリ不足での強制終了対策)
                                                                                                                   
    # === クラス別MAEと±1精度 ===
    def per_class_mae(y_true, y_pred, binning_fn):
        y_true_bin = np.array([binning_fn(v) for v in y_true])
        y_pred_bin = np.array([binning_fn(v) for v in y_pred])
        res = {}
        for c in np.unique(y_true_bin):
            mask = (y_true_bin == c)
            if mask.sum() > 0:
                res[int(c)] = mean_absolute_error(y_true[mask], y_pred[mask])
        return res
 
    def within_one_rate(y_true, y_pred, binning_fn):
        y_t = np.array([binning_fn(v) for v in y_true])
        y_p = np.array([binning_fn(v) for v in y_pred])
        return (np.abs(y_t - y_p) <= 1).mean()                                                                     # ±1段階以内に収まる割合
                                                                                                                   
    class_mae = per_class_mae(y_val, y_val_pred, binning)
    one_acc = within_one_rate(y_val, y_val_pred, binning)
    print("クラス別MAE:", class_mae)
    print("±1精度:", one_acc)
                                                                                                                   
    # === 残差プロットと誤差分布 ===
    res = y_val_pred - y_val                                                                                       # 残差(予測−実測)
    plt.scatter(y_val, y_val_pred, s=6, alpha=0.6)
    plt.plot([0, 5], [0, 5], 'r--')                                                                                # 完全一致ライン
    plt.xlabel("True"); plt.ylabel("Predicted")
    plt.savefig(os.path.join(output_dir, "true_vs_pred.png"))
    plt.close()
 
    plt.hist(res, bins=40, color="gray")
    plt.title("Residuals")
    plt.savefig(os.path.join(output_dir, "residuals.png"))
    plt.close()
                                                                                                                   
    # === モデル間誤差の相関(アンサンブル効果を定量化) ===
    if len(val_preds) >= 2:
        from scipy.stats import pearsonr                                                                           # ピアソン相関係数
        errors = [np.abs(y_val - p) for p in val_preds]
        corr = np.zeros((len(errors), len(errors)))                                                                # 相関行列
        for i in range(len(errors)):
            for j in range(len(errors)):
                corr[i, j], _ = pearsonr(errors[i], errors[j])
        pd.DataFrame(corr).to_csv(os.path.join(output_dir, "error_corr.csv"), index=False)
        print("誤差相関行列を保存しました")
                                                                                                                   
 # === モデル・変数を明示的に解放 ===
    del models_ensemble                                                                                            # モデルリストを削除
    del x_train, x_val, y_train, y_val                                                                             # データも削除
    tf.keras.backend.clear_session()                                                                               # TensorFlowセッションをリセット
    gc.collect()                                                                                                   # Pythonのガーベジコレクションを実行
    print(f"===== 実験 {run_id}: {pattern['name']} 完了 =====\n")
 
    # === モデル・データを完全解放 ===
    del models_ensemble
    del x_train, x_val, y_train, y_val
    tf.keras.backend.clear_session()
    gc.collect()
    print(f"===== 実験 {run_id}: {pattern['name']} 完了 =====\n")
                                                                                                                   
 # === 実験条件と評価結果の記録 ===
    summary = {
        "name": pattern["name"],                                                                                   # 学習パターン名
        "epochs": pattern["epochs"],                                                                               # エポック数
        "batch_size": pattern["batch_size"],                                                                       # バッチサイズ
        "num_aug": pattern["num_aug"],                                                                             # 水増し枚数
        "rotation": pattern["rotation"],                                                                           # 回転角度
        "brightness": str(pattern["brightness"]),                                                                  # 明るさ(tupleを文字列化)
        "width_shift": str(pattern["width_shift"]),                                                                # 横移動範囲
        "height_shift": str(pattern["height_shift"]),                                                              # 縦移動範囲
        "total_train_samples": total_labeled * pattern["num_aug"],                                                 # 水増し後の画像枚数
        "mae": mae_score,                                                                                          # 平均絶対誤差
        "accuracy": acc_score,                                                                                     # 正答率
        "macro_f1": macro_f1,                                                                                      # Macro平均F1スコア
        "class_mae": str(class_mae),                                                                               # 評点別のmae表示
        "within_one_acc": one_acc,                                                                                 # ±1精度
        "datetime": str(datetime.datetime.now())                                                                   # 実行終了時間を記録
    }
    pd.Series(summary).to_csv(os.path.join(output_dir, "summary.csv"))                                             # この学習条件の情報をCSVに保存
 
    return summary                                                                                                 # 実験結果の概要を返す
 
 # === メイン処理 ===
 if __name__ == "__main__":
     all_results = []                                                                                              # 全実験の結果をまとめるリスト
 
     for idx, pat in enumerate(patterns):                                                                          # 各パターンを順に実行
         summary = run_experiment(pat, idx)
         all_results.append(summary)
 
     # === 全実験の集計結果をCSVで保存 ===
     df_all = pd.DataFrame(all_results)
     df_all.to_csv(os.path.join(BASE_RESULTS_DIR, "all_results.csv"), index=False, encoding="utf-8-sig")
   print(f"✅ 全パターンの集計結果を {os.path.join(BASE_RESULTS_DIR, 'all_results.csv')} に保存しました。")

--上記のスクリプトを実行すると以下のファイルが保存される


 ・results_patterns1-2   # 各学習パターンの結果をまとめたフォルダ。どの条件でどんな結果が出たかを一覧で追えるよう整理されている。
    |
    |__ pattern_1   # 1つの学習パターン(=特定の学習条件セット)の出力結果で、この中にそのパターンで作成されたモデルや評価結果が保存されている。 
    |         |
    |         |__ ensemble_model_1.keras, ensemble_model_2.keras, ...  # 各アンサンブルモデルの学習済みウェイトファイル
    |          |                                             # CNNモデルが学習を終えた状態で保存されており、 
    |          |                                                          # .keras 形式は TensorFlow の新しい保存方式で再学習・再評価・転移学習などに再利用できる。
    |          |                                             # ensemble_model_1〜3 はそれぞれ異なる初期値で学習し、後で平均化してアンサンブルを構成。
    |         |
    |         |_ confusion_matrix.png                                   # 混同行列(分類の正誤マトリクス)
    |         |                                             # 評点(1〜5)の分類結果と実際の正解を比較した図で、対角線上に多く分布していれば正解率が高い。
    |           |                                   # どの評点を誤って別の評点として判断したのかが直感的に理解できるようになっている。
    |           | 
    |         |_ error_corr.csv                                         # 各CNNモデル同士の 誤差の相関行列 値が1.0に近いほどモデルの誤差傾向が似ている(=多様性が低い)ことを意味し、
    |         |                                                          # アンサンブル効果を上げるにはこの相関を下げることが重要。
    |          | 
    |         |_ residuals.png                                          # 予測誤差(残差)分布を表すヒストグラム
    |         |                                 # 横軸は予測値 − 実測値,  縦軸はその誤差が出現した回数を表しており、分布が「左右対称で0付近に集中」していれば良好な学習といえる。
    |          |                                                          # 一方で偏りがある場合は特定の評点を過小・過大評価している可能性を示す。
    |        |_ rust_score_with_features.csv                           # 各テスト画像に対する 推論結果一覧表 機械学習結果+画像解析結果が一緒に記録されている。
    |          | 
    |         |_ summary.csv                                            # そのパターンでの 学習条件と指標の要約 
    |         | 
    |          |_ true_vs_pred.png                                       # 実測スコア(横軸)と予測スコア(縦軸)の散布図 理想は赤い破線(完全一致ライン)上に点が並ぶこと。 
    |          |                                                          # 大きく外れている点は「誤予測」サンプルであり、そこから誤差の原因分析が可能。
    |         | 
    |         |_ umap_val.png                                           # 検証データのCNN特徴量を UMAP により2次元化した可視化図 
    |                                                                       # 点の色は実際のクラス(1〜5)を表しており、クラスごとに明確にまとまっていれば特徴抽出がうまく機能している証拠で
    |                                        # 混ざっている場合はモデルがクラス間を区別できていない可能性あり。
    | 
    |_ pattern_2___ ensemble_model_1.keras, ensemble_model_2.keras, ...
    |            |_ confusion_matrix.png
    |        |_  ...
    |_ ...

-10/15

ラベル付き画像全114枚から1枚あたり50枚水増ししたのち再度水増しを行って学習させた(2回水増し)。2回目の水増しは枚数を変化させることで学習結果に影響を及ぼすのか調べ、1, 2, 3, 5, 10, 15, 20, 30, 40, 50, 60, 70枚とそれぞれ水増しした学習が完了したのでその結果を以下の表に示す。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/kekka.png,1500x);

--MAEに関して250枚までは改善傾向が見られたが、500枚前後で各指標がピークに達した後ほぼ横ばい	ある程度の水増しで汎化性能が向上するがそれ以上では過学習または情報の重複により改善が鈍化した。
続いて正答率は50枚で約0.893と高く、150〜1000枚あたりでやや変動しつつも0.87〜0.89で安定	水増し50枚時点ですでに十分な精度が得られており、過度な水増しは有効性が限定的になる。
F1スコアについて同様に50枚〜2000枚付近で0.83〜0.87の範囲で安定	クラスバランスの維持も良好で極端な枚数ではやや低下傾向。
以上より最も良いバランスはMAE=0.074、正答率=0.895、F1=0.872 の 1500〜2000枚程度水増しが良さそうか。

--評点別MAEの変化
--⇒評点1・2といった重度錆は全体的に誤差が小さく、0.06〜0.09程度で安定  → 「錆が少ない」「初期段階の錆」の分類が安定している
--⇒評点3の中程度の錆は最もデータが多い領域で誤差が最少   → モデルが中央値付近のパターンをよく学習している
--⇒評点4・5の軽度錆はやや誤差が大きい   → 他クラスよりもモデルやバリエーションが少ないからか 水増しによってデータ数が増えても誤差改善は限定的と考える。

---水増しは1枚あたり500〜2000枚が理想
---3000枚を超えると、類似画像の増加や過学習の影響でわずかに精度低下
---2回水増しで水増し1回よりも少ない枚数で良好な結果が得られる安定したモデル学習が実現可能

***10月第3週(10/16〜10/22) [#j417c1016]
-10/16
--.keras ファイルを用いて錆画像の判別を行ってみた。学習元がモノクロであるためカラー錆画像の結果は参考値として見てほしい。補正値, binningはそれぞれ以下の通り

 ・補正値                                    |・binning
 def adjust_score_by_features(score, rust_ratio, rust_count, max_blob_area):     |def binning(score):
     correction = 0.0                                                            |    if score < 1.9: return 1
                                                                                 |    elif score < 2.6: return 2
     if score >= 4.5:                                                            |    elif score < 3.3: return 3
         # 高スコアはそのまま返す(補正・clipなし)                              |    elif score < 4.0: return 4
         return score                                                            |    else: return 5
                                                                                 |
     if rust_ratio >= 0.60:                                                      |
         correction -= 0.5                                                       |
     elif 0.45 <= rust_ratio < 0.60:                                             |
         correction -= 0.3                                                       |
     elif 0.25 <= rust_ratio < 0.45:                                             |
         correction -= 0.1                                                       |
     elif rust_ratio < 0.05:                                                     |
         correction += 0.3                                                       |
                                                                                 |
     if rust_count >= 1000:                                                      |
         correction -= 0.2                                                       |
     elif 500 <= rust_count < 1000:                                              |
         correction -= 0.1                                                       |
     elif rust_count < 100:                                                      |
         correction += 0.1                                                       |
                                                                                 |
     if max_blob_area >= 15000:                                                  |
         correction -= 0.4                                                       |
     elif 8000 <= max_blob_area < 15000:                                         |
         correction -= 0.2                                                       |
     elif max_blob_area < 100:                                                   |
         correction += 0.2                                                       |
                                                                                 |
     return score + correction                                                   |

--画像の名前について
--→頭の数字が評点になっている(例:1-111 ⇒ 評点1, 2-201 ⇒ 評点2)またハイフンの後の数字で区分を行っており、0番台がカラー画像, 10番台がモノクロ画像になっている。

--使用画像(カラー 左から評点1, 2, ... 5の順で並んでいる)


&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/1.png,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/2.png,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/3.png,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/4.png,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/5.png,300x);

--使用画像(モノクロ 左から評点1, 2, ... 5の順で並んでいる 但し評点2は2枚)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/11.jpg,250x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/12.jpg,250x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/22.jpg,250x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/13.jpg,250x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/14.jpg,250x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251015/15.jpg,250x);



--判別結果
, 画像 , 補正前評点 , 補正後評点 , 回帰スコア , 補正後スコア , 錆割合 , 錆数 , 最大錆 
, 1-01.png , 3 , 2 , 3.102 , 2.302 , 0.719 , 33 , 87309.0 
, 2-01.png , 3 , 2 , 3.151 , 2.451 , 0.379 , 1480 , 20794.0 
, 3-01.png , 3 , 2 , 3.196 , 2.396 , 0.817 , 12 , 88528.0 
, 4-01.png , 4 , 3 , 3.455 , 2.655 , 0.914 , 1 , 89187.5 
, 5-01.png , 3 , 2 , 3.105 , 2.305 , 0.953 , 2 , 89268.5 
, 1-11.jpg , 1 , 1 , 0.967 , 0.467 , 0.359 , 359 , 19832.0 
, 2-11.jpg , 3 , 3 , 2.762 , 2.662 , 0.274 , 451 , 3808.5 
, 2-12.jpg , 3 , 3 , 2.882 , 2.682 , 0.266 , 722 , 4102.0 
, 3-11.jpg , 4 , 4 , 3.575 , 3.375 , 0.163 , 1348 , 527.5 
, 4-11.jpg , 5 , 5 , 4.933 , 4.933 , 0.042 , 707 , 236.5 
, 5-11.jpg , 5 , 5 , 4.936 , 4.936 , 0.005 , 178 , 153.5 

補正関数を組み込んでも評点が変化することはあまりないことが分かったため、今後補正関数の導入を見送ることとする。ただし錆割合 , 錆数 , 最大錆 の抽出に関しては上手く特徴量を画像から抽出できているか判断する材料となり得るため今後も組み込むこととする。モノクロ画像に関して的中したのは全6枚のうち2枚と良い結果は得られなかった。しかし画像に付けたラベルは目視であり第三者的な目線から評価を行っていないため、ラベルと実際の評点が異なる可能性もある。逆に目視評価で評点1〜5付けるよりも学習データをもとに評点を付けたほうが精度が高くあってほしい。

-10/17

現地調査で撮影したカラーの錆画像を台形補正するスクリプトを作成した。だが画像によっては上手く補正できないこともあるのでその原因究明と補正成功率の向上を20日以降に行う。補正の詳しい内容については明後日以降記載する。

[[台形補正で参考にしたページ:https://qiita.com/otofu_dayo_/items/7299324e976afe051615]]

-10/20

台形変換のスクリプトがとりあえず完成したので貼り付けておく。今後はこれをもとに改良を重ねていくつもり ★マークが付いている行はパラメータを任意で変更可能

 ◯指定フォルダ内の画像に対して「白いプレート枠を検出して可視化画像として保存する処理」の主な流れ
 1,画像読み込み
 2,グレースケール変換と二値化
 3,ノイズ除去(モルフォロジー演算+ラベリング)
 4,最大矩形領域の抽出
 5,結果画像の保存
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------
 import cv2
 import numpy as np
 import os
 import shutil
 from glob import glob
 
 # === 設定 ===
 INPUT_DIR = "drone_images"         # ★入力画像フォルダ
 OUTPUT_DIR = "processed_patches3"  # ★出力フォルダ
 REF_WIDTH = 1000                   # ★出力画像の横サイズ
 REF_HEIGHT = 750                   # ★出力画像の縦サイズ
 
 os.makedirs(OUTPUT_DIR, exist_ok=True)
 
 # === フォルダーの削除・作成 ===
 folder_name = OUTPUT_DIR
 if os.path.exists(folder_name):
     shutil.rmtree(folder_name)
     print(f"既存のフォルダ '{folder_name}' を削除しました。")
 
 os.makedirs(folder_name, exist_ok=True)
 print(f"新しいフォルダ '{folder_name}' を作成しました。")
 
 # === 1. プレート領域のマスク処理 ===
 def mask_plate_region(img, threshold_mode="otsu"):                                   #★しきい値の解釈方法("otsu" または "adaptive")
     """
     撮影画像から白いプレート枠領域を抽出してマスク化する。
     """
     gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)                                     #カラー画像をグレースケール化
                                                                                      #OpenCVでは通常BGR形式なので、cv2.COLOR_BGR2GRAYを指定して輝度情報のみを抽出。
     blur = cv2.GaussianBlur(gray, (5, 5), 0)                                         #★ノイズを抑制しスムーズな二値化を行うためにガウシアンぼかしを適用。
                                                                                      #(ここでの (5,5) を (3,3) や (7,7) などに変更すると、ノイズ除去の度合いが変化)
 
     if threshold_mode == "otsu":
         _, binary = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) #大津の二値化法で明暗を自動的に分離。
     elif threshold_mode == "adaptive":
         binary = cv2.adaptiveThreshold(blur, 255,
                                        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                        cv2.THRESH_BINARY, 11, 2)                     #照明が不均一な場合でも適応的にしきい値を割り当てる方式。
     else:
         raise ValueError("threshold_mode must be 'otsu' or 'adaptive'")              #モード指定以外のパラメータが渡された場合に警告。
 
     # 白黒反転(白枠が白くなるように)
     if np.mean(binary) < 127:
         binary = cv2.bitwise_not(binary)
 
     return binary                                                                    #最終的なマスク画像(二値化画像)を返す。
 
 # === 2. ノイズ除去処理 ===
 def remove_noise(binary_mask):
     """
     二値化画像から小さなノイズを除去し、白枠を明確化する。
     """
     kernel = np.ones((5, 5), np.uint8)                                           #★モルフォロジー演算用の正方カーネルを生成。(小さいと細かく、大きいと滑らかに)
 
     opened = cv2.morphologyEx(binary_mask, cv2.MORPH_OPEN, kernel, iterations=2) #★ Opening:白ノイズ除去(iterations:繰り返し回数。強さを調整できる)
 
     closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=2)     #★ Closing:枠線の途切れ補完(上記と同様)
 
     # ラベリング処理で各領域を集計し、面積500以上の領域だけを残す。プレート以外の小さいブロブは除外。
     num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(closed)
     cleaned = np.zeros_like(closed)
     for i in range(1, num_labels):                                               # 0は背景
         area = stats[i, cv2.CC_STAT_AREA]
         if area > 500:
             cleaned[labels == i] = 255                                         #★ ここの 500 は小さいノイズ領域を除外する閾値で、画像スケールに合わせて調整可能。
 
     return cleaned                                                               #クリーンなマスク画像を返す。
 
 # === 3. 輪郭の取得と近似化 ===
 def detect_plate_contour(img, cleaned_mask):
     """
     白枠プレートの輪郭を検出し、最も大きな矩形を返す。
     """
     contours, _ = cv2.findContours(cleaned_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) #外側の輪郭を抽出。
 
     if not contours:
         print("⚠️ 輪郭が検出されませんでした。")
         return img, None
 
     contours = sorted(contours, key=cv2.contourArea, reverse=True)                           # 面積順にソートして最大の輪郭を選ぶ。
 
     # 最も大きい輪郭を選択
     largest_contour = contours[0]
     epsilon = 0.02 * cv2.arcLength(largest_contour, True)                                    #★ 小さくすると輪郭近似がより正確に、大きくすると単純化される。
     approx = cv2.approxPolyDP(largest_contour, epsilon, True)                                #輪郭を滑らかに近似(多角形化)してノイズを減らす。
 
     # 4頂点に近い輪郭のみ採用し、緑枠で描画し角座標を出力。
     contour_img = img.copy()
     if len(approx) == 4:
         cv2.drawContours(contour_img, [approx], -1, (0, 255, 0), 4)
         pts = approx.reshape(4, 2)
         print(f"🟩 四角形の頂点座標: {pts}")
         return contour_img, pts
     else:
         print(f"⚠️ 4頂点ではありません({len(approx)}点) → スキップ")
         cv2.drawContours(contour_img, [largest_contour], -1, (0, 0, 255), 3)
         return contour_img, None                                                             #矩形でない領域は赤枠で描画しスキップ。
 
 # === 4. メイン処理 ===
 for fname in os.listdir(INPUT_DIR):
     if not fname.lower().endswith((".jpg", ".jpeg", ".png")):
         continue                                               #入力ディレクトリを走査し、画像ファイルのみを処理。
 
     img_path = os.path.join(INPUT_DIR, fname)
     img = cv2.imread(img_path)                                 #画像を読み込み(BGR形式)。
 
     if img is None:
         print(f"⚠️ 読み込み失敗: {fname}")
         continue
 
     mask = mask_plate_region(img, threshold_mode="otsu")       #ステップ1: マスク化 
     cleaned = remove_noise(mask)                               #ステップ2: ノイズ除去
     contour_img, pts = detect_plate_contour(img, cleaned)      #ステップ3: 輪郭検出・近似化
 
     # --- 保存 ---
     save_path = os.path.join(OUTPUT_DIR, f"contour_{fname}")
     cv2.imwrite(save_path, contour_img)
     print(f"✅ 輪郭検出結果を保存しました: {save_path}")


-10/21

--昨日のスクリプトで輪郭の取得と近似化に修正を加えた
 # === 3. 輪郭の取得と近似化 ===
 def detect_plate_contour(img, cleaned_mask):
     """
     白枠プレートの輪郭を検出し、内側スケール矩形のみを抽出。
     外枠(画像外周)は無視。
     条件:
       - 面積が 20000〜1450×1060 の範囲
       - アスペクト比が 1.0〜a の範囲
       - 図形中心が画像の外周b%以内でない
       - 各角の角度が c°〜d° の範囲
     """
     h, w = img.shape[:2]
     max_allowed_area = 1450 * 1060
     min_allowed_area = 20000
 
     # --- ① 外枠除外マスク ---
     inner_mask = np.zeros_like(cleaned_mask)
     margin = int(min(h, w) * 0.12)  # 外周12%を無視
     inner_mask[margin:h - margin, margin:w - margin] = 255
     masked = cv2.bitwise_and(cleaned_mask, inner_mask)
 
     # --- ② 輪郭検出 ---
     contours, _ = cv2.findContours(masked, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
     if not contours:
         print("⚠️ 輪郭が検出されませんでした。")
         return img, None
 
     valid_candidates = []
     for cnt in contours:
         area = cv2.contourArea(cnt)
         if area < min_allowed_area or area > max_allowed_area:
             continue
 
         rect = cv2.minAreaRect(cnt)
         (cx, cy), (rw, rh), angle = rect
 
         # --- ③ 中心位置チェック(外周に寄りすぎていないか) ---
         if cx < margin or cx > (w - margin) or cy < margin or cy > (h - margin):
             continue
 
         # --- ④ アスペクト比チェック ---
         aspect_ratio = max(rw, rh) / (min(rw, rh) + 1e-6)
         if not (1.0 <= aspect_ratio <= 2.5):
             continue
 
         # --- ⑤ 近似と角度判定 ---
         epsilon = 0.02 * cv2.arcLength(cnt, True)
         approx = cv2.approxPolyDP(cnt, epsilon, True)
         if len(approx) != 4:
             continue
 
         pts = approx.reshape(4, 2)
         angles = []
         for i in range(4):
             p1, p2, p3 = pts[i], pts[(i + 1) % 4], pts[(i + 2) % 4]
             v1, v2 = p1 - p2, p3 - p2
             cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6)
             angle = np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
             angles.append(angle)
         if all(69 <= a <= 111 for a in angles):
             valid_candidates.append((area, pts))
 
     # --- ⑥ 最も適した候補を選択 ---
     if not valid_candidates:
         print("⚠️ 有効な内枠が見つかりませんでした。")
         return img, None
 
     best_area, best_pts = max(valid_candidates, key=lambda x: x[0])
     contour_img = img.copy()
     cv2.drawContours(contour_img, [best_pts.astype(int)], -1, (0, 255, 0), 4)
     print(f"🟩 内枠矩形を検出(面積={best_area:.0f}, アスペクト比={aspect_ratio:.2f})")
 
     return contour_img, best_pts
 
上記に示したスクリプトを Scalehosei3.py と定義し撮影したカラーの錆画像からカラースケールの内側枠を抽出・色,補正できる前段階まで持っていくことができた。

-スクリプトの処理内容について

--1, はじめに現地で撮影した錆画像を用意する。多少画面がぶれていても, カラースケールがはみ出していても内枠と色補正に必要なスケールが写っていれば処理可能。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251021/1.jpg,600x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251021/11.jpg,600x);

--2, Scalehosei3.py を実行すると最初に画像の明るい部分は黒, 暗い部分は白と出力される。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251021/2.jpg,600x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251021/12.jpg,600x);

--3, 次に 2 で出力された画像から内枠の認識に影響を及ぼすようなノイズを除去する。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251021/3.jpg,600x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251021/13.jpg,600x);

--4, 3 からカラースケールの内枠を抽出して元の画像に出力&保存。面積, アス比, 外周の除外等様々な条件を付与することで矩形の内枠抽出を全42枚の画像で成功した。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251021/4.jpg,600x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251021/14.jpg,600x);

-10/22

色補正を行う前段階としてカラースケールのカラーを切り抜くまでのスクリプトを作成したので貼り付けておく。

--今後の色補正スクリプトの作成目標
---1, 切り抜いた画像を純色(255)に変換する 変換過程を混同行列で保存
---2, 混同行列をもとに画像全体のRGB値を変換する
---3, 色補正と台形補正をどちらから行うかについては後々考えることとする


 import cv2
 import numpy as np
 import os
 import shutil                                                                                                    # OpenCV・NumPy・OS操作・フォルダ削除を扱うための基本ライブラリをインポート
 
 INPUT_DIR = "processed_patches3"
 OUTPUT_DIR = "processed_patches4"
 os.makedirs(OUTPUT_DIR, exist_ok=True)                                                                           # 入力・出力フォルダの指定 出力フォルダが存在しなければ新規作成
 
 # === フォルダーの削除・作成 ===
 folder_name = OUTPUT_DIR
 if os.path.exists(folder_name):
     shutil.rmtree(folder_name)
     print(f"既存のフォルダ '{folder_name}' を削除しました。")
 
 os.makedirs(folder_name, exist_ok=True)
 print(f"新しいフォルダ '{folder_name}' を作成しました。")
 
 def detect_and_extract_color_scale(img):                                                                         # メイン関数 画像からカラースケール領域(右端の赤・黄・緑・青)を検出して切り抜く。
 
     h, w = img.shape[:2]
     roi = img[:, int(w * 0.65):]                                                                                 # 画像の右側35%を ROI(Region of Interest)として抽出 カラースケールは右端にあるため。
 
     hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)                                                                   # BGR(OpenCV標準)を HSV 色空間に変換することで色の判別がしやすくなる。
 
     color_ranges = {
         'red1': ((0, 100, 100), (10, 255, 255)),
         'red2': ((160, 100, 100), (180, 255, 255)),
         'yellow': ((20, 100, 100), (40, 255, 255)),
         'green': ((40, 50, 50), (90, 255, 255)),                                                                 # HSVで赤・黄・緑・青を検出する範囲を定義。
         'blue': ((90, 50, 50), (150, 255, 255)),                                                                 # HSVにおける色相では赤は0°付近と180°付近の2か所に分かれるため、red1 と red2 の2つの範囲を指定。
     }
 
     mask_red = cv2.bitwise_or(cv2.inRange(hsv, *color_ranges['red1']), cv2.inRange(hsv, *color_ranges['red2']))
     mask_yellow = cv2.inRange(hsv, *color_ranges['yellow'])
     mask_green = cv2.inRange(hsv, *color_ranges['green'])                                                        # 各色(赤・黄・緑・青)の範囲で二値マスク(その色だけ白)を作成。
     mask_blue = cv2.inRange(hsv, *color_ranges['blue'])                                                          # cv2.inRange は指定範囲内のピクセルを255(白)にする。
 
     combined_mask = cv2.bitwise_or(cv2.bitwise_or(mask_red, mask_yellow), cv2.bitwise_or(mask_green, mask_blue)) # 全てのマスクを統合 4色のどれかに該当する部分が白になる。
 
     kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))                                                    # 小さい穴を埋めるモルフォロジー演算(Closing)。
     combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel)                                     # カラースケール領域をより一体的な白領域に整形。
 
     contours, _ = cv2.findContours(combined_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)                    # 白領域の輪郭を抽出。RETR_EXTERNAL なので外側の輪郭のみ取得。
 
     if not contours:
         print("カラースケール領域検出なし")
         return img, None
 
     candidates = []
     for cnt in contours:
         rect = cv2.minAreaRect(cnt)
         (cx, cy), (width, height), angle = rect                                                                  # 各輪郭に外接する最小回転矩形(斜めでもOK)を求める。
 
         if width == 0 or height == 0:
             continue                                                                                             # 面積ゼロのものはスキップ。
 
         aspect_ratio = max(width, height) / min(width, height)                                                   # 縦横比と面積を計算。
         area = width * height                                                                                    # カラースケールのような縦長矩形を選別するために使う。
 
         if area > 0.001 * roi.shape[0] * roi.shape[1] and 3.0 <= aspect_ratio <= 12.0:
             box = cv2.boxPoints(rect)
             box = np.int0(box)                                                                                   # 面積と縦横比の条件を満たす矩形のみを候補として保存。
             candidates.append((area, box, rect))                                                                 # 縦長(アス比3〜12)であることが条件。
 
     if not candidates:
         print("適合するカラースケール候補がありません")
         return img, None
 
     largest_candidate = max(candidates, key=lambda c: c[0])                                                      # 条件を満たす候補リストから最大面積のものを選択
     area, box, rect = largest_candidate                                                                          # 条件を満たした候補の中から最も大きい矩形(カラースケールとみなす)を選択。
 
     box[:,0] += int(w * 0.65)                                                                                    # 元画像座標に変換 → ROIは右端35%だったため、座標を元画像スケールに戻す。
 
     img_with_box = img.copy()
     cv2.drawContours(img_with_box, [box], 0, (0,255,0), 3)                                                       # 検出したカラースケール領域を緑の枠で可視化。
 
     width = int(rect[1][0])
     height = int(rect[1][1])
     if width == 0 or height == 0:
         print("切り出し用サイズ不正")
         return img_with_box, None                                                                                # 切り抜きサイズが0の場合はスキップ。
 
     dst_pts = np.array([[0,0],[width-1,0],[width-1,height-1],[0,height-1]], dtype=np.float32)
     src_pts = box.astype(np.float32)                                                                             # 透視変換行列(台形→長方形)を計算。
     M = cv2.getPerspectiveTransform(src_pts, dst_pts)                                                            # スケールを正面視点で切り抜くため。
 
     extracted = cv2.warpPerspective(img, M, (width, height))
     extracted = cv2.resize(extracted, (height, width))                                                           # 縦横を入れ替えてリサイズ → 台形補正して切り抜き、縦横を整形。
 
     return img_with_box, extracted                                                                               # 枠付き画像(確認用)と抽出したスケール画像を返す。
 
 def process_images(in_dir, out_dir):                                                                             # 複数画像を一括処理する関数。
 
     os.makedirs(out_dir, exist_ok=True)
     for fn in os.listdir(in_dir):
         if fn.lower().endswith(('.png','.jpg','.jpeg','.tif')):                                                  # 対象フォルダ内の画像ファイルをループ処理。
 
             path_in = os.path.join(in_dir, fn)
             path_out = os.path.join(out_dir, fn)
             path_crop = os.path.join(out_dir, "crop_"+fn)                                                        # 入力画像、枠付き画像、切り抜き画像のパスをそれぞれ定義。
 
             img = cv2.imread(path_in)
             if img is None:
                 print(f"{fn} 読み込み失敗")
                 continue
 
             img_boxed, crop_img = detect_and_extract_color_scale(img)                                            # カラースケールの検出・切り抜きを実行。
 
             cv2.imwrite(path_out, img_boxed)
             print(f"{fn} に枠を描画")                                                                            # 枠付き画像を保存。
 
             if crop_img is not None:
                 cv2.imwrite(path_crop, crop_img)
                 print(f"{fn} の切り出し領域を保存")                                                              # 切り抜き画像が得られた場合のみ保存。
 
 if __name__ == "__main__": #メイン実行部。
     process_images(INPUT_DIR, OUTPUT_DIR)                                                                        # スクリプトを直接実行したときにINPUT_DIR内の画像を処理して結果をOUTPUT_DIRに保存。
 
 まとめると
 1, 右側35%領域を「カラースケール候補」として抽出。
 2, HSVで赤・黄・緑・青の色領域を検出。
 3, 条件(面積・縦横比)を満たす最も大きな矩形をカラースケールと判断。
 4, 緑枠で描画して切り抜き画像を保存。

このスクリプトを通じて画像を変換させると元画像(入力画像)から枠付き画像、スケール切り抜き画像がそれぞれ出力される。

-1, 元画像

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/1.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/2.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/3.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/4.jpg,350x);

-2, 枠付き画像

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/11.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/12.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/13.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/14.jpg,350x);

-3, スケール切り抜き画像

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/21.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/22.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/23.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251022/24.jpg,350x);

***10月第4週(10/23〜10/29) [#j417c1023]
-10/23

スクリプトの案内
-Scalehosei1.py ... モノクロ変換
-Scalehosei2.py ... モノクロ変換+ノイズ除去
-Scalehosei3.py ... モノクロ変換+ノイズ除去+台形補正前段階
-Scalehosei4.py ... 色補正前段階・カラースケール抽出
-Scalehosei5.py ... カラースケール補正→変換行列作成・出力
-Scalehosei6.py ... 変換行列で元画像を色補正

とりあえず色補正を行う所までスクリプトを組むことができたが、補正するとどうしても画面が真っ白になってしまう

-10/26

カラースケールを純色にRGBの3×3の行列で変換した後その行列で画像全体の色補正を行った。このすぐ上にある4枚の画像を補正した結果を貼り付けておくが、あくまで作成した補正スクリプトは完成版ではないのでこれから微々たる修正を重ねていくと思う。全体的に画像に赤みがかかっているがどうか こんなのものなのか

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251026/1.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251026/2.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251026/3.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251026/4.jpg,350x);

-10/27

色補正スクリプトの方でRGB毎に影響の強弱を付けられるようにしたが限界があった。緑を純色に近づけようとすると青が黒に近くなり、青を純色にしようとすると緑がシアンに近づいてしまうという状況が発生してしまったのでなにか別のアプローチで補正する必要がありそう。変換行列の作成方法を最小二乗法から非線形多項式補正・ガンマ補正に変えて色補正を行うべきなのか。

-10/29

色補正後の赤みをある程度抑えられ、先に色補正を行ってから錆の枠を抽出して台形変換を行った。カラースケールのカラーだが元々が純色でないため無理やり補正で255と0表示に持っていった結果、補正画像において赤が強く反映されてしまった。なのですべての画像を純色にする必要はないのではとアドバイスをもらった。

--1段目:色補正

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/1.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/2.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/3.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/4.jpg,350x);

--2段目:錆枠抽出

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/11.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/12.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/13.jpg,350x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/14.jpg,350x);

--3段目:台形変換

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/21.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/22.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/23.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251028/24.jpg,350x);

**11月 [#j417c1100]
***11月第1週(10/30〜11/05) [#j417c1030]
-10/30

--旧モデル

, モデル名 , MAE , 正答率 , F1スコア 
, EfficientNetV2S , 0.3029 , 0.7865, 0.7623 
, DenseNet121 , 0.0888 , 0.9912 , 0.9891 
, MobileNetV3Large , 0.1656 , 0.9438 , 0.9409 
, ensemble , 0.1456 , 0.9859 , 0.9827 

--新モデル

, モデル名 , MAE , 正答率 , F1スコア 
, DenseNet121 , 0.0841 , 0.9859 , 0.9819 
, InceptionResNetV2 , 0.1649 , 0.9675 , 0.9651 
, Xception , 0.1215 , 0.9772 , 0.9743 
, ensemble , 0.0988 , 0.9938 , 0.9923 

--新・旧のDenseNet121

, モデル名 , MAE , 正答率 , F1スコア 
, 旧モデルDenseNet121 , 0.0888 , 0.9912 , 0.9891 
, 新モデルDenseNet121 , 0.0841 , 0.9859 , 0.9819 

同じ DenseNet121 を用いているのに結果が微妙に異なるのは学習過程でわずかに異なる条件が発生しているためであり、いくつか要因があると考えられている。

◯主原因1:初期化の乱数

TensorFlow / Keras の重み初期値は乱数で生成されることで同じモデル構造・データでも初期の重みが異なると学習経路も変わるため最終結果が微妙に変化する。特にMAEやF1が小数点3桁単位で異なるのは「乱数初期化」が主な原因とされる。
-対策: スクリプトの先頭で乱数シードを固定(再現性の確保)

◯主原因2:データ分割のランダム性

train_test_split を使うとrandom_state=42 のように固定しない限り訓練データと検証データが毎回異なる。評価指標(MAE, Accuracyなど)は検証セットに依存するため、分割が違うとスコアも変化する。
-対策:
 x_train, x_val, y_train, y_val = train_test_split(x, y, test_size=0.2, random_state=42)   #学習スクリプトでこのように設定する

◯主原因3:アンサンブル全体の重み干渉

アンサンブル処理内でモデル学習順序が異なる,  または他のモデルの出力が同じフォルダに上書きされるなど微妙な環境差があると挙動が変化するが、この差はMAE 0.01未満レベルで収まるケースがほとんど。

--来週行う内容
---白黒のラベル付き錆画像(114枚)を学習させた結果をもとにカラー・ドローンで撮影した錆画像の評点を求める
---カラーとドローンで撮影したものは白黒に変換
---カラーの方は256ピクセル四方に切り抜いた後評点判別にかけられるが、ドローン画像の判別に関しては読み取った画像に対して赤で囲んだこの部分は評点3, 緑で囲んだ部分は評点4のような感じで抽出と判別ができれば理想だがスクリプトで可能なのだろうか。

--やること
---錆画像の切り抜き → 白黒で学習にかける
---ドローン画像 → 枠で囲まれた部分が評点◯というようなスクリプトを組むことを目標にする

-10/31

-- 乱数シード固定(tf.random.set_seedなど)
-- 同じデータセット・分割を使う
-- 同じ前処理(正規化やaugmentation)を維持
-- GPU/CPUなど実行環境も統一

上記の条件下で新・旧モデルのDenseNet121を比較した。

-旧モデル
, モデル名 , MAE , 正答率 , F1スコア 
, EfficientNetV2S , 0.332241619945153 , 0.687170474516696 , 0.664742780762101 
, DenseNet121 , 0.123156605893157 , 0.806678383128295 , 0.782791670207675 
, MobileNetV3Large , 0.19573938891256 , 0.783831282952548 , 0.762552116173173 
, Ensemble , 0.177296200058912 , 0.807557117750439 , 0.779550819835585 


-新モデル
, モデル名 , MAE , 正答率 , F1スコア 
, InceptionResNetV2 , 0.153177455220403 , 0.79701230228471 , 0.759486069791022 
, DenseNet121 , 0.147729756383773 , 0.794376098418278 , 0.766856817460948 
, Xception , 0.142235872050986 , 0.808435852372584 , 0.778707125381728 
, Ensemble , 0.12868636939779 , 0.812829525483304 , 0.783772204597505 


-DenseNet121
, モデル名 , MAE , 正答率 , F1スコア 
, 旧モデルDenseNet121 , 0.123156605893157 , 0.806678383128295 , 0.782791670207675 
, 新モデルDenseNet121 , 0.147729756383773 , 0.794376098418278 , 0.766856817460948 
, (新・旧)の差 , 0.024573150490616 , -0.0123022847100169 , -0.015934852746727 


両学習モデルで乱数シードを固定し学習条件を統一した上で訓練を実施した。しかし同一スクリプト・同一データセット・同一ハイパーパラメータを用いても、DenseNet121の評価結果が完全には一致しなかった。原因として考えられるのがTensorFlowおよびKerasにおける内部処理の一部が非決定的(non-deterministic)な演算を含むことに起因する。以下の要素が主な原因である。

◯GPU演算の非決定性:CuDNN(NVIDIAのディープラーニングライブラリ)は高速化のため一部の畳み込み演算(特にConv2DやBatchNormalization)において非決定的なアルゴリズムを選択することがある。そのため、同じ乱数シードを設定してもGPU上での学習結果にわずかな差異が生じる可能性がある。

◯os, numpy, tensorflow のランダム初期化:スクリプトでは set_global_determinism関数によってPython, NumPy, TensorFlowの乱数シードを固定しているが、モデルの初期重み(特にImageNet事前学習済みモデルのロード時)には依然として一部乱数依存の初期化ステップが残る場合がある。

◯並列スレッドおよびGPUメモリスケジューリング:マルチスレッド環境ではスレッドの実行順序や演算スケジューリングが完全には制御できないため、浮動小数点誤差の蓄積順序が異なることで最終的なモデル出力に微小な差異が生じる。

◯EarlyStoppingとModelCheckpointの影響:各エポック終了時の検証損失に基づいてモデルが保存されるが、非決定的な誤差伝播によりわずかに異なるタイミングで「最良モデル」と判定されるケースがある。

-つまり同一環境・同一コードで学習を行ってもGPU演算の非決定性と浮動小数点誤差の伝播により再現性に微小な差が生じることで、これらの差が学習過程全体に累積し最終的に MAEを始めとする精度指標で数%程度の差異として現れてしまう。




-11/4

宮城県の橋梁調査データをもらい学習の事前準備を行った

--セロテープ画像 … 1つの画像に何枚も貼ってあったためセロテープ領域を自動検出するスクリプトを組んだ。切り抜きの実行と256ピクセル整形は明日以降に行う。
--近接画像 … 以前作成した台形変換・色補正スクリプトで枠の内側を切り抜く。これも明日以降に行う。


***11月第2週(11/6〜11/12) [#j417c1106]
-11/6

色補正・台形変換に関する各スクリプトに変更を加えたので以下に示す スクリプトの実行手順はScalehosei○.pyの○が順番を表している。

-Scalehosei1.py

 import cv2                                                                                                          #OpenCV(画像処理用ライブラリ)をインポート
 import numpy as np                                                                                                  #NumPy(数値計算用ライブラリ)をインポート
 import os                                                                                                           #OS関連の操作(ファイルパスなど)用ライブラリをインポート
 import shutil                                                                                                       #フォルダ削除などの高機能ファイル操作を行う標準ライブラリをインポート
 
 INPUT_DIR = "drone_images"                                                                                          #入力画像を格納したフォルダ名を指定
 OUTPUT_DIR = "processed_patches1"                                                                                   #処理後のファイル保存フォルダ名を指定
 os.makedirs(OUTPUT_DIR, exist_ok=True)                                                                              #出力フォルダがなければ新規作成
 
 # === フォルダーの削除・作成 ===
 folder_name = OUTPUT_DIR
 if os.path.exists(folder_name):
     shutil.rmtree(folder_name)                                                                                      #既存フォルダがあれば再帰的に中身ごと削除
     print(f"既存のフォルダ '{folder_name}' を削除しました。")
 
 os.makedirs(folder_name, exist_ok=True)                                                                             #新たに出力フォルダを再作成
 print(f"新しいフォルダ '{folder_name}' を作成しました。")
 
 # === 拡張子統一 (.JPG, .JPEG → .jpg) ===
 def unify_extensions(folder):                                                                                       #指定フォルダ内の画像拡張子を統一する関数
     for fname in os.listdir(folder):                                                                                #中のファイル名を走査
         old_path = os.path.join(folder, fname)                                                                      #フルパスを作成
         if not os.path.isfile(old_path):                                                                            #サブフォルダ等はスキップ
             continue
         base, ext = os.path.splitext(fname)                                                                         #ファイル名と拡張子を小文字分離
         ext_lower = ext.lower() 
         if ext_lower in [".jpeg", ".jpg", ".png", ".tif"]:                                                          #対象拡張子か判定
             new_path = os.path.join(folder, base + ".jpg")                                                          #新しいファイル名を「.jpg」で作成
             if new_path != old_path:                                                                                #名前が違えばリネーム実行
                 os.rename(old_path, new_path)
                 print(f"🔄 拡張子統一: {fname} → {os.path.basename(new_path)}")
 
 unify_extensions(INPUT_DIR)                                                                                         #入力フォルダ内拡張子を一括変換
 
 def detect_and_extract_color_scale(img):                                                                            #画像からカラースケール部分を抽出し、枠付き画像と抽出画像を返す
     h, w = img.shape[:2]
     roi = img[:, int(w * 0.65):]                                                                                    #画像右側(横65%より右)領域をROIとして選択
 
     hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)                                                                      #色判別用にHSV色空間に変換
     
     color_ranges = {                                                                                                #「赤・黄・緑・青」検出用の閾値(範囲)を辞書で定義
         'red1': ((0, 120, 80), (10, 255, 255)),
         'red2': ((160, 120, 80), (180, 255, 255)),
         'yellow': ((20, 150, 150), (35, 255, 255)),                                                                 # 白っぽい領域除外
         'green': ((45, 80, 80), (85, 255, 255)),
         'blue': ((90, 80, 80), (130, 255, 255)),
     }
 
     mask_red = cv2.bitwise_or(cv2.inRange(hsv, *color_ranges['red1']), cv2.inRange(hsv, *color_ranges['red2']))
     mask_yellow = cv2.inRange(hsv, *color_ranges['yellow'])
     mask_green = cv2.inRange(hsv, *color_ranges['green'])
     mask_blue = cv2.inRange(hsv, *color_ranges['blue'])                                                             #各色範囲についてヒストグラムマスク作成
     
     combined_mask = cv2.bitwise_or(cv2.bitwise_or(mask_red, mask_yellow), cv2.bitwise_or(mask_green, mask_blue))    #各マスクを結合し、カラースケール全域を一括検出
     
     # --- ② ノイズ除去・スムージング ---
     kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))                                                       #輪郭強調・ノイズ除去用カーネルを生成
     combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_OPEN, kernel)
     combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel)                                        #マスク画像に「開」「閉」処理でノイズ除去+領域平滑化
     
     contours, _ = cv2.findContours(combined_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)                       #領域の輪郭情報抽出
     candidates = []                                                                                                 #候補リスト準備
 
     for cnt in contours:                                                                                            #各輪郭それぞれについて検査
         rect = cv2.minAreaRect(cnt)                                                                                 #最小外接矩形のパラメータ取得
         (cx, cy), (width, height), angle = rect
         if width == 0 or height == 0:
             continue
         
         aspect_ratio = max(width, height) / (min(width, height) + 1e-6)
         area = width * height                                                                                       #アスペクト比や面積などで「縦長・サイズ条件」を満たす矩形だけ候補に採用。
         
         # --- ③ 縦長・面積条件 ---
         if area > 0.005 * roi.shape[0] * roi.shape[1] and 3.5 <= aspect_ratio <= 12.0:
             box = cv2.boxPoints(rect)
             box = np.int0(box)
             candidates.append((area, box, rect))
     
     # === 自動検出失敗時の手動選択 ===
     if not candidates:                                                                                              #候補がない場合は手動選択モードへ
         print("⚠️ 適合するカラースケール候補がありません。手動で4点を選択してください。")
 
         manual_points = []                                                                                          #マウスクリック位置を格納するリスト
 
  #以下「右クリックで追加・左クリックで取り消し」というインタラクション処理    4点選択完了で射影変換して切り出し抽出画像を返却(失敗時はスキップ)
         def mouse_callback(event, x, y, flags, param):
             if event == cv2.EVENT_RBUTTONDOWN:                                                                      # 右クリックで追加
                 manual_points.append((x, y))
                 cv2.drawMarker(param, (x, y), (0, 255, 0), cv2.MARKER_TILTED_CROSS, 20, 2)
                 cv2.imshow("manual_select", param)
                 print(f"✅ {len(manual_points)}点目 ({x},{y})")
 
             elif event == cv2.EVENT_LBUTTONDOWN:                                                                    # 左クリックで削除
                 if manual_points:
                     manual_points.pop()
                     print(f"↩️ 直前の点を削除。残り{len(manual_points)}点。")
                     temp = img.copy()
                     for px, py in manual_points:
                         cv2.drawMarker(temp, (px, py), (0, 255, 0), cv2.MARKER_TILTED_CROSS, 20, 2)
                     cv2.imshow("manual_select", temp)
 
         temp = img.copy()
         cv2.imshow("manual_select", temp)
         cv2.setMouseCallback("manual_select", mouse_callback, temp)
 
         print("🖱️ 右クリックで4点選択、左クリックで直前をキャンセル。完了後にEnterキー。")
         while True:
             key = cv2.waitKey(0)
             if key in [13, 10]:                                                                                      # Enterキーで確定
                 break
         cv2.destroyWindow("manual_select")
 
         if len(manual_points) != 4:
             print("⚠️ 4点が選択されませんでした。スキップします。")
             return img, None
 
         src_pts = np.array(manual_points, dtype=np.float32)
         w_scale = int(np.linalg.norm(src_pts[0] - src_pts[1]))
         h_scale = int(np.linalg.norm(src_pts[1] - src_pts[2]))
         dst_pts = np.array([[0,0],[w_scale-1,0],[w_scale-1,h_scale-1],[0,h_scale-1]], dtype=np.float32)
         M = cv2.getPerspectiveTransform(src_pts, dst_pts)
         extracted = cv2.warpPerspective(img, M, (w_scale, h_scale))
         cv2.destroyAllWindows()
         return img, extracted
 
     # --- 自動検出成功時 ---
     #最も大きい領域を選び元画像上に枠を描画して射影変換で抽出画像作成    必要なら縦横比を調整して返却
 
     area, box, rect = max(candidates, key=lambda c: c[0])
     box[:,0] += int(w * 0.65)
     img_with_box = img.copy()
     cv2.drawContours(img_with_box, [box], 0, (0,255,0), 3)
     
     width = int(rect[1][0])
     height = int(rect[1][1])
     dst_pts = np.array([[0,0],[width-1,0],[width-1,height-1],[0,height-1]], dtype=np.float32)
     src_pts = box.astype(np.float32)
     M = cv2.getPerspectiveTransform(src_pts, dst_pts)
     extracted = cv2.warpPerspective(img, M, (width, height))
     extracted = cv2.resize(extracted, (height, width))                                                                # 縦横を統一
     return img_with_box, extracted
 
 def process_images(in_dir, out_dir):                                                                                  #全画像を一括処理する関数
     os.makedirs(out_dir, exist_ok=True)
     for fn in os.listdir(in_dir):                                                                                     #指定拡張子の画像をループ
         if fn.lower().endswith(('.png','.jpg','.jpeg','.tif','.JPEG')):
             path_in = os.path.join(in_dir, fn)
             path_out = os.path.join(out_dir, fn)
             path_crop = os.path.join(out_dir, "crop_"+fn)
             
             img = cv2.imread(path_in)                                                                                 #画像読み込み
             if img is None:
                 print(f"{fn} 読み込み失敗")
                 continue
             
             img_boxed, crop_img = detect_and_extract_color_scale(img)                                                 #カラースケール検出・抽出
             cv2.imwrite(path_out, img_boxed)                                                                          #元画像へ枠描画したものを保存
             print(f"{fn} に枠を描画")
 
             if crop_img is not None:
                 cv2.imwrite(path_crop, crop_img)                                                             #抽出したカラースケール画像も保存
                 print(f"{fn} の切り出し領域を保存")
 
 if __name__ == "__main__":
     process_images(INPUT_DIR, OUTPUT_DIR)                                                                         #メインとして実行時、定義したフォルダ内のすべての画像を一括処理する
 
 このスクリプトは「ドローン画像」フォルダ内の画像ファイルを処理し特定のカラースケールを自動抽出・保存するもので、画像分類やサンプル収集, データ前処理の自動化に有用な構造となっている。



-Scalehosei2.py

 import cv2
 import numpy as np
 import os
 import shutil
 
 INPUT_DIR = "processed_patches1" 
 OUTPUT_DIR = "processed_patches2"
 MATRIX_DIR = "processed_patches2"
 
 os.makedirs(OUTPUT_DIR, exist_ok=True)
 os.makedirs(MATRIX_DIR, exist_ok=True)
 
 folder_name = OUTPUT_DIR
 if os.path.exists(folder_name):
     shutil.rmtree(folder_name)
     print(f"既存のフォルダ '{folder_name}' を削除しました。")
 
 os.makedirs(folder_name, exist_ok=True)
 print(f"新しいフォルダ '{folder_name}' を作成しました。")
 
 # 純色BGR値(OpenCVはBGR順)
 pure_colors = [     #画像上の標準「ターゲット純色」BGR値
     (0, 0, 255),    # 赤
     (0, 255, 255),  # 黄
     (0, 255, 0),    # 緑
     (255, 0, 0)     # 青
 ]
 
 def create_color_conversion_matrix(img):                                 #入力画像を4分割し、それぞれの平均BGRとターゲット純色を記述した辞書をリストに格納
     h, w = img.shape[:2]                                                 #高さ・幅を取得
     conversion_matrix = [] 
 
     if h > w:
         # 縦長:縦方向に4分割(色ごとの帯が縦に配置されている前提)
         segment_h = h // 4
         for i in range(4):
             region = img[i*segment_h:(i+1)*segment_h if i<3 else h, :]
             avg_bgr = region.mean(axis=(0,1))                            #各リージョンごとにregion.mean(axis=(0,1))平均色BGRを求めリスト化
             conversion_matrix.append({
                 'segment': i,
                 'avg_bgr': avg_bgr,
                 'target_color': pure_colors[i]
             })
     else:
         # 横長:横方向に4分割
         segment_w = w // 4
         # 横長は左から [青, 緑, 黄, 赤] のため色配列を逆順にする
         reversed_colors = pure_colors[::-1]
         for i in range(4):
             region = img[:, i*segment_w:(i+1)*segment_w if i<3 else w]
             avg_bgr = region.mean(axis=(0,1))
             conversion_matrix.append({
                 'segment': i,
                 'avg_bgr': avg_bgr,
                 'target_color': reversed_colors[i]
             }) 
 
     return conversion_matrix
 
 def save_conversion_matrix(matrix, filename):                             #np.save()でnpy形式保存
     np.save(filename, matrix)
 
 def apply_pure_colors(img, matrix):                                       #入力画像と変換行列から全ピクセルを純色ターゲットで塗り替えたイメージ画像を生成
     h, w = img.shape[:2]                                                  #区画ごとに指定色で塗り分ける
     result = np.zeros_like(img)
 
     if h > w:
         # 縦長:縦方向に4分割
         segment_h = h // 4
         for entry in matrix:
             i = entry['segment']
             color = entry['target_color']
             start_y = i * segment_h
             end_y = (i + 1) * segment_h if i < 3 else h
             result[start_y:end_y, :] = color
     else:
         # 横長:横方向に4分割
         segment_w = w // 4
         for entry in matrix:
             i = entry['segment']
             color = entry['target_color']
             start_x = i * segment_w
             end_x = (i + 1) * segment_w if i < 3 else w
             result[:, start_x:end_x] = color
 
     return result
 
 #指定ディレクトリ内の全画像を処理
 #ファイルごとに読込→変換行列作成・保存→純色置換画像生成→保存の流れ
 #例外処理も加味
 
 def process_images(input_dir, output_dir, matrix_dir): 
     for fn in os.listdir(input_dir): 
         if not fn.lower().endswith(('.png','.jpg','.jpeg','.tif','.JPEG')): 
             continue
 
         path_in = os.path.join(input_dir, fn)
         img = cv2.imread(path_in)
         if img is None:
             print(f"{fn} 読み込み失敗")
             continue
 
         matrix = create_color_conversion_matrix(img)
         # 拡張子なしのファイル名を取得
         base_name = os.path.splitext(fn)[0]
 
         # .npy保存時には拡張子なしファイル名にする
         save_path = os.path.join(matrix_dir, base_name + ".npy")
         save_conversion_matrix(matrix, save_path)
 
         pure_img = apply_pure_colors(img, matrix)
         path_out = os.path.join(output_dir, fn)
         cv2.imwrite(path_out, pure_img)
 
         print(f"{fn} 処理完了:変換行列保存と純色画像保存")
 
 if __name__ == "__main__":
    process_images(INPUT_DIR, OUTPUT_DIR, MATRIX_DIR) #メインで呼出すことで処理スタート
 
 このスクリプトは抽出したカラースケール画像の各色領域の平均値を求め、「赤・黄・緑・青」の純色に変換するための処理とその変換行列(マトリクス)の保存を行う。
 
 -なぜ色補正に「黄色」を加えたか
 →多くの画像処理・印刷系カラースケールやカラーチャートでは、赤・緑・青(RGB)だけでなく「黄色」も大変重要な指標色として含まれている。
 
 ・黄色は「赤+緑」成分が強調された複合色であり、RGBチャネルだけでは補正しきれない場合や印刷インクやセンサ特性の実用色域を反映するため。
 ・実際のカメラ画像やプリンタ特性, スキャナ入力の現場では、黄方向専用のズレや退色などが発生しやすくこれを補正しないと正確なカラーマッピングができない場合が多い。
 ・黄色チャネルの平均値を併せて色補正に参入することで、「中間色での精度維持」や「RGBのみでは表現できない偏りの補正」が可能に。
 つまり「赤・緑・青」に加えて「黄」も参照することで現実的なタイミング補正やカメラの色再現性を大きく高めることができるため、より正確な色補正を実現するために黄色チャネルも加えることが必要と考えた。

​
-Scalehosei3.py

 import cv2
 import numpy as np
 import os
 import shutil
 
 # === 設定 ===
 SCALE_DIR = "processed_patches2"    # カラースケール抽出済み(crop付き)
 INPUT_DIR = "drone_images"          # 元画像フォルダ
 OUTPUT_DIR = "processed_patches3"   # 補正後出力フォルダ
 
 # 出力フォルダを初期化
 if os.path.exists(OUTPUT_DIR):
     shutil.rmtree(OUTPUT_DIR)
 os.makedirs(OUTPUT_DIR, exist_ok=True)
 print(f"🗂️ 出力フォルダを作成: {OUTPUT_DIR}")
 
 # === 理想の純色(RGB順) ===
 PURE_RGB = np.array([ # 赤、黄、緑、青、それぞれの理想的なRGB値を定義(上から順に対応)
     [255, 0, 0],      # 赤
     [255, 255, 0],    # 黄
     [0, 255, 0],      # 緑
     [0, 0, 255]       # 青
 ], dtype=np.float32)
 
 # === カラースケールの4色平均を取得 ===
 #画像が縦長なら上→下に赤→黄→緑→青、横長なら左→右で青→緑→黄→赤
 #各区画ごとにピクセル平均を計算し、BGR→RGB順に並べてリスト化
 #横長時は配列の順番を赤→黄→緑→青のRGB順に修正
 
 def extract_scale_colors(scale_img): 
     """カラースケール画像から4色の平均色(RGB順)を抽出"""
     h, w = scale_img.shape[:2]
     vertical = h > w
     colors = []
 
     if vertical:
         # 縦長: 上から 赤→黄→緑→青
         step = h // 4
         for i in range(4):
             region = scale_img[i*step:(i+1)*step if i<3 else h, :]
             avg_bgr = region.mean(axis=(0,1))
             avg_rgb = avg_bgr[::-1]  # BGR→RGB
             colors.append(avg_rgb)
     else:
         # 横長: 左から 青→緑→黄→赤
         step = w // 4
         for i in range(4):
             region = scale_img[:, i*step:(i+1)*step if i<3 else w]
             avg_bgr = region.mean(axis=(0,1))
             avg_rgb = avg_bgr[::-1]
             colors.append(avg_rgb)
         # 横長は順序を赤→黄→緑→青に直す
         colors = [colors[3], colors[2], colors[1], colors[0]]
 
     return np.array(colors, dtype=np.float32)
 
 # === 変換行列の計算 ===
 #緑パッチの誤差を重視するために重み配列を生成
 #観測色から理想色への変換を重み付き最小二乗法(線形)で算出
 
 def weighted_least_squares_matrix(observed, pure, green_weight):              # サンプルごとに重み配列を作成
     weights = np.ones(4)
     weights[2] = green_weight
     W = np.diag(weights)    # 重み付き最小二乗: (O^T W O)x = O^T W P
     A = np.linalg.solve(observed.T @ W @ observed, observed.T @ W @ pure)
     return A
 
 # === 行列適用 ===
 # 多項式補正関数(RGB各チャンネルに2次多項式)
 #RGB各チャンネルに対し2次多項式補正(明るさ非線形)を加える
 def polynomial_correction(rgb_flat, coeffs):
     corrected = np.zeros_like(rgb_flat)
     for c in range(3):
         a, b, c0 = coeffs[c]
         corrected[:, c] = a * (rgb_flat[:, c]**2) + b * rgb_flat[:, c] + c0
     return corrected
 
 # 行列適用+多項式+ガンマ補正
 def apply_matrix(img, M, rgb_strength=(1.0, 0.7, 1.0), gamma=(1.0,1.0,1.0), poly_coeffs=[(0,1,0), (0,1,0), (0,1,0)]):
     """
     #入力画像をRGB変換→行列線形補正→強度倍率補正(チャンネル別)→多項式補正→ガンマ補正、の順に適用して最終画像を返す
     #img: 入力画像(BGR)
     #M: 3x3変換行列
     #rgb_strength: チャンネル毎の乗算強度
     #gamma: チャンネル毎のガンマ補正値(べき乗の逆数をかける)
     #poly_coeffs: 各チャンネルの多項式係数(a,b,c)のリスト
     """
     img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32)
     h, w = img_rgb.shape[:2]
     flat = img_rgb.reshape(-1, 3)
 
     # 線形変換
     corrected_flat = np.dot(flat, M)
 
     # 強度補正
     for c in range(3):
         corrected_flat[:, c] *= rgb_strength[c]
 
     # 多項式補正
     corrected_flat = polynomial_correction(corrected_flat, poly_coeffs)
 
     # ガンマ補正(正規化してべき乗)
     for c in range(3):
         # 0-255範囲を0-1に変換して補正、戻す
         corrected_flat[:, c] = np.power(np.clip(corrected_flat[:, c]/255.0, 0, 1), 1.0 / gamma[c]) * 255.0
 
     corrected_flat = np.clip(corrected_flat, 0, 255).astype(np.uint8)
     corrected = corrected_flat.reshape(h, w, 3)
     return cv2.cvtColor(corrected, cv2.COLOR_RGB2BGR)
 
 # === 誤差評価関数 ===
 #4色の各パッチに対し、色差誤差を計算して合計
 #とくに緑のB・R、赤のG・B、黄のR、青のG・Bの項目で誤差を合算
 def multi_patch_error(observed, pure, M, rgb_strength):
     pred = np.dot(observed, M)
     for i in range(4):
         pred[i, 0] *= rgb_strength[0]  # 赤
         pred[i, 1] *= rgb_strength[1]  # 緑
         pred[i, 2] *= rgb_strength[2]  # 青
 
     e_red_G   = np.abs(pred[0, 1] - 0)     
     e_red_B   = np.abs(pred[0, 2] - 0)      
     e_yellow_R = np.abs(pred[1,0] - 255)      
     e_green_R = np.abs(pred[2, 0] - 0)     
     e_green_B = np.abs(pred[2, 2] - 0)        
     e_blue_G  = np.abs(pred[3, 1] - 0)        
     e_blue_B  = np.abs(pred[3, 2] - 255)     
 
     error_total = e_green_B + e_green_R + e_red_G + e_red_B + e_blue_B + e_blue_G + e_yellow_R
     return error_total
 
 best_params = None
 best_err = float('inf')
 
 """
 # グリッド探索範囲
 #グリッド探索範囲として、緑重み・RGB倍率の候補群を用意。
 #最初の元画像1枚のパッチを観測値とし、
 #全ての候補パラメータで「変換→誤差評価」
 #最良パラメータを採用(loop脱出)。
 #採用パラメータを表示。
 """
 
 green_weights = np.arange(5, 81, 1)         # "5"から"80"まで"1"刻みで探索
 red_values   = np.arange(1.2, 1.3, 0.1)     # 1.2固定
 green_values = np.arange(1.3, 1.4, 0.1)     # 1.3固定
 blue_values  = np.arange(1.2, 1.3, 0.1)     # 1.2固定
 
 # カラースケール画像1枚目だけでパラメータ探索
 for fn in os.listdir(INPUT_DIR):
     if not fn.lower().endswith((".jpg", ".jpeg", ".png",'.tif','.JPEG')):
         continue
     base = os.path.splitext(fn)[0]
     scale_path = os.path.join(SCALE_DIR, f"crop_{base}.jpg")
     if not os.path.exists(scale_path):
         continue
     scale_img = cv2.imread(scale_path)
     observed = extract_scale_colors(scale_img)
     pure = PURE_RGB.copy()
     for gw in green_weights:
         for rv in red_values:
             for gv in green_values:
                 for bv in blue_values:
                     rgb_strength = (rv, gv, bv)
                     M = weighted_least_squares_matrix(observed, pure, gw)
                     err = multi_patch_error(observed, pure, M, rgb_strength)
                     if err < best_err:
                         best_err = err
                         best_params = dict(gw=gw, rv=rv, gv=gv, bv=bv, M=M, rgb_strength=rgb_strength)
     break  
 
 print(f"⭐ 最良パラメータ: weight={best_params['gw']}, rgb={best_params['rgb_strength']}")
 M = best_params["M"]
 rgb_strength = best_params["rgb_strength"]
 
 # 画像全体に最適化後の補正を適用
 #フォルダ内の全画像を対象にスケールパッチ画像を抽出し補正式で色補正
 #結果を出力フォルダに保存し進捗を通知
 for fn in os.listdir(INPUT_DIR):
     if not fn.lower().endswith((".jpg", ".jpeg", ".png",'.tif','.JPEG')):
         continue
     base = os.path.splitext(fn)[0]
     scale_path = os.path.join(SCALE_DIR, f"crop_{base}.jpg")
     img_path = os.path.join(INPUT_DIR, fn)
     out_path = os.path.join(OUTPUT_DIR, fn)
     if not os.path.exists(scale_path):
         print(f"⚠️ カラースケールが見つかりません: {fn} → スキップ")
         continue
     scale_img = cv2.imread(scale_path)
     observed = extract_scale_colors(scale_img)
     pure = PURE_RGB.copy()
     corrected = apply_matrix(cv2.imread(img_path), M, rgb_strength)
     cv2.imwrite(out_path, corrected)
     print(f"✅ 補正完了: {fn} → {out_path}")
 
 print("\n🎯 全画像の色補正が完了しました。")
 
 スクリプト前半で画像からカラースケール領域の抽出と各色の平均値を計算を行い、理想的な純色(赤・黄・緑・青)との差を最小化するように行列と補正関数をチューニングし、自動で最良の補正式を決定
 重要な補正式として「緑帯の誤差を重視」する重み調整ロジックがあるが、これは植物判別や野外画像用途など彩度管理でよく使われる手法である
 画像から特定パッチを切り出し平均RGB値を計算し、その値と理想カラーチャート(赤・黄・緑・青)をマッチング
 複数の補正パラメータ(行列、強度、ガンマなど)を探索し、4色全体の誤差を最小化するものを一括自動選定 選ばれた補正式で全データを統一的に補正


-Scalehosei4.py

 ◯プレート領域の白枠マスク・抽出スクリプト
 基本設定・フォルダ操作
 入出力フォルダ名などの初期設定
 既存の出力先は削除し、新たに作り直します
 プレート領域のマスク処理
 
 mask_plate_regionで画像をグレースケール化して白黒マスク像を生成し、白枠が主領域となるように反転処理
 
 ◯ノイズ除去(remove_noise)
 細かな領域や切れぎみの線の補完を行った後に小面積のゴミ除去(connectedComponentsWithStats利用)し輪郭抽出・検証・手 動で4点を指定する
 
 ◯輪郭取得(detect_plate_contour)
 画像から白枠(矩形状)の輪郭を検出し、面積・中心・角度・アスペクト比などで厳しくフィルタ
 基準に適合しない場合はGUIでユーザー4点指定もサポート

 import cv2
 import numpy as np
 import os
 import shutil
 from glob import glob
 
 # === 設定 ===
 INPUT_DIR = "processed_patches3"   # 入力画像フォルダ
 OUTPUT_DIR = "processed_patches4"  # 出力フォルダ
 REF_WIDTH = 1000                   # 出力画像の横サイズ
 REF_HEIGHT = 750                   # 出力画像の縦サイズ
 
 os.makedirs(OUTPUT_DIR, exist_ok=True)
 
 # === フォルダーの削除・作成 ===
 folder_name = OUTPUT_DIR
 if os.path.exists(folder_name):
     shutil.rmtree(folder_name)
     print(f"既存のフォルダ '{folder_name}' を削除しました。")
 
 os.makedirs(folder_name, exist_ok=True)
 print(f"新しいフォルダ '{folder_name}' を作成しました。")
 
 # === 1. プレート領域のマスク処理 ===
 def mask_plate_region(img, threshold_mode="adaptive"): #★
     """
     撮影画像から白いプレート枠領域を抽出してマスク化する。
     """
     gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
     blur = cv2.GaussianBlur(gray, (7, 7), 0) #★
 
     if threshold_mode == "otsu":
         _, binary = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
     elif threshold_mode == "adaptive":
         binary = cv2.adaptiveThreshold(blur, 255,
                                        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                        cv2.THRESH_BINARY, 15, 4) #★
     else:
         raise ValueError("threshold_mode must be 'otsu' or 'adaptive'")
 
     # 白黒反転(白枠が白くなるように)
     if np.mean(binary) < 127:
         binary = cv2.bitwise_not(binary)
 
     return binary
 
 # === 2. ノイズ除去処理 ===
 def remove_noise(binary_mask):
     """
     二値化画像から小さなノイズを除去し、白枠を明確化する。
     """
     kernel = np.ones((4, 4), np.uint8) #★
 
     # Opening:白ノイズ除去
     opened = cv2.morphologyEx(binary_mask, cv2.MORPH_OPEN, kernel, iterations=2) #★
 
     # Closing:枠線の途切れ補完
     closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=2) #★
 
     # Connected Componentで小領域除去
     num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(closed)
     cleaned = np.zeros_like(closed)
     for i in range(1, num_labels):  # 0は背景
         area = stats[i, cv2.CC_STAT_AREA]
         if area > 500:   #★
             cleaned[labels == i] = 255
 
     return cleaned
 
 # === 3. 輪郭の取得と近似化 ===
 def detect_plate_contour(img, cleaned_mask):
     """
     白枠プレートの輪郭を検出し、内側スケール矩形のみを抽出。
     外枠(画像外周)は無視。
     条件:
       - 面積が 20000〜1450×1060 の範囲
       - アスペクト比が 1.0〜a の範囲
       - 図形中心が画像の外周b%以内でない
       - 各角の角度が c°〜d° の範囲
     """
     h, w = img.shape[:2]
     max_allowed_area = 1450 * 1060
     min_allowed_area = 630 * 340
 
     # --- ① 外枠除外マスク ---
     inner_mask = np.zeros_like(cleaned_mask)
     margin = int(min(h, w) * 0.12)  # 外周を無視
     inner_mask[margin:h - margin, margin:w - margin] = 255
     masked = cv2.bitwise_and(cleaned_mask, inner_mask)
 
     # --- ② 輪郭検出 ---
     contours, _ = cv2.findContours(masked, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
     if not contours:
         print("⚠️ 輪郭が検出されませんでした。")
         return img, None
 
     valid_candidates = []
     for cnt in contours:
         area = cv2.contourArea(cnt)
         if area < min_allowed_area or area > max_allowed_area:
             continue
 
         rect = cv2.minAreaRect(cnt)
         (cx, cy), (rw, rh), angle = rect
 
         # --- ③ 中心位置チェック(外周に寄りすぎていないか) ---
         if cx < margin or cx > (w - margin) or cy < margin or cy > (h - margin):
             continue
 
         # --- ④ アスペクト比チェック ---
         aspect_ratio = max(rw, rh) / (min(rw, rh) + 1e-6)
         if not (1.5 <= aspect_ratio <= 2.0):
             continue
 
         # --- ⑤ 近似と角度判定 ---
         epsilon = 0.02 * cv2.arcLength(cnt, True)
         approx = cv2.approxPolyDP(cnt, epsilon, True)
         if len(approx) != 4:
             continue
 
         pts = approx.reshape(4, 2)
         angles = []
         for i in range(4):
             p1, p2, p3 = pts[i], pts[(i + 1) % 4], pts[(i + 2) % 4]
             v1, v2 = p1 - p2, p3 - p2
             cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6)
             angle = np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
             angles.append(angle)
         if all(69 <= a <= 111 for a in angles):
             valid_candidates.append((area, pts))
 
     # --- ⑥ 最も適した候補を選択 ---
     if not valid_candidates:
         print("⚠️ 有効な内枠が見つかりませんでした。手動で4点を選択してください。")
 
         manual_points = []
 
         def mouse_callback(event, x, y, flags, param):
             if event == cv2.EVENT_RBUTTONDOWN:  # 右クリックで追加
                 manual_points.append((x, y))
                 cv2.drawMarker(param, (x, y), (0, 255, 0), cv2.MARKER_TILTED_CROSS, 20, 2)
                 cv2.imshow("manual_select", param)
                 print(f"✅ 追加: {len(manual_points)}点目 ({x}, {y})")
 
             elif event == cv2.EVENT_LBUTTONDOWN:  # 左クリックでキャンセル
                 if manual_points:
                     manual_points.pop()
                     print(f"↩️ 直前の点を削除。残り {len(manual_points)} 点。")
                     param_copy = img.copy()
                     for px, py in manual_points:
                         cv2.drawMarker(param_copy, (px, py), (0, 255, 0), cv2.MARKER_TILTED_CROSS, 20, 2)
                     cv2.imshow("manual_select", param_copy)
 
         temp = img.copy()
         cv2.imshow("manual_select", temp)
         cv2.setMouseCallback("manual_select", mouse_callback, temp)
 
         print("🖱️ 右クリックで4点選択、左クリックで直前をキャンセル。完了後に Enter を押してください。")
         while True:
             key = cv2.waitKey(0)
             if key in [13, 10]:  # Enter
                 break
         cv2.destroyWindow("manual_select")
 
         if len(manual_points) != 4:
             print("⚠️ 4点が選択されませんでした。スキップします。")
             return img, None
             
         best_pts = np.array(manual_points, dtype=np.float32)
         contour_img = img.copy()
         cv2.polylines(contour_img, [best_pts.astype(int)], isClosed=True, color=(0, 255, 0), thickness=3)
         print("✅ 手動で内枠を確定しました。")
         return contour_img, best_pts
 
     best_area, best_pts = max(valid_candidates, key=lambda x: x[0])
     contour_img = img.copy()
     cv2.drawContours(contour_img, [best_pts.astype(int)], -1, (0, 255, 0), 4)
     print(f"🟩 内枠矩形を検出(面積={best_area:.0f}, アスペクト比={aspect_ratio:.2f})")
 
     return contour_img, best_pts
 
 # === 4. メイン処理 ===
 for fname in os.listdir(INPUT_DIR):
     if not fname.lower().endswith((".jpg", ".jpeg", ".png")):
         continue
 
     img_path = os.path.join(INPUT_DIR, fname)
     img = cv2.imread(img_path)
 
     if img is None:
         print(f"⚠️ 読み込み失敗: {fname}")
         continue
 
     # --- ステップ1: マスク化 ---
     mask = mask_plate_region(img, threshold_mode="otsu")


-Scalehosei5.py

 2. 緑枠検出と台形補正
 ○基本設定
 台形補正に使う出力画像サイズ、検出用緑範囲、フォルダ再初期化
 
 ○緑枠検出(detect_green_corners)
 BGR→HSV変換し、緑色HSV域をinRangeで抽出
 輪郭検出し、最大4点近似で「緑枠」四隅座標を取得
 
 ○透視変換による補正(perspective_correction)
 4点座標を「左上→右上→右下→左下」に順序付け
 指定したサイズに合わせてOpenCVの透視変換実行


 import cv2
 import numpy as np
 import os
 import shutil
 
 # === 設定 ===
 INPUT_DIR = "processed_patches4"   # 緑枠付き画像フォルダ
 OUTPUT_DIR = "processed_patches5"   # 台形補正後の出力フォルダ
 os.makedirs(OUTPUT_DIR, exist_ok=True)
 
 # === 出力画像サイズ ===
 # 縦8cm × 横14cm → 比率4:7 として例: 256×448 ピクセルに統一
 OUT_H, OUT_W = 256, 448
 
 # === 緑色検出用のHSV範囲(調整可能) ===
 HSV_LOWER = np.array([35, 80, 40])   # 緑の下限
 HSV_UPPER = np.array([85, 255, 255]) # 緑の上限
 
 # === フォルダ初期化 ===
 if os.path.exists(OUTPUT_DIR):
     shutil.rmtree(OUTPUT_DIR)
     print(f"🧹 既存のフォルダ '{OUTPUT_DIR}' を削除しました。")
 os.makedirs(OUTPUT_DIR, exist_ok=True)
 print(f"📁 新しいフォルダ '{OUTPUT_DIR}' を作成しました。")
 
 def detect_green_corners(img):
     """
     緑枠の四隅を検出して返す。
     成功すれば4点座標(np.float32)を返し、失敗時はNone。
     """
     hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
     mask = cv2.inRange(hsv, HSV_LOWER, HSV_UPPER)
 
     # ノイズ除去
     kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
     mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
     mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
 
     # 輪郭検出
     contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
     if not contours:
         return None
 
     # 面積の大きい順にソート
     contours = sorted(contours, key=cv2.contourArea, reverse=True)
     for cnt in contours:
         peri = cv2.arcLength(cnt, True)
         approx = cv2.approxPolyDP(cnt, 0.04 * peri, True)
         if len(approx) == 4:
             pts = approx.reshape(4, 2).astype(np.float32)
             return pts  # 四角形と認識されたら返す
 
     return None
 
 def order_points(pts):
     """4点を順序付きに整列(左上・右上・右下・左下)"""
     rect = np.zeros((4, 2), dtype="float32")
     s = pts.sum(axis=1)
     rect[0] = pts[np.argmin(s)]  # 左上
     rect[2] = pts[np.argmax(s)]  # 右下
 
     diff = np.diff(pts, axis=1)
     rect[1] = pts[np.argmin(diff)]  # 右上
     rect[3] = pts[np.argmax(diff)]  # 左下
     return rect
 
 def perspective_correction(img, pts, out_size=(OUT_W, OUT_H)):
     """透視変換を行い、指定サイズにリサイズ"""
     rect = order_points(pts)
     dst = np.array([
         [0, 0],
         [out_size[0] - 1, 0],
         [out_size[0] - 1, out_size[1] - 1],
         [0, out_size[1] - 1]
     ], dtype="float32")
 
     # dst の順番 (w,h)に注意:OpenCVでは横×縦
     M = cv2.getPerspectiveTransform(rect, dst)
     warped = cv2.warpPerspective(img, M, out_size)
     return warped
 
 # === メイン処理 ===
 for fname in os.listdir(INPUT_DIR):
     if not fname.lower().endswith((".jpg", ".jpeg", ".png")):
         continue
 
     path_in = os.path.join(INPUT_DIR, fname)
     img = cv2.imread(path_in)
     if img is None:
         print(f"⚠️ 画像読み込み失敗: {fname}")
         continue
 
     corners = detect_green_corners(img)
     if corners is None:
         print(f"⚠️ 緑枠の四隅が検出できません: {fname}")
         continue
 
     warped = perspective_correction(img, corners, out_size=(OUT_W, OUT_H))
     path_out = os.path.join(OUTPUT_DIR, fname)
     cv2.imwrite(path_out, warped)
     print(f"✅ {fname} を補正・保存しました。")
 
 print(f"\n🎯 台形補正が完了しました。出力フォルダ: {OUTPUT_DIR}")


-Scalehosei6.py

 ○基本設定
 目的ごとのフォルダ/クロップ範囲等を準備
 
 ○自動クロップ
 各入力画像を一定サイズ(例:256x256)でNUM_CROPS枚だけ切り抜き、オーバーラップ比率を調整しながらスライド
 保存ファイル名は評価点+連番で命名
 
 import cv2
 import os
 import numpy as np
 import shutil
 
 # === 設定 ===
 INPUT_DIR = "processed_patches5"   # 抽出済み錆画像フォルダ
 OUTPUT_DIR = "processed_patches6"     # 出力先
 CROP_SIZE = 256                    # 切り抜きサイズ(px)
 NUM_CROPS = 4                      # 1枚あたり生成枚数
 OVERLAP_RATIO = 0.3                # オーバーラップ率(0.0〜0.9)
 
 # === 出力フォルダ初期化 ===
 if os.path.exists(OUTPUT_DIR):
     shutil.rmtree(OUTPUT_DIR)
 os.makedirs(OUTPUT_DIR, exist_ok=True)
 print(f"🗂️ 出力フォルダを作成: {OUTPUT_DIR}")
 
 # === メイン処理 ===
 for fname in os.listdir(INPUT_DIR):
     if not fname.lower().endswith((".jpg", ".jpeg", ".png")):
         continue
 
     # 元画像の評点(例: "1S_0001.jpg" → score = 1)
     score = fname[0]
     img_path = os.path.join(INPUT_DIR, fname)
     img = cv2.imread(img_path)
 
     if img is None:
         print(f"⚠️ 読み込み失敗: {fname}")
         continue
 
     h, w = img.shape[:2]
 
     # 切り抜き可能数を超える場合は調整
     crop_w = CROP_SIZE
     crop_h = CROP_SIZE
 
     # オーバーラップを考慮して切り抜き開始座標を決定
     x_step = int((w - crop_w) / (NUM_CROPS - 1)) if NUM_CROPS > 1 else 0
     y_step = int((h - crop_h) / (NUM_CROPS - 1)) if NUM_CROPS > 1 else 0
 
     # オーバーラップの設定(比率で調整)
     x_step = max(int(crop_w * (1 - OVERLAP_RATIO)), 1)
     y_step = max(int(crop_h * (1 - OVERLAP_RATIO)), 1)
 
     # スライドウィンドウ的に切り抜き
     crop_count = 0
     for y in range(0, max(h - crop_h + 1, 1), y_step):
         for x in range(0, max(w - crop_w + 1, 1), x_step):
             crop = img[y:y+crop_h, x:x+crop_w]
             if crop.shape[0] != CROP_SIZE or crop.shape[1] != CROP_SIZE:
                 continue
             crop_count += 1
             save_name = f"{score}_{crop_count}.jpg"
             save_path = os.path.join(OUTPUT_DIR, save_name)
             cv2.imwrite(save_path, crop)
 
             if crop_count >= NUM_CROPS:
                 break
         if crop_count >= NUM_CROPS:
             break
 
     print(f"✅ {fname}: {crop_count}枚の256×256画像を生成")
 
 print("🎯 全画像の切り抜き完了。")




-11/7

カラーの錆画像を256ピクセル四方で切り抜くことができた。11日以降にCNNで学習させていきたいと思っている。

-色補正・切り抜き手順
--1, 始めにカラーの錆画像(drone_images)を用意する

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/01.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/02.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/03.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/04.jpg,350x); 

--2, Scalehosei1.pyを実行する そうすると元画像からカラースケールを枠で囲んだ画像(上)とその部分を抽出した画像(下)がそれぞれ保存される

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/1.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/2.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/3.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/4.jpg,350x); 

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/11.jpg,40x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/12.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/13.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/14.jpg,350x); 

--3, Scalehosei2.pyを実行する 純色に変換された切り抜かれたカラースケールと変換行列が保存される

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/21.jpg,40x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/22.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/23.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/24.jpg,350x); 
 
--4, Scalehosei3.pyを実行する 3の変換行列をもとに画像全体を色補正する

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/31.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/32.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/33.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/34.jpg,350x); 

--5, Scalehosei4.pyを実行する カラースケールの内側に緑色で枠をつける(上手く行かない場合は手動で四隅を選択)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/41.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/42.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/43.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/44.jpg,350x); 

--6, Scalehosei5.pyを実行する 枠から錆を切り抜く

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/51.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/52.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/53.jpg,350x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/54.jpg,350x); 

--7, Scalehosei6.pyを実行する 切り抜いた画像を学習用に256×256ピクセルでさらに切り抜く

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/101.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/102.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/103.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/104.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/105.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/106.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/107.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/108.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/109.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/110.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/111.jpg,250x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251107/112.jpg,250x); 

抽出の際に使用した緑の枠が残ってしまっているがとりあえずこのまま学習にかけてみて、もし上手く行かないのであれば取り除くといった方向性で考えている。

-11/9

カラーの錆画像で学習を行った。(評点1:6枚, 評点2:54枚, 評点3:105枚, 評点4:63枚, 評点5:6枚)の計234枚を元画像とし1枚あたり50枚水増しした11700枚のうち1割に当たる1170枚を評価用に、残りを学習用に充てた。学習回数は50回とし収束したら早めに切り上げるという条件下のもと行った。11日以降はドローンで撮影した画像を使って何かする予定。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251109.jpg,800x); 

-11/11

-DenseNet121 単体で3回学習(同一条件)を行ったところ以下の結果が得られた。学習させるたびに僅かながらではあるが差が生じており、条件を固定したところで同じ結果にはならないことが分かった

, Run , MAE , 正答率 , F1スコア 
, 1 , 0.282702960866563 , 0.553191489361702 , 0.36617183985605 
, 2 , 0.237414197718844 , 0.553191489361702 , 0.381286549707602 
, 3 , 0.237434574898253 , 0.531914893617021 , 0.383338632750398 

カラーの錆画像を学習させた結果( .keras ファイル)を用いてドローンで撮影した錆画像を判別させてみた。ただしドローン画像は補正処理を施していない。画像名と撮影画像は対応済み

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/1_1.jpg,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/1_2.jpg,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/1_3.jpg,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/2_1.jpg,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/3_1.jpg,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/3_2.jpg,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/3_3.jpg,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/4_1.jpg,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/4_4.png,150x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/4_5.png,150x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/4_61.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/4_62.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/4_63.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/4_64.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251111/4_65.jpg,300x);

, 画像 , 評点 , 錆割合 , 錆数 
, 1_1.jpg , 3 , 0.996 , 1 
, 1_2.jpg , 3 , 0.530 , 138 
, 1_3.jpg , 3 , 0.999 , 1 
, 2_1.jpg , 3 , 1.000 , 1 
, 3_1.jpg , 3 , 0.996 , 1 
, 3_2.jpg , 3 , 1.000 , 1 
, 3_3.jpg , 3 , 1.000 , 1 
, 4_1.jpg , 3 , 1.000 , 1 
, 4_4.png , 3 , 0.423 , 956 
, 4_5.png , 3 , 0.396 , 1038 
, 4_61.jpg , 3 , 0.477 , 26143 
, 4_62.jpg , 3 , 0.482 , 11092 
, 4_63.jpg , 4 , 0.729 , 3324 
, 4_64.jpg , 3 , 0.374 , 66397 
, 4_65.jpg , 2 , 0.330 , 217611 


ドローン撮影画像では照明・影・反射の影響が大きく学習済みCNNモデルが想定する色空間と乖離しやすい。そのため色補正を行わない場合は「学習段階での色変動拡張(Augmentation)」を十分に行い、推論時には「軽度のホワイトバランス補正」を加えることで評点抽出の安定性を確保できると考えた。これにより照明条件の異なる屋外錆画像でもほぼ同等の精度でエリアごとに評点を抽出・可視化が可能となる。

-11/12

ドローンで撮影した画像から評点を求めることを行った。錆の領域は手動で切り抜いてその中から錆の評点を求めることとした。

-β版(最初モノクロに変換してから領域と評点を自動で抽出 → 赤矩形の左上に書いてある数字は回帰スコア)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/1.jpg,750x); →
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/11.jpg,750x);


-改良版(白, 黒の強弱を調節し橋梁側面の錆をより自動で抽出しやすくした → 評点1は水色, 評点2は緑, 評点3は黄色, 評点4は橙の領域でそれぞれ囲まれている)

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/2.jpg,750x); →
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/12.jpg,750x);

-錆領域手動切り抜き版(予め橋梁側面の錆領域を定義してその中から評点を求める → 評点2は橙, 評点3は黄色, 評点4は緑の領域でそれぞれ囲まれるようにした)

--錆領域切り抜き

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/101.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/102.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/103.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/104.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/105.jpg,300x);

--評点領域を抽出

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/111.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/112.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/113.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/114.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251112/115.jpg,300x);



***11月第3週(11/13〜11/19) [#j417c1113]
-11/13

昨日行った評点領域抽出に関して納得の行く結果が得られなかったので別のアプローチから評点を求めることにした。

-ヒートマップでの評点抽出

ドローンで撮影された橋梁表面の錆画像から錆の劣化程度を視覚的に示すヒートマップを生成することで、画像中の局所的な錆の評点を定量的かつ空間的に把握することを可能とした。

○手順
--1, 入力画像の頂点選択
---対象となる橋梁のカラー画像を入力フォルダーに入力し、マウス操作により錆が存在する領域の頂点を選択する。錆領域は場合によって矩形で抽出できない場合があるため多角形でも選択できるように選択できる頂点の数は無制限にした。
---マウス操作のコマンド一覧

, コマンド , 実行内容 
, 左クリック , 頂点を追加 
, 右クリック , 直前の頂点を削除 
, Enterキー , 頂点選択確定 次の画像へ 
, Escapeキー , 選択中止 

---頂点選択を行う際には以下のようなウィンドウが出力される(左画像)ので好きなだけ頂点を選択する(中画像)。ただし頂点を選択しすぎると領域いっぱいにヒートマップが表示されなくなるので注意すること(右画像)。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251113/11.png,500x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251113/12.png,500x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251113/13.jpg,500x);

--2, 領域分割と評点推定
---選択された領域を任意サイズ(例:256×256ピクセル)の小領域(パッチ)にスライスする。※ピクセルサイズはスクリプトで変更可能
---各パッチ画像に対して事前に学習済みのCNNアンサンブルモデル(EfficientNetV2S・DenseNet121・MobileNetV3Large)を用いて評点(1〜5)を推定する。
---評点は以下の基準で分類される

, 評点 , 劣化状態 , 表示色 
, 1 , 重度の錆 , 🔴 赤 
, 2 , 進行した錆 , 🟠 橙 
, 3 , 中程度の錆 , 🟡 黄 
, 4 , 軽微な錆 , 🟢 緑 
, 5 , 錆がほとんどない , 🔵 水色 

--3, ヒートマップ生成
---各パッチの推定評点に応じて対応する色を半透明で重ね合わせることで、画像全体の劣化状態を示すヒートマップを生成。これにより錆の発生分布や局所的な劣化度の偏りを一目で把握できるようにした(手順1の右画像)。

--4, 出力と付加情報
---生成されたヒートマップには以下の情報が自動的に付与される

, 情報記載位置 , 内容 
, 左下 , ポリゴン内全体の平均評点値(Avg) 
, 右下 , 各評点に対応する色凡例(Legend) 

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251113/41.jpg,400x);      
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251113/42.jpg,400x);

-ヒートマップ抽出スクリプト
 import cv2
 import shutil
 import numpy as np
 from tensorflow.keras.models import load_model
 import os
 import glob
 
 # === 設定 ===
 MODEL_PATHS = [
     "ensemble_model_1.keras",
     "ensemble_model_2.keras",
     "ensemble_model_3.keras"
 ]
 INPUT_DIR = "drone_images"
 OUTPUT_DIR = "drone_detect_result"
 PATCH_SIZE = 256  # 任意変更可
 
 # === フォルダー初期化 ===
 if os.path.exists(OUTPUT_DIR):
     shutil.rmtree(OUTPUT_DIR)
     print(f"既存のフォルダ '{OUTPUT_DIR}' を削除しました。")
 os.makedirs(OUTPUT_DIR, exist_ok=True)
 
 # === モデル読み込み ===
 models = []
 for path in MODEL_PATHS:
     if os.path.exists(path):
         model = load_model(path, compile=False)
         models.append(model)
         print(f"✅ モデル読み込み完了: {path}")
 if not models:
     raise FileNotFoundError("❌ 有効な .keras モデルが見つかりません。")
 
 # === 評点ごとの色設定 ===
 score_colors = {
     1: (0, 0, 255),      # 赤
     2: (0, 165, 255),    # 橙
     3: (0, 255, 255),    # 黄
     4: (0, 255, 0),      # 緑
     5: (255, 255, 0),    # 水色
 }
 
 # === 評点推定関数 ===
 def predict_score(crop_img):
     resized = cv2.resize(crop_img, (256, 256))
     x = resized.astype("float32") / 255.0
     x = np.expand_dims(x, axis=0)
     preds = [m.predict(x, verbose=0)[0][0] for m in models]
     avg_pred = np.mean(preds)
     # 評点を5段階に丸める
     if avg_pred < 1.9: score = 1
     elif avg_pred < 2.6: score = 2
     elif avg_pred < 3.3: score = 3
     elif avg_pred < 4.0: score = 4
     else: score = 5
     return score, avg_pred
 
 # === 手動ポリゴン選択 ===
 points = []
 def draw_polygon(event, x, y, flags, param):
     global points
     img_copy = param["img_copy"]
     if event == cv2.EVENT_LBUTTONDOWN:
         points.append((x, y))
         cv2.circle(img_copy, (x, y), 5, (0, 0, 255), -1)
     elif event == cv2.EVENT_RBUTTONDOWN and points:
         points.pop()
         img_copy[:] = param["img"].copy()
         for px, py in points:
             cv2.circle(img_copy, (px, py), 5, (0, 0, 255), -1)
     elif event == cv2.EVENT_MOUSEMOVE:
         img_copy[:] = param["img"].copy()
         for px, py in points:
             cv2.circle(img_copy, (px, py), 5, (0, 0, 255), -1)
         if len(points) > 1:
             cv2.polylines(img_copy, [np.array(points)], isClosed=False, color=(0, 255, 255), thickness=2)
 
 # === 錆検出処理(ヒートマップ生成 + 凡例 + 平均) ===
 def detect_rust_heatmap(img_path, patch_size=256):
     global points
     basename = os.path.basename(img_path).split('.')[0]
     img = cv2.imread(img_path)
     if img is None:
         print(f"⚠️ 読み込み失敗: {img_path}")
         return
 
     # --- 画像を1980×1080で表示 ---
     display_img = cv2.resize(img, (1980, 1080))
     scale_x = img.shape[1] / 1980
     scale_y = img.shape[0] / 1080
 
     img_copy = display_img.copy()
     cv2.namedWindow("Select Area", cv2.WINDOW_NORMAL)
     cv2.setMouseCallback("Select Area", draw_polygon, {"img": display_img, "img_copy": img_copy})
 
     print("🟢 左クリック=頂点追加 / 右クリック=戻す / Enter=確定 / Esc=中止")
 
     while True:
         cv2.imshow("Select Area", img_copy)
         key = cv2.waitKey(1) & 0xFF
         if key == 13:  # Enter
             if len(points) >= 3:
                 print("✅ ポリゴン確定")
                 break
             else:
                 print("⚠️ 頂点が3つ未満です")
         elif key == 27:  # ESC
             print("🚫 中断されました")
             cv2.destroyAllWindows()
             return
 
     # --- スケール戻す ---
     scaled_points = np.array([[int(x * scale_x), int(y * scale_y)] for (x, y) in points])
     points = []
 
     # --- ポリゴンマスク作成 ---
     mask = np.zeros(img.shape[:2], np.uint8)
     cv2.fillPoly(mask, [scaled_points], 255)
 
     # --- 領域内スライスしてスコア予測 ---
     result = img.copy()
     heatmap = np.zeros_like(img)
     h, w = img.shape[:2]
     scores_list = []
 
     for y in range(0, h, patch_size):
         for x in range(0, w, patch_size):
             cx, cy = x + patch_size//2, y + patch_size//2
             if cx >= w or cy >= h:
                 continue
             # 中心がポリゴン内か確認
             if mask[cy, cx] == 0:
                 continue
             crop = img[y:y+patch_size, x:x+patch_size]
             if crop.shape[0] < patch_size or crop.shape[1] < patch_size:
                 continue
             score, raw_pred = predict_score(crop)
             scores_list.append(raw_pred)
             color = score_colors[score]
             overlay = np.full_like(crop, color, dtype=np.uint8)
             # 透明重ね
             cv2.addWeighted(overlay, 0.4, result[y:y+patch_size, x:x+patch_size], 0.6, 0, result[y:y+patch_size, x:x+patch_size])
 
     # --- 平均スコア ---
     avg_score = np.mean(scores_list) if scores_list else 0
     print(f"📊 平均スコア: {avg_score:.2f}")
 
     # --- ポリゴン枠 + 平均スコア表示(左下) ---
     cv2.polylines(result, [scaled_points], True, (255, 255, 255), 2)
     h, w = result.shape[:2]
     cv2.putText(result, f"Avg: {avg_score:.2f}", (30, h - 40),
                 cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255,255,255), 3, cv2.LINE_AA)
 
     # --- 凡例描画(右下) ---
     legend_x = w - 250
     legend_y = h - 180
     cv2.rectangle(result, (legend_x - 20, legend_y - 30), (w - 20, legend_y + 200), (40,40,40), -1)
     cv2.putText(result, "Legend", (legend_x, legend_y - 10),
                 cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2, cv2.LINE_AA)
     for i, (score, color) in enumerate(score_colors.items()):
         y_pos = legend_y + i * 35
         cv2.rectangle(result, (legend_x, y_pos), (legend_x + 30, y_pos + 30), color, -1)
         cv2.putText(result, f"{score}", (legend_x + 45, y_pos + 25),
                     cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2, cv2.LINE_AA)
 
     # --- 保存 ---
     out_path = os.path.join(OUTPUT_DIR, f"{basename}_heatmap.jpg")
     cv2.imwrite(out_path, result)
     print(f"🟩 ヒートマップを保存: {out_path}")
 
     cv2.destroyAllWindows()
 
 # === メイン ===
 def batch_process_images():
     image_files = sorted(glob.glob(os.path.join(INPUT_DIR, '*.*')))
     for idx, path in enumerate(image_files, 1):
         print(f"📸 ({idx}/{len(image_files)}) {path}")
         detect_rust_heatmap(path, patch_size=PATCH_SIZE)
 
 if __name__ == "__main__":
     batch_process_images()

-11/14

○ピクセル数を変化させると評点に変化がでるのか
--256ピクセル

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/1.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/2.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/3.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/4.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/5.jpg,300x);

, 画像 , 平均スコア 
, 左1 , 3.08 
, 左2 , 3.12 
, 中央 , 2.99 
, 右2 , 3.06 
, 右1 , 3.02 

--128ピクセル 

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/11.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/12.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/13.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/14.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/15.jpg,300x);

, 画像 , 平均スコア 
, 左1 , 3.01 
, 左2 , 3.03 
, 中央 , 2.99 
, 右2 , 3.01 
, 右1 , 3.02 

--64ピクセル

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/21.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/22.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/23.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/24.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/25.jpg,300x);

, 画像 , 平均スコア
, 左1 , 3.34 
, 左2 , 3.34 
, 中央 , 3.18 
, 右2 , 3.39 
, 右1 , 3.28 

--32ピクセル

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/31.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/32.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/33.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/34.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251114/35.jpg,300x);

, 画像 , 平均スコア
, 左1 , 3.44 
, 左2 , 3.62 
, 中央 , 3.49 
, 右2 ,  3.49 
, 右1 , 3.50 

出力時間を考慮したら128ピクセル、精度を考慮したら64ピクセルが理想的か

-11/17

これまで橋側面からSAM (Segment Anything Model)を用いて錆領域を抽出して評点を求めていたが面の抽出にはSAM以外にもK-meansやU-Net等があり、SAMはあらゆる物体を認識できるオールラウンダータイプ, K-meansは簡単に実装・学習が可能なライトタイプ、U-Netは橋面の色・光・角度が変わっても安定して検出できるものの他に比べ学習に時間とデータを費やすヘビータイプとそれぞれ特徴がある。U-Netでも抽出を行ってみようと思う。K-meansは構築するスピードに全ふりしたような感じで実際の中身が伴っていないように思えたので実装は省略する。

--SAMでの抽出(アンサンブルとCNNモデル単体比較)上段から順にアンサンブル, InceptionResNetV2, DenseNet121, Xception の単体モデルが並んでいる。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/1.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/2.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/3.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/4.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/5.jpg,300x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/11.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/12.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/13.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/14.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/15.jpg,300x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/21.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/22.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/23.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/24.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/25.jpg,300x);

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/31.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/32.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/33.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/34.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251117/35.jpg,300x);

大きく評点が変わることは無かったがヒートマップにはそれなりに違いが表れたのではないか。アンサンブルで評点の抽出を行ったものが一番信頼できそうな結果となった。

-11/19

U-netを用いて錆領域の自動判別を行った。錆側面以外にも護岸や河川といった背景も領域として認識されてしまうことが分かった。今後は橋側面、橋側面以外の錆領域、背景の3種類に分けて学習を進めていこうと思う。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251119/1.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251119/2.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251119/3.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251119/4.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251119/5.jpg,300x);


***11月第4週(11/20〜11/26) [#j417c1120]
-11/20

 import os
 import sys
 import cv2
 import json
 import time
 import glob
 import shutil
 import random
 import datetime
 import numpy as np
 import tensorflow as tf
 from tensorflow.keras import layers, models, Input, Model
 from tensorflow.keras.optimizers import Adam
 from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
 from sklearn.model_selection import train_test_split
 import matplotlib.pyplot as plt
 
 # -------------------------
 # CONFIGURATION - 必要に応じて書き換えてください
 # -------------------------
 TRAIN_IMAGES_DIR = "train_images"      # アノテ・学習用元画像
 MASKS_DIR = "masks"                    # アノテ結果(0=bg,2=rust) を保存するフォルダ
 MODELS_DIR = "models"                  # 保存先フォルダ
 UNET_MODEL_PATH = os.path.join(MODELS_DIR, "unet.keras")
 DRONE_IMAGES_DIR = "drone_images"      # 推論対象ドローン画像
 RESULTS_DIR = "results_unet"           # 推論出力(ヒートマップ等)
 
 # パッチ・ヒートマップ設定
 PATCH_SIZE = 256
 STRIDE = 126            # スライド幅(任意で調整)
 DISPLAY_MAX = (1980, 1080)  # 表示最大解像度(要求により1980x1080で表示)
 
 # CNN評点用の事前学習済みモデル群(任意)
 # ここに .keras ファイルを列挙すると、アンサンブルでスコアを平均します(省略可)
 ENSEMBLE_MODELS = [
      "ensemble_model_1.keras",
      "ensemble_model_2.keras",
      "ensemble_model_3.keras"
 ]
 
 RANDOM_SEED = 42
 np.random.seed(RANDOM_SEED)
 tf.random.set_seed(RANDOM_SEED)
 random.seed(RANDOM_SEED)
 
 # -------------------------
 # ユーティリティ
 # -------------------------
 def ensure_dirs():
     os.makedirs(MASKS_DIR, exist_ok=True)
     os.makedirs(MODELS_DIR, exist_ok=True)
     os.makedirs(RESULTS_DIR, exist_ok=True)
 
 # --- 錆マスクから bridge マスクを自動生成するユーティリティ ---
 def generate_bridge_mask_from_rust_mask(rust_mask_path, out_bridge_mask_path=None,
                                         dilate_iter=20, min_area=5000, convexify=True):
     """
     rust_mask_path: 二値 or ラベルマスク(255=rust など) のパス
     out_bridge_mask_path: 保存先(png)。None の場合は rust_mask_path の同じフォルダに作る。
         戻り値: bridge_mask (H,W) uint8 (0/255)
         パラメータ調整: - dilate_iter: 錆領域をどれだけ膨らませるか(橋側面を覆うために十分大きめに)
                  - min_area: 小領域を無視する閾値
     """
     m = cv2.imread(rust_mask_path, cv2.IMREAD_UNCHANGED)
     if m is None:
         raise FileNotFoundError(f"rust mask not found: {rust_mask_path}")
     # もしラベル(maskが0/1/2)なら rust に対応する値を抽出
     if m.ndim == 3:
         m_gray = cv2.cvtColor(m, cv2.COLOR_BGR2GRAY)
     else:
         m_gray = m.copy()
 
     # 二値化(非ゼロを錆とみなす)
     _, bw = cv2.threshold(m_gray, 1, 255, cv2.THRESH_BINARY)
 
     # 膨張で周囲をカバー(橋側面を含めたいので大きめ)
     kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15,15))
     dil = cv2.dilate(bw, kernel, iterations=dilate_iter)
 
     # 小領域除去(ノイズ排除)
     num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(dil, connectivity=8)
     mask_clean = np.zeros_like(dil)
     for i in range(1, num_labels):
         area = stats[i, cv2.CC_STAT_AREA]
         if area >= min_area:
             mask_clean[labels == i] = 255
 
     # 凸包で外形を滑らかに(オプション)
     if convexify:
         contours, _ = cv2.findContours(mask_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
         hull_mask = np.zeros_like(mask_clean)
         for cnt in contours:
             if cv2.contourArea(cnt) < min_area: 
                 continue
             hull = cv2.convexHull(cnt)
             cv2.drawContours(hull_mask, [hull], -1, 255, -1)
         mask_final = hull_mask
     else:
         mask_final = mask_clean
 
     # 少し膨らませて余裕を持たせる(bridge全体を覆う)
     kernel2 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (31,31))
     mask_final = cv2.dilate(mask_final, kernel2, iterations=2)
 
     if out_bridge_mask_path is None:
         base = os.path.splitext(rust_mask_path)[0]
         out_bridge_mask_path = base + "_bridge.png"
     cv2.imwrite(out_bridge_mask_path, mask_final)
     return mask_final
 
 def resize_for_display(img, max_size=DISPLAY_MAX):
     h, w = img.shape[:2]
     scale = min(max_size[0] / w, max_size[1] / h, 1.0)
     if scale < 1.0:
         return cv2.resize(img, (int(w*scale), int(h*scale))), scale
     else:
         return img, 1.0
 
 # -------------------------
 # 1) アノテーションツール(橋側面: 1ポリゴン、錆: 複数ポリゴン) 
 # -------------------------
 def annotate_mode(input_dir=TRAIN_IMAGES_DIR, masks_dir=MASKS_DIR):
     ensure_dirs()
     files = sorted([f for f in os.listdir(input_dir) if f.lower().endswith(('.jpg','.jpeg','.png'))])
     if not files:
         print("No images found in", input_dir); return
 
     for fname in files:
         img_path = os.path.join(input_dir, fname)
         img = cv2.imread(img_path)
         if img is None:
             print("Failed to load", img_path); continue
         basename = os.path.splitext(fname)[0]
         print(f"\n=== Annotate bridge only: {fname} ===")
         poly = polygon_draw_ui(img, title=f"Draw Bridge Polygon (Enter to confirm) : {fname}")
         if poly is None:
             print("Skipped:", fname)
             continue
         mask = np.zeros(img.shape[:2], dtype=np.uint8)
         cv2.fillPoly(mask, [np.array(poly)], 1)   # 1 = bridge
         save_path = os.path.join(masks_dir, f"{basename}_mask.png")
         # if existing rust mask exists, merge later during dataset loading; here save bridge as 1 and 0 background
         cv2.imwrite(save_path, mask)
         print("Saved bridge mask:", save_path)
 
 def polygon_draw_ui(img, title="Draw polygon", allow_quit_key=None):
     win = title
     img_work = img.copy()
     img_display = img.copy()
     points = []
 
     # ▼ 初期縮小(ウィンドウに合わせる)
     disp_img, scale = resize_for_display(img_display)
 
     def mouse_cb(event, x, y, flags, param):
         nonlocal img_display, points, scale
 
         # ▼ 表示画像 → 元画像座標へ変換
         orig_x = int(x / scale)
         orig_y = int(y / scale)
 
         if event == cv2.EVENT_LBUTTONDOWN:
             points.append((orig_x, orig_y))
         elif event == cv2.EVENT_RBUTTONDOWN:
             if points:
                 points.pop()
 
         # 描画は元画像サイズで行う
         img_display = img_work.copy()
         for px, py in points:
             cv2.circle(img_display, (px, py), 4, (0,0,255), -1)
         if len(points) > 1:
             cv2.polylines(img_display, [np.array(points)], isClosed=False, color=(0,255,255), thickness=2)
 
     cv2.namedWindow(win, cv2.WINDOW_NORMAL)
     cv2.setMouseCallback(win, mouse_cb)
 
     print("Instructions:")
     print(" - Left click: add vertex")
     print(" - Right click: undo last vertex")
     print(" - Enter: confirm polygon")
     print(" - Esc: cancel")
 
     while True:
         # ▲ 表示ごとに縮尺計算(ウィンドウサイズが変わるとscaleも変わる)
         disp_img, scale = resize_for_display(img_display)
         cv2.imshow(win, disp_img)
 
         k = cv2.waitKey(20) & 0xFF
         if k == 13:  # Enter
             if len(points) >= 3:
                 cv2.destroyWindow(win)
                 return points.copy()
             else:
                 print("Need at least 3 points.")
         elif k == 27:  # Esc
             cv2.destroyWindow(win)
             return None
         elif allow_quit_key and k == ord(allow_quit_key):
             cv2.destroyWindow(win)
             return None
 
 # -------------------------
 # 2) U-Net モデル定義(軽量版)
 # -------------------------
 def build_unet(input_shape=(256,256,3), n_classes=2, base_filters=32):
     inputs = Input(shape=input_shape)
     # Encoder
     c1 = conv_block(inputs, base_filters)
     p1 = layers.MaxPool2D()(c1)
     c2 = conv_block(p1, base_filters*2)
     p2 = layers.MaxPool2D()(c2)
     c3 = conv_block(p2, base_filters*4)
     p3 = layers.MaxPool2D()(c3)
     c4 = conv_block(p3, base_filters*8)
     p4 = layers.MaxPool2D()(c4)
 
     b = conv_block(p4, base_filters*16)
 
     # Decoder
     u1 = layers.UpSampling2D()(b)
     u1 = layers.Concatenate()([u1, c4])
     c5 = conv_block(u1, base_filters*8)
 
     u2 = layers.UpSampling2D()(c5)
     u2 = layers.Concatenate()([u2, c3])
     c6 = conv_block(u2, base_filters*4)
 
     u3 = layers.UpSampling2D()(c6)
     u3 = layers.Concatenate()([u3, c2])
     c7 = conv_block(u3, base_filters*2)
 
     u4 = layers.UpSampling2D()(c7)
     u4 = layers.Concatenate()([u4, c1])
     c8 = conv_block(u4, base_filters)
 
     outputs = layers.Conv2D(n_classes, kernel_size=1, activation='softmax')(c8)
     model = Model(inputs, outputs)
     model.compile(optimizer=Adam(1e-4), loss='sparse_categorical_crossentropy', metrics=['accuracy'])
     return model
 
 def conv_block(x, filters):
     x = layers.Conv2D(filters, 3, padding='same', activation='relu')(x)
     x = layers.BatchNormalization()(x)
     x = layers.Conv2D(filters, 3, padding='same', activation='relu')(x)
     x = layers.BatchNormalization()(x)
     return x
 
 # -------------------------
 # 3) Data loader for training
 # -------------------------
 def load_dataset(img_dir=TRAIN_IMAGES_DIR, mask_dir=MASKS_DIR, img_size=(256,256)):
     """
       - mask が missing の場合、可能なら rust-only mask を探して bridge mask を自動生成して統合する
       - 統合後: mask values -> 0(background) / 2(rust)
     """
     imgs = []
     masks = []
     files = sorted([f for f in os.listdir(img_dir) if f.lower().endswith(('.jpg','.png','.jpeg'))])
     for f in files:
         base = os.path.splitext(f)[0]
         img_path = os.path.join(img_dir, f)
         mask_path = os.path.join(mask_dir, base + "_mask.png")
 
         img = cv2.imread(img_path)
         if img is None:
             print("Failed to load image:", img_path); continue
 
         # ---- マスクが存在しない場合の自動処理 ----
         if not os.path.exists(mask_path):
             # try to find a rust-only mask: either base+"_rust.png" or base+"_rustmask.png" etc.
             rust_candidates = [
                 os.path.join(mask_dir, base + "_rust.png"),
                 os.path.join(mask_dir, base + "_rustmask.png"),
                 os.path.join(mask_dir, base + "_r.png"),
                 os.path.join(mask_dir, base + ".png"),
             ]
             rust_found = None
             for rc in rust_candidates:
                 if os.path.exists(rc):
                     rust_found = rc
                     break
             if rust_found:
                 # generate bridge mask from rust mask
                 bridge_mask = generate_bridge_mask_from_rust_mask(rust_found,
                                      out_bridge_mask_path=os.path.join(mask_dir, base + "_bridge_auto.png"))
                 # Now combine: create a 0/1/2 mask: start with zeros, set bridge=1, rust=2
                 combined = np.zeros(img.shape[:2], dtype=np.uint8)
                 combined[bridge_mask > 0] = 1
                 rust_img = cv2.imread(rust_found, cv2.IMREAD_UNCHANGED)
                 if rust_img is not None:
                     if rust_img.ndim == 3:
                         rust_gray = cv2.cvtColor(rust_img, cv2.COLOR_BGR2GRAY)
                     else:
                         rust_gray = rust_img
                     combined[rust_gray > 0] = 2
                 mask = combined
                 # save combined mask
                 cv2.imwrite(mask_path, mask)
                 print("Auto-generated mask saved:", mask_path)
             else:
                 print("Mask not found and no rust candidate:", base, "-> skipping")
                 continue
         else:
             mask = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED)
             if mask is None:
                 print("Invalid mask for", f, "- skipping"); continue
 
         # resize and append
         img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
         img_resized = cv2.resize(img_rgb, img_size)
         mask_resized = cv2.resize(mask, img_size, interpolation=cv2.INTER_NEAREST)
 
         imgs.append(img_resized.astype(np.float32) / 255.0)
         masks.append(mask_resized.astype(np.uint8))
     if not imgs:
         raise RuntimeError("No training pairs found. Run annotate_mode or auto-generation first.")
     X = np.stack(imgs, axis=0)
     Y = np.stack(masks, axis=0)
     return X, Y
 
 # -------------------------
 # 4) Training function
 # -------------------------
 def train_unet(epochs=20, batch_size=8, img_size=(256,256)):
     ensure_dirs()
     X, Y = load_dataset(img_size=img_size)
     X_train, X_val, Y_train, Y_val = train_test_split(X, Y, test_size=0.15, random_state=RANDOM_SEED)
     model = build_unet(input_shape=(*img_size, 3), n_classes=2)  # 0=bg, 1=rust
     print(model.summary())
 
     ckpt = ModelCheckpoint(UNET_MODEL_PATH, save_best_only=True, monitor='val_loss')
     es = EarlyStopping(patience=6, restore_best_weights=True)
     history = model.fit(X_train, Y_train, validation_data=(X_val, Y_val),
                         epochs=epochs, batch_size=batch_size, callbacks=[ckpt, es], verbose=2)
     print("Training finished. Model saved to", UNET_MODEL_PATH)
     return model, history
 
 # -------------------------
 # 5) U-Net 推論(bridge + rust マスクを返す)
 # -------------------------
 def unet_predict(img_bgr, model=None, img_size=(256,256)):
     """
     img_bgr: OpenCV BGR image
     returns: pred_mask (H,W) with values 0 or 1 (argmax over 2 classes)
     """
     if model is None:
         if not os.path.exists(UNET_MODEL_PATH):
             raise RuntimeError("No UNET model found. Train first.")
         model = tf.keras.models.load_model(UNET_MODEL_PATH, compile=False)
 
     orig_h, orig_w = img_bgr.shape[:2]
     img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
     inp = cv2.resize(img_rgb, img_size).astype(np.float32) / 255.0
     pred = model.predict(np.expand_dims(inp, axis=0), verbose=0)[0]  # (H,W,2)
     pred_mask = np.argmax(pred, axis=-1).astype(np.uint8)  # 0 or 1
     # resize back to original size
     pred_mask = cv2.resize(pred_mask, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST)
     return pred_mask
 
 # -------------------------
 # 6) Load ensemble CNN models (optional) for score prediction
 # -------------------------
 def load_ensemble_models(model_paths):
     models_list = []
     for p in model_paths:
         if os.path.exists(p):
             try:
                 m = tf.keras.models.load_model(p, compile=False)
                 models_list.append(m)
                 print("Loaded model:", p)
             except Exception as e:
                 print("Failed to load", p, ":", e)
         else:
             print("Model not found, skip:", p)
     return models_list
 
 ensem_models = load_ensemble_models(ENSEMBLE_MODELS)
 
 def predict_patch_score(patch_bgr):
     # パッチが 1px でも欠けていると CNN に入れられないため安全対策
     if patch_bgr is None or patch_bgr.shape[0] < 16 or patch_bgr.shape[1] < 16:
         return 0.0   # または skip
     
     # CNN の入力は必ず 256×256 に統一
     resized = cv2.resize(patch_bgr, (256, 256))
     x = resized.astype(np.float32) / 255.0
     x = np.expand_dims(x, axis=0)
 
     if not ensem_models:
         return float(x.mean()*5.0)
 
     preds = []
     for m in ensem_models:
         pred = m.predict(x, verbose=0)[0][0]
         preds.append(pred)
 
     return float(np.mean(preds))
 
 # -------------------------
 # 7) Detection & scoring pipeline (for drone images)
 # -------------------------
 SCORE_COLORS = {
     1: (0, 0, 255),      # 赤
     2: (0, 165, 255),    # 橙
     3: (0, 255, 255),    # 黄
     4: (0, 255, 0),      # 緑
     5: (255, 255, 0),    # 水色
 }
 
 def score_to_color(score):
     # score in continuous range ~ [0..5.5], convert to discrete 1..5
     if score < 1.9: s = 1
     elif score < 2.6: s = 2
     elif score < 3.3: s = 3
     elif score < 4.0: s = 4
     else: s = 5
     return SCORE_COLORS[s], s
 
 def detect_and_score(drone_dir=DRONE_IMAGES_DIR, out_dir=RESULTS_DIR, stride=STRIDE, patch_size=PATCH_SIZE):
     ensure_dirs()
     if not os.path.exists(UNET_MODEL_PATH):
         print("UNET model not found. Please train first.")
         return
     unet_model = tf.keras.models.load_model(UNET_MODEL_PATH, compile=False)
 
     files = sorted([f for f in os.listdir(drone_dir) if f.lower().endswith(('.jpg','.png','.jpeg'))])
     for fname in files:
         path = os.path.join(drone_dir, fname)
         img = cv2.imread(path)
         if img is None:
             print("Failed to read", path); continue
         orig = img.copy()
         print("Processing:", fname)
 
         # Predict mask (0=bg,1=rust)
         pred_mask = unet_predict(img, model=unet_model, img_size=(256,256))
         rust_mask = (pred_mask == 1).astype(np.uint8) * 255
 
         # 使用する領域:錆が検出されたバウンディングボックス
         ys, xs = np.where(rust_mask > 0)
         if len(ys) == 0:
             print("No rust found in", fname); 
             # それでも出力用フォルダは作る
             out_base = os.path.join(out_dir, os.path.splitext(fname)[0])
             os.makedirs(out_base, exist_ok=True)
             cv2.imwrite(os.path.join(out_base, "orig.jpg"), orig)
             cv2.imwrite(os.path.join(out_base, "pred_mask.png"), pred_mask*255)
             cv2.imwrite(os.path.join(out_base, "rust_mask.png"), rust_mask)
             continue
 
         miny, maxy = ys.min(), ys.max()
         minx, maxx = xs.min(), xs.max()
 
         heatmap = np.zeros((img.shape[0], img.shape[1]), dtype=np.float32)
         countmap = np.zeros_like(heatmap)
 
         # スライディング窓を錆のバウンディング箱だけで回す
         y_start = max(miny - patch_size, 0)
         y_end = min(maxy + patch_size, img.shape[0])
         x_start = max(minx - patch_size, 0)
         x_end = min(maxx + patch_size, img.shape[1])
 
         for y in range(y_start, y_end - patch_size + 1, stride):
             for x in range(x_start, x_end - patch_size + 1, stride):
                 patch_rust = rust_mask[y:y+patch_size, x:x+patch_size]
                 # 錆ピクセルが十分に含まれるパッチのみ評価
                 if patch_rust.sum() < 10:
                     continue
                 patch = img[y:y+patch_size, x:x+patch_size]
                 score = predict_patch_score(patch)  # 連続スコア
                 heatmap[y:y+patch_size, x:x+patch_size] += score
                 countmap[y:y+patch_size, x:x+patch_size] += 1
 
         # 平均マップ計算(count>0 のみ)
         avg_map = np.zeros_like(heatmap)
         mask_count = countmap > 0
         avg_map[mask_count] = heatmap[mask_count] / countmap[mask_count]
         # for visualization, create colored overlay
         overlay = img.copy()
         legend_items = []
         # create color-coded overlay by mapping each cell to discrete color
         disp = img.copy()
         for y in range(0, img.shape[0]-patch_size+1, 4):  # downsample display painting for speed
             for x in range(0, img.shape[1]-patch_size+1, 4):
                 val = avg_map[y, x]
                 if val <= 0: continue
                 color, s = score_to_color(val)
                 cv2.rectangle(disp, (x, y), (x+4, y+4), color, -1)
 
         # blend original and disp
         blended = cv2.addWeighted(img, 0.6, disp, 0.4, 0)
 
         # compute overall average score for area
         if mask_count.any():
             global_avg = float(avg_map[mask_count].mean())
         else:
             global_avg = 0.0
 
         # annotate global avg left-bottom, legend right-bottom
         text = f"avg_score: {global_avg:.2f}"
         cv2.putText(blended, text, (10, img.shape[0]-30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,255,255), 2, cv2.LINE_AA)
  
         # legend
         legend_h = 160
         legend_w = 200
         legend = np.zeros((legend_h, legend_w, 3), dtype=np.uint8)
         cv2.putText(legend, "Legend (score)", (10,20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200,200,200), 1)
         y0 = 40
         i = 0
         for s in sorted(SCORE_COLORS.keys()):
             color = SCORE_COLORS[s]
             cv2.rectangle(legend, (10, y0+i*24), (30, y0+14+i*24), color, -1)
             cv2.putText(legend, f"{s}", (40, y0+12+i*24), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200,200,200), 1)
             i += 1
         # paste legend on blended bottom-right
         lh, lw = legend.shape[:2]
         blended[-lh-10:-10, -lw-10:-10] = legend
 
         # save outputs
         out_base = os.path.join(out_dir, os.path.splitext(fname)[0])
         os.makedirs(out_base, exist_ok=True)
         cv2.imwrite(os.path.join(out_base, "orig.jpg"), orig)
         cv2.imwrite(os.path.join(out_base, "pred_mask.png"), pred_mask)
         cv2.imwrite(os.path.join(out_base, "rust_mask.png"), rust_mask)
         cv2.imwrite(os.path.join(out_base, "heatmap_overlay.jpg"), blended)
         print("Saved results to", out_base)
 
 # -------------------------
 # 8) small CLI to run steps
 # -------------------------
 def print_menu():
     print("""
 rust_unet_system - menu:
 1) Annotate images (create masks)
 2) Train UNet (from masks)
 3) Run detection & scoring on drone_images
 4) Exit
     """)
 
 def main():
     ensure_dirs()
     while True:
         print_menu()
         c = input("Choose option: ").strip()
         if c == '1':
             print("Starting annotation mode...")
             annotate_mode()
         elif c == '2':
             print("Starting training...")
             epochs = int(input("epochs [default 20]: ") or 20)
             batch = int(input("batch_size [default 8]: ") or 8)
             img_s = int(input("image_size (square) [default 256]: ") or 256)
             train_unet(epochs=epochs, batch_size=batch, img_size=(img_s, img_s))
         elif c == '3':
             print("Running detection & scoring...")
             detect_and_score()
         elif c in ('4','q','quit','exit'):
             print("Exit.")
             break
         else:
             print("Unknown option.")
 
 if __name__ == "__main__":
     main()

-①  画像アノテーション(マスク作成)モード

--目的:U-Net が学習するための「教師データ(錆領域マスク)」を作る。

--動作内容
---rain_images/ 内の画像を1枚ずつ表示して錆領域をポリゴンで描く → そのポリゴンをマスク画像 ”<画像名>_mask.png” として保存

--ポイント
---ポリゴンは1つだけ(錆領域のみ)
---以前のように「bridge」「rust」の2段階の描画ではない
---一度マスクが作られたら、その後は何度も学習に再利用可能

-② U-Net の学習モード

--目的:アノテーション済みマスクを使って錆領域を自動検出する U-Net を学習させる。

--処理内容
---train_images/ の画像とmasks/ 内の対応マスク(0:背景/2:錆の2クラス)を読み込む
---画像とマスクを (256×256) にリサイズしセットを作る
---U-Net(簡易軽量版)を構築

--学習後に得られるもの
---錆の領域をピクセル単位で推定できる U-Netファイル

-③ ドローン画像 → 錆領域抽出 → 評点ヒートマップ生成モード

-目的:ドローンで撮影した広い橋側面画像に対してU-Net で錆領域を検出し、その錆が写っている部分をスライスして CNN 評点を推定 → ヒートマップとして可視化して保存

・処理詳細
--(1) U-Net により錆領域の抽出
---入力画像を読み込み U-Net で推定  (pred_mask==0 → 背景, pred_mask==2 → 錆)
---必要なのは錆領域のみなのでrust_mask = (pred_mask == 2) によって抽出

--(2) 錆領域の周囲でスライドウィンドウ実施
---錆領域の bounding box(範囲)を計算し、その範囲でパッチサイズとストライドをそれぞれ設定して画像をスライスする。(今までは手動で領域選択を行っていたが今回から自動化した)

--(3) パッチごとに CNN でスコア予測
---スライスされたパッチは CNN(あなたが学習した3つのDenseNetモデルのアンサンブル)→ 0〜5.5の回帰値が得られるのでBinningで変換

--(4) スコアをヒートマップ領域に加算し、ピクセルごとの平均スコアに変換することで"領域全体の平均評点" がピクセル単位(ヒートマップ)で可視化可能になる。
--(5) 凡例と平均スコアを追記して保存
---左下:全体平均評点(avg_score)
---右下:凡例(1〜5の色)

-「錆の領域抽出(U-Net)+ ヒートマップで評点可視化」 を一つのパイプラインとして実行できるように設計されている。

-11/21

ヒートマップを作成するのに必要な撮影画像のピクセルサイズは大きければ大きいほど望ましい。(現在、横5280ピクセルで作成中)

上記のスクリプトを実行して錆とそうでない部分(ここでは背景と定義)を学習させて正確に錆部分を抽出できるように尽力し、前回よりも背景部分にヒートマップが描かれる割合が減少したように思える。背景だけの画像を撮影して学習させるのもありか。
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251120/1.jpg,300x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251120/2.jpg,300x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251120/3.jpg,300x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251120/4.jpg,300x); 
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251120/5.jpg,300x); 

-11/25

錆だけを予め学習させた状態でドローン撮影画像にヒートマップを描こうとすると橋面だけでなく欄干や堤防にまで色が塗られてしまうので、橋が写っていないただ川と表法面等に生えている植物群の写った背景写真を学習させて領域抽出の精度を向上させることにした。背景写真は Google map のストリートビューで条件を満たしている場所を探してスクリーンショットした。これからは背景, 橋側面, 錆の3種類を学習させて評価する方向性で進めていく。

&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251125/1.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251125/2.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251125/3.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251125/4.jpg,300x);
&ref(http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/20251125/5.jpg,300x);

***11月第5週(11/27〜12/3) [#j417c1127]

-11/26

◯ 1. U-Net を用いた錆領域抽出の開始

従来の手法では錆の抽出に SAM(Segment Anything Model)や色閾値処理などを使用していたが「橋側面以外(背景・河川・影など)が誤って錆として検出される」という問題が継続的に発生していたため、錆領域の判別精度を向上させる目的で U-Net によるセマンティックセグメンテーション方式へ移行した。U-netを導入した理由として
-ピクセル単位の分類能力が高い
-少量の教師データでも学習しやすい
-橋面に対して斜めから撮影された画像でも比較的安定した結果が得られる

◯ 2. 必要なクラス構造(背景・橋側面・錆)

U-Net 導入前までは「錆(対象)」と「それ以外(背景)」という 2 クラスモデルを使用していたが誤検出が多発したため、3 クラスモデルに拡張した

, クラス(class)番号 , 分類内容 , 役割 
, class0 , 背景 , 河川、空、堤防、民家、植物など「錆領域ではない」部分 
, class1 , 橋側面 , U-Net が抽出すべき橋のみを特定 
, class2 , 錆 , 評点推定の入力として利用 

◯ 3. 教師データの整備

教師データは以下の 3 種類で構成される

, 教師データ , クラス番号 , データ利用方法 
, 近接錆画像(256×256ピクセル四方),  class2(= 錆) , 錆の色・模様・質感の学習に利用 
, 近影画像(橋側面) , class1(= 橋側面)  , 背景が写り込むため橋側面の境界抽出・学習に利用 
, 背景画像(ただの風景画), class0(= 背景) , 橋が写っていない画像を学習させることでU-Net の背景認識能力を大幅に向上させる 


◯ 4. マスク画像(≒モノクロ画像)の作成

-① ポリゴン選択 UI により橋側面を手動で指定
-② class1 として塗りつぶし
-③ 錆部分がある場合は追加で class2 を塗り分ける
-④ 近接錆画像と背景画像は、自動で class2 / class0 としてマスク生成される。


◯ 5. U-Net の学習

U-Net の入出力は以下の通り
-入力:256×256 RGB値
-出力:256×256 の 3 クラス分類マップ(0,1,2)

学習プロセスでは EarlyStopping を導入しベストなエポックでのモデルを自動的に採用する方式を採用し、結果は .keras ファイルとして保存される


◯ 6. 橋側面の自動抽出

U-Net によって作成された「class1=橋側面」領域は自動的に抽出され、後続の評点処理に利用される。まず U-Net が 橋側面(class1)マスクを生成し、 class1 マスク内でのみ錆(class2) を探索することで背景(class0)は完全に排除されるという算段。

◯7. 錆ヒートマップ生成(class2 → 評点)

橋側面マスクが生成されたら次に錆領域を小領域パッチに分割しつつCNN を用いて評点(スコア)を推定し、最終的に以下を可視化する。
-錆ヒートマップ(5 色)
-橋側面マスク(白黒)
-予測された錆マスク
-元画像

スクリプトの改良を行ったところヒートマップが表示されなくなってしまった。原因は撮影画像からマスク画像に変換する際に橋側面を上手く抽出できておらず、画面が真っ黒になったことで撮影画像には側面がないと返されてしまったと考えられる。処理に関しては自動的に橋側面を白, それ以外の部分は黒として変換し、白になった部分にヒートマップが描かれるようにしている。

**12月 [#j417c1200]
***12月第1週(12/4〜12/10) [#j417c1204]
パラメータを調節しながら結果を見比べている

-検討要素
--画像の水増し…現在は撮影した画像そのものを学習させたいため、回転や明度変化をおこなっていないのだがカラーの錆近接画像に関しては水増しを行ってもいいのかもしれない。
--学習データ比較…1回各要素の学習枚数を1枚とかに変えて学習させるのはどうか。本当に学習結果が反映されているか確認できると思う




[[↑上がるゥ?>#contents]]

**画像のスキャンについて [#j991c400]
錆画像スキャン方法
・スキャナーは研究室にあるものを使用する
・使用するソフトはXSane(デスクトップの左上 "メニュー → すべて → XSane")

-1, XSane を立ち上げてOKを押す
-2, ウィンドウを消したい or 表示させたい場合はウィンドウの部分をクリックすると任意で表示を設定可能
-3, 様々な色が表示されているアイコンでカラー, モノクロに変更可能
-4, 3の2つ下にある部分で解像度の変更が可能

[[XSaneの詳しい使い方はこちら >https://kledgeb.blogspot.com/2014/07/ubuntu-xsane-1-xsaneui.html]]






































































































































*創造工房実習 [#v035a40a]
**4/18 [#j417c418]
 創造工房実習でI型断面等の三角形分布や塑性状態がどういう応力分布になっているのかを確認するのを忘れていたのでここに書いておく。
-I型断面に10Nの面載荷をかけたとき
--梁の断面に赤と青で濃く色分けされており、上下縁の両方は降伏に達していると確認できた。
--固定端近くでは、直応力σxxがほぼ対称に上縁で圧縮、下縁で引張になっており三角形状の応力分布に近い。一方で真ん中付近が少し平坦になっている部分もあるため全体的に見ると三角形分布ではないといえる。
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/1/tejun31.png 
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/1/tejun32.png 
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/1/tejun33.png 
-15Nの面載荷をかけたとき
--10Nの荷重をかけたときよりも梁の上下縁にかなりの力がかかっている。ただ解析ステップを10に設定しているところParavis上では22ステップになっているため各種データの信憑性は100%とは言い切れない。→10ステップにならないのは解析がうまく行っていない証拠。ちなみに面載荷を20N以上に設定するとエラーが出る。
--固定端近くでは10Nのときと同様、真ん中から先端付近にかけて上縁部分は三角形に近い形で応力分布が現れている。固定面付近ではかなりの直応力がかかっているがそれ以外の部分は三角形に近い形で応力分布が現れているといって良いのではないか。

http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/1/tejun34.png 
-5N, 6Nの面載荷をかけたとき
--固定面側は最大281MPaの直応力が加わっている上縁がありこの時点で塑性状態に入っていることが分かった。(6N)
--下端側よりも上端側のほうが直応力の値が高く出る傾向にある。面載荷にしているのになぜなのか。もしかしたら面載荷 = 等分布荷重として捉えることができ、それを1点にかかる集中荷重に置き換えるとたわんでいない状態での梁における中立軸よりも下端側にかかるため上端側のほうが直応力の値が高く出るのではないか。

http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/1/tejun35.png 
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/1/tejun36.png 

 上端

 ・ーーーーーーーーーー

 |          ーーーーー

 ・ーーーーーーーーー      ーーーー|

 下端        ーーーーー     |↓集中荷重位置   このように上端ー集中荷重位置の距離が下端よりも長いことから上端側のほうが直応力が大きい。

                ーーーーー|          逆方向に面載荷をかければ下端側の直応力が大きくなるだろう。

**4/17 [#j417c906]
-SALOME2021でメッシュが計算されない事例が発生
--1, メッシュの編集画面を開く
--2, アルゴリズムで"NETGEN 1D-2D-3D" を選択
--3, その下にある詳細設定右側の歯車をクリックして "NETGEN 3D Parameters" を選択すると最大サイズと最小サイズが任意で選択できる
--4, 最大サイズと最小サイズに数値を入れてOKをクリック (最小サイズは0にしても問題なくメッシュ計算ができる)
--5, メッシュの編集画面に戻るので"適用して閉じる"をクリック
--6, 左側のオブジェクトブラウザーにある"Mesh_1"を右クリックして"メッシュを作成"を選択するとメッシュの計算が開始される

**4/14 [#ja17c906]
-載荷点での荷重、たわみのプロット
   1ステップごとに面に載荷した荷重の10分の1を加えたものーDEPLとのグラフを作成
-軸方向直応力が最大となる点での直応力-直ひずみのプロット(降伏点付近で折れ曲がるか)
  知りたい方向のEPSI_NOEUーSIGM_NOEUとのグラフを作成 (0ならx軸 1ならy軸 2ならz軸)
-その点での相当応力(ミーゼス応力)―相当ひずみのプロット(上記との違いは)
  知りたい方向のSIEQ_NOEUーEPSI_NOEUのx,y,zの直ひずみ,せん断ひずみの計6つのひずみを相当ひずみの計算式に代入して算出されたものとのグラフを作成
-曲げモーメントが最大となる断面の軸方向直応力$\sigma_{zz}$を各荷重レベルでプロット($yz$の2次元と、できれば3次元も)し応力の三角形分布を確認する
  曲げモーメント最大は固定端(片持ち梁の場合)なので0に限りなく近い所でスライスし、SIGM_NOEUが最大になっている点を探し面に載荷した荷重の10分の1と理論値(荷重×荷重をかけた箇所からの距離)とのグラフを作成


**4/10 [#ja87c906]
-片持ち梁にかける荷重を6.25N以上にするとParavisでtime stepが0.8sあたりから0.25s間隔になる。→解析が上手く行っていないのこと
-6.25Nで解析を行うと実際の結果が降伏応力に収束しなくなる。グラフを描くなら7.5~8.0Nが望ましい。

-明日やること
--応力(ミーゼス)-ひずみ,応力-たわみのグラフを作成
--スライドの作成

http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/kekka6.2N.png
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/kekka8.0N.png

,経過時間,↖たわみ(mm),↖応力(MPa),↗たわみ(mm),↗応力(MPa)
,0,0,0,0,0
,1,0.000140332,34.4925,0.000184238,44.0832
,2,0.000280661,68.9829,0.000368391,88.1643
,3,0.000420986,103.471,0.000552457,132.243
,4,0.000561308,137.957,0.000736434,176.318
,5,0.000701624,172.44,0.000923909,220.147
,6,0.000842636,206.506,0.00123269,241.658
,7,0.00101987,237.23,0.002051,244.44
,8,0.00130101,257.808,0.00765896,243.452
,9,0.00191133,268.432,0.0109555,241.582
,10,0.00377436,280.533,0.0148525,239.658
,11,ー,ー,0.0191575,237.724
,12,ー,ー,0.0237442,235.762
,13,ー,ー,0.0285207,233.804
,14,ー,ー,0.0333983,231.819
,15,ー,ー,0.0383132,229.768
,16,ー,ー,0.043212,227.625



**4/7 [#ja87c006]
-Paravisでのグラフ作成方法
--https://www.rccm.co.jp/icem/pukiwiki/index.php?%E9%81%B8%E6%8A%9E%E3%81%97%E3%81%9F%E7%AF%80%E7%82%B9%E3%81%A7%E3%81%AE%E6%99%82%E7%B3%BB%E5%88%97%E3%83%97%E3%83%AD%E3%83%83%E3%83%88
--https://www.rccm.co.jp/icem/pukiwiki/index.php?%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%BC%20Plot%20Data%20Over%20Time

--Salomeで荷重をかける段階を設定する際に1.0と入力する箇所に小数点が入力できず01と入ってしまう。
 ⇒一度01と入力しOKを押した後再度1.0と入力する画面に戻ると小数点を入力できるようになっている。
--また弾塑性解析を行う際にSalome-Meca演習_弾塑性解析(2021)に書いてある手順で進めると解析を回した際にエラーを吐かれる。
 ⇒原因は関数がある範囲までしか定義されていないの事。Function and ListsのDEFI_FONCTIONにおいて関数の外側も定義できるようにCONSTANTまたはLINEARを追加しておく必要がある。





**2/7 [#ja87c596]
春休みの課題
-弾塑性班 
--鋼材の長方形断面の梁(想像しやすい大きさ)の片持ち梁と単純梁を曲げる
--軸方向:$z$, たわみ方向:$y$
--SS400ぐらいのヤング率と降伏応力で弾塑性解析の設定
--載荷点での荷重、たわみのプロット
--軸方向直応力が最大となる点での直応力-直ひずみのプロット(降伏点付近で折れ曲がるか)
--その点での相当応力(ミーゼス応力)―相当ひずみのプロット(上記との違いは)
--曲げモーメントが最大となる断面の軸方向直応力$\sigma_{zz}$を各荷重レベルでプロット($yz$の2次元と、できれば3次元も)し応力の三角形分布を確認する
--上記の三角形分布において、上下縁から徐々に降伏が入ってきて、最終的に全塑性の状態になるか。
--上記を確認できたら、I型断面とかの三角形分布や全塑性が、どういう応力分布になっているのかを確認。


**12/20 [#w0335ea2]
今日はviを用いて論文に画像を貼る方法について学んだ。


**12/13 [#w0335ea3]
今日はviを用いた論文の書き方を学んだ。


・文頭に%をつけるとその行に書かれた文章は反映されない


・強制改行したい場合は"\\"を入力する


・更新→:!pdfplatexsibup2


○式を書く(書き方は編集画面から)

$v=\frac{P\ell^{3}}{48EI} + \frac{P\ell}{4kGA}$

\begin{eqnarray}
v=\frac{P\ell^{3}}{48EI} + \frac{P\ell}{4kGA}
\\
I=\int_{A}y^{2}dA
\end{eqnarray}

**11/29 [#ceb57a7c]
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/sand.png

今回は鋼材で木材を挟んだサンドイッチ梁の解析を行い、縦軸に変位(mm), 横軸にボリューム数をとって上のグラフを作成した。
メッシュ長さの大小関係なく理論値と20%の誤差が生じ、これ以上メッシュ長さを短くしても理論値には近づくことがないと推測する。サンドイッチ梁は多分見たことがない以上イメージが湧かず誤差の推測をしようがないので実物にこの目で見て実験を行いたい。今回は鋼材で木材を挟んだサンドイッチ梁を解析したが、実用性を一旦置いて木材で鋼材を挟んだサンドイッチ梁の場合結果はどうなるのだろうか。

全員で作成した解析結果のグラフを下に示しておく

,メッシュ長さ,要素数,先端変位(4隅の平均値)[mm],相対誤差($\frac{salome-手計算}{手計算}$),計算者
,0.7,155419,0.0772,26.943,湊
,0.8,138734,0.0775,26.452,湊
,0.9,82935,0.0774,26.614,湊
,1.1,38671,0.0766,27.937,森井
,1.2,32044,0.0770,27.273,森井
,1.3,28599,0.0768,27.604,森井
,1.4,23950,0.07640,22.04,米谷
,1.5,19998,0.07641,22.03,米谷
,1.6,19448,0.07715,21.28,米谷
,1.7,13801,0.07567,22.79,米谷
,1.8,12677,0.07736,21.06,沼野
,1.9,11464,0.07546,23.00,沼野
,2,10699,0.07404,24.45,沼野
,3,3579,0.08414,15.004,國井
,4,1628,0.08279,16.37,國井
,5,1016,0.08303,16.26,國井
,6,839,0.08288,16.26,西澤
,7,554,0.08087,18.28,西澤
,8,285,0.07898,19.20,西澤
,9,261,0.01421,85.49,真庭
,10,232,0.03380,65.51,真庭
,11,208,0.00913,90.68,真庭

**11/22 [#ceb57a7d]
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/kadai1122.png

今回は単純異方性と等方性の解析を行い、縦軸に変位(mm), 横軸にボリューム数をとって上のグラフを作成した。異方性は上2つのグラフであるが、メッシュ長さを2以下で解析を行うと理論値(緑)と近しい値をとるようになる。一方で等方性の場合はメッシュ長さが5までなら理論値と似たような値をとる結果となった。前回まではメッシュを細かくするほど理論値に近づいたが異方性の場合はメッシュ長さを1.3にした時が理論値に一番近づき、等方性の場合は理論値と平行関係になってしまった。さらに長さを小さくして解析しても理論値には近づくことがないのではないか。

全員で作成した解析結果のグラフを下に示しておく

,メッシュ長さ,要素数,変位(異方性)[mm],相対誤差-異方性($\frac{salome-手計算}{手計算}$),変位(等方性)[mm],相対誤差-等方性($\frac{salome-手計算}{手計算}$),計算者
,0.7,171996,0.5068,2.993,0.4301,3.141,湊
,0.8,161561,0.5069,2.999,0.4300,3.116,湊
,0.9,94185,0.5021,2.071,0.4301,3.139,湊
,1.1,47998,0.4957,0.814,0.4122,1.056,森井
,1.2,47343,0.4952,0.712,0.4300,3.217,森井
,1.3,42112,0.4941,0.488,0.4298,3.169,森井
,1.4,38960,0.4937,0.407,0.4299,3.193,森井
,1.5,15041,0.4845,1.460,0.4298,3.179,米谷
,1.6,16071,0.4849,1.380,0.4298,3.157,米谷
,1.7,12933,0.4845,1.460,0.4299,3.182,米谷
,1.8,12993,0.4832,1.73,0.4298,3.19,沼野
,1.9,11235,0.4783,2.73,0.4295,3.10,沼野
,2,11456,0.4982,1.32,0.4296,3.12,沼野
,3,2514,0.4369,4.87,0.4293,3.05,國井
,4,1461,0.4341,4.20,0.4293,3.05,國井
,5,433,0.2803,32.7,0.4284,2.83,國井
,6,356,0.4283,2.80,0.3437,17.5,西澤
,7,102,0.4260,2.26,0.2225,46.6,西澤
,8,93,0.4260,2.26,0.1123,73.0,西澤
,9,81,0.2212,54.9,0.4255,2.13,真庭
,10,84,0.2051,58.3,0.4247,1.95,真庭
,11,74,0.2260,54.0,0.4246,1.91,真庭

**11/15 [#ceb57a7e]
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/kadai1115.png

単純梁の解析結果から縦軸に変位(mm), 横軸にボリューム数をとって上のグラフを作成した。
前回と同じくメッシュの長さを長くすると接点変位は小さくなり、相対誤差は大きくなった。前回は√nに近い形のグラフが描けた一方で今回は歪な形のグラフができてしまった。変曲点はメッシュ数が小さい(1メッシュあたりの長さを長くした)方もといグラフ左側に偏っており、メッシュ長さを長くして解析を行うほど解析結果の信憑性は低くなるのではないか。他のPCと同じ解析を行った場合結果は一緒になるのだろうか?機会があればやってみたいものだ。

全員で作成した解析結果のグラフを下に示しておく

,メッシュ長さ,要素数,先端変位(4隅の平均値)[mm],相対誤差($\frac{salome-手計算}{手計算}$),計算者
,0.7,171996,0.4260,2.207,湊
,0.8,161561,0.4256,2.115,湊
,0.9,94185,0.4169,0.0719,湊
,1.1,47998,0.4122,1.067,森井
,1.2,47343,0.4118,1.166,森井
,1.3,42112,0.4113,1.289,森井
,1.4,38960,0.4112,1.313,森井
,1.5,15041,0.3978,4.516,米谷
,1.6,16071,0.3999,4.002,米谷
,1.7,12993,0.3971,4.687,米谷
,1.8,12203,0.3964,4.85,沼野
,1.9,11235,0.3942,5.38,沼野
,2,11456,0.3991,4.20,沼野
,3,2514,0.2141,21.4,國井
,4,1461,0.34028,18.4,國井
,5,433,0.1354,67.8,國井
,6,356,0.2135,48.8,西澤
,7,102,0.11,73.6,西澤
,8,93,0.112,73.0,西澤
,9,81,0.1125,73.0,真庭
,10,84,0.0794,80.9,真庭
,11,74,0.1297,68.9,真庭

**11/8 [#ceb57a7f]
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/kadai2.png

全員で行った解析の結果を縦軸に変位, 横軸にボリューム数をとってグラフを作成した。
メッシュの長さを長くすると接点変位は小さくなり、相対誤差は大きくなった。
一方でメッシュの長さを小さくするほど接点変位は断面二次モーメントで算出された6.67mmに近づいた。
メッシュ長さを0.5や0.3として解析を行えばもっと理論値に値が近づくのではないか。

全員で作成した解析結果のグラフを下に示しておく

,メッシュ長さ,要素数,先端変位(4隅の平均値)[mm],相対誤差($\frac{salome-手計算}{手計算}$),計算者
,0.7,107380,6.47,2.96,湊
,0.8,57821,6.44,3.62,湊
,0.9,57698,6.43,3.73,湊
,1.1,57980,6.44,3.57,湊
,1.2,52123,6.41,3.90,森井
,1.3,45549,6.34,4.98,森井
,1.4,26951,6.32,5.31,森井
,1.5,16904,6.25,6.32,米谷
,1.6,14296,6.20,7.05,米谷
,1.7,13596,6.21,6.81,米谷
,1.8,6299,5.74,13.9,沼野
,1.9,6001,5.73,14.1,沼野
,2,5617,5.65,15.3,沼野
,3,2309,5.48,17.8,國井
,4,617,3.62,45.6,國井
,5,494,3.85,42.3,國井
,6,581,2.51,62.4,西澤
,7,133,1.41,78.8,西澤
,8,78,1.29,80.7,西澤
,9,72,1.288,80.69,真庭
,10,60,1.226,81.62,真庭
,11,65,1.231,81.54,真庭


**11/1 [#w68ec49g]
http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/abc.png

http://www.str.ce.akita-u.ac.jp/~gotouhan/j2024/morii/123.png

今日(11/1)はwikiにグラフの貼り付けとviとlinuxを使ってグラフの作成を行った

上のグラフは自身で作成したもの、下のグラフは過去の先輩方が作成したグラフの数値を拝借した。

先輩方が作成したグラフの数値を下に示しておく

,メッシュの長さ	,要素数	,変位[mm],相対誤差,計算者
,0.7,155192,0.08378905246,15.365,安藤
,0.8,138808,0.08380386491,15.350,安藤
,0.9,82587,0.083707073981,15.45,兼田
,1.1,38671,0.084201207602,14.95,兼田
,1.2,31929,0.083688,15.466,柴田
,1.3,28621,0.083669,15.4857,柴田
,1.4,28854,0.08368,15.47,佐藤
,1.5,20015,0.084052,15.10,佐藤
,1.6,19448,0.0835402938,15.62,皆川
,1.7,13801,0.0834355098,15.72,皆川
,1.8,12528,0.083733,15.42,永山
,1.9,11769,0.083924,15.23,永山
,2,10699,0.084076876559,15.074,辻
,3,3579,0.08414561753,15.004,辻
,4,1628,0.082794,16.37,服部
,5,1016,0.083033,18.89,服部
,6,839,0.082882,16.26,梶原
,7,554,0.080871,18.28,梶原
,8,285,0.079995,19.20,工藤
,9,261,0.078980,20.22,工藤
,10,232,0.081911,17.26,佐々木
,11,208,0.075676,23.56,佐々木

**10/4 [#w0335eb3]
今日は顔合わせをした

頑張りたい
"いきものがかり"についた

トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS