diff --git a/emioapi/__main__.py b/emioapi/__main__.py index 1a3d874..16c8a97 100644 --- a/emioapi/__main__.py +++ b/emioapi/__main__.py @@ -32,8 +32,8 @@ def calibrate(): import emioapi from emioapi._logging_config import logger - camera = emioapi.EmioCamera(show=True) print("Available cameras:", emioapi.EmioCamera.listCameras()) + camera = emioapi.EmioCamera(show=True, track_markers=True) if camera.open(): print(f"Camera {camera.camera_serial} opened.") diff --git a/emioapi/_camerafeedwindow.py b/emioapi/_camerafeedwindow.py deleted file mode 100644 index c29953f..0000000 --- a/emioapi/_camerafeedwindow.py +++ /dev/null @@ -1,68 +0,0 @@ -import tkinter as tk -from tkinter import ttk - -import cv2 as cv -from PIL import ImageTk, Image - -""" - This class is used to create a camera feed window that displays the video feed from a camera. - It runs in a separate thread and can be started and stopped using the start() and stop() methods. - The camera feed is displayed in a window named 'mask'. - The class uses OpenCV to create the window and display the video feed. - The camera feed is passed to the class as a frame parameter. -""" -class CameraFeedWindow(): - windowCount = 0 - def __init__(self, rootWindow, name=f"Camera Feed Window", trackbarParams={}, on_change=None, *args, **kwargs): - super().__init__(*args, **kwargs) - CameraFeedWindow.windowCount += 1 - self.rootWindow = rootWindow - self.name = name - self.running = True - self.window = tk.Toplevel(self.rootWindow) - self.image = None - - self.window.title(self.name) - # self.window.geometry("640x480") - self.window.resizable(False, False) - self.canvas = tk.Canvas(self.window, width=640, height=480) - self.canvasImage = self.canvas.create_image(320, 240, anchor="center") - self.canvas.pack() - - self.default_param = trackbarParams.copy() - self.trackbarParams = trackbarParams - self.trackbars = {} - if self.trackbarParams: - for key, _ in self.trackbarParams.items(): - self.trackbars[key] = ttk.Scale(self.window, from_=0, to=255, orient='horizontal') - self.trackbars[key].set(self.default_param[key]) - self.trackbars[key].configure(command = self.on_change if on_change is None else on_change) - ttk.Label(self.window, text=key).pack() - self.trackbars[key].pack(fill="x", padx=5) - self.resetButton = ttk.Button(self.window, text="Reset From File", command=self.reset) - self.resetButton.pack(pady=5) - - self.window.protocol("WM_DELETE_WINDOW", self.closed) - - def closed(self): - CameraFeedWindow.windowCount -= 1 - self.window.destroy() - self.running = False - - def on_change(self, value): - for key, _ in self.trackbarParams.items(): - self.trackbarParams[key] = int(self.trackbars[key].get()) - - def reset(self): - for key, val in self.trackbarParams.items(): - self.trackbars[key].set(self.default_param[key]) - self.trackbarParams[key] = self.default_param[key] - - def set_frame(self, frame): - # Convert the frame to a format that can be displayed in the Tkinter window - frame = cv.cvtColor(frame, cv.COLOR_BGR2RGB) - frame = cv.resize(frame, (640, 480)) - if self.rootWindow and self.window and self.window.winfo_exists(): - self.image = ImageTk.PhotoImage(image=Image.fromarray(frame)) - self.canvas.imgref = self.image - self.canvas.itemconfig(self.canvasImage, image=self.image) \ No newline at end of file diff --git a/emioapi/_depthcamera.py b/emioapi/_depthcamera.py index 693b928..dbaf5b5 100644 --- a/emioapi/_depthcamera.py +++ b/emioapi/_depthcamera.py @@ -1,12 +1,14 @@ import json import time from enum import Enum +import threading +from types import SimpleNamespace import numpy as np import cv2 as cv import pyrealsense2 as rs -from ._camerafeedwindow import CameraFeedWindow +from ._gui import EmioAPIGUI from ._positionestimation import PositionEstimation, CONFIG_FILENAME from emioapi._logging_config import logger @@ -119,6 +121,10 @@ def __init__(self, self._camera_serial = camera_serial self.initialized = True + self.gui_object: SimpleNamespace = None + """`gui_object`: holds references to the EmioAPIGUI instance itself `gui_handle` and to the running thread of the gui `gui_thread`""" + self.lock = threading.Lock() + if not self.initialized: return @@ -167,40 +173,13 @@ def set_depth_min(self, new_depth_min: int): def create_feed_windows(self): - import tkinter as tk - from tkinter import ttk - self.rootWindow = tk.Tk() - self.rootWindow.resizable(False, False) - - self.rootWindow.title("Camera Feed Manager") - ttk.Button(self.rootWindow, text="Close Windows", command=self.quit).pack(side=tk.BOTTOM, padx=5, pady=5) - ttk.Button(self.rootWindow, text="Save", command=lambda: json.dump(self.parameter, open(CONFIG_FILENAME, 'w'))).pack(side=tk.BOTTOM, padx=5, pady=5) - ttk.Button(self.rootWindow, text="Mask Window", command=self.create_mask_window).pack(side=tk.BOTTOM, padx=5, pady=5) - ttk.Button(self.rootWindow, text="Frame Window", command=self.create_frame_window).pack(side=tk.BOTTOM, padx=5, pady=5) - ttk.Button(self.rootWindow, text="HSV Window", command=self.create_HSV_window).pack(side=tk.BOTTOM, padx=5, pady=5) - ttk.Button(self.rootWindow, text="Depth Window", command=self.createDepthWindow).pack(side=tk.BOTTOM, padx=5, pady=5) - - self.create_mask_window() - self.create_frame_window() - - self.rootWindow.protocol("WM_DELETE_WINDOW", self.quit) - self.rootWindow.update_idletasks() - - def create_mask_window(self): - if self.maskWindow is None or not self.maskWindow.running: - self.maskWindow = CameraFeedWindow(rootWindow=self.rootWindow, trackbarParams=self.parameter, name='Mask') - - def create_frame_window(self): - if self.frameWindow is None or not self.frameWindow.running: - self.frameWindow = CameraFeedWindow(rootWindow=self.rootWindow, name='RGB Frame') + """ + Creates the GUI thread and the camera feed window. The GUI thread is created only once and the camera feed window is created in the GUI thread. The camera feed window is updated in the main thread.""" - def create_HSV_window(self): - if self.hsvWindow is None or not self.hsvWindow.running: - self.hsvWindow = CameraFeedWindow(rootWindow=self.rootWindow, name='HSV Frame') + self.gui_object = EmioAPIGUI.start_in_thread() + self.gui_window = EmioAPIGUI.add_camera_feed(trackbarParams=self.parameter, save_callback=lambda: json.dump(self.parameter, open(CONFIG_FILENAME, 'w'))) + """ EmioAPIGUI._CameraFeedWindow object. """ - def createDepthWindow(self): - if self.depthWindow is None or not self.depthWindow.running: - self.depthWindow = CameraFeedWindow(rootWindow=self.rootWindow, name='Depth Frame') def quit(self): for window in [self.maskWindow, self.frameWindow, self.hsvWindow, self.depthWindow]: @@ -259,8 +238,9 @@ def calibrate(self): success = False self.calibration_status = CalibrationStatusEnum.CALIBRATING - # Create the windows to display the binrary mask and the HSV frame - calibration_window = CameraFeedWindow(rootWindow=self.rootWindow, name='Calibration') + # Create the windows to display the binary mask and the HSV frame + if self.show_video_feed and self.gui_window: + calibration_window = self.gui_window if self.position_estimator is not None: while self.position_estimator.count_calibration_frames < 200 and time.time() - starttime < 300: @@ -269,16 +249,11 @@ def calibrate(self): if hasFrame: success = self.position_estimator.calibrate(self.frame, self.depth_frame, first, calibration_window) first = success if not first else first - if self.show_video_feed: - self.rootWindow.update() if success: self.position_estimator.compute_camera_to_simulation_transform() logger.info(f"Camera {self.camera_serial} successfully calibrated.") - # Close the calibration window - calibration_window.closed() - self.calibration_status = CalibrationStatusEnum.CALIBRATED if success else CalibrationStatusEnum.NOT_CALIBRATED return success @@ -295,11 +270,12 @@ def get_frame(self): return False # Convert images to numpy arrays - self.depth_frame = np.asanyarray(depth_frame.get_data()) - self.frame = np.asanyarray(color_frame.get_data()) - self.depth_rsframe = depth_frame + with self.lock: + self.depth_frame = np.asanyarray(depth_frame.get_data()) + self.frame = np.asanyarray(color_frame.get_data()) + self.depth_rsframe = depth_frame - return True + return True def update(self): @@ -311,78 +287,63 @@ def update(self): def process_frame(self): # if frame is read correctly ret is True + with self.lock: + self.hsvFrame = cv.cvtColor(self.frame, cv.COLOR_BGR2HSV) - self.hsvFrame = cv.cvtColor(self.frame, cv.COLOR_BGR2HSV) + # color definition + red_lower = np.array([self.parameter['hue_l'], self.parameter['sat_l'], self.parameter['value_l']]) + red_upper = np.array([self.parameter['hue_h'], self.parameter['sat_h'], self.parameter['value_h']]) - # color definition - red_lower = np.array([self.parameter['hue_l'], self.parameter['sat_l'], self.parameter['value_l']]) - red_upper = np.array([self.parameter['hue_h'], self.parameter['sat_h'], self.parameter['value_h']]) + # red color mask (sort of thresholding, actually segmentation) + mask = cv.inRange(self.hsvFrame, red_lower, red_upper) + mask2 = cv.inRange(self.depth_frame, self.depth_min, self.depth_max) - # red color mask (sort of thresholding, actually segmentation) - mask = cv.inRange(self.hsvFrame, red_lower, red_upper) - mask2 = cv.inRange(self.depth_frame, self.depth_min, self.depth_max) + mask = cv.bitwise_and(mask, mask2, mask=mask) - mask = cv.bitwise_and(mask, mask2, mask=mask) + erosion_shape = cv.MORPH_RECT + erosion_size = self.parameter['erosion_size'] + element = cv.getStructuringElement(erosion_shape, (2 * erosion_size + 1, 2 * erosion_size + 1), + (erosion_size, erosion_size)) - erosion_shape = cv.MORPH_RECT - erosion_size = self.parameter['erosion_size'] - element = cv.getStructuringElement(erosion_shape, (2 * erosion_size + 1, 2 * erosion_size + 1), - (erosion_size, erosion_size)) + mask = cv.erode(mask, element, iterations=3) + mask = cv.dilate(mask, element, iterations=3) - mask = cv.erode(mask, element, iterations=3) - mask = cv.dilate(mask, element, iterations=3) + self.maskFrame = cv.bitwise_and(self.frame, self.frame, mask=mask) - self.maskFrame = cv.bitwise_and(self.frame, self.frame, mask=mask) + if self.tracking: + contours, _ = cv.findContours(mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) + if len(contours) != 0: + areas = [cv.contourArea(cnt) for cnt in contours] - if self.tracking: - contours, _ = cv.findContours(mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) - if len(contours) != 0: - areas = [cv.contourArea(cnt) for cnt in contours] + self.trackers_pos = [] + self.trackers_pos_image = [] + for i, a in enumerate(areas): + if a > self.parameter['area']: + x, y = compute_contour_center(contours[i]) + marker_mask = np.zeros_like(mask) - self.trackers_pos = [] - self.trackers_pos_image = [] - for i, a in enumerate(areas): - if a > self.parameter['area']: - x, y = compute_contour_center(contours[i]) - marker_mask = np.zeros_like(mask) + depth = compute_median_depth(contours[i], self.depth_frame) if self.depth_frame[y, x] == 0 else self.depth_frame[y, x] + worldx, worldy, worldz = self.position_estimator.camera_image_to_simulation(x, y, depth) + self.trackers_pos.append([worldx, worldy, worldz]) + self.trackers_pos_image.append([x, y, depth]) - depth = compute_median_depth(contours[i], self.depth_frame) if self.depth_frame[y, x] == 0 else self.depth_frame[y, x] - worldx, worldy, worldz = self.position_estimator.camera_image_to_simulation(x, y, depth) - self.trackers_pos.append([worldx, worldy, worldz]) - self.trackers_pos_image.append([x, y, depth]) + cv.drawContours(marker_mask, [contours[i]], -1, color=255, thickness=-1) + for frame in [self.hsvFrame, self.frame]: + cv.circle(frame, (x, y), 2, color=255, thickness=-1) + cv.putText(frame, f"{i} ({x}, {y}, {depth})", (x, y), cv.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1) + cv.putText(frame, f"{i} ({worldx:.2f}, {worldy:.2f}, {worldz:.2f})", (x, y + 15), cv.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1) - cv.drawContours(marker_mask, [contours[i]], -1, color=255, thickness=-1) - for frame in [self.hsvFrame, self.frame]: - cv.circle(frame, (x, y), 2, color=255, thickness=-1) - cv.putText(frame, f"{i} ({x}, {y}, {depth})", (x, y), cv.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1) - cv.putText(frame, f"{i} ({worldx:.2f}, {worldy:.2f}, {worldz:.2f})", (x, y + 15), cv.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1) + if self.show_video_feed: + cv.drawContours(self.frame, [contours[i]], -1, (255, 255, 0), 3) - if self.show_video_feed: - cv.drawContours(self.frame, [contours[i]], -1, (255, 255, 0), 3) + if self.compute_point_cloud: + points = self.pc.calculate(self.depth_rsframe) + v = points.get_vertices() + self.point_cloud = np.asanyarray(v).view(np.float32).reshape(-1, 3) # xyz - if self.compute_point_cloud: - points = self.pc.calculate(self.depth_rsframe) - v = points.get_vertices() - self.point_cloud = np.asanyarray(v).view(np.float32).reshape(-1, 3) # xyz - - if self.show_video_feed: - if self.rootWindow is None: - self.create_feed_windows() - - if self.maskWindow is not None and self.maskWindow.running: - self.maskWindow.set_frame(self.maskFrame) - - if self.frameWindow is not None and self.frameWindow.running: - self.frameWindow.set_frame(self.frame) - - if self.hsvWindow is not None and self.hsvWindow.running: - self.hsvWindow.set_frame(self.hsvFrame) - - if self.depthWindow is not None and self.depthWindow.running: + if self.show_video_feed and self.gui_window and self.gui_object.gui_handle.is_running(): colorized = np.asanyarray(rs.colorizer().colorize(self.depth_rsframe).get_data()) - self.depthWindow.set_frame(colorized) - - self.rootWindow.update() + self.gui_window.set_frames(self.frame, colorized, self.maskFrame, self.hsvFrame) def close(self): diff --git a/emioapi/_gui.py b/emioapi/_gui.py new file mode 100644 index 0000000..62e2d85 --- /dev/null +++ b/emioapi/_gui.py @@ -0,0 +1,195 @@ +from typing import Final, Tuple +from types import SimpleNamespace + +import dearpygui.dearpygui as dpg +import cv2 as cv +import numpy as np + +from emioapi._logging_config import logger + + +class EmioAPIGUI(): + """ + This class is used to create a GUI for the Emio API. + The GUI is created in a separate thread and the main thread is used to update the camera frames. + It uses DearPyGui to create a window that displays the camera feed and allows the user to adjust parameters using trackbars. + The camera feed is displayed in tabs, and the user can switch between different camera feeds if multiple cameras are connected. + You need to feed the camera frames to the GUI using the `set_frames` method of the `_CameraFeedWindow` class returned by the `add_camera_feed` method of this class. + + Note: Only one instance of this class can be created per process. This is because DearPyGui does not support multiple contexts in the same process. If you need to create multiple camera feed windows, you can use the `add_camera_feed` method of this class to create additional windows in the same context. + """ + + TAG_MAINWINDOW:Final[str] = "MainWindow" + TAG_DEPTHTEX:Final[str] = "tex_depth" + TAG_RGBTEX:Final[str] = "tex_rgb" + TAG_HSVTEX:Final[str] = "tex_hsv" + TAG_MASKTEX:Final[str] = "tex_mask" + TEXWIDTH:Final[int] = 640 + TEXHEIGHT:Final[int] = 480 + + _TIMEOUT_THREAD: Final[int]=5 + + windowCount = 0 + main_window = None + gui = SimpleNamespace(gui_handle=None, gui_thread=None) + + @staticmethod + def start_in_thread(trackbarParams={}, save_callback=None): + from threading import Thread + from time import time, sleep + + def _gui_worker(result: SimpleNamespace, trackbarParams={}, save_callback=None): + result.gui_handle = EmioAPIGUI() + logger.debug(f"Starting Emio API GUI inside a thread.") + result.gui_handle.run_loop() + + if EmioAPIGUI.windowCount==0: + EmioAPIGUI.gui.gui_thread = Thread(target=_gui_worker, daemon=True, args=(EmioAPIGUI.gui, trackbarParams, save_callback)) + EmioAPIGUI.gui.gui_thread.start() + startime = time() + while not EmioAPIGUI.gui.gui_handle: + if time()-startime < EmioAPIGUI._TIMEOUT_THREAD: + logger.info(f"Waiting for thread {EmioAPIGUI.gui.gui_thread} to start") + sleep(0.5) + else: + raise TimeoutError("Could not open EmioAPIGUI thread in time.") + logger.debug(f"Started Emio API GUI inside a thread {EmioAPIGUI.gui.gui_thread}.") + else: + logger.warning(f"EmioAPIGUI thread is already running in a thread.") + + return EmioAPIGUI.gui + + + @staticmethod + def add_camera_feed(trackbarParams={}, save_callback=None): + return EmioAPIGUI._CameraFeedWindow(trackbarParams, save_callback) + + + def __init__(self): + try: + if EmioAPIGUI.windowCount==0: + dpg.create_context() + dpg.create_viewport(title="Camera Feed", width=1150, height=780, resizable=False) + dpg.setup_dearpygui() + EmioAPIGUI.main_window = dpg.add_window(tag=EmioAPIGUI.TAG_MAINWINDOW, no_title_bar=True) + dpg.add_tab_bar(tag="TAG_TABBAR", parent=EmioAPIGUI.main_window) + dpg.show_viewport() + dpg.set_primary_window(EmioAPIGUI.TAG_MAINWINDOW, True) + self.context_exists = True + except Exception as e: + logger.error(str(e)) + raise(e) + + + def run_loop(self): + while self.is_running(): + dpg.render_dearpygui_frame() + + logger.debug("Closing GUI...") + self.context_exists = False + dpg.destroy_context() + logger.warning("GUI is closed but EmioAPI is still running!") + + + def is_running(self): + return dpg.is_dearpygui_running() if self.context_exists else False + + + class _CameraFeedWindow(): + """ + This class is used to create a camera feed window that displays the video feed from a camera + It displays four frames: RGB, Depth, Mask and HSV. + The class also creates trackbars for the parameters passed in the trackbarParams dictionary.""" + + def __init__(self, trackbarParams={}, save_callback=None, label=""): + self.window_id = str(EmioAPIGUI.windowCount) + self.window_label = label if label else f"Window {self.window_id}" + self.window_tag = f"{self.window_label}_{self.window_id}" + + logger.debug(f"Creating CameraFeedWindow {self.window_label} with tag {self.window_tag}") + + self.default_param = trackbarParams.copy() + self.trackbarParams = trackbarParams + self.trackbars = {} + + self.images_scale = 0.6 + + self.save_callback = save_callback + + + # Textures for camera frames + with dpg.texture_registry(show=False): + blank_data = np.zeros((EmioAPIGUI.TEXWIDTH * EmioAPIGUI.TEXHEIGHT * 4,), dtype=np.float32) + dpg.add_dynamic_texture(width=EmioAPIGUI.TEXWIDTH, height=EmioAPIGUI.TEXHEIGHT, default_value=blank_data, tag=EmioAPIGUI.TAG_RGBTEX+self.window_id) + dpg.add_dynamic_texture(width=EmioAPIGUI.TEXWIDTH, height=EmioAPIGUI.TEXHEIGHT, default_value=blank_data, tag=EmioAPIGUI.TAG_DEPTHTEX+self.window_id) + dpg.add_dynamic_texture(width=EmioAPIGUI.TEXWIDTH, height=EmioAPIGUI.TEXHEIGHT, default_value=blank_data, tag=EmioAPIGUI.TAG_MASKTEX+self.window_id) + dpg.add_dynamic_texture(width=EmioAPIGUI.TEXWIDTH, height=EmioAPIGUI.TEXHEIGHT, default_value=blank_data, tag=EmioAPIGUI.TAG_HSVTEX+self.window_id) + + # Tab window + with dpg.tab(label=self.window_label, parent="TAG_TABBAR"): + with dpg.child_window(no_scroll_with_mouse=True, no_scrollbar=True): + # params + with dpg.child_window(width=300): + if self.trackbarParams: + for key, val in self.trackbarParams.items(): + dpg.add_text(default_value=str(key)) + dpg.add_slider_int(tag=f"TAG_SLIDER{self.window_id}_{key}",min_value=0, max_value=255, default_value=val, callback=self.on_change) + with dpg.group(horizontal=True): + dpg.add_button(label="Save Params", callback=self.save_params) + dpg.add_button(label="Reset Params", callback=self.reset_params) + + # frames + width, height = self.image_size + origin = (316, 8) + with dpg.child_window(label="RGB Frame", pos=origin, width=width, height=height+62): + dpg.add_text(default_value="RGB") + dpg.add_image(EmioAPIGUI.TAG_RGBTEX+self.window_id, label="RGB", width=width, height=height) + with dpg.child_window(label="Depth Frame", pos=(origin[0]+width+16, origin[1]), width=width, height=height+62): + dpg.add_text(default_value="Depth") + dpg.add_image(EmioAPIGUI.TAG_DEPTHTEX+self.window_id, width=width, height=height) + with dpg.child_window(label="Mask Frame", pos=(origin[0], origin[1]+height+35), width=width, height=height+62): + dpg.add_text(default_value="Mask") + dpg.add_image(EmioAPIGUI.TAG_MASKTEX+self.window_id, width=width, height=height) + with dpg.child_window(label="HSV Frame", pos=(origin[0]+width+16, origin[1]+height+35), width=width, height=height+62): + dpg.add_text(default_value="HSV") + dpg.add_image(EmioAPIGUI.TAG_HSVTEX+self.window_id, width=width, height=height) + + EmioAPIGUI.windowCount=+1 + + + @property + def image_size(self) -> Tuple[int, int]: + return (self.images_scale * EmioAPIGUI.TEXWIDTH, self.images_scale*EmioAPIGUI.TEXHEIGHT) + + + def reset_params(self): + for key, _ in self.trackbarParams.items(): + dpg.set_value(f"TAG_SLIDER{self.window_id}_{key}", self.default_param[key]) + self.trackbarParams[key] = self.default_param[key] + + + def save_params(self): + if self.save_callback is None: + logger.warning("Parameters not saved. No callback given at init.") + else: + self.save_callback() + + + def on_change(self, sender_data, app_data): + for key, _ in self.trackbarParams.items(): + self.trackbarParams[key] = int(dpg.get_value(f"TAG_SLIDER{self.window_id}_{key}")) + + + def set_frames(self, rgb, depth, mask, hsv): + if rgb is not None: + dpg.set_value(EmioAPIGUI.TAG_RGBTEX+self.window_id, np.ascontiguousarray(cv.cvtColor(rgb, cv.COLOR_BGR2RGBA), dtype=np.float32)/255) + if depth is not None: + dpg.set_value(EmioAPIGUI.TAG_DEPTHTEX+self.window_id, np.ascontiguousarray(cv.cvtColor(depth, cv.COLOR_BGR2RGBA), dtype=np.float32)/255) + if mask is not None: + dpg.set_value(EmioAPIGUI.TAG_MASKTEX+self.window_id, np.ascontiguousarray(cv.cvtColor(mask, cv.COLOR_BGR2RGBA), dtype=np.float32)/255) + if hsv is not None: + dpg.set_value(EmioAPIGUI.TAG_HSVTEX+self.window_id, np.ascontiguousarray(cv.cvtColor(hsv, cv.COLOR_BGR2RGBA), dtype=np.float32)/255) + + + def close(self): + dpg.delete_item(self.window_tag) \ No newline at end of file diff --git a/emioapi/_logging_config.py b/emioapi/_logging_config.py index 62a69a3..3bd7bbc 100644 --- a/emioapi/_logging_config.py +++ b/emioapi/_logging_config.py @@ -1,18 +1,21 @@ import logging +import sys # based on https://docs.python.org/3/howto/logging-cookbook.html logger = logging.getLogger('emioapi_logger') logger.setLevel(logging.INFO) # Log everything (DEBUG level or higher) -# Prevent multiple handlers if the logger is configured multiple times -# if not logger.handlers: -# Create a console handler to log to the console -ch = logging.StreamHandler() -ch.setLevel(logging.INFO) +if not logger.handlers: + # Prevent multiple handlers if the logger is configured multiple times + # if not logger.handlers: + # Create a console handler to log to the console + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) -# Define log format -FORMAT = "[%(levelname)s]\t[%(filename)s:%(lineno)s - %(funcName)s() ] %(message)s" -formatter = logging.Formatter(FORMAT) -ch.setFormatter(formatter) + # Define log format + FORMAT = "[%(levelname)s]\t[%(filename)s:%(lineno)s - %(funcName)s() ] %(message)s" + formatter = logging.Formatter(FORMAT) + ch.setFormatter(formatter) -logger.addHandler(ch) \ No newline at end of file + ch.flush = sys.stdout.flush + logger.addHandler(ch) \ No newline at end of file diff --git a/emioapi/_positionestimation.py b/emioapi/_positionestimation.py index 28bf8c0..314c7b1 100644 --- a/emioapi/_positionestimation.py +++ b/emioapi/_positionestimation.py @@ -337,10 +337,10 @@ def calibrate(self, frame, depth_image, aggregate, window=None)-> bool: (int(corners[0][0][i][0]), int(corners[0][0][i][1])), cv.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) for i in range(len(corners[0][0]))] frame = cv.putText(frame, f"Calibration progress: {self.count_calibration_frames}/200", (10, 30), - cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) + cv.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1) if window: - window.set_frame(frame) + window.set_frames(frame, depth_image, None, None) return True diff --git a/examples/camera_example.py b/examples/camera_example.py index 4c63510..addebac 100644 --- a/examples/camera_example.py +++ b/examples/camera_example.py @@ -10,25 +10,25 @@ from emioapi._logging_config import logger -def main(emio: EmioCamera): +def main(camera: EmioCamera): # emio.calibrate() # calibrate the camera if needed - while emio.is_running: + while camera.is_running: try: - emio.update() # update the camera frame and trackers + camera.update() # update the camera frame and trackers print("-"*20) - logger.info(f"Camera parameters: {emio.parameters}") - logger.info(f"Camera show: {emio.show_frames}") - logger.info(f"Camera tracking: {emio.track_markers}") - logger.info(f"Camera compute point cloud: {emio.compute_point_cloud}") - logger.info(f"Camera is running: {emio.is_running}") - logger.info(f"Count tracker: {len(emio.trackers_pos)}") - logger.info(f"Trackers positions: {emio.trackers_pos}") - logger.info(f"Point cloud shape: {emio.point_cloud.shape}") - logger.info(f"HSV Frame shape: {emio.hsv_frame.shape}") - logger.info(f"Mask Frame shape: {emio.mask_frame.shape}") + logger.info(f"Camera parameters: {camera.parameters}") + logger.info(f"Camera show: {camera.show_frames}") + logger.info(f"Camera tracking: {camera.track_markers}") + logger.info(f"Camera compute point cloud: {camera.compute_point_cloud}") + logger.info(f"Camera is running: {camera.is_running}") + logger.info(f"Count tracker: {len(camera.trackers_pos)}") + logger.info(f"Trackers positions: {camera.trackers_pos}") + logger.info(f"Point cloud shape: {camera.point_cloud.shape}") + logger.info(f"HSV Frame shape: {camera.hsv_frame.shape}") + logger.info(f"Mask Frame shape: {camera.mask_frame.shape}") except KeyboardInterrupt: logger.info("Keyboard interrupt received.") @@ -46,21 +46,21 @@ def main(emio: EmioCamera): logger.info("Opening and configuring EMIO Camera...") - emio = EmioCamera(show=True, track_markers=True, compute_point_cloud=True) - emio.fps = 30 # sets the fps to 30. Default is 60 and can only be one of 30. 60 or 90fps - emio.depth_max = 600 # sets the maximum depth to 600mm. Default is 430mm - emio.depth_min = 0 # sets the minimum depth to 0mm. Default is 2mm + emiocamera = EmioCamera(show=True, track_markers=True, compute_point_cloud=True) + emiocamera.fps = 30 # sets the fps to 30. Default is 60 and can only be one of 30. 60 or 90fps + emiocamera.depth_max = 600 # sets the maximum depth to 600mm. Default is 430mm + emiocamera.depth_min = 0 # sets the minimum depth to 0mm. Default is 2mm - if emio.open(): # This will open the first available Realsense camera + if emiocamera.open(): # This will open the first available Realsense camera - logger.info(f"Emio camera {emio.camera_serial} opened.") + logger.info(f"Emio camera {emiocamera.camera_serial} opened.") logger.info("Running main function...") - main(emio) + main(emiocamera) logger.info("Main function completed.") logger.info("Closing Emio API...") - emio.close() + emiocamera.close() logger.info("EMIO API closed.") except Exception as e: diff --git a/pyproject.toml b/pyproject.toml index 0414c24..5022dd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,10 @@ dependencies = [ "pyrealsense2-macosx; platform_system == 'Darwin'", "pillow>=12.3.0", "opencv-python>=4.11.0.86", - "dynamixelmotorsapi", + "dynamixelmotorsapi @ git+https://github.com/SofaComplianceRobotics/DynamixelMotorsAPI@main", "numpy", + "dearpygui>=2.3.1", + "cv2-enumerate-cameras>=1.4.0", ] description = "An API for controlling the motors of the EMIO robot." keywords = ["roboticsc", "emio", "api"] diff --git a/to_delete.py b/to_delete.py new file mode 100644 index 0000000..3fcf5b3 --- /dev/null +++ b/to_delete.py @@ -0,0 +1,72 @@ +import threading + +import cv2 +from cv2_enumerate_cameras import enumerate_cameras +import dearpygui.dearpygui as dpg +import dearpygui.demo as demo + +import dearpygui.dearpygui as dpg +import dearpygui.demo as demo + +# dpg.create_context() +# dpg.create_viewport(title='Custom Title', width=600, height=600) + +# demo.show_demo() + +# dpg.setup_dearpygui() +# dpg.show_viewport() +# dpg.start_dearpygui() +# dpg.destroy_context() + + +from emioapi._gui import EmioAPIGUI + +# for camera_info in enumerate_cameras(): +# print(f"Index: {camera_info.index}, Name: {camera_info.name}, Path: {camera_info.path}") + +# Initialisation de la caméra (0 = caméra par défaut) +cap = cv2.VideoCapture(0) +ret, frame = cap.read() +if not ret: + print("Erreur : Impossible d'accéder à la caméra 0.") + exit() + +# cap1 = cv2.VideoCapture(1) +# ret1, frame1 = cap1.read() +# if not ret1: +# print("Erreur : Impossible d'accéder à la caméra 1.") +# exit() + +# Récupération des dimensions de la capture caméra +height, width, _ = frame.shape + +running = True +gui = None +lock = threading.Lock() +mockparams = { + "a": 10, + "b": 128 +} + + +def update_camera(): + global emiogui, gui + import numpy as np + ret, frame = cap.read() + # ret1, frame1 = cap1.read() + if ret: + emiogui.set_frames(frame, frame, frame, frame) + frame1 = np.random.randint(0, 255, size=(EmioAPIGUI.TEXWIDTH, EmioAPIGUI.TEXHEIGHT, 3), dtype=np.uint8) # Uncomment if not extra camera + emiogui1.set_frames(frame1, frame1, frame1, frame1) + +gui = EmioAPIGUI.start_in_thread() +emiogui = EmioAPIGUI.add_camera_feed(trackbarParams=mockparams, save_callback=lambda: print("Saved params")) +emiogui1 = EmioAPIGUI.add_camera_feed(trackbarParams=mockparams, save_callback=lambda: print("Saved params")) + +while running : + if gui.gui_handle.is_running(): + update_camera() + +print("out of while") + +cap.release() diff --git a/uv.lock b/uv.lock index d5c0108..9b07475 100644 --- a/uv.lock +++ b/uv.lock @@ -247,6 +247,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, ] +[[package]] +name = "cv2-enumerate-cameras" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/61/65ea4f16f211b6bd90c18384296a6a79e7d5f49f56082ee36a2185a8f5d6/cv2_enumerate_cameras-1.4.0.tar.gz", hash = "sha256:d6f39a236e15cc1bddf00e852a4c6bdfea94d3eebb2d3213848bb5c4036b0689", size = 12315, upload-time = "2026-09-26T14:09:42.754Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/2f/5277bcabd62e675b85f00825d8ebc3ceaa23c7fd6362a4dc09f3accc716e/cv2_enumerate_cameras-1.4.0-cp310-abi3-win32.whl", hash = "sha256:9c5cd79dd6e8d2b6450e40bb208d968f50976cf15f276c5e10c9880498e23b25", size = 211333, upload-time = "2026-09-26T14:09:32.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6d/15b6f504b79749a45a53304310c1665044f03d1b79ec2b00598924fc0d99/cv2_enumerate_cameras-1.4.0-cp310-abi3-win_amd64.whl", hash = "sha256:0dcc60c0cc1638bbfb381d0430391d3aabb70324e5eef338d018831207813060", size = 209221, upload-time = "2026-09-26T14:09:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/74/e6/96981e59cd724bad4155949b14bbb67d45d0502c15b3cf5cc522a4cedb8d/cv2_enumerate_cameras-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:fb6312124060093227ff82f0c4a0ff4161b576e9bfcd6bbbafc63b5b0c4788c3", size = 217218, upload-time = "2026-09-26T14:09:35.636Z" }, + { url = "https://files.pythonhosted.org/packages/e6/be/933593c37b616a63df1491250b7b999bc0297399a69ac616c019a01d5e4b/cv2_enumerate_cameras-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d7282a37bd340bd63b65950b9fb8e6c2fc7dd5b9141f6cfbb77bf37507932bbe", size = 216375, upload-time = "2026-09-26T14:09:37.274Z" }, + { url = "https://files.pythonhosted.org/packages/41/4c/b2b96a9f88d0b406d36cf5d20af8137f1cb2c271cb13ec5b5e9df82714c0/cv2_enumerate_cameras-1.4.0-cp315-cp315t-win32.whl", hash = "sha256:c6bb01c203813ad3009f504a1bd914efb691174fecd9e99b42e21b8b9d522039", size = 217220, upload-time = "2026-09-26T14:09:38.851Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/bd22f6430faea2f47b11fd92c18a847b4d60cd707712cd913bd5df9b681a/cv2_enumerate_cameras-1.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:791e7f68c5d62c3c22861c53f87b1174f6aa663eeceef8a43fab17ca80fb099c", size = 216376, upload-time = "2026-09-26T14:09:40.31Z" }, + { url = "https://files.pythonhosted.org/packages/5f/99/e7ab30db5eb4f2f87be9f419eda101ba1cdba6192c5cfdc7ef60305866e7/cv2_enumerate_cameras-1.4.0-py3-none-any.whl", hash = "sha256:ef3db06a004ada2e82b62efaac69f37223f1505b64954bf0f838d21f4cf237cc", size = 13334, upload-time = "2026-09-26T14:09:41.693Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -296,6 +314,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/ef/318fd032781bbdd6fa92b4c545b00f71c7a7e0f12b1475177d048f2362e6/databind_json-4.5.5-py3-none-any.whl", hash = "sha256:b5d7934d2c407be16550bc97de905035b6a20a58b291cf781b9981d743db0a96", size = 1653, upload-time = "2026-05-22T17:23:49.584Z" }, ] +[[package]] +name = "dearpygui" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/eb/78ba8feb7fa01ae3b2eafe94783e5051ec5cf997463c4181840fc89b80c8/dearpygui-2.3.1-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:8c3db42d9496e1baed8117148718bb69cab671066435d9f312cafb90d0df530c", size = 2003069, upload-time = "2026-05-01T22:45:57.452Z" }, + { url = "https://files.pythonhosted.org/packages/3b/20/bab626e3e02645fdeba9b67dd1f6296e624a82d023fe2fa186fc4444c4dd/dearpygui-2.3.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:55ce713722583f1c874bfdb458cc3c0c09200c200419a31128534f7a86f9ccff", size = 2678634, upload-time = "2026-05-01T22:46:04.902Z" }, + { url = "https://files.pythonhosted.org/packages/d3/5c/e894ea047be360910320501b4283a2079bd84b1cc03e66bd6999862ee1cc/dearpygui-2.3.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:853e912c6d1d555711c002f0e1b7d3355b118fe56a42dda92b30e0dbd6528b76", size = 2502216, upload-time = "2026-05-01T22:46:15.506Z" }, + { url = "https://files.pythonhosted.org/packages/22/bb/86c6e8125c7d8b106624bf9c537abdd0f723ca94bb11c18bf25fb16487d8/dearpygui-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:36389e45dda45f91efb1d2d693b2c7198eb987db4799e6442862eba6e5b086bb", size = 1881760, upload-time = "2026-05-01T22:45:46.418Z" }, + { url = "https://files.pythonhosted.org/packages/6c/40/22c00a3b3e47c609ece39b0e1970b8e031ac613201fee065dd93c9cbcb6c/dearpygui-2.3.1-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:e1a9ffbb845600ab4a171daf2d7e7a1895f5b4433eed248d4cab42766b55fdff", size = 2002951, upload-time = "2026-05-01T22:45:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/31/7d/b2b26e75437a6173e44a89a816d5f1df76537ca2f4b3206a2a787ac9d479/dearpygui-2.3.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:686423b4f8a30aaf57f1044ec4e3b087beb9cc9b25be812cf92f4e7feff44fd6", size = 2678639, upload-time = "2026-05-01T22:46:06.246Z" }, + { url = "https://files.pythonhosted.org/packages/00/90/efaa3dbcdab267638b0b65551c88ea1e994235b74345c8d995b8a0a9d59e/dearpygui-2.3.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:0259a2db7b6c82459ac1402b0d42388a7cd180cd97d7dcb31a5210725c3171d3", size = 2501986, upload-time = "2026-05-01T22:46:17.044Z" }, + { url = "https://files.pythonhosted.org/packages/b4/db/8f15dfe06d104746591c4763a5bba31369b86d861aa916101da8f633bfc6/dearpygui-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:76ee38696a8f1f3805751b1588c98d9bb6567b033b91cb0f58111fb9fe907b5b", size = 1881816, upload-time = "2026-05-01T22:45:48.044Z" }, + { url = "https://files.pythonhosted.org/packages/20/f1/6f30c7eafa25755105d0c26049a95c89cc5c82b68971e1cc8ffea36b748a/dearpygui-2.3.1-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:6b1efd675a358176b3b76c039587a543d407d8fbb2cc3643a527e0ab7c0f5577", size = 2002717, upload-time = "2026-05-01T22:46:00.242Z" }, + { url = "https://files.pythonhosted.org/packages/cc/03/fa76c31087b6ebbc13948de6d9d3bc9ed9946824b13d9d1089356b54249c/dearpygui-2.3.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:364b5b6a0953999fec764d529d9bb0b2cc6ab6ed06188e0ded28237c1475ca98", size = 2678552, upload-time = "2026-05-01T22:46:07.502Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ef/657ec68a79664cd28d98d590eed85103c6ffc1e23e7d3c4e5aedb7ee887d/dearpygui-2.3.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:199dffebda245c9f59aa7c10c3b7f62963237c5f744150ad9be409b27a2661d3", size = 2501224, upload-time = "2026-05-01T22:46:18.8Z" }, + { url = "https://files.pythonhosted.org/packages/77/be/e67e5a04439f15e60cf596092a31b2a907272288954e47f5ced43c0b1678/dearpygui-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:26ed9197cdb30099c665bd1b23724e0dba60ad06b71344ecca7e52748c21874b", size = 1882080, upload-time = "2026-05-01T22:45:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/56a293ad392b8dafdd3eb1a0c4d367efbde99aa6930359685fe270984dd1/dearpygui-2.3.1-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:d2d29e72b031886893b32269e1478eff1fd651512e6c2bceda91059c3bc8ee0e", size = 2002694, upload-time = "2026-05-01T22:46:01.425Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e0/df598671f40a7fffb8390946115d9e9cf12d3f4a52f5b9381bddc5699aee/dearpygui-2.3.1-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:e3f64c8d4cae68a3b5be1b7531d08400ccbf1e4ff03c7acfc85954f98993f864", size = 2678564, upload-time = "2026-05-01T22:46:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d1/87cfb577d4a1615e809e2f6e0fd720bd4ad6cd25e365ef2fd08a6e09c86e/dearpygui-2.3.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:0d8b7a5a04cd4a6e25dbf162aa0405cf12143a1c82b33e371c20e5ba8d9349f3", size = 2503196, upload-time = "2026-05-01T22:46:20.276Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/05f74181a59353bdc927b6ec46333714f633c5755f3d4bcda34d6eb100a5/dearpygui-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:7946722c50f1e08866dfdb78eb33dd4e67dea1a3cfe4400b159e1c96861116dc", size = 1882082, upload-time = "2026-05-01T22:45:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/4a/08/68ce3ba941cf09bcf288f2603d7178c936d473172fd545e0665b885cb840/dearpygui-2.3.1-cp314-cp314-macosx_13_0_arm64.whl", hash = "sha256:e2cbabd8d383308ea3520360e100d1e95c9a957cd26247431cd4920b23c3b56e", size = 2003034, upload-time = "2026-05-01T22:46:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3f/ac003b6d636c9f5aae910a62d00b6c119403212cf2d7772d7c59ca8ef6dd/dearpygui-2.3.1-cp314-cp314-manylinux1_x86_64.whl", hash = "sha256:953442c7272e57f3686e393edc5cf6ce73cc2f47142739735dfabed403e33770", size = 2678545, upload-time = "2026-05-01T22:46:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6b/657a2f1e2f604f356c39ed5b870aaed5d07fdeac3139fae2a455473b64ca/dearpygui-2.3.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:43a561b5dc589944a3a2b469e5e68f1ab35ae38b3dcdf1f813ed9d6e024153f8", size = 2501699, upload-time = "2026-05-01T22:46:21.497Z" }, + { url = "https://files.pythonhosted.org/packages/8c/56/93b2310891589063e0226cb07fe05e08fbdb3f409f80f9edefb1cebea84e/dearpygui-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:272cc52c66bc5a4a332f13a3af5e603070d47ca7cc4976e6a8c247594e2b3dd0", size = 1943067, upload-time = "2026-05-01T22:45:52.255Z" }, +] + [[package]] name = "deprecated" version = "1.3.1" @@ -371,6 +416,8 @@ name = "emioapi" version = "0.3.0" source = { editable = "." } dependencies = [ + { name = "cv2-enumerate-cameras" }, + { name = "dearpygui" }, { name = "dynamixelmotorsapi" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, @@ -389,6 +436,8 @@ dev = [ [package.metadata] requires-dist = [ + { name = "cv2-enumerate-cameras", specifier = ">=1.4.0" }, + { name = "dearpygui", specifier = ">=2.3.1" }, { name = "dynamixelmotorsapi", git = "https://github.com/SofaComplianceRobotics/DynamixelMotorsAPI?rev=main" }, { name = "numpy" }, { name = "opencv-python", specifier = ">=4.11.0.86" }, @@ -1199,6 +1248,130 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/5a/ce0b056d9a95fd0c06a6cfa5972477d79353392d19230c748a7ba5a9df04/pydoc_markdown-4.8.2-py3-none-any.whl", hash = "sha256:203f74119e6bb2f9deba43d452422de7c8ec31955b61e0620fa4dd8c2611715f", size = 67830, upload-time = "2023-06-26T12:36:59.502Z" }, ] +[[package]] +name = "pyobjc-core" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/78/abc4ce5920305780aeb36b4067a86253378b36e29ba96673a3deb02eb03a/pyobjc_core-12.2.2.tar.gz", hash = "sha256:3906452339cd06a3bb07df103c2511d4cb0f7a22d8771c0b802eba15d9a642b6", size = 1067701, upload-time = "2026-08-11T19:43:39.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/1d/baf7197cee12f32a8eb9f8633093da1ec1ea702b0e1346bc1c7bfe022673/pyobjc_core-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:56c6c39f1de059fcbb174ebca5525505fc8feaa89be2a28c329bf09b6b25ee75", size = 6483051, upload-time = "2026-08-11T13:55:45.262Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8e/18284fec7913ef78b25a1c97f9689ebef98bc14038386191491516abeb25/pyobjc_core-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b9cdd686e32db8e451feb19f8a85bc4cd52c2893103881d04aca51e1f35371d1", size = 6481574, upload-time = "2026-08-11T14:13:27.153Z" }, + { url = "https://files.pythonhosted.org/packages/86/b2/bbf7f049880ab40d110e66f25122342a1f6c98d6fe3c59bb98985503c660/pyobjc_core-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:122e6ad302a2abf5d4d4adb0156db751600ddf2768441696cba17b31323085e7", size = 6427637, upload-time = "2026-08-11T14:51:36.038Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ed/a8bf040caf3704023d74086b7fb96cf4ed2e844e24bd94e5248ba214b700/pyobjc_core-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950bd2d9c74634398c4e3d24ef2f213d4e23d705083697464fa67afedc53c1ad", size = 6427113, upload-time = "2026-08-11T15:04:39.424Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5a/760f8b9e116edd43c57e33844dc17619158fbdd311250d4209910192d72d/pyobjc_core-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3772b406edb3ff78171530a17cda1c4a7817f87b87ded0d8715b3fa664df16db", size = 6666817, upload-time = "2026-08-11T19:30:17.01Z" }, + { url = "https://files.pythonhosted.org/packages/13/37/486d38a173b0b8dce973a3e13c74cf402ed1b8621586b5963bc9efd49a48/pyobjc_core-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2062e8ad30a310441cd022544a897553408bebeaa7820d5edba3c96fd7fd693b", size = 6421184, upload-time = "2026-08-11T19:30:21.081Z" }, + { url = "https://files.pythonhosted.org/packages/04/f1/d138fd9b9a66ea8db56a8138b77d3413b85da3defe13363a19f364f85529/pyobjc_core-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2c7ef3d2f865b4b3ebb14ec3556f7a3e8abb6d130c67275cd9daa08dbd6e4e4e", size = 6671824, upload-time = "2026-08-11T19:30:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/d5/85/577e2265cccf59daf48c460f0a8deeaf7dbe2991227a8859ab1eeab4945e/pyobjc_core-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:89acc6bc13aaa6e3f52b0ce652ede7e201edb6bf062741b246b0c5a44582f25f", size = 6477694, upload-time = "2026-08-11T19:30:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/bd9f830c64c6f334530831e75c01bfe0a770a3fbb00fddc70329223118b3/pyobjc_core-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59a77038ebe0ab1240f61c341e7fb67b8674f2b4cd41bc71a6472511a12b50f7", size = 6712233, upload-time = "2026-08-11T19:30:33.032Z" }, +] + +[[package]] +name = "pyobjc-framework-avfoundation" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudio", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/48/917494d86ea02372e922bc96ffb76a6161f847db21ae7885fe7e0c3d7755/pyobjc_framework_avfoundation-12.2.2.tar.gz", hash = "sha256:5b9f33bf382af9a84b2394c98e3603a8abf03f29b0d2036e29698806ea22851a", size = 411684, upload-time = "2026-08-11T19:43:51.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/0e/d8d75eef124963283756342ea6a6e5ef2c18329f00671c13cc8ff878fa93/pyobjc_framework_avfoundation-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b5feb1265d7a259f92e75c4bef791e047f2d4aa6b7faa3fd46077f135947adaf", size = 85548, upload-time = "2026-08-11T19:31:35.418Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/9d53f29d06f57de1398b56a0e535a3c9e85ca3ecb83607b83cafce318df6/pyobjc_framework_avfoundation-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6a1baf030b9c7997b99e38450e84ea7398c8ca9e762660ba9019c20837e733a0", size = 85543, upload-time = "2026-08-11T19:31:36.611Z" }, + { url = "https://files.pythonhosted.org/packages/33/8b/ea58df10fcf060b0a8a3fb56cbc0b06722b62cab6ef013b0ef346735761b/pyobjc_framework_avfoundation-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:248fd714f7a63535577cde3a5bd097f1e309011586b799c110fb9bab27e88f55", size = 85590, upload-time = "2026-08-11T19:31:37.654Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/ab3b86852704afd33de70f803c814af80a1d84b7dfafc51404d315d0921f/pyobjc_framework_avfoundation-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:73f34a9e7a5cc0ec6cc496ee10c133d14c4ddc768e17f09c684f95ac5b127408", size = 85625, upload-time = "2026-08-11T19:31:38.576Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c1/eda88125b7e4ae32dea2c831c5ab92705502355c80ce3d35e8eb8615d555/pyobjc_framework_avfoundation-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ca4976b18bee82142655b9dc1d6c0cd665627d9ef413694434e2ba5a187443f4", size = 86073, upload-time = "2026-08-11T19:31:39.533Z" }, + { url = "https://files.pythonhosted.org/packages/cf/61/a0969bfebcdddcaf1f8ccf721bf747e4e6ec803c742c6afd17faf0397f68/pyobjc_framework_avfoundation-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21a3c326455e5dce805e7440a3b7808c1e833e6d961279de88ab39cba70b821f", size = 85834, upload-time = "2026-08-11T19:31:40.455Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/14e479e8f6159d69d5fac942b2e0d0335529f43da0b9b0dac7a80662b46d/pyobjc_framework_avfoundation-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:45b915a35d7fddb6c29a7a43e8b221bd2f2fe73eb0b1c174e843cfa6cbba57ac", size = 86175, upload-time = "2026-08-11T19:31:41.338Z" }, + { url = "https://files.pythonhosted.org/packages/17/ec/ffb940584befa2d3a42e8169b1269ab492f77b40ed0183d37089f97626f6/pyobjc_framework_avfoundation-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:60199c8271f28dbf3f1b1f4fcaec3fbd7d6787571be95b0a291e70f1dbd13807", size = 85895, upload-time = "2026-08-11T19:31:42.161Z" }, + { url = "https://files.pythonhosted.org/packages/ea/64/7b789e97ce5675e43628166a2ff17bf39ef081e62a5725d51fbfc11c197a/pyobjc_framework_avfoundation-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9f9394180a7891085f8981ba2fcb2da292ee0608b46fa53d231d8a646a6a6bb8", size = 86241, upload-time = "2026-08-11T19:31:43.258Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/76/49c6da2c6a831020b4854ba20079d5a1030474bffc776b7b73c2eeff8c15/pyobjc_framework_cocoa-12.2.2.tar.gz", hash = "sha256:c96c0ef69a71afbbb0e6a7d594b455c5fe47d62e0db376ee7a2b4b828c16ace9", size = 3132831, upload-time = "2026-08-11T19:44:02.288Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/dd/aba439652cae293a736680ef5ed5cc29419adbbac1c6d4555b910741d516/pyobjc_framework_cocoa-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5a751c8033a3b51f7996f0327e0675eb44dcfdfe7920fae01e3d78b662723fff", size = 387296, upload-time = "2026-08-11T19:32:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/370f12143661dff66f2c68a735938afab6530aa3b153f6a7a6f12b5eabab/pyobjc_framework_cocoa-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:851dca4c16e70b405e5cd5a8c166cf7c445ae54a4cdd95ce9a523803172f32d1", size = 387300, upload-time = "2026-08-11T19:32:42.043Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2f/b67e73d8bc367e03fe7861cd9c49fff9dcfa6db83bc0630c0adcfb25b7fa/pyobjc_framework_cocoa-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e106f395531e67694376b0f1184612cbeea3ec8b9bf56b55ef41d026171d2a2d", size = 388117, upload-time = "2026-08-11T19:32:43.161Z" }, + { url = "https://files.pythonhosted.org/packages/db/e1/5d9b04ebb60042b9cb49adc2d33115e2f2c2e4ff7d548017bfaff8b7f536/pyobjc_framework_cocoa-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:600b1723184ca094931330e79355274949965460e23de38628d601b5a967baf9", size = 388181, upload-time = "2026-08-11T19:32:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/b4/25/2a343357d5fe09bbe9c0e294dc03450866a0d6c1792fad36b6bcc00174c0/pyobjc_framework_cocoa-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:875f2aad73963faa81a6b36ae674fd494a4658d6d999e1075e0e2aca3d2391df", size = 392275, upload-time = "2026-08-11T19:32:45.631Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1a/b99521999b9f54b89aad928ddff0faad507abfe33bc46599454bfa48a4b2/pyobjc_framework_cocoa-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:889d7bbd4ba2d4941078bfbbfb882138e51dbead27df006abfe0f2e0d49b5b2e", size = 388368, upload-time = "2026-08-11T19:32:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/6d/26/0c697dbc73dcc76bc0f68ea5aeed25bf7b05217df5102659e878501b2d5f/pyobjc_framework_cocoa-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:de69c5933750f3a4599ed962eccd92b6a71914c7e4318dacc7895738a8ae60d7", size = 392404, upload-time = "2026-08-11T19:32:47.918Z" }, + { url = "https://files.pythonhosted.org/packages/df/82/502f740fd8f4e9ef741c9d40ba67467ab2c8196f2c09dcba12936d28a4fd/pyobjc_framework_cocoa-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:0e8ace0d44a00d281281a723d17fcd05eea7544a38a6a512e1fd018ddb7aece2", size = 388585, upload-time = "2026-08-11T19:32:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3b/07ce3c0ab8d1e9e1bed74fea1bf1cce73527a365a7a23c755051d3be9865/pyobjc_framework_cocoa-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8fe5b2e79c9530f667b4e58a87a3a15ea62f86a5d19eec405517ecbd4f454868", size = 392693, upload-time = "2026-08-11T19:32:50.283Z" }, +] + +[[package]] +name = "pyobjc-framework-coreaudio" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/71/bfbf1dffe9f08f31b444685e9d194e25cc3506db9f7a9d6ffdb56cc4408e/pyobjc_framework_coreaudio-12.2.2.tar.gz", hash = "sha256:880214f7328d6502a0c60ee744fa1eabe77a0d0c9ae276009453b69474eacf38", size = 79573, upload-time = "2026-08-11T19:44:07.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/d3/6adf297241bb976ea85a087501915cccc758c2030fbb71612c611f2dbf23/pyobjc_framework_coreaudio-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:950e16f8c1cc21a76ca879d820b49795959fc6099eb8a41ccd1376951f42941e", size = 34986, upload-time = "2026-08-11T19:33:09.921Z" }, + { url = "https://files.pythonhosted.org/packages/10/75/556b8f887a8b68c3125aa64e8281c26cce42d5e7338b90ef5474d8a519b1/pyobjc_framework_coreaudio-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2217911fb21cd6d8002d0895f6353a15fd6ce2c4e191b3f4cb6e16ad7a428591", size = 34980, upload-time = "2026-08-11T19:33:10.982Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/bd18309c59eaaf694253ca932844743d6429023b99260684181367ba0564/pyobjc_framework_coreaudio-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7222c2b2317a6a350e08239b3b7ba7b4570b06ec8697b9bc53793bd6f85e3134", size = 35242, upload-time = "2026-08-11T19:33:11.787Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2a/d16c2f043c4e12274aa9c4444f9e00abc3fb2c5155a3b12b097eeffb6cdd/pyobjc_framework_coreaudio-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a19cc902a1eadb1b30d191d008e960768aaeeb7c027001820ef0cc01a4791422", size = 35271, upload-time = "2026-08-11T19:33:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/63/84/3c05b951ff16525bf32d6df1d4fe436df25ea93581ff257d4693205b4030/pyobjc_framework_coreaudio-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4cdc2be16ee481dea8ba1bad43c657bc02fcaeeb4a502d64e6bc28719b7cc103", size = 38026, upload-time = "2026-08-11T19:33:13.851Z" }, + { url = "https://files.pythonhosted.org/packages/7e/87/7bb04033a75abecc03642bcac7c23175e8c1d7a6c14be561a18378f23046/pyobjc_framework_coreaudio-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4e332b0acb976e84b3730d90f1bce8c92f1e52009a07d35ee7d3f8d494055d8a", size = 36520, upload-time = "2026-08-11T19:33:14.656Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/36436a40e253dd8005bed9d6ffb31e5be1fd4acccacdfd8e36b1af1b4542/pyobjc_framework_coreaudio-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:29b6d2b0d415246348717e501b424da5ad4cfadc8bb3c465c16851446e6ff590", size = 38131, upload-time = "2026-08-11T19:33:15.462Z" }, + { url = "https://files.pythonhosted.org/packages/23/14/424a6b90658b5ac597986b6b8dcb2ae2f9452840a28149c400f458f33d5a/pyobjc_framework_coreaudio-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:97ef7efc7f5c5d66ecb114050f76f029803a203b953103859bdfe1d4756de50d", size = 36552, upload-time = "2026-08-11T19:33:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/f276ed48473dd8d7f070517a5dbba40d689af3261127799b41ef17861464/pyobjc_framework_coreaudio-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:75bfc0de2b8f94521c9becb55d5dcf749b6ff479b6163cd1355b1f7c578166e4", size = 38162, upload-time = "2026-08-11T19:33:17.1Z" }, +] + +[[package]] +name = "pyobjc-framework-coremedia" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/12/0b3896ea04f0fbbe3f2cb37802cadce334f9aef204d911a81934013eba8c/pyobjc_framework_coremedia-12.2.2.tar.gz", hash = "sha256:fe9f972438674893e941e9db5b62f8ba99a33f94a7f1c2ce49b14416f154cd10", size = 98580, upload-time = "2026-08-11T19:44:11.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/7a/de17f374501561a30f50a9c194bfaf39607c717cad062057ba16b209e79e/pyobjc_framework_coremedia-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41142c919c557165df4a53ffe218e600ad0ccb7691bc9aed58a7839a88ba7c8b", size = 29520, upload-time = "2026-08-11T19:33:48.193Z" }, + { url = "https://files.pythonhosted.org/packages/38/18/5931478200f803db87be5cd6cb9af492d9ef2f2ac07cbfe019ecfa97db5a/pyobjc_framework_coremedia-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f8150428e6ae40d6b97714f6933b8c4322f3fb94de9bfcb206d00cb021f464", size = 29518, upload-time = "2026-08-11T19:33:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/cd/26/de907d6ead970916b39787e52bed679945328161f2abac533515b2ac8934/pyobjc_framework_coremedia-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:84f5deee8d366bf73069a0525509637aa578ccd0e6688923b09999a8bc3077a8", size = 29421, upload-time = "2026-08-11T19:33:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/36/65/6fe4c97d3442abda385f8798becd5bc82b73b4dfd979194e5850c5246dea/pyobjc_framework_coremedia-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c4430bda640c5427bf572f8b2a3281d8f4f16b962a499251cf553a0979d88a4e", size = 29431, upload-time = "2026-08-11T19:33:50.621Z" }, + { url = "https://files.pythonhosted.org/packages/5f/32/3c023f6b26fffb8c7d07cd6327e892bd546da7cfa8b3bbdaed113f5a59d3/pyobjc_framework_coremedia-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b80285a65465a5340f5806e9e293b46e22ac6f9c3132e79c8139a79b53b3f8c2", size = 29496, upload-time = "2026-08-11T19:33:51.396Z" }, + { url = "https://files.pythonhosted.org/packages/74/bb/0676dabebfbf9a89ea268ca26bd978a6c0e5dcf9b2baca86709bd8228708/pyobjc_framework_coremedia-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:edc9d03e5230123c92d4f0413c48449a7f08fa747b9f304b2cc396239acfefe3", size = 29465, upload-time = "2026-08-11T19:33:52.149Z" }, + { url = "https://files.pythonhosted.org/packages/17/33/1e2ab0438a546cc33ac18dcdb939c0a7cbbfcc070744f6451d08ccd80d6b/pyobjc_framework_coremedia-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:bb4c56edd411a5c0152a0a9eeb0138f9c173820041beff0cfaff291b9376bade", size = 29525, upload-time = "2026-08-11T19:33:52.943Z" }, + { url = "https://files.pythonhosted.org/packages/99/7f/1a5e92a337e924f4cf3781f61b30a0e9fba84eb6fbf2c377959bb842d1e8/pyobjc_framework_coremedia-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:49b53bbe1eea893f94bbf05f1f5220fcef3ccac7310a96d78434c0df6318dc79", size = 29503, upload-time = "2026-08-11T19:33:53.724Z" }, + { url = "https://files.pythonhosted.org/packages/db/f6/09e85dc8bffbafdcf74c53d8495db1411f9cc03b32a10593832f4fa81318/pyobjc_framework_coremedia-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:0bf4cd65f87bd073e74b3ede2381dc9355529e6f1b12184da22090f016e68bf7", size = 29560, upload-time = "2026-08-11T19:33:54.712Z" }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/b1/426a37c7ae37280b3ffca2571fb48f211946aee2f4ca31a603ed1943c4a7/pyobjc_framework_quartz-12.2.2.tar.gz", hash = "sha256:810f97b210cfd93704d240860286dfd6df09f9f1c52525fc5c2166723aea3f9e", size = 3218295, upload-time = "2026-08-11T19:45:15.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/94/2c9839958b0a2a040e22cca145040a8fc2612f099eda7524900d741f560d/pyobjc_framework_quartz-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d89a5f47c079b5c340d2b1cbb83eb6c4c92d4bb17cd4daf7d8c02c91a49f5399", size = 218007, upload-time = "2026-08-11T19:40:22.077Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e8/16d07170d4e1bd182a8e6084abdf283fac90979384b8f53107be9eb110e6/pyobjc_framework_quartz-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4b01e325b0cdc121e78730dde9756e971b23069bf141cd62efbcaac76d7b6dbb", size = 218008, upload-time = "2026-08-11T19:40:23.36Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e4/8be95d2ff850f82fb55b44c63333a00a920bf8a73642e7d9c2f3638a26d2/pyobjc_framework_quartz-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f668979d0c7320bf8f7ed6e030da578f93ab0f5dd619b295ec735cd8d5faa34", size = 219003, upload-time = "2026-08-11T19:40:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ae/b515852dbe491171f2f2e2eb7739588a5eb7f36720a739545337b8c0d706/pyobjc_framework_quartz-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0ec9751904ef975bf0789d760dc4fadcb400edc4ffe4a736eb54971968babe5c", size = 219404, upload-time = "2026-08-11T19:40:25.461Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5f/c7ee66f4483385396d91f65036c62f3dd20bccaa07354643ef17b259aa75/pyobjc_framework_quartz-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:63f6f0f3233dcf650aac1374781e78961b0b17b33e3351953bacf8bd0c430593", size = 224462, upload-time = "2026-08-11T19:40:26.647Z" }, + { url = "https://files.pythonhosted.org/packages/b1/33/230ae7777b0909fe2c24f28413c51c860faebf762d824b097a0e2fb48304/pyobjc_framework_quartz-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1f7f3d9010e38f03ea1fa266664c10ea349cd7492bd603b403584f49d713dbed", size = 219773, upload-time = "2026-08-11T19:40:27.64Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/7482fdd384521916e98a6164be220494b1f7794792b1d60fa3f5207d84a4/pyobjc_framework_quartz-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ebf8167ca2096cf3a05199decfa517be0df4c56048f49cf132bd6b1a6ab9c086", size = 224726, upload-time = "2026-08-11T19:40:28.718Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/b4b0ffc486b08492bfef4b33731d044b295f1696c8fb7d119c8367079139/pyobjc_framework_quartz-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cee63b891c2b6b7ccf98f233175411529f3e80286f58438793b3634af79858f1", size = 219827, upload-time = "2026-08-11T19:40:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/aa/eb/5fb627c2457046883c6fd12d25c44db40c12bcde4622dc0f30851108e106/pyobjc_framework_quartz-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8f58c589b5a76ba98f186b1f3b19fb1c8b730e82351f81fb62e6194f64a71622", size = 224767, upload-time = "2026-08-11T19:40:30.991Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2"