被写体までの距離と画像から算出される面積の関係

画像上での被写体のサイズはカメラ-被写体間の距離に依存します。例えば被写体がカメラの近くに存在するときは大きく写ります。このことは、植物の非破壊によるフェノタイピングを行うため、例えば植物を上から取った画像から葉面積を算出するといった際に大きな影響を及ぼしうると思います。そこで、今回はカメラと被写体の距離が算出される面積にどう影響するかを知るためにごく簡単な実験をしてみました。

前提技術

画像から植物個体を検出する(セグメンテーションする)方法については例えば、https://axa.biopapyrus.jp/ia/opencv/threshold.htmlhttps://www.ncbi.nlm.nih.gov/pmc/articles/PMC5422159/ などがあります。また、画像中にサイズ既知の物体を配置して現実の単位(例えばcm2)で長さや面積等を算出する方法としては例えば https://pyimagesearch.com/2016/03/28/measuring-size-of-objects-in-an-image-with-opencv/ があります。

方法

植物の画像を撮るとき、個体毎に基準面(スケールを置く面)とカメラの位置を変えるのは面倒で固定されることがよくあると思うので今回もその条件で画像を取得しました。図にすると以下のような形になります。

画像取得の模式図

カメラの高さは大体ですが55cmくらいのところに設置しています。

被写体は簡単のため、竹ひごと画用紙、針金で作ったもの(写真)を使いました。葉の裏には針金を通して曲げられるようにしました。今回は写真のように完全に平らな状態と葉を上に30度ほど曲げた場合を検証しました。

被写体植物モデル

竹ひごの長さを5cm, 10cm, 15cm, 20cmと変えて被写体となる植物が個体ごとに葉を展開している高さが異なることを表現しました。このモデルの測定対象となる部分の実際の面積はスキャナで測り、約154cm2であることを確認しました。

先の模式図に記載した数式から分かる通り、実際に置いたスケールで計算される長さ(半径)はカメラ-被写体間の距離(d)に反比例しており、面積はその2乗に反比例することが予想されます。

サイズ既知の物体としてはQRコードを画面内に置くことにしました。このQRコードは1辺がちょうど5cmだったのでそれを利用します。

結果

早速結果です。

import cv2
import glob
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import re

detector = cv2.QRCodeDetector()
real_qr_edge_length = 5 # cm
records = []
images = []
for file_path in sorted(glob.glob("data/distance/IMG_1*")):
    img = cv2.imread(file_path)

    if img is None:
        print(f"cannot read {file_path}")
        continue

    # detect green rigion
    lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab)
    _, a, _ = cv2.split(lab)
    a = cv2.GaussianBlur(a, (5, 5), 10)
    _, mask = cv2.threshold(a, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    
    
    leaf_area = cv2.countNonZero(mask)
    
    rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    
    # contours for detected region
    contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    cv2.drawContours(rgb, contours, -1, (255, 0,0), 20)

    ## QR code detection and scale calculation
    has_detected, decoded_info, points, straight_qrcode = detector.detectAndDecodeMulti(img)
    assert(len(decoded_info) == 1)
    decoded_info = decoded_info[0]
    points = points[0]
    

    if has_detected:
        qr_edge_lengths = []
        for i in range(len(points)):
            pt1 = points[i]
            pt2 = points[(i + 1) % len(points)]
            cv2.line(rgb, 
                     np.round(pt1).astype(int), 
                     np.round(pt2).astype(int),
                     color=(255, 0, 0), thickness=20)
            qr_edge_lengths.append(np.linalg.norm(pt1 - pt2))
    else:
        raise RuntimeError("Cannot detect QR code")
        
    cm_per_px = real_qr_edge_length / np.mean(qr_edge_lengths)
    height = int(re.match('\d+', decoded_info).group(0))
    bent = "bent" in decoded_info
    records.append({
        "plant height": height,  # 葉が展開されている高さ
        "bent": bent,            # 葉を曲げたかどうか
        "area(px)": leaf_area,   # 面積(単位 px)
        "area(cm^2)": leaf_area * cm_per_px * cm_per_px, # 面積(単位 cm^2)
        "QR size(px)": np.mean(qr_edge_lengths),         # QRコードの1辺の長さ(単位 px)
    })
    images.append((decoded_info, rgb))
fig, ax = plt.subplots(2, 4, figsize=(8, 6))
for ax_i, (info, img) in zip(ax.ravel(), images):
    ax_i.imshow(img)
    ax_i.set_title(info)
    ax_i.axis("off")
plt.show()

検出の様子

df = pd.DataFrame().from_records(records)  
df
plant height bent area(px) area(cm^2) QR size(px)
0 5 True 584740 178.169114 286.441010
1 10 True 739358 224.879806 286.696350
2 15 True 958015 291.909991 286.438690
3 20 True 1330709 405.963094 286.265015
4 5 False 584216 178.256865 286.242157
5 10 False 726138 221.730918 286.132019
6 15 False 938969 285.376689 286.804779
7 20 False 1244625 377.349463 287.155670
height_camera = 55
d = height_camera-df['plant height']
fig, ax = plt.subplots(figsize=(8, 6))
ax.grid()
scatter = ax.scatter(d, df['area(cm^2)'], c=df['bent'], s=100)
ax.legend(handles=scatter.legend_elements()[0], labels=['flat', 'bent'], fontsize=15)
ax.set_xticks([35, 40, 45, 50])
ax.set_xlabel("distance[d](cm)", fontsize=15)
ax.set_yticks([200, 300, 400])
ax.set_ylabel("area(cm^2)", fontsize=15)
ax.tick_params(labelsize=15)

plt.show()

カメラから被写体までの距離と算出される面積の関係

考察

なんとなくですが面積は被写体の距離の2乗に反比例していそうです。また、葉を曲げた場合とそうでない場合の差は被写体との距離が近いほど大きくなります。以上から、植物の高さや、葉の展開角度等の影響を最小限にとどめて葉面積を算出するためにはカメラと被写体の間の距離を十分保つことだ重要だと言えそうです。今回はカメラの位置を55cmとかなり低めに設定していますが、これをより高い位置にすることで、カメラと被写体との距離が十分に確保でき、植物の個体差による計測のブレを小さくすることができるかと思いました。

とはいえ被写体がロゼット型の植物だったりして植物体の高さに個体差があまりない場合はそこまで深刻に考えなくてもいいのかもしれません。

理想は画像取得毎に被写体までの距離を測って補正するのがいいと思っています。これは最近のiPhoneとかであれば、LiDAR搭載されていたりするので割と現実的だと思っているのですが如何せんiOSアプリを開発したことがないので誰か作ってくれないかなあと思っているところです。

参考