[作品提交] DigiKey“智造万物,快乐不停”创意大赛】提交:机器视觉打造全自动老板键智能键盘

eew_dy9f48   2024-1-11 12:24 楼主
利用机器视觉打造带有全自动老板键的智能键盘
作者:eew_dy9f48

 
一、作品简介
自带键盘的树莓派Pi400,其实可以看作一块强大的智能键盘,作为电脑的辅助;按照项目内容层层递进,首先,先配置树莓派p400,让它作为一块普通电脑键盘;接下来,我想尝试结合一些人工智能实现键盘命令的自动执行,比如全自动老板键,通过ESP32-CAM作为树莓派的网络摄像头,这样可以脱离连线限制,可以部署在任何地方。然后一旦识别到老板人脸,就自动发送Alt+Tab等老板键,瞬间切换至工作界面。除此之外,由于在上述任务中我们已经实现了树莓派HID键盘的配置,因此我们还可以使用它来作为游戏助手,实现键盘宏,按一个按键打出一整套combo。由于宏是内建在键盘中的,在pc端并没有任何相关进程,所以不会被游戏判定为使用辅助工具。这就可以实现很多功能上的延申,大家可以自行玩耍。
二、系统框图
113321zqeeww17d5330lq0.png
功能模块一共三部分:
  1. Pi400 HID键盘功能实现。
  2. Pi400 键盘动作的捕捉与独占。
  3. 人脸识别在Pi400上的实现。
    三、各部分功能说明
    首先,先介绍下项目中包含的硬件设计。
    这个项目包括了无线网络摄像头的制作。虽然网上有现成的ESP32CAM模块售卖,且价格非常便宜。但是由于做工良莠不齐,导致经常翻车。而且,ESP32性能较弱,且不支持USB,如果未来想做一些其他的开发也可能会力不从心。因此,趁这个项目的机会,我打算直接制作一块ESP32S2 CAM开发板出来。
    这个摄像头开发板其实我后面还重新绘制了第二个版本,增加了tf卡槽,同时修复了飞线的问题,并由于板子面积有限将所有封装换成了0402。但由于新版的板子打样回来焊接难度有点大,还一直迟迟没有动手,因此先使用老版本的板子完成项目。
    113321x0j22zd0fl0avf50.png
    113321b3s52bf5455gs1gx.png
    113321xzb4bkm3ecmn349n.png
    113321lt4tlgzz9z9rwwe4.png
    113321mq25h7z75gnw2v80.png
    接下来说说软件方面,我们具体介绍一下每一个功能模块的实现方法。
    1Pi400 HID 键盘功能的实现。
    在github上有一个zero_hid的库,可以实现使用树莓派zero模拟hid键盘。但这个库有一些问题,直接使用在组合键上会出很多的问题,因此我参考这个项目,重写了一下这个库。
    首先科普一下HID协议,HID键盘协议是一种基于报文的协议,通过在USB总线上进行通信。当用户按下键盘上的按键时,键盘将生成一个HID报文,并将其发送到计算机。计算机收到报文后,根据报文的内容来模拟相应的键盘操作,例如在文本编辑器中输入字符或执行特定的功能。
    HID键盘报文包含多个字段,其中最重要的是按键码(Keycode)。按键码表示按下的键的唯一标识符,例如“A”键的按键码是0x04。除了按键码外,报文还可以包含其他信息,如修饰键(如Shift、Ctrl和Alt键)的状态和组合键的状态。
    因此,在合成报文前,我们先要知道我们想输入的按键哪些是修饰键,而哪些是按键,他们要分开进行处理。
    在进入代码部分前,我们需要先安装一下驱动。首先先新建一个文件,命名为isticktoit_usb,添加可执行权限,并填入以下内容:
    ···
    #!/bin/bash
    cd /sys/kernel/config/usb_gadget/
    mkdir -p isticktoit
    cd isticktoit
    echo 0x1d6b > idVendor # Linux Foundation
    echo 0x0104 > idProduct # Multifunction Composite Gadget
    echo 0x0100 > bcdDevice # v1.0.0
    echo 0x0200 > bcdUSB # USB2
    mkdir -p strings/0x409
    echo "fedcba9876543210" > strings/0x409/serialnumber
    echo "Tobias Girstmair" > strings/0x409/manufacturer
    echo "iSticktoit.net USB Device" > strings/0x409/product
    mkdir -p configs/c.1/strings/0x409
    echo "Config 1: ECM network" > configs/c.1/strings/0x409/configuration
    echo 250 > configs/c.1/MaxPower
    # Add functions here
    mkdir -p functions/hid.usb0
    echo 1 > functions/hid.usb0/protocol
    echo 1 > functions/hid.usb0/subclass
    echo 8 > functions/hid.usb0/report_length
    echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.usb0/report_desc
    ln -s functions/hid.usb0 configs/c.1/
    # End functions
    ls /sys/class/udc > UDC
    ···

    接着运行以下命令,完成驱动配置:
    ···
    #!/bin/bash
    echo "" | sudo tee -a /boot/config.txt
    echo "# BEGIN HID Keyboard Simulation" | sudo tee -a /boot/config.txt
    echo "dtoverlay=dwc2" | sudo tee -a /boot/config.txt
    echo "# END HID Keyboard Simulation" | sudo tee -a /boot/config.txt
    echo "" | sudo tee -a /etc/modules
    echo "# BEGIN HID Keyboard Simulation" | sudo tee -a /etc/modules
    echo "dwc2" | sudo tee -a /etc/modules
    echo "libcomposite" | sudo tee -a /etc/modules
    echo "# END HID Keyboard Simulation" | sudo tee -a /etc/modules
    # Move to before exit 0
    echo "" | sudo tee -a /etc/rc.local
    echo "# BEGIN HID Keyboard Simulation" | sudo tee -a /etc/rc.local
    echo "sudo ./isticktoit_usb" | sudo tee -a /etc/rc.local
    echo "# END HID Keyboard Simulation" | sudo tee -a /etc/rc.local
    ···

    完成后,以后每次重启完成,只需要运行一下isticktoit_usb即可。
    处理报文部分的代码如下:
    ···
    from typing import List
    from .hid import hidwrite
    from .hid.keycodes import KeyCodes
    from time import sleep
    import json
    import pkgutil
    import os
    import pathlib
    class Keyboard:
    def __init__(self, dev='/dev/hidg0') -> None:
    self.dev = dev
    self.set_layout()
    self.control_pressed = []
    self.key_pressed = []
    def list_layout(self):
    keymaps_dir = pathlib.Path(__file__).parent.absolute() / 'keymaps'
    keymaps = os.listdir(keymaps_dir)
    files = [f for f in keymaps if f.endswith('.json')]
    for count, fname in enumerate(files, 1):
    with open(keymaps_dir / fname , encoding='UTF-8') as f:
    content = json.load(f)
    name, desc = content['Name'], content['Description']
    print(f'{count}. {name}: {desc}')
    def set_layout(self, language='US'):
    self.layout = json.loads( pkgutil.get_data(__name__, f"keymaps/{language}.json").decode() )
    def gen_list(self, keys = []):
    _control_pressed = []
    _key_pressed = []
    for key in keys:
    if key[:3] == "MOD":
    _control_pressed.append(KeyCodes[key])
    else:
    _key_pressed.append(KeyCodes[key])
    return _control_pressed, _key_pressed
    def gen_buf(self):
    self.buf = [sum(self.control_pressed),0] + self.key_pressed
    self.buf += [0] * (8 - len(self.buf)) # fill to lenth 8
    ##########################################################################
    # For user
    def press(self, keys = [], additive=False, hold=False):
    _control_pressed, _key_pressed = self.gen_list(keys)
    if not additive:
    self.control_pressed = []
    self.key_pressed = []
    self.control_pressed.extend(_control_pressed)
    self.control_pressed = list(set(self.control_pressed)) # remove repeated items
    self.key_pressed.extend(_key_pressed)
    self.key_pressed = list(set(self.key_pressed))[:6] # remove repeated items and cut until 6 items
    self.gen_buf()
    hidwrite.write_to_hid_interface(self.dev, self.buf)
    if not hold:
    self.release(keys)
    def release(self, keys = []):
    _control_pressed, _key_pressed = self.gen_list(keys)
    try:
    self.control_pressed = list(set(self.control_pressed) - set(_control_pressed))
    except:
    pass
    try:
    self.key_pressed = list(set(self.key_pressed) - set(_key_pressed))
    except:
    pass
    self.gen_buf()
    hidwrite.write_to_hid_interface(self.dev, self.buf)
    def release_all(self):
    self.control_pressed = []
    self.key_pressed = []
    self.gen_buf()
    hidwrite.write_to_hid_interface(self.dev, self.buf)
    def text(self, string, delay=0):
    for c in string:
    key_map = self.layout['Mapping'][c]
    key_map = key_map[0]
    mods = key_map['Modifiers']
    keys = key_map['Keys']
    self.press(mods + keys)
    sleep(delay)
    ···

    上面这段代码把想要输出的按键分为control(修饰按键)和key(普通按键)两块,再组合形成报文列表。使用的逻辑是输入当前想要按下的按键状态,然后程序发送对应的报文。
    测试一下:
    ···
    import os
    import zero_hid
    if os.geteuid() != 0:
    raise ImportError('You must be root to use this library on linux.')
    k = zero_hid.Keyboard()
    k.press(["KEY_H"], additive=False, hold=False)
    k.press(["KEY_E"], additive=False, hold=False)
    k.press(["KEY_L"], additive=False, hold=False)
    k.press(["KEY_L"], additive=False, hold=False)
    k.press(["KEY_O"], additive=False, hold=False)
    ···

    press方法中填入的是一个list,表示当前按下的所有按键。具体的键值列表在zero_hid/keymaps/US.json中。
    如果电脑成功打印,表示功能正常。
    image-20240111122156-1.png  
    2Pi400 键盘动作的捕捉与独占。
    一般在python中捕获键盘动作,大家使用的都是keyboard库,简单好用。但keyboard库有个致命的问题,就是无法独占键盘。这在我们当前的应用中是无法接受的。试想一下,当我们想发送ctrl+alt+del时,一旦按下,树莓派和电脑都进入了安全模式。你无法预期在键盘上的操作会在树莓派系统中整出什么幺蛾子。因此,我们需要在捕捉键盘动作的同时,对键盘资源进行独占,以此避免按键被其他的进程捕获。在这里我们使用evdev库来实现。
    ···
    import os
    import evdev
    if os.geteuid() != 0:
    raise ImportError('You must be root to use this library on linux.')
    dev = evdev.InputDevice('/dev/input/event0')
    dev.grab() # grab 是为了独占,保证此设备不会被别的进程捕获
    for event in dev.read_loop():
    key = evdev.categorize(event)
    if isinstance(key, evdev.KeyEvent) and key.keystate != 2:
    print(key.keycode)
    ···

    按下按键,我们就可以看到对应的键值被打印在终端里。
    image-20240111122215-2.png  
    接下来只需要把抓取到的键值组合成列表,发送到我们上一步实现的hid中即可。
    细心的同学可能会意识到,evdev抓取到的的键值如果和hid的键值不匹配怎么办?这里我们就需要人工进行匹配,创建一个文件,将他们一一对应起来。
    在项目文件夹下创建一个codemap.csv文件,写入以下对应:
    ···
    KEY_LEFTCTRL,MOD_LEFT_CONTROL
    KEY_RIGHTCTRL,MOD_RIGHT_CONTROL
    KEY_LEFTALT,MOD_LEFT_ALT
    KEY_RIGHTALT,MOD_RIGHT_ALT
    KEY_LEFTSHIFT,MOD_LEFT_SHIFT
    KEY_RIGHTSHIFT,MOD_RIGHT_SHIFT
    ,
    KEY_LEFTMETA,MOD_LEFT_GUI
    ,
    ,
    KEY_ESC,KEY_ESC
    KEY_TAB,KEY_TAB
    KEY_CAPSLOCK,KEY_CAPSLOCK
    ,
    KEY_NUMLOCK,KEY_NUMLOCK
    KEY_SYSRQ,KEY_SYSRQ
    KEY_DELETE,KEY_DELETE
    KEY_INSERT,KEY_INSERT
    KEY_BACKSPACE,KEY_BACKSPACE
    KEY_ENTER,KEY_ENTER
    ,
    KEY_SPACE,KEY_SPACE
    ,
    KEY_UP,KEY_UP
    KEY_DOWN,KEY_DOWN
    KEY_LEFT,KEY_LEFT
    KEY_RIGHT,KEY_RIGHT
    ,
    KEY_PAGEUP,KEY_PAGEUP
    KEY_PAGEDOWN,KEY_PAGEDOWN
    KEY_HOME,KEY_HOME
    KEY_END,KEY_END
    ,
    KEY_F1,KEY_F1
    KEY_F2,KEY_F2
    KEY_F3,KEY_F3
    KEY_F4,KEY_F4
    KEY_F5,KEY_F5
    KEY_F6,KEY_F6
    KEY_F7,KEY_F7
    KEY_F8,KEY_F8
    KEY_F9,KEY_F9
    KEY_F10,KEY_F10
    KEY_F11,KEY_F11
    KEY_F12,KEY_F12
    ,
    KEY_GRAVE,KEY_GRAVE
    KEY_1,KEY_1
    KEY_2,KEY_2
    KEY_3,KEY_3
    KEY_4,KEY_4
    KEY_5,KEY_5
    KEY_6,KEY_6
    KEY_7,KEY_7
    KEY_8,KEY_8
    KEY_9,KEY_9
    KEY_0,KEY_0
    KEY_MINUS,KEY_MINUS
    KEY_EQUAL,KEY_EQUAL
    ,
    KEY_Q,KEY_Q
    KEY_W,KEY_W
    KEY_E,KEY_E
    KEY_R,KEY_R
    KEY_T,KEY_T
    KEY_Y,KEY_Y
    KEY_U,KEY_U
    KEY_I,KEY_I
    KEY_O,KEY_O
    KEY_P,KEY_P
    KEY_A,KEY_A
    KEY_S,KEY_S
    KEY_D,KEY_D
    KEY_F,KEY_F
    KEY_G,KEY_G
    KEY_H,KEY_H
    KEY_J,KEY_J
    KEY_K,KEY_K
    KEY_L,KEY_L
    KEY_Z,KEY_Z
    KEY_X,KEY_X
    KEY_C,KEY_C
    KEY_V,KEY_V
    KEY_B,KEY_B
    KEY_N,KEY_N
    KEY_M,KEY_M
    ,
    KEY_LEFTBRACE,KEY_LEFTBRACE
    KEY_RIGHTBRACE,KEY_RIGHTBRACE
    KEY_BACKSLASH,KEY_BACKSLASH
    KEY_SEMICOLON,KEY_SEMICOLON
    KEY_APOSTROPHE,KEY_APOSTROPHE
    KEY_COMMA,KEY_COMMA
    KEY_DOT,KEY_DOT
    KEY_SLASH,KEY_SLASH
    ,
    KEY_KP0,KEY_KP0
    KEY_KP1,KEY_KP1
    KEY_KP2,KEY_KP2
    KEY_KP3,KEY_KP3
    KEY_KP4,KEY_KP4
    KEY_KP5,KEY_KP5
    KEY_KP6,KEY_KP6
    KEY_KP7,KEY_KP7
    KEY_KP8,KEY_KP8
    KEY_KP9,KEY_KP9
    KEY_KPASTERISK,KEY_KPASTERISK
    KEY_KPMINUS,KEY_KPMINUS
    KEY_KPPLUS,KEY_KPPLUS
    KEY_KPDOT,KEY_KPDOT
    KEY_KPSLASH,KEY_KPSLASH
    ···

    接着在代码中,我们只需要打开该文件,转换为字典,删除空白项,即可制作好对应的字典。每次捕捉到按键后,利用字典翻译一下即可。
    ···
    with open('./codemap.csv', 'r') as file:
    reader = csv.reader(file)
    codemap = {rows[0]:rows[1] for rows in reader}
    del codemap[""]
    ···

    3人脸识别在Pi400上的实现。
    实现人脸识别我们使用的工具是ultralytics。Ultralytics安装非常简单,只需要pip install ultralytics即可。唯一需要注意的是我们需要更换一下pytorch的版本,否则会出现Segmentation fault
    ···
    pip uninstall torch torchvision
    pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2
    ···
    image-20240111122255-3.png  
    完成安装后,我们要使用stream的方法,从网络推流中获取到视频流。视频流来源是我们一开始制作的ESP32 S2 CAM开发板。开发板上烧录的是arduino ide上的官方CameraWebServer例程。除了常规的选择对应开发板并修改wifi信息外,我们还需要自定义一下开发板引脚。假设我们这里选择#define CAMERA_MODEL_ESP32S2_CAM_BOARD,那么我们要把camera_pins.h中的对应部分改成:
    ···
    #elif defined(CAMERA_MODEL_ESP32S2_CAM_BOARD)
    #define PWDN_GPIO_NUM -1
    #define RESET_GPIO_NUM -1
    #define XCLK_GPIO_NUM 2
    #define SIOD_GPIO_NUM 42
    #define SIOC_GPIO_NUM 41
    #define Y9_GPIO_NUM 1
    #define Y8_GPIO_NUM 3
    #define Y7_GPIO_NUM 4
    #define Y6_GPIO_NUM 6
    #define Y5_GPIO_NUM 8
    #define Y4_GPIO_NUM 14
    #define Y3_GPIO_NUM 9
    #define Y2_GPIO_NUM 7
    #define VSYNC_GPIO_NUM 16
    #define HREF_GPIO_NUM 15
    #define PCLK_GPIO_NUM 5
    #define LED_GPIO_NUM 45
    ···

    按照下图所示配置进行烧录即可。
    image-20240111122314-4.png  
    Pi400这边的代码比较简单,ultralytics已经被设计的非常易于使用。
    ···
    from ultralytics import YOLO
    import requests
    import time
    url = "http://192.168.8.171"
    model = YOLO("yolov8n.pt")
    requests.get(url+"/control?var=framesize&val=" + str(8))
    results = model.predict(url+":81/stream", stream=True, show=True, conf = 0.5)
    for result in results:
    for box in result.boxes:
    class_id = result.names[box.cls[0].item()]
    cords = box.xyxy[0].tolist()
    cords = [round(x) for x in cords]
    conf = round(box.conf[0].item(), 2)
    print("Object type:", class_id)
    print("Coordinates:", cords)
    print("Probability:", conf)
    print("---")
    ···

    如果所安装的树莓派系统是桌面版本,我们在桌面版本上运行以上程序,就可以看到画面。如果是仅有terminal的系统,terminal中也会有相应信息打印。
    111.png

    最后我们只需要整合上述功能,就可以实现带有全自动老板键的智能键盘。
    完整主程序代码如下:
    ···
    import zero_hid
    import evdev
    import csv
    import signal
    import os
    import threading
    if os.geteuid() != 0:
    raise ImportError('You must be root to use this library on linux.')
    k = zero_hid.Keyboard()
    # dev = evdev.InputDevice('/dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd')
    dev = evdev.InputDevice('/dev/input/event0')
    dev.grab() # grab 是为了独占,保证此设备不会被别的进程捕获
    with open('./codemap.csv', 'r') as file:
    reader = csv.reader(file)
    codemap = {rows[0]:rows[1] for rows in reader}
    del codemap[""]
    curr_pressed = []
    def key_input(key):
    global curr_pressed
    if isinstance(key, evdev.KeyEvent) and key.keystate != 2:
    if key.keystate == 1:
    curr_pressed.append(key.keycode)
    if key.keystate == 0:
    curr_pressed.remove(key.keycode)
    print("\r" + "CODE: " + key.keycode + " ;STAT: " + str(key.keystate) + " "*40, end="")
    keys = [codemap[i] for i in curr_pressed]
    k.press(keys, additive=False, hold=True)
    def handler(signal, frame):
    k.release_all()
    dev.ungrab()
    dev.close()
    exit()
    signal.signal(signal.SIGTSTP, handler) # Ctrl+Z
    signal.signal(signal.SIGINT, handler) # Ctrl+C
    def thread1():
    for event in dev.read_loop():
    try:
    key_input(evdev.categorize(event))
    except Exception as error:
    print(error)
    t1 = threading.Thread(target=thread1, daemon=True)
    t1.start()
    from ultralytics import YOLO
    import requests
    url = "http://192.168.8.171"
    model = YOLO("yolov8n.pt")
    requests.get(url+"/control?var=framesize&val=" + str(8))
    results = model.predict(url+":81/stream", stream=True, show=True, conf = 0.5)
    delay = 1
    count = 0
    mode = 0
    pre_mode = 0
    for result in results:
    try:
    for box in result.boxes:
    class_id = result.names[box.cls[0].item()]
    cords = box.xyxy[0].tolist()
    cords = [round(x) for x in cords]
    conf = round(box.conf[0].item(), 2)
    print("Object type:", class_id)
    print("Coordinates:", cords)
    print("Probability:", conf)
    print("---")
    if class_id == "person":
    mode = 1
    count = 0
    if mode != pre_mode:
    pre_mode = mode
    k.press(["MOD_LEFT_ALT","KEY_TAB"], additive=False, hold=False)
    print("triggered!!!")
    count += 1
    if count > delay:
    mode = 0
    pre_mode = mode
    except Exception as error:
    print(error)
    ···

     

  4. 作品源码和报告
利用机器视觉打造带有全自动老板键的智能键盘.doc (4.68 MB)
(下载次数: 2, 2024-1-11 12:25 上传)
五、作品功能演示视频
六、项目总结
【DigiKey“智造万物,快乐不停”创意大赛】1,ESP32S2摄像头开发板设计
【DigiKey“智造万物,快乐不停”创意大赛】2,Pi400 HID 键盘功能的实现
【DigiKey“智造万物,快乐不停”创意大赛】3,Pi400 键盘动作的捕捉与独占。
【DigiKey“智造万物,快乐不停”创意大赛】4,人脸识别在Pi400上的实现。
【DigiKey“智造万物,快乐不停”创意大赛】5,机器视觉打造带有全自动老板键的智能键盘
本帖最后由 eew_dy9f48 于 2024-1-11 12:26 编辑

回复评论 (1)

我觉得是不是一些日常操作可以,直接语音识别。

点赞  2024-1-11 13:04
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复