Ổn thỏa! Cuối cùng tôi đã quản lý để có được một cái gì đó hoạt động ổn định! Vấn đề này đã kéo tôi trong vài ngày ... Thứ thú vị! Xin lỗi vì độ dài của câu trả lời này, nhưng tôi cần phải giải thích một chút về một số điều ... (Mặc dù tôi có thể lập kỷ lục cho câu trả lời stackoverflow không spam dài nhất từ trước đến nay!)
Lưu ý thêm, tôi đang sử dụng tập dữ liệu đầy đủ mà Ivo đã cung cấp liên kết đến trong câu hỏi ban đầu của anh ấy . Đó là một loạt các tệp rar (một tệp cho mỗi tệp), mỗi tệp chứa một số lần chạy thử nghiệm khác nhau được lưu trữ dưới dạng mảng ascii. Thay vì cố gắng sao chép-dán các ví dụ mã độc lập vào câu hỏi này, đây là một kho lưu trữ bitbucket thương mại với toàn bộ mã độc lập. Bạn có thể sao chép nó với
hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis
Tổng quat
Về cơ bản, có hai cách để tiếp cận vấn đề, như bạn đã lưu ý trong câu hỏi của mình. Tôi thực sự sẽ sử dụng cả hai theo những cách khác nhau.
- Sử dụng thứ tự (thời gian và không gian) của các tác động của chân để xác định xem đó là chân nào.
- Cố gắng xác định "dấu chân" hoàn toàn dựa trên hình dạng của nó.
Về cơ bản, phương pháp đầu tiên hoạt động với các bàn chân của con chó theo mô hình giống như hình thang được hiển thị trong câu hỏi của Ivo ở trên, nhưng không thành công bất cứ khi nào các bàn chân không theo mô hình đó. Nó khá dễ dàng để phát hiện theo chương trình khi nó không hoạt động.
Do đó, chúng ta có thể sử dụng các phép đo mà nó đã hoạt động để xây dựng tập dữ liệu huấn luyện (khoảng ~ 2000 chân tác động từ ~ 30 con chó khác nhau) để nhận ra chân nào là chân nào và vấn đề giảm xuống mức phân loại có giám sát (Với một số nếp nhăn bổ sung). .. Nhận dạng hình ảnh khó hơn một chút so với bài toán phân loại có giám sát "bình thường").
Phân tích mẫu
Để nói rõ hơn về phương pháp đầu tiên, khi một con chó đang đi bộ (không chạy!) Bình thường (mà một số con chó này có thể không), chúng tôi mong đợi các bàn chân sẽ tác động theo thứ tự: Phía trước bên trái, phía sau bên phải, phía trước bên phải, phía sau bên trái , Front Left, v.v ... Mẫu có thể bắt đầu với bàn chân trái phía trước hoặc phía trước bên phải.
Nếu điều này luôn xảy ra, chúng ta có thể chỉ cần sắp xếp các tác động theo thời gian tiếp xúc ban đầu và sử dụng mô-đun 4 để nhóm chúng theo chân.
Tuy nhiên, ngay cả khi mọi thứ "bình thường", điều này không hoạt động. Điều này là do hình dạng giống như hình thang của mô hình. Một bàn chân sau rơi vào khoảng không sau bàn chân trước trước đó.
Do đó, tác động của chân sau sau tác động ban đầu của chân trước thường rơi ra khỏi tấm cảm biến và không được ghi lại. Tương tự, tác động chân cuối cùng thường không phải là tác động tiếp theo trong trình tự, vì tác động chân trước khi nó xảy ra khỏi tấm cảm biến và không được ghi lại.
Tuy nhiên, chúng ta có thể sử dụng hình dạng của mô hình tác động chân để xác định thời điểm điều này xảy ra và liệu chúng ta đã bắt đầu với bàn chân trước bên trái hay bên phải. (Tôi thực sự đang bỏ qua các vấn đề với tác động cuối cùng ở đây. Tuy nhiên, không quá khó để thêm nó.)
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
Mặc dù vậy, nó thường không hoạt động chính xác. Nhiều con chó trong tập dữ liệu đầy đủ dường như đang chạy và các tác động lên chân không tuân theo thứ tự thời gian giống như khi con chó đang đi bộ. (Hoặc có lẽ con chó vừa bị bệnh nặng ở hông ...)
May mắn thay, chúng ta vẫn có thể phát hiện theo chương trình xem các tác động chân có tuân theo mô hình không gian dự kiến của chúng ta hay không:
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
Do đó, mặc dù phân loại không gian đơn giản không phải lúc nào cũng hoạt động, nhưng chúng ta có thể xác định thời điểm nó hoạt động với độ tin cậy hợp lý.
Tập dữ liệu đào tạo
Từ các phân loại dựa trên mẫu mà nó hoạt động chính xác, chúng tôi có thể xây dựng một tập dữ liệu huấn luyện rất lớn về các con tốt được phân loại chính xác (~ 2400 tác động của con từ 32 con chó khác nhau!).
Bây giờ chúng ta có thể bắt đầu xem xét mặt trước bên trái "trung bình", v.v., chân trông như thế nào.
Để làm điều này, chúng ta cần một số loại "chỉ số chân" có cùng kích thước cho bất kỳ con chó nào. (Trong tập dữ liệu đầy đủ, có cả chó rất lớn và rất nhỏ!) Hình in chân từ một chú chó săn nai sừng tấm Ailen sẽ rộng hơn và "nặng" hơn nhiều so với hình in chân từ một con chó xù đồ chơi. Chúng ta cần bán lại mỗi bản in chân để a) chúng có cùng số pixel và b) các giá trị áp suất được tiêu chuẩn hóa. Để làm điều này, tôi đã lấy mẫu lại từng bản in chân trên lưới 20x20 và thay đổi tỷ lệ các giá trị áp suất dựa trên giá trị áp suất tối đa, nhỏ nhất và trung bình cho tác động của chân.
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
Sau tất cả những điều này, cuối cùng chúng ta có thể xem một bàn chân trung bình trái trước, sau phải, v.v. trông như thế nào. Lưu ý rằng con số này được tính trung bình trên hơn 30 con chó có kích thước rất khác nhau và dường như chúng tôi đang nhận được kết quả nhất quán!
Tuy nhiên, trước khi thực hiện bất kỳ phân tích nào về những điều này, chúng ta cần phải trừ đi giá trị trung bình (chân trung bình cho tất cả các chân của tất cả các con chó).
Bây giờ chúng ta có thể phân tích sự khác biệt so với giá trị trung bình, dễ nhận ra hơn một chút:
Nhận dạng chân dựa trên hình ảnh
Được rồi ... Cuối cùng thì chúng ta cũng có một tập hợp các mẫu mà chúng ta có thể bắt đầu thử để đối sánh các bàn chân với nhau. Mỗi chân có thể được coi như một vectơ 400 chiều (do paw_image
hàm trả về ) có thể được so sánh với bốn vectơ 400 chiều này.
Thật không may, nếu chúng ta chỉ sử dụng thuật toán phân loại có giám sát "bình thường" (tức là tìm mẫu nào trong 4 mẫu gần nhất với một hình in chân cụ thể bằng cách sử dụng một khoảng cách đơn giản), nó không hoạt động nhất quán. Trên thực tế, nó không tốt hơn nhiều so với cơ hội ngẫu nhiên trên tập dữ liệu đào tạo.
Đây là một vấn đề phổ biến trong nhận dạng hình ảnh. Do dữ liệu đầu vào có kích thước cao và tính chất hơi "mờ" của hình ảnh (tức là các pixel liền kề có hiệp phương sai cao), chỉ cần nhìn vào sự khác biệt của hình ảnh với hình ảnh mẫu sẽ không cho phép đo lường chính xác sự giống nhau về hình dạng của chúng.
Eigenpaws
Để giải quyết vấn đề này, chúng ta cần xây dựng một tập hợp các "eigenpaws" (giống như "eigenfaces" trong nhận dạng khuôn mặt) và mô tả mỗi dấu chân là sự kết hợp của các eigenpaws này. Điều này giống với phân tích các thành phần chính và về cơ bản cung cấp một cách để giảm kích thước của dữ liệu của chúng tôi, do đó khoảng cách là một thước đo tốt về hình dạng.
Bởi vì chúng ta có nhiều hình ảnh đào tạo hơn kích thước (2400 so với 400), không cần phải làm đại số tuyến tính "ưa thích" cho tốc độ. Chúng tôi có thể làm việc trực tiếp với ma trận hiệp phương sai của tập dữ liệu đào tạo:
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
Đây basis_vecs
là những "eigenpaws".
Để sử dụng chúng, chúng tôi chỉ cần chấm (tức là phép nhân ma trận) mỗi hình ảnh chân (dưới dạng vectơ 400 chiều, chứ không phải là hình ảnh 20x20) với các vectơ cơ bản. Điều này cung cấp cho chúng ta một vectơ 50 chiều (một phần tử trên mỗi vectơ cơ sở) mà chúng ta có thể sử dụng để phân loại hình ảnh. Thay vì so sánh hình ảnh 20x20 với hình ảnh 20x20 của mỗi chân "mẫu", chúng tôi so sánh hình ảnh 50 chiều, đã biến đổi với mỗi chân mẫu được biến đổi 50 chiều. Điều này ít nhạy cảm hơn nhiều đối với các biến thể nhỏ về vị trí chính xác của từng ngón chân, v.v. và về cơ bản làm giảm chiều của vấn đề xuống chỉ các chiều có liên quan.
Phân loại Paw dựa trên Eigenpaw
Bây giờ chúng ta có thể chỉ cần sử dụng khoảng cách giữa vectơ 50 chiều và vectơ "mẫu" cho mỗi chân để phân loại chân nào là:
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()]
Dưới đây là một số kết quả:
Các vấn đề còn lại
Vẫn còn một số vấn đề, đặc biệt là với những con chó quá nhỏ để tạo ra một dấu chân rõ ràng ... (Nó hoạt động tốt nhất với những con chó lớn, vì các ngón chân được tách biệt rõ ràng hơn ở độ phân giải của cảm biến.) Ngoài ra, dấu chân một phần không được nhận dạng với điều này hệ thống, trong khi chúng có thể là với hệ thống dựa trên mô hình hình thang.
Tuy nhiên, vì phân tích chữ cái vốn dĩ sử dụng thước đo khoảng cách, chúng ta có thể phân loại các bàn chân theo cả hai cách và quay trở lại hệ thống dựa trên mẫu hình thang khi khoảng cách nhỏ nhất của bản phân tích chữ cái so với "sổ mã" vượt quá một số ngưỡng. Tuy nhiên, tôi vẫn chưa thực hiện điều này.
Phù ... Lâu quá! Tôi ngả mũ trước Ivo vì có một câu hỏi thú vị như vậy!