좋구나! 나는 마침내 일관되게 작동하는 것을 얻었습니다! 이 문제로 며칠 동안 저를 끌어 당겼습니다 ... 재미있는 것들! 이 답변의 길이에 대해 죄송하지만 몇 가지에 대해 조금 더 자세히 설명해야합니다 ... (스팸이 아닌 가장 긴 스택 오버플로 답변에 대한 기록을 세울 수는 있지만!)
참고로 Ivo 가 원래 질문 에서 링크를 제공 한 전체 데이터 세트를 사용하고 있습니다. 일련의 rar 파일 (개당 하나씩)은 각각 ascii 배열로 저장된 여러 다른 실험 실행을 포함합니다. 독립 실행 형 코드 예제를이 질문에 복사하여 붙여 넣는 대신 완전한 독립 실행 형 코드 가있는 bitbucket mercurial 저장소 가 있습니다. 다음으로 복제 할 수 있습니다.
hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis
개요
질문에서 언급했듯이 본질적으로 문제에 접근하는 방법에는 두 가지가 있습니다. 실제로 두 가지를 서로 다른 방식으로 사용할 것입니다.
- 발 충격의 (시간적 및 공간적) 순서를 사용하여 어느 발인지 결정합니다.
- 순전히 모양을 기반으로 "발자국"을 식별하십시오.
기본적으로 첫 번째 방법은 강아지의 발이 위의 Ivo의 질문에 표시된 사다리꼴 모양의 패턴을 따르지만 발이 그 패턴을 따르지 않을 때마다 실패합니다. 작동하지 않을 때 프로그래밍 방식으로 감지하는 것은 매우 쉽습니다.
따라서 우리는 훈련 데이터 세트 (~ 30 개의 다른 개로부터 ~ 2000 개의 발 영향)를 구축하기 위해 작업 한 측정 값을 사용하여 어느 발이 어느 것인지 인식하고 문제는 감독 된 분류 (몇 가지 추가 주름 포함)로 축소됩니다. .. 이미지 인식은 "일반적인"감독 분류 문제보다 약간 더 어렵습니다).
패턴 분석
첫 번째 방법에 대해 자세히 설명하기 위해 개가 정상적으로 걷고있을 때 (달리지 않을 수 있습니다!) (이 개 중 일부는 그렇지 않을 수 있음) 앞발이 앞쪽 왼쪽, 뒷쪽 오른쪽, 앞쪽 오른쪽, 뒷쪽 왼쪽 순서로 충돌 할 것으로 예상합니다. , 전면 왼쪽 등. 패턴은 전면 왼쪽 또는 전면 오른쪽 발로 시작할 수 있습니다.
이것이 항상 그런 경우라면 초기 접촉 시간별로 영향을 분류하고 모듈로 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
따라서 단순 공간 분류가 항상 작동하는 것은 아니지만 합리적인 확신을 가지고 작동하는시기를 결정할 수 있습니다.
훈련 데이터 세트
올바르게 작동 한 패턴 기반 분류에서 올바르게 분류 된 발의 매우 큰 훈련 데이터 세트를 구축 할 수 있습니다 (32 개의 다른 개에서 ~ 2400 개의 발 충격!).
이제 "평균적인"앞 왼쪽 등의 앞발이 어떻게 생겼는지 살펴볼 수 있습니다.
이를 위해서는 모든 개에 대해 동일한 차원 인 일종의 "발 메트릭스"가 필요합니다. (전체 데이터 세트에는 매우 크고 작은 개가 모두 있습니다!) 아일랜드 엘크 하운드의 발 프린트는 토이 푸들의 발 프린트보다 훨씬 넓고 "무겁습니다". 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 마리 이상의 개에 대해 평균을 내고 있으며 일관된 결과를 얻는 것 같습니다!
그러나 이들에 대한 분석을 수행하기 전에 평균 (모든 개의 모든 다리에 대한 평균 발)을 빼야합니다.
이제 우리는 평균과의 차이를 분석 할 수 있습니다.
이미지 기반 발 인식
좋아 .. 드디어 발을 일치시킬 수있는 패턴 세트가 생겼습니다. 각 발은 paw_image
이 4 개의 400 차원 벡터와 비교할 수 있는 400 차원 벡터 ( 함수에 의해 반환 됨 )로 취급 될 수 있습니다.
불행히도 "정상적인"감독 분류 알고리즘을 사용하는 경우 (즉, 간단한 거리를 사용하여 4 가지 패턴 중 어떤 패턴이 특정 발바닥에 가장 가까운지를 찾는 것) 일관되게 작동하지 않습니다. 사실, 그것은 훈련 데이터 세트에서 임의의 기회보다 훨씬 더 나은 것은 아닙니다.
이것은 이미지 인식의 일반적인 문제입니다. 입력 데이터의 높은 차원 성과 이미지의 다소 "모호한"특성 (즉, 인접 픽셀의 공분산이 높음)으로 인해 템플릿 이미지에서 이미지의 차이를 보는 것만으로는 모양의 유사성.
Eigenpaws
이 문제를 해결하기 위해 우리는 "고유 발"세트 (얼굴 인식의 "고유 얼굴"과 마찬가지로)를 만들고 각 발 인쇄를 이러한 고유 발의 조합으로 설명해야합니다. 이것은 주성분 분석과 동일하며 기본적으로 데이터의 차원을 줄이는 방법을 제공하므로 거리가 모양의 좋은 척도가됩니다.
우리는 차원 (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
"고 유발"입니다.
이를 사용하기 위해 기본 벡터로 각 발 이미지 (20x20 이미지가 아닌 400 차원 벡터)를 도트 (예 : 행렬 곱셈)하면됩니다. 이것은 이미지를 분류하는 데 사용할 수있는 50 차원 벡터 (기본 벡터 당 하나의 요소)를 제공합니다. 20x20 이미지를 각 "템플릿"발의 20x20 이미지와 비교하는 대신 50 차원 변환 이미지를 각 50 차원 변환 템플릿 발과 비교합니다. 이것은 각 발가락이 정확히 어떻게 위치하는지 등의 작은 변화에 훨씬 덜 민감하며 기본적으로 문제의 차원을 관련 차원으로 줄입니다.
Eigenpaw 기반 Paw 분류
이제 각 다리에 대해 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()]
결과는 다음과 같습니다.
남은 문제
여전히 몇 가지 문제가 있습니다. 특히 개가 너무 작아서 명확한 발자국을 만들 수없는 경우 ... (발가락이 센서의 해상도에서 더 명확하게 분리되므로 큰 개에서 가장 잘 작동합니다.) 또한이 방법으로 부분 발자국이 인식되지 않습니다. 시스템은 사다리꼴 패턴 기반 시스템을 사용할 수 있습니다.
그러나 eigenpaw 분석은 본질적으로 거리 측정법을 사용하기 때문에 발을 양방향으로 분류 할 수 있으며 "코드북"에서 eigenpaw 분석의 최소 거리가 임계 값을 초과 할 때 사다리꼴 패턴 기반 시스템으로 돌아갈 수 있습니다. 그래도 아직 구현하지 않았습니다.
휴 ... 깁니다! 내 모자는 그런 재미있는 질문에 대해 Ivo에게 떨어져 있습니다!