在2D数组中进行峰值检测
在2D数组中进行峰值检测
我正在帮助一家兽医诊所测量狗脚下的压力。我使用Python进行数据分析。现在我正在尝试将脚分成(解剖学)的子区域,但卡住了。
我为每只脚制作了一个2D数组,其中包含每个传感器在经过时间后所加载的最大值。下面是一个脚的示例,我使用Excel绘制了我想要“检测”的区域。这些是围绕具有局部最大值的传感器的2x2框,它们的总和最大。
因此,我尝试了一些实验,并决定简单地查找每行和每列的最大值(由于脚的形状,无法单向查找)。这似乎很好地“检测”了每个独立脚趾的位置,但也标记了相邻的传感器。
那么最好的方法是什么,可以告诉Python哪些极值是我想要的?
注意:2x2的正方形不能重叠,因为它们必须是独立的脚趾!
另外,我选择了2x2只是为了方便,任何更高级的解决方案都可以,但我只是一个人类运动学家,既不是真正的程序员也不是数学家,所以请让它保持“简单”。
这里有一个可以用np.loadtxt
加载的版本
结果
我尝试了@jextee的解决方案(请参见下面的结果)。您可以看到,它在前肢上工作得非常好,但在后腿上效果不佳。
更具体地说,它无法识别第四根脚趾头的小峰。这显然与环形从顶部向最低值望去,而没有考虑这个值位于何处有关。有人知道如何调整@jextee的算法,以便它也能够找到第四个脚趾吗?
由于我还没有处理其他试验,所以我无法提供其他样本。但是,我之前提供的数据是每个爪子的平均值。该文件是一个数组,其中按它们与板接触的顺序放置了9个爪子的最大数据。
这张图片展示了它们如何在板上空间分布。
更新:
我为任何有兴趣的人设置了一个博客,并设置了一个OneDrive以放置所有原始测量数据。因此,任何请求更多数据的人都可以有更多的动力!*/
新更新:在获得关于脚掌检测和脚掌分类的问题的帮助之后,我终于能够检查每只脚的脚趾检测了!结果是,它在除了像自己示例中大小的脚之外的任何地方都不起作用。当然,回顾时,在选取2×2时是我的自作聪明。
这里有一个很好的例子,说明它是如何失败的:一只脚趾甲被误认为是脚趾,而“脚跟”太宽了,被识别了两次!
脚掌太大了,所以如果不重叠地使用2×2的大小,会导致有些脚趾被检测到两次。反之,在小狗身上,它经常无法找到第五个脚趾,我怀疑这是因为2×2的区域太大了。
在尝试了当前的解决方案之后,我惊讶地发现,对于几乎所有的小狗,它都找不到第五个脚趾,在大狗的影响中,它在50%以上的情况下会找到更多!
所以,显然我需要改变它。我自己的猜测是将“邻域”的大小改为小狗更小,大狗更大。但是“generate_binary_structure”不会让我改变数组的大小。
因此,我希望有人对于定位脚趾有更好的建议,也许使脚趾区域随着脚掌大小改变?
解决方法
数据文件:paw.txt。源代码:
from scipy import * from operator import itemgetter n = 5 # how many fingers are we looking for d = loadtxt("paw.txt") width, height = d.shape # Create an array where every element is a sum of 2x2 squares. fourSums = d[:-1,:-1] + d[1:,:-1] + d[1:,1:] + d[:-1,1:] # Find positions of the fingers. # Pair each sum with its position number (from 0 to width*height-1), pairs = zip(arange(width*height), fourSums.flatten()) # Sort by descending sum value, filter overlapping squares def drop_overlapping(pairs): no_overlaps = [] def does_not_overlap(p1, p2): i1, i2 = p1[0], p2[0] r1, col1 = i1 / (width-1), i1 % (width-1) r2, col2 = i2 / (width-1), i2 % (width-1) return (max(abs(r1-r2),abs(col1-col2)) >= 2) for p in pairs: if all(map(lambda prev: does_not_overlap(p,prev), no_overlaps)): no_overlaps.append(p) return no_overlaps pairs2 = drop_overlapping(sorted(pairs, key=itemgetter(1), reverse=True)) # Take the first n with the heighest values positions = pairs2[:n] # Print results print d, "\n" for i, val in positions: row = i / (width-1) column = i % (width-1) print "sum = %f @ %d,%d (%d)" % (val, row, column, i) print d[row:row+2,column:column+2], "\n"
输出没有重叠正方形。似乎选中的区域与您的示例相同。
一些评论
最棘手的部分是计算所有2x2正方形的和。我假设您需要它们全部,因此可能会有一些重叠。我使用切片从原始的二维数组中删除第一/最后一列和行,然后将它们全部重叠在一起并计算和。
为了更好地理解它,请想象一个3x3的数组:
>>> a = arange(9).reshape(3,3) ; a array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
然后您可以获取它的切片:
>>> a[:-1,:-1] array([[0, 1], [3, 4]]) >>> a[1:,:-1] array([[3, 4], [6, 7]]) >>> a[:-1,1:] array([[1, 2], [4, 5]]) >>> a[1:,1:] array([[4, 5], [7, 8]])
现在想象将它们叠加在一起并在相同的位置上求和。这些总和将恰好是以相同位置的左上角为起点的2x2正方形的总和:
>>> sums = a[:-1,:-1] + a[1:,:-1] + a[:-1,1:] + a[1:,1:]; sums array([[ 8, 12], [20, 24]])
当您拥有了2x2正方形的总和后,可以使用 max
查找最大值,或者使用 sort
或 sorted
查找峰值。
为了记住峰值的位置,我将每个值(总和)与其在展平的数组中的序数位置(见 zip
)配对。然后在打印结果时再次计算行/列位置。
注释
我允许2x2正方形重叠。编辑后的版本会过滤掉其中的一些,以便只显示不重叠的正方形。
选择手指(一个思路)
另一个问题是如何选择可能是手指的峰值。我有一个想法,可能可以行得通,也可能不行。我现在没有时间实现它,所以只能提供伪代码。
我注意到,如果前面的手指停留在几乎完美的圆圈上,后面的手指应该在那个圆圈的内部。此外,前面的手指或多或少等距分布。我们可以尝试使用这些启发式属性来检测手指。
伪代码:
select the top N finger candidates (not too many, 10 or 12) consider all possible combinations of 5 out of N (use itertools.combinations) for each combination of 5 fingers: for each finger out of 5: fit the best circle to the remaining 4 => position of the center, radius check if the selected finger is inside of the circle check if the remaining four are evenly spread (for example, consider angles from the center of the circle) assign some cost (penalty) to this selection of 4 peaks + a rear finger (consider, probably weighted: circle fitting error, if the rear finger is inside, variance in the spreading of the front fingers, total intensity of 5 peaks) choose a combination of 4 peaks + a rear peak with the lowest penalty
这是一种暴力方法。如果N比较小,那么我认为这是可做的。对于N=12,有C_12^5=792种组合,再乘以选择后面手指的5种方式,每只爪子需要评估3960种情况。
我使用了本地最大值过滤器来检测峰值。以下是在你的第一个四爪数据集上的结果:
我也在第二个九爪数据集上运行了它,并且也成功了。
方法如下:
import numpy as np from scipy.ndimage.filters import maximum_filter from scipy.ndimage.morphology import generate_binary_structure, binary_erosion import matplotlib.pyplot as pp #for some reason I had to reshape. Numpy ignored the shape header. paws_data = np.loadtxt("paws.txt").reshape(4,11,14) #getting a list of images paws = [p.squeeze() for p in np.vsplit(paws_data,4)] def detect_peaks(image): """ Takes an image and detect the peaks usingthe local maximum filter. Returns a boolean mask of the peaks (i.e. 1 when the pixel's value is the neighborhood maximum, 0 otherwise) """ # define an 8-connected neighborhood neighborhood = generate_binary_structure(2,2) #apply the local maximum filter; all pixel of maximal value #in their neighborhood are set to 1 local_max = maximum_filter(image, footprint=neighborhood)==image #local_max is a mask that contains the peaks we are #looking for, but also the background. #In order to isolate the peaks we must remove the background from the mask. #we create the mask of the background background = (image==0) #a little technicality: we must erode the background in order to #successfully subtract it form local_max, otherwise a line will #appear along the background border (artifact of the local maximum filter) eroded_background = binary_erosion(background, structure=neighborhood, border_value=1) #we obtain the final mask, containing only peaks, #by removing the background from the local_max mask (xor operation) detected_peaks = local_max ^ eroded_background return detected_peaks #applying the detection and plotting results for i, paw in enumerate(paws): detected_peaks = detect_peaks(paw) pp.subplot(4,2,(2*i+1)) pp.imshow(paw) pp.subplot(4,2,(2*i+2) ) pp.imshow(detected_peaks) pp.show()
之后你只需要在掩膜上使用scipy.ndimage.measurements.label
来标记所有不同的对象。然后你就可以单独操作它们了。
请注意,此方法有效的原因是背景不嘈杂。如果是,你会在背景中检测到许多其他不需要的峰值。另一个重要因素是邻域的大小。如果峰值的大小改变了(它们应该保持大致成比例),你需要调整它。