服务机器人无接触手势交互控制系统开发
无接触手势交互是新一代商用服务机器人主流人机交互方案,广泛应用于商场导购、养老陪护、政务大厅引导机器人,无需物理触控即可调节语音播报音量、切换交互模式;基于视觉手势关键点识别开发无接触控制程序,是服务机器人应用技术员人机交互模块开发核心工作内容,要求实现手势距离与机器人语音输出参数线性映射,保障机器人面向老人、儿童等群体交互流畅稳定。
任务四:机器人手势无接触音量交互控制系统开发
本任务基于实验箱外接摄像头,通过识别拇指与食指指尖的距离,线性映射为系统音量大小,实现“手势滑动调节灯光”功能,具体要求如下:
1.调用外接摄像头,在窗口内实时显示画面、手掌关键点(带连接线条)、拇指与食指指尖标记(紫色圆点)及两点连线(紫色线条)。
2.手势控制逻辑:

  • 识别手掌关键点,提取拇指指尖和食指指尖的像素坐标;
  • 利用勾股定理计算两点距离,将距离范围[50,300]线性映射为系统RGB灯光亮度(0%-100%);
  • 距离越远,灯光越亮;距离越近,灯光越暗,实时同步调节。
    3.实时显示信息:
  • 窗口左下角显示灯光亮度百分比(格式:XX%,字体大小30,颜色蓝色);
  • 窗口左侧显示亮度进度条(蓝色矩形,高度随音量变化);
  • 窗口左上角显示帧率(格式:FPS:XX,字体大小30,颜色蓝色)。
    4.程序稳定性:
  • 手掌识别准确率≥80%,无频繁误触发;
  • 按下“ESC”键可正常退出程序,释放摄像头及系统资源。
    创建名为04.py的文件,存储至任务一的文件夹中,现场演示并验证功能。
    程序如下:

导入试验箱RGB灯环驱动模块,用来控制硬件LED灯环

from exboard import RGB

导入OpenCV计算机视觉库,负责摄像头读取、图像处理、绘图操作

import cv2

导入numpy数值运算库,用于数组、阈值矩阵、数学运算

import numpy as np

导入时间库,用来计算画面帧率FPS

import time

====================== 【步骤1:全局参数初始化】 ======================

设置摄像头设备编号为0,选用本机0号USB摄像头

CAM_ID = 0

拇指食指之间最小触发像素距离,小于该距离亮度归零

MIN_DIST = 50

拇指食指之间最大有效像素距离,超过之后亮度达到最大值

MAX_DIST = 300

计算有效识别距离区间的总跨度

DIST_RANGE = MAX_DIST - MIN_DIST

OpenCV颜色定义,颜色顺序为BGR;紫色 (B=255,G=0,R=255)

PURPLE = (255, 0, 255)

蓝色 (B=255,G=0,R=0),用于进度条、文字

BLUE = (255, 0, 0)

灰色,亮度进度条外框底色

GRAY_COLOR = (80, 80, 80)

创建RGB灯环硬件实例

rgb = RGB()

初始化24颗LED,全部设置为黑色熄灭状态

rgb.set([(0, 0, 0)] * 24)

上一帧的时间戳,用来计算每秒帧数

prev_time = 0

HSV颜色空间下肤色下限阈值

LOWER_SKIN = np.array([0, 10, 60], np.uint8)

HSV颜色空间下肤色上限阈值

UPPER_SKIN = np.array([25, 255, 255], np.uint8)

手掌轮廓长宽比上限,超过该数值判定为长条手臂轮廓直接舍弃

MAX_HAND_RATIO = 1.5

手掌轮廓最小像素面积,过滤微小噪点产生的无效轮廓

MIN_HAND_AREA = 1500

====================== 【步骤2:封装工具函数】 ======================

def get_hand_contour(origin_frame):
“”"
肤色分割 + 长宽比过滤,只识别手掌、过滤长条手臂
:param origin_frame: 原始BGR图像
:return: 手部轮廓,找不到返回None
“”"
# 将BGR格式原图转换至HSV色彩空间,方便进行肤色筛选
hsv = cv2.cvtColor(origin_frame, cv2.COLOR_BGR2HSV)
# 根据肤色阈值生成二值掩码,肤色区域变为白色,其余区域黑色
skin_mask = cv2.inRange(hsv, LOWER_SKIN, UPPER_SKIN)
# 高斯模糊处理掩码,平滑画面、消除细小噪点
skin_mask = cv2.GaussianBlur(skin_mask, (7, 7), 0)
# 创建3*3大小的卷积核,用于形态学处理
kernel = np.ones((3, 3), np.uint8)
# 闭运算:先膨胀再腐蚀,填补肤色掩码内部细小的空洞
skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel)
# 开运算:先腐蚀再膨胀,清除画面外面零散的白色噪点
skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel)

# 查找掩码图像当中所有最外层的物体轮廓
contours, _ = cv2.findContours(skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 如果没有检测出来任何轮廓,直接返回空值
if len(contours) == 0:
    return None

# 创建列表,用来存放筛选之后合格的手掌轮廓
valid_hand = []
# 遍历每一条检测得到的轮廓
for cnt in contours:
    # 计算当前轮廓所占像素面积
    area = cv2.contourArea(cnt)
    # 如果轮廓面积小于手掌最低面积阈值,则跳过该轮廓
    if area < MIN_HAND_AREA:
        continue
    # 获取轮廓外接矩形,返回矩形左上角坐标、宽度、高度
    x, y, w, h = cv2.boundingRect(cnt)
    # 计算矩形长边与短边的比值
    ratio = max(w, h) / min(w, h)
    # 长宽比小于阈值,代表形状接近手掌,存入合格列表
    if ratio < MAX_HAND_RATIO:
        valid_hand.append(cnt)

# 合格手掌轮廓列表为空,返回None
if len(valid_hand) == 0:
    return None
# 在合格轮廓当中选取面积最大的轮廓作为当前手掌
hand_cont = max(valid_hand, key=cv2.contourArea)
# 返回筛选完成的手掌轮廓
return hand_cont

def get_fingertips(hand_contour):
“”“凸缺陷算法精准提取指尖坐标”“”
# 获取手掌轮廓凸包,关闭返回点模式,仅返回轮廓索引
hull = cv2.convexHull(hand_contour, returnPoints=False)
# 根据凸包计算凸缺陷,手掌指缝位置就是凸缺陷
defects = cv2.convexityDefects(hand_contour, hull)
# 创建列表用来存储指尖坐标点
fingertips = []
# 判断凸缺陷是否成功检测到
if defects is not None:
# 循环遍历全部凸缺陷
for i in range(defects.shape[0]):
# s起始点索引、e终点索引、f凹陷点索引、d凹陷深度
s, e, f, d = defects[i, 0]
# 获取缺陷起始点坐标
start = tuple(hand_contour[s][0])
# 获取缺陷终点坐标
end = tuple(hand_contour[e][0])
# 将两个指尖坐标存入指尖数组
fingertips.append(list(start))
fingertips.append(list(end))
# 当凸缺陷没有获取到指尖,则直接读取凸包顶点当作指尖点位
if len(fingertips) <= 0:
# 获取凸包所有顶点坐标
hull_points = cv2.convexHull(hand_contour, returnPoints=True)
# 遍历凸包顶点存入指尖列表
for point in hull_points:
fingertips.append([point[0][0], point[0][1]])
# 将指尖列表转换为numpy数组并且返回
return np.array(fingertips)

def calc_euclidean(p1, p2):
“”“勾股定理计算两点像素距离”“”
# 欧氏距离公式,计算两个坐标点之间像素长度
return np.sqrt(np.square(p1[0] - p2[0]) + np.square(p1[1] - p2[1]))

def distance_to_brightness(dist):
“”“指尖距离线性映射灯光亮度0~100%”“”
# 限制距离数值处于有效区间50~300,防止数值越界
dist = np.clip(dist, MIN_DIST, MAX_DIST)
# 根据距离换算得到亮度百分比
percent = ((dist - MIN_DIST) / DIST_RANGE) * 100
# 保留一位小数之后返回亮度百分比
return round(percent, 1)

def draw_brightness_bar(frame, percent):
“”“左侧绘制蓝色亮度进度条”“”
# 设置进度条左上角坐标
bar_x, bar_y = 30, 50
# 设置进度条宽度、整体高度
bar_w, bar_h = 40, 400
# 绘制进度条灰色外边框
cv2.rectangle(frame, (bar_x, bar_y), (bar_x + bar_w, bar_y + bar_h), GRAY_COLOR, 2)
# 根据亮度百分比计算填充高度
fill_h = int(bar_h * (percent / 100))
# 计算填充矩形的上边坐标,进度条自下而上填充
fill_y = bar_y + bar_h - fill_h
# 绘制蓝色的亮度填充块
cv2.rectangle(frame, (bar_x, fill_y), (bar_x + bar_w, bar_y + bar_h), BLUE, -1)
# 返回绘制完成之后的画布
return frame

def draw_hand_connection(img, tip_points):
“”“绘制手掌关键点连接线”“”
# 循环遍历指尖点位,依次连线相邻指尖
for i in range(len(tip_points) - 1):
# 取出第i个指尖的整型坐标
p1 = (int(tip_points[i][0]), int(tip_points[i][1]))
# 取出后一个指尖整型坐标
p2 = (int(tip_points[i+1][0]), int(tip_points[i+1][1]))
# 使用紫色线条连接两个指尖
cv2.line(img, p1, p2, PURPLE, 2)
# 循环绘制每一处指尖圆点标记
for pt in tip_points:
cv2.circle(img, (int(pt[0]), int(pt[1])),6,PURPLE,-1)
# 返回绘制结束后的画布
return img

====================== 【步骤3:主程序循环】 ======================

def main():
# 声明使用全局时间变量prev_time
global prev_time
# 创建摄像头捕获对象,开启指定编号摄像头
cap = cv2.VideoCapture(CAM_ID)
# 设置摄像头输出画面宽度为640像素
cap.set(cv2.CAP_PROP_FRAME_WIDTH,640)
# 设置摄像头输出画面高度为480像素
cap.set(cv2.CAP_PROP_FRAME_HEIGHT,480)

# 判断摄像头是否开启失败
if not cap.isOpened():
    # 在终端打印摄像头开启失败提示英文
    print("Camera open failed, please modify CAM_ID")
    # 终止主函数
    return

# 摄像头图像无限循环读取
while True:
    # 读取一帧摄像头画面;ret代表读取成功标记,frame为图像矩阵
    ret, frame = cap.read()
    # 如果当前帧读取失败,则跳过本次循环继续读取下一帧
    if not ret:
        continue
    # 获取图像高度、宽度数值,通道数舍弃
    img_h, img_w = frame.shape[:2]
    # 将画面水平镜像翻转,适配人面对镜头时左右手方向
    frame = cv2.flip(frame,1)
    # 拷贝原图作为绘图画布,防止原图被修改
    canvas = frame.copy()

    # 获取当前系统时间戳
    cur_time = time.time()
    # 计算当前画面帧率,单位帧每秒
    fps = int(1/(cur_time - prev_time))
    # 更新上一帧时间戳为本帧时间
    prev_time = cur_time

    # 初始化灯光亮度百分比默认值为0
    brightness_percent = 0.0
    # 调用函数检测当前画面当中的手掌轮廓
    hand_cont = get_hand_contour(frame)

    # 判断已经成功检测到手部轮廓
    if hand_cont is not None:
        # 在画布绘制紫色手掌轮廓边线
        cv2.drawContours(canvas,[hand_cont],-1,PURPLE,2)
        # 获取当前手掌全部指尖点位数组
        tip_array = get_fingertips(hand_cont)
        # 控制台打印当前检测出来指尖的数量
        print(f"Detected fingertips quantity:{len(tip_array)}")
        # 指尖点位大于等于2,才可以计算双指间距
        if len(tip_array) >= 2:
            # 初始化指尖最大间距
            max_dis = 0
            # 存储距离最远两个指尖的数组下标
            index_a, index_b = 0,1
            # 双层循环遍历所有指尖组合
            for i in range(len(tip_array)):
                for j in range(i+1, len(tip_array)):
                    # 计算当前一组指尖之间像素距离
                    d = calc_euclidean(tip_array[i], tip_array[j])
                    # 如果距离大于当前最大距离,则更新最大距离与下标
                    if d > max_dis:
                        max_dis = d
                        index_a,index_b = i,j
            # 获取距离最远第一个指尖坐标
            point1 = tip_array[index_a]
            # 获取距离最远第二个指尖坐标
            point2 = tip_array[index_b]
            # 转为整型像素坐标
            x1,y1 = int(point1[0]),int(point1[1])
            x2,y2 = int(point2[0]),int(point2[1])
            # 控制台输出两根指尖之间的像素距离
            print(f"Finger‑finger pixel distance:{max_dis}")

            # 在画布绘制紫色实心圆点标记第一个指尖
            cv2.circle(canvas,(x1,y1),8,PURPLE,-1)
            # 在画布绘制紫色实心圆点标记第二个指尖
            cv2.circle(canvas,(x2,y2),8,PURPLE,-1)
            # 使用紫色线条连接两处指尖标记点
            cv2.line(canvas,(x1,y1),(x2,y2),PURPLE,3)
            # 通过指尖间距换算灯光亮度百分比
            brightness_percent = distance_to_brightness(max_dis)
            # 绘制所有指尖之间的连接线
            canvas = draw_hand_connection(canvas, tip_array)

    # 将0~100亮度百分比换算为LED通道数值范围0~255
    led_val = int((brightness_percent / 100) * 255)
    # 设置24颗灯环全部为当前亮度的白光
    rgb.set([(led_val, led_val, led_val)] * 24)

    # 在画布左上角绘制当前帧率FPS文字
    cv2.putText(canvas,f"FPS: {fps}",(10,35),
                cv2.FONT_HERSHEY_SIMPLEX,1,BLUE,2)
    # 在画布左下角输出当前灯光亮度百分比英文文本
    cv2.putText(canvas,f"Brightness: {brightness_percent}%",(20, img_h - 20),
                cv2.FONT_HERSHEY_SIMPLEX,1,BLUE,2)
    # 在画布左侧刷新可视化亮度进度条
    canvas = draw_brightness_bar(canvas, brightness_percent)

    # 设置预览窗口标题并且展示处理之后的画布画面
    cv2.imshow("-----", canvas)

    # 等待键盘按键输入,延时1毫秒
    key = cv2.waitKey(1)
    # 判断按下ESC按键,ESC按键ASCII码为27,结束主循环
    if key == 27:
        break

# 程序结束,释放摄像头硬件资源
cap.release()
# 销毁所有OpenCV创建出来的图像窗口
cv2.destroyAllWindows()
# 将RGB灯环所有LED熄灭
rgb.set([0, 0, 0] * 24)
# 控制台打印程序退出硬件释放完毕提示
print("Program exit, camera and led‑ring released")

程序入口,运行主函数

if name == “main”:
main()

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐