草庐IT

如何计算质心

有为少年 2025-04-16 原文

如何计算质心

原始文档:https://www.yuque.com/lart/idh721/gpbigm

概念

质心,即质量中心的简称。质点系的质心是质点系质量分布的平均位置。指物质系统上被认为质量集中于此的一个假想点,与重心不同的是质心不一定要在有重力场的系统中,值得注意的是除非重力场是均匀的,否则同一物质系统的质心与重心通常不在同一假想点上。

计算

质心坐标等于所有点关于每个坐标的以质量为权重的加权平均值。一般主要在二维空间讨论,尤其是图像数据,但是这里直接按照更一般的形式进行定义。首先对于任意 n n n维空间中的连续形式的子集 P P P的质心可以定义为:

C = ∫ p g ( p ) d p ∫ g ( p ) d p C = \frac{\int p g(p)dp}{\int g(p)dp} C=g(p)dppg(p)dp

其中:

  • p ∈ R n p \in \mathbb{R}^n pRn表示该子集中的一点;
  • g g g表示该子集的特征函数(indicator function or a characteristic function)。一个比较实际的场景是,它可以用来表示各个位置对应的质量。

也可以看到,这里的分母是对这个集合的一个度量,因而如果度量为0,那么就不可以被计算质心。

而对于第 k k k个坐标 C k C_k Ck的计算,可以通过如下形式:

C k = ∫ z S k ( z ) d z ∫ g ( x ) d z C_k = \frac{\int z S_k(z) dz}{\int g(x) dz} Ck=g(x)dzzSk(z)dz

这里 S k ( z ) S_k(z) Sk(z)表示的是对应的 P P P与由 p k = z p_k=z pk=z定义的超平面(hyperplane)的交集的度量,在这个超平面上,会涉及到其他所有的坐标轴。

对于一个平面图,即二维情形,上面的式子可以用于求取 C x C_x Cx C y C_y Cy

  • C x = ∫ x S x ( x ) d x A C_x = \frac{\int x S_x(x) dx}{A} Cx=AxSx(x)dx
  • C y = ∫ y S y ( y ) d y A C_y = \frac{\int y S_y(y) dy}{A} Cy=AySy(y)dy

即对单一轴向上的坐标积分,每个坐标对应乘以一个与之关联的量 S S S(该量会涉及到另一个轴), A = ∫ S x ( x ) d x = ∫ S y ( y ) d y A = \int S_x(x) dx = \int S_y(y) dy A=Sx(x)dx=Sy(y)dy表示图形的面积。一般情况下,我们可以简单的理解为这是过点 ( x , 0 ) (x, 0) (x,0)或是 ( 0 , y ) (0, y) (0,y)的垂线与图像区域的相交构成的线段的长度。

对于更实际的离散且有限点集的情形下,前面二维的形式可以转化为如下形式:

  • C x = ∑ i w i x i ∑ i w i C_x = \frac{\sum_i w_i x_i}{\sum_i w_i} Cx=iwiiwixi
  • C y = ∑ i w i y i ∑ i w i C_y = \frac{\sum_i w_i y_i}{\sum_i w_i} Cy=iwiiwiyi

这里要注意,公式中表示各个点的方式与前面直接基于坐标值的方式有所不同,而是通过一个额外的点索引 i i i来对不同的点进行编码排序,从而构建了公式。

对于不同的点有着不同的对应权重,我们可以理解为质量或者其他的度量形式,所以对应相同的点序号 i i i,其各个轴向上的坐标权重也是一样的 w i w_i wi,且 W = ∑ i w i W = \sum_i w_i W=iwi可以表示为图像对应的整体质量或者其他的度量。如果各点权重均为1,则这里的 W W W则实际上便是点的数量,对于数字图像而言,就是图形面积了。

更一般的,这里的点 i i i实际上还可以替换为有着面积(或者说离散点的数量) A i A_i Ai的区域 P i P_i Pi。计算过程中将其质心作为这里的点。

  • C x = ∑ i C i , x W i ∑ i W i = ∑ i ∑ j x i , j w i , j ∑ j w i , j ∑ j w i , j ∑ i ∑ j w i , j = ∑ i ∑ j x i , j w i , j ∑ i ∑ j w i , j = ∑ l x l w l ∑ l w l C_x = \frac{\sum_i C_{i,x} W_i}{\sum_i W_i} = \frac{\sum_i \frac{\sum_j x_{i,j} w_{i,j}}{\sum_j w_{i,j}} \sum_j w_{i,j} }{\sum_i \sum_j w_{i,j}} = \frac{\sum_i \sum_j x_{i,j} w_{i,j}}{\sum_i \sum_j w_{i,j}} = \frac{\sum_l x_l w_l}{\sum_l w_l} Cx=iWiiCi,xWi=ijwi,jijwi,jjxi,jwi,jjwi,j=ijwi,jijxi,jwi,j=lwllxlwl
  • C y = ∑ i C i , y A i ∑ i A i C_y = \frac{\sum_i C_{i,y}A_i}{\sum_i A_i} Cy=iAiiCi,yAi

除了直接基于定义的形式进行计算,还可以利用图像的 p + q p+q p+q阶矩(空间矩/几何矩/原点矩) m p q m_{pq} mpq和中心矩 μ p q \mu_{pq} μpq来定义。

对于一幅二维连续图像, f ( x , y ) ≥ 0 f(x, y) \ge 0 f(x,y)0,两个矩的定义为:

  • m p q = ∫ − ∞ ∞ ∫ − ∞ ∞ x p y q f ( x , y ) d x d y m_{pq} = \int^{\infty}_{-\infty}\int^{\infty}_{-\infty} x^p y^q f(x,y) dxdy mpq=xpyqf(x,y)dxdy
  • μ p q = ∫ − ∞ ∞ ∫ − ∞ ∞ ( x − x c ) p ( y − y c ) q f ( x , y ) d x d y \mu_{pq} = \int^{\infty}_{-\infty}\int^{\infty}_{-\infty} (x-x_c)^p (y-y_c)^q f(x,y) dxdy μpq=(xxc)p(yyc)qf(x,y)dxdy

这里的 ( x c , y c ) (x_c, y_c) (xc,yc)即为质心坐标:

  • x c = m 10 m 00 = ∫ − ∞ ∞ ∫ − ∞ ∞ x f ( x , y ) d x d y ∫ − ∞ ∞ ∫ − ∞ ∞ f ( x , y ) d x d y x_c = \frac{m_{10}}{m_{00}} = \frac{\int^{\infty}_{-\infty}\int^{\infty}_{-\infty} x f(x,y) dxdy}{\int^{\infty}_{-\infty}\int^{\infty}_{-\infty} f(x,y) dxdy} xc=m00m10=f(x,y)dxdyxf(x,y)dxdy
  • y c = m 01 m 00 = ∫ − ∞ ∞ ∫ − ∞ ∞ y f ( x , y ) d x d y ∫ − ∞ ∞ ∫ − ∞ ∞ f ( x , y ) d x d y y_c = \frac{m_{01}}{m_{00}} = \frac{\int^{\infty}_{-\infty}\int^{\infty}_{-\infty} y f(x,y) dxdy}{\int^{\infty}_{-\infty}\int^{\infty}_{-\infty} f(x,y) dxdy} yc=m00m01=f(x,y)dxdyyf(x,y)dxdy

对于离散情形,可以定义为:

  • m p q = ∑ i = 1 N ∑ j = 1 N x i p y j q f ( x i , y j ) m_{pq} = \sum^{N}_{i=1}\sum^{N}_{j=1} x_i^p y_j^q f(x_i,y_j) mpq=i=1Nj=1Nxipyjqf(xi,yj)
  • μ p q = ∑ i = 1 N ∑ j = 1 N ( x i − x c ) p ( y j − y c ) q f ( x i , y j ) \mu_{pq} = \sum^{N}_{i=1}\sum^{N}_{j=1} (x_i - x_c)^p (y_j-y_c)^q f(x_i,y_j) μpq=i=1Nj=1N(xixc)p(yjyc)qf(xi,yj)

对应的执行可以计算为:

  • x c = m 10 m 00 = ∑ i = 1 N ∑ j = 1 N x i p f ( x i , y j ) ∑ i = 1 N ∑ j = 1 N f ( x i , y j ) x_c = \frac{m_{10}}{m_{00}} = \frac{\sum^{N}_{i=1}\sum^{N}_{j=1} x_i^p f(x_i,y_j)}{\sum^{N}_{i=1}\sum^{N}_{j=1} f(x_i,y_j)} xc=m00m10=i=1Nj=1Nf(xi,yj)i=1Nj=1Nxipf(xi,yj)
  • y c = m 01 m 00 = ∑ i = 1 N ∑ j = 1 N y j q f ( x i , y j ) ∑ i = 1 N ∑ j = 1 N f ( x i , y j ) y_c = \frac{m_{01}}{m_{00}} = \frac{\sum^{N}_{i=1}\sum^{N}_{j=1} y_j^q f(x_i,y_j)}{\sum^{N}_{i=1}\sum^{N}_{j=1} f(x_i,y_j)} yc=m00m01=i=1Nj=1Nf(xi,yj)i=1Nj=1Nyjqf(xi,yj)

编程实现

网上有很多的实现方式,有基于定义的,也有基于矩的形式的,这里找到了几个进行一下简单的分析。

基于定义

scipy.ndimage.center_of_mass

文档可见https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.center_of_mass.html

从实现中我们可以直观看出来,这是直接基于定义实现的:

normalizer = sum(input, labels, index)
grids = numpy.ogrid[[slice(0, i) for i in input.shape]]
results = [sum(input * grids[dir].astype(float), labels, index) / normalizer
           for dir in range(input.ndim)]
if numpy.isscalar(results[0]):
    return tuple(results)
return [tuple(v) for v in numpy.array(results).T]

这里提供了同时对多个不同区域的质心计算的支持。

质心计算时,首先使用numpy.ogrid构造了坐标系网格,之后针对不同的坐标轴遍历,分别对图形区域内的坐标使用原始数据加权求和并归一化。

numpy.argwhere

对于特殊情况,即我们针对二值图计算质心时,可以考虑使用这一方法。

考虑到此时质心的计算实际上仅仅是图形内部坐标的平均,所以可以直接利用argwhere获得图像区域的像素坐标,对其平均即可。

# https://stackoverflow.com/a/38933601
np.argwhere(x).mean(0)

直接计算

https://github.com/lartpang/PySODMetrics/blob/4aa253a59aff71507f92daf2dffe539c5c97ce46/py_sod_metrics/sod_metrics.py#L277-L282

area_object = np.sum(matrix)
row_ids = np.arange(h)
col_ids = np.arange(w)
x = np.round(np.sum(np.sum(matrix, axis=0) * col_ids) / area_object)
y = np.round(np.sum(np.sum(matrix, axis=1) * row_ids) / area_object)

但是这里的代码存在溢出的风险。

numpy不同于python自身的数据表示形式,本身是存在类型限制的,尤其是整型数组容易出现溢出问题:
这里如果对于特别大的图像进行计算,会出现与前两种方式明显不同的结果:

In [53]: def print_centroid(x, h, w):
    ...:     print(np.sum(np.sum(x, axis=1) * np.arange(h)) / np.count_nonzero(x), np.sum(np.sum(x
    ...: , axis=0) * np.arange(w)) / np.count_nonzero(x))
    ...:     print(center_of_mass(x))
    ...:     print(np.argwhere(x).mean(0))

In [59]: print_centroid(np.random.random((1024, 1024)) > 0.7, 1024, 1024)
511.3934109266679 511.69975831584304
(511.3934109266679, 511.69975831584304)
[511.39341093 511.69975832]

In [60]: print_centroid(np.random.random((2*1024, 2*1024)) > 0.7, 2*1024, 2*1024)
1023.3879048154441 1022.7496662402068
(1023.3879048154441, 1022.7496662402068)
[1023.38790482 1022.74966624]

In [61]: print_centroid(np.random.random((3*1024, 3*1024)) > 0.7, 3*1024, 3*1024)
18.466653739911568 18.801060064556072  <--此时输出已经开始异常
(1535.4062819085118, 1535.7406882331563)
[1535.40628191 1535.74068823]

In [62]: print_centroid(np.random.random((4*1024, 4*1024)) > 0.7, 4*1024, 4*1024)
341.39504553161055 341.632512348338
(2047.3403782063924, 2047.5778450231198)
[2047.34037821 2047.57784502]

In [63]: print_centroid(np.random.random((8*1024, 8*1024)) > 0.7, 8*1024, 8*1024)
42.42419332351165 42.500630084653515
(4095.6871022111004, 4095.7635389722423)
[4095.68710221 4095.76353897]

参考前面scipy的实现,我们可以这样修改:

In [73]: def print_centroid(x, h, w):
    ...:     print(np.sum(np.sum(x, axis=1) * np.arange(h).astype(float)) / np.count_nonzero(x), n
    ...: p.sum(np.sum(x, axis=0) * np.arange(w).astype(float)) / np.count_nonzero(x))
    ...:     print(center_of_mass(x))
    ...:     print(np.argwhere(x).mean(0))

In [74]: print_centroid(np.random.random((8*1024, 8*1024)) > 0.7, 8*1024, 8*1024)
4095.778456882133 4095.5295789226266
(4095.778456882133, 4095.5295789226266)
[4095.77845688 4095.52957892]

In [75]: print_centroid(np.random.random((3*1024, 3*1024)) > 0.7, 3*1024, 3*1024)
1535.384796905072 1535.7708201396363
(1535.384796905072, 1535.7708201396363)
[1535.38479691 1535.77082014]

此时便不再容易溢出了。

基于矩的方式

cv2.moments

关于不同矩的介绍可见中的介绍。

从这篇文章https://www.geeksforgeeks.org/python-opencv-find-center-of-contour/中我们可以注意到,opencv提供了计算图像矩的功能。

该函数有两种使用方式:

  • 计算全图的质心。直接送入二值化后的图像。
  • 计算图中各个局部图形的质心:需要先提取轮廓再遍历轮廓计算。因此对于随机生成的离散点,就不太适合使用这一方式进行计算了。

核心代码如下:

# 直接处理图像
m = cv2.moments(image)
cx = m['m10']/m['m00']
cy = m['m01']/m['m00']

# 提取轮廓
contours, hierarchies = cv.findContours(thresh, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
# 根据轮廓计算不同轮廓对应的质心
for i in contours:
	m = cv.moments(i)
    cx = m['m10'] / m['m00']
    cy = m['m01'] / m['m00']

使用该函数计算最好使用真实图像,结果和前三种是一致的。

In [41]: from skimage import data, img_as_float
    ...: img = img_as_float(data.camera()) > 0.5

In [44]: def print_centroid(x, h, w):
    ...:     print(np.sum(np.sum(x, axis=1) * np.arange(h).astype(float)) / np.count_nonzero(x), n
    ...: p.sum(np.sum(x, axis=0) * np.arange(w).astype(float)) / np.count_nonzero(x))
    ...:     print(center_of_mass(x))
    ...:     print(np.argwhere(x).mean(0))
    ...:     mu = cv2.moments(x.astype(np.uint8))
    ...:     print(mu['m01'] / mu['m00'], mu['m10'] / mu['m00'])
    ...:

In [45]: np.count_nonzero(img)
Out[45]: 168559

In [46]: print_centroid(img, 512, 512)
231.7689117756987 309.7281604660683
(231.7689117756987, 309.7281604660683)
[231.76891178 309.72816047]
231.7689117756987 309.7281604660683

参考

有关如何计算质心的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  8. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  9. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  10. ruby - 如何使用文字标量样式在 YAML 中转储字符串? - 2

    我有一大串格式化数据(例如JSON),我想使用Psychinruby​​同时保留格式转储到YAML。基本上,我希望JSON使用literalstyle出现在YAML中:---json:|{"page":1,"results":["item","another"],"total_pages":0}但是,当我使用YAML.dump时,它不使用文字样式。我得到这样的东西:---json:!"{\n\"page\":1,\n\"results\":[\n\"item\",\"another\"\n],\n\"total_pages\":0\n}\n"我如何告诉Psych以想要的样式转储标量?解

随机推荐