实时绘图共享屏幕,无线投屏无限精彩
随着科技的飞速发展,Python编程语言在教育领域的应用越来越广泛。DFRobot推出的行空板M10,以其强大的功能和易用性,成为Python教学与创意开发的理想工具。本次创意开发活动旨在通过行空板M10扩展板组合,进一步拓展其使用场景,助力功能升级与应用进阶,为教育工作者和创客爱好者提供更丰富的教学资源和创作灵感。
行空板M10扩展板组合包括电机IO扩展板、金手指扩展板和800mAh电池扩展板,为参与者提供了丰富的硬件支持。活动鼓励参与者围绕智能机器人、STEAM创意结构和AI物联网应用等方向进行创作,以激发创新思维和实践能力。
非常高兴有幸参加这次活动,在制作小智MCP控制行空板的过程中,借助大模型的强大能力得以实现,无意完成了这个之前想做,但做不成的项目。这里我跟大家分享一下我的探索过程。
圆的绘制方法
根据行空板官方文档-unihiker库,圆的绘制可以通过以下代码实现:
from unihiker import GUI #导入包
gui=GUI() #实例化GUI类
gui.fill_circle(x=100, y=150, r=80, color="blue", onclick=lambda: print("fill circle clicked"))
import time
while True:
#增加等待,防止程序退出和卡住
time.sleep(1)
功能描述提示词
把驱动行空板绘制点的方式给模型后,再提供结构化的功能需求提示词,向大模型提出功能需求,如果需要进一步调整,可多轮提出具体要求进行完善:
项目目标
实现电脑端与行空板端图形同步显示。
——功能需求
1.电脑端
~创建240×320窗口,可放大。
~提供调色板更换颜色。
~鼠标绘制图形。
~UDP发送图形坐标。
2.行空板端
~接收UDP发送的坐标。
~根据坐标绘制图形。
3.其他
~两个屏幕显示相同,控制保持一致。
~'q': 退出程序,'c': 清空画布,'+': 增大画笔,'-': 减小画笔。
——程序结构
1.发送端(电脑端)
~语言:Python
~库:OpenCV
~功能:绘制图形并发送坐标
2.接收端(行空板端)
~语言:Python
~库:unihiker
~功能:接收坐标并绘制图形
代码阅读
在几轮交流后,模型基本了解你的具体需求,然后测试具体功能,相信应该可以完成你想要的功能.我这里做了一个比较简单的.代码如下,仅供参考:
——电脑端:
import cv2
import numpy as np
import socket
import pickle
from datetime import datetime
# 设置
SCALE_FACTOR = 2
WIDTH, HEIGHT = 240, 320
SCALED_WIDTH = WIDTH * SCALE_FACTOR
SCALED_HEIGHT = HEIGHT * SCALE_FACTOR
# 颜色调色板
COLORS = [
(255, 0, 0), # 红
(0, 255, 0), # 绿
(0, 0, 255), # 蓝
(0, 255, 255), # 黄
(255, 0, 255), # 紫
(255, 255, 0), # 青
(0, 0, 0), # 黑
(255, 255, 255) # 白
]
COLOR_NAMES = ["红", "绿", "蓝", "黄", "紫", "青", "黑", "白"]
# 初始化画布
canvas = np.ones((HEIGHT, WIDTH, 3), dtype=np.uint8) * 255
scaled_canvas = cv2.resize(canvas, (SCALED_WIDTH, SCALED_HEIGHT), interpolation=cv2.INTER_NEAREST)
# 初始化绘图状态
drawing = False
current_color = (0, 0, 0) # 默认黑色
current_color_index = 6 # 黑色
brush_size = 3
# UDP设置
UDP_IP = "192.168.31.134" # 接收端IP地址
UDP_PORT = 5005
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def send_data(event_type, x, y):
"""发送坐标点数据给接收端"""
# 构建数据包 (事件类型, 坐标, 颜色, 画笔大小)
data = {
'event': event_type, # 'start', 'draw', 'end'
'x': x,
'y': y,
'color': current_color,
'brush_size': brush_size
}
# 序列化并发送
serialized_data = pickle.dumps(data)
sock.sendto(serialized_data, (UDP_IP, UDP_PORT))
# 调试信息
now = datetime.now().strftime("%H:%M:%S.%f")[:-3]
print(f"[{now}] 发送: {event_type} @ ({x}, {y}), 颜色 {COLOR_NAMES[current_color_index]}, 大小 {brush_size}")
def draw_circle(event, x, y, flags, param):
global drawing
# 缩放坐标到实际画布大小
x_raw = int(x / SCALE_FACTOR)
y_raw = int(y / SCALE_FACTOR)
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
cv2.circle(canvas, (x_raw, y_raw), brush_size, current_color, -1)
send_data('start', x_raw, y_raw) # 开始绘制
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
cv2.circle(canvas, (x_raw, y_raw), brush_size, current_color, -1)
send_data('draw', x_raw, y_raw) # 绘制中
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
cv2.circle(canvas, (x_raw, y_raw), brush_size, current_color, -1)
send_data('end', x_raw, y_raw) # 结束绘制
# 创建窗口
cv2.namedWindow("Drawing Board")
cv2.setMouseCallback("Drawing Board", draw_circle)
# 创建调色板图像
palette = np.zeros((50, len(COLORS)*50, 3), dtype=np.uint8)
for i, color in enumerate(COLORS):
palette[:, i*50:(i+1)*50] = color
def select_color(event, x, y, flags, param):
global current_color, current_color_index
if event == cv2.EVENT_LBUTTONDOWN:
color_index = x // 50
if color_index < len(COLORS):
current_color = COLORS[color_index]
current_color_index = color_index
print(f"选择颜色: {COLOR_NAMES[color_index]} (RGB: {current_color})")
cv2.namedWindow("Palette")
cv2.setMouseCallback("Palette", select_color)
print("绘图程序启动(坐标传输模式)")
print("使用说明:")
print("- 在Drawing Board窗口上绘制")
print("- 在Palette窗口选择颜色")
print("- 按'q'退出, 'c'清空画布, '+/-'调整画笔大小")
while True:
scaled_canvas = cv2.resize(canvas, (SCALED_WIDTH, SCALED_HEIGHT), interpolation=cv2.INTER_NEAREST)
cv2.imshow("Drawing Board", scaled_canvas)
cv2.imshow("Palette", palette)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
print("退出程序")
# 发送退出指令
sock.sendto(pickle.dumps({'command': 'quit'}), (UDP_IP, UDP_PORT))
break
elif key == ord('c'):
canvas.fill(255)
print("清空画布")
# 发送清屏指令
sock.sendto(pickle.dumps({'command': 'clear'}), (UDP_IP, UDP_PORT))
elif key == ord('+'):
brush_size = min(brush_size + 1, 20)
print(f"增大画笔大小: {brush_size}")
elif key == ord('-'):
brush_size = max(brush_size - 1, 1)
print(f"减小画笔大小: {brush_size}")
cv2.destroyAllWindows()
sock.close()
——行空板端:
from unihiker import GUI
import socket
import pickle
import time
# 初始化UniHiker GUI
gui = GUI()
WIDTH, HEIGHT = 240, 320 # 与发送端保持一致
# 存储所有绘制的圆点对象
drawn_circles = []
background = gui.draw_rect(x=0, y=0, w=WIDTH, h=HEIGHT, color="#FFFFFF") # 白色背景
# UDP设置
UDP_IP = "0.0.0.0" # 监听所有网络接口
UDP_PORT = 5005 # 与发送端端口一致
# 创建UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
sock.settimeout(0.1) # 设置超时时间
print("接收端已启动,等待绘图数据...")
def rgb_to_hex(rgb):
"""将RGB元组转换为十六进制颜色代码"""
return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2])
def process_data(data_dict):
"""处理接收到的绘图数据"""
global drawn_circles, background
# 处理命令类型
if 'command' in data_dict:
if data_dict['command'] == 'clear':
# 高效清空画布
gui.clear() # 清除所有图形
# 重新创建白色背景
background = gui.draw_rect(x=0, y=0, w=WIDTH, h=HEIGHT, color="#FFFFFF")
drawn_circles = [] # 清空圆点列表
print("清空画布")
return True
elif data_dict['command'] == 'quit':
print("收到退出指令")
return False
# 处理绘图事件
if 'event' in data_dict:
x = data_dict['x']
y = data_dict['y']
color = data_dict.get('color', (0, 0, 0))
brush_size = data_dict.get('brush_size', 3)
# 转换为Unihiker可用的颜色格式
hex_color = rgb_to_hex(color)
# 绘制实心圆点
circle = gui.fill_circle(
x=x,
y=y,
r=brush_size,
color=hex_color
)
drawn_circles.append(circle)
print(f"绘制点: ({x}, {y}), 大小 {brush_size}, 颜色 {hex_color}")
return True
return True # 继续运行
while True:
try:
# 接收数据
data, addr = sock.recvfrom(65507) # UDP最大包大小
# 反序列化数据
try:
data_dict = pickle.loads(data)
# print(f"收到数据: {data_dict}")
# 处理数据并检查是否需要退出
if not process_data(data_dict):
break
except Exception as e:
print(f"数据解析错误: {str(e)}")
except socket.timeout:
# 超时处理,继续循环
pass
except Exception as e:
print(f"接收错误: {str(e)}")
break
# 短暂休眠
time.sleep(0.01)
# 清理所有图形
gui.clear() # 清除所有图形
sock.close()
print("接收端已关闭")
这是一个意外的收获,我自己感觉挺有意思,分享给大家.如果你也在做类似投屏功能,希望对你可以有所帮助.
评论