Microscope motorisé OpenMV, précision de 0,0001 millimètre

Tutoriel vidéo OpenMV de Singtown Technology - J'ai fabriqué un microscope motorisé OpenMV d'une précision de 0,0001 millimètre !

Le microscope OpenFlexure est un microscope open source, à faible coût et modulaire, conçu pour être facile à assembler, à utiliser et à modifier. Ce projet vise à offrir une alternative flexible et accessible aux microscopes de laboratoire traditionnels, pour l'éducation, la recherche et les environnements à faibles ressources. Singtown Technology a ajouté la prise en charge d'OpenMV à ce microscope.

Le microscope OpenFlexure présente les avantages suivants :

(1) Le plus caractéristique est la structure de la plateforme, qui exploite la flexibilité du matériau PLA, insensible aux frottements et aux vibrations, avec un assemblage extrêmement simple.

(2) Une précision très élevée, un pas du moteur pas à pas est inférieur à cent nanomètres, soit 1/100 d'un globule rouge.

(3) Un autre avantage est le coût relativement faible de l'impression 3D.

La structure la plus impressionnante du microscope est qu'elle exploite la flexibilité du matériau PLA, et que le corps principal est imprimé d'une seule pièce, le déplacement de la plateforme se fait sans frottement, la précision est donc excellente, ce qui permet d'atteindre une précision de 100 nanomètres. En raison des grandes limitations des microscopes du commerce, nous n'utilisons pas de microscope tout fait, nous nous sommes inspirés de la conception du microscope OpenFlexure pour la modifier, en concevant entièrement nous-mêmes le trajet optique.

Nous avons apporté les modifications suivantes au microscope OpenFlexure pour l'adapter à OpenMV, avec quatre changements principaux :

(1) Tout d'abord, nous avons modifié le fût optique pour l'adapter au capteur photosensible de la caméra OpenMV. Il convient de noter qu'il y a ici une lentille de tube, qui a deux fonctions : d'une part raccourcir la distance focale de l'objectif, d'autre part ajuster l'ouverture numérique (NA), afin de rendre l'image plus lumineuse.

(2) Le deuxième changement est l'amélioration de la conception du tiroir inférieur, avec l'ajout de boutons et d'une carte de pilotage moteur pour les moteurs pas à pas, ce qui permet de contrôler précisément le déplacement et la mise au point du microscope.

(3) Le troisième point est que nous avons conçu une nouvelle carte de pilotage de moteur pas à pas pour OpenMV, qui peut contrôler quatre moteurs pas à pas via I2C.

(4) Enfin, nous avons ajouté l'écran LCD d'OpenMV, et conçu une alimentation autonome globale.

Structure globale

La principale modification structurelle globale consiste à remplacer la solution Raspberry Pi d'origine par une solution OpenMV, en ajoutant des boutons et un écran LCD, et en utilisant une batterie pour une alimentation autonome, afin de permettre un fonctionnement indépendant.

Le microscope est entièrement fabriqué avec la technologie d'impression 3D ; grâce aux pièces imprimées en 3D à faible taux de remplissage et à faible épaisseur qui présentent une flexibilité extrêmement élevée, le microscope n'a pas nécessité de structure mécanique complexe pour réaliser de manière extrêmement astucieuse le déplacement de la platine porte-objet et de l'objectif ; cette partie sera détaillée plus loin dans le texte.

Déplacement de la platine porte-objet et du fût optique

Dans cette partie, la conception d'OpenFlexure exploite la grande flexibilité des pièces imprimées en 3D sous des paramètres spécifiques de taux de remplissage et d'épaisseur, pour concevoir une structure très astucieuse permettant le déplacement en translation de la platine porte-objet.

La structure centrale permettant le déplacement de la platine porte-objet est une structure ressemblant à une échelle, comme illustré ci-dessous. L'instabilité du quadrilatère interne de cette structure est au cœur du déplacement de la platine porte-objet. Lors de l'impression de cette structure avec une imprimante 3D, il faut absolument s'assurer que l'imprimante peut imprimer les deux barreaux intermédiaires ; ces barreaux sont la clé pour fournir suffisamment de flexibilité à la structure, ce qui impose donc certaines exigences sur les performances de l'imprimante 3D.

Prenons l'exemple de l'axe X pour le déplacement de la platine porte-objet. Une fois le corps principal du microscope imprimé, réaliser le déplacement de la platine porte-objet selon l'axe X ne nécessite qu'un jeu de vis et un élastique. La vis traverse un grand engrenage et est reliée au support de l'échelle, le grand engrenage étant ensuite entraîné par le moteur pas à pas. L'élastique est également relié au support de l'échelle, fournissant une force opposée à celle de la vis. Les schémas d'installation de la vis et de l'élastique sont illustrés dans les deux images suivantes :

La vis et l'élastique fournissent respectivement les deux forces de translation de la platine porte-objet selon l'axe X. Lorsque le moteur pas à pas entraîne le grand engrenage dans le sens horaire, la vis se resserre, le support de l'échelle est soulevé vers le haut, l'échelle se décale vers la gauche, réalisant le déplacement de la platine porte-objet dans le sens positif de X ; lorsque le moteur pas à pas entraîne le grand engrenage dans le sens antihoraire, la vis se desserre, le support de l'échelle est tiré vers le bas sous l'effet de la tension de l'élastique, l'échelle se décale vers la droite, la platine porte-objet se déplace dans le sens négatif de X :

  • Vis resserrée

  • État initial

  • Vis desserrée

Le déplacement selon l'axe Y fonctionne de la même manière.

Le déplacement vertical du fût optique utilise la même méthode : lorsque la vis est resserrée, le support est soulevé vers le haut, le fût optique se déplace vers le bas ; lorsque la vis est desserrée, le support est tiré vers le bas par l'élastique, le fût optique se déplace vers le haut.

Composition du fût optique

Concernant le fût optique, certaines modifications ont été apportées pour l'adapter au capteur photosensible de la caméra OpenMV. La conception d'origine du microscope OpenFlexure utilise la caméra Raspberry Pi arducam_b0196, dont la taille n'est pas adaptée à l'objectif d'OpenMV ; le code OpenSCAD a donc été modifié pour ajouter la prise en charge de l'objectif OpenMV :

  • Objectif d'origine :

  • Objectif OpenMV :

  • Le fût optique intègre une lentille convexe, le capteur photosensible et l'objectif :

  • Lentille convexe :

  • Capteur photosensible OpenMV :

  • Objectif :

Contrôle du moteur pas à pas

Compte tenu de la nécessité d'ajouter par la suite des boutons et un écran LCD, et du fait que le moteur pas à pas 28BYJ-48 peut être contrôlé sans pilote de moteur pas à pas dédié, une puce PCA9555BS a été utilisée ici pour l'extension IO, avec une communication I2C. Des transistors Darlington ULN2803LVS sont utilisés pour piloter le moteur pas à pas. OpenMV envoie les séquences d'entraînement au PCA9555BS via I2C, le PCA9555BS fait correspondre les sorties IO en niveau haut ou bas pour piloter le moteur pas à pas via les transistors Darlington :

OpenMV est configuré en I2C logiciel, P4 est SCL, P5 est SDA :

i2c = SoftI2C(scl=Pin("P4"), sda=Pin("P5"))

L'adresse du PCA9555BS est 0x20, tous les IO sont initialisés en mode sortie :

pca = PCA9555(i2c, address = 0x20)
for i in range(16):
    pca.outputPins(i)

Envoyer les séquences de gauche à droite fait tourner le moteur pas à pas dans le sens normal, les envoyer de droite à gauche le fait tourner en sens inverse :

[(1,0,0,0),(1,1,0,0),(0,1,0,0),(0,1,1,0),(0,0,1,0),(0,0,1,1),(0,0,0,1),(1,0,0,1),]
  • Carte d'extension moteur pas à pas :

  • Schéma de la carte d'extension moteur pas à pas :

Balayage des boutons

Les boutons sont lus via l'IO étendu d'un autre PCA9555BS, en utilisant toujours l'I2C logiciel d'OpenMV, P4 pour SCL, P5 pour SDA. Mais l'adresse du PCA9555BS des boutons est 0x21, l'IO est configuré en mode entrée :

pca2 = PCA9555(i2c, address = 0x21)
for i in range(16):
    pca2.inputPins(i)
  • Carte d'extension boutons :

  • Schéma de la carte d'extension boutons :

Plateau PCB

Le plateau de la conception d'origine du microscope est un plateau Raspberry Pi, sans interrupteur, écran ni boutons, le microscope ne pouvant donc pas fonctionner de manière autonome. Le plateau a été entièrement reconçu pour l'adapter à OpenMV, avec l'ajout de perçages pour les boutons, l'interface de débogage USB d'OpenMV et un emplacement pour l'interrupteur.

  • Plateau d'origine :

  • Plateau OpenMV :

Le PCB est installé sur le plateau, relié à l'écran LCD par des fils Dupont, avec la batterie installée, puis placé dans le socle :

  • Vue de face :

  • Vue de dessus :

  • Vue de droite :

Écran LCD

Utilise l'écran LCD fourni avec OpenMV, l'écran est relié à OpenMV avec des fils Dupont, et l'écran est fixé à l'extérieur du socle du microscope.

  • Socle :

  • Écran LCD :

Pour garantir que l'écran LCD affiche l'image complète, le code suivant est utilisé pour faire pivoter et redimensionner l'image d'origine, afin qu'elle corresponde à la résolution de l'écran :

lcd.write(img.copy(x_scale=0.25, y_scale=0.267, hint=image.ROTATE_90))

Démonstration des résultats du microscope

  • Disposition des pixels RGB d'un écran de smartphone :

  • Lame de nématode :

  • Frottis sanguin de grenouille :

  • Lame de paramécie :

Code complet OpenMV

main.py

from machine import SoftI2C, Pin
from pca9555 import PCA9555

import csi, image, time
import display
csi0 = csi.CSI()

csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.VGA)
csi0.snapshot(time = 2000)

clock = time.clock()

i2c=SoftI2C(scl=Pin("P4"),sda=Pin("P5"))
#print(i2c.scan())

pca=PCA9555(i2c, address = 0x20)

pca2=PCA9555(i2c, address = 0x21)

for i in range(16):
    pca.outputPins(i)

for i in range(16):
    pca2.inputPins(i)

thresholds = [
    (0, 34, -128, 127, -128, 127),
]# globules rouges

thresholds2 = [
    (0, 45, -128, 127, -128, 127),
]# paramécie

thresholds3 = [
    (0, 48, -128, 127, -128, 127),
]# euglène



class Motor():
    def __init__(self, motor_id):
        self.step_list = [(1,0,0,0),(1,1,0,0),(0,1,0,0),(0,1,1,0),(0,0,1,0),(0,0,1,1),(0,0,0,1),(1,0,0,1)]
        self.state_index = 0
        if motor_id == 1:
            self.pins = [0,1,2,3]
        elif motor_id == 2:
            self.pins = [4,5,6,7]
        elif motor_id == 3:
            self.pins = [8,9,10,11]
        elif motor_id == 4:
            self.pins = [12,13,14,15]

    def step(self, inverse=False):
        if inverse:
            self.state_index -= 1
            if self.state_index == -1:
                self.state_index = 7
        else:
            self.state_index += 1
            if self.state_index == 8:
                self.state_index = 0

        for i in range(4):
            pca.writePin(self.pins[i], self.step_list[self.state_index][i])

    def steps(self, n, delay=10):
        inverse = False
        if n > 0:
            inverse = True
        for i in range(abs(n)):
            self.step(inverse)
            time.sleep_ms(1)

mz = Motor(1)
#m2 = Motor(2)
mx = Motor(3)
my = Motor(4)

def key_read():
    if pca2.readPin(0)==0:
        my.steps(50)
    # time.sleep_ms(10)
    elif pca2.readPin(1)==0:
        mx.steps(50)
    # time.sleep_ms(10)
    elif pca2.readPin(2)==0:
        my.steps(-50)
    # time.sleep_ms(10)
    elif pca2.readPin(3)==0:
        mx.steps(-50)
    # time.sleep_ms(10)
    elif pca2.readPin(4)==0:
        mz.steps(50)
    # time.sleep_ms(10)
    elif pca2.readPin(5)==0:
        mz.steps(-50)

cell_num = 0
photo_num = 0
step_num = 0
sum_num = 0
avg_num = 0

lcd = display.SPIDisplay()

while(True):
    key_read()
    clock.tick()
    img = csi0.snapshot()

#    strs = str(photo_num) +".jpg"
#    img.save(strs)
#    photo_num=photo_num+1

#    for blob in img.find_blobs(thresholds, pixels_threshold=20, area_threshold=0,merge=0): # globules rouges
#        if blob.elongation > 0:
#            img.draw_cross((blob.cx, blob.cy),size=9)
#            cell_num=cell_num+1

#    for blob in img.find_blobs(thresholds2, pixels_threshold=200, area_threshold=200,merge=0):  # paramécie
#        if blob.elongation > 0:
#            img.draw_cross((blob.cx, blob.cy))
#            cell_num=cell_num+1

#    for blob in img.find_blobs(thresholds3, pixels_threshold=70, area_threshold=70,merge=0):  # euglène
#        if blob.elongation > 0:
#            img.draw_cross((blob.cx, blob.cy))
#            cell_num=cell_num+1

    sum_num=sum_num+cell_num
    cell_num=0
    step_num=step_num+1
    if step_num == 20:
       avg_num=sum_num/step_num
       step_num=0
       sum_num=0
       if round(avg_num)!=0:
           print(round(avg_num))
    lcd.write(img.copy(x_scale=0.25,y_scale =0.267,hint=image.ROTATE_90))  # Prend une photo et affiche l'image.
    # print(clock.fps())

microscope.py

from machine import SoftI2C, Pin
from pca9555 import PCA9555
import time

i2c=SoftI2C(scl=Pin("P4"),sda=Pin("P5"))
print(i2c.scan())

pca=PCA9555(i2c, address = 0x20)


for i in range(16):
    pca.outputPins(i)


class Motor():
    def __init__(self, motor_id):
        self.step_list = [(1,0,0,0),(1,1,0,0),(0,1,0,0),(0,1,1,0),(0,0,1,0),(0,0,1,1),(0,0,0,1),(1,0,0,1)]
        self.state_index = 0
        if motor_id == 1:
            self.pins = [0,1,2,3]
        elif motor_id == 2:
            self.pins = [4,5,6,7]
        elif motor_id == 3:
            self.pins = [8,9,10,11]
        elif motor_id == 4:
            self.pins = [12,13,14,15]

    def step(self, inverse=False):
        if inverse:
            self.state_index -= 1
            if self.state_index == -1:
                self.state_index = 7
        else:
            self.state_index += 1
            if self.state_index == 8:
                self.state_index = 0

        for i in range(4):
            pca.writePin(self.pins[i], self.step_list[self.state_index][i])

    def steps(self, n, delay=10):
        inverse = False
        if n > 0:
            inverse = True
        for i in range(abs(n)):
            self.step(inverse)
            time.sleep_ms(10)

m1 = Motor(1)

while True:
    m1.steps(10)
    m1.steps(-10)

pca9555.py

from machine import SoftI2C, Pin
import time


InputPort0 = 0x00
InputPort1 = 0x01
OutputPort0 = 0x02
OutputPort1 = 0x03
PolInversionPort0 = 0x04
PolInversionPort1 = 0x05
ConfigPort0 = 0x06
ConfigPort1 = 0x07

#############################################################################


class PCA9555:
    """PCA955 Driver
    16Bit IO extender
    i2c communication
    """


    def __init__(self, i2cBus, address=0x20):
        """
        Args:
            i2cBus: SoftI2C(Pin(*SCLpin*),Pin(*SDApin*))
            address: i2c address in hex (0x20 by default)
        """
        self.i2c = i2cBus
        self.address = address
        self.pinStats=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
        self.pinValues=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]


    def inputPins(self,inputPin):
        """
        Args:
            inputPin(int): 0-7 = IO0_0 - IO0_7 ; 8-15 = IO1_0-IO1_7
        """
        self.pinStats[inputPin]=1
        stats=0
        if inputPin <= 7:
            for i in range(8):
                stats+=self.pinStats[i]<<i
            stats=int(hex(stats),0)
            self.i2c.writeto_mem(self.address,ConfigPort0,bytes([stats]))
        else:
            for i in range(8):
                stats+=self.pinStats[i+8]<<i
            stats=int(hex(stats),0)
            self.i2c.writeto_mem(self.address,ConfigPort1,bytes([stats]))


    def outputPins(self,outputPin):
        """
        Args:
            outputPin(int): 0-7 = IO0_0 - IO0_7 ; 8-15 = IO1_0-IO1_7
        """
        self.pinStats[outputPin]=0
        stats=0
        if outputPin <= 7:
            for i in range(8):
                stats+=self.pinStats[i]<<i
            stats=int(hex(stats),0)
            self.i2c.writeto_mem(self.address,ConfigPort0,bytes([stats]))
        else:
            for i in range(8):
                stats+=self.pinStats[i+8]<<i
            stats=int(hex(stats),0)
            self.i2c.writeto_mem(self.address,ConfigPort1,bytes([stats]))


    def writePin(self,pin, value):
        """
        Args:
            Pin(int): 0-7 = IO0_0 - IO0_7 ; 8-15 = IO1_0-IO1_7
            value(int): 0 = off ; 1 = on
        """
        self.pinValues[pin]=value
        vals=0
        if self.pinStats[pin]:
            print('ATTENTION Pin '+str(pin)+' at i2c address '+str(self.address)+' is configed as INPUT')
            return
        elif pin <= 7:
            for i in range(8):
                vals+=self.pinValues[i]<<i
            vals=int(hex(vals),0)
            self.i2c.writeto_mem(self.address,OutputPort0,bytes([vals]))
        else:
            for i in range(8):
                vals+=self.pinValues[i+8]<<i
            vals=int(hex(vals),0)
            self.i2c.writeto_mem(self.address,OutputPort1,bytes([vals]))
#        print(vals)


    def readPin(self, pin):
        """Issue a measurement.
        Args:
            writeAddress (int): address to write to
        :return:
        """
        comeback = bytearray(1)
        if not self.pinStats[pin]:
            print('ATTENTION Pin '+str(pin)+' at i2c address '+str(self.address)+' is configed as OUTPUT')
            return
        elif pin <=7:
            comeback =self.i2c.readfrom_mem(self.address,InputPort0,1)
        else:
            self.i2c.readfrom_mem(self.address,InputPort1,2)
        raw = (comeback[0] >> (pin % 8)) & 1
        return raw

Téléchargement des fichiers d'impression 3D

Fichiers d'impression 3D du microscope motorisé OpenMV de Singtown Technology

Liste de matériel (BOM) détaillée et étapes d'installation du microscope

results matching ""

    No results matching ""