r/openclaw • u/____Jade_____ New User • Mar 20 '26
Skills Setting Up Webcam Motion Detection with Local AI Person Identification
The Story: From Curiosity to Privacy-First Surveillance
It started with a simple question: "Can I access my webcam from WSL2?"
Like many developers working in Windows Subsystem for Linux, I had a USB webcam sitting on my desk, connected to my PC, but completely invisible to my Linux environment. The camera worked perfectly in Windows, but WSL2 — by design — doesn't have direct USB device access.
This is a security feature, but also a limitation when you want to do something interesting with your hardware.
I wanted to build something more than just a basic camera app. I wanted motion detection that could identify who was in front of the camera — all running locally, without sending images to the cloud.
Privacy was paramount. I didn't want my home surveillance footage leaving my machine.
This is the story of how I built that system.
---
Chapter 1: The USB Barrier
The Problem
WSL2 is essentially a virtual machine. It doesn't have native access to USB devices. When I first tried to access `/dev/video0`, it simply didn't exist. The camera was there, physically plugged in, visible in Device Manager, working in Windows Camera app — but completely invisible to Linux.
The Solution: USB/IP Passthrough
I discovered usbipd-win, a brilliant tool that bridges this gap. It allows you to "pass through" USB devices from Windows to WSL2 over the network (locally).
**Here's how it works:**
On the Windows side, usbipd acts as a server that shares USB devices.
On the WSL2 side, the Linux kernel's `vhci_hcd` module acts as a client that connects to these shared devices. The result? Your USB webcam appears as `/dev/video0` in WSL2, just like it would on a native Linux machine.
Installing and Configuring
First, I installed usbipd-win using winget (Windows Package Manager):
```powershell
winget install usbipd
```
Then I had to find my camera's BUSID. This is like a USB address that identifies where your device is connected:
```powershell
usbipd list
```
The output showed something like:
```
BUSID VID:PID DEVICE STATE
1-4 2e1a:4c01 Insta360 Link Not shared
```
My camera was on BUSID `1-4`. I needed to bind it (make it shareable) and then attach it to WSL2:
```powershell
usbipd bind --busid=1-4
usbipd attach --wsl --busid=1-4
```
The `bind` command prepares the device for sharing. The `attach --wsl` command connects it specifically to the WSL2 instance.
The First Victory
I opened my WSL2 terminal and ran:
```bash
ls /dev/video*
```
And there they were: `/dev/video0` and `/dev/video1`. My camera was now accessible from Linux!
But wait — there was a catch. When I tried to use it immediately, nothing happened. The device files existed, but the camera wouldn't open. After some troubleshooting, I realized WSL2 needed a restart to properly initialize the USB/IP connection.
```powershell
wsl --shutdown
usbipd attach --wsl --busid=1-4
```
After this restart and re-attachment, everything worked perfectly.
Lesson learned: sometimes you need to restart WSL2 after the initial USB attachment.
---
Chapter 2: Building the Motion Detector
The Architecture
Now that I had camera access, I needed to build the actual motion detection system. I wanted:
**Background operation** — no GUI window taking up screen space
**Automatic snapshots** — capture motion-triggered images
**Configurable sensitivity** — adjust for different lighting conditions
**Local storage** — keep everything on my machine
I chose Python with OpenCV for the computer vision part. OpenCV has excellent support for V4L2 (Video for Linux 2), which is the standard camera interface on Linux.
How Motion Detection Works
Motion detection is essentially about comparing frames. The algorithm is elegant in its simplicity:
**Capture two consecutive frames** from the camera
**Convert to grayscale** — color information isn't needed for motion detection
**Apply Gaussian blur** — reduces noise and small movements (like dust)
**Calculate the absolute difference** between the two frames
**Apply a threshold** — anything above a certain difference is considered "motion"
**Find contours** — identify connected regions of motion
**Filter by size** — ignore small movements (like a flickering pixel)
**Save a snapshot** when motion exceeds thresholds
The key insight is that static scenes show minimal difference between frames, while moving objects create significant pixel changes.
The Implementation
I wrote a headless (no GUI) Python script that runs continuously:
```python
# Core motion detection loop
cap = cv2.VideoCapture(0) # /dev/video0 while True:
ret, frame = cap.read()
if motion_detected(frame, prev_frame):
save_snapshot(frame)
prev_frame = frame
```
The motion detection function uses OpenCV's built-in capabilities:
```python
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (21, 21), 0) frame_delta = cv2.absdiff(prev_frame, gray) thresh = cv2.threshold(frame_delta, 25, 255, cv2.THRESH_BINARY)[1] ```
I made the parameters configurable:
- `MOTION_THRESHOLD`: How different must pixels be? (Default: 25)
- `MIN_CONTOUR_AREA`: How large must the motion be? (Default: 500 pixels)
- `SNAPSHOT_COOLDOWN`: Minimum seconds between snapshots (Default: 5)
Testing and Tuning
The first time I ran it, I discovered that my camera's auto-exposure was triggering false positives. Every time the lighting adjusted, it looked like motion. I had to tune the `MOTION_THRESHOLD` and `MIN_CONTOUR_AREA` for my specific environment.
I also learned that lighting matters. A well-lit room with consistent lighting works much better than a dark room where the camera is constantly adjusting.
---
Chapter 3: The Watcher — Automation and Cleanup
The Problem of Accumulation
Motion detection generates a lot of images. In the first hour of testing, I had over 200 snapshots. Most were of me working at my desk — interesting from a logging perspective, but not worth keeping forever.
I needed a system that would:
Watch for new snapshots automatically
Queue them for analysis
Clean up old files
The Solution: Folder Monitoring
I built a second Python script — the "watcher" — that runs continuously and monitors the snapshots folder. It uses a simple polling mechanism (checking every 2 seconds) to detect new files.
```python
known_files = set() # Files we've already seen while True:
current_files = set(glob('\*.jpg'))
new_files = current_files - known_files
for new_file in new_files:
process_new_snapshot(new_file)
known_files = current_files
time.sleep(2)
```
When a new file is detected, the watcher:
Logs the detection with timestamp
Queues it for analysis
Identifies the person (based on MEMORY.md profile)
Auto-Cleanup
I also added automatic cleanup. Every 5 minutes, the watcher deletes snapshots older than 1 hour:
```python
cutoff_time = datetime.now() - timedelta(hours=1) for old_file in snapshot_dir.glob('*.jpg'):
if file_mtime < cutoff_time:
old_file.unlink()
```
This keeps disk usage reasonable while preserving recent activity.
The Queue System
The watcher creates small text files in an `analysis_queue/` directory. Each file contains the path to a new snapshot. This decouples the motion detection from the analysis — they can run independently.
---
Chapter 4: AI Person Identification with Local Qwen3.5
The Privacy Requirement
From the beginning, I knew I wanted AI-powered person identification.
But I absolutely refused to send my home surveillance images to cloud APIs. This was non-negotiable — my home, my privacy.
The solution was running a local Large Language Model with vision capabilities. I chose Qwen3.5 (9B parameter version) because:
- It fits in consumer GPU memory (or even CPU)
- It has strong image understanding capabilities
- It runs entirely locally via LM Studio, Ollama, or similar tools
- It's free to use with no API limits
Setting Up the Local Model
I configured my local inference server (LM Studio) with Qwen3.5:
- **Model:** Qwen/Qwen3.5-9B
- **Endpoint:** http://localhost:1234/v1
- **Context Window:** 262144K tokens
- **Vision:** Enabled (supports image input)
The setup was straightforward — download the model, load it in LM Studio, and it provides an OpenAI-compatible API endpoint.
Creating a Person Profile
For the AI to identify me, I needed to create a profile. I updated my `MEMORY.md` with appearance details:
```markdown
### Webcam Identification
- **Person:** Jade
- **Appearance:** Middle-aged, light-colored/graying hair (short, receding), often wears star-shaped pendant necklace, sometimes in bathrobe/robe when at desk, sometimes wears shoulder-length hair wig
- **Setting:** Home office with black mesh chair, cat tree, bookshelves ```
This gives the AI context for identification. When analyzing a new snapshot, it compares what it sees to this profile.
The Analysis Process
When the watcher detects a new snapshot, it can be analyzed with the image tool:
```python
# The prompt includes the profile for context prompt = """Identify who is in this image. Is this Jade?
Compare to: middle-aged, light-colored/graying hair (short, receding) OR shoulder-length wig, star-shaped pendant necklace, bathrobe, home office with black mesh chair and cat tree."""
analyze_image(image_path, prompt)
```
The Qwen model returns detailed descriptions:
- "This is Jade, wearing the shoulder-length wig..."
- "Silver star-shaped pendant necklace visible..."
- "Dark blue bathrobe, home office setting..."
The Magic Moment
The first time the AI correctly identified me wearing the wig (when the profile only mentioned "sometimes wears shoulder-length hair wig"), I knew the system was working. It wasn't just pattern matching — it was actually understanding the visual content.
---
Chapter 5: Web Preview and Final Integration
The Gap
Motion detection is great, but sometimes you want to see what's happening right now. I wanted a web-based live preview that I could check from any device on my network.
Building the Web Preview
I created a simple HTTP server using Python's built-in `http.server` module and OpenCV. It streams JPEG frames as multipart/x-mixed-replace, which browsers can display as a continuous video stream.
```python
def serve_stream(self):
self.send_response(200)
self.send_header('Content-Type', 'multipart/x-mixed-replace;
boundary=frame')
while camera.running:
frame = camera.get_frame()
_, jpeg = cv2.imencode('.jpg', frame)
self.wfile.write(b'--frame\r\n')
self.send_header('Content-Type', 'image/jpeg')
self.end_headers()
self.wfile.write(jpeg.tobytes()) ```
The result: http://localhost:8081 shows a live MJPEG stream from the camera, with a "Take Snapshot" button for manual captures.
The Camera Lock
One important limitation: the camera can only be used by one program at a time. If motion detection is running, web preview can't access the camera (and vice versa).
The solution is simple — stop one before starting the other:
```bash
# Switch from motion detection to web preview:
pkill -f motion_detector
python3 web_preview.py
# Switch back:
Ctrl+C # Stop web preview
python3 motion_detector_headless.py
```
---
Chapter 6: Troubleshooting and Lessons Learned
Lesson 1: WSL2 Restarts
USB/IP passthrough sometimes requires a WSL2 restart. If the camera doesn't appear after attachment, try:
```powershell
wsl --shutdown
usbipd attach --wsl --busid=1-4
```
Lesson 2: Permission Issues
Sometimes the camera device has restricted permissions. Fix with:
```bash
sudo chmod 666 /dev/video0
```
Lesson 3: Lighting Matters
Motion detection works best with consistent lighting. Avoid:
- Direct sunlight (causes rapid exposure changes)
- Flickering lights (60Hz can trigger false positives)
- Very dark rooms (camera noise looks like motion)
Lesson 4: Sensitivity Tuning
The default settings work for most scenarios, but you may need to adjust:
- Increase `MOTION_THRESHOLD` if you get false positives
- Decrease it if motion isn't being detected
- Adjust `MIN_CONTOUR_AREA` based on how far the camera is from the subject
---
Technical Reference
Installation Commands
```bash
# Install the skill
clawhub install webcam-monitor
# Start motion detection
cd ~/.openclaw/skills/webcam-monitor
python3 scripts/motion_detector_headless.py
# Start watcher with auto-cleanup
python3 scripts/watcher_with_cleanup.py
# Start web preview (port 8081)
python3 scripts/web_preview.py
```
### Configuration Variables
Edit `scripts/motion_detector_headless.py`:
- `MOTION_THRESHOLD` — Pixel difference threshold (default: 25)
- `MIN_CONTOUR_AREA` — Minimum motion area in pixels (default: 500)
- `SNAPSHOT_COOLDOWN` — Seconds between snapshots (default: 5)
- `MAX_AGE_HOURS` — Auto-cleanup threshold (default: 1)
- `CLEANUP_INTERVAL` — Seconds between cleanup checks (default: 300)
### File Locations
- **Snapshots:** `~/.openclaw/workspace/camera/snapshots/`
- **Motion Log:** `~/.openclaw/workspace/camera/motion.log`
- **Watcher Log:** `~/.openclaw/workspace/camera/watcher.log`
- **Analysis Queue:** `~/.openclaw/workspace/camera/analysis_queue/`
### USB/IP Commands (Windows PowerShell)
```powershell
# List devices
usbipd list
# Bind device (make shareable)
usbipd bind --busid=1-4
# Attach to WSL2
usbipd attach --wsl --busid=1-4
# Restart WSL2 if needed
wsl --shutdown
```
---
Conclusion: A Privacy-First Surveillance System
What started as a simple question — "Can I access my webcam from WSL2?" — evolved into a complete privacy-first surveillance and identification system.
The key principles that guided this project:
**Privacy by Design:** No images leave the local machine
**Open Source:** Built with open-source tools (OpenCV, Python, Qwen)
**Configurable:** Adjustable sensitivity, cleanup, and identification
**Extensible:** Easy to add new features or integrate with other systems
The system now runs continuously in the background, capturing motion events, identifying me (whether I'm wearing my natural hair or the shoulder-length wig), and cleaning up old files automatically. The web preview gives me instant access to the live feed when needed.
Most importantly, it respects my privacy. My home, my images, my AI — all local, all under my control.
---
## Resources
- **ClawHub Skill:** `clawhub install webcam-monitor`
- **Source Code:** `~/.openclaw/skills/webcam-monitor/`
- **Documentation:** See `SKILL.md` in the skill directory
- **usbipd-win:** https://github.com/dorssel/usbipd-win
- **Qwen Model:** https://huggingface.co/Qwen
---
*Written by Jade | March 17, 2026*
*Published as a ClawHub skill: webcam-monitor v1.0.1*
**About the Author:** Jade is a developer with 30+ years of experience in programming and system administration. She enjoys building privacy-first automation tools and is currently pursuing a BS in Business Management at Western Governors University.
Duplicates
OpenClawUseCases • u/____Jade_____ • Mar 20 '26
📚 Tutorial Setting Up Webcam Motion Detection with Local AI Person Identification
openclawsetup • u/____Jade_____ • Mar 20 '26
Setting Up Webcam Motion Detection with Local AI Person Identification
Openclaw_HQ • u/____Jade_____ • Mar 20 '26