Google Cloud Vision API の登録

Google Cloud Vision API の登録

https://cloud.google.com/vision/?hl=ja&_gl=1*1yrkeuh*_ga*NzQ0NTYyMjM5LjE2ODQxODU5MzA.*_ga_WH2QY8WWF5*MTcyMTE1MzQ3OS4yLjEuMTcyMTE1MzYyNS42LjAuMA..
でチュートリアル

料金については
https://cloud.google.com/vision/pricing?hl=ja#prices
を参考に
基本的に無料枠でできそう

プロジェクトを選択するのだが
Gmailと同じプロジェクトを選択する

Credentials.json
の中身を見ればOK

とりあえずデフォルトがこれなので
そのまま進める

APIとサービスを有効にする
をクリックし
cloud vision api
で検索

これでAPIを有効にする

多分認証のJSONファイルなので
権限などを増やしても変わらないはず

もしダメなら再度作成するだけ

とりあえずこれで進めてみる

Google colabでも実験できるのだが
Google drive へアップするときに
アップロード先のディレクトリがpublicに共有されていないことを確認すること
不安なら都度アップロードすればOK

とりあえずローカルで動かすのだが
Google Cloud SDK
が必要と chatgpt では言っている

本当か調べてみる

https://nikkie-ftnext.hatenablog.com/entry/ocr-with-google-vision-api-python-first-step
2024-01-03
GoogleのVision APIをPythonから呼び出して、画像内のテキストを検出する
に載っていた

サンプルコードを動かす前に準備が必要です。
「Google Cloud プロジェクトと認証のセットアップ」
* 大前提:Google Cloudのアカウント
* Google Cloudのプロジェクト1つ
* すでにあるものを選ぶ
* または、新規作成
* 余談:削除など管理したい場合は https://console.cloud.google.com/cloud-resource-manager から
* プロジェクトで課金が有効になっているか確認する
* 案内された別ドキュメントに従いました
* 直リンクは https://console.cloud.google.com/billing/linkedaccount?hl=ja&project=<選んだプロジェクトID> っぽいです
* 課金を有効にしますが、今回のサンプルコードの範囲内では無料で使える認識です
* 最初の 1,000 ユニット/月は無料

まではOK

次に
gcloudコマンドが使えるようにする設定が必要

https://cloud.google.com/docs/authentication/provide-credentials-adc?hl=ja#local-dev
の中の
gcloud CLI をインストールして初期化します。
をクリック

gcloud CLI をインストールする
https://cloud.google.com/sdk/docs/install?hl=ja
の中で
各種OSごとの
Google Cloud CLI 
のパッケージがダウンロードできるので
ここからダウンロード

私の場合 Macだけど
M1MacBookAir なので
macOS 64 ビット
(ARM64, Apple M1 silicon)
の方を選ぶ

google-cloud-cli-477.0.0-darwin-arm.tar.gz

をダウンロードしたら
展開して
ホームディレクトリに移動させる

Macの場合
圧縮ファイルをダブルクリックで展開できるので

 mv ~/Downloads/google-cloud-sdk .

でホームディレクトリ直下に移動

./google-cloud-sdk/install.sh

を実行したらエラーになるので

Reason: tried: '/opt/homebrew/opt/openssl@1.1/lib/libssl.1.1.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/openssl@1.1/lib/libssl.1.1.dylib' (no such file), '/opt/homebrew/opt/openssl@1.1/lib/libssl.1.1.dylib' (no such file), '/usr/local/lib/libssl.1.1.dylib' (no such file), '/usr/lib/libssl.1.1.dylib' (no such file, not in dyld cache)

で検索

Library not loaded: libssl.1.1.dylib (LoadError) 発生時の解決方法
によれば
OpenSSL@1.1のインストール
でOK

 brew install openssl@1.1

でインストールできるらしい

既にサポート終了しているはずと思い検索
「OpenSSL」にアップデート – 「同1.1.1」はサポート終了
https://www.security-next.com/149614

最初にchatgptでエラーの解決を調べた時にこの答えが出たけど
以前 YouTube live を再生するコードの時に
昔のライブラリ情報のインストールを提示され
ドツボにハマったことがあったので
念の為検索した

もうちょっと調べたら

PythonがOpenSSL周りのエラーで動かない

2024年05月24日
でこっちも同じ感じ

ただ

brew install openssl@1.1

を実行しても

brew install openssl@1.1

==> Auto-updating Homebrew...
Adjust how often this is run with HOMEBREW_AUTO_UPDATE_SECS or disable with
HOMEBREW_NO_AUTO_UPDATE. Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
==> Auto-updated Homebrew!
Updated 2 taps (homebrew/core and homebrew/cask).
==> New Formulae
libgit2@1.7                              tdb
==> New Casks
avbeam                     jagex                      twingate
duplicateaudiofinder       sq-mixpad

You have 23 outdated formulae installed.

Error: Cannot install under Rosetta 2 in ARM default prefix (/opt/homebrew)!
To rerun under ARM use:
    arch -arm64 brew install ...
To install under x86_64, install Homebrew into /usr/local.

となるので

arch -arm64 brew install openssl@1.1

を実行

Warning: openssl@1.1 has been deprecated because it is not supported upstream!
==> Downloading https://ghcr.io/v2/homebrew/core/openssl/1.1/manifests/1.1.1w
######################################################################### 100.0%
==> Fetching openssl@1.1
==> Downloading https://ghcr.io/v2/homebrew/core/openssl/1.1/blobs/sha256:38619f
######################################################################### 100.0%
==> Pouring openssl@1.1--1.1.1w.arm64_sonoma.bottle.tar.gz
==> Caveats
A CA file has been bootstrapped using certificates from the system
keychain. To add additional certificates, place .pem files in
  /opt/homebrew/etc/openssl@1.1/certs

and run
  /opt/homebrew/opt/openssl@1.1/bin/c_rehash

openssl@1.1 is keg-only, which means it was not symlinked into /opt/homebrew,
because this is an alternate version of another formula.

If you need to have openssl@1.1 first in your PATH, run:
  echo 'export PATH="/opt/homebrew/opt/openssl@1.1/bin:$PATH"' >> ~/.zshrc

For compilers to find openssl@1.1 you may need to set:
  export LDFLAGS="-L/opt/homebrew/opt/openssl@1.1/lib"
  export CPPFLAGS="-I/opt/homebrew/opt/openssl@1.1/include"

For pkg-config to find openssl@1.1 you may need to set:
  export PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@1.1/lib/pkgconfig"
==> Summary
🍺  /opt/homebrew/Cellar/openssl@1.1/1.1.1w: 8,102 files, 18MB
==> Running `brew cleanup openssl@1.1`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).

最後のメッセージが気になるので翻訳

openssl@1.1 は Keg 専用です。つまり、/opt/homebrew にシンボリックリンクされていません。
これは別の公式の代替バージョンであるためです。

PATH の最初に openssl@1.1 を含める必要がある場合は、次を実行します。
echo ‘export PATH=”/opt/homebrew/opt/openssl@1.1/bin:$PATH”‘ >> ~/.zshrc

コンパイラが openssl@1.1 を見つけるには、次の設定が必要になる場合があります。
エクスポート LDFLAGS=”-L/opt/homebrew/opt/openssl@1.1/lib”
エクスポート CPPFLAGS=”-I/opt/homebrew/opt/openssl@1.1/include”

pkg-config が openssl@1.1 を見つけるには、次の設定が必要になる場合があります。
import PKG_CONFIG_PATH=”/opt/homebrew/opt/openssl@1.1/lib/pkgconfig”
==> まとめ
🍺 /opt/homebrew/Cellar/openssl@1.1/1.1.1w: 8,102 ファイル、18MB
==> `brew cleanup openssl@1.1` を実行しています…
HOMEBREW_NO_INSTALL_CLEANUP を設定して、この動作を無効にします。
これらのヒントは HOMEBREW_NO_ENV_HINTS で非表示にします (「man brew」を参照)。

このため

export LDFLAGS="-L/opt/homebrew/opt/openssl@1.1/lib"
export CPPFLAGS="-I/opt/homebrew/opt/openssl@1.1/include"
export PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@1.1/lib/pkgconfig"

の後に

 ./google-cloud-sdk/install.sh

を実行すると

Welcome to the Google Cloud CLI!

To help improve the quality of this product, we collect anonymized usage data
and anonymized stacktraces when crashes are encountered; additional information
is available at <https://cloud.google.com/sdk/usage-statistics>. This data is
handled in accordance with our privacy policy
<https://cloud.google.com/terms/cloud-privacy-notice>. You may choose to opt in this
collection now (by choosing 'Y' at the below prompt), or at any time in the
future by running the following command:

    gcloud config set disable_usage_reporting false

Do you want to help improve the Google Cloud CLI (y/N)?  

となる
メッセージを検索し

gcloud CLI をインストールする
を参考に
これは
N
でよいらしい

ちなみに日本語訳は
Google Cloud CLI へようこそ。

この製品の品質向上に役立てるため、当社は匿名化された使用状況データを収集します。
クラッシュが発生した場合は匿名化されたスタックトレース。追加情報
で入手できます。このデータは
弊社のプライバシーポリシーに従って取り扱われます
。これを選択することもできます
今すぐ (以下のプロンプトで「Y」を選択)、またはいつでも収集できます。
次のコマンドを実行して、将来のことを確認します。

gcloud config set disable_usage_reporting false

Google Cloud CLI の改善に協力したいと思いますか (y/N)?

どうやらデータ収集に同意するか
ということらしい

次に

Your current Google Cloud CLI version is: 477.0.0
The latest available version is: 484.0.0

┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                     Components                                                     │
├──────────────────┬──────────────────────────────────────────────────────┬──────────────────────────────┬───────────┤
│      Status      │                         Name                         │              ID              │    Size   │
├──────────────────┼──────────────────────────────────────────────────────┼──────────────────────────────┼───────────┤
│ Update Available │ BigQuery Command Line Tool                           │ bq                           │   1.7 MiB │
│ Update Available │ Cloud Storage Command Line Tool                      │ gsutil                       │  11.3 MiB │
│ Update Available │ Google Cloud CLI Core Libraries                      │ core                         │  19.1 MiB │
│ Not Installed    │ App Engine Go Extensions                             │ app-engine-go                │   4.5 MiB │
│ Not Installed    │ Appctl                                               │ appctl                       │  18.5 MiB │
│ Not Installed    │ Artifact Registry Go Module Package Helper           │ package-go-module            │   < 1 MiB │
│ Not Installed    │ Cloud Bigtable Command Line Tool                     │ cbt                          │  17.0 MiB │
│ Not Installed    │ Cloud Bigtable Emulator                              │ bigtable                     │   7.0 MiB │
│ Not Installed    │ Cloud Datastore Emulator                             │ cloud-datastore-emulator     │  36.2 MiB │
│ Not Installed    │ Cloud Firestore Emulator                             │ cloud-firestore-emulator     │  45.2 MiB │
│ Not Installed    │ Cloud Pub/Sub Emulator                               │ pubsub-emulator              │  63.7 MiB │
│ Not Installed    │ Cloud Run Proxy                                      │ cloud-run-proxy              │  11.3 MiB │
│ Not Installed    │ Cloud SQL Proxy v2                                   │ cloud-sql-proxy              │  13.2 MiB │
│ Not Installed    │ Google Container Registry's Docker credential helper │ docker-credential-gcr        │           │
│ Not Installed    │ Kustomize                                            │ kustomize                    │   7.4 MiB │
│ Not Installed    │ Log Streaming                                        │ log-streaming                │  11.9 MiB │
│ Not Installed    │ Minikube                                             │ minikube                     │  34.8 MiB │
│ Not Installed    │ Nomos CLI                                            │ nomos                        │  30.1 MiB │
│ Not Installed    │ On-Demand Scanning API extraction helper             │ local-extract                │  13.7 MiB │
│ Not Installed    │ Skaffold                                             │ skaffold                     │  22.8 MiB │
│ Not Installed    │ Terraform Tools                                      │ terraform-tools              │  63.6 MiB │
│ Not Installed    │ anthos-auth                                          │ anthos-auth                  │  20.9 MiB │
│ Not Installed    │ config-connector                                     │ config-connector             │  88.7 MiB │
│ Not Installed    │ enterprise-certificate-proxy                         │ enterprise-certificate-proxy │   8.3 MiB │
│ Not Installed    │ gcloud Alpha Commands                                │ alpha                        │   < 1 MiB │
│ Not Installed    │ gcloud Beta Commands                                 │ beta                         │   < 1 MiB │
│ Not Installed    │ gcloud app Java Extensions                           │ app-engine-java              │ 127.8 MiB │
│ Not Installed    │ gcloud app Python Extensions                         │ app-engine-python            │   5.0 MiB │
│ Not Installed    │ gcloud app Python Extensions (Extra Libraries)       │ app-engine-python-extras     │   < 1 MiB │
│ Not Installed    │ gke-gcloud-auth-plugin                               │ gke-gcloud-auth-plugin       │   4.0 MiB │
│ Not Installed    │ istioctl                                             │ istioctl                     │  24.8 MiB │
│ Not Installed    │ kpt                                                  │ kpt                          │  13.8 MiB │
│ Not Installed    │ kubectl                                              │ kubectl                      │   < 1 MiB │
│ Not Installed    │ kubectl-oidc                                         │ kubectl-oidc                 │  20.9 MiB │
│ Not Installed    │ pkg                                                  │ pkg                          │           │
│ Installed        │ Google Cloud CRC32C Hash Tool                        │ gcloud-crc32c                │   1.2 MiB │
└──────────────────┴──────────────────────────────────────────────────────┴──────────────────────────────┴───────────┘
To install or remove components at your current SDK version [477.0.0], run:
  $ gcloud components install COMPONENT_ID
  $ gcloud components remove COMPONENT_ID

To update your SDK installation to the latest version [484.0.0], run:
  $ gcloud components update


Modify profile to update your $PATH and enable shell command completion?

Do you want to continue (Y/n)?  

これを訳そうとしたけど
Google 翻訳は3900文字以上だとできなかった

To install or remove components at your current SDK version [477.0.0], run:
  $ gcloud components install COMPONENT_ID
  $ gcloud components remove COMPONENT_ID

To update your SDK installation to the latest version [484.0.0], run:
  $ gcloud components update


Modify profile to update your $PATH and enable shell command completion?

Do you want to continue (Y/n)?  

だけにして翻訳

現在の SDK バージョン [477.0.0] でコンポーネントをインストールまたは削除するには、次を実行します。
$ gcloud コンポーネントのインストール COMPONENT_ID
$ gcloud コンポーネントは COMPONENT_ID を削除します

SDK インストールを最新バージョン [484.0.0] に更新するには、次を実行します。
$ gcloud コンポーネントの更新

プロファイルを変更して $PATH を更新し、シェル コマンド補完を有効にしますか?

続行しますか (Y/n)?

これは
Y
でよいらしい

The Google Cloud SDK installer will now prompt you to update an rc file to bring
 the Google Cloud CLIs into your environment.

Enter a path to an rc file to update, or leave blank to use 
[/Users/snowpool/.zshrc]:  

の翻訳は

Google Cloud SDK インストーラは、rc ファイルを更新して、
Google Cloud CLI を環境に導入します。

更新する rc ファイルへのパスを入力するか、空白のままにして使用します
[/ユーザー/snowpool/.zshrc]:

これは
Enterキーにした

Backing up [/Users/snowpool/.zshrc] to [/Users/snowpool/.zshrc.backup].
[/Users/snowpool/.zshrc] has been updated.

==> Start a new shell for the changes to take effect.


Google Cloud CLI works best with Python 3.11 and certain modules.

Download and run Python 3.11 installer? (Y/n)?  

の翻訳は

[/Users/snowpool/.zshrc] を [/Users/snowpool/.zshrc.backup] にバックアップしています。
[/Users/snowpool/.zshrc]が更新されました。

==> 変更を有効にするために新しいシェルを開始します。

Google Cloud CLI は、Python 3.11 および特定のモジュールで最適に動作します。

Python 3.11 インストーラーをダウンロードして実行しますか? (はい/いいえ)?
なので
Y
にする

Running Python 3.11 installer, you may be prompted for sudo password...
Password:

となるので
Macのパスワードを入れる

あとは処理が自動で行われ

installer: Package name is Python
installer: Installing at base path /

installer: The install was successful.
Setting up virtual environment
Creating virtualenv...
Installing modules...
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 89.7/89.7 kB 2.4 MB/s eta 0:00:00
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 59.0/59.0 kB 5.0 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.4/10.4 MB 7.2 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 163.0/163.0 kB 3.4 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.9/2.9 MB 7.4 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 182.4/182.4 kB 8.7 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 117.6/117.6 kB 6.5 MB/s eta 0:00:00
  Building wheel for crcmod (pyproject.toml) ... done


Updates are available for some Google Cloud CLI components.  To install them,
please run:
  $ gcloud components update

Virtual env enabled.

For more information on how to get started, please visit:
  https://cloud.google.com/sdk/docs/quickstarts

となる

source ~/.zshrc

を実行すれば

gcloud -v

が実行できるようになる

結果は

Google Cloud SDK 477.0.0
bq 2.1.4
core 2024.05.17
gcloud-crc32c 1.0.0
gsutil 5.29
Updates are available for some Google Cloud CLI components.  To install them,
please run:
  $ gcloud components update

次はgcloudの初期化をする

Markdownの記述メモとGithub へソースコード公開

Markdownの記述メモとGithub へソースコード公開

Markdownの書き方

を参考に

# 見出しレベル1
## 見出しレベル2
### 見出しレベル3
#### 見出しレベル4
##### 見出しレベル5

見出しレベル1
=========

見出しレベル2
---------

レベル1は
H1

と同じ

ソースコードは
バッククォートを使ってコードを囲む

改行は行末にスペースを2つ入れる

URLはそのままでOK

# mail_voice
Read unread Gmail text with URL removed. If there is a link in the text, open it with selenium and download the PDF.

特別支援学校のお知らせがCocooによりメールで送られてくるため  
Gmailでラベルを作成し、未読のメールを読み上げるようにしています。  
メール内の本文をvoicevox で読み上げ、  
文章内にURLがある場合 selenimでダウンロードし  
PDFの文字数が100文字以下なら読み上げ、  
それ以上ならPDFを確認するように音声が流れます。

## 動作環境
M1 MacbookAir 16GB で動作しています、  

音声の作成に voicevox の docker が必要になります  
`docker pull voicevox/voicevox_engine:cpu-ubuntu20.04-latest`  
で取得しています

動作させるには

バックグランドでの起動で
-d オプションをつけて  
`docker run -d  -p '192.168.1.69:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest`  
というように起動させます  
IPアドレス部分はご自身のマシンのIPに変えてください

Gmailを操作するためAPIとtoken.jsonが必要になります  
https://developers.google.com/gmail/api/quickstart/python?hl=ja  
を参考にAPIを使用可能にし、token.jsonを取得し同一ディレクトリに設置してください  

config.iniの中にGmailラベルの設定と  
dockerマシンのIPアドレスの設定  
次の顔検出までの設定時間があります  

それぞれの使用環境でセットしてください

として保存

次に
GitHubでパスワード認証が廃止されたため、
HTTPSを使ってリモートリポジトリにアクセスする際にはパーソナルアクセストークン(PAT)を使用するか、
SSHキー認証を使用する必要がある

今回はSSHキー認証を使う

ssh-keygen -t ed25519 -C “メールアドレス”

でパスフレーズを入力

cat id_ed25519

でファイルの中身が見れる

GtHubにログインし、右上のプロフィールアイコンをクリックして「Settings」に進み
「SSH and GPG keys」をクリックし、「New SSH key」ボタンをクリックし
Title」にキーの名前を入力し、「Key」フィールドに公開キーの内容をペースト

Add SSH key」をクリックしてキーを登録

しかし登録がされなかった

再度試す

cat ~/.ssh/id_ed25519.pub | pbcopy

でクリップボードにコピーした値を使ってみた

これが正しい値だった

これにより
SSHで git clone しようとした時の

You don't have any public SSH keys in your GitHub account. You can add a new public key, or try cloning this repository via HTTPS.

が消える

git clone git@github.com:Snowpooll/mail_voice.git


設定したSSHのパスフレーズを入力すれば
Git clone が実行される

次に gitignore と requirements.txtを作成する

 vim .gitignore

でファイルを作成

__pycache__/
query.json
token.json
downloaded_file.pdf
notice.wav
notice_pdf.wav
audio_output.wav
email_body.txt

として保存

次に

vim requirements.txt

でファイルを作成

中身は

PyMuPDF
playsound
configparser
pygame
google-api-python-client
google-auth-oauthlib
google-auth-httplib2
python-dateutil
opencv-python
selenium
requests

あとは

git add .
 git commit -m "add source code"
git push -u origin main

で公開

先にコードをアップして
READMEは後で書くことも可能

在庫管理のための検出画像ディレクトリと推論モデルのディレクトリ設定

在庫管理のための検出画像ディレクトリと推論モデルのディレクトリ設定

モデルと
推論する画像を格納するディレクトリを
設定ファイルconfig.iniで指定しておき
切り替えを簡単にできるようにする

これはモデルを今後作成しなおすのと
画像の対象をwebカメラで撮影したものにするか
もしくはリアルタイムで監視したものにするかを切り替えるため

mkdir inventory_images

で画像ファイルの置き場所を作成

vim config.ini

で設定ファイルを作成

[Settings]
model_path = inventory_model/best.pt
image_directory = inventory_images

として保存

count_inventory.py
の中身を
import json
from ultralytics import YOLO
from collections import defaultdict

# JSONファイルからクラスラベルのマッピングを読み込み
with open('label_mapping.json', 'r', encoding='utf-8') as f:
    label_mapping = json.load(f)

# YOLOv8モデルのロード
model = YOLO('inventory_model/best.pt')  # ここで適切なモデルを選択

# 画像のロード
image_path = 'path_to_your_image.jpg'
image = cv2.imread(image_path)

# 画像の検出
results = model(image)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls

# 検出物体のカウント
object_counts = defaultdict(int)
for cls in classes:
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    object_counts[label] += 1

# 検出結果の表示
for label, count in object_counts.items():
    print(f'{label}: {count}個')

から

import json
import cv2
import configparser
from ultralytics import YOLO
from collections import defaultdict
from line_notify import send_line_notify  # インポートを追加
from datetime import datetime
from inventory_database_module 
 import save_detection_to_db  # データベース保存用の関数をインポート
import os

# 設定ファイルの読み込み
config = configparser.ConfigParser()
config.read('config.ini')

# 設定ファイルからモデルパスと画像ディレクトリを取得
model_path = config['Settings']['model_path']
image_directory = config['Settings']['image_directory']

# ラベルマッピングファイルのパス
label_mapping_path = 'label_mapping.json'

# JSONファイルからクラスラベルのマッピングを読み込み
with open(label_mapping_path, 'r', encoding='utf-8') as f:
    label_mapping = json.load(f)

# YOLOv8モデルのロード
model = YOLO(model_path)  # 設定ファイルからモデルパスを使用

# 画像ディレクトリ内の全画像ファイルを処理
for image_filename in os.listdir(image_directory):
    image_path = os.path.join(image_directory, image_filename)
    if os.path.isfile(image_path) and image_path.lower().endswith(('.png', '.jpg', '.jpeg')):
        # 画像のロード
        image = cv2.imread(image_path)

        # 画像の検出
        results = model(image, save=True, conf=0.2, iou=0.5)

        # 検出結果の取得
        detections = results[0]  # 最初の結果を取得
        classes = detections.boxes.cls

        # 検出物体のカウント
        object_counts = defaultdict(int)
        for cls in classes:
            class_label = model.names[int(cls)]
            if class_label in label_mapping:
                label = label_mapping[class_label]
            else:
                label = class_label
            object_counts[label] += 1

        # 検出結果のフィルタリング(1以下のもの)
        filtered_object_counts = {label: count for label, count in object_counts.items() if count <= 1}

        # フィルタリングされた検出結果のメッセージ生成
        message_lines = [f'{label}: {count}個' for label, count in filtered_object_counts.items()]
        message = '\n'.join(message_lines)

        # 現在の時刻を取得
        current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        message = f"{message}\n\nMessage sent at: {current_time}"

        # 検出結果の表示
        for line in message_lines:
            print(line)

        # LINE Notifyにメッセージを送信(フィルタリングされた結果のみ)
        if message_lines:
            send_line_notify(message)
            save_detection_to_db(filtered_object_counts)  # データベースに検出結果を保存
        else:
            print("No objects with counts of 1 or less detected in file:", image_filename)

へ変更

試しに

cp data_bak/Baskulin4.jpg inventory_images

でデータを写し

python count_inventory.py

を実行すると

0: 640x512 1 baskulin, 131.0ms
Speed: 5.6ms preprocess, 131.0ms inference, 6.6ms postprocess per image at shape (1, 3, 640, 512)
Results saved to runs/detect/predict22
バスクリン: 1個
File: runs/detect/predict22/image0.jpg
200
{"status":200,"message":"ok"}

というようにLINEへ送信される

とりあえずここまでできたので
githubで公開し
モデルは roboflowなどを使って改良して後々公開する

yolov8の検出結果のDB格納

検出結果のDB格納

vim create_table.py

を作成

import sqlite3

def create_table():
    conn = sqlite3.connect('detections.db')
    cursor = conn.cursor()

    # テーブルを作成
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS detections (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            label TEXT NOT NULL,
            count INTEGER NOT NULL,
            timestamp TEXT NOT NULL
        )
    ''')

    conn.commit()
    conn.close()

if __name__ == '__main__':
    create_table()

これを実行し
DBを作成

次にDBへ保存するモジュールの作成

vim inventory_database_module.py

でファイルを作成

import sqlite3
from datetime import datetime

def save_detection_to_db(detections):
    conn = sqlite3.connect('detections.db')
    cursor = conn.cursor()

    # 現在の時刻を取得
    current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

    # 検出結果をテーブルに挿入
    for label, count in detections.items():
        cursor.execute('''
            INSERT INTO detections (label, count, timestamp)
            VALUES (?, ?, ?)
        ''', (label, count, current_time))

    conn.commit()
    conn.close()

として保存

import argparse
import json
import cv2
from ultralytics import YOLO
from collections import defaultdict
from line_notify import send_line_notify  # インポートを追加
from datetime import datetime
from inventory_database_module import save_detection_to_db  # データベース保存用の関数をインポート

# コマンドライン引数の解析
parser = argparse.ArgumentParser(description="YOLOv8 Object Detection")
parser.add_argument('image_path', type=str, help='Path to the input image file')
args = parser.parse_args()

# ラベルマッピングファイルのパス
label_mapping_path = 'label_mapping.json'

# JSONファイルからクラスラベルのマッピングを読み込み
with open(label_mapping_path, 'r', encoding='utf-8') as f:
    label_mapping = json.load(f)

# YOLOv8モデルのロード
model = YOLO('inventory_model/best.pt')  # ここで適切なモデルを選択

# 画像のロード
image = cv2.imread(args.image_path)

# 画像の検出
results = model(image, save=True, conf=0.1, iou=0.5)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls

# 検出物体のカウント
object_counts = defaultdict(int)
for cls in classes:
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    object_counts[label] += 1

# 検出結果のフィルタリング(1以下のもの)
filtered_object_counts = {label: count for label, count in object_counts.items() if count <= 1}

# フィルタリングされた検出結果のメッセージ生成
message_lines = [f'{label}: {count}個' for label, count in filtered_object_counts.items()]
message = '\n'.join(message_lines)

# 現在の時刻を取得
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
message = f"{message}\n在庫チェックの時刻: {current_time}"

# 検出結果の表示
for line in message_lines:
    print(line)

# LINE Notifyにメッセージを送信(フィルタリングされた結果のみ)
if message_lines:
    send_line_notify(message)
    save_detection_to_db(filtered_object_counts)  # データベースに検出結果を保存
else:
    print("No objects with counts of 1 or less detected.")

というように
結果をDBに保存し
在庫チェックの時刻も送信するようにコード変更

なお実行した後に
DBの中身を見るには

vim view_detections.py

import sqlite3

def view_detections(db_path='detections.db'):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    # テーブルの内容を取得
    cursor.execute('SELECT * FROM detections')
    rows = cursor.fetchall()

    # カラム名を取得
    column_names = [description[0] for description in cursor.description]

    # 結果を表示
    print(f"{' | '.join(column_names)}")
    print("-" * 50)
    for row in rows:
        print(" | ".join(str(value) for value in row))

    conn.close()

if __name__ == '__main__':
    view_detections()

として保存

python view_detections.py

を実行すると

id | label | count | timestamp
--------------------------------------------------
1 | バスクリン | 1 | 2024-07-07 06:45:47
2 | バスクリン | 1 | 2024-07-07 06:50:42
3 | バスクリン | 1 | 2024-07-07 06:51:43

となり検出結果の確認ができる

認識精度が低いため
精度を0.1まで下げないと認識しないし
並べた時の複数の検出ができていない

とりあえず指定ディレクトリの画像から検出するようにコード変更する

LINE notifyのモジュール化

LINE notifyのモジュール化

別のメソッドでも使えるようにモジュール化する
また

message = 'ファイルパス自動取得テスト' 

の部分は
他のプログラムで
生成された文字列を受け取って実行するようにコードを変更する

vim line_notify.py

import requests
import os
from PIL import Image
from io import BytesIO
from utils import load_config, get_latest_directory, get_image_files

def resize_image_if_needed(image_data, max_size=3 * 1024 * 1024):
    if len(image_data) > max_size:
        image = Image.open(BytesIO(image_data))
        new_size = (image.width // 2, image.height // 2)
        image = image.resize(new_size, Image.LANCZOS)

        output = BytesIO()
        image_format = image.format if image.format else 'JPEG'
        image.save(output, format=image_format)
        return output.getvalue()
    return image_data

def send_line_notify(message, config_path='config.json'):
    # 設定ファイルを読み込む
    config = load_config(config_path)

    # 設定ファイルからトークンとディレクトリパスを取得
    token = config['token']
    base_path = config['image_file_path']

    # 最新のpredictディレクトリを取得
    latest_dir = get_latest_directory(base_path)
    image_files = get_image_files(latest_dir)

    url = 'https://notify-api.line.me/api/notify'

    headers = {'Authorization': f"Bearer {token}"}
    params = {'message': message}

    # 最新のpredictディレクトリ内の全ての画像ファイルに対してLINE Notify APIにリクエストを送信
    for image_file_path in image_files:
        with open(image_file_path, 'rb') as img_file:
            img_data = img_file.read()
            img_data = resize_image_if_needed(img_data)

            # ファイルデータをバイトデータとして用意
            files = {'imageFile': BytesIO(img_data)}
            files['imageFile'].name = os.path.basename(image_file_path)

            # LINE Notify APIにリクエストを送信
            res = requests.post(url, headers=headers, params=params, files=files)

            # レスポンスを出力
            print(f"File: {image_file_path}")
            print(res.status_code)
            print(res.text)

とりあえずこれを使えるかテストする

import argparse
import json
import cv2
from ultralytics import YOLO
from collections import defaultdict

# コマンドライン引数の解析
parser = argparse.ArgumentParser(description="YOLOv8 Object Detection")
parser.add_argument('image_path', type=str, help='Path to the input image file')
args = parser.parse_args()

# ラベルマッピングファイルのパス
label_mapping_path = 'label_mapping.json'

# JSONファイルからクラスラベルのマッピングを読み込み
with open(label_mapping_path, 'r', encoding='utf-8') as f:
    label_mapping = json.load(f)

# YOLOv8モデルのロード
model = YOLO('inventory_model/best.pt')  # ここで適切なモデルを選択

# 画像のロード
image = cv2.imread(args.image_path)

# 画像の検出
results = model(image, save=True, conf=0.2, iou=0.5)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls

# 検出物体のカウント
object_counts = defaultdict(int)
for cls in classes:
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    object_counts[label] += 1

# 検出結果の表示
for label, count in object_counts.items():
    print(f'{label}: {count}個')

の中で呼び出すようにする

import argparse
import json
import cv2
from ultralytics import YOLO
from collections import defaultdict
from line_notify import send_line_notify  # インポートを追加

# コマンドライン引数の解析
parser = argparse.ArgumentParser(description="YOLOv8 Object Detection")
parser.add_argument('image_path', type=str, help='Path to the input image file')
args = parser.parse_args()

# ラベルマッピングファイルのパス
label_mapping_path = 'label_mapping.json'

# JSONファイルからクラスラベルのマッピングを読み込み
with open(label_mapping_path, 'r', encoding='utf-8') as f:
    label_mapping = json.load(f)

# YOLOv8モデルのロード
model = YOLO('inventory_model/best.pt')  # ここで適切なモデルを選択

# 画像のロード
image = cv2.imread(args.image_path)

# 画像の検出
results = model(image, save=True, conf=0.2, iou=0.5)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls

# 検出物体のカウント
object_counts = defaultdict(int)
for cls in classes:
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    object_counts[label] += 1

# 検出結果のメッセージ生成
message_lines = [f'{label}: {count}個' for label, count in object_counts.items()]
message = '\n'.join(message_lines)

# 検出結果の表示
for line in message_lines:
    print(line)

# LINE Notifyにメッセージを送信
send_line_notify(message)

これを

python count_inventory_terminal.py data_bak/Baskulin4.jpg

で実行すると

0: 640x512 1 baskulin, 125.4ms
Speed: 7.7ms preprocess, 125.4ms inference, 7.6ms postprocess per image at shape (1, 3, 640, 512)
Results saved to runs/detect/predict4
バスクリン: 1個
File: runs/detect/predict4/image0.jpg
200
{"status":200,"message":"ok"}

となり画像つきメッセージが送信される

次は在庫の数が1以下のものをリストにして送信するようにする

import argparse
import json
import cv2
from ultralytics import YOLO
from collections import defaultdict
from line_notify import send_line_notify  # インポートを追加

# コマンドライン引数の解析
parser = argparse.ArgumentParser(description="YOLOv8 Object Detection")
parser.add_argument('image_path', type=str, help='Path to the input image file')
args = parser.parse_args()

# ラベルマッピングファイルのパス
label_mapping_path = 'label_mapping.json'

# JSONファイルからクラスラベルのマッピングを読み込み
with open(label_mapping_path, 'r', encoding='utf-8') as f:
    label_mapping = json.load(f)

# YOLOv8モデルのロード
model = YOLO('inventory_model/best.pt')  # ここで適切なモデルを選択

# 画像のロード
image = cv2.imread(args.image_path)

# 画像の検出
results = model(image, save=True, conf=0.2, iou=0.5)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls

# 検出物体のカウント
object_counts = defaultdict(int)
for cls in classes:
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    object_counts[label] += 1

# 検出結果のフィルタリング(1以下のもの)
filtered_object_counts = {label: count for label, count in object_counts.items() if count <= 1}

# フィルタリングされた検出結果のメッセージ生成
message_lines = [f'{label}: {count}個' for label, count in filtered_object_counts.items()]
message = '\n'.join(message_lines)

# 検出結果の表示
for line in message_lines:
    print(line)

# LINE Notifyにメッセージを送信(フィルタリングされた結果のみ)
if message:
    send_line_notify(message)
else:
    print("No objects with counts of 1 or less detected.")

これで今度は

python count_inventory_terminal.py data_bak/potato_starch1.jpg 

として検出されない時には

0: 640x512 (no detections), 123.0ms
Speed: 5.7ms preprocess, 123.0ms inference, 5.4ms postprocess per image at shape (1, 3, 640, 512)
Results saved to runs/detect/predict6
No objects with counts of 1 or less detected.

となって
LINE送信はされなくなる

今回の画像はモデルの学習不足のためか
片栗粉の検出ができなかったので
それを認識できない場合のテストに使った

しかし、画像読み取りエラーなどを考慮し
今後何らかのアクションを取るようにした方が良いかもしれない

エラーログ以外のものを考えるようにする

また、送信するタイミングは、在庫数が1以下になった時に送るようにしました。

この場合、画像が検出できなかったりした時に判定ができないため
今後の課題とします
解決方法としては、検出結果をDBへ格納しておき
実行したタイムスタンプも記録、検出結果が0の時にはアラートを飛ばすなどがありそうです

とりあえず、ターミナル実行のみの状態なので
今後はどこから画像を撮ってくるのか、またwebカメラで行うのか、それとも
ラズパイゼロなどで撮影した画像を使うのか、それを考えてからまた改良していこうと思います

LINE通知の時の画像サイズ変更

LINE通知の時の画像サイズ変更

3M以上の画像サイズになると

File: runs/detect/predict3/image0.jpg
400
{"status":400,"message":"Image size must be less than 3MB."}

となるため、画像ファイルの大きさを変更するプログラムが必要

なお画像については

results = model(image, save=True, conf=0.2, iou=0.5)

というように
save=True をつければ検出結果が出力されるので
その画像を使う

画像サイズが3MB以下になるように画像を1/4にリサイズして送信するようにコード変更

import requests
import os
from utils import load_config, get_latest_directory, get_image_files

# 設定ファイルを読み込む
config = load_config('config.json')

# 設定ファイルからトークンとディレクトリパスを取得
token = config['token']
base_path = config['image_file_path']

# 最新のpredictディレクトリを取得
latest_dir = get_latest_directory(base_path)
image_files = get_image_files(latest_dir)

url = 'https://notify-api.line.me/api/notify'
message = 'ファイルパス自動取得テスト'

headers = {'Authorization': f"Bearer {token}"}
params = {'message': message}

# 最新のpredictディレクトリ内の全ての画像ファイルに対してLINE Notify APIにリクエストを送信
for image_file_path in image_files:
    files = {'imageFile': open(image_file_path, 'rb')}
    
    # LINE Notify APIにリクエストを送信
    res = requests.post(url, headers=headers, params=params, files=files)

    # レスポンスを出力
    print(f"File: {image_file_path}")
    print(res.status_code)
    print(res.text)

import requests
import os
from PIL import Image
from io import BytesIO
from utils import load_config, get_latest_directory, get_image_files

# 設定ファイルを読み込む
config = load_config('config.json')

# 設定ファイルからトークンとディレクトリパスを取得
token = config['token']
base_path = config['image_file_path']

# 最新のpredictディレクトリを取得
latest_dir = get_latest_directory(base_path)
image_files = get_image_files(latest_dir)

url = 'https://notify-api.line.me/api/notify'
message = 'ファイルパス自動取得テスト'

headers = {'Authorization': f"Bearer {token}"}
params = {'message': message}

# 最新のpredictディレクトリ内の全ての画像ファイルに対してLINE Notify APIにリクエストを送信
for image_file_path in image_files:
    with open(image_file_path, 'rb') as img_file:
        img_data = img_file.read()
        
        # 画像ファイルのサイズをチェック
        if len(img_data) > 3 * 1024 * 1024:  # 3MB
            # 画像をリサイズ
            image = Image.open(BytesIO(img_data))
            new_size = (image.width // 2, image.height // 2)
            image = image.resize(new_size, Image.ANTIALIAS)
            
            # リサイズした画像をバイトデータに変換
            output = BytesIO()
            image.save(output, format=image.format)
            img_data = output.getvalue()
        
        # ファイルデータをバイトデータとして用意
        files = {'imageFile': BytesIO(img_data)}
        files['imageFile'].name = os.path.basename(image_file_path)
        
        # LINE Notify APIにリクエストを送信
        res = requests.post(url, headers=headers, params=params, files=files)

        # レスポンスを出力
        print(f"File: {image_file_path}")
        print(res.status_code)
        print(res.text)

としたが

/Users/snowpool/aw10s/inventory/line_order.py:34: DeprecationWarning: ANTIALIAS is deprecated and will be removed in Pillow 10 (2023-07-01). Use LANCZOS or Resampling.LANCZOS instead.
  image = image.resize(new_size, Image.ANTIALIAS)
Traceback (most recent call last):
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/PIL/Image.py", line 2408, in save
    format = EXTENSION[ext]
KeyError: ''

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/line_order.py", line 38, in <module>
    image.save(output, format=image.format)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/PIL/Image.py", line 2411, in save
    raise ValueError(msg) from e
ValueError: unknown file extension: 

となる

これは
ANTIALIASの非推奨:ANTIALIASがPillow 10で非推奨となり、代わりにLANCZOSを使用する必要がある

画像の形式が不明:image.saveメソッドに指定された形式が正しく解釈されていないため、KeyErrorが発生しています。これは、image.formatが正しく設定されていないか、空であるため
の2つが原因

import requests
import os
from PIL import Image
from io import BytesIO
from utils import load_config, get_latest_directory, get_image_files

# 設定ファイルを読み込む
config = load_config('config.json')

# 設定ファイルからトークンとディレクトリパスを取得
token = config['token']
base_path = config['image_file_path']

# 最新のpredictディレクトリを取得
latest_dir = get_latest_directory(base_path)
image_files = get_image_files(latest_dir)

url = 'https://notify-api.line.me/api/notify'
message = 'ファイルパス自動取得テスト'

headers = {'Authorization': f"Bearer {token}"}
params = {'message': message}

# 最新のpredictディレクトリ内の全ての画像ファイルに対してLINE Notify APIにリクエストを送信
for image_file_path in image_files:
    with open(image_file_path, 'rb') as img_file:
        img_data = img_file.read()
        
        # 画像ファイルのサイズをチェック
        if len(img_data) > 3 * 1024 * 1024:  # 3MB
            # 画像をリサイズ
            image = Image.open(BytesIO(img_data))
            new_size = (image.width // 2, image.height // 2)
            image = image.resize(new_size, Image.LANCZOS)
            
            # リサイズした画像をバイトデータに変換
            output = BytesIO()
            image_format = image.format if image.format else 'JPEG'  # デフォルトでJPEG形式を設定
            image.save(output, format=image_format)
            img_data = output.getvalue()
        
        # ファイルデータをバイトデータとして用意
        files = {'imageFile': BytesIO(img_data)}
        files['imageFile'].name = os.path.basename(image_file_path)
        
        # LINE Notify APIにリクエストを送信
        res = requests.post(url, headers=headers, params=params, files=files)

        # レスポンスを出力
        print(f"File: {image_file_path}")
        print(res.status_code)
        print(res.text)

とすることで

File: runs/detect/predict3/image0.jpg
200
{"status":200,"message":"ok"}

となり、元の画像サイズが大きくても問題なく送信できるようになった

LINE Notify を通知で使う

LINEで通知できるようにする

在庫管理をできるようにしたら
足りないものを知らせる機能が必要

LINEで買い物リストとして昼ぐらいに送信すれば
帰りに購入して帰ることができる

過去記事を参考に
LINE Messasging API でメッセージ送信

を参考にリンクをしたら

LINE Business ID
になってしまうので
再度調べることにする

LINE: LINE Notifyを用いてWindowsのcurlコマンドからメッセージを投稿する
を参考に再度設定をしていく

スマホのLINEアプリで
トーク > トークルームの作成 > グループ

友達は誰も選択せずに次へ

次に友達をグループに追加があるけど
デフォルトで友達をグループに自動で追加がチェックされているので
チェックを外す

グループ名は買い物リスト
とした

次にLINE Notifyにログインする
https://notify-bot.line.me/ja/

ログインの時にQRコードからログインできるので
スマホのLINEアプリでQRコードを読み取る
もしくはスマホのカメラアプリでQRコードを読み取ると
LINEアプリでQRコードを読み取るように出るので
そのまま実行していくと認証画面になり
PCの画面に表示された数字をスマホで打ち込めば
ログインできる

ログインできたらトークンを発行する

マイページ > アクセストークンの発行

トークンを発行する
をクリックし
トークン名を入力し
通知を送信するトークルームを選択

今回は両方とも
買い物リストを選択

これでトークンが発行される

次にスマホで
買い物リストの
トークルームを開き
設定 > 招待 で
LINE notifyを選択し招待する

ここへはcurlコマンドでメッセージを送信できる

https://notify-bot.line.me/doc/ja/
のサンプルは

 curl -X POST -H 'Authorization: Bearer <access_token>' -F 'message=foobar' \
https://notify-api.line.me/api/notify
{"status":200,"message":"ok"}

 curl -v -X POST -H 'Authorization: Bearer invalidtoken' -F 'message=foobar' \
https://notify-api.line.me/api/notify
{"status":401,"message":"Invalid access token"}

https://qiita.com/frozencatpisces/items/679d66ab1d617b7a40cb#1-投稿先トークルームの作成
では

curl -X POST -H "Authorization: Bearer 発行したトークン" -F "message=foobar" https://notify-api.line.me/api/notify

これをリストにする場合は複数行必要なので
LINE Notify で curl で改行する
https://blog.framinal.life/entry/2023/06/14/151933
を参考に

URLエンコーディングされた改行文字(%0A) に変換して送ることでできそう

message="こんにちは\n元気ですか?\n\n"

# Convert newline characters to URL-encoded form
message_encoded=$(echo -e $message | awk '{printf "%s%%0A", $0}')

curl -X POST -H "Authorization: Bearer XXXXXX" --data-binary "message=$message_encoded" https://notify-api.line.me/api/notify

ちょっと古い情報で3年前のだと

LINE NotifyからのLINE通知を改行する方法【Python/LINE Notify(API)】
だと
Lineで通知する文章を改行したい場合、「\n」を入れると改行できるらしい

send_contents = f'\n今日は\n{strftime}({day_of_the_week[weekday]})です。'

また

send_contents = '\n今日は\n' + str(strftime) + '(' + str(day_of_the_week[weekday]) + ')です。'

というようにしてもOK

“Line Notify”を利用してPythonでLineに通知を送る
では
requesta
を使っている

pip install requests

テキストだけなら

import requests

url = 'https://notify-api.line.me/api/notify'

token = '発行したトークン'
message = '通知したいメッセージ'

headers = {'Authorization': f"Bearer {token}"}
params = {'message': message}

requests.post(url, headers=headers, params=params)

テキストと画像付きなら

import requests

url = 'https://notify-api.line.me/api/notify'

token = '発行したトークン'
message = '通知したいテキスト'
image_file_path = 'イメージファイルパス'

headers = {'Authorization': f"Bearer {token}"}
params = {'message': message}
files = {'imageFile': open(image_file_path, 'rb')}

res = requests.post(url, headers=headers, params=params, files=files)

テキストの改行なら

message = 'ここで改行\n改行後のテキスト'

PythonでLINE Notifyを使ってみよう
によれば
・メッセージを改行したい場合は「\n」を挿入
・メッセージは最大1000文字まで

import requests
def notify(message):

        url = 'https://notify-api.line.me/api/notify'
        token = '発行されたトークン'
        headers = {'Authorization': 'Bearer ' + token}

        message = message
        params = {'message': message}

        requests.post(url, headers=headers, params=params)

if __name__ == '__main__':
    notify('テスト')

がサンプル

こちらもrequestを使用

[Python]LINEで天気を自動通知させてみた[初心者]
によれば
Webから天気情報を取得してLINEで自動通知をしてみました。定期実行までやります
では
Cronで定期実行

実行するコードは

import datetime
import urllib.request as req
import requests
from bs4 import BeautifulSoup
import re

#LINE notifyの設定を行う
url = "https://notify-api.line.me/api/notify"
access_token = '○○○○○'
headers = {'Authorization': 'Bearer ' + access_token}

#天気サイトから欲しい情報を取得する
url2 = "https://tenki.jp/forecast/3/17/4610/14100/"   #欲しい情報があるURLを指定
res = requests.get(url2)                              #上記URL情報を取得する
soup = BeautifulSoup(res.content, 'html.parser')      #取得した情報をhtmlで解析する

# 以下各種情報を取得
ddd = soup.find(class_="left-style")                  

telop = soup.find("p", class_="weather-telop").string

highlists = soup.find("dd",class_="high-temp temp")

lowlists = soup.find("dd",class_="low-temp temp")

ttt = soup.find(class_="rain-probability")

row=[]
for t in ttt:
    row.append(t)

# message変数に通知したい文を代入する 改行したい場合は "\n" とダブルクォテーションで囲う
message="\n" + ddd.text + "\n" + telop + "\n" + "最高 " + highlists.text + "\n" + "最低 " + lowlists.text + "\n"+ "---------" + "\n" +row[1].text +"\n" + "~6  : " + row[3].text + "\n" + "~12 : " + row[5].text +"\n" + "~18 : " + row[7].text +"\n" + "~24 : " + row[9].text +"\n" +"今日も元気に٩( 'ω' )و "

payload = {'message': message}
r = requests.post(url, headers=headers, params=payload,)

というように requestを使っている

あと
1人のユーザーにつき、1時間に通知できる回数は1000回まで
という縛りがあるが
買い物リストは1日1回程度だと思うし
カメラ画像からとして考えても3箇所程度だと思うので問題なし

とりあえずchatgptで調べた結果
Curl でも requestでも問題はなさそう

コードメンテを考えるとrequestsの方が良さそう

ということで

vim line_order.py

でファイルを作成し

vim config.json

で設定ファイルを作成

{
  "token": "発行したトークン",
  "image_file_path": "イメージファイルパス"
}
import requests
import json

# 設定ファイルを読み込む関数
def load_config(file_path):
    with open(file_path, 'r') as file:
        return json.load(file)

# 設定ファイルを読み込む
config = load_config('config.json')

# 設定ファイルからトークンとファイルパスを取得
token = config['token']
image_file_path = config['image_file_path']

url = 'https://notify-api.line.me/api/notify'
message = '通知したいテキスト'

headers = {'Authorization': f"Bearer {token}"}
params = {'message': message}
files = {'imageFile': open(image_file_path, 'rb')}

# LINE Notify APIにリクエストを送信
res = requests.post(url, headers=headers, params=params, files=files)

# レスポンスを出力
print(res.status_code)
print(res.text)


line_order.py
の内容を書き換えても

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/inventory/line_order.py", line 21, in <module>
    files = {'imageFile': open(image_file_path, 'rb')}
IsADirectoryError: [Errno 21] Is a directory: 'image/'

となる

とりあえず画像を指定する

{
  "token": "発行したトークン",
  "image_file_path": "runs/detect/predict7/Baskulin1.jpg"
}

とすれば成功

改良点としては
メッセージの文章を
検出結果のラベルを変換した文字列にすること
検出結果は
runs/detect/
の中にどんどん新しい番号が付けられて増えていくため
動的にパスを取得するスクリプトにすること

osモジュールを使用して、指定されたディレクトリ内のサブディレクトリをリストアップし、その中で最新の番号を持つディレクトリを特定できる

1. os.listdir(base_path)を使用して、指定されたディレクトリ内の全てのファイルとディレクトリのリストを取得します。
2. リスト内の要素がディレクトリであるかどうかを確認するためにos.path.isdir()を使用します。
3. predictプレフィックスを削除して数値に変換し、max()関数を使用して最大の数値を持つディレクトリを特定します。
4. os.path.join(base_path, latest_dir)を使用して、フルパスを生成します。

import os

def get_latest_directory(base_path):
    # 指定されたディレクトリ内の全てのサブディレクトリを取得
    subdirs = [d for d in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, d))]
    
    # サブディレクトリ名を数値に変換し、ソートして最新のディレクトリを特定
    latest_dir = max(subdirs, key=lambda x: int(x.replace('predict', '')))
    
    return os.path.join(base_path, latest_dir)

# 使用例
base_path = 'runs/detect'
latest_dir = get_latest_directory(base_path)
print(f"Latest directory: {latest_dir}")

実行すると

File "/Users/snowpool/aw10s/inventory/utils.py", line 14, in <module> latest_dir = get_latest_directory(base_path) File "/Users/snowpool/aw10s/inventory/utils.py", line 8, in get_latest_directory latest_dir = max(subdirs, key=lambda x: int(x.replace('predict', ''))) File "/Users/snowpool/aw10s/inventory/utils.py", line 8, in <lambda> latest_dir = max(subdirs, key=lambda x: int(x.replace('predict', ''))) ValueError: invalid literal for int() with base 10: '' 

となる

原因は

ValueError: invalid literal for int() with base 10: ''

というエラーは
int()関数が空文字列を処理しようとしたときに発生

これは、predictという文字列をreplaceで削除した結果が
空文字列になる場合に起こる

例えば、predictという名前のディレクトリがある場合など

この問題を解決するために
ディレクトリ名がpredictのプレフィックスを持っているかどうかをチェックし
それ以外のディレクトリ名を無視するようにする

また、predictの後の文字列が数字であることを確認するために
追加のチェックを行う

対処として
1. predictで始まり、その後に数字が続くディレクトリのみを対象とするようにフィルタリングしています。
2. 有効なディレクトリが存在しない場合に適切なエラーメッセージを出力します。

import os

def get_latest_directory(base_path):
    # 指定されたディレクトリ内の全てのサブディレクトリを取得
    subdirs = [d for d in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, d))]

    # サブディレクトリ名が 'predict' で始まり、その後に数字が続くものをフィルタリング
    predict_dirs = [d for d in subdirs if d.startswith('predict') and d[7:].isdigit()]

    if not predict_dirs:
        raise ValueError("No valid 'predict' directories found")

    # サブディレクトリ名を数値に変換し、ソートして最新のディレクトリを特定
    latest_dir = max(predict_dirs, key=lambda x: int(x[7:]))

    return os.path.join(base_path, latest_dir)

# 使用例
base_path = 'runs/detect'
latest_dir = get_latest_directory(base_path)
print(f"Latest directory: {latest_dir}")

これで実行すると

Latest directory: runs/detect/predict7

となった

次は画像ファイルパスの取得

import os

def get_latest_directory(base_path):
    # 指定されたディレクトリ内の全てのサブディレクトリを取得
    subdirs = [d for d in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, d))]

    # サブディレクトリ名が 'predict' で始まり、その後に数字が続くものをフィルタリング
    predict_dirs = [d for d in subdirs if d.startswith('predict') and d[7:].isdigit()]

    if not predict_dirs:
        raise ValueError("No valid 'predict' directories found")

    # サブディレクトリ名を数値に変換し、ソートして最新のディレクトリを特定
    latest_dir = max(predict_dirs, key=lambda x: int(x[7:]))

    return os.path.join(base_path, latest_dir)

def get_image_files(directory):
    # 指定されたディレクトリ内の全ての画像ファイルのパスを取得
    image_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.bmp')
    image_files = [os.path.join(directory, file) for file in os.listdir(directory) if file.lower().endswith(image_extensions)]
    return image_files

# 使用例
base_path = 'runs/detect'
latest_dir = get_latest_directory(base_path)
image_files = get_image_files(latest_dir)

print(f"Latest directory: {latest_dir}")
print("Image files:")
for image_file in image_files:
    print(image_file)

とすることで画像ファイルのパスが取得できた

Latest directory: runs/detect/predict7
Image files:
runs/detect/predict7/Baskulin1.jpg

次にこれをモジュールにして
LINEの画像パスにして送信テストする

import os
import json

def load_config(file_path):
    with open(file_path, 'r') as file:
        return json.load(file)

def get_latest_directory(base_path):
    # 指定されたディレクトリ内の全てのサブディレクトリを取得
    subdirs = [d for d in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, d))]

    # サブディレクトリ名が 'predict' で始まり、その後に数字が続くものをフィルタリング
    predict_dirs = [d for d in subdirs if d.startswith('predict') and d[7:].isdigit()]

    if not predict_dirs:
        raise ValueError("No valid 'predict' directories found")

    # サブディレクトリ名を数値に変換し、ソートして最新のディレクトリを特定
    latest_dir = max(predict_dirs, key=lambda x: int(x[7:]))

    return os.path.join(base_path, latest_dir)

def get_image_files(directory):
    # 指定されたディレクトリ内の全ての画像ファイルのパスを取得
    image_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.bmp')
    image_files = [os.path.join(directory, file) for file in os.listdir(directory) if file.lower().endswith(image_extensions)]
    return image_files

として

config.jsonの中の

  "image_file_path": "runs/detect/predict7/Baskulin1.jpg"

という指定を

  "image_file_path": "runs/detect"

に変更

line_order.pyの中身を

import requests
import os
from utils import load_config, get_latest_directory, get_image_files

# 設定ファイルを読み込む
config = load_config('config.json')

# 設定ファイルからトークンとディレクトリパスを取得
token = config['token']
base_path = config['image_file_path']

# 最新のpredictディレクトリを取得
latest_dir = get_latest_directory(base_path)
image_files = get_image_files(latest_dir)

url = 'https://notify-api.line.me/api/notify'
message = '通知したいテキスト'

headers = {'Authorization': f"Bearer {token}"}
params = {'message': message}

# 最新のpredictディレクトリ内の全ての画像ファイルに対してLINE Notify APIにリクエストを送信
for image_file_path in image_files:
    files = {'imageFile': open(image_file_path, 'rb')}
    
    # LINE Notify APIにリクエストを送信
    res = requests.post(url, headers=headers, params=params, files=files)

    # レスポンスを出力
    print(f"File: {image_file_path}")
    print(res.status_code)
    print(res.text)

として保存

これで

python line_order.py 

を実行すれば画像付きでLINEで送信してくれる

試しに再度新しいyolov8での推論をして
できたディレクトリを対象にするか実験する

しかし

python count_inventory_terminal.py data_bak/Baskulin1.jpg

0: 640x512 1 baskulin, 93.9ms
Speed: 2.7ms preprocess, 93.9ms inference, 2.9ms postprocess per image at shape (1, 3, 640, 512)
バスクリン: 1個

の後に

python line_order.py

を実行しても

File: runs/detect/predict7/Baskulin1.jpg
200
{"status":200,"message":"ok"}

となる

ls runs/detect
の結果も
predict		predict3	predict5	predict7
predict2	predict4	predict6

となる

どうやらyoloのコマンドで実行した時だけ
runs/predict以下に保存されるらしい

つまりカウントした後に画像を保存するプログラムを追加しないとだめ
あと、カウントした時に残り1以下になった時に
ラベルを書き出すプログラムが必要

Yolov8検出結果と文字列の結び付け

Yolov8検出結果と文字列の結び付け

在庫管理のため yolov8で在庫対象とするものを検出し
その在庫数が2以下になれば
LINEで買い物リストを送信するものを作成したい
このためには検出結果のカウント
そして
検出されたものを日本語にする必要がある

YOLOv8の検出結果と対応する文字列を結びつけるためには、
検出されたクラスのラベルを
日本語の対応するラベルに変換するマッピングを作成する必要があり

まずYOLOv8の結果取得:
画像をYOLOv8に入力し、検出結果を取得
検出結果には
各物体のクラスラベル
バウンディングボックスの座標などが含まれている

そして
クラスラベルのマッピング作成
検出されたクラスラベルを
日本語の対応するラベルに変換する辞書を作成

例として

label_mapping = {
    'baskulin': 'バスクリン',
    'potato_starch': '片栗粉',
    'shampoo': 'シャンプー'
}

という感じ

以下はchatgptのコード例

import json

# YOLOv8の検出結果の例
# ここではJSON形式で表現されていますが、実際にはYOLOv8から得られたデータ形式に合わせてください
detection_results = [
    {"class": "baskulin", "bbox": [100, 100, 150, 150]},
    {"class": "potato_starch", "bbox": [200, 200, 250, 250]},
    {"class": "shampoo", "bbox": [300, 300, 350, 350]},
]

# クラスラベルのマッピング
label_mapping = {
    'baskulin': 'バスクリン',
    'potato_starch': '片栗粉',
    'shampoo': 'シャンプー'
}

# マッピングの適用
for detection in detection_results:
    class_label = detection['class']
    if class_label in label_mapping:
        detection['class'] = label_mapping[class_label]

# 結果の表示
print(json.dumps(detection_results, ensure_ascii=False, indent=2))

yolov8の検出結果を表示するには?
とすると

import cv2
import matplotlib.pyplot as plt
from ultralytics import YOLO

# YOLOv8モデルのロード
model = YOLO('yolov8s.pt')  # ここで適切なモデルを選択

# 画像のロード
image_path = 'path_to_your_image.jpg'
image = cv2.imread(image_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# 画像の検出
results = model(image)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls
boxes = detections.boxes.xyxy
scores = detections.boxes.conf

# クラスラベルのマッピング
label_mapping = {
    'baskulin': 'バスクリン',
    'potato_starch': '片栗粉',
    'shampoo': 'シャンプー'
}

# バウンディングボックスとラベルの描画
for cls, box, score in zip(classes, boxes, scores):
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    x1, y1, x2, y2 = map(int, box)
    cv2.rectangle(image, (x1, y1), (x2, y2), (255, 0, 0), 2)
    cv2.putText(image, f'{label} {score:.2f}', (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 0, 0), 2)

# 結果の表示
plt.figure(figsize=(10, 10))
plt.imshow(image)
plt.axis('off')
plt.show()

となるが

バウンディングボックスとラベルを画像に描画
結果をMatplotlibを使って表示
となる

結果の表示で画像での結果表示は不要

from ultralytics import YOLO

# YOLOv8モデルのロード
model = YOLO('yolov8s.pt')  # ここで適切なモデルを選択

# 画像のロード
image_path = 'path_to_your_image.jpg'
image = cv2.imread(image_path)

# 画像の検出
results = model(image)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls
boxes = detections.boxes.xyxy
scores = detections.boxes.conf

# クラスラベルのマッピング
label_mapping = {
    'baskulin': 'バスクリン',
    'potato_starch': '片栗粉',
    'shampoo': 'シャンプー'
}

# 検出結果のテキスト表示
for cls, box, score in zip(classes, boxes, scores):
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    x1, y1, x2, y2 = map(int, box)
    print(f'検出: {label}, 信頼度: {score:.2f}, バウンディングボックス: ({x1}, {y1}, {x2}, {y2})')

バウンディングボックスの座標も不要

from ultralytics import YOLO

# YOLOv8モデルのロード
model = YOLO('yolov8s.pt')  # ここで適切なモデルを選択

# 画像のロード
image_path = 'path_to_your_image.jpg'
image = cv2.imread(image_path)

# 画像の検出
results = model(image)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls
scores = detections.boxes.conf

# クラスラベルのマッピング
label_mapping = {
    'baskulin': 'バスクリン',
    'potato_starch': '片栗粉',
    'shampoo': 'シャンプー'
}

# 検出結果のテキスト表示
for cls, score in zip(classes, scores):
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    print(f'検出: {label}, 信頼度: {score:.2f}')

次に
検出した物体のそれぞれの個数をカウントして表示

from ultralytics import YOLO
from collections import defaultdict

# YOLOv8モデルのロード
model = YOLO('yolov8s.pt')  # ここで適切なモデルを選択

# 画像のロード
image_path = 'path_to_your_image.jpg'
image = cv2.imread(image_path)

# 画像の検出
results = model(image)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls

# クラスラベルのマッピング
label_mapping = {
    'baskulin': 'バスクリン',
    'potato_starch': '片栗粉',
    'shampoo': 'シャンプー'
}

# 検出物体のカウント
object_counts = defaultdict(int)
for cls in classes:
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    object_counts[label] += 1

# 検出結果の表示
for label, count in object_counts.items():
    print(f'{label}: {count}個')

これでほぼ目的のものに近くなった

from collections import defaultdict

これは
Pythonの標準ライブラリであるcollectionsモジュールから
defaultdictクラスをインポートしています

defaultdictは、
キーが存在しない場合にデフォルト値を
自動的に提供する辞書を作成するために使用

この場合、物体のカウントを行うために使用している

ただ、在庫管理のものはどんどん追加していくため
別のファイルにして読み込むようにする

vim label_mapping.json

でJSONファイルを作成

{
    "baskulin": "バスクリン",
    "potato_starch": "片栗粉",
    "shampoo": "シャンプー"
}

次にJSON ファイルの読み込み

import json
from ultralytics import YOLO
from collections import defaultdict

# JSONファイルからクラスラベルのマッピングを読み込み
with open('label_mapping.json', 'r', encoding='utf-8') as f:
    label_mapping = json.load(f)

# YOLOv8モデルのロード
model = YOLO('yolov8s.pt')  # ここで適切なモデルを選択

# 画像のロード
image_path = 'path_to_your_image.jpg'
image = cv2.imread(image_path)

# 画像の検出
results = model(image)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls

# 検出物体のカウント
object_counts = defaultdict(int)
for cls in classes:
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    object_counts[label] += 1

# 検出結果の表示
for label, count in object_counts.items():
    print(f'{label}: {count}個')

これを書き換える

コマンドラインで動作するようにする

コマンドラインから画像ファイルを指定して使用するようには
Pythonのargparseモジュールを使用

これにより
コマンドラインから画像ファイルのパスを指定できるようになる

また使用するモデルは

inventory_model/best.pt

とする

vim count_inventory_terminal.py

で中身を

import argparse
import json
import cv2
from ultralytics import YOLO
from collections import defaultdict

# コマンドライン引数の解析
parser = argparse.ArgumentParser(description="YOLOv8 Object Detection")
parser.add_argument('image_path', type=str, help='Path to the input image file')
args = parser.parse_args()

# ラベルマッピングファイルのパス
label_mapping_path = 'label_mapping.json'

# JSONファイルからクラスラベルのマッピングを読み込み
with open(label_mapping_path, 'r', encoding='utf-8') as f:
    label_mapping = json.load(f)

# YOLOv8モデルのロード
model = YOLO('inventory_model/best.pt')  # ここで適切なモデルを選択

# 画像のロード
image = cv2.imread(args.image_path)

# 画像の検出
results = model(image)

# 検出結果の取得
detections = results[0]  # 最初の結果を取得
classes = detections.boxes.cls

# 検出物体のカウント
object_counts = defaultdict(int)
for cls in classes:
    class_label = model.names[int(cls)]
    if class_label in label_mapping:
        label = label_mapping[class_label]
    else:
        label = class_label
    object_counts[label] += 1

# 検出結果の表示
for label, count in object_counts.items():
    print(f'{label}: {count}個')

これで実行すると

python count_inventory_terminal.py data_bak/Baskulin1.jpg

0: 640x512 1 baskulin, 114.8ms
Speed: 9.4ms preprocess, 114.8ms inference, 7.5ms postprocess per image at shape (1, 3, 640, 512)
バスクリン: 1個

となって検出結果の日本語表示ができる

とりあえず対象物の日本語化とカウントができたので
次はLINEで送信機能を作成する

FaceRecognizerSFによる顔の認識の実践

FaceRecognizerSFによる顔の認識の実践

スマホの写真のサイズを1/4にすることで

generate_aligned_faces.py

による顔の切り出しが成功した

取り出した顔画像は
Face00x.jpg
となっているので、個人ごとの名前ファイルに改名する

次に
顔画像から特徴を抽出、特徴辞書として保存する

python generate_feature_dictionary.py snowpool.jpg

これで
snowpool.npy
が作成される

同様に家族分も実行する

python resize_save.py PXL_20240218_063620749.jpg

で画像ファイルを1/4にして

python generate_aligned_faces.py PXL_20240218_063620749_quarter.jpg 

で写真から顔を抽出

mv face001.jpg child.jpg

python generate_feature_dictionary.py child.jpg

これでそれぞれのnpyファイルができる

次に識別
モデルが変更になっているので

     weights = os.path.join(directory, "yunet.onnx")

    weights = os.path.join(directory, "face_detection_yunet_2023mar.onnx")

というように
指定するモデルを
変更する

編集するファイルは

face_recognizer.py

これで

python face_recognizer.py PXL_20240218_063620749.jpg 

を実行したら

OpenCV: Couldn't read video stream from file "/Users/snowpool/aw10s/face_recog/image.jpg"

となる

https://sites.google.com/iot-com.net/home/ホーム/実験室/jetson-nano/jetson-nanoのopen-cvで顔認証
によれば

#            return True, (user_id, cos_score)   ←オリジナルのtypo
            return True, (user_id, score)

とあるので

            # return True, (user_id, cos_score)
            return True, (user_id, score)

というように修正

そして
そのままだとファイル名が指定されているので
コマンドラインからファイル名を指定して実行できるように
ソースを変更する

Mainの部分を

# main関数の引数を追加 def main(image_path): # captureの初期化を変更 capture = cv2.VideoCapture(image_path)  # コマンドラインから指定された画像ファイル

として

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Face Recognition")
    parser.add_argument('image', help="Path to the image file")
    args = parser.parse_args()
    main(args.image)

と書き換える

これで再度実行してみる

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/face_recog/face_recognizer.py", line 108, in <module>
    main(args.image)
  File "/Users/snowpool/aw10s/face_recog/face_recognizer.py", line 37, in main
    files = glob.glob(os.path.join(directory, "*.npy"))
NameError: name 'directory' is not defined

となった

原因は

    directory = os.path.dirname(__file__)

を削除したことでディレクトリの指定ができなくなっていた

import os
import sys
import glob
import numpy as np
import cv2
import argparse

COSINE_THRESHOLD = 0.363
NORML2_THRESHOLD = 1.128

# 特徴を辞書と比較してマッチしたユーザーとスコアを返す関数
def match(recognizer, feature1, dictionary):
    for element in dictionary:
        user_id, feature2 = element
        score = recognizer.match(feature1, feature2, cv2.FaceRecognizerSF_FR_COSINE)
        if score > COSINE_THRESHOLD:
            # return True, (user_id, cos_score)
            return True, (user_id, score)

    return False, ("", 0.0)

# def main():
#     # キャプチャを開く
#     directory = os.path.dirname(__file__)
#     capture = cv2.VideoCapture(os.path.join(directory, "image.jpg")) # 画像ファイル
# main関数の引数を追加
def main(image_path):
    # captureの初期化を変更
    directory = os.path.dirname(__file__)
    
    capture = cv2.VideoCapture(image_path)  # コマンドラインから指定された画像ファイル

    #capture = cv2.VideoCapture(0) # カメラ
    if not capture.isOpened():
        exit()

    # 特徴を読み込む
    dictionary = []
    files = glob.glob(os.path.join(directory, "*.npy"))
    for file in files:
        feature = np.load(file)
        user_id = os.path.splitext(os.path.basename(file))[0]
        dictionary.append((user_id, feature))

    # モデルを読み込む
    # weights = os.path.join(directory, "yunet.onnx")
    weights = os.path.join(directory, "face_detection_yunet_2023mar.onnx")
    face_detector = cv2.FaceDetectorYN_create(weights, "", (0, 0))
    weights = os.path.join(directory, "face_recognizer_fast.onnx")
    face_recognizer = cv2.FaceRecognizerSF_create(weights, "")

    while True:
        # フレームをキャプチャして画像を読み込む
        result, image = capture.read()
        if result is False:
            cv2.waitKey(0)
            break

        # 画像が3チャンネル以外の場合は3チャンネルに変換する
        channels = 1 if len(image.shape) == 2 else image.shape[2]
        if channels == 1:
            image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
        if channels == 4:
            image = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR)

        # 入力サイズを指定する
        height, width, _ = image.shape
        face_detector.setInputSize((width, height))

        # 顔を検出する
        result, faces = face_detector.detect(image)
        faces = faces if faces is not None else []

        for face in faces:
            # 顔を切り抜き特徴を抽出する
            aligned_face = face_recognizer.alignCrop(image, face)
            feature = face_recognizer.feature(aligned_face)

            # 辞書とマッチングする
            result, user = match(face_recognizer, feature, dictionary)

            # 顔のバウンディングボックスを描画する
            box = list(map(int, face[:4]))
            color = (0, 255, 0) if result else (0, 0, 255)
            thickness = 2
            cv2.rectangle(image, box, color, thickness, cv2.LINE_AA)

            # 認識の結果を描画する
            id, score = user if result else ("unknown", 0.0)
            text = "{0} ({1:.2f})".format(id, score)
            position = (box[0], box[1] - 10)
            font = cv2.FONT_HERSHEY_SIMPLEX
            scale = 0.6
            cv2.putText(image, text, position, font, scale, color, thickness, cv2.LINE_AA)

        # 画像を表示する
        cv2.imshow("face recognition", image)
        key = cv2.waitKey(1)
        if key == ord('q'):
            break
    
    cv2.destroyAllWindows()

# if __name__ == '__main__':
#     main()
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Face Recognition")
    parser.add_argument('image', help="Path to the image file")
    args = parser.parse_args()
    main(args.image)


再度

python face_recognizer.py PXL_20240218_063620749.jpg 


実行したら顔の認識ができた

yolov8 を Google Colab で実行

yolov8のテスト

自動ラベルで作成したものが間違っているのか
それとも変換したのが問題なのかを知りたいので
一度試す

Colabで実験する

# Install ultralytics
!pip install ultralytics

でyolov8インストール

from google.colab import drive
drive.mount('/content/drive')

でマウント

!yolo obb train data=/content/drive/MyDrive/InventoryControl/daily_necessities_label/data.yaml pretrained=yolov8n-obb.pt epochs=100 imgsz=640 exist_ok=True

で学習

このコマンドは、YOLO (You Only Look Once) モデルを用いて物体検出の学習を行うためのものです。特に、YOLOv8n-obbモデルを用いて、向き付き境界ボックス(Oriented Bounding Boxes, OBB)を使用して物体を検出する訓練を行います。以下は各パラメータの詳細です:
* train: このオプションは、モデルを訓練モードに設定します。
* data=/content/drive/MyDrive/InventoryControl/daily_necessities_label/data.yaml: 訓練に使用するデータセットの設定ファイルのパスです。このYAMLファイルには、訓練データ、検証データのパスや、クラス名が含まれています。
* pretrained=yolov8n-obb.pt: 事前訓練済みのモデルファイル。このファイルを初期の重みとして使用して、訓練を開始します。
* epochs=100: モデルが訓練データを何回繰り返して学習するかを指定します。この場合、100回繰り返します。
* imgsz=640: 入力画像のサイズを640ピクセルにリサイズします。
* exist_ok=True: 既に訓練結果のフォルダが存在しても、エラーを出さずに上書きまたは新たに訓練を開始することを許可します。
このコマンドを実行することで、指定されたパラメータでYOLOモデルの訓練が行われ、物体検出の精度を向上させることができます。

とりあえず、バスクリンだけでなく
バスロマンも学習させる

そして肌おもいも学習させて、その状態から実行してみる

とりあえずバスロマンと肌おもいの画像からは
バスロマンをバスクリンと誤認識してるけど
カウントはできた

バスクリンの在庫を使い切ったため
バスロマンと肌おもいの写真で識別してみました

バスクリンとバスロマンを誤認識してますが
数は合っていますので
在庫管理には使えるとは思います

いっそバスロマンも学習すれば誤認識はなくなるかもしれません

以下ログと使用したテストの画像です

# Install ultralytics
!pip install ultralytics

でyolov8インストール

from google.colab import drive
drive.mount('/content/drive')

でgoogle driveマウント

!yolo obb train data=/content/drive/MyDrive/InventoryControl/daily_necessities_label/data.yaml pretrained=yolov8n-obb.pt epochs=400 exist_ok=True

で前回370程度のエポックで停止したので
今回は400にしてA100で実行

import os
import subprocess

source_file = '/content/drive/MyDrive/PXL_20240617_182349485.jpg'
# テキストファイルのパスを構築(画像ファイル名と同じ)
file_name, file_extension = os.path.splitext(source_file)
label_file_path = '/content/runs/obb/predict/labels/' + os.path.basename(file_name) + '.txt'
# ファイルの存在を確認し、存在する場合は削除
if os.path.exists(label_file_path):
    os.remove(label_file_path)
# YOLOを使用して予測を実行
!yolo obb predict model=/content/runs/obb/train/weights/best.pt source='{source_file}' save=True save_txt=True exist_ok=True
# ファイルが存在する場合のみ、テキストファイルの行数を取得して表示
if os.path.exists(label_file_path):
    num_lines = subprocess.check_output(["wc", "-l", label_file_path]).decode().split()[0]
    print("バスクリンの数は", num_lines)
else:
    print("ファイルが見つかりませんでした。")

実行結果は

Ultralytics YOLOv8.2.35 :rocket: Python-3.10.12 torch-2.3.0+cu121 CUDA:0 (NVIDIA A100-SXM4-40GB, 40514MiB)
YOLOv8n-obb summary (fused): 187 layers, 3077804 parameters, 0 gradients, 8.3 GFLOPs

image 1/1 /content/drive/MyDrive/PXL_20240617_182349485.jpg: 1024x800 143.7ms
Speed: 12.4ms preprocess, 143.7ms inference, 197.3ms postprocess per image at shape (1, 3, 1024, 800)
Results saved to runs/obb/predict
1 label saved to runs/obb/predict/labels
:bulb: Learn more at https://docs.ultralytics.com/modes/predict
バスクリンの数は 1

日用品の買い物の時に少しずつ写真を撮影し
学習データに使っていこうと思います