使用關鍵點檢測打造小工具Padoodle,讓塗鴉小人跟隨真人學跳舞

使用關鍵點檢測打造小工具Padoodle,讓塗鴉小人跟隨真人學跳舞

Windows自帶的畫圖軟體曾陪伴了我小時候最初接觸電腦的幾年時光,這個簡單的小工具對於小時候的我來說神奇,彷彿什麼都可以畫出來。我畫過很多人像,也曾幻想著讓這些人像跟著我一起動起來(當時還不知道有flash這種東西)。前段時間,我使用飛槳實現了一個讓塗鴉動起來的小專案,今天來和大家分享我小時候的想法——讓塗鴉小人跟著人動起來。

使用關鍵點檢測打造小工具Padoodle,讓塗鴉小人跟隨真人學跳舞

使用關鍵點檢測打造小工具Padoodle,讓塗鴉小人跟隨真人學跳舞

MetaAI釋出的專案

關鍵點檢測

想讓塗鴉小人和我做出一樣的動作,首先我們需要一個人體關鍵點檢測模型。飛槳提供人體關鍵點檢測模型的有飛槳預訓練模型應用工具PaddleHub與飛槳目標檢測套件PaddleDetection。本專案使用的是PaddleHub中的human_pose_estimation_resnet50_mpii模型。這個模型相對於openpose更快,但是效果稍微差一點,如果對塗鴉小人動作的準確度需求特別高,或者希望模型返回關鍵點座標的置信度資訊的,可以試試openpose_body_estimation模型,或是PaddleDetection中的HRNet系列模型和最新的PP-TinyPose。

PaddleDetection連結:

https://

github。com/PaddlePaddle

/PaddleDetection

PaddleHub連結:

https://

github。com/PaddlePaddle

/PaddleHub

由於塗鴉中的每一個點都要與骨骼點繫結在一起,所以骨骼點並不能太少,否則與骨骼點不相近的點也會因為一些特殊原因繫結在一起。我們需要對檢測出的骨骼點K進行一定的擴充,本專案在每兩個相鄰骨骼點之間計算中點作為擴充的骨骼點,然後重複這個操作兩次。human_pose_estimation_resnet50_mpii這個模型的訓練資料集是mpii,有16個關鍵點。我們先在“thorax”和“pelvis”兩個關鍵點之間建立一箇中心點作為所有關鍵點的根。然後構建我們整個人體的一個以關鍵點為結點的樹形結構。這個樹形結構在後面都會用到。最終,我們擴充套件後的關鍵點個數為65,這裡將這個擴充套件的關鍵點組記為K’。將關鍵點檢測模型封裝一下:

class estUtil(): #封裝的關鍵點檢測類 def __init__(self): super(estUtil, self)。__init__() # 使用human_pose_estimation_resnet50_mpii模型 self。module = hub。Module(name=‘human_pose_estimation_resnet50_mpii’) def do_est(self, frame): res = self。module。keypoint_detection(images=[frame], use_gpu=True) return res[0][‘data’]

擴充關鍵點的方法:

def complexres(res, FatherAndSon):#擴充關鍵點,但仍然要保持邏輯上的關鍵點的節點順序 cres = copy。deepcopy(res) for key,pos in res。items(): father = FatherAndSon[key] if father == key:#當時根節點的時候停止 continue if key[0] == ‘m’ or father[0] == ‘m’:#子節點第一種命名規則 midkey = ‘m’+key+‘_’+father else: kn = ‘’ for t in key。split(‘_’): kn += t[0] fn = ‘’ for t in father。split(‘_’): fn += t[0]#子節點第二種命名規則 midkey = ‘m_’+kn+‘_’+fn#計算中點,並把結果按邏輯順序存到字典中 midvalue = [(pos[0] + res[father][0]) / 2, (pos[1] + res[father][1])/2] FatherAndSon[key] = midkey FatherAndSon[midkey] = father cres[midkey] = midvalue return cres, FatherAndSon

使用關鍵點檢測打造小工具Padoodle,讓塗鴉小人跟隨真人學跳舞

左為擴充套件前的關鍵點k,右為擴充套件後的關鍵點k’。

塗鴉的記錄與最佳化

為了方便互動,我使用OpenCV製作了一個簡單的小畫板,使用者可以選擇不同的顏色來繪製塗鴉。這裡為了後續繫結關鍵點與塗鴉,我先將模板的人體關鍵點畫在了畫布上,使用者就擁有了一套參考座標,更方便地繪製自己的塗鴉小人。

OpenCV會在使用者按下滑鼠後不斷地採集我們的畫筆(滑鼠)的當前位置(Mouse_x,Mouse_y)。當用戶鬆開滑鼠後,程式則停止記錄。將剛才記錄的點依次連線起來,就是我們滑鼠剛才點選並移動的軌跡。一幅畫可以由一筆完成,也可以多筆多個顏色來完成。我們把這裡得到的塗鴉小人記作B。由於OpenCV是按照一定的幀率來採集我們的畫筆位置,對於同樣長的一條線,如果我們畫得慢,則取樣的點就會多;如果我們畫得快,則取樣的點就會少。在後續的使用中,因為要大量的計算取樣點和骨骼點的相對關係(透過評估,這部分的時間會遠遠大於模型的運算時間,成為了流暢執行的瓶頸),所以這裡我們要對這些取樣點B進行過濾,我這裡使用最直觀的過濾方法,即當連續的三個點在同一條直線上時,將中間的那個點過濾掉,只保留兩個端點。透過這個方法,一個簡單的塗鴉小人的點數可以從幾千個降低到幾十個,專案也能更平滑的執行。這裡我們將過濾後的取樣點記為B’。過濾簡化面板資料的方法:

def linesFilter(): global lines for line in lines: linelen = len(line) sindex = 0 mindex = 1 while mindex < len(line): eindex = mindex + 1 if eindex >= len(line): break d1 = line[mindex][0] - line[sindex][0] d2 = line[mindex][1] - line[sindex][1] d3 = line[eindex][0] - line[sindex][0] d4 = line[eindex][1] - line[sindex][1]#判斷三個點是否在一條直線上 if abs(d1*d4-d2*d3) <= 1e-6: line。pop(mindex) else: sindex += 1 mindex += 1def linesCompose():#防止刪除點過多,在每兩個點中間插值出一個新的點 global lines tlines = [] for line in lines: tlines。append([line[0]]) for i in range(1,len(line)): l_1 = tlines[-1][-1] tlines[-1]。append(((l_1[0] + line[i][0]) / 2,(l_1[1] + line[i][1]) / 2)) tlines[-1]。append((line[i])) lines = tlines

關鍵點與塗鴉的繫結

錨點繫結數量:在動畫開始之前,還有非常重要的一步,就是要把我們取樣後畫的面板B’和擴充後的關鍵點組K’進行繫結。透過上面的描述,我們知道面板B’其實就是一個個點,這個過程就是面板的點和關鍵點之間的繫結,更專業的詞語來說,我們要為面板點選擇它們的錨點(Anchor),這些錨點都是來源於骨骼關鍵點。在這個專案中我們每個面板點和最多四個關鍵點繫結,這個數量和我們關鍵點K’的個數有關,當我們的關鍵點K’足夠稠密的時候,我們的錨點數量是可以少一點。

錨點繫結標準:這裡選定錨點的衡量標準為距離,也就是選擇面板點n最近的m個關鍵點。這種方法有缺點,譬如在例子中,由於我們選擇的是最近的關鍵點,在我們畫的鬍鬚的對應的錨點中,我們肩部的錨點反而比臉部的某些點更近,這就導致鬍鬚會跟隨者我們的肩膀來運動。如果想要更精確的匹配錨點,也可以人為干預這個過程,刪除某些上述類似的不合理的繫結。

使用關鍵點檢測打造小工具Padoodle,讓塗鴉小人跟隨真人學跳舞

左為我們畫好的塗鴉,右為塗鴉和關鍵點繫結的效果

繫結面板資料和骨骼資料:

def buildskin(lines, colors, cirRads, nodes): if lines is None or nodes is None or len(lines) == 0 or len(nodes) == 0: return [] skins = [] print(“doodle node length”, len(nodes)) #將opencv獲取的面板點列表封裝成skinItem類的物件列表 for lineindex in range(len(lines)): init = True line = lines[lineindex] color = colors[lineindex] cirRad = cirRads[lineindex] for p in line: if init: skins。append(skinItem(p[0], p[1], True, color, cirRad)) init = False else: skins。append(skinItem(p[0], p[1], False, color, cirRad)) #計算每個skinItem物件最近的四個骨骼點並封裝為錨點 for skin in skins: md = [float(“inf”), float(“inf”), float(“inf”), float(“inf”)] mn = [None, None, None, None] mdlen = 0 for key,node in nodes。items(): d = distance(skin。getPos(), node。getPos()) maxi = judge(md) if d < md[maxi]: md[maxi] = d mn[maxi] = node mdlen += 1 if mdlen < 4: md = md[:mdlen] mn = mn[:mdlen] ws = dist2weight(md) # 分配每個錨點的權重 for j in range(len(mn)): th = math。atan2(skin。y-mn[j]。y, skin。x-mn[j]。x) r = distance(skin。getPos(), mn[j]。getPos()) w = ws[j] skin。appendAnchor(anchorItem(mn[j], th-mn[j]。thabs, r, w)) return skins

塗鴉的更新

在我們做好前一步的初始化後,就可以在之後的每一幀中,計算面板點的新位置了。在前一步繫結的時候,我們同時還記錄了一些錨點的其他資訊:該面板點與錨點的距離和角度資訊。在得到四個錨點之後,我們還要計算一個初始權重α。這樣一來,當我們的關鍵點的位置變化了之後,我們能根據錨點的新位置計算出一個加權的面板點的新位置S’’。我們按照S’’的順序把面板都畫出來,就完成了整個專案。

使用關鍵點檢測打造小工具Padoodle,讓塗鴉小人跟隨真人學跳舞

在每幀中根據新的骨骼計算新的面板點:

def calculateSkin(skins, scale): for skin in skins: xw = 0 yw = 0#根據面板點每個錨點的座標與角度,計算出新的面板點的座標 for anchor in skin。getAnchor(): x = anchor。node。x + math。cos(anchor。th+anchor。node。thabs) * anchor。r * scale y = anchor。node。y + math。sin(anchor。th+anchor。node。thabs) * anchor。r * scale xw += x * anchor。w yw += y * anchor。w skin。x = xw skin。y = yw return skins

目前的一些問題與改進方向

1)問題human_pose_estimation_resnet50_mpii這個模型很大的一個缺點就是沒有關節的置信度的輸出,因此我們沒有辦法對結果進行過濾。如果輸入不完整的人體影象,模型仍然會輸出16個關鍵點,其中本應不在影象中的關鍵點也會存在,這些虛假的結果點會導致面板點也畫錯的現象。除此之外,為了更好的效果,輸入的影片最好背景少一點,減少一些影響因素。2)改進方向

大家可以嘗試使用飛槳目標檢測套件PaddleDetection中的其他的關鍵點檢測模型,值得注意的是,如果你使用的模型是基於COCO資料集,需要更改doodle檔案。

為了能有更流暢的體驗,可以嘗試把關鍵點檢測模型放到另一個執行緒裡,這樣的效果會更好一點。

我在飛槳開發者說專欄也直播進行了本專案的分享,歡迎大家移步B站觀看影片分享。

分享影片:

https://www。

bilibili。com/video/BV1N

34y1y72o

?spm_id_from=333。999。0。0

專案連結:

https://

aistudio。baidu。com/aist

udio/projectdetail/2498845

PaddleDetection:

https://

github。com/PaddlePaddle

/PaddleDetection

PaddleHub:

https://

github。com/PaddlePaddle