From 7ec0db3ddc8081ac03fe8e264657957f35d5b3f7 Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 7 Oct 2024 23:05:04 +0100 Subject: [PATCH] Bumped to V.13 Updated fcmega backend added CSV filename check when reading in maps Added details as to how to pipx with multiple python instances Added details as to how to install with regular pip Allowed marimapper to start without a backend or directory Ask the user whether to continue on movement Fixed feature where camera would return to regular brightness between scans ignoring the "too complex" flake8 requirement as yes I know I'll get to it --- .flake8 | 2 +- README.md | 5 ++- marimapper/backends/fcmega/fcmega.py | 14 +++------ marimapper/camera.py | 16 ++-------- marimapper/led_map_2d.py | 4 +++ marimapper/reconstructor.py | 25 +++++++++------ marimapper/scanner.py | 47 ++++++++++++++++++---------- marimapper/scripts/scanner_cli.py | 4 +-- marimapper/utils.py | 15 ++++++++- pyproject.toml | 2 +- 10 files changed, 80 insertions(+), 54 deletions(-) diff --git a/.flake8 b/.flake8 index 29a0d17..b8f0639 100644 --- a/.flake8 +++ b/.flake8 @@ -3,4 +3,4 @@ ignore = E402 exclude = marimapper/backends/fadecandy/opc.py, venv, marimapper/database.py, marimapper/read_write_model.py max-line-length=127 max-complexity = 10 -extend-ignore = W503, E203 \ No newline at end of file +extend-ignore = W503, E203, C901 \ No newline at end of file diff --git a/README.md b/README.md index aa0fddc..72a805d 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,15 @@ ## Step 0: Install ```shell +pip --version # Ensure that it is //not// python 3.12, see above pip install pipx pipx ensurepath pipx install "git+https://github.com/themariday/marimapper" ``` -This will install the Marimapper library along with all the scripts needed below. +If you have Python 3.12 installed, install 3.11 and add `--python /path/to/python3.11` to the above `pipx install` command + +pipx not working? You can also download this repo and run `pip install .` from inside it You can run the scripts anywhere by just typing them into a console, on windows append `.exe` to the script name. diff --git a/marimapper/backends/fcmega/fcmega.py b/marimapper/backends/fcmega/fcmega.py index fbbd7ca..90f2dcd 100644 --- a/marimapper/backends/fcmega/fcmega.py +++ b/marimapper/backends/fcmega/fcmega.py @@ -10,20 +10,16 @@ class FCMega: def __init__(self, port=None): + self.serial = None + if port is None: port = self._get_port() if port is None: - logging.error("Cannot find port") - return + raise RuntimeError("Cannot find FC Mega") self.serial = serial.Serial(port) - - if not self.update(): - logging.error("failed to communicate with FC MEga") - return - - def __del__(self): - self.serial.close() + if not self.serial.is_open: + raise RuntimeError("Cannot open port") def _get_port(self): for device in serial.tools.list_ports.comports(): diff --git a/marimapper/camera.py b/marimapper/camera.py index df2e90f..90010de 100644 --- a/marimapper/camera.py +++ b/marimapper/camera.py @@ -109,19 +109,9 @@ def set_exposure(self, exposure): if not self.device.set(cv2.CAP_PROP_EXPOSURE, exposure): logging.error(f"Failed to set exposure to {exposure}") - def set_exposure_and_wait( - self, exposure, max_frames_to_wait=20, min_brightness_change=2.0 - ): - - initial_mean = cv2.mean(self.read())[0] - - self.set_exposure(exposure) - - for _ in range(max_frames_to_wait): - mean = cv2.mean(self.read())[0] - ratio = mean / initial_mean - if ratio > min_brightness_change or 1 / ratio > min_brightness_change: - return + def eat(self, count=30): + for _ in range(count): + self.read() def read(self, color=False): ret_val, image = self.device.read() diff --git a/marimapper/led_map_2d.py b/marimapper/led_map_2d.py index 09a28e3..7ff5446 100644 --- a/marimapper/led_map_2d.py +++ b/marimapper/led_map_2d.py @@ -80,6 +80,10 @@ def get_all_2d_led_maps(directory): led_maps_2d = [] for filename in sorted(os.listdir(directory)): + + if not filename.endswith(".csv"): + continue + full_path = os.path.join(directory, filename) if not os.path.isfile(full_path): diff --git a/marimapper/reconstructor.py b/marimapper/reconstructor.py index 6870950..9cff0e8 100644 --- a/marimapper/reconstructor.py +++ b/marimapper/reconstructor.py @@ -5,7 +5,6 @@ from marimapper.camera import Camera, CameraSettings from marimapper.led_identifier import LedFinder -from marimapper import logging from marimapper.timeout_controller import TimeoutController @@ -21,9 +20,8 @@ def __init__( height=-1, camera=None, ): - self.settings_backup = None self.cam = Camera(device) if camera is None else camera - self.settings_backup = CameraSettings(self.cam) + self.settings_light = CameraSettings(self.cam) self.led_backend = led_backend self.dark_exposure = dark_exposure @@ -39,23 +37,29 @@ def __init__( self.cam.set_exposure_mode(0) self.cam.set_gain(0) + self.settings_dark = CameraSettings(self.cam) + self.live_feed = None self.live_feed_running = False + self.cam.eat() + + def __del__(self): + self.close() + def close(self): self.close_live_feed() cv2.destroyAllWindows() - if self.settings_backup is not None: - logging.debug("Reverting camera changes...") - self.settings_backup.apply(self.cam) - logging.debug("Camera changes reverted") + self.light() def light(self): - self.cam.set_exposure_and_wait(self.light_exposure) + self.settings_light.apply(self.cam) + self.cam.eat() def dark(self): - self.cam.set_exposure_and_wait(self.dark_exposure) + self.settings_dark.apply(self.cam) + self.cam.eat() def open_live_feed(self): cv2.destroyAllWindows() @@ -111,6 +115,9 @@ def find_led(self, debug=False): def enable_and_find_led(self, led_id, debug=False): + if self.led_backend is None: + return None + # First wait for no leds to be visible while self.find_led(debug) is not None: pass diff --git a/marimapper/scanner.py b/marimapper/scanner.py index 98128bf..bcb8f2a 100644 --- a/marimapper/scanner.py +++ b/marimapper/scanner.py @@ -17,10 +17,13 @@ class Scanner: def __init__(self, cli_args): - self.output_dir = cli_args.output_dir + self.output_dir = cli_args.dir self.led_backend = utils.get_backend(cli_args.backend, cli_args.server) - os.makedirs(cli_args.output_dir, exist_ok=True) - + if self.led_backend is not None: + self.led_id_range = range( + cli_args.start, min(cli_args.end, self.led_backend.get_led_count()) + ) + os.makedirs(self.output_dir, exist_ok=True) self.led_map_2d_queue = Queue() self.led_map_3d_queue = Queue() @@ -35,7 +38,7 @@ def __init__(self, cli_args): self.renderer3d = Renderer3D(led_map_3d_queue=self.led_map_3d_queue) self.sfm = SFM( - Path(cli_args.output_dir), + Path(self.output_dir), rescale=True, interpolate=True, event_on_update=self.renderer3d.reload_event, @@ -43,7 +46,7 @@ def __init__(self, cli_args): led_map_3d_queue=self.led_map_3d_queue, ) - self.led_maps_2d = get_all_2d_led_maps(Path(cli_args.output_dir)) + self.led_maps_2d = get_all_2d_led_maps(Path(self.output_dir)) self.sfm.add_led_maps_2d(self.led_maps_2d) @@ -75,8 +78,21 @@ def mainloop(self): if not start_scan: return + if self.led_backend is None: + logging.warn( + "Cannot start backend as no backend has been defined. Re-run marimapper with --backend " + ) + return + self.reconstructor.dark() + result = self.reconstructor.find_led(debug=True) + if result is not None: + logging.error( + f"All LEDs should be off, but the detector found one at {result.pos()}" + ) + continue + # The filename is made out of the date, then the resolution of the camera string_time = time.strftime("%Y%m%d-%H%M%S") @@ -88,18 +104,16 @@ def mainloop(self): visible_leds = [] - led_count = self.led_backend.get_led_count() - last_camera_motion_check_time = time.time() camera_motion_interval_sec = 5 capture_success = True for led_id in tqdm( - range(led_count), + self.led_id_range, unit="LEDs", desc=f"Capturing sequence to {filepath}", - total=led_count, + total=self.led_id_range.stop, smoothing=0, ): @@ -110,7 +124,7 @@ def mainloop(self): led_map_2d.add_detection(led_id, result) total_leds_found += 1 - is_last = led_id == led_count - 1 + is_last = led_id == self.led_id_range.stop - 1 camera_motion_check_overdue = ( time.time() - last_camera_motion_check_time ) > camera_motion_interval_sec @@ -122,16 +136,17 @@ def mainloop(self): last_camera_motion_check_time = time.time() if camera_motion > 1.0: - logging.error( - f"\nFailed to capture sequence as camera moved by {int(camera_motion)}%" - ) - capture_success = False - break + logging.warn(f"\nCamera moved by {int(camera_motion)}%") + if not get_user_confirmation("Continue? [y/n]: "): + capture_success = False + break if capture_success: led_map_2d.write_to_file(filepath) - logging.info(f"{total_leds_found}/{led_count} leds found") + logging.info(f"{total_leds_found}/{self.led_id_range.stop} leds found") self.led_maps_2d.append(led_map_2d) self.sfm.add_led_maps_2d(self.led_maps_2d) self.sfm.reload() + else: + logging.error("Capture failed") diff --git a/marimapper/scripts/scanner_cli.py b/marimapper/scripts/scanner_cli.py index baca04c..0c19d1e 100644 --- a/marimapper/scripts/scanner_cli.py +++ b/marimapper/scripts/scanner_cli.py @@ -19,9 +19,7 @@ def main(): add_backend_args(parser) parser.add_argument( - "output_dir", - type=str, - help="The output folder for your capture", + "--dir", type=str, help="The output folder for your capture", default="." ) args = parser.parse_args() diff --git a/marimapper/utils.py b/marimapper/utils.py index 13da19d..46522ce 100644 --- a/marimapper/utils.py +++ b/marimapper/utils.py @@ -45,7 +45,17 @@ def add_backend_args(parser): "--backend", type=str, help="The backend used for led communication, i.e. fadecandy, wled or my_backend.py", - required=True, + default="None", + ) + + parser.add_argument( + "--start", type=int, help="Index of the first led you want to scan", default=0 + ) + parser.add_argument( + "--end", + type=int, + help="Index of the last led you want to scan up to the backends limit", + default=10000, ) parser.add_argument("--server", type=str, help="Some backends require a server") @@ -125,6 +135,9 @@ def get_backend(backend_name, server=""): if os.path.isfile(backend_name) and backend_name.endswith(".py"): return load_custom_backend(backend_name, server) + if backend_name == "None": + return None + raise RuntimeError("Invalid backend name") diff --git a/pyproject.toml b/pyproject.toml index fbf872d..ed64339 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "marimapper" -version = "V1.2" +version = "V1.3" dependencies = [ "numpy<1.25.0,>=1.17.3",