Microscopio motorizado OpenMV, precisión de 0.0001 mm

Tutorial en video de SingTown OpenMV - ¡Fabriqué un microscopio motorizado OpenMV con una precisión de 0.0001 mm!

El microscopio OpenFlexure es un microscopio de código abierto, bajo costo y modular, diseñado para ser fácil de ensamblar, operar y modificar. El proyecto tiene como objetivo ofrecer una alternativa flexible y accesible a los microscopios de laboratorio tradicionales para la educación, la investigación y entornos con recursos limitados. SingTown añadió compatibilidad con OpenMV a este microscopio.

El microscopio OpenFlexure tiene las siguientes ventajas:

(1) La característica más distintiva es la estructura de la plataforma, que aprovecha la flexibilidad del material PLA, no se ve afectada por la fricción ni las vibraciones, y su ensamblaje es muy sencillo.

(2) La precisión es muy alta: un paso del motor paso a paso es inferior a cien nanómetros, es decir, 1/100 del tamaño de un glóbulo rojo.

(3) Otra ventaja es que el costo de impresión 3D es relativamente bajo.

Lo más impresionante de la estructura del microscopio es que aprovecha la flexibilidad del material PLA, y el cuerpo principal se imprime en una sola pieza, de modo que el movimiento de la plataforma no tiene fricción y la precisión es excelente, permitiendo alcanzar una precisión de 100 nanómetros. Dado que los microscopios comerciales terminados tienen muchas limitaciones, no usamos un microscopio comercial, sino que modificamos el diseño tomando como referencia el microscopio OpenFlexure y diseñamos por completo la trayectoria óptica nosotros mismos.

Realizamos las siguientes modificaciones al microscopio OpenFlexure para adaptarlo a OpenMV; los cambios principales son cuatro:

(1) Primero modificamos el tubo del microscopio para adaptarlo al sensor de la cámara OpenMV. Cabe destacar que aquí hay una lente de tubo que cumple dos funciones: acortar la distancia focal del objetivo y ajustar la apertura numérica (NA), lo que hace que la imagen sea más brillante.

(2) En segundo lugar, mejoramos el diseño del cajón inferior y añadimos botones y una placa controladora para los motores paso a paso, lo que permite controlar con precisión el movimiento y el enfoque del microscopio.

(3) En tercer lugar, diseñamos una nueva placa controladora de motores paso a paso para OpenMV, capaz de controlar cuatro motores paso a paso mediante I2C.

(4) Por último, añadimos la pantalla LCD de OpenMV y diseñamos una alimentación eléctrica independiente para todo el conjunto.

Estructura general

La principal modificación en la estructura general consiste en reemplazar la solución original basada en Raspberry Pi por una solución basada en OpenMV, añadiendo botones y una pantalla LCD, y utilizando una batería para lograr una alimentación independiente y un funcionamiento autónomo.

El microscopio en su conjunto se fabrica mediante tecnología de impresión 3D. Aprovechando que las piezas impresas en 3D con bajo relleno y poco espesor poseen una flexibilidad extremadamente alta, el microscopio no requiere una estructura mecánica compleja, logrando el movimiento de la platina y del lente de una manera sumamente ingeniosa. Esta parte se detallará más adelante.

Movimiento de la platina y el tubo del microscopio

En esta sección, el diseño de OpenFlexure aprovecha que las piezas impresas en 3D, con un relleno y un grosor específicos, poseen una gran flexibilidad, diseñando una estructura muy ingeniosa que logra el desplazamiento lateral de la platina.

La estructura central que permite el movimiento de la platina es una estructura similar a una escalera, como se muestra en la siguiente imagen. La inestabilidad del cuadrilátero interior de esta estructura es la clave para lograr el movimiento de la platina. Al imprimir esta estructura con una impresora 3D, es fundamental asegurarse de que la impresora pueda imprimir correctamente los dos peldaños centrales, ya que estos son la clave para proporcionar suficiente flexibilidad a la estructura, por lo que se exige cierto nivel de rendimiento a la impresora 3D.

Tomando como ejemplo el eje X: una vez impreso el cuerpo principal del microscopio, lograr el movimiento de la platina en la dirección del eje X solo requiere un conjunto de tornillos y una banda elástica. El tornillo atraviesa un engranaje grande y se conecta con la placa de soporte de la escalera; el engranaje grande será posteriormente accionado por el motor paso a paso. La banda elástica también se conecta con la placa de soporte de la escalera, proporcionando una fuerza opuesta a la del tornillo. La instalación del tornillo y de la banda elástica se muestra en las dos imágenes siguientes:

El tornillo y la banda elástica proporcionan, respectivamente, las dos fuerzas que desplazan la platina en la dirección del eje X. Cuando el motor paso a paso hace girar el engranaje grande en sentido horario, el tornillo se aprieta, la placa de soporte de la escalera se levanta y la escalera se desplaza hacia la izquierda, logrando el movimiento de la platina en la dirección positiva del eje X; cuando el motor paso a paso hace girar el engranaje grande en sentido antihorario, el tornillo se afloja, la placa de soporte de la escalera es arrastrada hacia abajo por la tracción de la banda elástica, la escalera se desplaza hacia la derecha, y la platina se mueve en la dirección negativa del eje X:

  • Tornillo apretado

  • Estado inicial

  • Tornillo aflojado

El movimiento en la dirección del eje Y funciona de la misma manera.

El movimiento hacia arriba y hacia abajo del tubo del microscopio utiliza el mismo método: cuando el tornillo se aprieta, la placa de soporte se levanta y el tubo se mueve hacia abajo; cuando el tornillo se afloja, la placa de soporte es arrastrada hacia abajo por la banda elástica y el tubo se mueve hacia arriba.

Composición del tubo del microscopio

En cuanto al tubo del microscopio, se realizaron algunas modificaciones para adaptarlo al sensor de la cámara OpenMV. La solución original del microscopio OpenFlexure utilizaba la cámara Arducam B0196 de Raspberry Pi, cuyas dimensiones no eran compatibles con el lente de OpenMV, por lo que se modificó su código OpenSCAD para añadir compatibilidad con el lente de OpenMV:

  • Lente original:

  • Lente de OpenMV:

  • El tubo del microscopio aloja la lente convexa, el sensor y el objetivo:

  • Lente convexa:

  • Sensor de OpenMV:

  • Objetivo:

Control de los motores paso a paso

Teniendo en cuenta que posteriormente sería necesario añadir botones y una pantalla LCD, y que el motor paso a paso 28BYJ-48 puede controlarse sin necesidad de un controlador dedicado, aquí se utiliza el chip PCA9555BS para la expansión de E/S, con comunicación I2C. Se emplean transistores Darlington ULN2803LVS para accionar los motores paso a paso. OpenMV envía la secuencia de pasos al PCA9555BS mediante I2C, y el PCA9555BS produce niveles altos/bajos en las E/S correspondientes para accionar los transistores Darlington que controlan los motores paso a paso:

OpenMV se configura con I2C por software, donde P4 es SCL y P5 es SDA:

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

La dirección del PCA9555BS es 0x20, y todas las E/S se inicializan en modo de salida:

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

Enviar la secuencia de pasos de izquierda a derecha hace girar el motor paso a paso en sentido directo; enviarla de derecha a izquierda hace que el motor gire en sentido inverso:

[(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),]
  • Placa de expansión para el motor paso a paso:

  • Esquema de la placa de expansión para el motor paso a paso:

Escaneo de botones

Los botones se leen mediante otra placa de expansión de E/S PCA9555BS, utilizando también el I2C por software de OpenMV, con P4 como SCL y P5 como SDA. Sin embargo, la dirección del PCA9555BS de los botones es 0x21, y las E/S se configuran en modo de entrada:

pca2 = PCA9555(i2c, address = 0x21)
for i in range(16):
    pca2.inputPins(i)
  • Placa de expansión de botones:

  • Esquema de la placa de expansión de botones:

Bandeja PCB

La bandeja de la solución original del microscopio era una bandeja para Raspberry Pi, sin interruptor, pantalla ni botones, por lo que el microscopio no podía funcionar de forma independiente. Se rediseñó la bandeja para adaptarla a OpenMV, añadiendo orificios para los botones, el puerto USB de depuración de OpenMV y la ranura del interruptor.

  • Bandeja original:

  • Bandeja para OpenMV:

La PCB se instala sobre la bandeja, se conectan los cables Dupont hacia la pantalla LCD, se instala la batería y luego se coloca todo dentro de la base:

  • Vista frontal:

  • Vista superior:

  • Vista derecha:

Pantalla LCD

Se utiliza la pantalla LCD complementaria de OpenMV; la pantalla se conecta a OpenMV mediante cables Dupont y se fija en el exterior de la base del microscopio.

  • Base:

  • Pantalla LCD:

Para garantizar que la pantalla LCD muestre la imagen completa, se utiliza el siguiente código para rotar y escalar la imagen original, de modo que coincida con la resolución de la pantalla:

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

Resultados del microscopio

  • Disposición de píxeles RGB de la pantalla de un teléfono móvil:

  • Preparación de nematodos:

  • Frotis de sangre de rana:

  • Preparación de paramecios:

Código completo de 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),
]#glóbulos rojos

thresholds2 = [
    (0, 45, -128, 127, -128, 127),
]#paramecios

thresholds3 = [
    (0, 48, -128, 127, -128, 127),
]#euglena



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): #glóbulos rojos
#        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):  #paramecios
#        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):  #euglena
#        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))  # Toma una fotografía y muestra la imagen.
    # 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

Descarga de archivos de impresión 3D

Archivos de impresión 3D del microscopio motorizado OpenMV de SingTown

Lista de materiales (BOM) detallada y pasos de instalación del microscopio

results matching ""

    No results matching ""