Моторизованный микроскоп OpenMV с точностью 0.0001 мм

Видеоурок SingTown OpenMV — я сделал моторизованный микроскоп OpenMV с точностью 0.0001 мм!

OpenFlexure Microscope — это микроскоп с открытым исходным кодом, недорогой и модульный, созданный так, чтобы его было легко собирать, использовать и модифицировать. Цель проекта — предложить гибкую и доступную альтернативу традиционным лабораторным микроскопам для образования, исследований и условий с ограниченными ресурсами. Компания SingTown добавила в этот микроскоп поддержку OpenMV.

У микроскопа OpenFlexure есть несколько преимуществ:

(1) Самая характерная особенность — конструкция платформы, использующая гибкость PLA-пластика: она не подвержена трению и вибрациям, а сборка очень проста.

(2) Очень высокая точность: один шаг шагового двигателя — менее ста нанометров, то есть 1/100 диаметра эритроцита.

(3) Ещё одно преимущество — сравнительно низкая стоимость 3D-печати.

Самое крутое в конструкции микроскопа — использование гибкости PLA-пластика: корпус печатается единым целым, платформа перемещается без трения, что даёт очень высокую точность — вплоть до 100 нанометров. Поскольку готовые микроскопы имеют слишком много ограничений, мы не стали использовать готовый микроскоп, а взяли за основу конструкцию OpenFlexure и доработали её, полностью спроектировав всю оптическую систему самостоятельно.

Чтобы адаптировать микроскоп OpenFlexure под OpenMV, мы внесли следующие изменения — всего четыре основных доработки:

(1) Во-первых, мы изменили тубус, чтобы он подходил под светочувствительный элемент камеры OpenMV. Стоит отметить, что здесь используется дополнительная линза в тубусе, которая выполняет две функции: сокращает фокусное расстояние объектива и регулирует числовую апертуру (NA), делая изображение более ярким.

(2) Во-вторых, мы улучшили конструкцию нижнего выдвижного отсека и добавили кнопки и плату драйвера шаговых двигателей, что позволяет точно управлять перемещением и фокусировкой микроскопа.

(3) В-третьих, мы разработали для OpenMV новую плату драйвера шаговых двигателей, которая управляет четырьмя шаговыми двигателями по шине I2C.

(4) И наконец, мы добавили LCD-дисплей OpenMV и разработали автономное питание всей системы.

Общая конструкция

Основное изменение общей конструкции заключается в замене исходного решения на базе Raspberry Pi на решение на базе OpenMV, добавлении кнопок и LCD-дисплея, а также использовании автономного питания от батареи для независимой работы.

Весь микроскоп изготовлен с помощью 3D-печати. За счёт использования тонкостенных деталей с низким процентом заполнения, обладающих чрезвычайно высокой гибкостью, в микроскопе не потребовалась сложная механическая конструкция — перемещение предметного столика и объектива реализовано чрезвычайно изящным способом, подробнее об этом рассказывается далее.

Перемещение предметного столика и тубуса

В этой части конструкция OpenFlexure использует высокую гибкость 3D-печатных деталей при определённых параметрах заполнения и толщины, что позволило создать очень изящную структуру для реализации перемещения предметного столика.

Ключевой элемент, обеспечивающий перемещение предметного столика, — это структура, похожая на лестницу, показанная на рисунке ниже. Неустойчивость внутреннего четырёхугольника этой структуры — основа механизма перемещения столика. При печати такой структуры на 3D-принтере необходимо убедиться, что принтер способен качественно напечатать две средние перекладины — именно они обеспечивают структуре необходимую гибкость, поэтому к характеристикам 3D-принтера предъявляются определённые требования.

Рассмотрим перемещение столика на примере оси X. После того как корпус микроскопа напечатан, для реализации перемещения столика вдоль оси X требуется всего один винт и одна резинка. Винт проходит через большую шестерню и соединяется с опорной пластиной «лестницы»; впоследствии большая шестерня будет приводиться в движение шаговым двигателем. Резинка также соединена с опорной пластиной «лестницы» и создаёт силу, противоположную силе винта. Схемы установки винта и резинки показаны на двух рисунках ниже:

Винт и резинка создают две силы, обеспечивающие перемещение столика вдоль оси X. Когда шаговый двигатель вращает большую шестерню по часовой стрелке, винт затягивается, опорная пластина «лестницы» приподнимается, «лестница» смещается влево, и столик перемещается в положительном направлении оси X; когда шаговый двигатель вращает шестерню против часовой стрелки, винт ослабевает, опорная пластина под действием резинки опускается вниз, «лестница» смещается вправо, и столик перемещается в отрицательном направлении оси X:

  • Винт затянут

  • Исходное состояние

  • Винт ослаблен

Перемещение по оси Y реализовано аналогично.

Перемещение тубуса вверх и вниз реализовано тем же способом: когда винт затянут, опорная пластина приподнимается и тубус опускается вниз; когда винт ослаблен, пластина под действием резинки опускается, и тубус поднимается вверх.

Устройство тубуса

В тубус были внесены частичные изменения для адаптации к светочувствительному элементу камеры OpenMV. В исходном решении OpenFlexure использовалась камера Raspberry Pi arducam_b0196, размеры которой не подходят под объектив OpenMV, поэтому исходный код OpenSCAD был изменён, чтобы добавить поддержку объектива OpenMV:

  • Исходный объектив:

  • Объектив OpenMV:

  • В тубусе устанавливаются собирающая линза, светочувствительный элемент и объектив:

  • Собирающая линза:

  • Светочувствительный элемент OpenMV:

  • Объектив:

Управление шаговыми двигателями

Учитывая, что впоследствии потребуется добавить кнопки и LCD-дисплей, а шаговый двигатель 28BYJ-48 можно приводить в движение без специального драйвера, здесь для расширения портов ввода-вывода используется микросхема PCA9555BS с интерфейсом I2C. Для управления шаговым двигателем используется транзисторная сборка Дарлингтона ULN2803LVS. OpenMV по шине I2C отправляет такты управления на PCA9555BS, а PCA9555BS выдаёт на соответствующие выводы высокий или низкий уровень, управляя сборкой Дарлингтона, которая приводит в движение шаговый двигатель:

OpenMV настраивается на программный I2C: P4 — SCL, P5 — SDA:

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

Адрес PCA9555BS — 0x20, все выводы инициализируются в режиме выхода:

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

Отправка тактов слева направо вращает шаговый двигатель в прямом направлении, справа налево — в обратном:

[(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),]
  • Плата расширения для шагового двигателя:

  • Принципиальная схема платы расширения для шагового двигателя:

Опрос кнопок

Кнопки считываются через ещё одну микросхему расширения PCA9555BS, также по программному I2C OpenMV: P4 — SCL, P5 — SDA. Однако адрес PCA9555BS для кнопок — 0x21, а выводы настроены в режиме входа:

pca2 = PCA9555(i2c, address = 0x21)
for i in range(16):
    pca2.inputPins(i)
  • Плата расширения для кнопок:

  • Принципиальная схема платы расширения для кнопок:

PCB-лоток

В исходном решении лоток микроскопа был рассчитан на Raspberry Pi и не имел выключателя, дисплея и кнопок, из-за чего микроскоп не мог работать автономно. Лоток был переработан под OpenMV, в нём добавлены отверстия под кнопки, USB-разъём для отладки OpenMV и паз под выключатель.

  • Исходный лоток:

  • Лоток для OpenMV:

Плата PCB устанавливается на лоток, к ней подключаются провода Dupont для LCD-дисплея, устанавливается батарея, после чего всё помещается в основание:

  • Вид спереди:

  • Вид сверху:

  • Вид справа:

LCD-дисплей

Используется LCD-дисплей, поставляемый в комплекте с OpenMV. Дисплей подключается к OpenMV проводами Dupont и закрепляется снаружи на основании микроскопа.

  • Основание:

  • LCD-дисплей:

Чтобы LCD-дисплей корректно отображал изображение целиком, исходное изображение поворачивается и масштабируется следующим кодом, чтобы оно соответствовало разрешению дисплея:

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

Демонстрация работы микроскопа

  • Расположение RGB-пикселей экрана смартфона:

  • Препарат нематоды:

  • Мазок крови лягушки:

  • Препарат инфузории-туфельки:

Полный код для 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),
]#эритроциты

thresholds2 = [
    (0, 45, -128, 127, -128, 127),
]#инфузории

thresholds3 = [
    (0, 48, -128, 127, -128, 127),
]#эвглены



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): #эритроциты
#        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):  #инфузории
#        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):  #эвглены
#        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))  # Сделать снимок и вывести изображение на экран.
    # 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

Скачать файлы для 3D-печати

Файлы для 3D-печати моторизованного микроскопа OpenMV от SingTown

Подробный список компонентов (BOM) и шаги сборки микроскопа

results matching ""

    No results matching ""