粒を数えてみた

以前に投稿した記事
[セルカウント健忘録](https://qiita.com/bobrock/items/732ef43ced543cc58a98)

色々と手探りで作った粒を数えるスクリプト

実際に実装してみて何とか使えるレベルまでにはもって行けたのでメモ書きとして残します。

##目的
液体中の粒子の数を数えて汚れ具合を判断する。いわゆるNAS等級ってやつ
めっちゃ高い機器を使えば精密測定は可能。
(↑もちろんこれが出来るなら作らない)

ただ、ざっくりとした判定は見本を見ての人による判定なのである意味ガバガバ。
まずは線引きしてガバガバの範囲を狭くして平準化するのが目的。

##作成環境
Windows10 64bit
Jupiter Notebook
Python 3.6.10
opencv 3.4.2.17
numpy 1.19.0
matplotlib 3.2.2

## ライブラリ
```python:cap_save.ipynb
import cv2
import sys
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import time
from datetime import datetime
```
基本的にはopencvを使った画像処理がメインなのでこのセットで使用。

##画像保存
```python:cap_save.ipynb
#撮影画像保存先設定
dir_path_class00 = '保存ディレクトリ指定/01_class00'
dir_path_class0 = '保存ディレクトリ指定/01_class01'
#…以下、必要分設定

#保存画像ファイル名
filename1 = 'original_img'#元画像
filename2 = 'processing_img'#加工画像

#保存dir作成(変更厳禁)
os.makedirs(dir_path_class00,exist_ok=True)
os.makedirs(dir_path_class0,exist_ok=True)
#…以下、必要分設定

#ファイルパス結合(元画像:1と加工画像:2保存時のファイル名とする)
base_path_00_org = os.path.join(dir_path_class00,filename1)
base_path_00_proc = os.path.join(dir_path_class00,filename2)

base_path_0_org = os.path.join(dir_path_class0,filename1)
base_path_0_proc = os.path.join(dir_path_class0,filename2)
#…以下、必要分設定

#画像保存日(※月※日※時※分)
datename = datetime.now().strftime('%m%d%H%M')

#画像保存時の名前設定(変更時は該当部分を変更する)
name1 = "cell"
name2 = ".jpg"
```
厳密に言えばISOではなく旧のNAS等級をメインに扱うので
保存先は旧規格の等級00から等級12までの保存先をそれぞれ作成しました。

一応元画像と加工画像(枠付け)を残したかったので2つ設定。

後は、ディレクトリ作成に必要なスクリプトを必要分羅列してＯＫ。

##読込画像指定
```python:cap_save.ipynb
#検出画像の読み込み(変更は末尾の～.jpgのみ)                   
img = cv2.imread('確認したい画像の保存先とファイル名を指定',1)
img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
```
取り込み画像部分を`cv2.VideoCapture()`にしても問題なく加工は出来ました。
(実際の2値化等の処理部分も変更は必要です。)
ただ、実装しようとしたPCに使用しているUSBカメラとOpencvの相性問題で認識出来なかったので
画像選択からの処理に切り替えました。
(専用ファームウェアのせい？メーカーに質問したけどいまだ未回答)

普通に市販されているUSBカメラなら問題なく動作して処理出来たので大丈夫です。
PCのカメラでも(●｀･ω･)ゞ＜ok！

##処理部分
```python:cap_save.ipynb
#画像サイズ
w, h, = (640, 480)
#倍率
mag = 1

#処理１
img_blur = cv2.GaussianBlur(img_gray,(5,5),1)
#カーネルセット
kernel = np.ones((1,1),dtype=np.uint8)
img_erode = cv2.erode(img_gray,kernel)

def onTrackbar(position):
    global threshold
    threshold = position
cv2.namedWindow("img")
threshold = 100
cv2.createTrackbar("track","img",threshold,255,onTrackbar)

n = 0

while True:
    key = cv2.waitKey(1) & 0xFF
    ret, img_th = cv2.threshold(img_blur,threshold,255,cv2.THRESH_BINARY)
    __,contours, hierarchy = cv2.findContours(img_th.astype(np.uint8),cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    img_raw = cv2.resize(img,(w*mag, h*mag))
    img_cut = cv2.drawContours(img_raw, contours, -1,(0,255,255),1)
    #print(len(contours))
    cv2.imshow("img",img_th)
    cv2.imshow("scr",img_cut)
    if key == ord('a'):
        answer = len(contours)
        print("カウント数は:",answer,"個")
    
    if key == 27:
        break
```
問題点の一つとして輪郭検出時にthresholdを指定するが固定値で行うとどうしても
画像によってばらつきキチンと輪郭を検出しない不具合があったので
threshold値をトラックバーによって変更する様に作成。

この辺りについてはUdemyの講座を参照して作成。

後は、測定機器のレンズ倍率及び画素数の関係でerode処理した方がある程度検出出来たので
追加しています。

この辺りは検出物体によって変更していけば何とかなると思います。
後は輪郭検出した際のcontoursを`len(contours)`とする事で検出個数としています。

ぶっちゃけここが一番苦労しました。
満足したかといえば検出したい画像の大外枠もカウントされてしまっているので
正直微妙ですが今の私の頭では限界ですのであきらめました。

![2020-09-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607764/62f16bf6-0053-40bf-da33-14dc628a60bb.png)

こんな感じでトラックバーを動かせば輪郭検出からのカウントがとりあえず出来ました。

##カウント数からの判定
```python:cap_save.ipynb
if answer == 1:
    cv2.imwrite((base_path_00_org + "_" + str(answer) + "cell" + "_" + datename + str(n) + ".jpg"),img)
    cv2.imwrite((base_path_00_proc + "_" + str(answer) + "cell" + "_" + datename + str(n) + ".jpg"),img_cut)
    n += 1
    print("nas等級は:00相当です。")

elif answer == 2:
    cv2.imwrite((base_path_0_org + "_" + str(answer) + name1 + "_" + datename + str(n) + name2),img)
    cv2.imwrite((base_path_0_proc + "_" + str(answer) + name1 + "_" + datename + str(n) + name2),img_cut)
    n += 1
    print("nas等級は:0相当です。")

elif 3 <= answer < 5:
    cv2.imwrite((base_path_1_org + "_" + str(answer) + name1 + "_" + datename + str(n) + name2),img)
    cv2.imwrite((base_path_1_proc + "_" + str(answer) + name1 + "_" + datename + str(n) + name2),img_cut)
    n += 1
    print("nas等級は:1相当です。")

…以下、必要分設定。

elif 5780 <= answer:
    cv2.imwrite((base_path_12_org  + "_" + str(answer) + name1 + "_" + datename + str(n) + name2),img)
    cv2.imwrite((base_path_12_proc + "_" + str(answer) + name1 + "_" + datename + str(n) + name2),img_cut)
    n += 1
    print("nas等級は:12相当です。")
    
cv2.destroyAllWindows()
```
判定の元となる閾値(設定値)についてはNAS等級のクラスを参照。
厳密に言えば検出物体の大きさ、数による判定をしない事には精密判定は不可能だが
あくまでも簡易判定、人の誤差をなるべく平準化する為の線引きが目的なので
そこまでの精度は求めていません。

ここまで走らせると、輪郭検出された物の数と判定結果を表示して
該当ディレクトリに保存される様にしました。

##まとめ
今回のスクリプトによって、具体的な数値による線引きが可能になったので
バラつきをある程度抑える事が出来たと思います。

ただ、目指す所は一発簡易判定なので
画像データをある程度蓄積させて、そのデータを元に学習データを作成して
推論による判定にもっていくのがゴール。

後、やっぱりＧＵＩを実装出来なかったのが痛い。
相変わらずtkinterが理解出来なくて泣けてきた課題でした。

以上です。