Vison Framework を利用することで簡単に円形度の計算ができたのでその紹介です。
サンプルとしてエゴマと蕎麦の実の形状の違いを円形度で比較してみます。
<完成イメージ>
円形度が高いと青いパス、低いと赤色のパスで描画しています。
より円に近いエゴマの実が青くなっています。
1. 円形度
円形度は次のように計算できます。
$$
円形度=\frac{4 \pi S}{L^2}
$$
Sが面積でLが周囲長です。真円が1となり最大で、複雑な形状ほど値は小さくなります。
2. 円を認識させるステップ
次のステップで処理します。
- 画像の前処理
- 輪郭検出
- 円形度の計算
2-1) 画像の前処理
円を認識しやすいように事前に次のような加工をしました。
エッジをきれいに認識させる&ノイズを除去する、が目的です。
1.グレースケール化 | 2.鮮鋭化 | 3.二値化 | 4.モルフォロジー処理 |
---|---|---|---|
![]() |
![]() |
![]() |
![]() |
1.グレースケール化
処理の理解をシンプルにしたいのでカラー画像をグレースケールに変換します。
ここではRGB→YUVの変換時に用いる輝度変換の計算をCIColorMatrixフィルターを使って実装しています。
// グレースケール化
let colorMatrixFilter = CIFilter.colorMatrix()
colorMatrixFilter.inputImage = ciImage
let grayScaleVector = CIVector(x: 0.298912, y: 0.586611, z:0.114478, w: 0)
colorMatrixFilter.rVector = grayScaleVector
colorMatrixFilter.gVector = grayScaleVector
colorMatrixFilter.bVector = grayScaleVector
colorMatrixFilter.aVector = CIVector(x: 0, y: 0, z: 0, w: 1)
let grayScaleImage = colorMatrixFilter.outputImage
2.鮮鋭化
対象の色と背景色が似ていると思ったような輪郭検出ができなかったので、対象のエッジが際立つように鮮鋭化を行います。CIFilterにアンシャープマスキングを行うフィルタCIUnsharpMaskがあるのでこれを利用します。
// 鮮鋭化
let unSharpMaskFilter = CIFilter.unsharpMask()
unSharpMaskFilter.inputImage = grayScaleImage
unSharpMaskFilter.radius = 5.0
unSharpMaskFilter.intensity = 2.0
let unSharpMaskImage = unSharpMaskFilter.outputImage
※VNDetectContoursRequest
には輪郭検出時のコントラストを調整する contrastAdjustment
プロパティがありますがこちらは試していません。
3.二値化/モルフォロジー処理
白黒画像に変換、および、小さな黒い画素(ノイズ)を削除します。
処理方法は次の記事で解説しているので興味のある方は参照ください。
【参考】iOSで 光学迷彩
2-2) 輪郭検出
輪郭検出にはVision FrameworkのVNDetectContoursRequestを使います。
利用方法はとても簡単です。次のようにCIImageを渡すだけで結果が得られます。
// 輪郭検出
private func getContours(ciImage: CIImage) -> [VNContour] {
let contourRequest = VNDetectContoursRequest.init()
contourRequest.maximumImageDimension = 1024
try? VNImageRequestHandler(ciImage: ciImage).perform([contourRequest])
// 検出結果取得
guard let observation = contourRequest.results?.first else { return [] }
// 外側の輪郭だけ返す
return observation.topLevelContours
}
検出された輪郭は次のようなネスト構造になっています。
今回は蕎麦やエゴマの実の内部の輪郭は不要で、外側の形状だけ知りたいので一番外側の輪郭が格納されているtopLevelContours
を取得します。
VNDetectContoursRequestを使った輪郭検出ついては次の記事でも解説しています。
【参考】ARKit+Vision+iOS14 で らくがき のジオメトリ化①【輪郭検出】
2-3) 円形度の計算
VNGeometryUtils
を使うことで簡単に面積と周囲長が得られます。
次のように引数に輪郭検出で得られたVNContour
を与えるだけです。
円形度はその値を使って計算します。
// 面積
var area: Double = 0.0
try VNGeometryUtils.calculateArea(&area, for: contour, orientedArea: false)
// 周囲長
var perimeter: Double = 0.0
try VNGeometryUtils.calculatePerimeter(&perimeter, for: contour)
// 円形度
var roundness = (4.0 * .pi * area) / (perimeter * perimeter)
ちなみにVNContour
から輪郭を構成する点の座標を配列で取得することができるので次の計算式で自分で面積を求めることもできます。
\begin{align}
S=\frac{1}{2}\left| \sum_{ i = 1 }^{ n } (x_{i}y_{i+1} - x_{i+1}y_{i}) \right| \\
i=n のとき、n+1 = 1
\end{align}
VNContour
のnormalizedPoints
プロパティから座標の配列が[simd_float2]で得られるので、次の処理で面積、周囲長、円形度を計算できます。
func calcRoundness(points: [simd_float2]) -> (area: Float, perimeter: Float, roundness: Float) {
var area: Float = 0
var perimeter: Float = 0
for i in 0..<points.count {
let point = points[i]
let nextPoint: simd_float2
if i == (points.count - 1) {
nextPoint = points[0]
} else {
nextPoint = points[i + 1]
}
// 面積。2点の外積のz成分(後で半分にする)
area += simd_cross(point, nextPoint).z
// 周囲長
perimeter += simd_distance(point, nextPoint)
}
area = abs(area / 2)
// 円形度
var roundness = (4.0 * .pi * area) / (perimeter * perimeter)
if roundness.isInfinite || roundness.isNaN {
roundness = 0
}
return (area, perimeter, roundness)
}
3. 真円は1になるか?
調整すればほぼ1になります。
調整前 | 調整後 | さらに調整後 | |
---|---|---|---|
maximumImageDimension | 1,024 | 512 | 512 |
polygonApproximation | - | - | 0.002 |
結果 | ![]() |
![]() |
![]() |
円形度 | 0.895 | 0.928 | 0.993 |
- 解像度が高いと輪郭部分のノイズ(小さな凸凹)がより多くなり、結果として周囲長が長くなり、円形度が小さくなります【調整前】。※最初、原因がわかりませんでした。
- 解像度を下げてもノイズが残るため周囲長が長くなる傾向があります【調整後】。
-
VNContour
に用意されているpolygonApproximation
というノイズを除去する仕組みを利用するとほぼ1にすることができます【さらに調整後】。なお、この機能でノイズを消してしまうと形状の違いの差も小さくなってしまうので注意が必要です。
let contour = try noisyContour.polygonApproximation(epsilon: 0.002)
- ノイズを減らすには強い照明も必要です。この十円玉の画像は四方から明るいLEDライトを当てて撮影しています。
4. 最後に
円形度だけだとエゴマと蕎麦の実も思いのほか似ていました。
クラス識別をするなら面積、色といった特徴量を加えたパターン認識が必要だと思います。
記事内容の誤りや改善箇所があればご指摘いただけると幸いです。
【12/22追記】
手持ちの実の円形度と面積を調べたところ次のようになりました。
サンプルサイズ:エゴマ=159, 蕎麦=117
面積だけで識別できそうですが、この記事はVision Frameworkを使って円形度を取得する方法の記事なので、この事実は傍におきます。