Skip to content

Commit

Permalink
Bumped to V.13
Browse files Browse the repository at this point in the history
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
  • Loading branch information
TheMariday committed Oct 7, 2024
1 parent d96442a commit 7ec0db3
Show file tree
Hide file tree
Showing 10 changed files with 80 additions and 54 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -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
extend-ignore = W503, E203, C901
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 5 additions & 9 deletions marimapper/backends/fcmega/fcmega.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
16 changes: 3 additions & 13 deletions marimapper/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions marimapper/led_map_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
25 changes: 16 additions & 9 deletions marimapper/reconstructor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
47 changes: 31 additions & 16 deletions marimapper/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -35,15 +38,15 @@ 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,
led_map_2d_queue=self.led_map_2d_queue,
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)

Expand Down Expand Up @@ -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 <backend name>"
)
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")

Expand All @@ -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,
):

Expand All @@ -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
Expand All @@ -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")
4 changes: 1 addition & 3 deletions marimapper/scripts/scanner_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
15 changes: 14 additions & 1 deletion marimapper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 7ec0db3

Please sign in to comment.