????????前邊我們已經講解了使用cv2進行圖像預處理以及針對實時視頻流文件的操作方法,這里我們通過實時手勢檢測這一案例來學習和實操一下。
大致思路
- 根據手勢的種類以及指定手勢圖片數量來構建一個自己的手勢圖片數據集
- CNN模型訓練手勢圖片數據集
- 使用訓練好的模型進行實時預測
手勢圖片數據集的構建
????????經典的手勢圖片數據集有很多,但是都比較大,下載費時且模型訓練時間長,因此這里我決定自行采集手勢圖片來構建一個小型數據集。手勢圖片的獲取方法比較簡單,就是使用cv2.VideoCapture函數打開攝像頭來進行采集。這里我把我的方法分享給大家。
采集手勢圖片
import cv2
import os
DATASET_DIR='GesturesPhotos'#保存所有待采集手勢的圖片的文件夾的路徑
gesture_kinds=5#手勢種類:單手可以是1-10,我這里是1-5
photo_num=10#圖片數量
classes=list(range(1,gesture_kinds+1,1))#使用1-gesture_kinds來表示所有待預測類別
###############################################
gestures=photo_num//gesture_kinds*classes#photo_num//gesture_kinds=10//5=2,2*[1,2,3,4,5]=[1,2,3,4,5,1,2,3,4,5]
gestures.extend(classes[:photo_num%gesture_kinds])#photo_num%5=10%5=0,extend([:0])相當于extend([])
'''
經過這兩步運算,gestures為長度與圖片數量一致且由類別構成的列表
gestures主要用來標定每次采集的種類
比如,gesture_kinds=5,photo_num=7,手勢種類為5,那么這7次要采集的順序為[1,2,3,4,5,1,2]
'''
###############################################
os.makedirs(DATASET_DIR, exist_ok=True)#exist_ok=True可以避免二次采集時重建新文件夾
def capture_gestures(gesture:str,count:int):'''Args:gesture:每次采集的手勢,要標記在視頻中,防止忘記采集的手勢是多少導致實際類別與真實采集結果不一致從而成為噪聲!\ncount:用來命名每次保存的圖片,這里直接用記錄圖片數量來命名\n'''cv2.namedWindow('Data Collection', cv2.WND_PROP_FULLSCREEN)cv2.setWindowProperty('Data Collection', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)cap=cv2.VideoCapture(0)print(f'采集手勢{gesture}(按ESC保存并退出)') while True:ret,frame=cap.read()if not ret: breakroi=frame[160:440,50:250]#roi區域,可以自行修改cv2.rectangle(frame, (50,160),(250,440),(0,255,0), 2)#roi區域處繪制方框cv2.putText(frame,text=f'No.{count+1} Photo gesture {gesture}',org=(250,100),fontScale=2,thickness=5,color=(0,0,255),fontFace=1)cv2.imshow(f'Data Collection',frame)key=cv2.waitKey(1)if key==27:#按下ESC保存并退出img_path=f'{DATASET_DIR}/{count}.jpg'cv2.imwrite(img_path,roi)break cap.release()cv2.destroyAllWindows()
for i in range(len(gestures)):capture_gestures(gestures[i],i)
?????????運行上述代碼后,便可以開始采集手勢圖片了,這里我使用上述代碼總共采集了200張圖片用于后續CNN模型的訓練。?
說明
????????采集時,將右手放置在視頻中的綠色框內,盡可能的放置在中央,gesture后的數字表示當前要表示的手勢種類。如果采集時出現錯誤,那么只需要刪除掉原來的圖片,自行指定新的類別(gesture)以及原來圖片的編號,調用一次capture_gestures函數重新采集即可。
采集效果?
采集結果(0-199 40組1-5的手勢圖片)
????????這里我沒有對背景進行太多處理,如果有大佬愿意,可以嘗試將采集到的圖片的背景虛化,突出手掌主體。
?數據預處理
? ? ? ?????這里的數據預處理主要就是將我們的圖像數據劃分訓練集與測試集后轉換為tensor類型的DataLoder。
#數據預處理
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms
class GestureDataset(Dataset):def __init__(self, data_dir=DATASET_DIR,gesture_kinds=gesture_kinds,transform=None):self.data_dir = data_dirself.transform = transformself.image_paths = []self.labels = []# 讀取數據集for img_name in os.listdir(data_dir):if img_name.endswith('.jpg'):self.image_paths.append(os.path.join(data_dir, img_name))self.labels.append(int(img_name.split('.')[0])%gesture_kinds)#0-4對于1-5def __len__(self):return len(self.image_paths)def __getitem__(self, idx):img_path=self.image_paths[idx]image=cv2.imread(img_path)image=cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 轉換為RGBlabel=self.labels[idx]if self.transform:image=self.transform(image)return image, labeldef process_data(data_dir=DATASET_DIR, batch_size=4):# 數據預處理transform = transforms.Compose([transforms.ToPILImage(),transforms.Resize((64, 64)),transforms.ToTensor(),transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])])dataset=GestureDataset(data_dir, transform=transform)train_size=int(0.8 * len(dataset))test_size=len(dataset) - train_sizetrain_dataset, test_dataset = torch.utils.data.random_split(dataset, [train_size, test_size])train_loader=DataLoader(train_dataset, batch_size=batch_size, shuffle=True)test_loader=DataLoader(test_dataset, batch_size=batch_size, shuffle=False)return train_loader, test_loader
CNN模型訓練
? ? ? ? 考慮到我的數據集比較少且該分類問題比較簡單,所以這里我的模型也沒有太復雜只是使用了2層卷積操作。倘若你的數據集比較大,分類種類比較多,可以嘗試使用一些其他的CNN模型,比如mobilenet,resnet等。
#CNN模型
class GestureCNN(nn.Module):def __init__(self, num_classes=5):super(GestureCNN, self).__init__()self.conv1=nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1)self.relu=nn.ReLU()self.maxpool=nn.MaxPool2d(kernel_size=2, stride=2)self.conv2=nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)self.fc1=nn.Linear(32*16*16, 128)self.fc2=nn.Linear(128, num_classes)def forward(self, x):x=self.conv1(x)x=self.relu(x)x=self.maxpool(x)x=self.conv2(x)x=self.relu(x)x=self.maxpool(x)x=x.view(x.size(0), -1)x=self.fc1(x)x=self.relu(x)x=self.fc2(x)return xdef train_model(train_loader, test_loader, num_epochs=10):device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')model=GestureCNN(num_classes=5).to(device)criterion=nn.CrossEntropyLoss()optimizer=optim.Adam(model.parameters(), lr=0.001)for epoch in range(num_epochs):model.train()running_loss=0.0correct=0total=0for images, labels in train_loader:images=images.to(device)labels=labels.to(device)optimizer.zero_grad()outputs=model(images)loss=criterion(outputs, labels)loss.backward()optimizer.step()running_loss+=loss.item()_, predicted=torch.max(outputs.data, 1)total+=labels.size(0)correct+=(predicted==labels).sum().item()train_loss = running_loss / len(train_loader)train_acc = 100 * correct / total# 測試集評估model.eval()test_correct = 0test_total = 0with torch.no_grad():for images, labels in test_loader:images=images.to(device)labels=labels.to(device)outputs=model(images)_, predicted=torch.max(outputs.data, 1)test_total+=labels.size(0)test_correct+=(predicted==labels).sum().item()test_acc=100*test_correct/test_totalprint(f'Epoch [{epoch+1}/{num_epochs}], 'f'Train Loss: {train_loss:.4f}, 'f'Train Acc: {train_acc:.2f}%, 'f'Test Acc: {test_acc:.2f}%')# 保存模型torch.save(model.state_dict(), 'gesture_cnn.pth')print('訓練完成,模型已保存為 gesture_cnn.pth')return model
實時預測?
? ? ? ? 實時預測的思路是:打開攝像頭,獲取實時視頻流文件中的每一幀圖片中的手勢,使用訓練好的模型預測并將結果標注在視頻流文件的每一幀上。
#實時預測
def realtime_prediction(model_path='gesture_cnn.pth'):device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')#加載模型model = GestureCNN(num_classes=5).to(device)model.load_state_dict(torch.load(model_path))model.eval()#預處理transform=transforms.Compose([transforms.ToPILImage(),transforms.Resize((64, 64)),transforms.ToTensor(),transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])])cap=cv2.VideoCapture(0)cv2.namedWindow('Gesture Recognition', cv2.WND_PROP_FULLSCREEN)cv2.setWindowProperty('Gesture Recognition', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)CLASSES=gestureswith torch.no_grad():while True:ret, frame = cap.read()if not ret: break # 手勢檢測區域roi = frame[160:440, 50:250]cv2.rectangle(frame, (50, 160), (250, 440), (0, 255, 0), 2)try:input_tensor = transform(cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)).unsqueeze(0).to(device)output = model(input_tensor)_, pred=torch.max(output, 1)probabilities=torch.nn.functional.softmax(output[0], dim=0) confidence, pred=torch.max(probabilities, 0)confidence=confidence.item()*100 #轉換為百分比confidence=round(confidence,2)cv2.putText(frame, f'Prediction: {CLASSES[pred.item()]}', (50, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)cv2.putText(frame,f'confidence:{confidence}',(70,70),cv2.FONT_HERSHEY_SIMPLEX,0.5, (0, 0, 255), 2)except Exception as e:print(f"預測錯誤: {e}")cv2.imshow('Gesture Recognition', frame)if cv2.waitKey(1)==27: breakcap.release()cv2.destroyAllWindows()train_loader, test_loader = process_data()
model=train_model(train_loader, test_loader, num_epochs=10)
realtime_prediction()
?
效果:
?
cv2不支持中文字體,因此只能使用英文來標注……?
總結
????????以上便是計算機視覺cv2入門之實時手勢檢測的所有內容,如果你感到本文對你有用,還勞駕各位一鍵三連支持一下博主。