OpenMV电动显微镜 精度0.0001毫米
星瞳科技OpenMV视频教程 - 我制作了一个精度0.0001毫米的OpenMV电动显微镜!
OpenFlexure 显微镜 是一款开放源代码、低成本、模块化的显微镜,旨在易于组装、操作和修改。该项目旨在为教育、研究和低资源环境提供一种灵活且可访问的替代传统实验室显微镜的方案。星瞳科技增添了此显微镜对 OpenMV 的支持。
OpenFlexure显微键有以下几个优点:
(1)最有特点的就是平台结构,利用了PLA材料的柔韧性,不受摩擦和震动的影响,组装非常简单。
(2)精度非常高,步进电机的一步小于一百纳米,就是一个红细胞的1/100。
(3)另一个优点就是3D打印的成本比较低。
显微镜的结构最酷炫的是它利用了PLA材料的柔韧性,并且主体是一体打印的,平台移动没有摩擦,精度非常好,因此可以做到100纳米的精度,因为成品显微镜的限制很大,我们不使用成品显微镜,参考OpenFlexure显微镜的设计进行修改,完全自己设计整个光路。
我们对OpenFlexure显微镜,进行了以下的改动来适配OpenMV,主要的改动有四个:
(1)首先我们修改了镜筒来适配OpenMV摄像头的感光元件。值得一提的是这里有一个筒镜,它有两个作用,一是缩短物镜的焦距,二是调整NA孔径,使得画面更加明亮。
(2)第二个是我们改进了底部抽屉的设计,并增加了按键和步进电机的电机驱动板,这样就能精确的控制显微镜的移动和对焦了。
(3)第三点是我们为OpenMV设计了一个新的步进电机的驱动板,它可以通过i2c来控制,四个步进电机。
(4)最后一点是,我们增加了OpenMV的LCD显示屏,并设计了整体的独立供电。
整体结构
整体结构主要修改在于将原本的树莓派方案替换为 OpenMV 方案,新增按键与 LCD 显示,并使用电池独立供电,实现独立运行。
显微镜整体使用 3D 打印技术制作,借助低填充量薄厚度 3D 打印物品具有极高柔韧性特点,显微镜并没有设计复杂的机械结构,以极其巧妙的方式实现载物台和镜头的移动,此部分在后文再详细赘述。
载物台和镜筒移动
在此部分,OpenFlexure的设计方案借助特定填充量特定厚度参数下3D打印件具有很大柔韧性的特定,设计了非常巧妙的结构,实现了载物台的平移。
实现载物台移动的核心结构为如下图所示的类似梯子的结构。此结构的内四边形的不稳定性是实现载物台移动的核心。在使用3D打印机打印此结构时,必须确保打印机可以打印出中间的两阶横杠,横杠是给结构提供足够的柔韧的关键,所以对3D打印机的性能有一定要求。
载物台的移动以X轴为例。在确保已经打印出显微镜主体后,实现载物台在X轴方向上移动仅需要一组螺丝和一个橡皮筋。螺丝穿过一个大齿轮和梯子的托板相连,大齿轮后续将被步进电机带动。橡皮筋同样和梯子的托板相连,提供与螺丝相反的力。螺丝安装示意和橡皮筋安装示意如下两图所示:
螺丝和橡皮筋分别提供载物台在X轴方向平移的两个力。当步进电机带动大齿轮顺时针旋转时,螺丝收紧,梯子托板被向上抬起,梯子向左偏移,实现载物台X正方向移动;当步进电机带动大齿轮逆时针旋转时,螺丝放松,梯子托板在橡皮筋拉力的作用下被向下拉扯,梯子向右偏移,载物台向X反方向移动:
螺丝收紧
初始状态
螺丝放松
Y轴方向上的移动同理。
镜筒的上下移动使用的也是同样的方法,当螺丝收紧时,托板被向上抬起,镜筒向下移动,当螺丝放松时,托板被橡皮筋向下拉扯,镜筒向上移动。
镜筒构成
在镜筒方面,进行了部分修改,以适配OpenMV摄像头的感光元件。OpenFlexure显微镜的原方案使用树莓派arducam_b0196摄像头,尺寸不适配OpenMV的镜头,对其OpenSCAD代码进行修改,添加了对OpenMV镜头的支持:
原镜头:
OpenMV镜头:
镜筒安装凸透镜,感光元件以及物镜:
凸透镜:
OpenMV感光元件:
物镜:
步进电机控制
考虑到后续需要添加按键和LCD显示屏,且28BYJ-48步进电机可以不需要使用专门的步进电机驱动进行控制,在此采用PCA9555BS芯片进行IO拓展,采用I2C通讯。使用ULN2803LVS达林顿管来驱动步进电机。OpenMV通过I2C发送驱动拍数给PCA9555BS,PCA9555BS对应IO输出高低电平给达林顿管驱动步进电机:
OpenMV配置为软件I2C,P4为SCL,P5为SDA:
i2c = SoftI2C(scl=Pin("P4"), sda=Pin("P5"))
PCA9555BS的地址为0x20,所有IO初始化为输出模式:
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拓展的IO来读取,依然使用OpenMV的软件按I2C,P4为SCL,P5为SDA。但按键的PCA9555BS地址为0x21,IO配置为输入模式:
pca2 = PCA9555(i2c, address = 0x21)
for i in range(16):
pca2.inputPins(i)
按键扩展板:
按键扩展板原理图:
PCB托盘
显微镜原方案托盘为树莓派托盘,且没有开关、显示屏和按键,显微镜工作无法独立工作。重修设计托盘,使其适配OpenMV,且添加按键的开孔,OpenMV的USB调试接口以及开关槽位。
原托盘:
OpenMV托盘:
PCB安装到托盘之上,并连接号到LCD显示屏的杜邦线,安装号电池,随后放入到底座中:
正视图:
顶视图:
右视图:
LCD显示屏
使用OpenMV配套的LCD显示屏,显示屏与OpenMV使用杜邦线连接,并将显示屏固定在显微镜底座外部。
底座:
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 sensor, image, time
import display
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.VGA)
sensor.skip_frames(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 = sensor.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)) # 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