这个项目介绍了如何制作和控制一只仿生手。作者最初受到Instagram上一个视频的启发,该视频展示了使用MPU6050传感器追踪手部动作并在屏幕上显示3D模型。作者决定将这个想法进一步发展,使用OpenCV来控制一只真实的仿生手。

大家好,在这篇教程中,我想和大家分享一下如何制作并控制一只自己的仿生手。这个想法源于我在无意中刷Instagram时,看到一段短视频:一个人通过MPU6050传感器来跟踪手部运动,并在屏幕上显示手的3D模型。因为我之前也使用过这个传感器,所以觉得这个我也能完成。我一直喜欢将编程与现实世界结合起来,于是我想,为什么不将这些测量数据传输到一个真实的仿生手上呢?后来我决定,使用OpenCV代替MPU6050会更加高效,部分原因也是我想借此机会学习另一种技能。
特别感谢Gaël Langevin,他在InMoov项目[1]中设计了这个手的模型,并慷慨地分享了出来。
效果展示
所需材料
InMoov手及前臂
- 3D打印机
- 焊接工具
- 约1公斤的耗材(PETG 或 ABS 或 PLA)
- 3米钓鱼线(能承重约20公斤)
- 5根扩展弹簧(3/16″ x 1-3/4)
- RTV硅胶 Ecoflex 00-10
螺丝、螺母和螺栓
- 10个M2x4平头木螺丝
- 10个M3x4mm平头螺丝
- 4个M3x12mm平头木螺丝
- 20个M3x12mm平头螺丝
- 25个M3x16mm平头螺丝
- 10个M3x20mm平头螺丝
- 35个M3螺母
电子元件
- 1块ESP32 38-pin 开发模块
- 1根micro USB数据线
- 5个线性霍尔传感器(49E)
- 5个直径2.5mm x 1mm的磁盘磁铁
- 1根16芯彩排线
- 5个1k电阻
- 5个2k电阻
- 6个伺服电机(JX PDI-6225MG-300)
- 1块定制PCB(可选)
- 1个电源(理想情况下为6V或5V,功率约100W,因为每个伺服电机的电流可达3A)
步骤1:3D打印手部


3D打印文件见文末。

打印时,建议使用稍高的填充率(约30%),以提高部件的耐用性。关于材料,InMoov使用的是ABS,不过如果你没有稳定打印ABS的设备,PETG或PLA同样可以使用。
步骤2:3D打印前臂

同样地,手部所需的文件如下,并且也在inmoov STL零件库[2]中。请注意,在inmoov零件库中有原版inmoov机器人的文件。这个手是i2版本,因此你只需要前臂部分的一些零件。另外一个需要注意的是,当打印Bolt_entretoise7时,你只需要中间的螺栓和夹子(其他部分是为旧版手设计的)。

3D打印文件文末下载。
你还可以打印一个我自己在Fusion 360中设计的小展示支架。

链接:https://www.printables.com/model/593999-inmoov-hand-stand?lang=cs
步骤3:组装

在组装时,可以参考InMoov提供的hand i2[3]与前臂[4]的教程,这些教程非常详细,提供了所有必要的信息。



初始部件的组装相对简单,只需用螺丝将整个设计固定在一起。稍微复杂的部分是确保钓鱼线的布置不打结,以及将霍尔传感器正确安装在指尖。
步骤4:硅胶指尖



对于指尖来说,使用非常柔软的硅胶是很重要的,因为霍尔传感器的读取有一定的不确定性。硅胶越软,内部的磁铁运动幅度越大,从而更容易从数据中识别。将硅胶部分粘到3D打印出的部件上之后,可以用它来调整霍尔传感器的突出程度。

在这一切设置好之后,强烈建议将霍尔传感器固定在手指的末端,否则在手指运动过程中,霍尔传感器可能会稍微移动,从而影响测量结果。
步骤5:电路


电路方面,使用16路舵机驱动模块会带来显著的效果,但也存在一些缺点。该驱动模块有两种不同的版本,虽然它们几乎相同,但在反极性保护电路(用于电容)所使用的晶体管上有区别,一个版本可承受约8A电流,而另一个版本仅可承受约0.5A,这远低于舵机实际需要的电流。因此,最好不要让伺服电机通过驱动模块供电,或者按照视频[5]中所述进行小改动,并在使用电容时要格外小心。
关于霍尔传感器,我们需要使用一个电压分压器,因为它输出的电压范围在0V到5V之间,而ESP32只能正确读取0V到3.3V的ADC值。
对于整个电路,可以选择使用面包板,或者更好的是使用定制PCB(作者版本的GitHub链接[6])。
步骤6:测试
由于每个伺服电机和霍尔传感器都略有不同,所以需要对它们进行测试。
最重要的是测试霍尔传感器,因为它们测量的值将决定仿生手是否施加了足够的压力。我建议使用Arduino IDE的绘图功能来绘制数据,以观察数值何时超过自然不确定性范围。
为此,我们可以使用这个非常简单的代码片段:
inthall="Pinnumberyourhallsensorisconnectedto";
voidsetup(){
Serial.begin(115200);
pinMode(hall,INPUT);
}
voidloop(){
Serial.println(analogRead(hall));
delay(10);
}
步骤7:代码
OpenCV(在VSCode中运行的Python代码)
就运行在带有网络摄像头的PC上的代码而言,我们需要完成两个主要任务:
第一个任务是使用OpenCV追踪手部及其元素。基于这些元素我们可以计算每根手指的位置。
第二个任务是通过串口将数据发送到ESP32,以便控制伺服电机。这些数据可以相对简化,因为我们不需要发送精确的角度值,而只需发送每个手指是否弯曲的信息。因此,我们可以发送五个0或1,并在末尾加一个符号以便后续识别每个数字的索引。
这种方法将手部追踪和数据传输简化为一个二进制状态系统,使得数据处理和传输更加高效,同时仍能提供足够的信息来控制仿生手的动作。
首先,我们需要为Python代码导入以下库:
importcv2
importmediapipeasmp
importtime
importserial
然后,我们需要创建一个用于处理摄像头数据的类:
classHandDetector():
#Constructoroftheclasswithparametersforthemeasurement
def__init__(self,mode=False,maxHands=1,detectionCon=0.5,trackCon=0.5):
self.mode=mode
self.maxHands=maxHands
self.detectionCon=detectionCon
self.trackCon=trackCon
self.mpHands=mp.solutions.hands
self.hands=self.mpHands.Hands()
self.mpDraw=mp.solutions.drawing_utils
#Functionforfindinganddrawingthehand
deffindHands(self,frame,draw=True):
imgRGB=cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)
self.results=self.hands.process(imgRGB)
ifself.results.multi_hand_landmarks:
forhandLmsinself.results.multi_hand_landmarks:
ifdraw:
self.mpDraw.draw_landmarks(frame,handLms,self.mpHands.HAND_CONNECTIONS)
returnframe
#Functionforfindingeachhandlandmarkanddrawingitsposition
deffindPosition(self,frame,handNo=0,draw=False):
lmList=[]
ifself.results.multi_hand_landmarks:
myHand=self.results.multi_hand_landmarks[handNo]
forid,lminenumerate(myHand.landmark):
h,w,c=frame.shape
cx,cy=int(lm.x*w),int(lm.y*h)
lmList.append([id,cx,cy])
ifdrawandid==0:
cv2.circle(frame,(cx,cy),15,(255,0,255),-1)
returnlmList
接下来定义主函数:
defmain():
#TheprevTimeandcurrentTimeareusedtocalculatetheFPSlater
prevTime=0
currentTime=0
#Arrayforstoringtheinfoaboutthehand
hand=[["Wrist",False],["Index",False],["Middle",False],
["Ring",False],["Thumb",False],["Pinky",False]]
#InitializingtheSerialandopencv
ser=serial.Serial(port="ThenameoftheporttheESP32isconnectedto")
#Ihadtoincludethe"cv2.CAP_DSHOW"becauseIhadissueswiththewebcamloadingonmylinuxmachine
cap=cv2.VideoCapture(0,cv2.CAP_DSHOW)
detector=HandDetector()
#MAINLOOPOFTHECODE#
#Releasingthestuffallocatedforopencv
cap.release()
cv2.destroyAllWindows()
main()
以及代码的主循环:
while(True):
#Findingthehandsandreadingthepositionofhtelandmarks
ret,frame=cap.read()
frame=detector.findHands(frame)
lmList=detector.findPosition(frame)
iflen(lmList)>0:
j=1
change=False
#Loopwhichchecksifthetopofthefingerisbelowthesecondmosttop
foriinrange(1,6):
ifi==1andlmList[4][1]< lmList[3][1]andnothand[4][1]:
#Incasethatitistrueitchangesalltheneededdata
hand[4][1]=True
change=True
print(hand[4][0],hand[4][1])
elifi==1andlmList[4][1]>lmList[3][1]andhand[4][1]:
hand[4][1]=False
change=True
print(hand[4][0],hand[4][1])
elifi!=1:
iflmList[i*4][2]>lmList[(i*4)-2][2]andnothand[j][1]:
hand[j][1]=True
change=True
print(hand[j][0],hand[j][0])
eliflmList[i*4][2]< lmList[(i*4)-2][2]andhand[j][1]:
hand[j][1]=False
change=True
print(hand[j][0],hand[j][0])
ifj==3:
j+=2
else:
j+=1
#Iftherehasbeenanychangeinthestateofthehandthiscodeblockwillrun
ifchange:
msg=""
#Convertsthebooleanvaluesto0sand1s
foriinrange(6):
ifhand[i][1]:
msg+="1"
else:
msg+="0"
#AddstheendingsymbolandsendsthedataovertotheESP32
msg+='\n'
print(msg)
ser.write(msg.encode("Ascii"))
#CalculatestheFPSanddisplaysitontheframe
currentTime=time.time()
fps=1/(currentTime-prevTime)
prevTime=currentTime
cv2.putText(frame,str(int(fps)),(10,70),cv2.FONT_HERSHEY_SIMPLEX,3,(255,0,255),3)
#Showswhatthewebcamseesonaframe
cv2.imshow("frame",frame)
#Ifwepress"q"itquitsrunningtheprogram
ifcv2.waitKey(1)&0xFF==ord("q"):
break
整个代码 OpenCV:
importcv2
importmediapipeasmp
importtime
importserial
classHandDetector():
def__init__(self,mode=False,maxHands=2,detectionCon=0.5,trackCon=0.5):
self.mode=mode
self.maxHands=maxHands
self.detectionCon=detectionCon
self.trackCon=trackCon
self.mpHands=mp.solutions.hands
self.hands=self.mpHands.Hands()
self.mpDraw=mp.solutions.drawing_utils
deffindHands(self,frame,draw=True):
imgRGB=cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)
self.results=self.hands.process(imgRGB)
ifself.results.multi_hand_landmarks:
forhandLmsinself.results.multi_hand_landmarks:
ifdraw:
self.mpDraw.draw_landmarks(frame,handLms,self.mpHands.HAND_CONNECTIONS)
returnframe
deffindPosition(self,frame,handNo=0,draw=False):
lmList=[]
ifself.results.multi_hand_landmarks:
myHand=self.results.multi_hand_landmarks[handNo]
forid,lminenumerate(myHand.landmark):
h,w,c=frame.shape
cx,cy=int(lm.x*w),int(lm.y*h)
lmList.append([id,cx,cy])
ifdrawandid==0:
cv2.circle(frame,(cx,cy),15,(255,0,255),-1)
returnlmList
defmain():
prevTime=0
currentTime=0
hand=[["Wrist",False],["Index",False],["Middle",False],
["Ring",False],["Thumb",False],["Pinky",False]]
ser=serial.Serial(port="COM3")
cap=cv2.VideoCapture(0,cv2.CAP_DSHOW)
detector=HandDetector()
while(True):
ret,frame=cap.read()
frame=detector.findHands(frame)
lmList=detector.findPosition(frame)
iflen(lmList)>0:
j=1
change=False
foriinrange(1,6):
ifi==1andlmList[4][1]< lmList[3][1]andnothand[4][1]:
hand[4][1]=True
change=True
print(hand[4][0],hand[4][1])
elifi==1andlmList[4][1]>lmList[3][1]andhand[4][1]:
hand[4][1]=False
change=True
print(hand[4][0],hand[4][1])
elifi!=1:
iflmList[i*4][2]>lmList[(i*4)-2][2]andnothand[j][1]:
hand[j][1]=True
change=True
print(hand[j][0],hand[j][0])
eliflmList[i*4][2]< lmList[(i*4)-2][2]andhand[j][1]:
hand[j][1]=False
change=True
print(hand[j][0],hand[j][0])
ifj==3:
j+=2
else:
j+=1
ifchange:
msg=""
foriinrange(6):
ifhand[i][1]:
msg+="1"
else:
msg+="0"
msg+='\n'
print(msg)
ser.write(msg.encode("Ascii"))
currentTime=time.time()
fps=1/(currentTime-prevTime)
prevTime=currentTime
cv2.putText(frame,str(int(fps)),(10,70),cv2.FONT_HERSHEY_SIMPLEX,3,(255,0,255),3)
cv2.imshow("frame",frame)
ifcv2.waitKey(1)&0xFF==ord("q"):
break
cap.release()
cv2.destroyAllWindows()
main()
ESP32(Arduino IDE)
我们可以充分利用ESP32是双核这一特点,类似于PC的代码,我们同样需要完成两项主要工作。
首先是接收来自PC的数据。正如前面提到的,数据基本上是一个带有六位二进制数和结束符的字符串。此外,由于只有在状态变化时才会传输数据,我们可以立即将这些值(转换为true或false)分配给相应的变量。将这个任务分配给核心0,而主循环则在核心1上运行。
第二项工作就是控制手部运动。为此,我们需要不停地检查这些变量的状态是否发生变化,一旦有变化,伺服电机就会按小步长进行线性移动。在每一步后,首先需要检查变量是否没有再次变化,并且还要测量霍尔传感器读取的值。如果霍尔传感器的值过高,意味着磁铁距离手指核心太近,此时也要停止伺服电机的运动。
最初,我们需要用于伺服驱动的库,并且还将包含用于I2C通信的Wire库:
#include
#include
之后,我们需要定义脉冲长度的值,这些值因伺服类型而异,所以强烈建议查找特定伺服的信息或者像这样测试[7]它们。
//OperatingSpeedofmyServo(6V):0.21sec/60°
#defineSERVOMIN"Yourvalue(minewas70)"//Thisisthe'minimum'pulselengthcount(outof4096)
#defineSERVOMAX"Yourvalue(minewas510)"//Thisisthe'maximum'pulselengthcount(outof4096)
#defineSERVO_FREQ50//Analogservosrunat~50Hzupdates
现在我们必须定义其余要使用到的变量:
//Initializingservodriverobject
Adafruit_PWMServoDriverpwm=Adafruit_PWMServoDriver();
//Index,Middle,Ring,Thumb,Pinky
//"state0"isthestatethehandonthewebcamisinand"state"
//isthestuffhappeningontheactualhand
boolstate0[6]={false,false,false,false,false,false};
boolstate[6]={false,false,false,false,false,false};
//Variablewhichindicatesiftherehasbeenanychangemadetothestate
boolchange=false;
//VariablesneededforreadingthedatafromSerial
charsData;
Stringstate;
//Variableforthehallsensor
//Index,Middle,Ring,Thumb,Pinky
//{pin,measuredvalue,maximumvalue}
//ALLOFTHEMAXVALUESWEREMEASUREDBYMETHUSTHEYWILLMOSTLIKELYNOTBESAMEFORYOU
inthall[5][3]={{26,0,2200},{27,0,2400},{14,0,2300},{25,0,2200},{12,0,2300}};
//Settingtheindexnumbersofeachmotor
intwrist=0;
intthumb=4;
intindex=1;
intmiddle=2;
intring=3;//IMPORTANTthismotorwillrotateintheopositedirection
intpinky=5;//IMPORTANTthismotorwillrotateintheopositedirection
//FunctionforcalculatingthePWMbasedonthedegreeyouwant
intdegToPwm(intdegree){
returnmap(degree,0,320,SERVOMIN,SERVOMAX);
}
//Settingthedegreethresholdsused
intdeg=degToPwm(75);
intdeg1=degToPwm(95);
intdeg2=degToPwm(85);
intstartDeg=degToPwm(180);
接下来,需要定义我们将要使用的函数:
//Initializationofthetask
TaskHandle_trecieveData;
//FunctionwhichreadsthedatafromSerial
voidrecieveDataCode(void*parameter){
for(;;){
//Loopwhichrunswhenthereisamessagesent
while(Serial.available()){
//Readingbyeachcharacter
sData=Serial.read();
//Ifthecharacteristhelineendingsymbolweknowitistheendofthemessage
if(sData=='\n'){
//Loopforconvertingthestring0sand1stoboolean
for(inti=0;i< 6;i++){
state0[i]=state.substring(i,i+1).toInt();
}
//Resetingthestatetemporaryvariable
state="";
//Showingachangeinstatehappened
change=true;
break;
}else{//Ifthecharacterisnotthelineendingsymbolweaddittothetemporarystate
state+=sData;
}
}
delay(10);
}
}
//Functionforactuallymovingtheservos
voidmoveFinger(intfingerId,boolflex,intiteration){
//Becausetheringandpinkymotorsmoveinoppositedirection
//wehavetocheckwhichmotorswearemoving
if(fingerId!=ring&&fingerId!=pinky){
//Wealsoneedtocheckifwewantthefingertoflexorstraighten
if(flex){
//Moreoverthethumbmovesalittlelesssowealsocheckforthat
if(fingerId==thumb){
//Becausewewanttobeabletocontrolthemovementthroughoutwehaveto
//divideitintosmallerparts
floatfPwm=SERVOMIN+(float(103)*float(iteration))/float(130);
//Butwealsohavetomakesuretoconvertbacktointbecausefloatwould
//notbeacceptedbypwmfunction
intiPwm=round(fPwm);
pwm.setPWM(fingerId,0,iPwm);
}else{//Ifthefingerisnotthethumbwejustmoveit
pwm.setPWM(fingerId,0,SERVOMIN+iteration);
}
}else{//Forthecasethatisretractingwehavetojustdotheopposite
if(fingerId==thumb){
floatfPwm=deg-(float(103)*float(iteration))/float(130);
intiPwm=round(fPwm);
pwm.setPWM(fingerId,0,iPwm);
}else{
pwm.setPWM(fingerId,0,deg1-iteration);
}
}
}elseif(fingerId==ring||fingerId==pinky){
//Inthecaseoftheringorpinkyfingerwedoagainthesame
if(flex){
pwm.setPWM(fingerId,0,startDeg-iteration);
}else{
pwm.setPWM(fingerId,0,deg2+iteration);
}
}
}
补上设置和循环功能:
voidsetup(){
//StartingSerialonthesamefrequencyasonthePC
Serial.begin(9600);
//AssigningthepinModetoallpinsconnectedtohallsensor
for(inti=0;i< 5;i++){
pinMode(hall[i][0],INPUT);
}
//Setupandstartingtheservodriver
pwm.begin();
pwm.setOscillatorFrequency(27000000);
pwm.setPWMFreq(SERVO_FREQ);
delay(10);
//Pinningthecreatedtasktocore0
xTaskCreatePinnedToCore(
recieveDataCode,
"recieveData",
10000,
NULL,
0,
&recieveData,
0);
delay(500);
}
voidloop(){
//Oncetherehasbeenachangeinthestatethiscodeblockwillrun
if(change){
//Loopingfirstlythroughthetotalstepsoftheservos
for(inti=5;i< 135;i+=5){
//Secondlythroughallofthehallsensorsandreadingthevalues
for(intk=0;k< 5;k++){
hall[k][1]=analogRead(hall[k][0]);
//Ifthemeasuredvalueisgreaterthanmaximumvaluewestopthemovement
if(hall[k][1]>hall[k][2]){
state1[k+1]=state0[k+1];
}
}
//Thirdlythroughalltheservomotors
for(intj=0;j< 6;j++){
if(state0[j]!=state1[j]){
//IfthestateonthePCdoesnotmatchtheoneontheesp32we
//callthefunctionformovingtherespectivefinger
moveFinger(j,state0[j],i);
}
}
//Thisdelayisveryimportantasitsetsthespeedofthemovements
delay(17);
}
//Attheandwemakethestatevariablesequalagain
for(inti=0;i< 6;i++){
state1[i]=state0[i];
}
}
delay(100);
}
ESP32的完整代码:
#include
#include
#defineSERVOMIN"Yourvalue"
#defineSERVOMAX"Yourvalue"
#defineSERVO_FREQ50
Adafruit_PWMServoDriverpwm=Adafruit_PWMServoDriver();
boolstate0[6]={false,false,false,false,false,false};
boolstate1[6]={false,false,false,false,false,false};
boolchange=false;
charsData;
Stringstate;
inthall[5][3]={{26,0,2200},{27,0,2400},{14,0,2300},{25,0,2200},{12,0,2300}};
intwrist=0;
intthumb=4;
intindex=1;
intmiddle=2;
intring=3;
intpinky=5;
intdegToPwm(intdegree){
returnmap(degree,0,320,SERVOMIN,SERVOMAX);
}
intdeg=degToPwm(75);
intdeg1=degToPwm(95);
intdeg2=degToPwm(85);
intstartDeg=degToPwm(180);
TaskHandle_trecieveData;
voidrecieveDataCode(void*parameter){
for(;;){
while(Serial.available()){
sData=Serial.read();
if(sData=='\n'){
for(inti=0;i< 6;i++){
state0[i]=state.substring(i,i+1).toInt();
}
state="";
change=true;
break;
}else{
state+=sData;
}
}
delay(10);
}
}
voidmoveFinger(intfingerId,boolflex,intiteration){
if(fingerId!=ring&&fingerId!=pinky){
if(flex){
if(fingerId==thumb){
floatfPwm=SERVOMIN+(float(103)*float(iteration))/float(130);
intiPwm=round(fPwm);
pwm.setPWM(fingerId,0,iPwm);
}else{
pwm.setPWM(fingerId,0,SERVOMIN+iteration);
}
}else{
if(fingerId==thumb){
floatfPwm=deg-(float(103)*float(iteration))/float(130);
intiPwm=round(fPwm);
pwm.setPWM(fingerId,0,iPwm);
}else{
pwm.setPWM(fingerId,0,deg1-iteration);
}
}
}else/*if(fingerId==ring||fingerId==pinky)*/{
if(flex){
pwm.setPWM(fingerId,0,startDeg-iteration);
}else{
pwm.setPWM(fingerId,0,deg2+iteration);
}
}
}
voidsetup(){
Serial.begin(9600);
for(inti=0;i< 5;i++){
pinMode(hall[i][0],INPUT);
}
pwm.begin();
pwm.setOscillatorFrequency(27000000);
pwm.setPWMFreq(SERVO_FREQ);
delay(10);
xTaskCreatePinnedToCore(
recieveDataCode,
"recieveData",
10000,
NULL,
0,
&recieveData,
0);
delay(500);
}
voidloop(){
if(change){
for(inti=5;i< 135;i+=5){
for(intk=0;k< 5;k++){
hall[k][1]=analogRead(hall[k][0]);
if(hall[k][1]>hall[k][2]){
state1[k+1]=state0[k+1];
}
}
for(intj=0;j< 6;j++){
if(state0[j]!=state1[j]){
moveFinger(j,state0[j],i);
}
}
delay(17);
}
for(inti=0;i< 6;i++){
state1[i]=state0[i];
}
}
delay(100);}
原文地址:https://www.instructables.com/Bionic-Hand-Controlled-by-OpenCV/
项目作者:bloudakm
-
传感器
+关注
关注
2574文章
54414浏览量
786251 -
OpenCV
+关注
关注
33文章
651浏览量
44427 -
仿生手
+关注
关注
0文章
15浏览量
10298
发布评论请先 登录
意大利新型仿生手,体验“逼真”的触感!
【开源项目】做一只由 OpenCV 控制的仿生手
分享仿生手的设计方案
Youbionic仿生手变身多臂超人不再是梦!

如何制作和控制一只仿生手
评论