ChatGPT の Code Interpreter でアニメ原画の領域(特徴点)認識プログラムを書かせたよ

# これは

- これは [ドワンゴ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dwango) 6日目の記事です
    - (ゆいもっぷアニメ作りについての) 前回の記事 (2023.10.11)
        - https://ao-love-yuimop.hatenablog.com/entry/2023/10/11/001819#%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3 (ゆいもっぷアニメ制作記 - アニメMV作り)
    - 去年のアドカレの記事 (2022.12.25🎄🏀)
        - https://qiita.com/ystmg/items/c78c2b3e0bcdf0f77125 (ゆいもっぷアニメ作り) (バスケットゆいもっぷ)

# この記事の<ins>概要</ins>

## 背景・経緯
- ↓で、 **萌え萌え** <ins>ゆいもっぷアニメが作れます</ins>
    - 「<ins>ゆいもっぷ</ins>(萌え萌え二次元)<ins>画像</ins>」<ins>複数枚</ins>で、<ins>特徴点</ins> (・左目の上端、・口の上端、・最も左にある肌の座標…など) を<ins>指定</ins>し、SmoothGraphic(※) と (i2iが使える) 画像生成AIを使う
    - その <ins>ゆいもっぷアニメ</ins> は↓の<ins>特徴</ins>がある
        - <ins>原画だけ</ins>で作れる (全フレーム分の動きが入力不要で、原画に絵として描かれた動きだけでOK)
        - <ins>どんな動きでも</ins>できる (とても大きな動きもできる)
        - 背景まで<ins>一貫製法</ins>
            - 背景の移動による動きの表現もばっちり (バスケットゆいもっぷ)
        - <ins>カメラモーション</ins>が自由 (しっかり回り込みとかズームイン/アウトとか俯瞰とかアオリとか平行移動もできる)
        - <ins>画面サイズ</ins>が大きくて綺麗 & 詳細まで描画される (前回(7/26)は 1344x896、次は辺の長さ+10%予定)
        - 原画で指定した状態に必ずできる (=絵に描けることなら全てさせれる)
        - <ins>長尺</ins>可能 (10秒でも60秒～でも)
        - 前回作品は 2023/7/26 に公開のものですが、フレーム間のチラつき (一貫性)は、次回 アレ を適用で解消を目指します
    - 詳しくはこちら: [あおのゆいもっぷアニメ制作記](https://ao-love-yuimop.hatenablog.com/entry/2023/10/11/001819#%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3) 
    - ※: SmoothGraphic Copyright(C)2013-2017　チラ裏エリア
- <ins>ChatGPT Plus</ins> は、ChatGPT に書かせた <ins>Python のプログラムを ChatGPTの中で実行できる</ins>し、その実行結果を元に回答を変えられるし(※)、 <ins>Pythonのプログラムを書くのがとても得意</ins>
    - この仕組みの改名の歴史: Code Interpreter (→ Advanced Data Analysis に改名 → ChatGPT-4 に統合) 
    - ※: 色のヒストグラムをChatGPTが作ってChatGPTが自分で確認して、「まあ、いくらかは必要な色があるので続行しよう」(後述) と判断してくれる、みたいな

## この記事でやったこと

- ゆいもっぷちゃんの<ins>特徴点の指定を人力でしないで済むよう</ins>に、<ins>ゆいもっぷ(20歳 酒豪(お酒ゲーム配信) 自動車教習所No.1教官 Vtuber)</ins> の **<ins>特徴点を取得できるプログラムを ChatGPTに書かせたよ</ins>** (※)
    - この記事を読むと、<ins>ChatGPTにどのようにプログラムを書かせられるかが分かる！</ins>
    - (実写の人間の特徴点取得ライブラリやソフトはネットでたくさん見つかるけど、萌え萌えイラストでは見つけられませんでした。いいものがあったら教えてください)
    - ※: (<ins>ゆいもっぷちゃんのほしいも: https://t.co/dEFPoqePSV </ins>)
        - (ポプテピピックみたいな名前の17%のお酒を所望しているようです)
- <ins>このように</ins>ゆいもっぷの特徴点を<ins>得られます</ins> (コード全文も↓↓に掲載)
- ![gen1-6_kamp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/f057a498-d60e-2b31-001b-f8ef47f7b9c0.png)
    - (↓↓にもっと結果画像あります)


## ゆいもっぷさんとは 🐶

- ご存知**激かわ**犬系自動車教習指導員(国家資格) Vtuber(20歳)
    - 今年(2023年)の抱負は「自動車教習ゲームのゲーム実況をする」
    - <ins>お酒が大好き</ins>で、お酒を飲みながらカワボ (イケボ) でゲーム配信してくれる😍

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">宝箱見つけた！！ <a href="https://twitter.com/hashtag/shorts?src=hash&amp;ref_src=twsrc%5Etfw">#shorts</a> <a href="https://twitter.com/hashtag/varkshorts?src=hash&amp;ref_src=twsrc%5Etfw">#varkshorts</a> <a href="https://t.co/5OZB80Xkn0">https://t.co/5OZB80Xkn0</a> <a href="https://t.co/EmgmGSAzwQ">pic.twitter.com/EmgmGSAzwQ</a></p>&mdash; ゆいもっぷ🍺犬系VTuber (@yui_mop) <a href="https://twitter.com/yui_mop/status/1730509645108892144?ref_src=twsrc%5Etfw">December 1, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

# 日頃やっていること

- <ins>ゆいもっぷアニメ</ins>を作っています (ゆいもっぷアニメクリエイター)
    - X User Screenname: @ao_love_yuimop
- 過去作品
    - 多分、リンク先のニコニコ動画だと綺麗に見れます(1080pを設定(歯車)で選ぶ)
    - 2023.7.26 の作品だけ、画面全体にランダムで現れるプロンプトを使っちゃってる(光や煙)ので、モヤがひどいですね… (他はそうでもない)
- 2023.7.26 作品 (⚠少しセンシティブ)

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">画像生成AIの中間画像作画でゆいもっぷさんの夏アニメMV作りました！ 手づかみラーメン！ (原画指定で動画化) 制作手法製作記は後日 SmoothGraphic Copyright(C)2013-2017 チラ裏エリア<br>Full(高画質):<a href="https://t.co/Mc5Smg6qV4">https://t.co/Mc5Smg6qV4</a> <a href="https://t.co/KWa5THQlAP">https://t.co/KWa5THQlAP</a> <a href="https://twitter.com/hashtag/AIArt?src=hash&amp;ref_src=twsrc%5Etfw">#AIArt</a> <a href="https://twitter.com/hashtag/AI%E3%82%A4%E3%83%A9%E3%82%B9%E3%83%88?src=hash&amp;ref_src=twsrc%5Etfw">#AIイラスト</a> <a href="https://twitter.com/hashtag/AIAnime?src=hash&amp;ref_src=twsrc%5Etfw">#AIAnime</a> <a href="https://twitter.com/hashtag/AI%E3%82%A2%E3%83%8B%E3%83%A1?src=hash&amp;ref_src=twsrc%5Etfw">#AIアニメ</a> <a href="https://twitter.com/hashtag/AIMovie?src=hash&amp;ref_src=twsrc%5Etfw">#AIMovie</a> <a href="https://t.co/Wk42ntFLNW">pic.twitter.com/Wk42ntFLNW</a></p>&mdash; あお (@ao_love_yuimop) <a href="https://twitter.com/ao_love_yuimop/status/1683876143102050305?ref_src=twsrc%5Etfw">July 25, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

- 2023.4.9 作品

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">画像生成AIのNovelAIやSmoothGraphic等を使って ゆいもっぷ ちゃん(20)の激カワな歌にアニメPVをつけました(ファンアニメ)🏖<br>SmoothGraphic Copyright(C)2013-2017　チラ裏エリア <br><br>Full: <a href="https://t.co/9fcWqWJ481">https://t.co/9fcWqWJ481</a> <a href="https://t.co/ZuwuqFNS5Y">https://t.co/ZuwuqFNS5Y</a> (digest)<a href="https://twitter.com/hashtag/AIArt?src=hash&amp;ref_src=twsrc%5Etfw">#AIArt</a> <a href="https://twitter.com/hashtag/AI%E3%82%A4%E3%83%A9%E3%82%B9%E3%83%88?src=hash&amp;ref_src=twsrc%5Etfw">#AIイラスト</a> <a href="https://twitter.com/hashtag/AIAnime?src=hash&amp;ref_src=twsrc%5Etfw">#AIAnime</a> <a href="https://t.co/3GdAdOOsV9">pic.twitter.com/3GdAdOOsV9</a></p>&mdash; あお (@ao_love_yuimop) <a href="https://twitter.com/ao_love_yuimop/status/1645026318726344704?ref_src=twsrc%5Etfw">April 9, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

- 2023.2.10 作品 (『バスケットゆいもっぷ』の9fps化。後半が好きです)

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">NovelAIアニメ『バスケットゆいもっぷ』を9fpsにしました(前回は実質2.9fps)　SGをi2i入力画像生成に活用しました<br>動画（高画質）: <a href="https://t.co/jcm91PGGM6">https://t.co/jcm91PGGM6</a><br>今回の制作記: <a href="https://t.co/wgV4438TGL">https://t.co/wgV4438TGL</a><a href="https://twitter.com/hashtag/%E3%82%86%E3%81%84%E3%82%82%E3%81%A3%E3%81%B7?src=hash&amp;ref_src=twsrc%5Etfw">#ゆいもっぷ</a> <a href="https://twitter.com/hashtag/NovelAI?src=hash&amp;ref_src=twsrc%5Etfw">#NovelAI</a> <a href="https://twitter.com/hashtag/AI%E3%82%A4%E3%83%A9%E3%82%B9%E3%83%88?src=hash&amp;ref_src=twsrc%5Etfw">#AIイラスト</a><br>SmoothGraphic Copyright(C)2013-2017　チラ裏エリア <a href="https://t.co/dBZUssS7zW">pic.twitter.com/dBZUssS7zW</a></p>&mdash; あお (@ao_love_yuimop) <a href="https://twitter.com/ao_love_yuimop/status/1623849114634158080?ref_src=twsrc%5Etfw">February 10, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

- どの数字が青っぽいか。 (ゆいもっぷさん的には「3」) (Bing Chat的には「5」) (なおゆいもっぷさんの誕生日(20歳)は「3月5日」)

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">めちゃ論理的（青っぽい数字は5、6、5） <a href="https://twitter.com/hashtag/Bing?src=hash&amp;ref_src=twsrc%5Etfw">#Bing</a> <a href="https://twitter.com/hashtag/%E6%96%B0%E3%81%97%E3%81%84bing?src=hash&amp;ref_src=twsrc%5Etfw">#新しいbing</a> <a href="https://t.co/CE1xKNznAA">pic.twitter.com/CE1xKNznAA</a></p>&mdash; あお (@ao_love_yuimop) <a href="https://twitter.com/ao_love_yuimop/status/1638276599161458688?ref_src=twsrc%5Etfw">March 21, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

- 初作(前回のアドカレ)の「バスケットゆいもっぷ」(2022.12.25 🎄🏀)
    - 前回のアドカレ
        - https://qiita.com/ystmg/items/c78c2b3e0bcdf0f77125

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">画像生成AIのNovelAIで中割り画像を作る方法を発見したので、「ゆいもっぷ」さんの2Dアニメ動画を作りました(原画8枚、動画78枚)(中割画像生成用画像生成サイトつき)<br>方法: <a href="https://t.co/1QrbKjpJcP">https://t.co/1QrbKjpJcP</a><br>動画(高画質): <a href="https://t.co/jsF8WAMY7Q">https://t.co/jsF8WAMY7Q</a><a href="https://twitter.com/hashtag/NovelAI?src=hash&amp;ref_src=twsrc%5Etfw">#NovelAI</a> <a href="https://twitter.com/hashtag/AI%E3%82%A4%E3%83%A9%E3%82%B9%E3%83%88?src=hash&amp;ref_src=twsrc%5Etfw">#AIイラスト</a> <a href="https://t.co/8giFiZ8Uym">pic.twitter.com/8giFiZ8Uym</a></p>&mdash; あお (@ao_love_yuimop) <a href="https://twitter.com/ao_love_yuimop/status/1606670363651633152?ref_src=twsrc%5Etfw">December 24, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

- かわいいキーホルダー (ゆいもっぷさん作)
    - ゆいしょっぷ: https://yuimop.booth.pm/

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">ゆいしょっぷから届きました <a href="https://twitter.com/hashtag/%E3%82%86%E3%81%84%E3%82%82%E3%81%A3%E3%81%B7?src=hash&amp;ref_src=twsrc%5Etfw">#ゆいもっぷ</a><br>ゆいもっぷのアクリルキーホルダー | ゆいしょっぷ <a href="https://t.co/FjrTevryxn">https://t.co/FjrTevryxn</a> <a href="https://twitter.com/hashtag/booth_pm?src=hash&amp;ref_src=twsrc%5Etfw">#booth_pm</a> <a href="https://t.co/D8qltQ7nVX">pic.twitter.com/D8qltQ7nVX</a></p>&mdash; あお (@ao_love_yuimop) <a href="https://twitter.com/ao_love_yuimop/status/1619526910853414914?ref_src=twsrc%5Etfw">January 29, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 


# 背景・目的

- 省力化を進めて、<ins>週刊ゆいもっぷアニメMV</ins> 状態になるのが最終目的

# 開発手法・開発の流れ

- <ins>ChatGPT 4</ins> では、画像を入力して、その画像に対して<ins>プログラミングさせることができます</ins>
- <ins>大雑把</ins>に「この画像の、左目の左端の点の座標を算出するプログラムを書いてください」と指定しても<ins>全然ダメ</ins>
- しかし、「こういう手法(やり方・手順)でなら行けそうだな」と人間である私 (<ins>ゆいもっぷファン</ins>) が考え、<ins>「どういう手法・手順で算出しなさい」と指示に含めたら</ins>、ChatGPT 4 がその手順でプログラムを書き、ChatGPT上でのプログラムの実行で画像の欲しい領域を取得できた (←<ins>ゆいもっぷの勝利</ins>)
    - ChatGPTに色んな手法・手順をやってみさせて、良い手順を探り出した
    - 70～80分くらい(8～9回試行) 取り組んだら正解 (ChatGPT 4が1つの入力画像に対して正答を出せるプログラムを書ける手順)に到達した 
    - この<ins>勝利をゆいもっぷに</ins>捧げましょう(崇拝)
- とはいえ、プログラム(というより処理)が長くなってくると、ChatGPTは 処理すべてを完遂しなくなったり、最後の方の処理でプログラミングを誤るようになるので、「よい手順」を見つけたら、<ins>手元のプログラミング環境にコードをコピペ</ins>していった
    - 実は、手順をそれなりに分割して指示していけば(複数回答に分ければ) かなり長くいけそう(？) (面倒)

## 環境構築

### ChatGPT

- https://chat.openai.com/
- sign in / log inします
- 左下のユーザ名の「<ins>マイプラン</ins>」からChatGPT Plus を契約しました (月額3000円程度)
- チャット画面の最上部で「GPT-4」を選びます
    - (少し前までは、ここで「Advanced Data Analysis」を選ぶ必要がありました)

#### (Advanced Data Analysis を有効にする手順) (legacy)

- https://www.marke-media.net/whitepaper/code-interpreter-chatgpt/
- Settings で　ADA を有効にしたあとに、上の「GPT-4」のところをクリックして ADAを選んでいた (ChatGPT4への統合の前の話)

### 手元のプログラム実行環境の整備

(ChatGPT上でプログラムを実行させるだけだったら要らないのですが、結局は実用段階では手元で実行させるのが実行時間が短かったり、実行自動化できていいと思います)

- <ins>VSCode</ins>を入れました (Windowsユーザー)
    - アドカレ 1日めの記事(↓) もよろしくね (Windowsつながり)
        - [「ゆるくやるRAMMapなどを使ってプロセスの使用量に表れないメモリ使用量を特定 (Windows、DriverLocked) (Bing Chat頼り)」](https://t-hazawa.hatenablog.com/entry/2023/12/01/%E3%82%86%E3%82%8B%E3%81%8F%E3%82%84%E3%82%8BRAMMap%E3%81%AA%E3%81%A9%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E3%81%AE%E4%BD%BF%E7%94%A8%E9%87%8F%E3%81%AB%E8%A1%A8)
- https://qiita.com/84zume/items/27d143f529396c9fa1cc などを参考に <ins>Python実行環境</ins> を整えたのだと思います 
    - (もしかしたら VSCodeをショートカットを右クリックすることで管理者権限で起動しないといけないのかも？)
- 実行しようとして入ってない <ins>Python のライブラリ</ins>があった場合は、VSCodeのコンソールの中で pip install ライブラリ名 や <ins>pip install ライブラリ名 --user</ins> をしたのだと思います
    - --user を付けないと入れられないこともあった
- <ins>コードの実行</ins>自体は、Python のプログラムを VSCode のエディタに貼り付けると、そのファイルのタブの右にPlayボタンみたいな<ins>三角マーク</ins>が出るので、そこをクリックすると実行されます。
    - 一度実行されたら、コンソールで↑キーを押せば、直前に実行したコマンドが出るので、そこでエンターを押しても再実行できます


# いざ！実際のChatGPTに開発させる例へ！🥅

## (1st) 右も左も分からないので、とにかくやってほしいことをお願いする編

- ☆超POINT: プログラム書かせる時も、指示(プロンプト)を<ins>英語</ins>にした方が…というか、ギリギリできるかできないかくらいの難しめのこと(今回やってるくらいのこと) は<ins>英語にしないと</ins>ちゃんと解けるプログラムを書いてもらえませんでした (2023/10/20前後時点)
    - <ins>DeepL</ins>で英語にしたりしました
    - 英語にしていたのは、最初の「全部をChatGPT上で実行させて方向性(手法)として有望か」確かめる段階で、その先の「手元にプログラムをコピペして継ぎ接ぎして育てていく」<ins>段階では大体日本語のまま</ins>指示していました。(そこまで行くと、やることが単純になるので)

### ADAを有効にしてなかった頃のトライ

(※ ADA: Advanced Data Analysis)
(※ 現在では ChatGPT 4 に統合されたので、そのままでプログラムも実行してくれます) (←実行エラーの文章が見れなくなったみたいだけど。。)


#### [最初にした質問 (1st) (超ざっくり質問)] 
- (以下、明示しない場合は全て ChatGPT 4 です)

```
質問:
(ゆいもっぷ画像を与えつつ)
この画像のキャラの左目のまつげの左端の座標は？
```

(入力画像)
<img width="270" alt="first_extracted_eye.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/eeb1796c-b5b3-fdfe-cebd-32c111d64f98.png">
(本当はストレートだけどwavy hair の方がアニメ映えする) (解釈自由度高)

```
回答: 
(要約: Photoshopでポインタを合わせれば座標が表示される)

I'm unable to provide pixel-perfect coordinates from an image, 
but I can guide you on how you might determine this yourself.

To find the exact coordinates of the leftmost eyelash of the character's 
left eye, you'd typically use an image editing software like Adobe Photoshop, 
GIMP, or even simpler online image editors. Most of these programs will 
show the exact X and Y coordinates of the cursor when you hover over or 
click on a specific point in the image. 

By moving your cursor to the desired point, you should be able to see and 
note down the coordinates.
```
- ADAを有効にしてなかったので、プログラミングでやるという発想がChatGPTになかった

#### [2回めにした質問] (ADA無効)
(質問を英語にした。<ins>プログラムを書いて</ins>実行してと明示した)

<pre>質問:
(要約: 左目のまつ毛の一番左の座標を出すプログラムを
書いて)
Write and run a program to find the coordinates of the leftmost 
eyelash of the left eye of the character in this image.
</pre>

- プログラムは書いてくれたが、 ADA を有効にしてなかったので、実行までしてくれなかった。

<pre>回答:
(要約: OpenCVを入れて、目全体の領域を入力すれば 
edgeをとって左端を取ればそこがまつ毛の左端。
ChatGPT上では実行できないよ 
↑ しかし、後の
トライ(Advanced Data Analysisを有効にした
トライ)　ではOpenCVをChatGPT上で動かしてくれた。)

To find the coordinates of the leftmost eyelash of the character's left eye,
I can guide you through an approach using Python and the OpenCV library.
Unfortunately, I can't run the code directly here, but you can follow
the steps below to do it on your local machine.

1. Install the required libraries:
```bash
pip install opencv-python
```

2. Here's a step-by-step code guide:

</pre>

```python
import cv2
import numpy as np

# Read the image
img = cv2.imread('path_to_your_image.jpg')

# Convert the image to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Detect the edges in the image using Canny detector
edges = cv2.Canny(gray, 50, 150)

# Crop a region of interest around the character's left eye.
# (You might need to adjust these values based on your image's resolution)
roi = edges[100:250, 100:250]  # Adjust this as per your image

# Find the x-coordinate of the leftmost non-zero pixel in the ROI
leftmost = np.where(roi != 0)[1].min()

# Find the y-coordinate corresponding to the leftmost x-coordinate
y_coord = np.where(roi[:, leftmost] != 0)[0][0]

# Adjusting coordinates relative to original image
final_x = 100 + leftmost  # The x starting point of ROI
final_y = 100 + y_coord   # The y starting point of ROI

print(f"The coordinates of the leftmost eyelash of the character's left eye are: ({final_x}, {final_y})")

# Optional: Show the image with a circle on the detected point
cv2.circle(img, (final_x, final_y), 5, (0, 0, 255), -1)
cv2.imshow('Detected Point', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
```
<pre>
(要約: 画像パスを自分で変えてね)
Replace `'path_to_your_image.jpg'` with the path to your image.
Run the program, and it should print the coordinates of the leftmost
eyelash of the character's left eye.

(要約: 目の領域は自分で指定してね 
↑もっと上手くChatGPTを使いこなす (=指示を
詳しくする)ことで自分で指定しなくてよい手法を
後のトライで確立できた)

Remember that the cropping values (the region of interest) are just placeholders.
Adjust the values `[100:250, 100:250]` to closely fit around the character's 
left eye for better accuracy. The approach may need some tweaking based on the 
image's resolution and the specific location of the eye.
</pre>

(↑ところで、上のようにChatGPTが「ChatGPT上では実行できないよ」というのは、ChatGPTにChatGPTの<ins>開発者が与えているプロンプト</ins>にそういう内容があるのでしょうね。 
Bing Chat だと、難しいことをさせてると、たまにBing Chatが「私に与えられたプロンプトはユーザーに表示できます」と突然言い出して、それならと聞いてみると、ユーザにどう回答するかが記されているので)

#### [2.1回めの質問] (ADA無効)

- ほぼ同じ質問を再度したら(←？)、今度は他の方法(の名前) を教えてくれた。
    - (数回に1度、確率的に良い回答が帰ってくるのも <ins>LLMあるある</ins>)

<pre>注意：このプログラムは基本的な手法であり，すべての画像に対して完璧に
動作するとは限りません．正確な結果を得るためには，パラメータ，
特に閾値を調整する必要があります．また、`'image_path.jpg'` は必ず
画像の実際のパスに置き換えてください。

よりロバストな視線検出や特徴検出を行うには、事前に学習させたモデルや
DlibやMTCNNのようなフレームワークを使うことを検討してください。
</pre>

----
### [<ins>ADAを有効</ins>にして1回めの質問] (☆ここから本番☆🔥) (雑な指示でも ChatGPTが手順考えて挑戦してくれる ←結果は失敗する)

- POINT: 
    - ChatGPTの応答全体の<ins>コピーボタン</ins>をクリックしても、 <ins>その中にあるプログラムはコピーされない</ins> ことが多いので気をつけよう
    - (なので、↓のプログラム部分は 2023/11/24 に改めてChatGPT 4 に書かせたプログラム)

<pre>質問: 
(要約: 画像のキャラの左目のまつ毛の左端の座標を
求めるプログラムを書いて実行してください。)
Write and run a program to find the coordinates of the 
leftmost eyelash of the left eye of the character in this image.
</pre>

<img width="270" alt="first_extracted_eye.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/eeb1796c-b5b3-fdfe-cebd-32c111d64f98.png">

- 以下のように、ChatGPT4 の Code Interpreter (Adv Dat Ana) は、途中の様子をしっかりわかりやすく<ins>図示</ins>してくれるのが<ins>魅力的</ins>です。
- <ins>どういう手法で座標を算出せよ、と指定してないので、方法をChatGPTが考えてくれた</ins>
    - (しかしその手法では正しく算出できない)
- ADAが有効なら、↑のような簡単なChatGPTへの指示でも、しっかり意図したことに ChatGPT4は挑戦してくれますね。
    - (ただし、ちゃんとうまく行く手法を指示で指定しないと正解には至らない)
<pre>
回答:
(1.～5.の手順を考えました。
グレスケ → edge → 'haarcascade_eye'
目検出 → どうにかしてまつげ検出 
→ 左端の点を取る)

Detecting the leftmost eyelash of the left eye in an image 
requires a combination of image processing techniques. 
Here's a general outline of the approach I'll take:

1. Load the image and convert it to grayscale.
2. Apply edge detection to highlight prominent features.
3. Detect the eyes in the image.
4. Once the left eye is located, further process the region 
 to detect the eyelashes.
5. Find the leftmost point of the detected eyelashes.

Let's start by loading the image and visualizing it.
</pre>

```python
(画像表示)
from PIL import Image
import numpy as np
import cv2

# Load the image
img_path = '/mnt/data/first_extracted_eye.png'
img = cv2.imread(img_path)

# Convert the original BGR image to RGB for proper display in Jupyter
original_img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# Display the original image
Image.fromarray(original_img_rgb)
```

<img width="170" alt="first_extracted_eye.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/eeb1796c-b5b3-fdfe-cebd-32c111d64f98.png">

<pre>The image has been successfully loaded. 

Next, I'll convert the image to grayscale and apply edge 
detection to highlight the prominent features, which will 
make it easier to detect the eyes and eyelashes.
</pre> 

```python
(グレスケ化)
# Convert the image to grayscale
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Convert BGR to RGB for display in Jupyter
gray_img_rgb = cv2.cvtColor(gray_img, cv2.COLOR_GRAY2RGB)

# Display the grayscale image
Image.fromarray(gray_img_rgb)
```

<img width="270" alt="gray7eb1ede9-1a6a-492c-807b-f2e5bd1296d7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/b906bb92-dcb8-472b-c0b6-efe427749b11.png">

```Python
(edge化)
# Use Canny edge detector to find edges in the image
edges = cv2.Canny(gray_img, threshold1=100, threshold2=200)

# Convert edges to RGB for display
edges_rgb = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB)

# Display the edges image
Image.fromarray(edges_rgb)
```

<img width="370" alt="edge37db34a2-c579-41ef-8c79-c08a4ca6698e.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/355bcf49-73f7-8643-b527-95334949000f.png">

<pre>The edge detection has successfully highlighted the prominent 
features in the image.

(要約: OpenCVの haarcascade_eye 
(←人間用目検出器)を使う)
Next, I'll proceed to detect the eyes. For this task, I'll 
use a pre-trained Haar cascade classifier provided by OpenCV. 
Once the eyes are detected, we'll focus on the left eye 
(from our viewpoint) to further process and detect the eyelashes.
</pre>

```Python
# Load the pre-trained Haar cascade for eye detection from OpenCV
# Note: The cascade file should be present in the cv2 data directory 
#       or the path needs to be adjusted accordingly
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades \
+ 'haarcascade_eye.xml')

# Detect eyes in the image
eyes = eye_cascade.detectMultiScale(gray_img, scaleFactor=1.1, minNeighbors=5,\
minSize=(30, 30))

# If eyes are detected, proceed to focus on the left eye (from our viewpoint)
if len(eyes) > 0:
    # Sort the eye detections by their x-coordinate so that
    # the leftmost eye is first
    sorted_eyes = sorted(eyes, key=lambda x: x[0])
    left_eye = sorted_eyes[0]
    x, y, w, h = left_eye

    # Draw a rectangle around the left eye for visual verification
    eye_img = cv2.rectangle(gray_img.copy(), (x, y), (x+w, y+h),\
        (255, 0, 0), 2)

# Convert the image with the detected eye to RGB for display
eye_img_rgb = cv2.cvtColor(eye_img, cv2.COLOR_GRAY2RGB)

# Display the image with the detected eye
Image.fromarray(eye_img_rgb)

```

(目が一つ取れた)
<img width="370" alt="eye_dd5eb14c-49ca-4efd-a61c-8374defe9348.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/3a1f386b-97a4-6e34-ac31-7175f8b3a2a4.png">


<pre>(要約: 目の領域が1つだけ取れたよ)
It appears that only one eye has been detected in the image. 
This could either be the left eye or the right eye from our viewpoint. 
Since we're interested in the left eye, we'll proceed with 
this detected region.
</pre>

- ここから↓はプログラムを現在のChatGPT4で再現できなかった (<ins>結果的には脚の付け根</ins>)
    - (Advanced Data Analysis の時は、ChatGPT上で実行したプログラムのエラーが表示されて、それをChatGPTが見て、勝手に修正をしていっていたと思うけど、ChatGPT4に統合された現在では表示されない？)
    
<pre>The next step is to isolate the region around the detected eye 
and apply further image processing to detect the eyelashes. 
Let's focus on that region.
</pre>


<pre>(要約: どうにかしてまつ毛を特定
できたので、左端の座標をとって完成)
The eyelashes are now more visible in the thresholded eye region.

Next, I'll detect the eyelashes' contours and find the leftmost 
point among them. This point should correspond to the leftmost 
eyelash of the left eye.

The leftmost eyelash of the left eye (from our viewpoint) in 
the image is located at the coordinates \((294, 668)\). 

You can see the point highlighted with a green circle in the 
displayed image.
</pre>
(↓最終的に出てきた座標 =左側の脚の付け根)
- <img width="370" alt="![zahyoeye_dd5eb14c-49ca-4efd-a61c-8374defe9348.png]" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/c02f2015-786d-7ed9-b230-d3099ab5c5b0.png">


- ↑ (ADA有効にして) <ins>第一回のトライでは、この見当違いなところ</ins> (294,668 = <ins>左側の脚のつけね</ins>)が、左目のまつげの左端と出てきたのだった
- とはいえ、なんか(ChatGPTへの指示を)頑張ればうまくいきそうな感じもあるので、8~9試行色々指示を変えてみたのだった↓

### ☆ 8~9試行(70~80分)後に到達した初の【大成功】な指示(プロンプト) ☆

- <ins>どういう手順で出せばいいかを指定したら大成功した</ins>
- (概要: ゆいもっぷちゃんの中心のX座標において、Y軸方向に上から下まで色相をとり、色相が変わるところで領域分け・分類する ←上手く行く) (最終形では使わなかったけど)
    - <ins>イラストのゆいもっぷちゃんは領域ごとに色がまとまってるので、こういう手法が有効</ins>なのですね。
- Haar cascade は ChatGPT から教えてもらった
- 指示(プロンプト)の書き方も、8～9試行する間に洗練されていった

<pre>指示:
この画像にはイラストの二次元美少女キャラクターが描かれています。
この美少女キャラクターの前髪の領域、顔の領域、上半身の服の領域、
下半身の服の領域を取得したいです。
そのために、以下の手順を含むプログラムを書いて実行してください。

1. キャラクターがX軸のどこにいるかを知るために、Haar cascadeで顔を検出して
X軸の座標を求める。検出された領域のX軸の値の真ん中の値
(領域の左端のX座標と右端のX座標の平均)を採用する。
ただし、Haar cascadeはイラストでは精度が劣るので、X軸の座標取得だけに使う。
 
2.  1.で取得したX軸の座標について、上から色を取得していき、
黒色の線部分の影響をなくすため、Y軸座標について、前後10ピクセルくらいで
移動平均化を行い、色相が大きく変わるところで区切って領域を分割してください。
6個前後の領域になるように区切ってください。どのように区切ったか表示してください。
 
3. 2.で区切った領域のうち、以下のエリア群のそれぞれと色が近く、
画像における位置も合っている領域をそのエリアと考えてください。
その各領域の中心点の座標と領域とを、画像上に表示する。
 
[エリア群]:
前髪: 色は RGB(165,232,239) に近い色。画像の上から1/4までの位置にある。
顔: 色は RGB(251,240,231) に近い色。画像の上から1/4あたりの位置にある。
上半身の服: 色は RGB(255,254,253) に近い色。画像の上から1/4付近から始まり、Y軸方向で中央前後まで続く。
下半身の服: 色は RGB(75,120,210) に近い色。画像の上から1/2～3/4あたりにある。
 
(指示はDeepLで英訳する)
</pre>

<ins>[指示の概要]</ins>
1. <ins>Haar cascade</ins> は、実写人間用の顔検出手法だが、ゆいもっぷちゃんイラストでも「<ins>X軸座標のどこにゆいもっぷちゃんがいるか</ins>」は出せた (胸あたりが検出されるけどX軸座標は正しい感じ)
    - (とはいえ、<ins>最終的には</ins>、(ゆいもっぷちゃんの<ins>目が紫</ins> ←[他のところに現れにくいので、とても検出に使いやすい色] であることを利用した) 両目検出、両目のX軸座標の平均…を使うことになった) 
1. <ins>ゆいもっぷちゃんは縦長</ins>なので、ゆいもっぷちゃんの中心X軸座標について <ins>Y軸を上から下まで走査</ins>して、どのY軸座標範囲が ゆいもっぷちゃんのどの領域 (顔など) か判定させることにした
    - 領域の区切りは、<ins>色相</ins>の移動平均が大きく<ins>変化したところ</ins>で区切った 
        - (このあとにさらに進化した手法では、事前に指定した 7～9種類の色のいずれかに分類して、色が変わるところ、みたいにした)
1. <ins>ゆいもっぷちゃん</ins>の各領域(顔など)が <ins>大体 どこ</ins> (上から15%～50%のどこか) あたりにあって、どれくらいの色かは分かってる ので<ins>与えた</ins>
    - (「大体このあたりにある」は分かってるが、ゆいもっぷちゃんアニメ作りのためには、各領域の「概ね合ってる」くらいの座標が必要)

―
- CAUTION: (2023/11/16 の) ChatGPT-4 の Advanced Data Analysis だと、↓くらいの長さの回答を一気に書いて実行してくれたが、<ins>↓くらいの長さが正常なプログラムを書いて返せる回答の長さの限界</ins>。
    - これ以上 (1つの回答が) 長くなると、プログラムミスが多発した。
    - そのため、<ins>詳細に書きすぎることはできない</ins>。(1回の指示だけだと)

(入力ゆいもっぷちゃん画像)
<img width="270" alt="first_extracted_eye.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/eeb1796c-b5b3-fdfe-cebd-32c111d64f98.png"> 

<pre>回答(かなり長い):

(要約: ゆいもっぷアニメクリエイター(YAC)からの
指示を解釈しアウトラインを提示)
Alright, let's break down our approach step-by-step:

1. **Face Detection with Haar Cascade**: We'll use the Haar cascade to detect the face
 and obtain the X-axis coordinates of the face. We will take the average of the
 X-coordinates of the left end and the right end of the detected area to get the middle value.

2. **Acquiring Colors and Dividing Areas**: For the X-axis coordinates acquired in step 1, 
 we'll start from the top of the image and move downwards. We'll obtain colors, and in
 order to reduce the influence of black lines, we'll perform a moving average of 
 the Y-axis coordinates, considering about 10 pixels before and after the current 
 position. We'll track changes in hue to divide the image into about 6 areas and note 
 how we've divided them.

3. **Area Matching**: Based on the specified RGB values and positions in the image, 
  we'll identify the forelock, face, upper body clothing, and lower body clothing. 
  We'll display the coordinates of the center point of each of these areas and the area 
  on the image.

―
(要約: Step1 として、人間用の顔検出手法で
X軸座標を得る)
Let's start with step 1: Detect the face using Haar cascade and find the X-axis coordinate.
</pre>

```python
import cv2
import numpy as np
from matplotlib import pyplot as plt

# Load the image
image_path = "/mnt/data/{{{{{{{{{{{{{{{{{{{ The most powerful school uniform}}}}}}}}}}}}}}}}}}} ,\
s-2402576765.png"
image = cv2.imread(image_path)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Load Haar Cascade for face detection
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

# Detect face
faces = face_cascade.detectMultiScale(cv2.cvtColor(image, cv2.COLOR_RGB2GRAY), scaleFactor=1.1,\
minNeighbors=5, minSize=(30, 30))

# If no face is detected, return an error message
if len(faces) == 0:
    result = "No face detected!"
else:
    # Draw a rectangle around the detected face and compute the center X-coordinate
    for (x, y, w, h) in faces:
        cv2.rectangle(image_rgb, (x, y), (x+w, y+h), (255, 0, 0), 2)
        face_center_x = (x + x + w) // 2

    # Display the image with the detected face
    plt.imshow(image_rgb)
    plt.title("Detected Face")
    plt.axis('off')
    plt.show()

    result = face_center_x

result
# 345
```

(↓人間用顔検出手法を適用した結果 = X軸座標だけなら使える)
<img width="370" alt="![ym_df.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/e64617ec-3cf2-1537-fb83-462945749703.png">
(ChatGPTは、こういう風に中間状態を図示して示してくれるのが、とてもいいですね。)

<pre>The face has been detected, and the center X-coordinate of the face is 
approximately \( x = 345 \).

(要約: 人間用の顔検出手法でX軸座標は取れたので、
Y軸方向に走査して色相の変化を見る)
Now, let's move to step 2. For the X-axis coordinate acquired, we'll start from 
the top of the image and move downwards. We'll obtain colors and perform 
a moving average of the Y-axis coordinates.
We aim to divide the image into approximately 6 areas based on significant hue changes.

Let's begin with this process.
</pre>

```python
def moving_average_color(image, x, window_size=10):
    """Compute the moving average color for a given X-coordinate in the image."""
    height, _, _ = image.shape
    avg_colors = []
    
    for y in range(height):
        # Determine the window range
        start_y = max(0, y - window_size)
        end_y = min(height, y + window_size)
        
        # Calculate the average color in the window range for the given X-coordinate
        avg_color = np.mean(image[start_y:end_y, x, :], axis=0)
        avg_colors.append(avg_color)
    
    return avg_colors

# Get moving average colors for the face's center X-coordinate
avg_colors = moving_average_color(image_rgb, face_center_x)

# Convert RGB to HSV for hue detection
hsv_colors = [cv2.cvtColor(np.uint8([[color]]), cv2.COLOR_RGB2HSV)[0][0] for color in avg_colors]

# Extract hue values
hues = [color[0] for color in hsv_colors]

# Plot the hue values along the Y-axis
plt.figure(figsize=(10, 6))
plt.plot(hues)
plt.title("Hue values along the Y-axis")
plt.xlabel("Y-coordinate")
plt.ylabel("Hue value")
plt.grid(True)
plt.show()
```

(↓<ins>ゆいもっぷちゃんの中心X座標</ins>(←尊い) における上から下までの色相 (の10px移動平均))
<img width="590" alt="![hva.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/de163633-ea8d-e4b6-f764-3ede780aa199.png">

<pre>The plot showcases the hue values along the Y-axis for the X-coordinate corresponding to
the center of the face. As we can see, there are several peaks and valleys indicating
changes in hue, which will help us segment the image into different areas.

(要約: 色相で6つの領域に分ける)
For the next step, we will identify significant hue changes to divide the image into
about 6 areas. Let's proceed with this segmentation.
</pre>

```Python
def find_divisions(hues, num_divisions=6):
    """Find Y-coordinates for divisions based on hue changes."""
    # Calculate the differences between consecutive hue values
    hue_diffs = np.diff(hues)
    
    # Find the indices of the largest differences
    division_indices = np.argsort(np.abs(hue_diffs))[-num_divisions+1:]
    division_indices = np.sort(division_indices)
    
    return division_indices

# Find the Y-coordinates for divisions
divisions = find_divisions(hues)

# Display the image with division lines
plt.figure(figsize=(10, 15))
plt.imshow(image_rgb)
plt.title("Divisions based on Hue Changes")

for division in divisions:
    plt.axhline(y=division, color='red', linestyle='--')

plt.axis('off')
plt.show()

divisions # array([200, 239, 305, 496, 659])
```

(↓ ChatGPTが色相が変わるところで <ins>ゆいもっぷちゃんを6つに分けた</ins> 結果をChatGPTが示してくれた画像)
<img width="470" alt="[dbhc.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/c8e70fa0-072f-a9c2-2ec8-4491628de36e.png">

<pre>The image has been divided based on significant hue changes. The approximate Y-coordinates of
the divisions are at 200, 239, 305, 496, and 659. This gives us six regions as intended.

(要約: 分けた6つの領域を、前髪などの指示で
定義された領域に割り当てていく)
Now, for step 3, we will identify the areas corresponding to the forelock, face, upper body
clothing, and lower body clothing based on the provided RGB values and their expected positions
in the image. We will then display the coordinates of the center point of each of these areas
on the image.

Let's proceed with this identification.
</pre>

```Python
def find_matching_area(image, x, target_rgb, y_start, y_end, tolerance=30):
    """Find the matching area based on the target RGB value and Y-coordinate range."""
    # Extract the RGB values for the specified X-coordinate and Y-coordinate range
    region_colors = image[y_start:y_end, x, :]
    
    # Calculate the distance from the target RGB value for each pixel
    distances = np.linalg.norm(region_colors - target_rgb, axis=1)
    
    # Identify the pixels that are close to the target RGB value within the tolerance
    matching_indices = np.where(distances < tolerance)[0]
    
    # If no matching area is found, return None
    if len(matching_indices) == 0:
        return None
    
    # Find the Y-coordinate of the center of the matching area
    center_y = (matching_indices[0] + matching_indices[-1]) // 2 + y_start
    return center_y

# Define the target RGB values and Y-coordinate ranges for each area
areas = {
    "Forelock": {"RGB": [165, 232, 239], "Y_range": (0, divisions[1])},
    "Face": {"RGB": [251, 240, 231], "Y_range": (0, divisions[1])},
    "Upper body clothing": {"RGB": [255, 254, 253], "Y_range": (divisions[1], divisions[3])},
    "Lower body clothing": {"RGB": [75, 120, 210], "Y_range": (divisions[3], divisions[4])}
}

# Find the center Y-coordinates for each area
centers = {}
for area, properties in areas.items():
    center_y = find_matching_area(image_rgb, face_center_x, properties["RGB"], \
    *properties["Y_range"])
    if center_y is not None:
        centers[area] = center_y

# Display the image with center points for each area
plt.figure(figsize=(10, 15))
plt.imshow(image_rgb)
plt.title("Center Points of Areas")

for area, center_y in centers.items():
    plt.scatter(face_center_x, center_y, color='red', marker='x')
    plt.text(face_center_x+10, center_y, area, color='white', backgroundcolor='black',\
    fontsize=12)

plt.axis('off')
plt.show()

centers
```

```python
結果

{'Forelock': 80,
 'Face': 192,
 'Upper body clothing': 359,
 'Lower body clothing': 558}
```

(↓☆<ins>最終結果</ins>: 領域を、指示で与えた4領域に割り当てた結果)
<img width="500" alt="![resl3.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/1a545793-bce4-220e-a8c4-578a273f6214.png"> (←大成功！)

<pre>The areas corresponding to the forelock, face, upper body clothing, 
and lower body clothing have been identified based on the provided RGB values 
and their expected positions in the image.

Here are the approximate center points of each area:
- Forelock: \( Y = 80 \)
- Face: \( Y = 192 \)
- Upper body clothing: \( Y = 359 \)
- Lower body clothing: \( Y = 558 \)

These points are also marked on the displayed image for clarity. 

Would you like to proceed with anything else?
(回答終わり)
</pre>

- ここまでを1回答でするのだから GPT-4 はすごいですよね
- とはいえ、この「色相が変わる場所」手法では、<ins>(中心点だけでなく) 領域</ins>(顔の領域 = 上端&下端)を取ろうとしても、(ChatGPT上では) (1つの指示でやらせるには長くなりすぎたからか？) <ins>上手くいかなかった</ins>
    - そのため、この後、「色を指示で指定した7～9色に分類する」方向性に私 (ゆいもっぷアニメクリエイター) は進んだ

#### 8～9試行するまでの途中の指示(プロンプト) (大成功に至るまでの進化の軌跡)

- (↓これくらいの手法についての指示のアバウトさだと上手く行かなかった) (試行錯誤)
    - とはいえ、1試行に3～4分ほどしかかからないので、かなり低労力で色んな手法を<ins>たくさん試行できる</ins>
- 基本的に指示はどんどん詳細に(長く)なっていく
- (待ち時間なので別のこともできる)

(↓試行途中のプロンプト)
<pre>-(指示の成長の道程)-
指示:
この画像にはイラストの二次元美少女キャラクターが描かれています。
この美少女キャラクターの顔の領域、上半身の服の領域、両目の領域、前髪の領域を取得したいです。
そのために、以下の手順を含むプログラムを書いて実行してください。
 
1. 画像のうち背景部分を除去する
2. キャラクターがX軸のどこにいるかを知るために、Haar cascadeで顔を検出してX軸の座標を求める。
ただし、Haar cascadeはイラストでは精度が劣るので、X軸の座標取得だけに使う。
3.  2.で取得したX軸の座標について、背景部分でない部分について、同じ色で連続して
塗られているエリアについて、
上から代表色と、そのエリアの中心点の座標と、そのエリアの領域を取得する。
4. 3. の各エリアについて、上から「前髪」、「顔」、「上半身の服」、「下半身の服」と名付ける。
</pre>

(↓大成功した「8～9試行した後のプロンプト」(再掲)(比較用))
<pre>指示:
この画像にはイラストの二次元美少女キャラクターが描かれています。
この美少女キャラクターの前髪の領域、顔の領域、上半身の服の領域、
下半身の服の領域を取得したいです。
そのために、以下の手順を含むプログラムを書いて実行してください。

1. キャラクターがX軸のどこにいるかを知るために、Haar cascadeで顔を検出して
X軸の座標を求める。検出された領域のX軸の値の真ん中の値
(領域の左端のX座標と右端のX座標の平均)を採用する。
ただし、Haar cascadeはイラストでは精度が劣るので、X軸の座標取得だけに使う。
 
2.  1.で取得したX軸の座標について、上から色を取得していき、
黒色の線部分の影響をなくすため、Y軸座標について、前後10ピクセルくらいで
移動平均化を行い、色相が大きく変わるところで区切って領域を分割してください。
6個前後の領域になるように区切ってください。どのように区切ったか表示してください。
 
3. 2.で区切った領域のうち、以下のエリア群のそれぞれと色が近く、
画像における位置も合っている領域をそのエリアと考えてください。
その各領域の中心点の座標と領域とを、画像上に表示する。
 
[エリア群]:
前髪: 色は RGB(165,232,239) に近い色。画像の上から1/4までの位置にある。
顔: 色は RGB(251,240,231) に近い色。画像の上から1/4あたりの位置にある。
上半身の服: 色は RGB(255,254,253) に近い色。画像の上から1/4付近から始まり、Y軸方向で中央前後まで続く。
下半身の服: 色は RGB(75,120,210) に近い色。画像の上から1/2～3/4あたりにある。
 
(指示はDeepLで英訳する)
</pre>

- …試行途中のプロンプトだと…↓
- (↓ChatGPT的に<ins>背景を除去したつもり</ins>の画像) (できてない)
    - (もっと詳細に<背景を除去する手順を示せばできるのかもしれないけど。)
- <img width="370" alt="![bg.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/8d6419a9-17d6-7544-e556-5f7d4e8bb0a0.png">
- (↓ 上記の途中のプロンプトの最終結果)

<img width="470" alt="![akan.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/96f7c891-a30f-2189-8686-41b5c6d487b0.png">

#### さらにその前の途中の指示(プロンプト)

- ↓のようにChatGPTに言われたことがあって、<ins>大まかな位置</ins>(上から15%～50%の間にある、とか色とか)を指示で<ins>指定するように</ins>なった (このようにして指示を良くしていく)
    - (ChatGPT上ではアニメ的イラスト用の顔領域検出ライブラリは使えない) (← …というより、世の中にあるのかな？)

<pre>質問:
この画像にはイラストの二次元美少女キャラクターが描かれています。この美少女キャラクターの顔の領域、
上半身の服の領域、両目の領域、前髪の領域を取得するプログラムを書いて実行してください。

ただし、検出した領域に服が含まれないようにしてください。
アニメ的イラスト用の顔領域検出ライブラリを使用してください。

―
回答:
画像が表示されましたが、この先に進むには、私たちが興味を持っているさまざまな部分に対応する色や
パターンを識別するために、あなたの助けが必要です：

    顔
    上半身の服
    目
    前髪

各パーツの見分け方の説明やガイドをお願いします。
あるいは、各パーツを表すピクセルのおおよその座標を
指定していただければ、そのピクセルから色を抽出してセグメンテーションに使用できます。
</pre>


### ☆◎ 事前に指定した色に分類して領域分け (到達した手法) (10/19)◎☆ (最終形でも主力)

- (「色相の変化」では領域を取れなかったので)色々手法を ChatGPT上で試してたら、↓の<ins>「事前に指定した色に分類」がめちゃうまくいった</ins>

<pre>質問:
この画像にはイラストの二次元美少女キャラクターが描かれています。この美少女キャラクターの
前髪の領域、顔の領域、上半身の服の領域、下半身の服の領域を取得したいです。そのために、
以下の手順を含むプログラムを書いて実行してください。

1: キャラクターはX軸座標の 243 の位置にいます。

2:  1.で取得したX軸の座標について、Y軸に沿って上から2ピクセルごとに色を取得していき、
そのピクセルの色を、以下のいずれかに割り当ててください。「オレンジ,明るめの水色,
明るい肌色,暗い肌色,白の影色としての灰色,白,強いやや紫みの青」 割当結果を図示してください。

3: 2. で割り当てた色名を用いて、含まれる色名が同じものが多くなるようにY軸方向に領域を
分けてください。

4: 3.で区切った領域のうち、以下のエリア群のそれぞれと色が近く、画像における位置も
合っている領域をそのエリアと考えてください。1つの領域は1つのエリアにしか対応しません。
各エリアは連続していて、各1個ずつしか存在しません。その各領域の中心点の座標と領域とを、
画像上に表示する。

[エリア群]
上から 前髪 → 顔 → 上半身の服 → 下半身の服 の順に画像上に存在している。

前髪: 色は明るめの水色である。画像の上から0%～30%の範囲内にある。
顔: 色は明るい肌色である。画像の上から15%～50%の範囲内にある。
上半身の服: 色は白か白の影色としての灰色である。画像の上から35%～80%の範囲内にある。
下半身の服: 色は強いやや紫みの青。画像の上から70%～100%の範囲内にある。

(指示はDeepLで英訳して伝えた)
</pre>

- (人間用顔検出ライブラリを使っていると回答が<ins>長くなって</ins>ChatGPTの<ins>誤答</ins>が増えるので、X=243で<ins>与えた</ins>)

(入力画像)
<img width="270" alt="![second.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/67b20995-1cfe-a1a4-1918-d004ad2ca855.png">


<pre>回答: 
(要約: ゆいもっぷアニメクリエイターからの指示を
解釈して復唱)
Alright! Let's break down the task:

1. Identify the x-coordinate 243 on the image.
2. For the y-axis corresponding to this x-coordinate, obtain a color for every two pixels
   starting from the top. Map this color to one of the predefined colors (orange,
   light sky blue, light skin tone, dark skin tone, gray, white, or strong slightly 
   purplish blue).
3. Based on the sequence of colors obtained in step 2, segment the y-axis into regions
   where the majority color is the same.
4. For each of the predefined regions (bangs, face, upper body clothing, and lower
   body clothing), identify the area by their color and approximate position in the
   image. Compute the center and the area of each of these regions.

(要約:まず表示)
Let's start by visualizing the image and plotting the line at x=243 to get a sense of
what we're working with.
</pre>

```python
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import cv2

# Load the image
img = cv2.imread('/mnt/data/{{{{{{{{{{{{{{{{{{{{secondThe most powerful school uniform}}}}}}}}}}}}}}}}}}} , ma s-1507457775.png')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# Plot the image with a line at x=243
plt.figure(figsize=(10, 10))
plt.imshow(img_rgb)
plt.axvline(x=243, color='red', linestyle='--')
plt.title("Image with line at x=243")
plt.show()
```

<img width="370" alt="![dot9f216b59-2cb2-4d80-bb91-9e98f95336d1.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/e743459e-17e3-6f6f-264e-c3733ec2d78f.png">

<pre>Here's the image with a red dashed line at \( x = 243 \). 

(要約:X=243を上から指定された色に分類するぞ。
まず言葉で指示された色のRGB値を決めるぞ。
ゆいもっぷアニメクリエイターが指示してもいいし、
私が適当に仮定もできるぞ。)
Next, let's extract the colors along the line at \( x = 243 \) for every two pixels
starting from the top. We'll then map these colors to one of our predefined colors:
orange, light sky blue, light skin tone, dark skin tone, gray (as a shadow color of
white), white, or strong slightly purplish blue. 

Let's start by defining the RGB ranges for these predefined colors. Could you please 
provide the RGB values or ranges for each of these colors? If you don't have specific 
values, I can make reasonable assumptions based on typical color definitions.
</pre>

<pre>質問:
Proceed with your assumptions.
</pre>

<pre>回答:
(要約:いい感じに各色のRGB値を仮定したよ)
Alright, I'll make assumptions for the RGB values of the predefined colors
based on typical color definitions. Here are my assumptions:

1. **Orange**: Typically, an RGB value of (255, 165, 0) is associated with
   orange. We'll consider a range around this value to classify a pixel as orange.
2. **Light Sky Blue**: An RGB value of (135, 206, 235) is associated with
   light sky blue. Again, we'll consider a range around this value.
3. **Light Skin Tone**: There's a wide range of skin tones, but for the 
   sake of simplicity, I'll assume an RGB value of (255, 220, 178) and 
   consider a range around this value.
4. **Dark Skin Tone**: Assuming an RGB value of (205, 133, 63) and 
   considering a range.
5. **Gray (as a shadow color of white)**: A midpoint gray might be around
   (128, 128, 128). We'll take a range around this and also consider 
   lighter shades closer to white.
6. **White**: Pure white is (255, 255, 255). We'll consider values close
   to this as white.
7. **Strong Slightly Purplish Blue**: Assuming an RGB value of 
   (70, 60, 150) and considering a range around it.

(要約:この色設定を元にX=243の色を分類する)
With these assumptions, I'll extract the colors from the image at \( x = 243 \)
and classify each pixel color into one of the predefined categories.
</pre>

```python
def classify_color(rgb):
    """
    Classify the given RGB value into one of the predefined colors.
    """
    # Define the color ranges
    colors = {
        'orange': ([200, 100, 0], [255, 190, 50]),
        'light sky blue': ([100, 180, 220], [170, 230, 250]),
        'light skin tone': ([230, 190, 150], [255, 250, 210]),
        'dark skin tone': ([150, 100, 40], [260, 170, 90]),
        'gray': ([100, 100, 100], [190, 190, 190]),
        'white': ([230, 230, 230], [255, 255, 255]),
        'strong slightly purplish blue': ([40, 30, 130], [100, 90, 170])
    }

    for color, (lower, upper) in colors.items():
        if lower[0] <= rgb[0] <= upper[0] and lower[1] <= rgb[1] <= upper[1] \
          and lower[2] <= rgb[2] <= upper[2]:
            return color
    return "unknown"

# Extract the colors at x=243 for every two pixels
colors_at_x243 = [classify_color(img_rgb[y, 243]) for y in range(0, img_rgb.shape[0], 2)]

colors_at_x243[:10]  # Displaying the first 10 classified colors for inspection

--
Result:
['unknown',
 'unknown',
 'unknown',
 'unknown',
 'unknown',
 'unknown',
 'unknown',
 'unknown',
 'unknown',
 'unknown']
```

<pre>(要約: 最初の10ピクセルはunknownになったね)
It appears that the first 10 colors at \( x = 243 \) have been classified as "unknown,"
which means they did not fit into any of the predefined color ranges I assumed. 

(要約:でも、X=243全体だとうまくいってるかもだから、
ヒストグラムを書いてみたよ)
However, this is just a small portion of the extracted colors. To get a better
understanding of the distribution of colors along this line, I'll plot a histogram.
This will help us see which colors dominate along this vertical line and whether 
our color definitions need adjustment.
</pre>

```python
# Plot a histogram of the classified colors
plt.figure(figsize=(12, 6))
plt.hist(colors_at_x243, bins=np.arange(len(set(colors_at_x243)) + 1) - 0.5, rwidth=0.8, align='mid')
plt.xticks(rotation=45)
plt.title("Histogram of Colors at x=243")
plt.ylabel("Frequency")
plt.xlabel("Colors")
plt.tight_layout()
plt.show()
```

(↓X=243の色を指定した色に分類した後のヒストグラム)
![histc4217755-ea88-42a6-8b48-44d618d12be5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/6324ebb2-eb1b-b485-8048-cf18b8fe7417.png)


<pre>(要約: ヒストグラムにしてみると、
色の大部分を捉えてられなかったね)
The histogram shows the frequency of each classified color along the line 
at \( x = 243 \). As we can see, a significant portion of the colors are 
classified as "unknown," which suggests that our predefined color ranges 
may not perfectly capture the actual colors in the image.

(要約: とはいえ、いくらかは分類できてるから
それを元に領域分けするぞ！)
However, the presence of known colors such as "light skin tone," "white,"
and "strong slightly purplish blue" suggests that we can still proceed
with segmenting the regions based on these known colors.

Let's move to the next step: segmenting the y-axis into regions where 
the majority color is the same. This will help in identifying the 
contiguous areas corresponding to bangs, face, upper body clothing, 
and lower body clothing.
</pre>

```python
def segment_regions(colors):
    """
    Segment the y-axis into regions based on the majority color.
    """
    regions = []
    current_color = colors[0]
    start_idx = 0

    for idx, color in enumerate(colors):
        if color != current_color:
            regions.append((current_color, start_idx, idx))
            start_idx = idx
            current_color = color

    # Add the last region
    regions.append((current_color, start_idx, len(colors)))

    return regions

# Segment the colors into regions
regions = segment_regions(colors_at_x243)

# Adjusting the regions' indices since we took every second pixel
regions_adjusted = [(color, start_idx*2, end_idx*2) for color, start_idx, end_idx in regions]

regions_adjusted

--
Result

[('unknown', 0, 36),
 ('white', 36, 38),
 ('unknown', 38, 52),
 ('gray', 52, 54),
 ('unknown', 54, 56),
 ('gray', 56, 58),
 ('light sky blue', 58, 60),
 ('unknown', 60, 68),
 ('light sky blue', 68, 70),
 ('unknown', 70, 122),
 ('white', 122, 132),
 ('unknown', 132, 152),
 ('light sky blue', 152, 154),
…
 ('light skin tone', 412, 414),
 ('unknown', 414, 418),
 ('white', 418, 420),
 ('unknown', 420, 440),
 ('gray', 440, 442),
 ('unknown', 442, 444),
 ('white', 444, 452),
 ('unknown', 452, 514),
 ('white', 514, 532),
 ('unknown', 532, 534),
 ('white', 534, 576),
 ('unknown', 576, 578),
 ('white', 578, 582),
 ('unknown', 582, 768)]
```

<pre>(要約: 上から下まで分類したよ)
The list above shows the segmented regions along the line at \( x = 243 \). 
Each tuple represents a segment and contains the classified color, starting 
y-coordinate, and ending y-coordinate of the segment.

(要約: 指定されたゆいもっぷの領域に
対応させるぞ)
Now, we'll use the information from the segmented regions and the provided 
color ranges to identify the areas corresponding to the bangs, face, upper 
body clothing, and lower body clothing. We'll compute the center and the 
area of each of these regions and display them on the image.

Let's proceed with this.
</pre>

```python
# Define the expected regions and their color ranges
expected_regions = {
    'bangs': ('light sky blue', 0, 0.30 * img_rgb.shape[0]),
    'face': ('light skin tone', 0.15 * img_rgb.shape[0], 0.50 * img_rgb.shape[0]),
    'upper body clothing': ('white', 0.35 * img_rgb.shape[0], 0.80 * img_rgb.shape[0]),
    'lower body clothing': ('strong slightly purplish blue', \
      0.70 * img_rgb.shape[0], img_rgb.shape[0])
}

# Identify the areas corresponding to the expected regions
identified_regions = {}
for region, (color, start, end) in expected_regions.items():
    for seg_color, seg_start, seg_end in regions_adjusted:
        if seg_color == color and seg_end > start and seg_start < end:
            if region in identified_regions:
                # Extend the region if it's already identified
                identified_regions[region][1] = seg_end
            else:
                identified_regions[region] = [seg_start, seg_end]

identified_regions

# Now let's re-plot the image with the identified regions
plt.figure(figsize=(10, 10))
plt.imshow(img_rgb)
plt.axvline(x=243, color='red', linestyle='--')
plt.title("Image with line at x=243")

# For each identified region, calculate the center and draw a rectangle around the region
for region, (start_y, end_y) in identified_regions.items():
    center_y = (start_y + end_y) // 2
    plt.text(243, center_y, f'{region}\nCenter: (243, {center_y})', fontsize=12, color='white')
    rect = plt.Rectangle((0, start_y), img_rgb.shape[1], end_y - start_y, linewidth=1, edgecolor='r', facecolor='none')
    plt.gca().add_patch(rect)

plt.show()
--
Result
{'bangs': [58, 204], 'face': [212, 322], 'upper body clothing': [268, 582]}
```

<img width="370" alt="![wake18187fcd-cb45-4ade-b845-3716b937e252.png]" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/ec435fa5-b297-c5f5-2360-911a632da4e5.png">

- 「下半身の服」が出てなかったりしたけど、それまでの試行と比べ、<ins>とてもまともな結果</ins>がChatGPT上で<ins>出た</ins>ので、この手法で行こうと<ins>ゆいもっぷアニメクリエイター</ins>は思ったのだった
    - (実際にうまくいった)
- なお、↑の最後の画像の部分は、ChatGPTの会話履歴を1.5ヶ月遡ってみてみても、その最後の部分だけ残って無かったので、「以下の回答の部分の出力画像を再現してください」と ChatGPT4にお願いして得ました。


### ☆☆ 目(紫)検出 ☆☆ (さらに到達した手法)(10/23) 👁‍🗨

- ゆいもっぷちゃんの **<ins>目は紫</ins>** (<ins>基礎知識</ins>)
    - ゆいもっぷちゃんの誕生日は **<ins>3月5日</ins>** (<ins>基礎知識</ins>)
        - (<ins>全ての会社</ins>は3月5日を「 **<ins>ゆいもっぷ記念休み</ins>** 」にすると良いと思います)
            - (そういうのがあったら<ins>ゆいもっぷ先進的</ins>な **<ins>ゆいもっぷ会社</ins>** だと思います)
    - ゆいもっぷちゃんの(中学での)部活は **<ins>剣道部</ins>** (<ins>基礎知識</ins>)
    - ゆいもっぷちゃんの<ins>魅力</ins>: 言動がかわいい(ツッコミ&天然)、カワボ、イケボ、両声類、酒豪、 **<ins>若い女子に超モテ (人柄)</ins>** 、飲酒配信 (飲みながらゲーム配信) (テイルズのパズル)
        - みんなもほしいもでお酒を送ろう！
    - 紫は他の箇所に出てこないので、とても検出しやすい(<ins>神デザイン</ins>)
- 残念ながら、ChatGPTの<ins>知識はゆいもっぷがいない世界の知識に限定</ins>されているので、「<ins>目だけが紫</ins>」という知識を使って、「紫を目の候補として活用」しようと考えついたのは僕だと思う (ゆいもっぷプロ)

<pre>質問:
この画像の中には紫(RGB(155,71,176)やRGB(244,163,218) に近い色)が
多く集まった領域が2つあります。
それらの2つの領域の重心点を求めて、この画像の上に図示してください。

(指示はDeepLで英訳した)
</pre>

(入力画像群)
<img width="270" alt="![third.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/e34688b2-d28b-fc37-6e97-331082840974.png">

<img width="270" alt="![second.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/9483a3e2-0b92-1bec-26e1-01a6d8250be0.png"> 

(ゆいもっぷ汎用性を目指すので2枚) 
(※: 1枚ずつChatGPTの別セッションで目を特定してもらった)

- ↓かなり漠然としたような感じの指示だったけど、<ins>ChatGPTが自分で手順を具体化</ins>して良い結果を出してくれた
    - <ins>紫マスク</ins>を作る手法は僕 (<ins>ゆいもっぷアニメクリエイター</ins>) の中には無かった (ChatGPTに教えてもらった)
<pre>回答:
(要約: ゆいもっぷアニメクリエイターの指示を解釈 
& アウトライン提示)
To identify and compute the center of gravity of the purple regions in the image, 
I'll take the following steps:

(要約: 読み込み → RGBに → 紫範囲を定義
→ 紫マスクを作る → そのマスクの輪郭を取る
→ 輪郭の重心を取る)
1. Load the image and visualize it.
2. Convert the image to the RGB color space.
3. Define a color range for the purple shades based on the provided RGB values.
4. Create a binary mask for the purple regions.
5. Find the contours of the binary mask.
6. Compute the center of gravity for each contour.
7. Plot the centers of gravity on the original image.

Let's start by loading and visualizing the image.

(画像読み込み結果は略)

(要約: 紫マスクを作る)
The image has been loaded successfully. Now, I will proceed to identify 
the purple regions by creating a mask based on the provided RGB values. 
I will define a range around these values to accommodate slight variations 
in the purple shades.
</pre>

```python
import matplotlib.pyplot as plt
import cv2

# Load the image using OpenCV
image = cv2.imread('/mnt/data/third.png')
# Convert the image from BGR to RGB format
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Display the image
plt.imshow(image_rgb)
plt.axis('off')
plt.show()


import numpy as np

# Define the color ranges for the two shades of purple
lower_purple1 = np.array([140, 60, 160])
upper_purple1 = np.array([170, 85, 195])

lower_purple2 = np.array([230, 150, 210])
upper_purple2 = np.array([255, 180, 230])

# Create binary masks for each shade of purple
mask_purple1 = cv2.inRange(image_rgb, lower_purple1, upper_purple1)
mask_purple2 = cv2.inRange(image_rgb, lower_purple2, upper_purple2)

# Combine the masks
combined_mask = cv2.bitwise_or(mask_purple1, mask_purple2)

# Display the combined mask
plt.imshow(combined_mask, cmap='gray')
plt.axis('off')
plt.show()
```

(↓ChatGPTが作った<ins>紫マスク</ins>)
<img width="370" alt="![purple_2287285f-01c8-4f36-b193-54a1cf16af36.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/95082a00-fcfd-eead-f629-e22c93cb3c05.png">

<pre>(要約: 各紫の輪郭をとって重心を
計算するよ)
Next, I will detect the contours of these regions and compute the 
center of gravity for each one.

The centers of gravity for the two purple regions have been successfully 
identified and marked with blue circles on the image.
</pre>

```python
# Find the contours in the binary mask
contours, _ = cv2.findContours(combined_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# Compute the center of gravity for each contour
centers = []
for contour in contours:
    M = cv2.moments(contour)
    if M["m00"] != 0:  # To avoid division by zero
        cX = int(M["m10"] / M["m00"])
        cY = int(M["m01"] / M["m00"])
    else:
        cX, cY = 0, 0
    centers.append((cX, cY))

# Plot the centers of gravity on the original image
image_with_centers = image_rgb.copy()
for center in centers:
    # Draw a blue circle at each center
    cv2.circle(image_with_centers, center, 10, (255, 0, 0), -1)  

plt.imshow(image_with_centers)
plt.axis('off')
plt.show()
```

(↓ChatGPTが<ins>抽出した紫ポイント</ins>(上だけにすれば目になる ←人間が持つ<ins>「ゆいもっぷ知識」</ins>の賜物))
<img width="370" alt="![me_73644679-d812-4fda-acdd-c1d24898beac.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/2c7f3429-1358-bccc-eb67-f633f123f381.png">

(回答中では blue circle と間違えるのがかわいい)

- 基本的な原理は↑のまま、手元でプログラムを育てていく段階では、<ins>上の方(というか顔の範囲内)にあるものだけにしたり、左と右で1つずつにしたりとブラッシュアップ</ins>させました。
    - (そこでも、どういう手法でそういうことをやろうか考えて、ChatGPTにコードを書かせた)

(↓もう一枚の検出結果) (ゆいもっぷ汎用性有り)
<img width="370" alt="![eye222.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/b33a1ee0-a8d1-0c2d-c71c-69b397030189.png">

### edge を中心から外に走査して、つながってていけるところまでとって、ゆいもっぷ本体の<ins>端っこ</ins>がどこかを検出する方向性

- 同じようにChatGPTに指示して、↑のことをして、ゆいもっぷX中心ではなく、<ins>端っこ</ins>も取れるようにした
    - (最終的な統合したコードの中で見れます)

- 入力画像
    - https://ao-love-yuimop.hatenablog.com/entry/2023/10/11/001819 にある<ins>ゆいもっぷアニメ原画</ins>です(<ins>「ラーメン」シーンの(実際に動画になる)第二原画</ins> 1枚め)

![gen1(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/161a1be1-ecbc-0517-1418-7a067e619143.png)

ChatGPTが<ins>edge化</ins>した

![edges_gen1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/b437a3ac-f72d-f356-9530-0d7be9be8119.png)

このエッジ画像に対して、以下のような指示をした

<pre>質問:
このエッジ画像のY=269の高さの部分について、
画像のX軸について中心から左に走査していって、
周囲30ピクセル以内に他のエッジがある中で
最も左にある点を出すコードを書いてください。英語で回答してください。

(回答略)

上下だけでなく左右にもエッジがある場合はOKにしてください

(回答略)

「中心から左に走査していく」のが肝要ですから、
最初の点が条件を満たしていたら、
その左にさらに条件を満たす点がないかどんどん左をチェックしていってください

(回答略)

(↓普通に条件のand/or を間違えてたので指摘したら正解した)
radius を 30 に戻して、 
if vertical_edge_found and horizontal_edge_found: の条件を
if vertical_edge_found or horizontal_edge_found: にして
 再実行してください。

 --
 Result 
 (362, 269)
</pre>


<img width="700" alt="![image2023-10-29_13-49-32.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/88072118-9868-b52c-d56b-058f5e5b8a76.png">


- ◎ 正解は (392, 269) なので、最後にradius (= 30) だけ行き過ぎるので、<ins>30を足せば正解になるコードが得られた</ins>
    - 難しいと、惜しいところまでいくけど、<ins>簡単な間違い</ins>をするので、そこさえ(ChatGPTに指摘したりして) 修正すれば正解になる


- 上記のように、こういう方向性 (手法) を、指示をするだけでChatGPTに実行させて実行結果を図示させるまでできて、とても<ins>お手軽に</ins>色々な手法を<ins>試せる</ins>のがいいですね

### 小ネタ

- プログラミング言語の習得に便利なChatGPT

(PHPでいう in_array の Python版)
<pre>質問:
if (region_name == "face" or region_name == "mouth" 
or region_name == "upper_body_clothing") : をスッキリ書き直してください

ー
回答: 
Pythonでは、複数の条件をチェックする際に `in` 演算子を使用して、
値がリスト内に存在するかどうかを確認することができます。これにより、
コードをスッキリと書き換えることができます。以下のように書き直すことができます：

```python
if region_name in ("face", "mouth", "upper_body_clothing"):
```

このコードは `region_name` が指定されたいずれかの文字列に等しい場合に条件を満たすと判断します。
`in` はリスト、タプル、セットなどのイテラブルに対して使うことができ、コードが読みやすくなります。
</pre>


- ↓回答が間違ってる時に、下みたいに指摘してプログラムを修正してもらうのも結構する

<pre>質問:
[(610, 230), (617, 337), (638, 258)] を引数にとって、外れ値を除く関数を作りたいです。
つまり、この場合は、2点目だけが337とY軸座標が離れていて、これを弾きたいわけです。

つまり、638/610 = 1.0459 なのでこれはそんなに離れてないので許容して
258/230 = 1.1217 なので、これも許容して、
337/230 = 1.4652 なので、これは許容しない、
まぁ、他のグループ(点の数が多いグループ)から 25%以上X軸座標またはY軸座標が離れていたら、
その点を除く関数を作ってください。

(回答略)

質問:
それではダメです。求めているのは、返り値が [(610, 230),  (638, 258)] となる関数です。

--
回答:
了解しました。関数を修正して、期待通りの結果が得られるようにします。
修正するアプローチは、各点を他の点と比較し、X軸とY軸の座標が他のグループから
25%以上離れている場合にのみ外れ値とみなします。この方法で、期待される結果 
[(610, 230), (638, 258)] を得ることができるはずです。修正してみます。
(後略)
</pre>


## ChatGPTから出てきたプログラムを手元にコピペして伸ばしていく編

- 上記のような感じで、手法をChatGPTに試させて、<ins>どの手法がうまくいくか知り</ins>、また、コードを書いてもらって、あとは、<ins>書いてもらったコードを手元でつなげていって</ins>、ほしいプログラムにしていった (↓↓につなげた結果)


# 🏆👑成果👑🏆

- 横長画像の出典は https://ao-love-yuimop.hatenablog.com/entry/2023/10/11/001819 にある<ins>ゆいもっぷアニメ原画</ins>です(「<ins>ラーメン</ins>」シーンの(実際に動画になる)<ins>第二原画</ins> 1～3枚め)

![gen1-6_kamp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/35fd0c94-0083-ce3f-9172-86a576266358.png)

- ↑のデバッグ用出力画像(↓)
- 目標のX軸座標における、指示で指定した色への分類結果
    - こういうデバッグ出力をリッチにできるのがChatGPTの魅力

<img width="370" alt="[gen1-6_611.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/863dcffd-5698-a879-12be-42f4927c4597.png">

![gen2-5_kamp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/d85465cd-bbf9-2a84-0705-15812bdc2c05.png)

<img width="370" alt="![gen2-5_608.png](" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/35c28d8c-f790-18f6-c9be-ff9b656510ec.png">


![gen3-5_kamp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/b6461ebd-8e3f-2553-00d1-0930742edfee.png)

(↑豪快な手づかみ(ゆいもっぷアニメ動画では動く))

<img width="370" alt="![gen3-5688.png]" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/2425604d-2e02-48bf-2742-9df5ff29c30d.png">


(↑ これくらいのズレなら、 SmoothGraphic や 画像生成AI はとても強力なのできっと大丈夫！)(11/23の結果)

----

(↓ 端点なし: 10/23の結果)

![2023-10-23fig3_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/6b767f74-bb05-b365-4c9b-f510ccc36a3f.png)

![sec_regFigure_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/046e6f3b-0807-4b63-f4d8-9e4587aa848c.png)

![thfukFigure_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/d3753597-4fe2-236a-ba0e-b4ccd2412de5.png)

# コード全文

## 入力画像ごとに入力する部分

- 分類先の色も、領域が大体何割のところにあるかも、<ins>やっていたら</ins>多くのゆいもっぷ画像に共通する<ins>決定版が見つかるのでは</ins>ないかな？
    - アニメ原画は前後の原画とかなり構成が近いので入力を相当使い回せると思う

### 基本固定でいけるはずの部分

- 分類先の色
- どれが髪色か
- どれが首色か
- どれが肌色か
- 背景色？ 最近は使ってない
- どの領域が大体どのあたりにあるか (上から15%～50%とか)


## コード全文

- よい子(<ins>よいゆいもっぷファン</ins>)は関数をクラスに分けるととても見やすくなります
    - 関数になってないコードを<ins>関数分け</ins>してもらったり、「このコードの関数を<ins>クラスに分ける</ins>としたら、どう分けるといいですか？」みたいなのも ChatGPT 4 (旧Code Interpreter) にコードを貼り付けて訊いたら、適切に分けてくれると思います。
    - <ins>テストコード</ins>を書かせて、それを満たすように本体のコードを書かせる方向性もありそう
- みなさんもこのコードを実行して、<ins>ゆいもっぷちゃんの特徴点</ins>を得たり、 **<ins>ゆいもっぷアニメを作る</ins>** といいと思います


```python
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np

# ↓ 画像ごとに入力する部分
image_name = "gen1"
main_hair_color_name = "brighter_light_blue"
neck_color_name = "bit_dark_neck"
eyelash_colors = ["eyelash_black", "dark_mouth"]
if image_name == "gen1":
   neck_color_name = "little_dark_neck"
skin_color = "medium_skin"
skin_colors = ["medium_skin", "medium_dark_skin", "little_dark_skin", "bit_dark_neck"]#, "little_dark_neck"]
edge_skin_colors = skin_colors
left_shoulder_colors = right_shoulder_colors = skin_colors
if image_name == "gen3":
  left_shoulder_colors = ["light_dark_skin", "medium_dark_skin"]
  right_shoulder_colors = ["dark_skin", "medium_dark_skin"]
if image_name == "gen2":
  edge_skin_colors = ["little_dark_skin"]
hair_colors = ["very_brighter_light_blue", "brighter_light_blue", "light_blue"]
# ↓これが悪さして、下半身の服を取れてない疑いもある
background_color_name = "orange"
adjust_dogear_y = 0
if image_name == "gen1":
  adjust_dogear_y = -10

image_path = "C:\\Users\\ao_love_yuimop\\images\\" + image_name + ".png"

# Define representative colors
color_values = {
    # first
    "yellow": (254, 226, 55),
    "gray": (221, 216, 237),
    "white": (255, 252, 252),
    "black": (0, 0, 0),
    # second
    "orange": (255, 165, 0),
    "gray_as_shadow_of_white": (128, 128, 128),
    "brighter_white": (255, 255, 255),
    "strong_slightly_purplish_blue": (70, 130, 180),
    # third
    "darker_white": (226, 224, 240),
    "dull_blue_green": (59, 142, 181),
    # mouth
    "dull_mouth": (207, 117, 126),
    "dark_mouth": (139, 36, 50),
    # hair
    "very_brighter_light_blue": (227, 244, 250),
    "brighter_light_blue": (173, 216, 230),
    "light_blue": (157, 223, 237),
    "blue": (75, 112, 187),
    "shadow_blue": (28, 51, 110),
    # eye
    "eye_bit_highlight": (238, 103, 255),
    "eyelash_black": (27, 24, 34),
    # neck
    "neck": (250, 198, 189),
    "bit_dark_neck": (210, 174, 176),
    "little_dark_neck": (201, 129, 126),
    "dark_neck": (167, 105, 107),
    # skin
    "light_skin": (251, 241, 232),
    "light_skin_tone": (255, 244, 229),
    "medium_skin": (237, 215, 211),
    "medium_dark_skin": (237, 206, 198),
    "little_dark_skin": (214, 194, 164),
    "dark_skin_tone": (205, 175, 149),
    "dark_skin": (180, 173, 133),
    # light
    "shining_light": (226, 216, 197),
    "dull_light": (201, 152, 131)
}
if image_name == "first":
  use_colors = ["yellow", "light_blue", "light_skin", "gray", "white", "blue", "shadow_blue", "dark_neck"] # first
if image_name == "second":
  use_colors = ["orange", "brighter_light_blue", "light_skin_tone", "dark_skin_tone", "gray_as_shadow_of_white", "brighter_white", "strong_slightly_purplish_blue", "neck"] # second
if image_name == "third":
  use_colors = ["light_skin_tone", "very_brighter_light_blue", "darker_white", "dull_blue_green", "orange"] # third
if image_name == "gen1":
  use_colors = ["light_skin_tone", "eyelash_black", "light_blue", "dull_light", "medium_dark_skin", "shining_light", "medium_skin", "dull_mouth", "little_dark_neck", "very_brighter_light_blue", main_hair_color_name, "white", "orange"] # gen1
if image_name == "gen2":
  use_colors = ["light_skin_tone", "eyelash_black", "little_dark_skin", "bit_dark_neck", "light_blue", "dull_light", "medium_dark_skin", "shining_light", "medium_skin", "dull_mouth", "dark_mouth", "very_brighter_light_blue", main_hair_color_name, "white", "orange"] # gen1
if image_name == "gen3":
  use_colors = ["light_skin_tone", "eyelash_black", "eye_bit_highlight", "dark_skin", "dark_mouth", "dark_neck", "little_dark_skin", "bit_dark_neck", "light_blue", "dull_light", "medium_dark_skin", "shining_light", "medium_skin", "dull_mouth",  "very_brighter_light_blue", main_hair_color_name, "white", "orange"] # gen1

# Define the regions and their criteria
# first
if image_name == "first":
  regions = {
      "bangs": {"colors": ["light_blue"], "range": (0, 0.3)},
      "face": {"colors": ["light_skin"], "range": (0.15, 0.5)},
      "upper_body_clothing": {"colors": ["white", "gray"], "range": (0.35, 0.8)},
      "lower_body_clothing": {"colors": ["blue", "shadow_blue"], "range": (0.5, 1.0)}
  }
if image_name == "second":
  # second
  regions = {
      "bangs": {"colors": ["brighter_light_blue"],"range": (0, 0.3)},
      "face": {"colors": ["light_skin_tone"],"range": (0.15, 0.50)},
      "upper_body_clothing": {"colors": ["brighter_white", "gray_as_shadow_of_white"],"range": (0.35, 0.80)},
      "lower_body_clothing": {"colors": ["strong_slightly_purplish_blue"],"range": (0.70, 1)}
  }
# third
if image_name == "third":
  regions = {
      "bangs": {"colors": ["very_brighter_light_blue"], "range": (0, 0.3)},
      "face": {"colors": ["light_skin_tone"], "range": (0.15, 0.5)},
      "upper_body_clothing": {"colors": ["darker_white"], "range": (0.35, 0.8)},
      "lower_body_clothing": {"colors": ["dull_blue_green"], "range": (0.55, 1.0)}
  }
if image_name == "gen1":
  regions = {
      "bangs": {"colors": ["very_brighter_light_blue", "brighter_light_blue", "light_blue"], "range": (0, 0.3)},
      "face": {"colors": ["light_skin_tone", "medium_skin", "medium_dark_skin"], "range": (0.15, 0.5)},
      "mouth": {"colors": ["dull_mouth"], "range": (0.15, 0.5)},
      "neck_shadow": {"colors": ["little_dark_neck", "dull_light"], "range": (0.25, 0.65)},
      "upper_body_clothing": {"colors": ["white"], "range": (0.35, 0.8)},
      "lower_body_clothing": {"colors": [], "range": (0.55, 1.0)}
  }
if image_name == "gen2":
  regions = {
      "bangs": {"colors": ["very_brighter_light_blue", "brighter_light_blue", "light_blue"], "range": (0, 0.3)},
      "face": {"colors": ["light_skin_tone", "medium_skin", "medium_dark_skin"], "range": (0.15, 0.65)},
      "mouth": {"colors": ["dull_mouth", "dark_mouth"], "range": (0.15, 0.65)},
      "neck_shadow": {"colors": ["bit_dark_neck", "dull_light", "medium_dark_skin"], "range": (0.25, 0.65)},
      "upper_body_clothing": {"colors": ["white"], "range": (0.35, 0.8)},
      "lower_body_clothing": {"colors": [], "range": (0.55, 1.0)}
  }
if image_name == "gen3":
  regions = {
      "bangs": {"colors": ["very_brighter_light_blue", "brighter_light_blue", "light_blue"], "range": (0, 0.3)},
      "face": {"colors": ["light_skin_tone", "medium_skin", "medium_dark_skin"], "range": (0.15, 0.65)},
      "mouth": {"colors": ["dull_mouth", "dark_mouth", "dark_neck"], "range": (0.15, 0.65)},
      "neck_shadow": {"colors": ["dark_neck", "bit_dark_neck", "dull_light"], "range": (0.25, 0.65)},
      "upper_body_clothing": {"colors": ["white"], "range": (0.35, 0.8)},
      "lower_body_clothing": {"colors": [], "range": (0.55, 1.0)}
  }

# ↑画像ごとに入力することはここまで

# Define colors for each region
region_colors = {
    'bangs': 'blue',
    'face': 'yellow',
    'mouth': 'red',
    'neck_shadow': 'green',
    'upper_body_clothing': 'orange',
    'lower_body_clothing': 'purple',
    'eye_0': 'brown',
    'eye_1': 'brown'
}

debug_eye_centers = True #True #False
colors = {k: v for k, v in color_values.items() if k in use_colors} # 内包表記で辞書を作成

### 1-A 画像を読み込む
# Load the image
img = Image.open(image_path)


### 1-B HaarでX軸を取る (現在2023/11/04では、目のX軸の平均を使ってるので Haarは不使用)
import cv2
import numpy as np
from matplotlib import pyplot as plt

# Load the image
image = cv2.imread(image_path)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

"""
# Load Haar cascade for face detection
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

# Detect face
faces = face_cascade.detectMultiScale(image, 1.1, 4) # 1.1, 4 だった first second うまくいったのは

# Draw rectangle around the detected face
for (x, y, w, h) in faces:
    cv2.rectangle(image_rgb, (x, y), (x+w, y+h), (255, 0, 0), 2)

# Calculate the middle X value
if len(faces) == 1:
    middle_x = (faces[0][0] + faces[0][0] + faces[0][2]) / 2
else:
    middle_x = None

if 1:
  plt.imshow(image_rgb)
  plt.title('Face Detection using Haar Cascades')
  plt.axis('off')
  plt.show()

print ("1-B: ミドル")
print(middle_x)
x_column = int(middle_x)
"""

### 1-C 両目の領域を取って、その中心をキャラのX軸位置とする
# Define the color ranges for the two shades of purple
# todo: 次はここをリストにしたい2023/10/24  9:40
lower_purple1 = np.array([140, 60, 160])
upper_purple1 = np.array([170, 85, 195])

lower_purple2 = np.array([230, 150, 210])
upper_purple2 = np.array([255, 180, 230])

# RGB(70,58,120) の前後
lower_purple3 = np.array([60, 50, 110])
upper_purple3 = np.array([80, 70, 130])

# RGB(184,113,201) の前後
lower_purple4 = np.array([174, 103, 191])
upper_purple4 = np.array([194, 123, 211])

# RGB(45,37,112) の前後
lower_purple5 = np.array([35, 27, 102])
upper_purple5 = np.array([55, 47, 122])

# RGB(243,155,220) の前後
lower_purple6 = np.array([233, 145, 210])
upper_purple6 = np.array([253, 165, 230])

# RGB(74,47,84) の前後
lower_purple7 = np.array([64, 37, 74])
upper_purple7 = np.array([84, 57, 94])

# RGB(255,196,248) の前後
lower_purple8 = np.array([245, 186, 238])
upper_purple8 = np.array([265, 206, 258])

# RGB(57, 18, 148) の前後
lower_purple9 = np.array([47, 8, 138])
upper_purple9 = np.array([67, 28, 158])

# RGB(73, 45, 139) の前後
lower_purple10 = np.array([63, 35, 129])
upper_purple10 = np.array([83, 55, 149])


# Create binary masks for each shade of purple
mask_purples = []
mask_purples.append (cv2.inRange(image_rgb, lower_purple1, upper_purple1))
mask_purples.append (cv2.inRange(image_rgb, lower_purple2, upper_purple2))
mask_purples.append (cv2.inRange(image_rgb, lower_purple3, upper_purple3))
mask_purples.append (cv2.inRange(image_rgb, lower_purple4, upper_purple4))
mask_purples.append (cv2.inRange(image_rgb, lower_purple5, upper_purple5))
mask_purples.append (cv2.inRange(image_rgb, lower_purple6, upper_purple6))
mask_purples.append (cv2.inRange(image_rgb, lower_purple7, upper_purple7))
mask_purples.append (cv2.inRange(image_rgb, lower_purple8, upper_purple8))
mask_purples.append (cv2.inRange(image_rgb, lower_purple9, upper_purple9))

# Combine the masks
combined_mask = mask_purples[0]
for mask_purple in mask_purples:
  combined_mask = cv2.bitwise_or(combined_mask, mask_purple)

# Display the combined mask
if debug_eye_centers:
  plt.figure(figsize=(10, 9), num="Purple Combined Mask")
  plt.imshow(combined_mask, cmap='gray')
  plt.axis('off')
  plt.show(block=False) # ここをやると紫なところが表示される

## このあたりから本当に目の位置を特定しようとしていく
# まずは、紫なところを出して目っぽいところを出す
# Find the contours in the binary mask
contours, _ = cv2.findContours(combined_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# Compute the center of gravity for each contour
centers = []
for contour in contours:
    M = cv2.moments(contour)
    if M["m00"] != 0:  # To avoid division by zero
        cX = int(M["m10"] / M["m00"])
        cY = int(M["m01"] / M["m00"])
    else:
        cX, cY = 0, 0
    if (cX != 0 and cY != 0):
      centers.append((cX, cY))


# 1-C-b 目の候補地点について、Y軸の値が 350 以上のものはなくして、近すぎる点は1つにまとめる(できれば平均化したい)
def average_points(points):
    """Compute the average of a list of points."""
    x_coords = [x for x, y in points]
    y_coords = [y for x, y in points]
    return (int(np.mean(x_coords)), int(np.mean(y_coords)))

def distance(point1, point2):
    """Compute the Euclidean distance between two points."""
    return np.sqrt((point1[0] - point2[0])**2 + (point1[1] - point2[1])**2)

# Filter out points with Y value >= 350
filtered_points = [point for point in centers if point[1] < 350]

print ("1-C-b: 元々の両目の中心")
print (filtered_points)

# 1-C-b: 元々の両目の中心を全部図示する
# Plot the centers of gravity on the original image
def plot_centers(image_rgb, centers, title):
  plt.figure(figsize=(10, 9), num=title)
  image_with_centers = image_rgb.copy()
  for center in centers:
      cv2.circle(image_with_centers, center, 7, (255, 0, 0), -1)  # Draw a blue circle at each center

  plt.imshow(image_with_centers)
  plt.title(title)
  plt.axis('off')
  plt.show(block=False) # ここをやると両目の位置が表示される

if debug_eye_centers:
  plot_centers(image_rgb, filtered_points, "1-C-b: original eye centers")

# Combine points that are too close to each other
def merge_close_points(filtered_points):
  threshold = 25 # fst では15 だったがg1で25にした
  combined_points = []
  already_combined = set()

  for i in range(len(filtered_points)):
      if i in already_combined:
          continue
      close_points = [filtered_points[i]]
      for j in range(i+1, len(filtered_points)):
          if distance(filtered_points[i], filtered_points[j]) <= threshold:
              close_points.append(filtered_points[j])
              already_combined.add(j)
      combined_points.append(average_points(close_points))
  return combined_points

# 紫なところで出した点を近いところはまとめる(それが1stになる)
centers = merge_close_points(filtered_points)
print ("1-C-b: 両目の中心(1回目)")
print (centers)
if debug_eye_centers:
  plot_centers(image_rgb, centers, "1-C-b: combined eye centers(1st)")

# 1stの点をさらに近いものをまとめて2nd にする
print ("1-C: 両目の中心(2回目)")
centers = merge_close_points(centers)
print (centers)
if debug_eye_centers:
  plot_centers(image_rgb, centers, "1-C-b: combined eye centers(2nd)")

# image 上の points の中から、 color_range_start と color_range_end の範囲内の色を一番多く持つ点を返す
# ついさっきまで使われてなかったが、これを黒的ポイントを消すのに使いたい…が color_range_start / end という仕組みが使いにくいので、
# B が R の threshold = 1.5 以上無い点はなくす、という仕組みに改造した別の関数にしてそっちを使おう
def find_points_with_color(image, points, color_range_start, color_range_end):
    counts = []

    for point in points:
        x, y = point
        count = 0

        # Loop through each pixel within a radius of 10 pixels from the point
        for i in range(-10, 11):
            for j in range(-10, 11):
                if 0 <= x+i < image.shape[1] and 0 <= y+j < image.shape[0]:  # Ensure we're within image boundaries
                    if np.linalg.norm([i, j]) <= 10:  # Ensure we're within a circle of radius 10
                        pixel = image[y+j, x+i, :3]  # Get the RGB values, ignore alpha
                        # Check if the pixel color is within the specified range
                        if np.all(pixel >= color_range_start) and np.all(pixel <= color_range_end):
                            count += 1

        counts.append(count)

    # Get the points with the highest and second highest counts
    sorted_indices = np.argsort(counts)[::-1]
    return points[sorted_indices[0]] #, points[sorted_indices[1]]  # Return the top two points

import numpy as np

# image 上の points の中から、B が R の1.5倍以上でない点を取り除く
def filter_points_by_color_ratio(image, points, threshold=1.5):
    filtered_points = []

    for point in points:
        x, y = point

        # Check if the point is within the image boundaries
        if 0 <= x < image.shape[1] and 0 <= y < image.shape[0]:
            pixel = image[y, x, :3]  # Get the RGB values, ignore alpha

            # Check if B is not less than 1.5 times R
            if pixel[2] >= threshold * pixel[0]:
                filtered_points.append(point)

    return filtered_points


# points を 近いものでグループにして、2つのグループに分ける
# @param: points: list ex [(214, 371), (141, 188), (155, 185), (228, 186)]
def split_points_by_nearest_center(points):
    # Sort the points based on their x-coordinates
    sorted_points = sorted(points, key=lambda point: point[0])

    # Define the two centers as the first and last points in the sorted list
    center1, center2 = sorted_points[0], sorted_points[-1]

    group1 = []
    group2 = []

    # Assign each point to the group of the nearest center
    for point in sorted_points:
        if abs(point[0] - center1[0]) < abs(point[0] - center2[0]):
            group1.append(point)
        else:
            group2.append(point)

    return group1, group2

# 座標のリストの平均点を返す
def center_point(points):
  x_mean = round(sum(p[0] for p in points) / len(points))
  y_mean = round(sum(p[1] for p in points) / len(points))

  center_point = (x_mean, y_mean)

  return center_point


# Split the points into two groups based on proximity to the centers
#group1, group2 = split_points_by_nearest_center(points)
#group1, group2
# 2nd のポイントを左と右に分けて処理していく
splited_points_by_left_right = split_points_by_nearest_center(filter_points_by_color_ratio(image_rgb, centers, 1.1))

print ("splited_points_by_left_right", splited_points_by_left_right) if debug_eye_centers else None

# X軸座標かY軸座標のどちらかが外れ値の場合、その点を削除する
def remove_outliers(points, threshold=0.25):
    """
    Removes outliers from a list of points based on the threshold, comparing each point with others directly.

    :param points: List of tuples representing (x, y) coordinates
    :param threshold: Threshold for determining outliers
    :return: List of points without outliers
    """
    # Function to determine if a point is an outlier compared to a list of points
    def is_outlier(point, other_points):
        if not other_points:
            return False
        x_coords = [p[0] for p in other_points]
        y_coords = [p[1] for p in other_points]
        avg_x = sum(x_coords) / len(x_coords)
        avg_y = sum(y_coords) / len(y_coords)

        return abs(point[0] - avg_x) / avg_x > threshold or abs(point[1] - avg_y) / avg_y > threshold

    # Filter out points that are outliers
    filtered_points = []
    for point in points:
        if not is_outlier(point, [p for p in points if p != point]):
            filtered_points.append(point)

    return filtered_points


# 左と右に分けた2nd の点を外れ値を除いて、それぞれの中心点を3rdにする
left_eye = center_point(remove_outliers(splited_points_by_left_right[0]))
right_eye = center_point(remove_outliers(splited_points_by_left_right[1]))

print ("left_eye", left_eye) if debug_eye_centers else None
print ("right_eye", right_eye) if debug_eye_centers else None
def get_column_data(img, x_column):
  column_data = np.array(img)[:, x_column, :3]  # Extract only RGB, ignore alpha if present
  return column_data

from typing import Tuple

def classify_color_by_rgb(pixel: Tuple[int, int, int]) -> str:
    """Classify the color of a pixel based on RGB values."""
    min_distance = float("inf")
    classified_color = None

    for color_name, representative_rgb in colors.items():
        distance = np.linalg.norm(np.array(pixel) - np.array(representative_rgb))
        if distance < min_distance:
            min_distance = distance
            classified_color = color_name

    return classified_color

def classify_colors(column_data):
  classified_colors = [classify_color_by_rgb(column_data[i]) for i in range(0, column_data.shape[0], 1)]
  return classified_colors

def get_continuous_color_groups(classified_colors):
    # This function returns a list of tuples indicating the start and end indices of continuous color groups
    if not classified_colors:
        return []

    groups = []
    current_color = classified_colors[0]
    start_idx = 0

    for idx, color in enumerate(classified_colors[1:], 1):
        if color != current_color:
            groups.append((start_idx, idx-1))
            start_idx = idx
            current_color = color

    # Add the last group
    groups.append((start_idx, len(classified_colors)-1))

    return groups

# regionごとに、criteria['range']内で 色基準(criteria['colors'])にある色の領域の start, end center を返す
# @param: criteria: dict ex {"colors": ["light_skin", "medium_skin", "medium_dark_skin"], "range": (0.15, 0.5)}
def region_start_end_center(region_name, criteria, classified_colors, x_column):
  print ("reg_st st", "region_name", region_name, criteria, x_column, "skin_colors", skin_colors) if debug_eye_centers else None
  total_length = len(classified_colors)
  start_percentage, end_percentage = criteria["range"]
  start_index = int(start_percentage * total_length)
  end_index = int(end_percentage * total_length)
  appended_count = 0
  print ("start_index", start_index, "end_index", end_index) if debug_eye_centers else None

  # Find the indices for the specific region colors within the defined range
  region_indices = []  #[i for i, color in enumerate(classified_colors[start_index:end_index]) if color in criteria["colors"]]
  eye_above_skin_exists = False
  eyelash_found = False
  eye_below_skin_count = 0
  # not_continuousで始まる場合のためにTrueで始める
  is_eye_initial_not_continuous = True
  is_eye_first_not_continuous = False
  eye_above_not_continuous_count = 0
  #if region_name == "face":
    #print (region_name)
  #print (classified_colors[start_index:end_index]) if  region_name == "eye" and debug_eye_centers else None
  for i, color in enumerate(classified_colors[start_index:end_index]):
    # 目の場合、目の下の肌の色が10px以上続いたら、そこまでを領域とする
    # 目の領域のY軸方向に沿っての探索が、not_continuous で始まると、 eye_above_skin_exists が Trueにならないので、
    # i > 40でもう目の中にいると判定している (下にもう一つある)
    if (region_name in ("eye") and color in skin_colors and (eye_above_skin_exists or i > 40)):
       eye_below_skin_count += 1
    else:
       eye_below_skin_count = 0

    # 顔、口、上半身の服の場合、首の色が出てきたら、そこまでを領域とする (既に30px以上領域を追加していたら)
    if (region_name in ("face", "mouth", "upper_body_clothing") and (color == neck_color_name or color in regions["neck_shadow"]["colors"]) and appended_count > 25)\
      or (region_name in ("neck_shadow") and (color in skin_colors and not (color in criteria["colors"])) and appended_count > 30)\
      or (region_name in ("eye") and color in skin_colors and appended_count > 40 and eye_below_skin_count > 10 and (eye_above_skin_exists or i > 40)): # 50は厳しすぎるかも。大きい目しか取れなさそう
      print ("breaking")
      break

    # todo: 目の上部は eyelash_color で判定するようにしたので、これらの 目の上にskinがあるかや目の上の not_continuous のコードは要らないかも
    #       でも、まつ毛の色が髪色と同じ時に役立つかも
    if region_name == "eye":
      if (color == "not_continuous" and is_eye_first_not_continuous == True):
          is_eye_initial_not_continuous = False
          is_eye_first_not_continuous = False


    # 上半身の服では、上から走査して首の色が出てきたら、リセットする
    # 下半身の服では、上から走査して背景色が出てきたら、リセットする (つまり、背景より下の部分だけを抽出する)
    # 顔の場合、上から走査して髪の色が出てきたら、リセットする
          # or (region_name in ("eye")  and color in skin_colors and appended_count < 22)\
    if (region_name == "upper_body_clothing" and color == neck_color_name) \
      or (region_name == "lower_body_clothing" and color == background_color_name)\
      or (region_name == "mouth" and color in skin_colors)\
      or (region_name in ("neck_shadow")  and (color in skin_colors and not (color in criteria["colors"]) ))\
      or (region_name in ("eye")  and color in skin_colors and appended_count < 22 and eyelash_found == False)\
      or (region_name == "face" and color in regions["bangs"]["colors"]):# main_hair_color_name:
        region_indices = []
        appended_count = 0
        is_eye_first_not_continuous = True
        eye_above_not_continuous_count = 0
        #print ("reseted", "is_eye_first_not_continuous", is_eye_first_not_continuous) if debug_eye_centers and region_name == "eye" else None
        if region_name == "eye":
          eye_above_skin_exists = True

    #print ("region_name", region_name, "color", color, "appended_count", appended_count, "eyelash_color", eyelash_color) if debug_eye_centers else None
    if region_name == "eye" and color in eyelash_colors: # and eye_above_skin_exists
      eyelash_found = True
      #print ("eyelash_found", eyelash_found) if debug_eye_centers else None

    # 目のときは(上に肌色があって、まつ毛色も始まっていたら)何色であってもappendする
    if color in criteria["colors"] or (region_name == "eye" and eyelash_found):
      region_indices.append(i) # + start_index)  # Adjust index based on start_index
      appended_count += 1
      #if color == "not_continuous":
      #   print ("is_eye_first_not_continuous", is_eye_first_not_continuous) if debug_eye_centers else None
      if color == "not_continuous" and (is_eye_first_not_continuous or is_eye_initial_not_continuous):
        eye_above_not_continuous_count += 1
        #print ("eye_above_not_continuous_count", eye_above_not_continuous_count) if debug_eye_centers else None

  # 特定した領域の開始地点と終了地点と中心地点を計算する
  if region_indices:
      print ("region_indices", region_indices) if debug_eye_centers else None
      print ("eye_below_skin_count", eye_below_skin_count) if debug_eye_centers else None
      # 目は eye_below_skin_count の分だけ下に進んでるので、その分 region_indices を削る
      if region_name == "eye" and eye_below_skin_count > 0:
        region_indices = region_indices[:-eye_below_skin_count]

      print ("eye_above_not_continuous_count", eye_above_not_continuous_count) if debug_eye_centers else None
      # 目は eye_above_not_continuous_count の分だけ上に進んでるので、その分 region_indices を削る
      # 目の上端は eyelash_colors で始まる、にした時に邪魔になったのでコメントアウトした
      #if region_name == "eye" and eye_above_not_continuous_count > 0:
      #  region_indices = region_indices[eye_above_not_continuous_count:]

      # Adjust indices based on start_index
      region_indices = [index + start_index for index in region_indices]

      # Calculate the center point
      center = (x_column, (region_indices[0] + region_indices[-1]) // 2 * 1)  # Multiply by 2 since we took every 2nd pixel

      _region_start_end_center = {
        "start": region_indices[0] * 1,
        "end": region_indices[-1] * 1,
        "center": center
      }
  else:
      _region_start_end_center = {
        "start": 1,
        "end": 1,
        "center": (1, 1)
      }
  return _region_start_end_center

from itertools import groupby

# Group consecutive colors
#grouped_colors = [(color, sum(1 for _ in group)) for color, group in groupby(classified_colors)]
# Group colors and track the starting index
def group_colors(classified_colors):
  current_index = 0
  grouped_colors = []
  for color, group in groupby(classified_colors):
      length = sum(1 for _ in group)
      grouped_colors.append((current_index, color, length))
      current_index += length
  return grouped_colors

# 同じ言葉が連続して指定した回数以上連続していない場合、その言葉を "not_continuous" に変換する
# @param: classified_colors: list ex. ["yellow", "yellow", "yellow", "light_blue", "light_blue", "gray", "gray", "gray", "gray"]
# @param: threshold: int ex 3
# @return: classified_colors: list ex. ["yellow", "yellow", "yellow", "not_continuous", "not_continuous", "gray", "gray", "gray", "gray"]
def replace_not_continuous_color(classified_colors, threshold):
    check_color = classified_colors[0]
    check_count = 0
    start_index = 0

    for i, color in enumerate(classified_colors):
        #print ("color", color) if debug_eye_centers else None
        #print ("check_color", check_color) if debug_eye_centers else None
        if color == check_color:
            check_count += 1
        else:
            if check_count <= threshold:
                for j in range(start_index, i):
                    classified_colors[j] = "not_continuous"
            start_index = i
            check_color = color
            check_count = 1

    # Handle the case for the last sequence of colors in the list
    if check_count <= threshold:
        for j in range(start_index, len(classified_colors)):
            classified_colors[j] = "not_continuous"

    return classified_colors


# 目 4th: 3rd の left_eye, right_eye のそれぞれについて、肌の色が出ない範囲で領域をとって、その中心を目の位置とする
top_eyes = [left_eye, right_eye]
eye_results = {}

for i, eye in enumerate(top_eyes):
  classified_colors_eye = classify_colors(get_column_data(img, eye[0]))
  #print ("classified_colors_left_eye", i, classified_colors_eye) if debug_eye_centers else None
  classified_colors_eye = replace_not_continuous_color(classified_colors_eye, 3) #6)
  #classified_column_eye = np.array([colors[color] for color in classified_colors_eye]).reshape(-1, 1, 3)
  color_groups_eye = get_continuous_color_groups(classified_colors_eye)
  print ("groupsedeye", i, group_colors(classified_colors_eye)) if debug_eye_centers else None
  y_start_prop = (eye[1] - 50) / img.size[1]
  y_end_prop = (eye[1] + 50) / img.size[1]
  eye_region = {"colors": ["light_blue"], "range": (y_start_prop, y_end_prop)}
  _region_start_end_center = region_start_end_center("eye", eye_region, classified_colors_eye, eye[0])
  print ("これが4th目_region_start_end_center", i, _region_start_end_center)
  eye_results['eye_' + str(i)] = {"start": _region_start_end_center["start"], "end": _region_start_end_center["end"], "center": _region_start_end_center["center"]}

#top_eyes = find_points_with_color(image_rgb, centers, lower_purple9, upper_purple9)
top_eyes = {eye_results['eye_0']['center'], eye_results['eye_1']['center']}


# Y座標の平均を整数で計算
#average_y = int(sum(y for _, y in top_eyes) / len(top_eyes))
# 新しい座標リストを生成。X座標はそのままで、Y座標を平均値にする。
#top_eyes = [(x, average_y) for x, _ in top_eyes]

print ("top_eyes")
print (top_eyes) # ここでやっと目の位置が確定した

""" # 使わなくなった　2023/11/03 21:54
def closest_y_points(coordinates):
    # Sort the coordinates based on Y values
    sorted_coordinates = sorted(coordinates, key=lambda x: x[1])

    # Compute the difference in Y values for each adjacent pair
    y_diffs = [(sorted_coordinates[i], sorted_coordinates[i+1]) for i in range(len(sorted_coordinates)-1)]
    y_diff_values = [abs(pair[1][1] - pair[0][1]) for pair in y_diffs]

    # Find the pair with the smallest difference
    min_diff_index = y_diff_values.index(min(y_diff_values))
    return y_diffs[min_diff_index]
"""

centers = list(top_eyes) #closest_y_points(centers)
#print ("1-C: 両目の中心3(Y軸が近い2点にした)")
print ("1-C: 両目の中心3(4th)")
print (centers)
eye_centers = centers

if debug_eye_centers:
  plot_centers(image_rgb, centers, "1-C-b: combined eye centers(3rd)")
# Plot the centers of gravity on the original image
"""
image_with_centers = image_rgb.copy()
for center in centers:
    cv2.circle(image_with_centers, center, 10, (255, 0, 0), -1)  # Draw a blue circle at each center

plt.imshow(image_with_centers)
plt.axis('off')
plt.show() # ここをやると両目の位置が表示される
"""
x_column = int((centers[0][0] + centers[1][0]) / 2)
eye_y = int((centers[0][1] + centers[1][1]) / 2)
print ("1-C: 両目の中心の平均")
print (x_column)



column_data = get_column_data(img, x_column)

print ("np.array(img)")
# ↑ 目の位置の話はここまで

### 2 色分類関数を作る

# Test the function
test_pixel = (160, 220, 238)  # Should be close to light_blue
print (classify_color_by_rgb(test_pixel))


### 3 対象のX座標の色を上からグループ化していく
# (対象のX座標の色は column_data[y座標-1] に入っている)
# Classify every two pixels from the top along the Y-axis

classified_colors = classify_colors(column_data)
# Resize for visualization
classified_column = np.array([colors[color] for color in classified_colors]).reshape(-1, 1, 3)



# Get the continuous color groups
color_groups = get_continuous_color_groups(classified_colors)

def plot_classified_column(classified_column, color_groups, classified_colors, x_column):
  # todo: not_continuous の後で図示すべし
  # Plot the classified column
  plt.figure(figsize=(5, 10), num="Classified Column"+str(x_column))
  plt.imshow(classified_column, aspect=0.01)
  plt.xlim(0, 1) # x軸の表示範囲を設定
  # Add the color names at the center of continuous regions
  # Add the color names at the center of continuous regions and dashed lines at color boundaries
  for start, end in color_groups:
    center_idx = (start + end) / 2
    plt.text(1.1, center_idx, classified_colors[start], va='center')

    # Draw dashed black line at the boundary if it's not the last group
    if end != len(classified_colors) - 1:
        plt.axhline(y=end+0.5, color='black', linestyle='--')
  """
  for start, end in color_groups:
    center_idx = (start + end) / 2
    plt.text(1.1, center_idx, classified_colors[start], va='center')
  """
  plt.title("Classified Column at X=" + str(x_column))
  plt.axis('off')
  plt.show(block=False)

if 1: # 分類した色の図示はここ
  plot_classified_column(classified_column, color_groups, classified_colors, x_column)

# Return the classified colors for further steps
classified_colors[:10]  # Display the first 10 classified colors for verification
print (classified_colors[:10])


classified_colors = replace_not_continuous_color(classified_colors, 6)
print ("連続してないところをnot_continuousに変換")
print (classified_colors[:10])


grouped_colors = group_colors(classified_colors)

print("これがgroupedされた後")
print(grouped_colors)

# つまり、 L453で column_data = get_column_data(img, x_column) として得た
# column_data を L487 で classified_colors = classify_colors(column_data) にして
##### それを L512で color_groups = get_continuous_color_groups(classified_colors) と group にした…のは図示のためなので関係なくて
# その classified_colors を L570 で classified_colors = replace_not_continuous_color(classified_colors, 6) して 非連続をつけて
# L588 で grouped_colors = group_colors(classified_colors) して完成
# でも、実際には L570 の classified_colors が多用されてる。とはいえ、 grouped_colors も、まぁ、わかりやすくなってるといえばなってる


### 4 分類してグループ化した色を元に、領域を特定する
# Initialize the results
results = {}

# Identify the regions
# 基本的には、各regionについて、start_percentage から end_percentage までの範囲で、
#   最初に region の色が出てきたところを start として、最後に region の色が出てきたところを end としている
for region_name, criteria in regions.items():
    _region_start_end_center = region_start_end_center(region_name, criteria, classified_colors, x_column)

    # Store the results
    results[region_name] = _region_start_end_center


print (results)

# ↑ここまでで、各領域を特定できた。
# ↓ここからは、左右の両端を特定していく

# 前後のいずれにも shining_light (などの光の色) がない肌色(などの指定した色)のy座標を判定する関数
# 肌色がなかったら False を返す
# @param target_colors: list ex ["medium_skin", "medium_dark_skin"]
def target_color_y(grouped_colors, target_colors, y_start_prop, y_end_prop, avoid_next2_light = True):
    for i, color in enumerate(grouped_colors):
        if i == 0:
            continue
        image_height = grouped_colors[-1][0] + grouped_colors[-1][2]
        if color[0] / image_height < y_start_prop or color[0] / image_height > y_end_prop:
            continue
        # 色名が対象の色を示すものであるかどうかを判定
        if color[1] in target_colors:  #color[1] == skin_color:  # この名前は適切に変更することができます
            #print (color, skin_colors)
            #print (skin_colors)
            # 前の要素と次の要素の色名を取得
            prev_color = grouped_colors[i-1][1] if i-1 >= 0 else None
            next_color = grouped_colors[i+1][1] if i+1 < len(grouped_colors) else None
            next2_color = grouped_colors[i+2][1] if i+2 < len(grouped_colors) else None

            # 前後の要素の色名が 'shining_light' でないかどうかを判定
            # todo: 2つ先は、1つ先が not_continuous の時だけでいいかも
            if (prev_color != 'shining_light' and next_color != 'shining_light' and (next2_color != 'dull_light' and avoid_next2_light == True)):
              #and (prev_color != 'not_continuous' or next_color != 'not_continuous'):
                return color[0]
    return False

### 5: leftmost, rightmost の肌の位置を出す

# 5-1: 画像をX座標1の方から順に、Y座標を上から走査していって上記の色分類を行って、肌色が出るまでX座標を++していく

# 関数 first_color_index は、画像の特定の範囲内で、与えられた色（target_colors）を検出するための座標を探索する
# todo: きっと将来的に方向指定になる
# …と思ったけど、 interval をマイナスにするだけでよかった
# @param start（開始座標）、interval（探索の間隔）、target_colors（検出したい色）、そして y_start_prop と y_end_prop（探索を行うY座標の比率）
# @return 目的の色が検出された場合、そのX座標（i）と、色が見つかったY座標（_skin_y）のタプルを返します。
# todo: limit は インターフェースをあわせるためで未使用
def first_color_index(start, interval, target_colors, y_start_prop, y_end_prop, avoid_next2_light = True, limit = 1000):
  # todo: ちゃんと画像の端座標を受け取るようにする
  is_debug = False #True
  end = 1199 if start < 700 else 0 # ここバグの温床
  start = 1199 if start > 1200 else start
  for i in range(start, end, interval):
    column_data = np.array(img)[:, i, :3]  # Extract only RGB, ignore alpha if present
    classified_colors = [classify_color_by_rgb(column_data[i]) for i in range(0, column_data.shape[0], 1)]
    classified_colors = replace_not_continuous_color(classified_colors, 6)
    print(i) if is_debug else None
    grouped_colors = group_colors(classified_colors)
    if abs(interval) >= 100 and is_debug:
      print (i, grouped_colors)
    _skin_y = False
    _skin_y = target_color_y(grouped_colors, target_colors, y_start_prop, y_end_prop, avoid_next2_light)
    if _skin_y:
      print (i, grouped_colors) if is_debug else None
      print (i, _skin_y) if is_debug else None
      return (i, _skin_y)

def center_color_index(start, interval, target_colors, y_start_prop, y_end_prop, avoid_next2_light = True):
   return None

def check_skin_color_presence(grouped_colors, skin_colors, threshold=30):
    # Initialize a variable to keep track of total skin color pixels
    total_skin_pixels = 0

    # Iterate through the grouped_colors list
    for color_group in grouped_colors:
        # Check if the color name in the color_group is in the skin_colors list
        if color_group[1] in skin_colors:
            # Add the number of pixels of this skin color to the total
            total_skin_pixels += color_group[2]

    # Check if total skin color pixels is 30 or more
    return total_skin_pixels >= threshold

# @param limit: 走査を何回するか、つまり step数
def most_frequent_color_index(start, interval, target_colors, y_start_prop, y_end_prop, avoid_next2_light = True, limit = 50):
    # Initialization
    is_debug = True
    max_count = 0
    max_count_x = None

    # Determine the direction of the scan based on the interval
    """
    if interval > 0:
        end = min(start + limit, img.shape[1])  # Ensure we don't go past the image width
    else:
        end = max(start - limit, 0)  # Ensure we don't go past the image start
    """

    image_x_end = 1199 #if start < 700 else 0 # ここバグの温床
    # Calculate the number of steps to take
    steps = limit

    # Determine the direction of the scan based on the interval
    direction = 1 if interval > 0 else -1

    # Calculate the actual end considering the limit
    end = start + (steps * interval)

    # Debug print
    print("start", start, "end", end, "interval", interval, "steps", steps) if is_debug else None

    # Scan within the limit
    for _ in range(steps):
        # 開始地点を元の中心地点にしたいので interval を足した (呼び出し元でintervalを引いてるので)
        # 呼び元で 10間隔のときは 10しか引いてないけど、 1間隔の時も10引いてるのでその分補正する
        fix_width = interval if interval >= 10 else interval * 10
        i = start + (fix_width * direction) + (_ * interval * direction)
        # Ensure we don't go out of the image bounds
        if i < 0 or i >= image_x_end:
            break
        column_data = np.array(img)[:, i, :3]  # Extract only RGB, ignore alpha if present
        classified_colors = [classify_color_by_rgb(column_data[j]) for j in range(int(y_start_prop * len(column_data)), int(y_end_prop * len(column_data)))]
        classified_colors = replace_not_continuous_color(classified_colors, 6)
        #print(i) if is_debug else None

        # 今のところ、口の検出だけでこの関数は使われている
        # ので、 "most_frequent_color_index" に 肌色が要るよ という条件を足してしまう
        grouped_colors = group_colors(classified_colors)
        is_skin_present = check_skin_color_presence(grouped_colors, skin_colors, 30)


        # Count the occurrences of the target color within the specified y-axis range
        #print (classified_colors) if is_debug else None
        target_color_count = sum(1 for color in classified_colors if color in target_colors)
        # ↓結構使うprint
        #print (i, "grouped_colors", grouped_colors, "target_color_count", target_color_count) #if abs(interval) >= 100 and is_debug


        if max_count == 0:
            count_at_start_point = target_color_count
            max_count = target_color_count
            max_count_x = i
        # Update the maximum count and the corresponding x-coordinate if necessary
        # 開始地点より大きく上回らない限り開始地点を使う
        # できるだけ開始地点に近いものがよいので 1.1倍を超えたら更新にした
        if target_color_count > (max_count * 1.1) and is_skin_present and (target_color_count > count_at_start_point * 1.25):
            max_count = target_color_count
            max_count_x = i

    # Return the x-coordinate with the most frequent occurrence of the target color
    print("target_colors", target_colors) if is_debug else None
    print("return", max_count_x, max_count) if is_debug else None
    return max_count_x, max_count if max_count_x is not None else (None, 0)

# 端を(色分類利用で)検出する関数
#    start: 探索を開始する位置（インデックス）。
#    direction: 探索の方向。1 は正の方向、-1 は負の方向を意味します。
#    target_colors: 検出する対象の色のリストか範囲。
#    y_start_prop と y_end_prop: y軸に沿った検出範囲の開始と終了の比率。
# @param int direction 1 or -1
def detect_edge_by_color(start=1, direction=1, target_colors=skin_colors, y_start_prop=0.0, y_end_prop=1.0, start_interval=100, use_function="first_color_index"):
    if use_function == "most_frequent_color_index":
        color_index_function = most_frequent_color_index
    else:
        color_index_function = first_color_index

    if start_interval != 10:
        leftmost_skin_color_index100 = color_index_function(start, start_interval * direction, target_colors, y_start_prop, y_end_prop, True)
        if leftmost_skin_color_index100 is None:
            print("leftmost_skin_color_index100 is None")
            return
    else:
        leftmost_skin_color_index100 = (start, 9999)
        print(leftmost_skin_color_index100)

    print("leftmost_skin_color_index100", leftmost_skin_color_index100)
    leftmost_skin_color_index10 = color_index_function(leftmost_skin_color_index100[0] - (start_interval * direction), 10 * direction, target_colors, y_start_prop, y_end_prop, True, 20)
    print("leftmost_skin_color_index10", leftmost_skin_color_index10)

    if leftmost_skin_color_index10 is None:
      return None

    if use_function == "first_color_index" and color_index_function(leftmost_skin_color_index10[0] + (10 * direction), 10 * direction, target_colors, y_start_prop, y_end_prop) != leftmost_skin_color_index10[0] + (10 * direction):
        leftmost_skin_color_index10 = color_index_function(leftmost_skin_color_index10[0] + (10 * direction), 10 * direction, target_colors, y_start_prop, y_end_prop, True)

    leftmost_skin_color_index = color_index_function(leftmost_skin_color_index10[0] - (10 * direction), 1 * direction, target_colors, y_start_prop, y_end_prop, True, 20)

    if use_function == "first_color_index":
      check_right = color_index_function(leftmost_skin_color_index[0] + (10 * direction), 10 * direction, target_colors, y_start_prop, y_end_prop)
      if check_right[0] != leftmost_skin_color_index[0] + (10 * direction) or abs(check_right[1] - leftmost_skin_color_index[1]) > 100:
        leftmost_skin_color_index = color_index_function(leftmost_skin_color_index[0] + (3 * direction), 1 * direction, target_colors, y_start_prop, y_end_prop)

    print("5-1: leftmost_skin_color_index")
    print(leftmost_skin_color_index)
    return leftmost_skin_color_index

"""
def detect_edge_by_color(start = 1, direction = 1, target_colors = skin_colors, y_start_prop = 0.0, y_end_prop = 1.0, start_interval = 100):
  if start_interval == 100:
    leftmost_skin_color_index100 = first_color_index(start, 100 * direction, target_colors, y_start_prop, y_end_prop, True)
    # 右の方も肌色でないと肌色として認めない
    if leftmost_skin_color_index100 is None:
      print ("leftmost_skin_color_index100 is None")
      return
  else:
     leftmost_skin_color_index100 = (start, 9999)
     print (leftmost_skin_color_index100)

  print ("leftmost_skin_color_index100", leftmost_skin_color_index100)
  leftmost_skin_color_index10 = first_color_index(leftmost_skin_color_index100[0] - (100 * direction), 10 * direction, target_colors, y_start_prop, y_end_prop, True)
  print ("leftmost_skin_color_index10", leftmost_skin_color_index10)
  #a = leftmost_skin_color_index10[0]

  if first_color_index(leftmost_skin_color_index10[0] + (10 * direction ), 10 * direction, target_colors, y_start_prop, y_end_prop) != leftmost_skin_color_index10[0] + (10 * direction):
    leftmost_skin_color_index10 = first_color_index(leftmost_skin_color_index10[0] + (10 * direction), 10 * direction, target_colors, y_start_prop, y_end_prop, True)

  leftmost_skin_color_index = first_color_index(leftmost_skin_color_index10[0] - (10 * direction), 1 * direction, target_colors, y_start_prop, y_end_prop)
  # X座標が10大きいところが同じ色でないかY座標が100以上離れていたら、X方向に3先からさらに探索する (周囲に目的の色が連続しているかどうかの判定)
  check_right = first_color_index(leftmost_skin_color_index[0] + (10 * direction), 10 * direction, target_colors, y_start_prop, y_end_prop)
  if check_right[0] != leftmost_skin_color_index[0] + (10 * direction) or abs(check_right[1] - leftmost_skin_color_index[1]) > 100:
    leftmost_skin_color_index = first_color_index(leftmost_skin_color_index[0] + (3 * direction), 1 * direction, target_colors, y_start_prop, y_end_prop)

  print ("5-1: leftmost_skin_color_index")
  print (leftmost_skin_color_index)
  return leftmost_skin_color_index
"""

# 肌色の最も左と右を出す
leftmost_skin_color_index = detect_edge_by_color(1, 1, edge_skin_colors)
#sys.exit(0)
rightmost_skin_color_index = detect_edge_by_color(img.size[0] - 1, -1, skin_colors)

# 今のところ、口の検出だけでこれや "most_frequent_color_index" は使われている
# (ので、 "most_frequent_color_index" に 肌色が要るよ という条件を足してしまう)
def detect_center_by_color(start=1, direction=1, target_colors=skin_colors, y_start_prop=0.0, y_end_prop=1.0, start_interval=100):
   return detect_edge_by_color(start, direction, target_colors, y_start_prop, y_end_prop, start_interval, "most_frequent_color_index")

# 5-2: 口のあるX座標を特定し、口の領域も特定する
# 5-2-a: 口のあるX座標を特定する
# x_column(両目のX座標の平均由来) から+方向と-方向にdetect_edge_by_colorして、x_column に近い方 …としようと思ったが、
# やっぱり、一番target_colorが多いX座標を探そうとなった
print ("5-2-a: 口のあるX座標を特定する")
image_height = grouped_colors[-1][0] + grouped_colors[-1][2]
plus_mouth_x = detect_center_by_color(x_column, 1, regions['mouth']['colors'], (results["face"]['start'] / image_height), (results["face"]['end'] / image_height), 10)

print ("plus_mouth_x", x_column, (results['face']['start'] / image_height), (results["face"]['end'] / image_height))
print (grouped_colors[-1][0], grouped_colors[-1][2], image_height, results['face']['start'])
print (regions['mouth']['colors'])
print (results)
print ("plus_mouth_x", plus_mouth_x)
# todo: 本来は minus 方向についても走査しないといけない (そういう入力画像が来たらやる)

# 5-2-b: 口の領域を特定する
# ここでは、 plus_mouse_x について、前半にやってる処理をして、 results['mouth'] に格納する
classified_colors_mouth = classify_colors(get_column_data(img, plus_mouth_x[0]))
classified_column_mouth = np.array([colors[color] for color in classified_colors_mouth]).reshape(-1, 1, 3)
color_groups_mouth = get_continuous_color_groups(classified_colors_mouth)
plot_classified_column(classified_column_mouth, color_groups_mouth, classified_colors_mouth, plus_mouth_x[0])

_region_start_end_center = region_start_end_center("mouth", regions["mouth"], classified_colors_mouth, plus_mouth_x[0])
print ("これが5-2-bの_region_start_end_center", _region_start_end_center)
results["mouth"] = _region_start_end_center

print(regions)
print ("ここがmouth後のresults", results)


### 6: 背景色青の横にある髪色青みたいな色分類では抽出できない点を
#      edgeを出して取得する

# 6-1: edge を出す
# 必要なライブラリを再度インポート
#import cv2
#import matplotlib.pyplot as plt

# 画像を再度読み込む
image2 = cv2.imread(image_path, cv2.IMREAD_COLOR)
image2 = cv2.cvtColor(image2, cv2.COLOR_BGR2RGB)

# グレースケールに変換
gray = cv2.cvtColor(image2, cv2.COLOR_RGB2GRAY)

# Canny エッジ検出器を使用してエッジを取得
edges = cv2.Canny(gray, 50, 150)

# 結果の画像を表示
plt.figure(figsize=(10, 9), num="Edge")
plt.imshow(edges, cmap='gray')
plt.axis('off')
plt.show(block=False)


# 6-2: edge から、髪の左端を出す
# エッジ画像の中心のx座標を取得

# @param direction: int 1 or -1 (1: 右方向, -1: 左方向)
def get_edge(edges, y_target, x_radius, y_radius, center_x, direction = -1, max_x = 99999):
  # Starting from the center, scan towards the left on the x-axis at Y=269
  # todo: 右端を画面端にしたい
  leftmost_x_valid = None
  for x in range(center_x, -1 if direction == -1 else 1600, direction):
      vertical_edge_found = False
      horizontal_edge_found = False
      #print (edges.shape)

      # Check within a 30-pixel radius vertically
      for d in range(-y_radius, y_radius + 1):
          # Check vertically
          y = y_target + d
          if 0 <= y < edges.shape[0] and edges[y, x] > 0:
              vertical_edge_found = True

      # Check within a 30-pixel radius horizontally
      for d in range(-x_radius, x_radius + 1):
          # Check horizontally
          if 0 <= x + d < edges.shape[1] and edges[y_target, x + d] > 0:
              horizontal_edge_found = True

      # If either vertical or horizontal edge is found, update the x-coordinate
      if horizontal_edge_found or vertical_edge_found:
          leftmost_x_valid = x
      else:
          # If we find a point that doesn't satisfy the condition after finding a valid point,
          # we stop the search.
          if leftmost_x_valid is not None and leftmost_x_valid <= max_x:
              break

  valid_edge_point_with_or_condition = (leftmost_x_valid - (x_radius * direction), y_target) if leftmost_x_valid is not None else None
  print("leftmost_hair_eyeline", valid_edge_point_with_or_condition)
  return valid_edge_point_with_or_condition

center_x = edges.shape[1] // 2
y_target = eye_y
radius = 30 # 30
y_radius = 5
leftmost_hair_color_index = get_edge(edges, y_target, radius, y_radius, center_x, -1)
radius = 30
rightmost_hair_color_index = get_edge(edges, y_target, radius, radius, center_x, 1)

### 6-2: bangsのcenterの左右のedgeを出す
#print (results)
y_target = results["bangs"]["center"][1] + adjust_dogear_y
radius = 30
leftmost_dogear_index = get_edge(edges, y_target, radius, radius, center_x, -1)
rightmost_dogear_index = get_edge(edges, y_target, radius, radius, center_x, 1)

### 6-3: neck_shadowのcenterの左右のedgeを出す

# 肌色の最も左と右を出す
y_start_prop = (results["neck_shadow"]["center"][1] - 50) / image_height
y_end_prop = (results["neck_shadow"]["center"][1] + 50) / image_height

print ("y_start_prop", y_start_prop, "y_end_prop", y_end_prop)
leftmost_neck_shadow_index = detect_edge_by_color(1, 1, left_shoulder_colors, y_start_prop, y_end_prop, 100)#, "most_frequent_color_index")
#sys.exit(0)
rightmost_neck_shadow_index = detect_edge_by_color(img.size[0] - 1, -1, right_shoulder_colors, y_start_prop, y_end_prop, 50)#, "most_frequent_color_index")

### 10: 結果を図に表示する
### 10-1
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import cv2
import numpy as np

# Load the image
image = cv2.imread(image_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)  # Convert from BGR to RGB

# Define the regions and their properties
# results と eye_results をマージする
regions = {**results, **eye_results}
print (regions)

### 10-2

def label(image, center, name):
   cv2.circle(image, center, 10, (255, 0, 0), -1)  # Draw a blue circle at each center
   if center is not None:
     ax.text(center[0] + 15, center[1], name, color='black')#, backgroundcolor='gray')

# Re-plot with center point color matching the rectangle color
fig, ax = plt.subplots(figsize=(10, 10))
for i, center in enumerate(eye_centers):
   # todo: もしかしたら左右が不定かも。
   label(image, center, "righteye" if i == 1 else "lefteye")

label(image, leftmost_skin_color_index, "leftmost_skin")
label(image, rightmost_skin_color_index, "rightmost_skin")
label(image, leftmost_hair_color_index, "leftmost_hair")
label(image, rightmost_hair_color_index, "rightmost_hair")
label(image, leftmost_dogear_index, "leftmost_dogear")
label(image, rightmost_dogear_index, "rightmost_dogear")
label(image, leftmost_neck_shadow_index, "leftmost_shoulder")
label(image, rightmost_neck_shadow_index, "rightmost_shoulder")

ax.imshow(image)

# Plot each region with coordinates, different colors, and matching center point color
for region, properties in regions.items():
    # Draw rectangle to indicate the region with specified color
    rect = patches.Rectangle((properties['center'][0] - 5, properties['start']),
                             10, properties['end'] - properties['start'],
                             linewidth=1, edgecolor=region_colors[region], facecolor='none')
    ax.add_patch(rect)
    # Draw center point with matching color
    ax.scatter(properties['center'][0], properties['center'][1], color=region_colors[region], s=50)
    # Label the region name
    ax.text(properties['center'][0] + 15, properties['center'][1], region, color='white', backgroundcolor='black')
    # Label the start, center, and end coordinates
    ax.text(properties['center'][0] - 70, properties['start'], f'start: {properties["start"]}', color=region_colors[region])
    ax.text(properties['center'][0] - 70, properties['center'][1], f'center: {properties["center"][1]}', color=region_colors[region])
    ax.text(properties['center'][0] - 70, properties['end'], f'end: {properties["end"]}', color=region_colors[region])

# Show the image with plotted regions, their coordinates, different rectangle colors, and matching center point colors
ax.axis('off')
plt.tight_layout()
plt.show()
```


----

(↓まとめコーナー↓)
# ツールの特色

## ChatGPT

- 1.5ヶ月前のチャットでも、遡って閲覧できる(履歴機能)
    - でも、会話が途中までしか表示されなかったり、画像がところどころしか表示されなかったりするような気がする…
- チャットターン進むとエラー癖つくかも
- メモリ少なめ

### ChatGPT上で動かせるプログラムの制限

- プログラミング言語はPythonのみ
- メジャーなライブラリのみ

## DeepL

- 列挙系は結構たまに全然合ってなかったりするので注意

```
「オレンジ,明るめの水色,明るい肌色,暗い肌色,白の影色としての灰色,白,強いやや紫みの青」
↓
1: "orange", "light water", ★"light blue", ★"dark blue". ★Orange, ★light blue,
light skin tone, dark skin tone, gray as a shadow color of white, white,
strong slightly purplish blue.
(余分なものが4つ入っている)
```

- 手順番号は 1. ではなく、 1: と原文(日本語)で書くと、ちゃんと訳してくれる (1.だと文末と勘違いする)

## VSCode

- Python の画像を表示させて、コンソールで Ctrl-Cする場合、画像ウィンドウ上で <ins>マウスカーソルが動かさないと</ins>実行終了しない

## Github Copilot

- github copilot も<ins>コメントを書くと</ins>、かなりその内容を書いてもらえる
- ただ、<ins>少し複雑な処理とかだとできない</ins> (2023/12初旬現在)
- 今回やった画像解析のような複雑なことは、やはり、実際にプログラムを(LLMが)実行して、間違ってたらエラー文言を元に修正までしてくれる <ins>ChatGPT</ins> の方がだいぶ<ins>書く力がある</ins>と思う
- <ins>とはいえ、書きたいことを Tab で一発で出せたり、引数に何をいれるかとかは完全に頼れるので、 github copilot も無いと厳しい</ins>
    - プログラムの他にそっくりな処理がある場合も完璧に書ける

# 所要時間

- 10/12 (2023)
    - ChatGPTの Advanced Data Analysis に触れ始める (勉強を始める)
- 10/15 22:56
    - ADAを使っての<ins>ゆいもっぷ画像特徴点解析を始める</ins>
- 10/16 23:16
    - 色相が大きく変わったところで領域を分割するという方向性で領域分割ができている (<ins>1日経過で到達</ins>)
    - (1日40分くらい？)
- 10/19
    - 「指示で指定した7～9色に分類して指定した領域に割当」に到達 (4日経過で到達)
- 10/23
    - 「(紫をとって)目を取る」に到達 (8日経過でとう)
- 11/23
    - 原画に対しての適用 & 機能拡張完了 (1ヶ月かかってる)
    - (1日30分くらい？)
- 12/3 (<ins>息子誕生</ins>👶🎉)
    - 記事をほぼ書き上げる(10日くらい？）
    - (1日40分くらい + 最終日5時間？)
    - 51500字 (文庫本100ページ相当)

# 調べていて知ったこと

(ChatGPTへの聞き方)
- https://chat-gpt.school/what-is-chatgpt-prompt/
    - > 「不明点があれば質問してください」と書く (と良い回答が得られる)
    - 大事そう

(ChatGPTのプラグイン)
- https://note.com/hantani/n/n23b04e81f399 (2023/6)
    - SceneXplain プラグイン (画像説明) (ChatGPTにもプラグインがある)
        - (いまではデフォルトでいけるのかも？)
- https://note.com/hantani/n/n4bf380124b0b
    - Pixellow プラグイン  (画像説明)
        - (同じく、いまではデフォルトでいけるのかも？)

# やるにあたってのポイント

- ChatGPTなどからの応答は別テキストファイルに<ins>コピペして保存</ins>していっているよ
- ブラウザでChat GPT4をリロードしないと画像アップロードが成功しなくなったことがあった(2023/10/15 23:05)

----

# 感想

- **ゆいもっぷちゃん**の<ins>特徴点が自動</ins>で出て、(動きを全フレーム分自分で作らなくて楽ちんに) 原画指定だけでの、カメラモーション自在の<ins>ゆいもっぷアニメ作りが捗りそう</ins>でよかった
- 他にも色々欲しい<ins>ソフトを作れそう</ins>な感触が得られて良かった
    - ディレクトリ内のBingChatのスクショ全てを<ins>OCR</ins>して、ダブってるテキストは重複をなくして、テキストファイルに出力するソフトなど
- それでは来年も<ins>ゆいかわ！</ins> (明けましてこんばんわんわんワルダフル！)

![yyy220230808230502.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/f10249ea-6238-8cb4-f310-1ab739e5c2bf.png)
(ロングヘアーの方がアニメ映えする) (解釈自由度高)

![www68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f37373432332f61313961393434642d353931342d353063332d386232642d3530616562343761656261612e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/ec079274-bf0e-d6d1-83e9-8878a15120c7.png)

![www68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f37373432332f32623032373633332d656464352d346266312d653337662d3238666635643762383237612e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77423/6b8576ac-3ca9-a8d0-8931-b2e208677f1d.png)

- マイティスマイル(ゆいもっぷさんがセンターとして所属している宇宙組織) のBingチャットによる説明

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Bingチャットも観ている宇宙戦艦マイティスマイル！（隔週土曜22:00-）　マーシーさんだけが宇宙服のヘルメットを被ってる事を知っててすごいですね <a href="https://t.co/GztX1qNYzl">pic.twitter.com/GztX1qNYzl</a></p>&mdash; あお (@ao_love_yuimop) <a href="https://twitter.com/ao_love_yuimop/status/1642533269513310209?ref_src=twsrc%5Etfw">April 2, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>