diff --git a/.gitignore b/.gitignore index 2a054fe..6517bf9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Config file +config.py + .device data data-* diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..0d5b1ea --- /dev/null +++ b/Readme.md @@ -0,0 +1,83 @@ +# NenuScanner + + +## GPIO + +### Cablage des LEDs existantes de la salle noire + +| GPIO | ID LED salle noire | +| GPIO 17 | LED 2 | +| GPIO 18 | LED 3 | +| GPIO 27 | LED 5 | +| GPIO 23 | LED 6 | +| GPIO 23 | LED 6 | + + + +## Appareil photo + + +### Réglages EOS 7D + +Prise de photo: +``` + gphoto2 --capture-image-and-download +``` + +#### Format image + +Télécharger RAW et JPEG + +``` + gphoto2 --set-config imageformat=10 + #Choice: 10 RAW + Small Fine JPEG +``` + + + +Liste des formats: +``` +gphoto2 --get-config imageformat +``` + +#### Capture target + +Stock sur la mémoire interne ou SD de l'apparail + +Sur la mémoire innterne, au download c'est toujours le mêm fichier (écrasement) + +``` +gphoto2 --get-config capturetarget 0 ↵ + +Label: Capture Target +Readonly: 0 +Type: RADIO +Current: Memory card +Choice: 0 Internal RAM +Choice: 1 Memory card +``` +En prod choix 0 : on ne conserve pas les photos sur l'appareil +Et test choix 1 : on conserve sur l'appareil + +Exemple choix 1: +``` +gphoto2 --set-config capturetarget=1 +``` + + +# ISO + +ISO 100 + +``` +gphoto2 --set-config iso=1 +``` + +### réglages objectif + +| Ouverture | aperture | gphoto2 --set-config aperture=14 | Ouverture f/10 | +| temps de pose | shutterspeed | gphoto2 --set-config shutterspeed=35 | Vitesse 1/100 s | +| Picture Style | picturestyle | gphoto2 --set-config picturestyle=0 | Standard | +| Exposition | aeb | gphoto2 --set-config aeb=0 | 0 | + + diff --git a/__init__.py b/__init__.py index 175437f..151441d 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ from flask import Flask, send_from_directory, session import os -from . import db, config, routes, utils +from . import db, config, routes, utils, leds app = Flask(__name__) @@ -25,7 +25,7 @@ def inject(): conn = db.get() return { 'calibration': utils.get_calibration(conn), - 'leds': config.LEDS_UUIDS, + 'leds': leds.get().leds, 'CalibrationState': db.CalibrationState, } diff --git a/camera.py b/camera.py new file mode 100644 index 0000000..f95434a --- /dev/null +++ b/camera.py @@ -0,0 +1,71 @@ +import gphoto2 as gp +import shutil +from . import leds, config + + +class Camera: + def capture(self, output_path: str) -> bool: + return False + + +class RealCamera(Camera): + def __init__(self): + self.inner = gp.Camera() + + def __enter__(self): + self.inner.init() + return self + + def __exit__(self, *args): + self.inner.exit() + + def capture(self, output_path: str) -> bool: + try: + file_path = self.inner.capture(gp.GP_CAPTURE_IMAGE) + preview = self.inner.file_get(file_path.folder, file_path.name[:-3] + '.JPG', gp.GP_FILE_TYPE_NORMAL) + raw = self.inner.file_get(file_path.folder, file_path.name, gp.GP_FILE_TYPE_RAW) + + preview.sve(output_path + '.jpg') + raw.save(output_path + '.cr2') + return True + + except Exception as e: + print('An error occured when capturing photo', e) + return False + + +class DummyCamera(Camera): + def __init__(self, leds: leds.DummyLeds): + self.leds = leds + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def capture(self, output_path: str) -> bool: + # Find which leds are turned on + found = None + all_on = False + for led in self.leds.leds: + if led.is_on: + if found is None: + found = led + else: + all_on = True + + if all_on: + shutil.copyfile('data-keep/small/all_on.jpg', output_path + '.jpg') + elif found is not None: + shutil.copyfile('data-keep/small/' + str(found) + '.jpg', output_path + '.jpg') + else: + print('ALL_OFF') + shutil.copyfile('data-keep/small/all_off.jpg', output_path + '.jpg') + + +camera = DummyCamera(leds.get()) if config.CAMERA == "dummy" else RealCamera() + + +def get(): + return camera diff --git a/config.darkroom.py b/config.darkroom.py new file mode 100644 index 0000000..dae0118 --- /dev/null +++ b/config.darkroom.py @@ -0,0 +1,12 @@ +from os.path import join + +DATA_DIR = 'data' +BACKUPS_DIR = 'data-backups' +CALIBRATION_DIR = join(DATA_DIR, 'calibrations') +OBJECT_DIR = join(DATA_DIR, 'objects') +DATABASE_PATH = join(DATA_DIR, 'db.sqlite') + +AUTO_USE_LAST_CALIBRATION = True +GPIO_CHIP = 'gpiochip0' +LEDS_UUIDS = [17, 18, 22, 23, 24, 27] +CAMERA = 'real' diff --git a/config.local.py b/config.local.py new file mode 100644 index 0000000..5442bf4 --- /dev/null +++ b/config.local.py @@ -0,0 +1,12 @@ +from os.path import join + +DATA_DIR = 'data' +BACKUPS_DIR = 'data-backups' +CALIBRATION_DIR = join(DATA_DIR, 'calibrations') +OBJECT_DIR = join(DATA_DIR, 'objects') +DATABASE_PATH = join(DATA_DIR, 'db.sqlite') + +AUTO_USE_LAST_CALIBRATION = False +GPIO_CHIP = None +LEDS_UUIDS = [17, 18, 22, 23, 24, 27] +CAMERA = 'dummy' diff --git a/config.py b/config.py deleted file mode 100644 index 3947dac..0000000 --- a/config.py +++ /dev/null @@ -1,24 +0,0 @@ -from os.path import join - -DATA_DIR = 'data' -BACKUPS_DIR = 'data-backups' -CALIBRATION_DIR = join(DATA_DIR, 'calibrations') -OBJECT_DIR = join(DATA_DIR, 'objects') -DATABASE_PATH = join(DATA_DIR, 'db.sqlite') - -AUTO_USE_LAST_CALIBRATION = False - -LEDS_UUIDS = [ - 'ac59350e-3787-46d2-88fa-743c1d34fe86', - '83ab3133-d4de-4a42-99a7-8f8a3f3732ba', - '577d6e72-f518-4d65-af28-f8faf1ca3f5d', - 'ec49a20c-bddd-4614-b828-fa45e01bfb19', - '5c249cce-d2b6-4b56-96c8-5caa43d8f040', - '22d783fb-ae55-4581-a3c6-e010d9d5e9de', - '12cb6a32-04a6-433b-8146-b753b8f1286d', - '461255a3-259a-4291-adc3-2fb736231a04', - '3896662f-9826-4445-ad70-86c2e6c143e7', - 'f87698ec-3cba-42fe-9a61-5ac9f77d767a', - '4c77a655-4b68-4557-a696-29345c6676a1', - 'b1cfe287-aa3b-445e-bcdc-75ae375efe43', -] diff --git a/leds.py b/leds.py new file mode 100644 index 0000000..f5eb9b0 --- /dev/null +++ b/leds.py @@ -0,0 +1,119 @@ +import gpiod + +from . import config + + +class Leds: + def __enter__(self): + pass + + def __exit__(self, *args): + pass + + def on(self): + pass + + def off(self): + pass + + +class GpioLed: + def __init__(self, gpio_pin: int): + self.gpio_pin = gpio_pin + self.led = None + + def enter(self, chip: gpiod.Chip): + self.led = chip.get_line(self.gpio_pin) + self.led.request(consumer=str(self), type=gpiod.LINE_REQ_DIR_OUT) + self.off() + + def exit(self): + self.off() + self.led.release() + self.led = None + + def on(self): + self.led.set_value(1) + + def off(self): + self.led.set_value(0) + + def __str__(self): + return f'LED{self.gpio_pin:02}' + + +class GpioLeds(Leds): + def __init__(self, chip: str, gpio_pins: list[int]): + self.chip = gpiod.Chip(chip) + self.leds = [] + for pin in gpio_pins: + self.leds.append(GpioLed(pin)) + + def __enter__(self): + for led in self.leds: + led.enter(self.chip) + return self + + def __exit__(self, *args): + for led in self.leds: + led.exit() + + def off(self): + for led in self.leds: + led.off() + + def on(self): + for led in self.leds: + led.on() + + +class DummyLed: + def __init__(self, gpio_pin: int): + self.gpio_pin = gpio_pin + self.is_on = False + + def enter(self): + pass + + def exit(self): + pass + + def on(self): + self.is_on = True + + def off(self): + self.is_on = False + + def __str__(self): + return f'LED{self.gpio_pin:02}' + + +class DummyLeds(Leds): + def __init__(self, gpio_pins: list[int]): + self.leds = [] + for pin in gpio_pins: + self.leds.append(DummyLed(pin)) + + def __enter__(self): + for led in self.leds: + led.enter() + return self + + def __exit__(self, *args): + for led in self.leds: + led.exit() + + def off(self): + for led in self.leds: + led.off() + + def on(self): + for led in self.leds: + led.on() + + +_leds = GpioLeds(config.GPIO_CHIP, config.LEDS_UUIDS) if config.GPIO_CHIP is not None else DummyLeds(config.LEDS_UUIDS) + + +def get() -> Leds: + return _leds diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eb2089e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Flask +pillow +gphoto2 +scipy +gpiod diff --git a/routes/acquisition.py b/routes/acquisition.py index fa26dd7..8896f0c 100644 --- a/routes/acquisition.py +++ b/routes/acquisition.py @@ -26,7 +26,7 @@ def scan_existing(id: int): conn = db.get() calibrated = session.get('calibration_id', None) is not None acquisition = db.Acquisition.get_from_id(id, conn) - object = acquisition.object(conn) + object = acquisition.object(conn) if acquisition is not None else None return render_template('scan.html', object=object, acquisition=acquisition, calibrated=calibrated) @@ -48,7 +48,7 @@ def run(object_id: int): def generate(): yield str(acquisition.id) - length = len(config.LEDS_UUIDS) + length = len(config.LEDS_UUIDS) + 2 # with all_on and all_off for index, led_uuid in enumerate(scanner.scan(join(config.OBJECT_DIR, str(object.id), str(acquisition.id)))): yield f"{led_uuid},{(index+1)/length}\n" @@ -74,7 +74,7 @@ def rescan(acquisition_id: int): object = acquisition.object(conn) def generate(): - length = len(config.LEDS_UUIDS) + length = len(config.LEDS_UUIDS) + 2 # with all_on and all_off for index, led_uuid in enumerate(scanner.scan(join(config.OBJECT_DIR, str(object.id), str(acquisition.id)))): yield f"{led_uuid},{(index+1)/length}\n" @@ -87,7 +87,7 @@ def validate(acquisition_id: int): acquisition = db.Acquisition.get_from_id(acquisition_id, conn) if acquisition is None: - raise f"Aucune acquisition d'id {acquisition_id}" + raise Exception(f"Aucune acquisition d'id {acquisition_id}") object = acquisition.object(conn) diff --git a/routes/calibration.py b/routes/calibration.py index e976a46..0f58911 100644 --- a/routes/calibration.py +++ b/routes/calibration.py @@ -57,7 +57,7 @@ def scan(): def generate(): length = len(config.LEDS_UUIDS) - for index, led_uuid in enumerate(scanner.scan(join(config.CALIBRATION_DIR, calibration_id))): + for index, led_uuid in enumerate(scanner.scan(join(config.CALIBRATION_DIR, calibration_id), False)): yield f"{led_uuid},{(index+1)/length}\n" with conn: @@ -125,4 +125,4 @@ def use_last(): conn = db.get() calib = db.Calibration.get_last(conn) session['calibration_id'] = calib.id - return redirect('/calibrate') + return redirect('/calibration/calibrate') diff --git a/scanner.py b/scanner.py index 3cbf673..f70bc32 100644 --- a/scanner.py +++ b/scanner.py @@ -1,47 +1,51 @@ -import cv2 import os from os.path import join -import shutil import time -from . import config +from . import leds, camera # Delay between to captures DELAY = 0.5 -def capture(output_path: str) -> bool: - try: - with open('.device', 'r') as f: - device_id = int(f.read().rstrip()) - except: - device_id = 0 +def delay_capture(cam, output_path): + # Measure the time it takes to capture + start = time.time() + output = cam.capture(output_path) + delta = time.time() - start - cam = cv2.VideoCapture(device_id) - s, img = cam.read() - if s: - cv2.imwrite(output_path, img) - cam.release() - return s + # Wait for at least one second between each capture + if delta < DELAY: + time.sleep(DELAY - delta) + + return output -def scan(output_dir: str): +def scan(output_dir: str, on_and_off: bool = True): os.makedirs(output_dir, exist_ok=True) - for led in config.LEDS_UUIDS: - print(f'Turn on {led}') - img = join(output_dir, led + '.jpg') - # Measure the time it takes to capture - start = time.time() + with leds.get() as gpio_leds: + for count, led in enumerate(gpio_leds.leds): + with camera.get() as cam: + print(f'Turn on {led}') + img = join(output_dir, f'{led}') - # capture(img) - # For debug purposes - shutil.copyfile(join('data-keep/small', led + '.jpg'), img) + led.on() + delay_capture(cam, img) + led.off() - delta = time.time() - start + print(f'Turn off {led}') + yield str(led) - # Wait for at least one second between each capture - if delta < DELAY: - time.sleep(DELAY - delta) + # capture with all leds ON OFF + if on_and_off: + with camera.get() as cam: + gpio_leds.on() + img = join(output_dir, 'all_on') + delay_capture(cam, img) + yield 'all_on' - print(f'Turn off {led}') - yield led + with camera.get() as cam: + gpio_leds.off() + img = join(output_dir, 'all_off') + delay_capture(cam, img) + yield 'all_off' diff --git a/templates/index.html b/templates/index.html index 8422c06..43d48fb 100644 --- a/templates/index.html +++ b/templates/index.html @@ -18,7 +18,7 @@
{% for project in projects %} - +
{{ project.name }}
@@ -35,7 +35,7 @@
@@ -70,7 +70,7 @@
Voulez-vous vraiment supprimer l'objet ?
-
Supprimer cet objet @@ -83,23 +83,25 @@ {% endblock content %} -{% block extrajs %}{% if calibration.state < CalibrationState.IsValidated %} +{% block extrajs %} -{% endif %}{% endblock %} +{% endblock %} diff --git a/templates/scan.html b/templates/scan.html index df23253..27d70ea 100644 --- a/templates/scan.html +++ b/templates/scan.html @@ -50,7 +50,7 @@
Voulez-vous vraiment supprimer l'acquisition ?
-
Supprimer cette acquisition @@ -102,6 +102,22 @@ cell.appendChild(img); grid.appendChild(cell); {% endfor %} + + cell = document.createElement('div'); + cell.classList.add('cell'); + img = document.createElement('img'); + img.classList.add('is-loading'); + img.src = '/data/objects/{{ object.id }}/' + acquisitionId + '/all_on.jpg?v=0'; + cell.appendChild(img); + grid.appendChild(cell); + + cell = document.createElement('div'); + cell.classList.add('cell'); + img = document.createElement('img'); + img.classList.add('is-loading'); + img.src = '/data/objects/{{ object.id }}/' + acquisitionId + '/all_off.jpg?v=0'; + cell.appendChild(img); + grid.appendChild(cell); } scanButton.addEventListener('click', async () => {