Skip to main content

One post tagged with "MediaPipe"

View All Tags

DshanPI-A1 Review Part 4: Modifying an Open-Source Gesture Project

· 7 min read
Yuxuan
100askTeam yuxuan.

All of the project modifications in this article are based mainly on compatibility, smoothness, screen display method, and camera invocation.

Display Output Adaptation

Main Adaptation Issues

1. RK3576 Runs a Pure Wayland Environment

RK3576 runs a pure Wayland environment with no X11 or libGL support, so the traditional cv2.imshow() cannot be used to display images.

[Solution]

Adopt the FIFO + GStreamer + Wayland display pipeline:

# Python side code
fifo = open('/tmp/gesture_fifo', 'wb')
while True:
_, jpeg = cv2.imencode('.jpg', processed_frame,
[cv2.IMWRITE_JPEG_QUALITY, 85])
fifo.write(jpeg.tobytes())
fifo.flush()
# Shell side code
# GStreamer reads the JPEG stream from the pipe and displays it
gst-launch-1.0 filesrc location=/tmp/gesture_fifo ! \
jpegparse ! jpegdec ! videoconvert ! waylandsink fullscreen=true

2. IMX415 Camera Color Anomaly on Second Launch

The IMX415 camera shows a color anomaly on its second launch, with the kernel reporting the error "no first iq setting".

[Solution]

Restart rkaiq_3A_server before each camera open:

def restart_3a(self):
os.system("killall rkaiq_3A_server 2>/dev/null")
time.sleep(2)
os.system("rm -f /tmp/.rkaiq_3A* 2>/dev/null")
os.system("/etc/init.d/S40rkaiq_3A start >/dev/null 2>&1")
time.sleep(5)

Project 1: Snake Game

Open-source project address: Project2/SnakeGame/main.py at main · WLHSDXN/Project2

Modification Process

1. Multi-Layer Detector Architecture

To accommodate different dependency environments, a three-layer detector fallback mechanism was designed:

Priority 1: cvzone (MediaPipe wrapper, high accuracy)
↓ unavailable
Priority 2: native MediaPipe (21 keypoints)
↓ unavailable
Priority 3: HSV skin-color detection (lightweight fallback)

Code implementation:

# Detector selection logic
if USE_CVZONE:
detector = CvzoneHandDetector(detectionCon=0.8, maxHands=1)
elif USE_MEDIAPIPE:
detector = MediapipeHandDetector(maxHands=1,
detectionCon=0.5,
drawLandmarks=False)
else:
detector = SimpleHandDetector() # HSV fallback solution

2. MediaPipe Integration and Wrapping

Implemented the MediapipeHandDetector class, returning a data format compatible with cvzone:

class MediapipeHandDetector:
def findHands(self, frame, flipType=False):
img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = self.hands.process(img_rgb)

hands = []
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
# Extract 21 keypoint coordinates
lmList = []
for lm in hand_landmarks.landmark:
x_px = int(lm.x * width)
y_px = int(lm.y * height)
lmList.append([x_px, y_px, lm.z])

hands.append({'lmList': lmList})

return hands, frame

[Key Point]

The index fingertip is lmList[8], used directly as the snake-head control point.

3. HSV Skin-Color Detection Fallback

When MediaPipe is unavailable, use simple skin-color detection:

def detect_hand_simple(frame):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, [0, 30, 60], [255, 255, 255])

# Morphological denoising
kernel = np.ones((7, 7), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=3)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2)

contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if contours:
c = max(contours, key=cv2.contourArea)
hull = cv2.convexHull(c)
# Extract the topmost point as the "fingertip"
topmost = hull[hull[:, :, 1].argmin()][0]
return topmost

4. MediaPipe Performance Tuning

Optimization 1: Use a Lightweight Model
self.hands = mp.solutions.hands.Hands(
model_complexity=0, # 0=lite, 1=full (default)
max_num_hands=1,
min_detection_confidence=0.5, # lower threshold for speed
min_tracking_confidence=0.5
)
Optimization 2: Disable Visualization Drawing
# Remove the time-consuming keypoint drawing
# mp_drawing.draw_landmarks(frame, landmarks, connections) # commented out
drawLandmarks=False # new switch
Optimization 3: Reduce Input Resolution
# 640x480 is already the optimal balance point
# Lowering further to 320x240 can boost FPS, but hurts detection accuracy
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
Optimization 4: Optimize the Camera Buffer
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)  # reduce latency

[Optimization Effect]

FPS increased from 5-10 to 15-25 FPS, meeting the needs of game interaction.

5. File-Based Control Interface

Because keyboard input cannot be captured directly in FIFO mode, a control-file approach is used:

# Shell script section
# Read keypresses and write control files
stty echo icanon
while read n1 t 0.1 key; do
if [ "$key" = "r" ]; then
touch /tmp/snake_restart
elif [ "$key" = "q" ]; then
touch /tmp/snake_quit
fi
done
# Python section
# Detect control files
if os.path.exists('/tmp/snake_quit'):
print('Quit command detected')
break

if os.path.exists('/tmp/snake_restart'):
os.remove('/tmp/snake_restart')
# Reset game state
self.game.gameOver = False
self.game.points = []
self.game.previousHead = (0, 0)

Effect Demonstration

The code and demo video are in the attachments.

img

img

Project 2: Virtual Drawing Board

Open-source project: [Based on hand keypoint detection, air-control the mouse / air-draw] https://www.bilibili.com/video/BV1364y1h7PS?vd_source=a16ca768198c38baa684546cf5060811

Modification Process

1. Core Logic Extraction

Gesture Recognition Logic:
fingers = detector.fingersUp()  # returns 5 values, 1 means finger extended

# Mode 1: select tool (index + middle finger extended)
if fingers[1] and fingers[2]:
if y1 < 153: # in the top toolbar area
if 0 < x1 < 320: color = [50, 128, 250] # blue
elif 320 < x1 < 640: color = [0, 0, 255] # red
elif 640 < x1 < 960: color = [0, 255, 0] # green
elif 960 < x1 < 1280: color = [0, 0, 0] # eraser

# Mode 2: draw (only index finger extended)
elif fingers[1] and not fingers[2]:
cv2.line(imgCanvas, (xp, yp), (x1, y1), color, brushThickness)
Canvas Compositing Logic:
# 1. Convert the canvas to grayscale and binarize it
imgGray = cv2.cvtColor(imgCanvas, cv2.COLOR_BGR2GRAY)
_, imgInv = cv2.threshold(imgGray, 50, 255, cv2.THRESH_BINARY_INV)

# 2. Composite with bitwise operations
img = cv2.bitwise_and(img, imgInv) # keep non-drawing area of camera frame
img = cv2.bitwise_or(img, imgCanvas) # overlay drawing content

2. Display System Rebuild

Reuse the snake game's display solution: FIFO + GStreamer.

3. Toolbar Internalization

The original project depended on 4 PNG images as the toolbar, which is inconvenient for managing external resources on an embedded system.

[Solution]

Generate the toolbar with OpenCV drawing APIs:

def create_header(self):
"""Dynamically generate the toolbar"""
header = np.zeros((100, self.width, 3), np.uint8)
header[:] = (200, 200, 200) # gray background

tools = [
((250, 128, 50), "Blue"), # BGR format
((0, 0, 255), "Red"),
((0, 255, 0), "Green"),
((0, 0, 0), "Eraser")
]

section_width = self.width // 4
for i, (color, label) in enumerate(tools):
x1 = i * section_width
x2 = (i + 1) * section_width

# Draw color block
cv2.rectangle(header, (x1 + 10, 20), (x2 - 10, 80), color, -1)
cv2.rectangle(header, (x1 + 10, 20), (x2 - 10, 80),
(255, 255, 255), 2) # white border

# Text label
cv2.putText(header, label, (x1 + 20, 95),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (50, 50, 50), 1)

return header

This way there is zero dependency on external resources, which is better for porting and project development!

4. Camera and 3A Service Adaptation

Reuse the snake game's fix.

5. Resolution vs. Performance Trade-off

[Original Configuration]:

width = 1280, height = 720
canvas: imgCanvas = np.zeros((720, 1280, 3), np.uint8)

[RK3576 Optimization]

width = 640, height = 480  # reduce resolution by 50%
canvas: imgCanvas = np.zeros((480, 640, 3), np.uint8)

[Reasons]

  1. MediaPipe's FPS roughly doubles at 640x480
  2. The drawing-board application does not demand as high a resolution as vision recognition
  3. JPEG encoding/transmission is faster

[Toolbar Adaptation]

Original: 153 pixels tall at the top, divided into 4 regions 320 pixels wide RK3576: 100 pixels tall at the top, divided into 4 regions 160 pixels wide

section_width = self.width // 4  # adaptive width
if y1 < 100: # toolbar height
if 0 < x1 < section_width:
self.color = (250, 128, 50) # blue
elif section_width < x1 < section_width * 2:
self.color = (0, 0, 255) # red
# ...

6. Interaction Control Improvements

1. Clear-Canvas Mechanism

[Original]

if all(x >= 1 for x in fingers):
imgCanvas = np.zeros((720, 1280, 3), np.uint8)

[Problem]

High false-trigger rate; inconvenient for fine control.

[RK3576 Improvement]

Use keypress control:

# Shell side
while read -n1 -t 0.1 key; do
if [ "$key" = "c" ]; then
touch /tmp/painter_clear
fi
done
# Python side
if os.path.exists('/tmp/painter_clear'):
os.remove('/tmp/painter_clear')
self.imgCanvas = np.zeros((self.height, self.width, 3), np.uint8)
print("Canvas cleared")
2. Exit Control

[Original]

Can only capture the keyboard through cv2.waitKey(1), dependent on window focus.

[RK3576]

Dual exit mechanism:

  1. Keypress control: touch /tmp/painter_quit -> Python detects it and exits

  2. Ctrl+C: Shell script traps the signal -> kills all processes -> cleans up the FIFO

Effect Demonstration

The code and demo video are in the attachments.

img

img

Technical Summary and Lessons

  1. Cross-Platform Display Adaptation PC GUI solutions do not apply to embedded systems; the output method must be chosen based on system characteristics (Wayland/Framebuffer).

  2. Resource Internalization Embedded systems tend toward single-file deployment; external resources should be turned into code-generated content or packed into the program.

  3. Tiered Performance Optimization

    • Algorithm layer: lightweight models
    • Implementation layer: disable non-essential drawing
    • Hardware layer: buffer/resolution tuning
  4. Interaction Adaptation Keyboard/mouse events that GUIs depend on must be converted to file control or GPIO triggers.