Last active
April 2, 2026 14:37
-
-
Save dmd/94019afcd63a6606566e1ae70f742f04 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run --script | |
| # /// script | |
| # dependencies = [ | |
| # "opencv-python", | |
| # "pyvirtualcam", | |
| # "numpy", | |
| # "pillow", | |
| # ] | |
| # /// | |
| """ | |
| CUSeeMe-style virtual camera filter for Zoom. | |
| 120x120, 1bpp with Atkinson dithering, scaled up with nearest-neighbor. | |
| Requires OBS virtual camera (install OBS, start virtual camera once). | |
| """ | |
| import cv2 | |
| import numpy as np | |
| import pyvirtualcam | |
| CUSEEME_SIZE = 240 | |
| OUTPUT_W, OUTPUT_H = 640, 480 | |
| FPS = 15 | |
| def atkinson_dither(gray: np.ndarray) -> np.ndarray: | |
| """Apply Atkinson dithering to produce 1bpp output.""" | |
| img = gray.astype(np.float32) | |
| h, w = img.shape | |
| for y in range(h): | |
| for x in range(w): | |
| old = img[y, x] | |
| new = 255.0 if old >= 128 else 0.0 | |
| img[y, x] = new | |
| err = (old - new) / 8.0 | |
| # Atkinson distributes 6/8 of error (loses 2/8 intentionally) | |
| if x + 1 < w: | |
| img[y, x + 1] += err | |
| if x + 2 < w: | |
| img[y, x + 2] += err | |
| if y + 1 < h: | |
| if x - 1 >= 0: | |
| img[y + 1, x - 1] += err | |
| img[y + 1, x] += err | |
| if x + 1 < w: | |
| img[y + 1, x + 1] += err | |
| if y + 2 < h: | |
| img[y + 2, x] += err | |
| return np.clip(img, 0, 255).astype(np.uint8) | |
| def crop_center_square(frame: np.ndarray) -> np.ndarray: | |
| """Crop the largest centered square from the frame.""" | |
| h, w = frame.shape[:2] | |
| side = min(h, w) | |
| x = (w - side) // 2 | |
| y = (h - side) // 2 | |
| return frame[y:y+side, x:x+side] | |
| def cuseeme_filter(frame: np.ndarray) -> np.ndarray: | |
| """Apply CUSeeMe look: crop square, shrink, grayscale, 4-bit quantize, scale back up.""" | |
| # Crop to square | |
| square = crop_center_square(frame) | |
| # Convert to grayscale | |
| gray = cv2.cvtColor(square, cv2.COLOR_BGR2GRAY) | |
| # Shrink to CUSeeMe resolution | |
| small = cv2.resize(gray, (CUSEEME_SIZE, CUSEEME_SIZE), interpolation=cv2.INTER_AREA) | |
| # Atkinson dither to 1bpp | |
| small = atkinson_dither(small) | |
| # Scale up with nearest-neighbor for blocky look | |
| scale = OUTPUT_H // CUSEEME_SIZE # 480 / 160 = 3x | |
| big = cv2.resize(small, (OUTPUT_H, OUTPUT_H), interpolation=cv2.INTER_NEAREST) | |
| # Embed in standard 640x480 frame with black pillarboxing | |
| frame_out = np.zeros((OUTPUT_H, OUTPUT_W), dtype=np.uint8) | |
| x_offset = (OUTPUT_W - OUTPUT_H) // 2 | |
| frame_out[:, x_offset:x_offset+OUTPUT_H] = big | |
| # Convert back to RGB (3-channel gray) for virtual camera | |
| rgb = cv2.cvtColor(frame_out, cv2.COLOR_GRAY2RGB) | |
| return rgb | |
| def main(): | |
| cap = cv2.VideoCapture(0) | |
| if not cap.isOpened(): | |
| print("Error: cannot open webcam") | |
| return | |
| cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) | |
| cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) | |
| print(f"Starting CUSeeMe filter: {CUSEEME_SIZE}x{CUSEEME_SIZE}, 1bpp Atkinson dithered") | |
| print("Select 'OBS Virtual Camera' in Zoom's camera settings.") | |
| print("Press Ctrl+C to stop.") | |
| with pyvirtualcam.Camera(width=OUTPUT_W, height=OUTPUT_H, fps=FPS) as cam: | |
| print(f"Virtual camera: {cam.device}") | |
| frame_count = 0 | |
| while True: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| if frame_count == 0: | |
| h, w = frame.shape[:2] | |
| print(f"Webcam raw frame: {w}x{h}") | |
| filtered = cuseeme_filter(frame) | |
| if frame_count == 30: | |
| # Save debug images to verify sizing | |
| gray = cv2.cvtColor(crop_center_square(frame), cv2.COLOR_BGR2GRAY) | |
| small = cv2.resize(gray, (CUSEEME_SIZE, CUSEEME_SIZE), interpolation=cv2.INTER_AREA) | |
| small = atkinson_dither(small) | |
| cv2.imwrite("/Users/dmd/zoomfilter/debug_160.png", small) | |
| cv2.imwrite("/Users/dmd/zoomfilter/debug_output.png", cv2.cvtColor(filtered, cv2.COLOR_RGB2BGR)) | |
| print(f"Debug images saved. small shape: {small.shape}, output shape: {filtered.shape}") | |
| cam.send(filtered) | |
| cam.sleep_until_next_frame() | |
| frame_count += 1 | |
| cap.release() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment