diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-01-24 02:31:52 -0800 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-01-24 02:31:52 -0800 |
| commit | 5d488d56434f486b42dae04d5326a5a81106ad31 (patch) | |
| tree | 22aef0b29bfaa09e2bed4a8c13b64efc7a21ff0a /chuni-hands-evolved.py | |
initial commit1.0
Diffstat (limited to 'chuni-hands-evolved.py')
| -rw-r--r-- | chuni-hands-evolved.py | 649 |
1 files changed, 649 insertions, 0 deletions
diff --git a/chuni-hands-evolved.py b/chuni-hands-evolved.py new file mode 100644 index 0000000..0dee3fd --- /dev/null +++ b/chuni-hands-evolved.py @@ -0,0 +1,649 @@ +import cv2 +import tkinter as tk +from tkinter import Scale, StringVar, Button, Checkbutton, BooleanVar, messagebox +import chuniio +from PIL import Image, ImageTk +import numpy as np +import keyboard # Requires `keyboard` library +import json + +zones = [ + {"x": 300, "y": 100, "width": 50, "height": 50}, + {"x": 300, "y": 200, "width": 50, "height": 50}, + {"x": 300, "y": 300, "width": 50, "height": 50}, + {"x": 300, "y": 400, "width": 50, "height": 50}, + {"x": 300, "y": 500, "width": 50, "height": 50}, + {"x": 300, "y": 600, "width": 50, "height": 50}, +] + +_UMIGIRI_32_AIRZONE_LAYOUT = { + 0: "o", + 1: "0", + 2: "p", + 3: "l", + 4: ",", + 5: ".", +} + +base_positions = [{"x": zone["x"], "y": zone["y"]} for zone in zones] +zone_color_state = [] +CONFIG_FILE = "config.json" +CAMERA_WIDTH = 1280 +CAMERA_HEIGHT = 720 +ZONE_TRIGGERED_STATE = [False] * len(zones) + + +def get_avg_brightness(frame, zone): + x, y, w, h = zone["x"], zone["y"], zone["width"], zone["height"] + roi = frame[y : y + h, x : x + w] + if roi.size == 0: + return 0 + return np.mean(cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)) + + +def get_available_cameras(): + print("Now testing to see which cameras are available... (disregard the errors here)") + available_cameras = [] + for i in range(10): + cap = cv2.VideoCapture(i) + if cap.isOpened(): + available_cameras.append(i) + cap.release() + return available_cameras + + +def calculate_preview_size(width, height, max_height=720): + """Calculate preview dimensions maintaining aspect ratio""" + if height <= max_height: + return width, height + + aspect_ratio = width / height + new_height = max_height + new_width = int(aspect_ratio * new_height) + return new_width, new_height + + +def setup_gui( + camera_width: int, camera_height: int, preview_width: int, preview_height: int +) -> tk.Tk: + root = tk.Tk() + root.title("chuni-hands-evolved") + root.attributes("-topmost", True) + + screen_width = root.winfo_screenwidth() + screen_height = root.winfo_screenheight() + + window_width = min(int(screen_width * 0.8), preview_width + 40) + window_height = min(int(screen_height * 0.8), preview_height + 300) + + x = (screen_width - window_width) // 2 + y = (screen_height - window_height) // 2 + root.geometry(f"{window_width}x{window_height}+{x}+{y}") + + style = { + "bg": "#f0f0f0", + "padding": 10, + "button_bg": "#4a90e2", + "button_fg": "white", + "frame_bg": "#ffffff", + "label_font": ("Arial", 10), + "button_font": ("Arial", 10, "bold"), + } + + root.configure(bg=style["bg"]) + + main_container = tk.Frame( + root, bg=style["bg"], padx=style["padding"], pady=style["padding"] + ) + main_container.pack( + fill="both", expand=True, padx=style["padding"], pady=style["padding"] + ) + + # Video canvas + video_frame = tk.Frame(main_container, bg=style["frame_bg"], relief="ridge", bd=2) + video_frame.pack(fill="both", expand=True, pady=(0, style["padding"])) + + video_canvas = tk.Canvas(video_frame, width=preview_width, height=preview_height) + video_canvas.pack(fill="both", expand=True) + + # Camera selection + camera_frame = tk.Frame( + main_container, bg=style["frame_bg"], relief="ridge", bd=2, padx=10, pady=10 + ) + camera_frame.pack(fill="x", pady=(0, style["padding"])) + + tk.Label( + camera_frame, + text="Camera Number:", + font=style["label_font"], + bg=style["frame_bg"], + ).pack(side="left") + + available_cameras = get_available_cameras() + if not available_cameras: + print("No cameras found!") + available_cameras = [0] + + camera_var = StringVar() + camera_var.set( + str( + current_camera_index + if current_camera_index in available_cameras + else available_cameras[0] + ) + ) + camera_dropdown = tk.OptionMenu(camera_frame, camera_var, *available_cameras) + camera_dropdown.configure(width=10) + camera_dropdown.pack(side="left", padx=(10, 0)) + + tk.Label( + camera_frame, + text="Update Rate (ms):", + font=style["label_font"], + bg=style["frame_bg"], + ).pack(side="left") + + update_rate = tk.StringVar(value="5") + update_rate_spinbox = tk.Spinbox( + camera_frame, + from_=1, + to=100, + textvariable=update_rate, + width=5, + ) + update_rate_spinbox.pack(side="left", padx=5) + + tk.Label( + camera_frame, + text="You should set the update rate to something like 100 while editing for a smoother experience, then set it back to 5 for normal use.", + font=style["label_font"], + bg=style["frame_bg"], + ).pack(side="top", pady=(0, style["padding"])) + + controls_frame = tk.Frame( + main_container, bg=style["frame_bg"], relief="ridge", bd=2, padx=10, pady=10 + ) + controls_frame.pack(fill="x", pady=(0, style["padding"])) + + inputs_frame = tk.Frame(controls_frame, bg=style["frame_bg"]) + inputs_frame.pack(fill="x") + + x_frame = tk.Frame(inputs_frame, bg=style["frame_bg"]) + x_frame.pack(fill="x", pady=(0, 5)) + tk.Label( + x_frame, + text="X Position:", + font=style["label_font"], + bg=style["frame_bg"], + width=15, + ).pack(side="left") + + x_var = tk.StringVar(value="0") + x_spinbox = tk.Spinbox( + x_frame, + from_=-500, + to=1280, + textvariable=x_var, + increment=10, + width=10, + command=lambda: update_position("x"), + ) + x_spinbox.pack(side="left", padx=5) + + x_slider = Scale( + x_frame, + from_=-500, + to=1280, + orient="horizontal", + length=400, + showvalue=0, + command=lambda v: x_var.set(str(int(float(v)))), + ) + x_slider.pack(side="left", fill="x", expand=True, padx=5) + + y_frame = tk.Frame(inputs_frame, bg=style["frame_bg"]) + y_frame.pack(fill="x", pady=(0, 5)) + tk.Label( + y_frame, + text="Y Position:", + font=style["label_font"], + bg=style["frame_bg"], + width=15, + ).pack(side="left") + + y_var = tk.StringVar(value="0") + y_spinbox = tk.Spinbox( + y_frame, + from_=-200, + to=720, + textvariable=y_var, + increment=10, + width=10, + command=lambda: update_position("y"), + ) + y_spinbox.pack(side="left", padx=5) + + y_slider = Scale( + y_frame, + from_=-200, + to=720, + orient="horizontal", + length=400, + showvalue=0, + command=lambda v: y_var.set(str(int(float(v)))), + ) + y_slider.pack(side="left", fill="x", expand=True, padx=5) + + # Spacing controls + spacing_frame = tk.Frame(inputs_frame, bg=style["frame_bg"]) + spacing_frame.pack(fill="x") + tk.Label( + spacing_frame, + text="Sensor Spacing:", + font=style["label_font"], + bg=style["frame_bg"], + width=15, + ).pack(side="left") + + spacing_var = tk.StringVar(value="100") + spacing_spinbox = tk.Spinbox( + spacing_frame, + from_=10, + to=300, + textvariable=spacing_var, + increment=5, + width=10, + command=lambda: update_position("spacing"), + ) + spacing_spinbox.pack(side="left", padx=5) + + spacing_slider = Scale( + spacing_frame, + from_=10, + to=300, + orient="horizontal", + length=400, + showvalue=0, + command=lambda v: spacing_var.set(str(int(float(v)))), + ) + spacing_slider.pack(side="left", fill="x", expand=True, padx=5) + + width_frame = tk.Frame(inputs_frame, bg=style["frame_bg"]) + width_frame.pack(fill="x") + tk.Label( + width_frame, + text="Sensor Width:", + font=style["label_font"], + bg=style["frame_bg"], + width=15, + ).pack(side="left") + + width_var = tk.StringVar(value="50") + width_spinbox = tk.Spinbox( + width_frame, + from_=10, + to=1000, + textvariable=width_var, + increment=5, + width=10, + command=lambda: update_width(), + ) + width_spinbox.pack(side="left", padx=5) + + def update_width(): + try: + value = int(width_var.get()) + if 10 <= value <= 1000: + for zone in zones: + zone["width"] = value + except ValueError: + pass + + def update_position(type): + try: + if type == "x": + value = int(x_var.get()) + if -500 <= value <= 1280: + x_slider.set(value) + elif type == "y": + value = int(y_var.get()) + if -200 <= value <= 720: + y_slider.set(value) + elif type == "spacing": + value = int(spacing_var.get()) + if 10 <= value <= 300: + spacing_slider.set(value) + except ValueError: + pass + + def on_mousewheel(event, spinbox): + if event.delta > 0: + spinbox.invoke("buttonup") + else: + spinbox.invoke("buttondown") + + x_spinbox.bind("<MouseWheel>", lambda e: on_mousewheel(e, x_spinbox)) + y_spinbox.bind("<MouseWheel>", lambda e: on_mousewheel(e, y_spinbox)) + spacing_spinbox.bind("<MouseWheel>", lambda e: on_mousewheel(e, spacing_spinbox)) + width_spinbox.bind("<MouseWheel>", lambda e: on_mousewheel(e, width_spinbox)) + + buttons_frame = tk.Frame( + main_container, bg=style["frame_bg"], relief="ridge", bd=2, padx=10, pady=10 + ) + buttons_frame.pack(fill="x") + + def recalibrate(): + ret, frame = cap.read() + if ret: + calibrate(frame) + print("Recalibration complete.") + else: + print("Error: Could not capture frame for recalibration.") + + def save_config(): + config = { + "x_offset": x_slider.get(), + "y_offset": y_slider.get(), + "spacing": spacing_slider.get(), + "width": int(width_var.get()), + "camera_index": current_camera_index, + "chuniio_enabled": chuniio_enabled.get(), + } + with open(CONFIG_FILE, "w") as f: + json.dump(config, f) + messagebox.showinfo("Config", "Configuration saved successfully.") + + button_kwargs = { + "bg": style["button_bg"], + "fg": style["button_fg"], + "font": style["button_font"], + "relief": "flat", + "padx": 20, + "pady": 5, + } + + calibration_button = Button( + buttons_frame, text="Recalibrate", command=recalibrate, **button_kwargs + ) + calibration_button.pack(side="left", padx=5) + + save_button = Button( + buttons_frame, text="Save Config", command=save_config, **button_kwargs + ) + save_button.pack(side="left", padx=5) + + chuniio_enabled = BooleanVar(value=True) + chuniio_button = Checkbutton( + buttons_frame, + text="Brokenithm Evolved chuniio", + variable=chuniio_enabled, + font=style["button_font"], + relief="flat", + padx=20, + pady=5, + bg=style["frame_bg"], + ) + chuniio_button.pack(side="right", padx=5) + + keystrokes_enabled = BooleanVar(value=False) + keystroke_button = Button( + buttons_frame, + text="⌨ Keystrokes: OFF", + command=lambda: toggle_keystrokes(), + **button_kwargs, + ) + keystroke_button.pack(side="right", padx=5) + + def toggle_keystrokes(): + current = keystrokes_enabled.get() + keystrokes_enabled.set(not current) + keystroke_button.config( + text="⌨ Keystrokes: ON" if not current else "⌨ Keystrokes: OFF", + bg="#4a90e2" if not current else "#e74c3c", + ) + + def on_window_resize(event): + if event.widget == root: + new_width = video_frame.winfo_width() + new_height = video_frame.winfo_height() + aspect_ratio = preview_width / preview_height + + if new_width / new_height > aspect_ratio: + canvas_height = new_height + canvas_width = int(canvas_height * aspect_ratio) + else: + canvas_width = new_width + canvas_height = int(canvas_width / aspect_ratio) + + video_canvas.configure(width=canvas_width, height=canvas_height) + + root.bind("<Configure>", on_window_resize) + controls_frame = tk.Frame( + main_container, bg=style["frame_bg"], relief="ridge", bd=2 + ) + controls_frame.pack(fill="x", pady=(0, style["padding"])) + + buttons_frame = tk.Frame(main_container, bg=style["frame_bg"], relief="ridge", bd=2) + buttons_frame.pack(fill="x") + for slider in [x_slider, y_slider, spacing_slider]: + slider.pack(fill="x", expand=True, padx=5) + + def switch_camera(camera_width: int, camera_height: int): + global cap, current_camera_index + current_camera_index = int(camera_var.get()) + cap.release() + cap = cv2.VideoCapture(current_camera_index) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, camera_width) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, camera_height) + if not cap.isOpened(): + messagebox.showerror( + "Camera Error", "Failed to access the selected camera." + ) + + camera_var.trace_add( + "write", + lambda *args: switch_camera( + camera_width=camera_width, camera_height=camera_height + ), + ) + + return ( + root, + video_canvas, + x_slider, + y_slider, + width_var, + spacing_slider, + keystrokes_enabled, + chuniio_enabled, + update_rate, + ) + + +def calibrate(frame): + global zone_color_state + zone_color_state = [] + for zone in zones: + brightness = get_avg_brightness(frame, zone) + zone_color_state.append(brightness) + print("Calibration completed. Initial lighting values:", zone_color_state) + + +def update_frame( + preview_width, + preview_height, + chuniio_shared_memory, +): + global cap + ret, frame = cap.read() + if not ret: + print("Error: Could not capture frame.") + return + + frame = cv2.flip(frame, 1) + x_offset = x_slider.get() + y_offset = y_slider.get() + spacing = spacing_slider.get() + + # Update zone positions + for i, zone in enumerate(zones): + zone["x"] = base_positions[i]["x"] + x_offset + zone["y"] = base_positions[0]["y"] + y_offset + i * spacing + + # Process zones + for i, zone in enumerate(zones): + current_brightness = get_avg_brightness(frame, zone) + if abs(current_brightness - zone_color_state[i]) > 20: + if not ZONE_TRIGGERED_STATE[i]: + if keystrokes_enabled.get(): + print(f"Key '{_UMIGIRI_32_AIRZONE_LAYOUT[i]}' pressed.") + keyboard.press(_UMIGIRI_32_AIRZONE_LAYOUT[i]) + print(f"Zone {i} triggered") + ZONE_TRIGGERED_STATE[i] = True + cv2.rectangle( + frame, + (zone["x"], zone["y"]), + (zone["x"] + zone["width"], zone["y"] + zone["height"]), + (0, 255, 0), + 3, + ) + else: + if ZONE_TRIGGERED_STATE[i]: + if keystrokes_enabled.get(): + keyboard.release(_UMIGIRI_32_AIRZONE_LAYOUT[i]) + print(f"Key '{_UMIGIRI_32_AIRZONE_LAYOUT[i]}' released.") + ZONE_TRIGGERED_STATE[i] = False + cv2.rectangle( + frame, + (zone["x"], zone["y"]), + (zone["x"] + zone["width"], zone["y"] + zone["height"]), + (0, 0, 255), + 2, + ) + + if chuniio_enabled.get(): + chuniio.write_to_airzone(ZONE_TRIGGERED_STATE, chuniio_shared_memory) + + # Convert to RGB + rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # Get canvas dimensions + canvas_width = video_canvas.winfo_width() + canvas_height = video_canvas.winfo_height() + + # Ensure valid dimensions before resizing + if canvas_width <= 0 or canvas_height <= 0: + canvas_width = preview_width + canvas_height = preview_height + + # Calculate aspect ratio preserving dimensions + aspect_ratio = frame.shape[1] / frame.shape[0] + if canvas_width / canvas_height > aspect_ratio: + display_height = canvas_height + display_width = int(display_height * aspect_ratio) + else: + display_width = canvas_width + display_height = int(display_width / aspect_ratio) + + # Ensure minimum dimensions + display_width = max(display_width, 1) + display_height = max(display_height, 1) + + # Resize frame + try: + rgb_frame = cv2.resize(rgb_frame, (display_width, display_height)) + except cv2.error as e: + print(f"Resize error: {display_width}x{display_height}") + return + + # Display frame + img = ImageTk.PhotoImage(Image.fromarray(rgb_frame)) + video_canvas.delete("all") + video_canvas.create_image( + canvas_width // 2, canvas_height // 2, anchor="center", image=img + ) + video_canvas.image = img + + try: + rate = max(1, int(update_rate.get())) + except ValueError: + rate = 5 + root.after(rate, update_frame, preview_width, preview_height, chuniio_shared_memory) + + +def load_config() -> dict: + try: + with open(CONFIG_FILE, "r") as f: + return json.load(f) + except FileNotFoundError: + return {} + + +def main(): + global \ + root, \ + current_camera_index, \ + keystrokes_enabled, \ + chuniio_enabled, \ + video_canvas, \ + x_slider, \ + y_slider, \ + width_var, \ + spacing_slider, \ + cap, \ + update_rate, \ + CAMERA_WIDTH, \ + CAMERA_HEIGHT + print("Now starting up... please wait a bit...") + config = load_config() + current_camera_index = config.get("camera_index", 0) + cap = cv2.VideoCapture(current_camera_index) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, CAMERA_WIDTH) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, CAMERA_HEIGHT) + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + cap.set(cv2.CAP_PROP_FPS, 30) + preview_width, preview_height = calculate_preview_size(CAMERA_WIDTH, CAMERA_HEIGHT) + + actual_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH) + actual_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) + + if actual_width != CAMERA_WIDTH or actual_height != CAMERA_HEIGHT: + print( + f"Resolution {CAMERA_WIDTH}x{CAMERA_HEIGHT} not supported. Using {actual_width}x{actual_height}." + ) + CAMERA_WIDTH = int(actual_width) + CAMERA_HEIGHT = int(actual_height) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, actual_width) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, actual_height) + ret, frame = cap.read() + if ret: + calibrate(frame) + else: + print("Error: Could not capture initial frame. Check your camera.") + + ( + root, + video_canvas, + x_slider, + y_slider, + width_var, + spacing_slider, + keystrokes_enabled, + chuniio_enabled, + update_rate, + ) = setup_gui(CAMERA_WIDTH, CAMERA_HEIGHT, preview_width, preview_height) + + x_slider.set(config.get("x_offset", 0)) + y_slider.set(config.get("y_offset", 0)) + spacing_slider.set(config.get("spacing", 100)) + chuniio_enabled.set(config.get("chuniio_enabled", True)) + chuniio_shared_memory = chuniio.open_sharedmem() if chuniio_enabled.get() else None + width_var.set(str(config.get("width", 50))) + for zone in zones: + zone["width"] = int(width_var.get()) + update_frame(preview_width, preview_height, chuniio_shared_memory) + root.mainloop() + cap.release() + + +main() |
