Das USBMikroskop am RaspberryPI          

von Frank Behlich        

Elektronik-Labor  Projekte  Mikrocontroller  Raspberry     




Videos: https://youtu.be/U901uQsq68I
https://youtu.be/DeWQArK3lwc
https://youtu.be/fXEitZkoQ_c

Bei dem bekannten Aktionshaus im Netz gibt es USBMikroskope für den PC. Sie werden direkt aus China geliefert und man kann sie für 11 € erstehen. Eine Software für Windows liegt bei, doch die habe ich nie verwendet, da ich nur Linux nutze. Ich wollte eines haben und hoffte, dass es mit OpenCV ausgelesen werden kann. Falls es nicht geklappt hätte, dann wäre es auch ein Geschenk für Windowsnutzer gewesen. Es klappte mit der OpenCVBibliothek (Open Source Computer Vision Library) für python. Und ich schrieb ein Script mit Funktionen, die mir für meinen Einsatz wichtig erschienen. Es gibt eine umfangreichere Version mit Zoom und einer Bildgröße von 640X480(max. Bildgröße Mikroskop), sowie ein abgespecktes Script ohne Zoom im Format 320X240. Beide laufen auf dem Raspberry PI2(B), doch der kommt bei der großen Version an seine Grenzen. Die mitgelieferte Software des RaspberryPI öffnet das Programm mit der mitgelieferten Entwicklungsumgebung Thonny und versucht es mit python3 zu öffnen. Dies geht nicht, da python2.7 benötigt wird ! Es ist keine Nachinstallation nötig, denn beide Versionen sind installiert und mit dem Aufruf von einem Terminal mit



lässt es sich starten. Hier befinde ich mich auf meinem USBStick und dieser wird bei jedem einen anderen Namen tragen. Wenn man sich in seinem Homeverzeichnis befindet, kann man sich zu seinem Stick etc. durcharbeiten:



Mit „ls“ wird der Inhalt eines Verzeichnisses bzw. Ordners angezeigt und mit „cd ..“ bzw. „cd mein Ordner“ der/das Ordner/Verzeichnis gewechselt. Es geht auch mit kompletter Pfadangabe:



Falls installierte Pakete fehlen, dann meldet sich die IDE oder die Fehlerausgabe im Terminal:



Diese fehlenden Pakete können über die Paketverwaltung im Terminal nachinstalliert werden:



Ist alles installiert, dann sieht es in der „großen“ Version so aus:






Ich habe in den Scripten auf die "DEFAULT_CAM_ID = -1" hingewiesen, da die ID immer die erste eingebaute/angeschlossene Kamera erkennt. Sollte eine weitere Kamera angeschlossen sein, dann wird evt. diese "DEFAULT_CAM" erkannt und nicht das Mikroskop.
_______________________scriptmicroscope.py_______________________

#! /usr/bin/env python
# -*- coding: utf-8

try:
import tkinter as tk
except ImportError:
import Tkinter as tk

import time
import datetime
import cv2
from PIL import Image, ImageTk
from functools import partial
WIDTH = 640
HEIGHT = 480
VIDEO_CODEC = "XVID"
# -1 --> erste vorhandene kamera
# erste = 0 / zweite = 1 usw. --> aendern, wenn mehr vorhanden sind
DEFAULT_CAM_ID = -1

class Microscope(object):

PROPID_WIDTH = 3
PROPID_HEIGHT = 4

def __init__(self, cam_id = id):
self.cam = cv2.VideoCapture(cam_id)
self.recording = False
if not self.cam.isOpened():
raise RuntimeError("can not open camera {0!r}".format(
cam_id))
self.cam_width = self.zoom_width = self.cam.get(self.PROPID_WIDTH)
self.cam_height = self.zoom_height = self.cam.get(self.PROPID_HEIGHT)
self.aspect_ratio = self.zoom_width / self.zoom_height


def __enter__(self):
return self

def __exit__(self, *args):
self.release()

@property
def size(self):
return (int(self.cam_width), int(self.cam_height))

@property
def zoom_size(self):
return (int(self.zoom_width), int(self.zoom_height))

def get_image(self):
state, frame = self.cam.read()

if state:
image = Image.frombytes("RGB", self.size ,frame,
"raw", "BGR").resize(self.zoom_size)
if self.recording:
self.video_writer.write(frame)
return image
else:
return state

def recording_start_stop(self, state, path = ""):
self.recording = state
if self.recording:
self.video_writer = cv2.VideoWriter(path, cv2.cv.CV_FOURCC(
* VIDEO_CODEC), 24, (self.size))

def take_picture(self, path):
self.get_image().save(path)

def zoom_image(self, step):
width = int(self.zoom_width + step * self.aspect_ratio)
height = int(self.zoom_height + step)
if width > 0 and height > 0:
self.zoom_width = width
self.zoom_height = height

def reset_zoom(self):
self.zoom_width, self.zoom_height = self.size

def release(self):
self.cam.release()

class MicroscopeUI(tk.Frame):

UPDATE_INTERVAL = 200

def __init__(self, parent, microscope, width, height,
zoom_step = 10, picture_path = "", video_path = ""):
tk.Frame.__init__(self, parent)
self.parent = parent
self.width = width
self.height = height
self.picture_path = picture_path
self.video_path = video_path
self.tk_image = None
self.buttons = list()
self.microscope = microscope
self.canvas = tk.Canvas(self, width=width, height=height)
self.canvas.grid(column=0, row=0)
vscrollbar = tk.Scrollbar(self)
vscrollbar.grid(column=1, row=0, sticky=tk.N+tk.S)
self.canvas.config(yscrollcommand=vscrollbar.set)
vscrollbar.config(command=self.canvas.yview)
hscrollbar = tk.Scrollbar(self, orient=tk.HORIZONTAL)
hscrollbar.grid(column=0, row=1, columnspan=5, sticky=tk.E+tk.W)
self.canvas.config(xscrollcommand=hscrollbar.set)
hscrollbar.config(command=self.canvas.xview)
button_frame = tk.Frame(self)
button_frame.grid(column=0, row=3, columnspan=2)
for column, (text, command, var)in enumerate(
(("||", self.start_stop, None),
("+", self.microscope.zoom_image, zoom_step),
("-", self.microscope.zoom_image, -zoom_step),
("+/-", self.reset_zoom, None),
("[ ]", self.take_picture, None),
("REC", self.recording_film, None))):
button = tk.Button(button_frame, text=text, width=4,
relief="raised", font="Arial 10 bold")
button.grid(column=column, row=0)
self.buttons.append(button)
if var:
button.config(command=partial(command, var))
else:
button.config(command=command)
self.buttons[-1].config(bg = "lightgreen")

def start_stop(self):
if self.after_id is None:
self.buttons[0].config(text = "||")
self.run()
else:
self.buttons[0].config(text = ">")
self.after_cancel(self.after_id)
self.after_id = None

def run(self):
self.tk_image = self.microscope.get_image()
if self.tk_image:
self.canvas.delete("img")
self.tk_image = ImageTk.PhotoImage(self.tk_image)
self.canvas.create_image((0,0), anchor=tk.NW,
image=self.tk_image, tag="img")
width, height = self.microscope.zoom_size
self.canvas.config(scrollregion = (0, 0, width, height))
self.after_id = self.after(self.UPDATE_INTERVAL, self.run)
else:
self.raise_cam_id_error()

def raise_cam_id_error(self):
self.canvas.create_text((self.width / 2, self.height / 2),
text='NO CAM', font='Arial 40')
for button in self.buttons:
button.config(state="disabled")

def reset_zoom(self):
self.microscope.reset_zoom()

def recording_film(self):
if self.buttons[-1].config("bg")[-1] == "lightgreen":
self.buttons[-1].config(bg = "red")
self.microscope.recording_start_stop(True,
"{0}{1:%d%b%Y_%H_%M_%S.%f}.avi".format(self.video_path,
datetime.datetime.utcnow()))
else:
self.buttons[-1].config(bg = "lightgreen")
self.microscope.recording_start_stop(False)

def take_picture(self):
self.microscope.take_picture("{0}{1:%d%b%Y_%H_%M_%S.%f}.tiff"
.format(self.picture_path, datetime.datetime.utcnow()))

def release(self):
self.parent.destroy()

def main():
root = tk.Tk()
root.title('MICROSCOPE')
try:
with Microscope(DEFAULT_CAM_ID) as microscope:
microscope_ui = MicroscopeUI(
root, microscope, WIDTH, HEIGHT)
microscope_ui.pack(expand=tk.YES)
microscope_ui.run()
root.protocol("WM_DELETE_WINDOW", microscope_ui.release)
root.mainloop()

except RuntimeError:
tk.Label(root, text = 'can not open camera {0!r}'.format(
DEFAULT_CAM_ID), font = "Arial 20", height = 10).pack()
root.mainloop()

if __name__ == '__main__':
main()

__________________________________________________________________
... und in der „kleinen“ Version:






__________________scriptmicroscope_raspberry.py__________________


#! /usr/bin/env python
# -*- coding: utf-8

try:
import tkinter as tk
except ImportError:
import Tkinter as tk

import time
import datetime
import cv2
from PIL import Image, ImageTk
from functools import partial
WIDTH = 320
HEIGHT = 240
VIDEO_CODEC = "XVID"
# -1 --> erste vorhandene kamera
# erste = 0 / zweite = 1 usw. --> aendern, wenn mehr vorhanden sind
DEFAULT_CAM_ID = -1

class Microscope(object):

PROPID_WIDTH = 3
PROPID_HEIGHT = 4

def __init__(self, cam_id = id):
self.cam = cv2.VideoCapture(cam_id)
self.recording = False
if not self.cam.isOpened():
raise RuntimeError("can not open camera {0!r}".format(
cam_id))
self.cam_width = self.cam.get(self.PROPID_WIDTH)
self.cam_height = self.cam.get(self.PROPID_HEIGHT)

def __enter__(self):
return self

def __exit__(self, *args):
self.release()

@property
def size(self):
return (int(self.cam_width), int(self.cam_height))

def get_image(self):
state, frame = self.cam.read()
if state:
image = Image.frombytes("RGB", self.size ,frame,
"raw", "BGR")
if self.recording:
self.video_writer.write(frame)
return image
else:
return state

def recording_start_stop(self, state, path = ""):
self.recording = state
if self.recording:
self.video_writer = cv2.VideoWriter(path, cv2.cv.CV_FOURCC(
* VIDEO_CODEC), 24, (self.size))

def take_picture(self, path):
self.get_image().save(path)

def release(self):
self.cam.release()

class MicroscopeUI(tk.Frame):

UPDATE_INTERVAL = 100

def __init__(self, parent, microscope, width, height,
zoom_step = 10, picture_path = "", video_path = ""):
tk.Frame.__init__(self, parent)
self.parent = parent
self.width = width
self.height = height
self.picture_path = picture_path
self.video_path = video_path
self.tk_image = None
self.buttons = list()
self.microscope = microscope
self.canvas = tk.Canvas(self, width=width, height=height)
self.canvas.grid(column=0, row=0)
button_frame = tk.Frame(self)
button_frame.grid(column=0, row=3, columnspan=2)
for column, (text, command, var)in enumerate(
(("||", self.start_stop, None),
("[]", self.take_picture, None),
("REC", self.recording_film, None))):
button = tk.Button(button_frame, text=text, width=4,
relief="raised", font="Arial 10 bold")
button.grid(column=column, row=0)
self.buttons.append(button)
if var:
button.config(command=partial(command, var))
else:
button.config(command=command)
self.buttons[-1].config(bg = "lightgreen")

def start_stop(self):
if self.after_id is None:
self.buttons[0].config(text = "||")
self.run()
else:
self.buttons[0].config(text = ">")
self.after_cancel(self.after_id)
self.after_id = None

def run(self):
self.tk_image = self.microscope.get_image().resize(
(self.width, self.height))
if self.tk_image:
self.canvas.delete("img")
self.tk_image = ImageTk.PhotoImage(self.tk_image)
self.canvas.create_image((0,0), anchor=tk.NW,
image=self.tk_image, tag="img")
self.after_id = self.after(self.UPDATE_INTERVAL, self.run)
else:
self.canvas.create_text((self.width / 2, self.height / 2),
text='NO CAM', font='Arial 40')
for button in self.buttons:
button.config(state="disabled")

def recording_film(self):
if self.buttons[-1].config("bg")[-1] == "lightgreen":
self.buttons[-1].config(bg = "red")
self.microscope.recording_start_stop(True,
"{0}{1:%d%b%Y_%H_%M_%S.%f}.avi".format(self.video_path,
datetime.datetime.utcnow()))
else:
self.buttons[-1].config(bg = "lightgreen")
self.microscope.recording_start_stop(False)

def take_picture(self):
self.microscope.take_picture("{0}{1:%d%b%Y_%H_%M_%S.%f}.tiff"
.format(self.picture_path, datetime.datetime.utcnow()))

def release(self):
self.parent.destroy()

def main():
root = tk.Tk()
root.title('MICROSCOPE')
try:
with Microscope(DEFAULT_CAM_ID) as microscope:
microscope_ui = MicroscopeUI(
root, microscope, WIDTH, HEIGHT)
microscope_ui.pack(expand=tk.YES)
microscope_ui.run()
root.protocol("WM_DELETE_WINDOW", microscope_ui.release)
root.mainloop()

except RuntimeError:
tk.Label(root, text = 'can not open camera {0!r}'.format(
DEFAULT_CAM_ID), font = "Arial 20", height = 10).pack()
root.mainloop()

if __name__ == '__main__':
main()


__________________________________________________________________

Der Halter des Mikroskops hat sich nach einiger Zeit und Kinderhänden zerlegt, doch die Qualität ist bei diesen Preisen völlig ausreichend und das eigentliche Mikroskop hält schon 3-4 Jahre ohne Probleme. Jetzt war basteln angesagt und aus Resten entstand ein neuer Halter. Der Alustab und die Festellmutter stammt aus dem Baumarkt. Die Innenfläche des Holzarmes, im Bereich der Durchführung des Alustabes, habe ich mit Heißkleber „gummiert“ und so den Reibschluss erhöht. Die Gummiklebefüße gab es im Set beim Discounter um die Ecke. Ein mit Schmirgelpapier angeschliffener Konus hält das Mikroskop in stabiler Position und mit einen leichten Ruck geht es auch wieder raus.



Eine weitere Version mit dem Raspberry PI und einen WaveshareDisplay 3.2 ist in Arbeit und erste Ergebnisse sehen so aus:




Zwei Anmerkungen  von Ralf Beesner

1.) Frank verwendet vermutlich noch die ältere Raspbian-Version (Codename "wheezy"). Unter der aktuellen Version (Codename "stretch") wurde eines der nachzuinstallierenden Pakete umbenannt. "python-imaging-tk" heißt nun "python-pil.imagetk".
Unter den (neuesten) Ubuntu-Varianten 17.10 gilt das ebenfalls.

2.) Mein schon ziemlich altes USB-Mikroskop wird unter Linux als v4l (Video-for-Linux)-Gerät erkannt, also als analoge TV-Karte. Man kann daher ein TV-Programm zur Wiedergabe verwenden; ich nutze meist das Kommandozeilenprogramm mplayer (Aufruf "mplayer tv://").
Möglicherweise funktioniert das auch mit neueren USB-Mikroskopen.



Elektronik-Labor  Projekte  Mikrocontroller  Raspberry