La caméra HQ pour Raspberry Pi offre une résolution de 12,3MP ainsi qu’un adaptateur pour monture C qui permet d’utiliser des objectifs professionnels dédiés à l’analyse d’image. Pour ce projet de microscope, nous allons utiliser des éléments d’une CNC 3018 pour construire un statif motorisé. La caméra est installée sur un axe vertical imprimé en 3D. L’axe vertical est motorisé par un moteur Nema 17 ptiloté depuis un contrôleur A4988 en Python à l’aide de la librairie RpiMotorLib de Gavin Lyons. En ajoutant des tubes d’extension, il est possible d’obtenir un grossissement plus important pour aller jusqu’au microscope d’appoint.
Avant de débuter, vous pouvez commencer par lire cet article qui explique comment piloter un moteur Nema 17 à l’aide de la librairie Python RpiMotorLib.
Pour en savoir plus sur la caméra HQ, je vous conseille de lire cette excellente et très complète série d’articles réalisée par notre ami François Mocq.
Sommaire
- 1 Composants utilisés pour le support (statif)
- 2 Caméra HQ, objectif, tube d’extension
- 3 Comment choisir la longueur du tube d’extension ?
- 4 Comment calculer (estimer) le grossissement optique
- 5 Circuit électronique
- 6 Pièces à imprimer en 3D
- 7 Assemblage du statif
- 8 Installer les librairies Python
- 9 Activer la caméra, les bus SPI et I2C
- 10 Code Python du projet. Flux vidéo de la caméra HQ diffusé sur une interface HTML de commande
- 11 Explication du code
- 12 L’interface HTML du microscope
- 13 Exemples de grossissement obtenu avec différent éclairage
- 14 Observation d’une LED RGB
- 15 Essai d’observation d’épiderme d’oignon
- 16 Afficher l’interface en plein écran, mode kiosk
Composants utilisés pour le support (statif)
Voici les composants recyclés d’une CNC 3018 utilisés pour le statif :
- x2 profilés aluminium 2020 (section carrée 20x20mm) de longueur 220mm environ
- x2 profilés aluminium 2020 (section carrée 20x20mm) de longueur 350mm environ
- x4 équerres 30mm
- Ecrous en T pour profilé aluminium 2020
Vous pouvez également imprimer en 3D vos profilés 2020 à l’aide de ce projet disponible sur Thingiverse.
Et les éléments pour l’axe vertical motorisé
- x2 tiges ø10mm de longueur 400mm mini
- x2 guidages linéaires ø10mm
- x4 supports pour tige ø10mm
- x1 platine moteur Nema
- x1 vis T8 de longueur 350mm minimum
- x1 élément de transmission direct à vis
- x1 raccord flexible ø8mm
Vous trouverez également tous les composants mécaniques nécessaires dans cet article
Caméra HQ, objectif, tube d’extension
- Caméra HQ 12.3MP v1.0 2018 avec support pour objectif à monture C
- Objectif 50mm à monture C
- Anneau lumineux Neopixel 16 LED RGB
- Kit de tubes d’extension / bague allonge
Vous trouverez tous les composants nécessaires dans cet article
Comment choisir la longueur du tube d’extension ?
Le statif présenté dans ce projet est équipé d’un objectif 50mm avec un tube allonge de 40mm ce qui permet un mode macro satisfaisant pour l’électronique.
La bague allonge (ou tube d’extension) vient se visser entre la caméra HQ et l’objectif. Cela permet de modifier la distance focale et ainsi d’obtenir un grossissement supérieur. C’est une solution économique pour obtenir un mode macro sans investir dans un objectif dédié.
Objectif Pentax/Cosmicar C5028-M installé sur la Caméra HQ du Raspberry Pi via un via tube d’extension de 50mm (40+10).
Vous pouvez également imprimer vos bagues d’extension en utilisant ces fichiers STL. Le pas de vis étant très fin, imprimez avec une résolution fine de 0,1mm idéalement.
Attention toutefois, car plus la bague allonge est grande, plus la profondeur de champ est courte. Il ne sera pas possible de faire la netteté sur l’ensemble de l’image lorsqu’il y a du relief. C’est à dire qu’il sera impossible de voir nettement le circuit et les composants électroniques.
Pour estimer la distance de mise au point vous pouvez lire cet article publié par Nikon Passion.
Voici quelques grandissements et distances de mise au point pour un objectif de 50mm en fonction de la bague allonge employée. La distance est calculée lorsque la mise au point est faite à l’infini.
Bague allonge (mm) | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | 110 | 120 |
Grandissement | 0,20 | 0,40 | 0,60 | 0,80 | 1,00 | 1,20 | 1,40 | 1,60 | 1,80 | 2,00 | 2,20 | 2,40 |
Distance de l’objet (mm)* | 300 | 175 | 133 | 113 | 100 | 92 | 86 | 81 | 78 | 75 | 73 | 71 |
(*) distance théorique approximative
Comment calculer (estimer) le grossissement optique
Il est assez facile d’estimer le grossissement optique à partir d’une mesure connue par rapport à ce qu’on obtient sur l’écran.
Ici, par exemple, on peut lire environ 9 graduations d’un pied à coulisse sur l’écran se qui représente 9mm. Sur mon écran, je mesure environ 165mm, ce qui nous donne un grossissement de 165 / 9 ∼ x18.
Circuit électronique
Les composants récupérés sur une CNC 3018
Voici le repérage des broches du circuit
Pilote moteur pas à pas A4988 |
|
Alimentation 12V | Broche A4988 |
+12V de la batterie ou de l’alimentation secteur | VMOT |
GND de la batterie ou de l’alimentation secteur | GND |
GPIO du Raspberry Pi (toute version) | |
5V du Raspberry Pi | VDD |
GND du Raspberry Pi | GND |
GPIO 21 | STP |
GPIO 20 | DIR |
Vers le moteur pas à pas
Généralement le moteur est livré avec un câble muni d’un connecteur au pas de 2,54mm. Si le déplacement est inversé, inverser le connecteur ou modifiez le code Python |
1A, 1B, 2A, 2B |
GPIO 14 | MS1 |
GPIO 15 | MS2 |
GPIO 18 | MS3 |
Connecter les broches RESET et SLEEP ensembles | |
Anneau lumineux Neopixel 16 LED RGB | |
GND du Raspberry Pi connectée au GND de l’alimentation externe | GND |
+5V alimentation externe | VIN |
GPIO 12 | In |
Remarques
La librairie Adafruit Neopixel ne prend en charge que les broches 10, 12, 18 et 21 du Raspberry Pi. Pour pouvoir utiliser la broche 10, il faudra activer le bus SPI depuis les réglages système de Raspberry Pi OS.
Toutes les masses (GND) doivent être connectées ensembles.
Pièces à imprimer en 3D
Voici les pièces à imprimer en 3D. Les fichiers STL ainsi que les modèles Fusion 360 sont disponibles sur Thingiverse, Cults3D et GitHub.
Support pour caméra HQ v1.0
Le support est conçu pour les éléments d’une CNC 3018 (guidage à bille, transmission par vis) |
|
Support pour Raspberry Pi à fixer sur la base du statif | |
Support anneau lumineux (recommandé) compatible avec n’importe quel objectif à monture C | |
Bague allonge monture C.
Le pas de vis étant très fin, imprimez avec la résolution la plus fine (0,1mm en général) qu’offre votre imprimante. Fichier STL à télécharger sur Thingiverse |
Assemblage du statif
Voici quelques photos qui montrent l’assemblage des profilés aluminium 2020.
Le support pour le Raspberry Pi (tout modèle) vient se fixer à l’aide l’un écrou en T à droite ou à gauche de l’axe vertical.
La caméra HQ est installée sur le support imprimé en 3D. La longueur du ruban livré en standard est suffisant si le Raspberry Pi est installé à la vertical sur le support proposé.
Objectif 50mm installé sur le support imprimé en 3D |
Anneau de LED Neopixel installé dans le support imprimé en 3D puis inséré sur l’objectif. |
Installer les librairies Python
Le projets nécessite l’installation des librairies Python suivantes
- RpiMotorLib
- PiCamera
- Adafruit NeoPixel
Pour vérifier que la version 3 de Python est bien installée (ce qui est toujours le cas sur Raspberry Pi OS normalement), exécutez la commande suivante dans un Terminal.
Si Python3 est correctement installé, vous devez obtenir le numéro de version en réponse.
python3 --version
Python 3.7.3
Pour installer les librairies Python, on aura besoin du script pip3. Pour vérifier qu’il est bien installer, exécuter cette commande qui doit vous renvoyer le chemin vers le script
pip3 --version
Si pip3 n’est pas installé, exécuter
sudo apt install python3-pip
Maintenant, on peut installer les librairies
sudo pip3 install rpi_ws281x adafruit-circuitpython-neopixel
sudo python3 -m pip install --force-reinstall adafruit-blinka
sudo pip3 install rpimotorlib
sudo apt install python3-picamera
Activer la caméra, les bus SPI et I2C
L’accès au GPIO et à certaines broches de ce dernier nécessitent un activation. Il faudra également activer l’accès au port CSI avant de pouvoir utiliser la caméra.
Depuis le menu Préférences, ouvrir la Configuration
Activer I2C, SPI, caméra
Code Python du projet. Flux vidéo de la caméra HQ diffusé sur une interface HTML de commande
Le code source qui permet de récupérer le flux vidéo de la caméra est directement tiré de l’exemple Web Streaming disponible dans la documentation en ligne de la librairie PiCamera.
Il aurait été beaucoup plus facile de développer l’interface avec Flask (ce que j’ai essayé), mais le flux vidéo obtenu est beaucoup moins fluide 😥 En attendant de trouver une solution technique plus efficace, nous allons utiliser les fonctions HTTP standards de Python.
L’interface utilise la feuille de style de Bootstrap 4 ce qui permet d’obtenir un rendu professionnel et responsive quelque soit la taille de l’écran y compris sur un smartphone.
L’interface HTML est accessible sur le réseau local. Le Raspberry Pi offre suffisamment de puissance pour que plusieurs utilisateurs puissent visualiser le flux vidéo simultanément. Pratique pour une salle de classe.
Plusieurs paramètres peuvent être personnalisées avant de lancer le script
- GPIO_pins broches MS1, MS2 et MS3 pour communiquer avec le contrôleur A4988
- direction broche pour piloter la direction du moteur Nema 17
- step broche pour piloter l’avance du moteur Nema 17
- step_per_mm calibration du moteur Nema. Par défaut 72 pas par millimètre
- distance 72 pas à parcourir par défaut, soit 1mm.
- LED_COUNT nombre de LED de l’anneau lumineux Neopixel
- LED_PIN broche pour piloter le contrôleur WS2812B de l’anneau lumineux Neopixel. Attention, la broche 10 ne fonctionne pas (sur mon Raspberry Pi 3). C’est la seule broche PWM du Raspberry Pi disponible, les autres (D18 et D21) sont utilisées pour piloter le contrôleur A4988
- LED_BRIGHTNESS niveau de luminosité. Contrairement à la version Arduino, il n’existe aucune fonction pour modifier le niveau de luminosité après initialisation de la librairie Neopixel Adafruit pour le langage Python.
- LED_ORDER ordre des LED. Par défaut RGB. Modifier si la couleur obtenue n’est pas conforme à ce qui est demandé depuis l’interface HTML.
Code source
# Source code based on the Web streaming example from the official PiCamera package # http://picamera.readthedocs.io/en/latest/recipes2.html#web-streaming import io, picamera, logging, socketserver, os from statistics import mean from threading import Condition, Thread from http import server import RPi.GPIO as GPIO from RpiMotorLib import RpiMotorLib import board, time, neopixel #define GPIO pins GPIO_pins = (14, 15, 18) # Microstep Resolution MS1-MS3 -> GPIO Pin direction= 20 # Direction Pin step = 21 # Step Pin step_per_mm = 72 # Step by millimeter | stepper per millimeter distance = 72 # By default move 1mm => 72 steps per mm stepper = "1mm" debug = False # LED strip configuration LED_COUNT = 16 # Number of LED ring. | Nombre de LED LED_PIN = board.D12 # GPIO pin. Don't use D10 | Ne fonctionne pas sur la broche D10 LED_BRIGHTNESS = 1 # LED brightness, from 0 to 1 | Niveau de luminosité, compris entre 0 à 1 LED_ORDER = neopixel.GRB # Order of LED colours. May also be RGB, GRBW, or RGBW | Type de LED ring_status = False # LED Ring status ring_status_label = "Off" # LED Ring status for user | Etat de l'anneau de LED affiché sur la page Web color = "white" # Default color | Couleur d'éclairage par défaut (blanc) camera_rotation = 270 # Image rotation in degrees (0,90,180,270) | rotation de l'image en degrées (0,90,180,270) # Create instance for the Stepper Motor mymotortest = RpiMotorLib.A4988Nema(direction, step, GPIO_pins, "A4988") print("A4988 initialized") # Neopixel Ring object # auto_write must be set to False to change color | l'option auto_write doit être False pour pouvoir changer de couleur ring = neopixel.NeoPixel(board.D12, LED_COUNT, brightness = LED_BRIGHTNESS, auto_write=False, pixel_order = LED_ORDER) ring.fill((0,0,0)) ring.show() class StreamingOutput(object): def __init__(self): self.frame = None self.buffer = io.BytesIO() self.condition = Condition() def write(self, buf): if buf.startswith(b'\xff\xd8'): # New frame, copy the existing buffer's content and notify all # clients it's available self.buffer.truncate() with self.condition: self.frame = self.buffer.getvalue() self.condition.notify_all() self.buffer.seek(0) return self.buffer.write(buf) class StreamingHandler(server.BaseHTTPRequestHandler): def getPage(self): PAGE=""" Raspberry Pi HQ Camera Magnifying Up Down {button_label} W R Y G 0.1 mm 1 mm 10 mm
Info
Step: {step}
LED: {ring_status}
Color: {ring_color} """.format(ring_status=ring_status_label, ring_color=color,button_label= "OFF" if ring_status else "ON", step=stepper) return PAGE def changeLedColor(self): global ring_status, color if ring_status == True: print("color changed to ", color) if color == "red": print('red') ring.fill((255,0,0)) ring.show() elif color == "yellow": print('yellow') ring.fill((255,255,0)) ring.show() elif color == "green": print('green') ring.fill((70,245,10)) ring.show() else: print('white') ring.fill((255,255,255)) ring.show() else: print("Ring if OFF") def do_POST(self): global distance, ring_status, color, stepper, ring_status_label, step_per_mm, distance content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) if self.path == '/up': print("Go Up to ", distance) mymotortest.motor_go(False, "Full" , distance, 0.001 , False, .05) self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == '/down': print("Go Down to ", distance) mymotortest.motor_go(True, "Full" , distance, 0.001 , False, .05) self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == "/changecolor": color = post_data[10:].decode('utf-8') self.changeLedColor() self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == "/switchonoffring": try: if ring_status == False: ring_status = True ring_status_label = "On" self.changeLedColor() print("Switch ON LED") else: ring.fill((0, 0, 0)) ring.show() ring_status_label = 'Off' ring_status = False print("Switch OFF LED") except Exception as e: logging.warning('Neopixel error: %s', str(e)) self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == '/setdistance': distance = int(post_data[9:]) stepper = str( round(distance / step_per_mm, 1) ) + "mm" print("Set stepper to ", distance) self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() else: self.send_error(404) self.end_headers() def do_GET(self): if self.path == '/': self.send_response(301) self.send_header('Location', '/index.html') self.end_headers() elif self.path == '/index.html': content = self.getPage().encode('utf-8') self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', len(content)) self.end_headers() self.wfile.write(content) elif self.path == '/stream.mjpg': self.send_response(200) self.send_header('Age', 0) self.send_header('Cache-Control', 'no-cache, private') self.send_header('Pragma', 'no-cache') self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME') self.end_headers() try: while True: with output.condition: output.condition.wait() frame = output.frame self.wfile.write(b'--FRAME\r\n') self.send_header('Content-Type', 'image/jpeg') self.send_header('Content-Length', len(frame)) self.end_headers() self.wfile.write(frame) self.wfile.write(b'\r\n') except Exception as e: logging.warning( 'Removed streaming client %s: %s', self.client_address, str(e)) else: self.send_error(404) self.end_headers() class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer): allow_reuse_address = True daemon_threads = True with picamera.PiCamera(resolution='1296x972', framerate=24) as camera: output = StreamingOutput() #Uncomment the next line to change your Pi's Camera rotation (in degrees) camera.rotation = camera_rotation camera.start_recording(output, format='mjpeg') try: #print("try to get distance") address = ('', 8000) server = StreamingServer(address, StreamingHandler) server.serve_forever() #finally: # print("Stop camera stream") # camera.stop_recording() except KeyboardInterrupt: print("Shtdown HTTP server") server.shutdown() print("Close camera") camera.stop_recording() print("Switch off LED and cleanup GPIO") ring.fill((0,0,0)) ring.show() time.sleep(0.1) ring.deinit() GPIO.cleanup() os._exit(0)
Enregistrer le script Python
Explication du code
Nous allons détailler ici uniquement les points les plus importants du code source.
La librairie Python Adafruit NeoPixels permet de contrôler indépendamment chaque LED pilotées par le circuit WS2812B. Contrairement à la version Arduino, il n’est pas possible de modifier le niveau de luminosité (brightness) sauf à libérer la ressource et re-créer un nouvel objet NeoPixel. Pour pouvoir changer la couleur, il faut initialiser le paramètre auto_write à False.
La broche D10 (bus SPI) pose problème sur mon Raspberry Pi (à moins que cela ne provienne de la librairie). Il n’y a que la broche D12 disponible pour ce projet.
ring = neopixel.NeoPixel(board.D12, LED_COUNT, brightness = LED_BRIGHTNESS, auto_write=False, pixel_order = LED_ORDER)
La méthode fill() permet de changer la couleur de toutes les LED de l’anneau. On applique le changement en exécutant la méthode show().
ring.fill((255,0,0))
ring.show()
L’interface HTML est gérée par un serveur HTTP Python classique. L’interface HTML est utilise le thème Bootstrap déjà utilisé avec Flask.
Contrairement à Flask, le data binding (mise à jour automatique d’état) n’est pas disponible. Il faut donc renvoyer à chaque mise à jour une version actualisée de l’interface. Comme celle-ci est stockée dans une chaîne de caractère, il suffit d’utiliser la méthode format pour substituer la valeur de chaque variable.
PAGE=""" ...
Step: {step}
LED: {ring_status}
Color: {ring_color} ... """.format(ring_status=ring_status_label, ring_color=color,button_label= "OFF" if ring_status else "ON", step=stepper)
La méthode StreamingHandler() permet d’intercepter toutes les requêtes POST et GET envoyées au serveur HTTP depuis le la navigateur. Par exemple ici, on intercepte la commande UP pour déplacer l’axe vertical du microscope à l’aide du moteur Nema 17. La colonne de droite montre la différence d’écriture par rapport à Flask.
Serveur Python HTTP standard | Avec Flask |
|
|
Pour arrêter le script, on intercepte la combinaison de touche Ctrl + C de manière à
- server.shutdown() arrêter le serveur HTTP
- camera.stop_recording() arrêter le flux vidéo et libérer la caméra
- ring.fill((0,0,0)) éteindre l’anneau lumineux
- ring.deinit() libérer les ressources de l’objet NeoPixel
- GPIO.cleanup() libérer le GPIO
- os._exit(0) quitter le script Python
Pour pouvoir piloter l’anneau de LED Neopixel depuis le GPIO du Raspberry Pi, il faut démarrer le script en mode administrateur en faisant précédé la commande par un sudo.
Ouvrez le Terminal et placez vous dans le répertoire du script Python, par exemple de la dossier Document
cd /home/pi/Documents
Puis lancer le script en faisant précédé la commande d’un sudo
sudo python3 nom_du_script.py
L’interface HTML du microscope
L’interface HTML affiche le flux vidéo provenant de la caméra HQ. Sur le coté de l’image, on trouve les commandes UP et DOWN pour faire monter et descendre l’axe Z du statif.
Un sélecteur permet de choisir le déplacement à effectuer. 0.1mm 1mm ou 10mm à chaque mouvement. Vous pouvez modifier le code source pour changer cette pré-sélection.
La commande ON/OFF permet d’allumer l’anneau lumineux. Il est possible de changer la couleur de l’éclairage. Par défaut en blanc, rouge, jaune et vert.
Les réglages sont affichés sous le sélecteur
Exemples de grossissement obtenu avec différent éclairage
En faisant varier la couleur de l’éclairage à LED, il est possible de mettre en évidence certains détails du circuits (composants, pistes, inscriptions…).
Blanc |
Rouge |
Vert |
Jaune |
Voici une démonstration en vidéo
https://projetsdiy.fr/data/uploads/2020/11/demo-hq-camera-microscope-stand-raspberry-pi.mp4?_=1
Voici quelques clichés à différents grossissements d’un ESP8266EX installé sur une carte de développement ESP01.
Eclairage blanc avec 100% de luminosité.
Tube allonge 30m | Tube allonge 40mm | Tube allonge 50mm |
Grossissement x18 | Grossissement x26 | Grossissement x33 |
Observation d’une LED RGB
Juste pour le fun, voici deux clichés d’une LED RGB de l’anneau Neopixel utilisé pour l’éclairage. On peut visualiser très clairement le circuit intégré, les connexions et chaque source LED pour la couleur Rouge, Verte et Bleu.
Anneau allumé | Anneau éteint |
Essai d’observation d’épiderme d’oignon
Voici un dernier essai réalisé avec un un tube d’extension de 85mm. J’ai remplacé le bleu de méthylène par du colorant alimentaire pour mettre en évidence les cellules de l’épiderme. Pour obtenir le cliché, l’éclairage a été posé sur la table.
On distingue très bien les cellules mais pas du tout le noyau (certainement à cause du colorant utilisé et du grossissement trop faible).
Ce montage reste encore limité pour des observations scientifiques.
Observation de l’épiderme d’un oignon.
Afficher l’interface en plein écran, mode kiosk
Toutes les navigateurs internent proposent une version adaptée au écrans d’affichage. Par rapport à la touche F11, le mode Kiosk offre les avantages suivants :
- L’utilisateur ne peut pas voir les détails du bureau ou du système d’exploitation
- Le bouton X (fermer) est masqué
- La touche F11 est désactivée
- Les barres de menus, les barres d’outils ne sont pas visibles
- La barre d’état en bas n’est pas visible
- Le menu contextuel du clic droit ne fonctionne pas
- Les liens de destination ne sont pas visibles lors du survol des liens
Ajouter un onglet au Terminal ou ouvrez un nouveau Terminal puis exécuter la commande suivante
chromium-browser -kiosk 127.0.0.1:8000
Chromium s’ouvre en plein écran sans la barre d’adresse et les ascenseurs. Super pratique pour faire ses soudures sous un microscope !
Pour en savoir plus sur le mode kiosk de Chromium, lisez ce tutoriel
Avez-vous aimé cet article ?
[Total: 1 Moyenne: 5]