引言

我们在图像处理的任务中,常常需要对某些形状区域进行描述,比如形状的质心、面积、方向等等。还需要为形状选取合适的特征描述符,用于进行形状的分类任务等等。

图像矩就是用于分析、描述分割后的形状的一种经典方法。

所以,本文会整理下OpenCV是如何定义矩、如何计算矩、如何应用矩的相关知识点,并配上原理公式和代码,方便你用的时候进行查阅。
在这里插入图片描述
首先,你需要理解,矩的定义是什么?

矩的定义

在图像处理、计算机视觉和相关领域中,图像矩(image moments)定义为图像像素强度的某个特定加权平均值(矩),或者是此类矩的函数。

图像矩通常用于分析、描述分割后的形状。

通过图像矩可以获取一些形状的简单属性,比如面积、质心和方向信息。

对于像素强度为 I ( x , y ) I ( x , y ) I(x,y) 的标量(灰度)图像,原始图像矩 M i j M_{ij} Mij 由下式计算:
M i j = ∑ x ∑ y x i y i I ( x , y ) M_{ij} = \sum _{x}\sum_{y} {x^i y^i I ( x , y )} Mij=xyxiyiI(x,y)

图像的中心矩定义为:
μ p q = ∑ x ∑ y ( x − x ˉ ) p ( y − y ˉ ) q I ( x , y ) \mu_{pq}=\sum_x \sum_y (x-\bar{x})^p(y-\bar{y})^q I(x,y) μpq=xy(xxˉ)p(yyˉ)qI(x,y)
通过证明可得:中心矩 μ p q \mu_{pq} μpq平移不变的。

你现在了解了矩的计算原理,那么在实际应用中,我们可以利用 OpenCV 的现成函数直接计算得出矩。

OpenCV中的矩(moments)

OpenCV 采用以下函数计算向量形状或栅格形状的矩,结果返回在结构cv::Moments中。

轮廓的矩以相同的方式定义,但使用格林公式计算

C++:

#include <opencv2/imgproc.hpp>
Moments cv::moments(InputArray 	array,bool binaryImage = false)		

Python:

import cv2 as cv
cv.moments( array[, binaryImage] ) ->	retval
  • 参数
    array:栅格图,单通道、8位或2D点阵列(Point2f)
    binaryImage:如果为true,图像的非零像素会被当做1,这个参数仅在处理images时使用。

  • 返回
    moments.

OpenCV中的 Moments 包含以下矩定义:

// 空间矩 spatial moments
double  m00, m10, m01, m20, m11, m02, m30, m21, m12, m03;
// 中心矩 central moments
double  mu20, mu11, mu02, mu30, mu21, mu12, mu03;
// 中心归一化矩 central normalized moments
double  nu20, nu11, nu02, nu30, nu21, nu12, nu03;
  • 空间矩的计算:
    m j i = ∑ x , y ( array ( x , y ) ⋅ x j ⋅ y i ) \texttt{m} _{ji}= \sum _{x,y} \left ( \texttt{array} (x,y) \cdot x^j \cdot y^i \right ) mji=x,y(array(x,y)xjyi)

  • 中心矩的计算:
    mu j i = ∑ x , y ( array ( x , y ) ⋅ ( x − x ˉ ) j ⋅ ( y − y ˉ ) i ) \texttt{mu} _{ji}= \sum _{x,y} \left ( \texttt{array} (x,y) \cdot (x - \bar{x} )^j \cdot (y - \bar{y} )^i \right ) muji=x,y(array(x,y)(xxˉ)j(yyˉ)i)
    任何阶的中心矩都有平移不变性。

  • 中心归一化矩的计算:
    nu j i = mu j i m 00 ( i + j ) / 2 + 1 \texttt{nu} _{ji}= \frac{\texttt{mu}_{ji}}{\texttt{m}_{00}^{(i+j)/2+1}} nuji=m00(i+j)/2+1muji
    中心归一化矩还具有尺度不变形。

OpenCV中的Hu不变矩(HuMoments)

Hu不变矩 (Hu invariants) 是对平移、缩放、镜像和旋转都不敏感的7个二维不变矩的集合。常被用作于特征描述符。

C++:

void HuMoments(const Moments& m, OutputArray hu)
void HuMoments(const Moments& moments, double hu[7])

Python:

import cv2 as cv
cv.HuMoments(m[, hu]) → hu

7个二维不变矩的定义如下:
h u [ 0 ] = η 20 + η 02 h u [ 1 ] = ( η 20 − η 02 ) 2 + 4 η 11 2 h u [ 2 ] = ( η 30 − 3 η 12 ) 2 + ( 3 η 21 − η 03 ) 2 h u [ 3 ] = ( η 30 + η 12 ) 2 + ( η 21 + η 03 ) 2 h u [ 4 ] = ( η 30 − 3 η 12 ) ( η 30 + η 12 ) [ ( η 30 + η 12 ) 2 − 3 ( η 21 + η 03 ) 2 ] + ( 3 η 21 − η 03 ) ( η 21 + η 03 ) [ 3 ( η 30 + η 12 ) 2 − ( η 21 + η 03 ) 2 ] h u [ 5 ] = ( η 20 − η 02 ) [ ( η 30 + η 12 ) 2 − ( η 21 + η 03 ) 2 ] + 4 η 11 ( η 30 + η 12 ) ( η 21 + η 03 ) h u [ 6 ] = ( 3 η 21 − η 03 ) ( η 21 + η 03 ) [ 3 ( η 30 + η 12 ) 2 − ( η 21 + η 03 ) 2 ] − ( η 30 − 3 η 12 ) ( η 21 + η 03 ) [ 3 ( η 30 + η 12 ) 2 − ( η 21 + η 03 ) 2 ] \begin{array}{l} hu[0]= \eta _{20}+ \eta _{02} \\ hu[1]=( \eta _{20}- \eta _{02})^{2}+4 \eta _{11}^{2} \\ hu[2]=( \eta _{30}-3 \eta _{12})^{2}+ (3 \eta _{21}- \eta _{03})^{2} \\ hu[3]=( \eta _{30}+ \eta _{12})^{2}+ ( \eta _{21}+ \eta _{03})^{2} \\ hu[4]=( \eta _{30}-3 \eta _{12})( \eta _{30}+ \eta _{12})[( \eta _{30}+ \eta _{12})^{2}-3( \eta _{21}+ \eta _{03})^{2}]+(3 \eta _{21}- \eta _{03})( \eta _{21}+ \eta _{03})[3( \eta _{30}+ \eta _{12})^{2}-( \eta _{21}+ \eta _{03})^{2}] \\ hu[5]=( \eta _{20}- \eta _{02})[( \eta _{30}+ \eta _{12})^{2}- ( \eta _{21}+ \eta _{03})^{2}]+4 \eta _{11}( \eta _{30}+ \eta _{12})( \eta _{21}+ \eta _{03}) \\ hu[6]=(3 \eta _{21}- \eta _{03})( \eta _{21}+ \eta _{03})[3( \eta _{30}+ \eta _{12})^{2}-( \eta _{21}+ \eta _{03})^{2}]-( \eta _{30}-3 \eta _{12})( \eta _{21}+ \eta _{03})[3( \eta _{30}+ \eta _{12})^{2}-( \eta _{21}+ \eta _{03})^{2}] \\ \end{array} hu[0]=η20+η02hu[1]=(η20η02)2+4η112hu[2]=(η303η12)2+(3η21η03)2hu[3]=(η30+η12)2+(η21+η03)2hu[4]=(η303η12)(η30+η12)[(η30+η12)23(η21+η03)2]+(3η21η03)(η21+η03)[3(η30+η12)2(η21+η03)2]hu[5]=(η20η02)[(η30+η12)2(η21+η03)2]+4η11(η30+η12)(η21+η03)hu[6]=(3η21η03)(η21+η03)[3(η30+η12)2(η21+η03)2](η303η12)(η21+η03)[3(η30+η12)2(η21+η03)2]

其中, η j i η_{ji} ηji 表示 Moments:: n u j i nu_{ji} nuji

h u [ 0 ] hu[0] hu[0] 类似于图像质心周围的惯性矩,其中像素的强度类似于物理密度。
h u [ 0 ] hu[0] hu[0] h u [ 5 ] hu[5] hu[5] 是反射对称的,即如果图像变成镜像,值也不会改变。
h u [ 6 ] hu[6] hu[6] 是反射反对称的,即在反射下会改变符号。

矩的应用

比较常用的是质心、面积、方向

(1) 质心坐标:
x ˉ = m 10 m 00 ,    y ˉ = m 01 m 00 \bar{x} = \frac{\texttt{m}_{10}}{\texttt{m}_{00}} , \; \bar{y} = \frac{\texttt{m}_{01}}{\texttt{m}_{00}} xˉ=m00m10,yˉ=m00m01
(2) 面积:
a r e a = m 00 area = \texttt{m}_{00} area=m00
(3)方向:

图像的方向信息可以通过二阶中心矩构造协方差矩阵获得

μ 20 ′ = μ 20 / μ 00 = M 20 / M 00 − x ˉ 2 \mu_{20}'=\mu_{20}/\mu_{00}=M_{20}/M_{00}-\bar{x}^2 μ20=μ20/μ00=M20/M00xˉ2
μ 02 ′ = μ 02 / μ 00 = M 02 / M 00 − y ˉ 2 \mu_{02}'=\mu_{02}/\mu_{00}=M_{02}/M_{00}-\bar{y}^2 μ02=μ02/μ00=M02/M00yˉ2
μ 11 ′ = μ 11 / μ 00 = M 11 / M 00 − x ˉ y ˉ \mu_{11}'=\mu_{11}/\mu_{00}=M_{11}/M_{00}-\bar{x}\bar{y} μ11=μ11/μ00=M11/M00xˉyˉ

图像的协方差矩阵
c o v [ I ( x , y ) ] = [ μ 20 ′ μ 11 ′ μ 11 ′ μ 02 ′ ] cov[I(x,y)]=\begin{bmatrix} \mu_{20}' & \mu_{11}' \\ \mu_{11}'& \mu_{02}' \end{bmatrix} cov[I(x,y)]=[μ20μ11μ11μ02]
该矩阵的特征向量对应于图像强度的长轴和短轴。
角度可以由下式给出:
θ = 1 2 a r c t a n ( 2 μ 11 ′ μ 20 ′ − μ 02 ′ ) \theta=\frac{1}{2} arctan(\frac{2\mu_{11}'}{\mu_{20}'-\mu_{02}'}) θ=21arctan(μ20μ022μ11)
特征值大小的相对差异表示图像的偏心率,或图像的拉长程度,偏心度可以由下式给出:
1 − λ 2 λ 1 \sqrt{1-\frac{\lambda_2}{\lambda_1}} 1λ1λ2

代码示例

获取图像中某些区域的轮廓,并计算每个轮廓形状的面积和质心。

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

# 获取亮度高于阈值的区域
select = (srcMat >= thresh)
select_img = select.astype(np.uint8)

# 膨胀,填补孔洞
kernel = cv.getStructuringElement(cv.MORPH_RECT, (2, 2))
dilate_img = cv.dilate(select_img, kernel)

# 提取轮廓
_, contours, hierarchy = cv.findContours(dilate_img.copy(), cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)

# 准备可视化
vis = dilate_img.copy()*150
vis = cv.merge([vis,vis,vis])
title = ""

# 遍历轮廓
for cnt in contours:
	# 跳过点特别少的轮廓
	if cnt.shape[0] < 8:
		continue
	# 质心
	M = cv.moments(cnt)
	cx = int(M['m10'] / M['m00'])
	cy = int(M['m01'] / M['m00'])
	# 面积
	area = M['m00']
	# 方向
    mu20 = M['m20']/M['m00']-cx**2
    mu02 = M['m02']/M['m00']-cy**2
    mu11 = M['m11']/M['m00']-cx*cy
    angle = 1/2*math.atan(2*mu11/(mu20-mu02))
    degree = math.degrees(angle)
    cv.circle(vis, (cx,cy), 1, (0, 255, 0), -1)
	print(f"area={area},center=({cx},{cy})")

plt.title(title)
plt.imshow(vis)

输出这个轮廓的相关信息,并画出了轮廓的质心位置:
在这里插入图片描述

参考链接

https://docs.opencv.org/3.4/d8/d23/classcv_1_1Moments.html#a8b1b4917d1123abc3a3c16b007a7319b
Hu. Visual Pattern Recognition by Moment Invariants, IRE Transactions on Information Theory, 8:2, pp. 179-187, 1962.
http://en.wikipedia.org/wiki/Image_moment

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐