如何对我的爪子排序?
在我之前的问题中,我得到了一个很棒的答案,帮助我检测到爪子是如何触碰到压力板的,但现在我在把这些结果和对应的爪子联系起来时遇到了困难。
我手动标注了爪子(RF=右前爪,RH=右后爪,LF=左前爪,LH=左后爪)。
如你所见,这里明显有一个重复的模式,几乎在每次测量中都会出现。这里有一个链接,展示了6次手动标注的实验。
我最初的想法是用一些经验法则来进行排序,比如:
- 前爪和后爪的承重比例大约是60-40%;
- 后爪的表面积通常较小;
- 爪子通常在空间上是左右分开的。
不过,我对这些经验法则有点怀疑,因为一旦遇到我没想到的变化,它们就会失效。而且,它们也无法处理跛脚狗的测量数据,因为这些狗可能有自己的规则。
此外,Joe建议的标注有时会出错,并没有考虑到爪子的实际样子。
根据我在关于爪子内峰值检测的问题中收到的回答,我希望能找到更高级的解决方案来对爪子进行排序。特别是因为每只爪子的压力分布和变化都是不同的,几乎就像指纹一样。我希望有一种方法可以利用这些信息来对我的爪子进行聚类,而不仅仅是按出现的顺序排序。
所以我在寻找一种更好的方法来将结果与对应的爪子进行排序。
对于任何有兴趣的人,我已经将一个字典进行了序列化,里面包含了每只爪子的压力数据的所有切片数组(按测量分组)以及描述它们位置的切片(在压力板上的位置和时间)。
为了澄清一下:walk_sliced_data是一个字典,包含['ser_3', 'ser_2', 'sel_1', 'sel_2', 'ser_1', 'sel_3'],这些是测量的名称。每个测量又包含一个字典,[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10](来自'sel_1'的示例),这些代表提取到的冲击。
还要注意的是,像爪子部分被测量到的“假”冲击(在空间或时间上)可以被忽略。它们只在识别模式时有用,但不会被分析。
如果有人感兴趣,我正在写一个博客,记录项目的所有更新!
3 个回答
你能让技术人员手动输入第一个爪子(或者前两个爪子)吗?这个过程可能是这样的:
- 给技术人员展示步骤顺序的图片,并要求他们标注第一个爪子。
- 根据第一个爪子来标记其他爪子,并允许技术人员进行修改或者重新测试。这样可以适应跛脚或者三条腿的狗。
根据时间的长短来分析信息,我觉得可以用一些运动学的技巧,特别是逆运动学。结合方向、长度、时间和总重量,这些信息可以帮助我们找到某种规律,我希望这能成为解决你“排序爪子”问题的第一步。
所有这些数据可以用来创建一个有边界的多边形(或者叫元组)的列表,然后你可以先按步长排序,再按爪子的特性排序。
好的!我终于成功让一些东西稳定运行了!这个问题让我纠结了好几天……真是有趣的事情!抱歉我的回答有点长,但我需要详细说明一些事情……(虽然我可能会创造出史上最长的非垃圾堆栈溢出回答!)
顺便提一下,我使用的是Ivo在他原始问题中提供的链接中的完整数据集。这是一系列的rar文件(每只狗一个),每个文件包含几个不同实验的结果,存储为ascii数组。与其试图将独立的代码示例复制粘贴到这个问题中,不如直接提供一个bitbucket的代码库,里面有完整的独立代码。你可以用以下命令克隆它:
hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis
概述
正如你在问题中提到的,解决这个问题大致有两种方法。我实际上会以不同的方式使用这两种方法。
- 利用爪子的撞击顺序来判断是哪只爪子。
- 尝试仅根据爪子的形状来识别“爪印”。
基本上,第一种方法适用于狗的爪子按照Ivo问题中展示的梯形模式撞击,但当爪子不遵循这个模式时就会失败。程序上检测到这种情况也相对简单。
因此,我们可以利用那些成功的测量数据来建立一个训练数据集(大约2000个爪子撞击来自30只不同的狗),以识别哪只爪子是哪个,问题就变成了一个监督分类问题(还有一些额外的复杂性……图像识别比“正常”的监督分类问题要难一些)。
模式分析
进一步说明第一种方法,当狗正常走路(不是跑步!)时(这些狗中有些可能不是),我们期望爪子的撞击顺序是:前左、后右、前右、后左、前左,等等。这个模式可以从前左或前右爪开始。
如果这种情况总是成立,我们可以简单地按初始接触时间对撞击进行排序,并用4取模来将它们按爪子分组。
然而,即使在一切“正常”的情况下,这种方法也不总是有效。这是因为模式的梯形形状,后爪在空间上落后于前爪。
因此,初始前爪撞击后的后爪撞击往往会掉出传感器板,无法记录。同样,最后的爪子撞击往往不是序列中的下一只爪子,因为它之前的撞击发生在传感器板外,也没有被记录。
尽管如此,我们可以利用爪子撞击模式的形状来判断何时发生了这种情况,以及我们是从左前爪还是右前爪开始的。(我实际上在这里忽略了最后一次撞击的问题。不过,添加这个并不太难。)
def group_paws(data_slices, time):
# Sort slices by initial contact time
data_slices.sort(key=lambda s: s[-1].start)
# Get the centroid for each paw impact...
paw_coords = []
for x,y,z in data_slices:
paw_coords.append([(item.stop + item.start) / 2.0 for item in (x,y)])
paw_coords = np.array(paw_coords)
# Make a vector between each sucessive impact...
dx, dy = np.diff(paw_coords, axis=0).T
#-- Group paws -------------------------------------------
paw_code = {0:'LF', 1:'RH', 2:'RF', 3:'LH'}
paw_number = np.arange(len(paw_coords))
# Did we miss the hind paw impact after the first
# front paw impact? If so, first dx will be positive...
if dx[0] > 0:
paw_number[1:] += 1
# Are we starting with the left or right front paw...
# We assume we're starting with the left, and check dy[0].
# If dy[0] > 0 (i.e. the next paw impacts to the left), then
# it's actually the right front paw, instead of the left.
if dy[0] > 0: # Right front paw impact...
paw_number += 2
# Now we can determine the paw with a simple modulo 4..
paw_codes = paw_number % 4
paw_labels = [paw_code[code] for code in paw_codes]
return paw_labels
尽管如此,它经常无法正确工作。完整数据集中的许多狗似乎在跑步,爪子的撞击顺序与狗走路时并不相同。(或者可能是狗有严重的髋关节问题……)
幸运的是,我们仍然可以程序上检测爪子的撞击是否遵循我们预期的空间模式:
def paw_pattern_problems(paw_labels, dx, dy):
"""Check whether or not the label sequence "paw_labels" conforms to our
expected spatial pattern of paw impacts. "paw_labels" should be a sequence
of the strings: "LH", "RH", "LF", "RF" corresponding to the different paws"""
# Check for problems... (This could be written a _lot_ more cleanly...)
problems = False
last = paw_labels[0]
for paw, dy, dx in zip(paw_labels[1:], dy, dx):
# Going from a left paw to a right, dy should be negative
if last.startswith('L') and paw.startswith('R') and (dy > 0):
problems = True
break
# Going from a right paw to a left, dy should be positive
if last.startswith('R') and paw.startswith('L') and (dy < 0):
problems = True
break
# Going from a front paw to a hind paw, dx should be negative
if last.endswith('F') and paw.endswith('H') and (dx > 0):
problems = True
break
# Going from a hind paw to a front paw, dx should be positive
if last.endswith('H') and paw.endswith('F') and (dx < 0):
problems = True
break
last = paw
return problems
因此,尽管简单的空间分类并不总是有效,但我们可以合理自信地判断何时它是有效的。
训练数据集
从模式分类中那些正确的结果,我们可以建立一个非常大的训练数据集,包含大约2400个正确分类的爪子撞击(来自32只不同的狗!)。
现在我们可以开始观察“平均”的前左爪等的样子。
为此,我们需要某种“爪子度量”,它在任何狗身上都是相同的维度。(在完整数据集中,有非常大和非常小的狗!)爱尔兰猎狼犬的爪印会比玩具贵宾犬的爪印宽得多且“重得多”。我们需要重新缩放每个爪印,以便a)它们具有相同的像素数量,b)压力值是标准化的。为此,我将每个爪印重新采样到一个20x20的网格上,并根据爪子撞击的最大、最小和平均压力值重新缩放压力值。
def paw_image(paw):
from scipy.ndimage import map_coordinates
ny, nx = paw.shape
# Trim off any "blank" edges around the paw...
mask = paw > 0.01 * paw.max()
y, x = np.mgrid[:ny, :nx]
ymin, ymax = y[mask].min(), y[mask].max()
xmin, xmax = x[mask].min(), x[mask].max()
# Make a 20x20 grid to resample the paw pressure values onto
numx, numy = 20, 20
xi = np.linspace(xmin, xmax, numx)
yi = np.linspace(ymin, ymax, numy)
xi, yi = np.meshgrid(xi, yi)
# Resample the values onto the 20x20 grid
coords = np.vstack([yi.flatten(), xi.flatten()])
zi = map_coordinates(paw, coords)
zi = zi.reshape((numy, numx))
# Rescale the pressure values
zi -= zi.min()
zi /= zi.max()
zi -= zi.mean() #<- Helps distinguish front from hind paws...
return zi
经过这些处理后,我们终于可以看看平均的左前爪、后右爪等的样子。请注意,这些是基于超过30只大小差异很大的狗的平均值,我们似乎得到了稳定的结果!
不过,在对这些进行任何分析之前,我们需要减去均值(所有狗所有腿的平均爪子)。
现在我们可以分析与均值的差异,这样更容易识别:
基于图像的爪子识别
好了……我们终于有了一组可以开始尝试与爪子匹配的模式。每只爪子可以被视为一个400维的向量(由paw_image
函数返回),可以与这四个400维的向量进行比较。
不幸的是,如果我们仅仅使用“正常”的监督分类算法(即找出哪个4个模式与特定爪印最接近,使用简单的距离),它并不总是有效。事实上,在训练数据集上,它的表现并没有比随机机会好多少。
这是图像识别中的一个常见问题。由于输入数据的高维度和图像的“模糊”特性(即相邻像素之间有很高的协方差),仅仅查看图像与模板图像之间的差异并不能很好地衡量它们形状的相似性。
特征爪
为了解决这个问题,我们需要构建一组“特征爪”(就像面部识别中的“特征脸”),并将每个爪印描述为这些特征爪的组合。这与主成分分析相同,基本上提供了一种减少数据维度的方法,使得距离成为形状的良好度量。
因为我们有的训练图像数量多于维度(2400对400),所以不需要进行“复杂”的线性代数来提高速度。我们可以直接使用训练数据集的协方差矩阵:
def make_eigenpaws(paw_data):
"""Creates a set of eigenpaws based on paw_data.
paw_data is a numdata by numdimensions matrix of all of the observations."""
average_paw = paw_data.mean(axis=0)
paw_data -= average_paw
# Determine the eigenvectors of the covariance matrix of the data
cov = np.cov(paw_data.T)
eigvals, eigvecs = np.linalg.eig(cov)
# Sort the eigenvectors by ascending eigenvalue (largest is last)
eig_idx = np.argsort(eigvals)
sorted_eigvecs = eigvecs[:,eig_idx]
sorted_eigvals = eigvals[:,eig_idx]
# Now choose a cutoff number of eigenvectors to use
# (50 seems to work well, but it's arbirtrary...
num_basis_vecs = 50
basis_vecs = sorted_eigvecs[:,-num_basis_vecs:]
return basis_vecs
这些basis_vecs
就是“特征爪”。
使用这些,我们只需将每个爪子图像(作为400维向量,而不是20x20图像)与基向量进行点乘(即矩阵乘法)。这会给我们一个50维的向量(每个基向量一个元素),我们可以用它来分类图像。我们不再是将20x20图像与每个“模板”爪子的20x20图像进行比较,而是将50维的变换图像与每个50维的变换模板爪进行比较。这对每个爪子的具体位置的小变化不那么敏感,并基本上将问题的维度减少到相关的维度。
基于特征爪的爪子分类
现在我们可以简单地使用50维向量与每条腿的“模板”向量之间的距离来分类哪只爪子是哪个:
codebook = np.load('codebook.npy') # Template vectors for each paw
average_paw = np.load('average_paw.npy')
basis_stds = np.load('basis_stds.npy') # Needed to "whiten" the dataset...
basis_vecs = np.load('basis_vecs.npy')
paw_code = {0:'LF', 1:'RH', 2:'RF', 3:'LH'}
def classify(paw):
paw = paw.flatten()
paw -= average_paw
scores = paw.dot(basis_vecs) / basis_stds
diff = codebook - scores
diff *= diff
diff = np.sqrt(diff.sum(axis=1))
return paw_code[diff.argmin()]
以下是一些结果:
剩余问题
仍然存在一些问题,特别是对于那些爪印不清晰的小狗……(对于大狗来说效果最好,因为它们的脚趾在传感器的分辨率下更清晰分开。)此外,部分爪印在这个系统中无法识别,而在基于梯形模式的系统中可以识别。
不过,由于特征爪分析本质上使用了一种距离度量,我们可以两种方式对爪子进行分类,当特征爪分析与“代码本”之间的最小距离超过某个阈值时,可以退回到基于梯形模式的系统。我还没有实现这一点。
呼……这真是太长了!我向Ivo致敬,感谢他提出这样有趣的问题!