Microscópio motorizado OpenMV com precisão de 0,0001 milímetro

Tutorial em vídeo OpenMV da SingTown - Eu construí um microscópio motorizado OpenMV com precisão de 0,0001 milímetro!

O microscópio OpenFlexure é um microscópio de código aberto, baixo custo e modular, projetado para ser fácil de montar, operar e modificar. O projeto visa oferecer uma alternativa flexível e acessível aos microscópios de laboratório tradicionais para educação, pesquisa e ambientes com poucos recursos. A SingTown adicionou suporte deste microscópio ao OpenMV.

O microscópio OpenFlexure tem as seguintes vantagens:

(1) A característica mais marcante é a estrutura da plataforma, que aproveita a flexibilidade do material PLA, não sendo afetada por atrito ou vibração, e é extremamente simples de montar.

(2) A precisão é muito alta: um passo do motor de passo é menor que cem nanômetros, ou seja, 1/100 de uma hemácia.

(3) Outra vantagem é o custo relativamente baixo da impressão 3D.

O mais interessante da estrutura do microscópio é que ele aproveita a flexibilidade do material PLA, e o corpo principal é impresso de uma só peça. O movimento da plataforma não tem atrito, e a precisão é muito boa, alcançando 100 nanômetros de precisão. Como os microscópios prontos têm limitações significativas, não usamos um microscópio pronto — em vez disso, nos baseamos no design do microscópio OpenFlexure e o modificamos, projetando todo o caminho óptico do zero.

Fizemos as seguintes modificações no microscópio OpenFlexure para adaptá-lo ao OpenMV, sendo as quatro principais:

(1) Primeiro, modificamos o tubo da lente para adaptá-lo ao sensor de imagem da câmera OpenMV. Vale destacar que há uma lente de tubo aqui, que tem duas funções: encurtar a distância focal da objetiva e ajustar a abertura NA, para tornar a imagem mais clara.

(2) A segunda modificação foi aprimorar o design da gaveta inferior, além de adicionar botões e uma placa de acionamento de motor de passo, permitindo o controle preciso do movimento e do foco do microscópio.

(3) O terceiro ponto foi projetar uma nova placa de acionamento de motor de passo para o OpenMV, que pode controlar quatro motores de passo via I2C.

(4) Por fim, adicionamos a tela LCD do OpenMV e projetamos uma alimentação independente para todo o conjunto.

Estrutura geral

A principal modificação na estrutura geral foi substituir o esquema original baseado em Raspberry Pi pelo esquema OpenMV, adicionar botões e uma tela LCD, e usar uma bateria para alimentação independente, permitindo operação autônoma.

O microscópio como um todo é fabricado com tecnologia de impressão 3D. Graças à alta flexibilidade das peças impressas em 3D com baixo preenchimento e paredes finas, o microscópio não precisou de uma estrutura mecânica complexa, implementando o movimento da plataforma e da lente de forma extremamente engenhosa — detalhado mais adiante neste texto.

Movimento da plataforma e do tubo da lente

Nesta parte, o design do OpenFlexure aproveita a grande flexibilidade das peças impressas em 3D com um determinado preenchimento e espessura, criando uma estrutura muito engenhosa que realiza a translação da plataforma.

A estrutura central que realiza o movimento da plataforma é semelhante a uma escada, como mostrado na figura abaixo. A instabilidade do quadrilátero interno dessa estrutura é o cerne do movimento da plataforma. Ao imprimir essa estrutura em uma impressora 3D, é preciso garantir que a impressora consiga imprimir as duas travessas intermediárias — essas travessas são a chave para fornecer flexibilidade suficiente à estrutura, portanto há certas exigências quanto ao desempenho da impressora 3D.

Tomando o eixo X como exemplo para o movimento da plataforma: depois de garantir que o corpo principal do microscópio já foi impresso, o movimento da plataforma no eixo X requer apenas um conjunto de parafusos e um elástico. O parafuso passa por uma engrenagem grande e se conecta ao suporte da "escada"; a engrenagem grande será posteriormente acionada pelo motor de passo. O elástico também se conecta ao suporte da escada, fornecendo uma força oposta à do parafuso. A ilustração da instalação do parafuso e do elástico é mostrada nas duas figuras a seguir:

O parafuso e o elástico fornecem, respectivamente, as duas forças que deslocam a plataforma na direção do eixo X. Quando o motor de passo aciona a engrenagem grande em sentido horário, o parafuso se tensiona, o suporte da escada é levantado, a escada se desloca para a esquerda, realizando o movimento da plataforma na direção positiva do eixo X; quando o motor de passo aciona a engrenagem grande em sentido anti-horário, o parafuso relaxa, o suporte da escada é puxado para baixo pela força do elástico, a escada se desloca para a direita, e a plataforma se move na direção negativa do eixo X:

  • Parafuso tensionado

  • Estado inicial

  • Parafuso relaxado

O movimento na direção do eixo Y funciona da mesma maneira.

O movimento vertical do tubo da lente usa o mesmo método: quando o parafuso é tensionado, o suporte é levantado e a lente se move para baixo; quando o parafuso é relaxado, o suporte é puxado para baixo pelo elástico e a lente se move para cima.

Composição do tubo da lente

No tubo da lente, foram feitas algumas modificações para adaptá-lo ao sensor de imagem da câmera OpenMV. O projeto original do microscópio OpenFlexure usa a câmera arducam_b0196 do Raspberry Pi, cujo tamanho não é compatível com a lente do OpenMV. Modificamos o código OpenSCAD original para adicionar suporte à lente do OpenMV:

  • Lente original:

  • Lente OpenMV:

  • O tubo da lente aloja a lente convexa, o sensor de imagem e a objetiva:

  • Lente convexa:

  • Sensor de imagem OpenMV:

  • Objetiva:

Controle do motor de passo

Considerando que seria necessário adicionar botões e uma tela LCD posteriormente, e que o motor de passo 28BYJ-48 pode ser controlado sem um driver dedicado de motor de passo, adotamos aqui o chip PCA9555BS para expansão de IO, usando comunicação I2C. Usamos o transistor Darlington ULN2803LVS para acionar o motor de passo. O OpenMV envia via I2C os pulsos de acionamento para o PCA9555BS, que gera nas suas saídas de IO os níveis alto e baixo correspondentes para acionar o motor de passo por meio do Darlington:

O OpenMV é configurado com I2C por software, com P4 como SCL e P5 como SDA:

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

O endereço do PCA9555BS é 0x20, e todas as IOs são inicializadas em modo de saída:

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

Enviando pulsos da esquerda para a direita o motor de passo gira no sentido direto; enviando da direita para a esquerda, o motor gira em sentido reverso:

[(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 expansão do motor de passo:

  • Esquema da placa de expansão do motor de passo:

Varredura dos botões

Os botões usam outra placa de expansão de IO PCA9555BS para leitura, também usando o I2C por software do OpenMV, com P4 como SCL e P5 como SDA. Mas o endereço do PCA9555BS dos botões é 0x21, e as IOs são configuradas em modo de entrada:

pca2 = PCA9555(i2c, address = 0x21)
for i in range(16):
    pca2.inputPins(i)
  • Placa de expansão de botões:

  • Esquema da placa de expansão de botões:

Bandeja de PCB

No projeto original do microscópio, a bandeja era feita para o Raspberry Pi e não tinha interruptor, tela nem botões — o microscópio não podia funcionar de forma independente. Redesenhamos a bandeja para adaptá-la ao OpenMV, adicionando os furos para os botões, a porta de depuração USB do OpenMV e o slot do interruptor.

  • Bandeja original:

  • Bandeja OpenMV:

A PCB é montada sobre a bandeja, conectada à tela LCD por meio de cabos jumper, a bateria é instalada, e então tudo é colocado na base:

  • Vista frontal:

  • Vista superior:

  • Vista lateral direita:

Tela LCD

Usamos a tela LCD que acompanha o OpenMV, conectada ao OpenMV por meio de cabos jumper, e fixada na parte externa da base do microscópio.

  • Base:

  • Tela LCD:

Para garantir que a tela LCD exiba a imagem completa, o código a seguir gira e redimensiona a imagem original para que ela corresponda à resolução da tela:

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

Demonstração dos resultados do microscópio

  • Arranjo de pixels RGB da tela de um celular:

  • Lâmina de nematódeo:

  • Esfregaço de sangue de rã:

  • Lâmina de paramécio:

Código completo do 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),
]#hemácias

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

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



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): #hemácias
#        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écios
#        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):  #euglenas
#        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))  # Take a picture and display the 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

Download dos arquivos de impressão 3D

Arquivos de impressão 3D do microscópio motorizado OpenMV da SingTown

Lista de materiais (BOM) e etapas de montagem do microscópio

results matching ""

    No results matching ""