画像解析で間違い探しをしてみる

# はじめに
ふと、画像解析をやってみようと思いました。
OpenCVを使いたかったのですが、事情がありC#のOpenCvSharpで試しました。

画像解析と言えば間違い探しかなと思い、某ファミリーレストランの間違い探しを行ないます。

https://www.saizeriya.co.jp/entertainment/202411_quiz/

# 間違い探し
### STEP1. 画像を左右に分ける
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/65582e1c-7ca2-48c5-ba8b-162f3677155c.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/658774d3-7f4c-4a67-8337-defb1466c59f.png" width=40%>　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/479e7a43-b99f-42bf-8081-b352a05f96cf.png" width=40%>

```c#:画像を切り出す
using (Mat input_image = Cv2.ImRead("読み込みファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat(input_image, new OpenCvSharp.Rect(0, 0, 950, 963)))
    Cv2.ImWrite("保存ファイルパス", output_image);
```

### STEP2. 微調整
左右の画像に微妙にズレがあるので整えます。
⇒右側の画像を左側の画像に合わせます。

これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/479e7a43-b99f-42bf-8081-b352a05f96cf.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/a46f2671-34c3-4427-b2a4-8b4da3ba30f7.png" width=50%>

```c#:特徴点マッチング
using (Mat input_image1 = Cv2.ImRead("左側の画像ファイルパス", ImreadModes.Unchanged))
using (Mat input_image2 = Cv2.ImRead("右側の画像ファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    KeyPoint[] key_point1;        //比較元画像の特徴点
    KeyPoint[] key_point2;        //比較先画像の特徴点
    Mat descriptor1 = new Mat();  //比較元画像の特徴量
    Mat descriptor2 = new Mat();  //比較先画像の特徴量
    List<Point2d> refPoints = new List<Point2d>();
    List<Point2d> floatPoints = new List<Point2d>();

    DetectAKAZE(input_image1, out key_point1, out descriptor1);
    DetectAKAZE(input_image2, out key_point2, out descriptor2);

    DescriptorMatcher matcher = DescriptorMatcher.Create("BruteForce");
    DMatch[] matches = matcher.Match(descriptor1, descriptor2);

    foreach (DMatch v in matches)
    {
        refPoints.Add(new Point2d(double.Parse(key_point1[v.QueryIdx].Pt.X.ToString()), double.Parse(key_point1[v.QueryIdx].Pt.Y.ToString())));
        floatPoints.Add(new Point2d(double.Parse(key_point2[v.TrainIdx].Pt.X.ToString()), double.Parse(key_point2[v.TrainIdx].Pt.Y.ToString())));
    }
    
    Mat homo = Cv2.FindHomography(InputArray.Create(floatPoints), InputArray.Create(refPoints), HomographyMethods.Ransac);

    Cv2.WarpPerspective(input_image2, output_image, homo, mat.Size());

    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

### STEP3. 差分取得
2つの画像の差分を取ります。

これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/658774d3-7f4c-4a67-8337-defb1466c59f.png" width=40%>　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/a46f2671-34c3-4427-b2a4-8b4da3ba30f7.png" width=40%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/bd0b3f29-12d3-47c0-a3ba-682e52be4086.png" width=50%>

```c#:画像比較
using (Mat input_image1 = Cv2.ImRead("左側の画像ファイルパス", ImreadModes.Unchanged))
using (Mat input_image2 = Cv2.ImRead("右側の画像ファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    Cv2.Absdiff(input_image1, input_image2, output_image);
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

### STEP4. ノイズ除去
微妙なズレによる差分が出るので除去します。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/bd0b3f29-12d3-47c0-a3ba-682e52be4086.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/f9717071-7e35-41cd-af9d-6e0791e1dd5c.png" width=50%>

```c#:ノイズ除去
using (Mat input_image = Cv2.ImRead("読み込みファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    Cv2.MedianBlur(input_image, output_image, 5);
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

### STEP5. 二極化(色検知)
わずかに残っているノイズを除去して、モノクロにします。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/f9717071-7e35-41cd-af9d-6e0791e1dd5c.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/955fb105-453b-4f51-9214-7a636d6425b5.png" width=50%>

```c#:二極化
using (Mat input_image = Cv2.ImRead("読み込みファイルパス", ImreadModes.Grayscale))
using (Mat output_image = new Mat())
{
    Cv2.Threshold(input_image, output_image, 50, 255, ThresholdTypes.Binary);
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```
二極化によって除去したかったのですが、左下の差分も消えてしまいました。
そこで、色検出を使いました。
```c#:色検知
using (Mat input_image = Cv2.ImRead("読み込みファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    Scalar s_min = new Scalar(0, 0, 0);
    Scalar s_max = new Scalar(50, 50, 50);
    
    Cv2.InRange(input_image, s_min, s_max, output_image);
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

### STEP6. 淵の差分除去
淵の部分の差分を除去します。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/955fb105-453b-4f51-9214-7a636d6425b5.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/93a52737-d137-4df4-bc73-83e1c172f737.png" width=50%>

### STEP7. 差分範囲の膨張
差分の付近を求めたいので差分範囲を膨張させます。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/93a52737-d137-4df4-bc73-83e1c172f737.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/2ba39dd2-59ee-4693-b5d3-1b480d4d4f41.png" width=50%>

```c#:膨張
using (Mat input_image = Cv2.ImRead("読み込みファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    Cv2.Dilate(input_image, output_image, new Mat(new OpenCvSharp.Size(21, 21), MatType.CV_8UC1));
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

### STEP8. 差分範囲を縁取り
差分の範囲を囲む線を求めます。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/2ba39dd2-59ee-4693-b5d3-1b480d4d4f41.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/8105abbf-01ae-4fbc-a1be-5ec429d15898.png" width=50%>

```c#:輪郭取得
using (Mat neiborhood8 = new Mat(new OpenCvSharp.Size(3, 3), MatType.CV_8U))
using (Mat gray_image = Cv2.ImRead("読み込みファイルパス", ImreadModes.Grayscale))
using (Mat dilate_image = new Mat())
using (Mat diff_image = new Mat())
using (Mat output_image = new Mat())
{
    for (int y = 0; y < 3; y++)
        for (int x = 0; x < 3; x++)
            neiborhood8.Set<byte>(y, x, 1);

    Cv2.Dilate(gray_image, dilate_image, neiborhood8, null, 1);
    Cv2.Absdiff(gray_image, dilate_image, diff_image);
    Cv2.BitwiseNot(diff_image, output_image);

    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

### STEP9. 淵の膨張
縁取りした線を太くします。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/8105abbf-01ae-4fbc-a1be-5ec429d15898.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/d15338eb-0829-4eec-92ed-052d6edaec9e.png" width=50%>

```c#:膨張
using (Mat input_image = Cv2.ImRead("読み込みファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    Cv2.Dilate(input_image, output_image, new Mat(new OpenCvSharp.Size(21, 21), MatType.CV_8UC1));
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

### STEP10. 色変換
線の色を赤くします。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/d15338eb-0829-4eec-92ed-052d6edaec9e.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/801c8c05-5ffe-4a42-8f53-ef71f512a779.png" width=50%>

```c#
using (Mat input_image = Cv2.ImRead("読み込みファイルパス", ImreadModes.Unchanged))
{
    for (int i = 0; i < input_image.Width; i++)
        for (int j = 0; j < input_image.Height; j++)
        {
            Vec3b pix = mat.Get<Vec3b>(j, i);
            pix[0] = (byte)0;
            pix[1] = (byte)0;
            mat.Set<Vec3b>(j, i, pix);
        }

    Cv2.ImWrite("保存ファイルパス", input_image);
}
```

### STEP11. 色の反転
STEP9で作成した画像を反転します。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/d15338eb-0829-4eec-92ed-052d6edaec9e.png" width=50%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/f46ce304-1e4b-43c6-8f9b-87b73f08e4a8.png" width=50%>

```c#
using (Mat input_image = Cv2.ImRead("読み込みファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    Cv2.BitwiseNot(input_image, output_image);
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

### STEP12. 元画像に差分を描画
元画像とSTEP10の画像を合成します。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/658774d3-7f4c-4a67-8337-defb1466c59f.png" width=40%>　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/479e7a43-b99f-42bf-8081-b352a05f96cf.png" width=40%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/8426c4e4-5037-4eac-8078-959fa2a32fd9.png" width=40%>　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/3bd24629-72b3-4f3b-bdb2-cef0173de878.png" width=40%>

2段階で作成します。
```c#
using (Mat input_image = Cv2.ImRead("STEP1の画像ファイルパス", ImreadModes.Unchanged))
using (Mat diff_image = Cv2.ImRead("STEP11の画像ファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    Cv2.BitwiseAnd(input_image, diff_image, output_image);
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

```c#
using (Mat input_image = Cv2.ImRead("上記の画像のファイルパス", ImreadModes.Unchanged))
using (Mat diff_image = Cv2.ImRead("STEP10の画像ファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    Cv2.BitwiseOr(input_image, diff_image, output_image);
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

### STEP13. 画像を結合
左側と右側に分けた画像を結合します。
これを
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/8426c4e4-5037-4eac-8078-959fa2a32fd9.png" width=40%>　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/3bd24629-72b3-4f3b-bdb2-cef0173de878.png" width=40%>
↓
こう
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779420/3d2fd4b1-8983-4181-abdd-aa1546f7ae3c.png" width=80%>

```c#
using (Mat input_image1 = Cv2.ImRead("左側の画像ファイルパス", ImreadModes.Unchanged))
using (Mat input_image2 = Cv2.ImRead("右側の画像ファイルパス", ImreadModes.Unchanged))
using (Mat output_image = new Mat())
{
    Cv2.HConcat(input_image1, input_image2, output_image);
    Cv2.ImWrite("保存ファイルパス", output_image);
}
```

# おわりに
デジタル画像であれば単純に比較すれば差分が取れます。
※STEP3の時点でも目視で確認が出来ます。
次の段階として、写真から差分を取ってみます。