からあげ博士の日常と研究と

博士課程を満期退学した人が好きなことを好きなままに書くところ。

マクロレンズとpythonで100円玉の直径を求めてみる

 こんにちは。からあげ博士(@phd_karaage)です。マクロレンズってなんだかんだ使っちゃいますよね。寄れるし。ただバリバリの等倍を使ったことがあるかと言われるとすごく微妙です。まずそんなに寄って撮ることが自分の場合はまずないですからね。

 という訳で、せっかく等倍まで寄れるのだからたまにはその性能を生かしてあげよう。ついでになんか面白い解析ができないかなというのが今回の記事の趣旨になります。

 我が家にあるマクロレンズたち。ジャンクもの揃いですが一応写る。

目次

等倍マクロって?

 マクロレンズの等倍ってなんぞやという話ですが、実物大の大きさのまま撮像面(CMOSセンサーやフィルム)に写すことができることを言います。この写真を見ていただくと分かりやすいのではないでしょうか。

 なんやただの定規じゃないか、という感じですが重要なのは写っている領域です。3cm~6.5cmの部分まで写っていますね。ということはこの画面上(すなわちセンサー)に横3.5cmの範囲が写っていることになります。これを撮影したα7Rのセンサーサイズはフルサイズ、すなわち35.9×24.0mmですから、センサーに写っているサイズと定規の写っている大きさが等倍であると言うことができますね。

 9mmぶんどこに行ったんだ?という話は恐らく「誤差」と、「総画素数、有効画素数」の違いによるものだと考えられますね。特に後者は有効画素数が総画素数より少ないことでごくごく一部クロップされますから撮像面が小さくなるのは納得です。

 あまり誤差を生じさせないように三脚まで立てて頑張った。机の上が汚いのがバレバレですね。

 このマクロレンズを使って等倍でフィギュアを撮影するとなんかポートレートっぽく撮影することもできます。

 ハーフマクロでもこんな感じ。ハーフマクロだと撮像面に1/2サイズで写ります。

 このフィギュアだとハーフマクロくらいがちょうどいいですね。

100円玉を等倍で撮影する

 等倍で撮影するにはレンズのピントリングを最短撮影距離にセットして、カメラを動かしながら撮影することになります。実際には三脚のエレベーターで微調整する訳ですが。

 かなり綺麗に撮影できたのでちょっと加工で赤線を引いています。

 ゴミが写っていたり、影ができていたりと解析するにはちょっと嫌な感じですがとりあえず解析を進めてみましょう。

python + OpenCVで二値化

www.phd-karaage.com

 基本的にはこの記事で取り扱った解析を同じように行っていくだけですね。

import cv2
import numpy as np
import matplotlib.pyplot as plt

path = "E://DSC09283.JPG"

img = cv2.imread(path)

 同様に読み込んだうえで二値化を行っていきます。単純なグレースケールで2値化を行うとどうなるのでしょうか。正直嫌な予感しかしませんね。

img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, th = cv2.threshold(img_gray, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)

plt.imshow(th)

 結果は案の定という感じで陰になっている部分がかなり取れてきていますね。これはよくない。

 ということで、Lab色空間に変えて試してみましょう。

img_lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab)
img_l = img_lab[:, :, 0]
img_a = img_lab[:, :, 1]
img_b = img_lab[:, :, 2]

_, th_l = cv2.threshold(img_l, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)
_, th_a = cv2.threshold(img_a, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)
_, th_b = cv2.threshold(img_b, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)

plt.imshow(th_l)
plt.imshow(th_a)
plt.imshow(th_b)

 結果はそれぞれこんな感じ。

 左からL, a, bでトライした結果。Lはグレースケールでやったときと大して変わらず。aは結構ノイズが乗っていますね。bが比較的マシかなという感じ。とりあえずbでやってみましょうか。

cv2.findContoursで輪郭抽出

 それでは100円玉の輪郭を抽出していきましょう。面積からアプローチもできますが、完全に100円玉を切り抜けている訳ではないので輪郭からアプローチしていきましょう。

contours, _ = cv2.findContours(th_b, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

zero = np.zeros(img.shape, dtype = "uint8")
cnt = np.asarray(0)

for i in range(len(contours)):
    contour_i = contours[i]
    contour_count = np.sum(contour_i.shape)
    if contour_count > np.sum(cnt.shape):
        cnt = contour_i


img_cont = cv2.drawContours(zero, [cnt], 0, (0, 255, 0), 3)
plt.imshow(img_cont)

 輪郭抽出は以前紹介した方法と変わりませんね。一番大きい輪郭を取ってくるという方法を使っています。結果を見てみましょう。

 輪郭は取れているけれど……ちょっと微妙な感じですね。これで円周を取ってくる、すなわちcv2.arcLengthなんかを使おうものなら結果が変わってしまうのが目に見えますね。

 もともとの画像に重ねてみてみましょう。

img_cont_2 = cv2.drawContours(img, [cnt], 0, (0, 255, 0), 3)
plt.imshow(img_cont_2)

 影はうまく避けているけど、ちょっと100円玉の内側部分を取ってきている感じがしますね。

最小外接円を求める

 100円玉はギザギザもあるし、あらゆる人の手に渡るタイミングで周囲が削れることから真円ではないことが想定されますが、とりあえず円を仮定します。矩形の最小外接円もOpenCVで簡単に求まります。関数はcv2.minEnclosingCircle()です。引数に輪郭を入れてあげることで最小外接円を求めてくれます。返り値は座標(x,y)と半径です。

(x, y), rad = cv2.minEnclosingCircle(cnt)

print(x, y)

 この時返り値の中心座標、x,yはカッコで括ってあげる必要がある点に注意が必要です。

 返り値は整数値ではないので、整数値にしてプロットしてみましょう。円を描くのもcv2.circle()で行うことができます。

center = (int(x), int(y))
rad_int = int(rad)

zero = np.zeros(img.shape, dtype = "uint8")
img_circle = cv2.circle(zero, center, rad_int, (0,255,0), 10)

plt.imshow(img_circle)

 半径も同じように整数値にしてあげて、cv2.circleに投げてあげます。そうすると綺麗な円が描けていることが分かりますね。

 もともとの画像に重ねてみましょうか。

img = cv2.imread(path)
img_circle_2 = cv2.circle(img, center, rad_int, (0,255,0), 10)

plt.imshow(img_circle_2)

 ほぼほぼ100円玉をトレースできていることが分かりますね。

ピクセル半径から直径を求める

 という訳で外接円の半径から直径を求めましょう。radに半径が入っていますが、この値はピクセルにおける半径を表しています。ということはピクセルとmmの単位換算が必要ですね。

yoko = img.shape[1]

mm = yoko / 35
print(mm)

rad_mm = rad / mm
print(rad_mm)

dia = rad_mm * 2
print(dia)

 画像の横幅は今回7360ピクセル、その幅に35mm写っている訳ですから、1mmが何ピクセルかは単純な割り算で求めることができますね。これを使って半径が何mmか求めて直径はその2倍ですから、あとは簡単ですね。pythonを電卓のように使うだけです。

 という訳で得られた結果はこちら。

 21.47mmという結果になりました。実際の値はどれくらいでしょうか?

ja.wikipedia.org

 Wikipedia曰く22.6mmとのことで、1mm程度の誤差が生じました。うーむ……。

 こうして見てみると最小外接円がちょっと内側に推定されているかなという感じがします。1mm差が生じるのに210ピクセルですからもう少し追い込めばもっと精度高く出そうですね。

ちょっとの測定誤差を許容できるならちゃんと使える測定法

 今回はマクロレンズの等倍を使うということでトライしてみましたが、別に等倍である必要はなく実際のスケールと撮像倍率の対応が取れればこの計測法はどんなものにも使うことができますね。今回は100円玉という簡単なものでトライしましたが、顕微鏡画像を使った表現型解析や、実際の測量なんかでもこうした画像解析は使われています。

 今回はpythonを使いましたが、ImageJでも同様のことができますね。実際学部の卒業研究ではImageJを使っていました。

 もちろん1つ1つ丁寧にノギスで測ればもっと精度が高まるのでしょうが、そのデータ処理(入力、データ成形)なんかを考えると画像解析も侮れません。これが1個の100円玉の計測だったら手間を考えてノギスで測れよ。という話になりますが100個の100円玉となるとちょっと難しい。

 そういう意味でもこんな感じで効率化できそうだよね。というお話でした。