距離変換を用いて、画像からトマトの数を当てる

# 目標
今回の目標は、画像からミニトマトの数を判定することを目標としてプログラムをかいてみました。
現在、CNNなどの畳み込み層を用いた、深層学習によって、精度高く、画像を認識することができていますが、今回はそのような深層学習を使わずに、画像処理にてミニトマトの数を機械にわからせることを目標にしました。

簡単のためミニトマトの画像のなかでも、今回は、背景は白であるものを想定します。

今回の対象とするミニトマトの画像は以下のようなものである。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/d54fa8c9-1415-cce5-41ab-4e306c07bebf.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/fc043c0f-72a9-5e1f-845d-605e49504621.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/92165463-c556-593f-f4ea-be6302ae43c6.png)


# 処理手順の設計

処理手順は以下のようである。

1.	画像の読み込み:
•	画像ファイルを読み込む。
2.	画像のリサイズ:
•	画像のサイズを変更する。
3.	HSV色空間に変換:
•	画像をHSV色空間に変換する。HSV色空間は、色相(Hue)、彩度(Saturation)、明度(Value)の3つの成分からなる。
4.	赤色の範囲を定義:
•	HSV色空間で赤色の範囲を定義する。低い色相範囲（0〜5度）と高い色相範囲（170〜180度）をマスクする。
5.	赤色のマスクを結合:
•	2つの赤色マスクを論理和(OR)演算で結合する。
6.	S + V > 240 の条件で赤色部分をフィルタリング:
•	彩度(S)と明度(V)を加算し、その合計が240以上のピクセルを二値化する。
7.	赤色マスクと S + V 条件の AND 演算:
•	赤色マスクとS+V条件のマスクを論理積(AND)演算で結合する。
8.	緑色の範囲をRGBで定義:
•	BGR色空間において、緑色チャンネル(G)の値が,青色チャンネル(B)より20以上大きいピクセルを緑色とみなす。
9.	緑色のマスクを作成:
•	緑色部分のマスクを作成する。
10.	赤色マスクと緑色マスクを結合:
•	赤色マスクと緑色マスクを論理和(OR)演算で結合する。
11.	ノイズ除去のためのモルフォロジー変換:
•	ノイズを除去するために、クロージング(膨張して縮小)操作を繰り返す。
12.	距離変換:
•	距離変換を適用し、その点（画素）がどれだけマスクされていない（赤と緑と判定されていない）画素までの距離を計算する。
13.	距離画像を二値化:
•	距離変換した画像を二値化する。（正規化して、0.6以上にする）
14.	輪郭の検出:
•	二値化した距離画像から輪郭を検出する。
15.	面積が一定以下の輪郭を除去:
•	面積が一定以下の小さな輪郭を除去する。（ノイズとして、検出されるものは省きたいため）
16.	検出した輪郭を描画:
•	検出した輪郭を元の画像に描画する。
17.	トマトの数を画像に出力:
•	検出したトマトの数を出力する。
18.	結果を表示:
•	結果を表示する。

## 使用したアルゴリズム

今回は主にこのようなアルゴリズムを使った。それぞれわかりやすく、アルゴリズムについて解説している記事をあげておく。

モルフォロジー変換（クロージング処理）について

https://pystyle.info/opencv-morpology-operation/

距離変換について

https://pystyle.info/opencv-distance-transform/

## 工夫した点

ここで、トマトの赤の部分や、へたの緑の部分を抽出するときに、どのようにすれば抽出したらいいかは、[このサイト](https://www.rapidtables.org/ja/web/color/RGB_Color.html
)で、自分がマスクしたい画素の様子を認識しながら行った。

ここで、できるだけトマトの赤の部分だけを出力したく、影の部分はマスクしたくないために、S + V > 240 の条件で赤色部分をフィルタリングすることによって、影の部分を取り除く工夫を行った。

また、膨張（dilate）と収縮（erode）を繰り返す、モルフォロジー処理を行うことで、トマト内にできてしまう光沢の部分を、ある程度消すことができると思ったためこの処理を行った。

また、単純にマスクが画像の輪郭検出するのではなく、距離変換することにより、もしマスク画像で、トマトがくっついていた場合にも、別のトマトと判断できるようにしたいため、この処理を行った。また距離画像の二値化では、正規化して、あるしきい値でもって二値化を行うようにした。


# 作成したプログラム


```c++
#include <opencv2/opencv.hpp>
#include <vector>
#include <iostream>

using namespace cv;
using namespace std;

int main() {
    // 画像の読み込み //自分が読み込みたい画像を指定によって変える
    Mat image = imread("tomato1.jpg");

    // 画像が読み込めなかった場合のエラーメッセージ
    if (image.empty()) {
        cerr << "Error loading image" << endl;
        return -1;
    }

    // 画像のサイズを変更
    resize(image, image, Size(600, 600));

    Mat hsv, mask1, mask2, redMask, greenMask, sumSV, combinedMask, morph, distTransform, distThresholded;

    // 1. HSV色空間に変換
    // 画像をHSV色空間に変換する。HSV色空間は、色相(Hue)、彩度(Saturation)、明度(Value)の3つの成分
    cvtColor(image, hsv, COLOR_BGR2HSV);

    // 2. 赤色の範囲を定義
    // HSV色空間における赤色の範囲を定義する。低い色相範囲（0〜5度）と高い色相範囲（170〜180度）をマスクする。
    int lowH1 = 0, highH1 = 5;
    int lowH2 = 170, highH2 = 180;
    inRange(hsv, Scalar(lowH1, 50, 0), Scalar(highH1, 255, 255), mask1);
    inRange(hsv, Scalar(lowH2, 50, 0), Scalar(highH2, 255, 255), mask2);

    // 3. 赤色のマスクを結合
    // 2つの赤色マスクを論理和(OR)演算で結合する。
    redMask = mask1 | mask2;
    imshow("redMask", redMask);

    // 4. S + V > 240 の条件で赤色部分をフィルタリング
    // 彩度(S)と明度(V)を加算し、その合計が240以上のピクセルを二値化する。
    vector<Mat> hsvChannels;
    split(hsv, hsvChannels);
    int sumSVThreshold = 240;
    sumSV = hsvChannels[1] + hsvChannels[2];
    threshold(sumSV, sumSV, sumSVThreshold, 255, THRESH_BINARY);
    imshow("sumSV", sumSV);

    // 5. 赤色マスクと S + V 条件の AND 演算
    // 赤色マスクとS+V条件のマスクを論理積(AND)演算で結合する。
    bitwise_and(redMask, sumSV, redMask);

    // 6. 緑色の範囲をRGBで定義
    // BGR色空間において、緑色チャンネル(G)の値が,青色チャンネル(B)より20以上大きいピクセルを緑色とみなす。
    vector<Mat> rgbChannels;
    split(image, rgbChannels);
    greenMask = (rgbChannels[1] > (rgbChannels[0] + 20));
    greenMask = greenMask * 255;
    imshow("Green Mask", greenMask);

    // 7. 赤色マスクと緑色マスクを結合
    // 赤色マスクと緑色マスクを論理和(OR)演算で結合する。
    bitwise_or(redMask, greenMask, combinedMask);
    imshow("Combined Mask", combinedMask);

    // 8. ノイズ除去のためのモルフォロジー変換
    // ノイズを除去するために、オープニングとクロージング操作を繰り返す。
    morphologyEx(combinedMask, morph, MORPH_CLOSE, getStructuringElement(MORPH_ELLIPSE, Size(10, 10)));
    morphologyEx(morph, morph, MORPH_CLOSE, getStructuringElement(MORPH_ELLIPSE, Size(10, 10)));
    morphologyEx(morph, morph, MORPH_CLOSE, getStructuringElement(MORPH_ELLIPSE, Size(10, 10)));
    imshow("Morphology Image", morph);

    // 9. 距離変換
    // 距離変換を適用し、物体の中心からの距離を計算する。
    distanceTransform(morph, distTransform, DIST_L2, 5);
    normalize(distTransform, distTransform, 0, 1.0, NORM_MINMAX);//正規化する
    imshow("Distance Transform", distTransform);

    // 10. 距離画像を二値化
    // 距離変換した画像を二値化する。//ここではしきい値は60%に設定している。（正規化しているので）
    int distThreshold = 60;
    threshold(distTransform, distThresholded, distThreshold / 100.0, 1.0, THRESH_BINARY);
    imshow("Distance Thresholded", distThresholded);
    distThresholded.convertTo(distThresholded, CV_8U);
    

    // 11. 輪郭の検出
    // 二値化した距離画像から輪郭を検出する。
    vector<vector<Point>> contours;
    findContours(distThresholded, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

    // 12. 面積が一定以下の輪郭を除去
    // 面積が一定以下の小さな輪郭を除去する。
    double minArea = 20; // 面積の閾値
    contours.erase(remove_if(contours.begin(), contours.end(),
        [minArea](const vector<Point>& contour) {
            return contourArea(contour) < minArea;
        }),
        contours.end());

    

    // 13. 検出した輪郭を描画
    // 検出した輪郭を元の画像に描画する。
    Mat contourImage = image.clone();
    for (size_t i = 0; i < contours.size(); ++i) {
        drawContours(contourImage, contours, static_cast<int>(i), Scalar(0, 255, 0), 3);
    }

    // 14. トマトの数を画像に表示
    // トマトの数を取得する
    int tomatoCount = contours.size();
    // トマトの数を画像に描画
    string text = "Number of tomatoes: " + to_string(tomatoCount);
    putText(contourImage, text, Point(10, 30), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 0, 255), 2);

  
    // 15. 結果を表示
    // 結果を表示する。
    imshow("Detected Tomatoes", contourImage);
    waitKey(0);
    destroyAllWindows();

    return 0;
}
```

OpenCVのversionは　OpenCV 3.4.5　です。
それ以外でも動くと思います。

# 結果

入力画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/e161b49b-e438-9e6b-ffc0-6dcd93faf75b.png)


赤の部分を抽出したマスク画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/a7df5c67-d10c-44e8-3bd0-4c368da9fb5e.png)


影の部分以外を抽出したマスク画像

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/ec8ee849-848b-8035-9a73-54ba1e1a1cc9.png)

緑の部分を抽出したマスク画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/c6de09a4-2400-a1da-69cb-abfd4ce59875.png)

トマトの部分を抽出したマスク画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/e1fe304f-a8d3-4393-4024-274601f37c46.png)

モルフォロジー処理後の画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/3d71ce2e-2bb3-68cd-d7b0-25d463ca5bfc.png)

距離変換後の画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/717f3fec-c29e-48ce-beb7-12d9d228f2df.png)

距離変換後の二値化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/be810978-e18d-17c4-41b1-405bcf81f2e8.png)

最終的な結果の画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/478803b1-d7d6-9cc0-b56d-f20c22bb0753.png)

上手く判別できています。

ここからは**最終的な結果のみ**載せていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/b9d4b763-b8e8-ad8b-9aff-f02fe50d1bda.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/aa79a4ed-142d-9175-00bb-b12787208667.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/fa67cbc4-1158-16ee-6a4d-c822b4a22b3c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/85c66472-aaa8-be48-5ced-2e839be6bfc5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/9778fe0a-53af-cd8a-cded-02e1aa610c95.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/7fa8c584-d60a-72df-707a-31e7f27a00c0.png)

こんな感じである程度、ミニトマトどうしが近くてもきちんと判別できています。

ちなみに、似たようなイチゴやさくらんぼでも判別できました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/9443f948-dae7-ef16-6afc-956bff2870cc.png" width=50%>

<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/e6d2a8f0-2455-2902-1ca5-a26a3c5e1ecf.png" width = 50%>

<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/f71088cb-c86d-20c1-45c6-2048bdea44a5.png" width = 50%>


**上手くいかなった画像**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/4dcc5306-478a-d3a5-c734-2fe8c4333c8b.png)

距離が近すぎたりしたらこの手法だと限界がありました。


クロージング処理でほぼひとかたまりになってしまったことが原因ですね
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/eec43c96-4a6c-66ac-c1e1-919a79fa8a4c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/a879962b-4f3e-236f-2104-7b76c5bcb8a3.png)


# 考察、感想

今回の実験では、全体的にうまくトマトの数を判定できました。モルフォロジー処理（クロージング処理※膨張して収縮）によってノイズが除去できたため、距離変換の際に正確に判断できたと思います。ミニトマト同士がくっついていても別のものと判別できたのは距離変換を挟んだためできました。この二つの処理を施すことで精度が上がったと考えられます。

今回のプログラムでミニトマトの数を判定することができましが、背景が白でない場合やトマトが重なっている場合など、今後の課題が残ります。特に背景が緑や赤の場合にはうまく判定できないだろうと推定できます。

ちなみに、ChatGPT4-oに画像をアップロードして何個写っているか答えられるのか試した所、現段階（2024/8/10）では、トマトの数が少なければあっていることが多いが、トマトの数が増える（5以上になる）と急激に精度が落ちました。これは、画像を違う方法で認識しているからだと思われます。

今後の展望として、背景が何であってもトマトの数を判別できるプログラムを作りたいです。それには、大量に画像を用意して深層学習技術を使ってなど上手く使いで学習させるといけたりするのではないかなと思っています。