Pojďme programovat elektroniku | Mikroskop

Programování elektroniky: Proměnili jsme starý školní mikroskop v digitální dělo

  • Máte doma starý optický mikroskop z dětství?
  • Dnes jeden takový vrátíme do 21. století
  • Připojíme k němu Raspberry Pi a jeho kameru

Před třemi týdny jsme si v našem seriálu o programování elektroniky pohráli s novou oficiální kamerou pro Raspberry Pi Camera Module 3. Její snímací čip Sony IMX708 nabízí 12 megapixelů a modul samotný pak motorizovanou ostřící čočku. Nechybí tedy ani autofokus a softwarové manuální ostření.

Minule jsme nastavovali vzdálenost ostřící čočky otáčením rotačního enkodéru připojeného k Raspberry Pi, no a tentokrát do hry zapojíme starý redakční optický mikroskop AmScope. Není to žádné dělo – spíše taková lepší a už notně zašlá lupa, kterou kdysi doplňoval také nasazovací okulár s kamerou.

Mikroskop před lety používali zejména kolegové z časopisu Computer třeba pro testy tiskáren, prehistorické a dále nevyvíjené ovladače už ale nefungují v novějších verzích Windows a časem se ztratil i samotný okulár s USB kamerou.  

Podívejte se na video starého mikroskopu,  ke kterému jsme připojili Camera Module 3 a napojili jej na knihovnu OpenCV pro analýzu obrazu:

Adaptér z 3D tiskárny

Namísto původního kamerového okuláru, který měl beztak tragické rozlišení a kvalitu obrazu, proto nasadím na ten běžný optický právě Camera Module 3 připojený k Raspberry Pi 4.  Aby to vypadalo k světu, v TinkerCadu jsem si navrhl primitivní nasazovací adaptér s rozměry okuláru, který jsem si poté vyrobil na 3D tiskárně.

189839eb-c0d9-4058-bc98-5ed902c9895bad14efa5-1322-414a-ba1e-efdfd31d4717675e82ac-4489-42a4-ad14-3205c758358a
Kameru připojíme k okuláru mikroskopu pomocí adaptéru z 3D tiskárny

Kamera má zorné pole 66°, které se ještě trošku zmenší při zaostření na nejbližší bod, i to je ale stále příliš mnoho, takže při fotografování v plném rozlišení 4608×2592 pixelů využijeme jen středovou část. Zbytek snímku vyplní černé pixely stínu tubusu. Řešením by byla leda výroba vhodné předčočky, která upraví optické parametry našeho kamerového modulu.

84b3e484-7a92-4fb5-bbfe-1993967e8e13
List rýmovníku Vládi Klusky pod redakčním mikroskopem 

Živé video je rychlé, protože z kamery stahujeme snímky v náhledovém rozlišení

Při živém pohledu na obraz kamery ve formě videa, které budeme promítat do okna grafického desktopu Raspberry Pi OS a na běžný monitor, to ale ničemu nevadí, v tu chvíli totiž budeme CM3 používat v režimu náhledu.

23b00410-1628-416e-a912-ae955c6913ff
Náš program v Pythonu streamuje ořezaný obraz z kamery v nízkém rozlišení a provádí analýzu. V tomto případě se díváme na detail tištěné antény Bluetooth

Obraz tedy bude mít rozlišení jen 640×480 pixelů a bude se jednat o středový ořez, do kterého nebude tubus téměř vůbec zasahovat. Snížené rozlišení je klíčové proto, abychom mohli s obrazem dále pracovat a promítat jej s co možná nejnižší latencí do okna. Později bychom jej mohli navyšovat a hledat limity toho, co ještě Raspberry Pi utáhne.

Náš mikroskop provádí živou analýzu

Aby byl náš vylepšený mikroskop ještě schopnější než mnohé asijské cetky z AliExpressu, nebudeme v okně promítat jen živý obraz z kamery, ale v reálném čase jej proženeme i několikanásobným zpracováním.

e0a595d3-5cb4-47bd-854a-851285812f0a34c7a985-f9e3-4cba-95b5-75b07e7b9eff8840ece2-a22f-4705-89d2-312a62b39221
Detail čipu adresovatelné RGB LED, detaily různých prototypovacích desek
51b71194-3ee6-4696-9802-1486bdf0086d540a1af6-beba-4170-ae99-900eaeae70471e0cdd12-2eb3-4c3c-b135-e356d972c749
Detail prototypovací desky, detail listu rýmovníku a rezavý šroub velikosti M2 (2 mm)

S tím nám pomůže populární knihovna OpenCV pro počítačové vidění. Po spuštění skriptu v Pythonu se proto zobrazí šachovnice čtyř snímků vedle sebe. Originál doplní ještě digitální lupa, negativ, který často pomůže s rozlišením málo kontrastních předělů v pravých barvách, a konečně také černobílý snímek s detekcí hran.

Ten poslední se hodí pro odhalování struktury a může to být také indikátor korektního zaostření (čím více patrné struktury, tím i lepší zaostření).

cb62fd08-3739-4c16-aed8-dec2f0d04ec226798609-732d-437d-b2c6-5500bb98b43ac3fbd716-d618-407a-9f95-35e595155bd5
96cb03d5-42af-43cf-b063-9a8dec7f4d5c5c5746e3-8940-4228-babb-ebd9cf49ceed85f93ed4-e085-4a2f-9eeb-f5881c6666c2
Zatímco v živé analýze pracujeme se zmenšeným výřezem, 12MP JPEG uloží celé zorné pole fotoaparátu, které odhalí, že podstatnou část záběru tvoří stín tubusu

Program ovládáme klávesovými zkratkami

Okno se čtyřmi náhledy a různým zpracováním reaguje na stisky kláves:

  • ESC ukončí program
  • p přiblíží náhled digitální lupy o krok
  • o oddálí náhled digitální lupy o krok
  • s uloží obsah okna jako PNG soubor
  • f přepne kameru na režim fotografie a pořídí 12MP neořezaný snímek

Instalujeme OpenCV na Raspberry Pi OS

Knihovna OpenCV pro Python sice není základní součástí systému Raspberry Pi OS, k dispozici je ale balíček pro instalátor pip. Dále předpokládejme, že máte na Malině nejnovější systém  Raspberry Pi OS řady Bullseye (podzim 2021 a dál), ve kterém už s kamerou pracujeme skrze vrstvu Libcamera a předinstalovanou knihovnu Picamera2. Vše jsme si vysvětlili v předchozím dílu, takže do něj případně nahlédněte.

Nejprve nainstalujeme prerekvizity pro OpenCV pomocí vestavěného instalátoru Apt:

sudo apt update
sudo apt install build-essential cmake pkg-config libjpeg-dev libtiff5-dev libjasper-dev libpng-dev libavcodec-dev libavformat-dev libswscale-dev libv4l-dev libxvidcore-dev libx264-dev libfontconfig1-dev libcairo2-dev libgdk-pixbuf2.0-dev libpango1.0-dev libgtk2.0-dev libgtk-3-dev libatlas-base-dev gfortran libhdf5-dev libhdf5-serial-dev libhdf5-103 python3-pyqt5 python3-dev -y

A poté samotný balíček OpenCV pro Python pomocí instalátoru pip:

pip install opencv-python==4.5.3.56

Anebo pro všechny uživatele:

sudo pip install opencv-python==4.5.3.56

Instalujeme konkrétní verzi OpenCV 4.5.3.56, která zaručeně poběží i na aktuální verzi Raspberry Pi OS. Pokud číslo verze neuvedete, stáhne se ta poslední, která ale nemusí být vždy funkční.

Že je vše v pořádku, ověříte přímo v interpretu Pythonu vložením knihovny a třeba vypsáním verze:

import cv2
cv2.__version__

V mém případě vyskočila chyba odkazují na knihovnu pro práci s komplexními poli NumPy. Pokud se vám to stane také, zkuste ji aktualizovat:

pip install -U numpy

Anebo pro všechny uživatele:

sudo pip install -U numpy

Pokud to stále nebude fungovat, odkážu vás na tento podrobnější návod z GitHubu i s bohatou diskuzí pod ním.

f47670eb-729a-403a-afb6-585aa3adb2c8
Hurá, OpenCV v Pythonu na Raspberry Pi OS funguje

Nejprve nastartujeme kameru

Náš program bude uložený v souboru mikroskop.py a budeme jej moci spustit v interpretu Pythonu příkazem:

python mikroskop.py

Komentovaný zdrojový kód najdete níže, takže si jen stručně projdeme, co se v něm vlastně odehrává.

Na začátku vytvoříme objekt kamery pomocí knihovny Picamera2 pro Python. Ta je součástí systému a nemusíte ji ručně instalovat.  Picamera2 pracuje se všemi oficiálními kamerami od Raspberry Pi.

Poté vytvoříme dva profily:

  • Náhledový ve formátu YUV420p
  • Plnohodnotný profil pro pořizování 12MP fotografií

Každý z profilů může mít hromadu dalších a pokročilých voleb, to už vás ale opět odkážu na oficiální dokumentaci v PDF.

Nakonec nakonfigurujeme kameru s náhledovým profilem a nastartujeme ji.

Ve smyčce čteme snímek za snímkem

Veškerý následující běh programu se odehrává v nekonečné smyčce, ze které vyskočíme stisknutím klávesy ESC (fokus musí mít okno s obrazem z kamery), což poté povede i k ukončení celého skriptu.

V každém cyklu smyčky přečteme pomocí metody capture_array snímek v aktuálním snímacím profilu jako multidimenzionální pole pixelů. Pole je ve formátu knihovny NumPy, se kterým pracuje i OpenCV.

Náhledové záběry stahujeme ve formátu YUV420p, pro jednodušší práci jej proto dále převedeme na formát RGB. Ze snímku poté vytvoříme několik dílčích kopií, přičemž každou z nich proženeme různým filtrem. Jeden bitmapu zvětší a ořeže na stejný rozměr, druhý pomocí vestavěné funkce Canny pro detekci hran algoritmem canny edge detection najde struktury v obrazu, no a třetí elementární metodou bitwise_not spočítá opačnou hodnotu pixelů pro negativ.

Negativní barvou kanálů R, G, a B je v případě osmibitové hloubky vlastně jen rozdíl 255 - hodnota. Z naprosté černé (0, 0, 0) se tedy stane bílá (255, 255, 255), z tmavě šedé (20, 20, 20), téměř bílá (235, 235, 235) a tak dále.

Ještě to poskládat dohromady

Jelikož jsou jednotlivé snímky ve své podstatě jen pole pixelů, můžeme tato pole složit do šachovnice – jednoho velkého pole. Pomůže nám s tím knihovna NumPy a její metody hstack (horizontální skládání) a vstack (vertikální skládání). Tuto složenou scénu konečně zobrazíme na monitoru metodou knihovny OpenCV imshow.

102fd264-8637-4a08-b7c7-d5cc2d9e696e8423ea50-bf05-401f-bc51-c06866442661
Horizontální a vertikální skládání polí (tedy i našich snímků) pomocí knihovny NumPy do hotové šachovnice, kterou zobrazíme v okně

Při stisku „s“ uložíme stejné pole jako obrázek ve formátu PNG pomocí metody imwrite. Pro pořízení plné fotografie při stisku „f“ pak pomocí metody switch_mode knihovny Picamera2 nejprve přepneme na v úvodu vytvořený profil pro fotografie, metodou capture_file uložíme fotografii do souboru, a opětovným voláním metody switch_mode se vrátíme do náhledového režimu.

V obrazu si přepnutí všimnete probliknutím obrazu při novém měření expozice, jinak by ale mělo vše zabrat jen zlomek času.

Kompletní kód mikroskopu v Pythonu pro knihovny Picamera2 a OpenCV

Dnes jsme si tedy ukázali, k čemu jsou dobré prototypovací kamerové moduly pro Raspberry Pi. Nemusejí hned nahrazovat webkameru nebo fotoaparát ve vašem mobilu, ale jako rychlý prototyp vrátí do 21. století třeba starý školní mikroskop z dob uhlí a páry jako v dnešním experimentu.

# Oficialni knihovna Raspberry Pi Picamera2 pro praci s kamwerami
# skrze novou vrstvu Libcamera, kterou Raspberry Pi OS pouziva od verze "Bullseye" (podzim 2021)
# Manual: https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf
from picamera2 import Picamera2

# Knihovna pro analyzu obrazu 
# Instalace pro Python na Raspberry Pi OS:
# https://raspberrypi-guide.github.io/programming/install-opencv
# https://opencv.org
import cv2

# Knihovna pro pokrocile operace s kompelxnimi poli
# https://numpy.org
import numpy as np

# Vestavena knihovna pro zjisteni aktualniho data a casu
# Budeme pouzivat v nazvech ukladanych snimku
from datetime import datetime

# Pomocna funkce pro digitalni zoom a orez na stejny rozmer
# Pozor, pri velkem zoomu narocne, nejprve totiz snimek zvetsim a teprve pak orezu
# Pokud operace otocim, nepotrebuji tolik RAM a CPU, ale slozitejsi kod
def priblizit_a_vycentrovat(snimek, _priblizeni=2):
    vyska, sirka = snimek.shape[:2]
    zvetseny = cv2.resize(snimek, None, fx=_priblizeni, fy=_priblizeni)
    _vyska, _sirka = zvetseny.shape[:2]
    x = int(_sirka/2) - int(sirka/2)
    y = int(_vyska/2) - int(vyska/2)
    orezany = zvetseny[y:y+vyska, x:x+sirka]
    return orezany
    

# Zacatek behu naseho programu
if __name__ == "__main__": 

    print("Startuji mikroskop...")

    priblizeni = 2
    popisky = True


    # Vytvorim objekt kamery
    kamera = Picamera2()

    # Vytvorim konfiguraci pro rychle/nahledove snaimani obrazu ve vychozim nizkem rozliseni 640x480 a barevnem formatu YUV420p
    # Diky tomu bude Raspberry Pi stihat zobrazovat snimky videa ve vysoke rychlosti 
    config_nahled = kamera.create_preview_configuration({"format": "YUV420"})
    config_fotoaparat = kamera.create_still_configuration()

    # Nakonfigurujeme a nastartujeme kameru
    kamera.configure(config_nahled)
    kamera.start()

    while True:
        # Ziskam pole pixelu v barevnem fotmatu YUV420p
        yuv420 = kamera.capture_array()

        # Vytvorim si novy snimek, ktery uz bude v RGB
        snimek = cv2.cvtColor(yuv420, cv2.COLOR_YUV420P2RGB) 

        # Vytvorim priblizeny snimek stejnych rozmeru vycentrovany na stred
        # Priblizeni mohu ridit pomoci klaves p/o
        # !!! Pozor, narocny processing kvuli jendoduchosti kodu !!!
        # Pri priblizeni 10x a vice uz pomalejsi, protoze nejprve snimek zvetsim a pak teprve orezu
        # Pri zvetseni 10x a vice tedy vytvarim velmi velky obrazek
        # Jak to predelat? Nejprve podle miry priblizeni spocitam,
        # jak ma vypadat vyrez, pote jej vyriznu a ten zvetsim na stejne rozmery nahledu
        snimek_priblizeny = priblizit_a_vycentrovat(snimek, priblizeni)

        # Vytvorim snimek s detekovanymi hranami pomoci vestaveneho detektoru
        # a algorimtu Canny Edge Detection (https://docs.opencv.org/4.x/da/d22/tutorial_py_canny.html)
        # Nejprve snimek zjednodusim prevodem do odstinu sedi mirnym rozmazanim
        snimek_sedy = cv2.cvtColor(snimek, cv2.COLOR_RGB2GRAY)
        snimek_rozmazany = cv2.GaussianBlur(snimek_sedy, (3,3), 0)
        snimek_detekce_hran = cv2.Canny(image=snimek_rozmazany, threshold1=100, threshold2=200)
        # Snimek je v odstinech sedi, a tak je prevedu zpet do RGB, protoze
        # pri finalnim skladani scen museji mit vsechny dilci snimky stejny format dat
        snimek_detekce_hran = cv2.cvtColor(snimek_detekce_hran, cv2.COLOR_GRAY2RGB)

        # Vytvorim snimek barevneho negativu, ktery muze odhalit skryte detaily
        # Negativni 8bitovy pixel je vlastne prtepocet 255-pixel. Za nas to udela
        # rychla vestavena metoda bitwise_not (https://docs.opencv.org/3.4/d0/d86/tutorial_py_image_arithmetics.html)
        snimek_negativ = cv2.bitwise_not(snimek)


        # Pripojeni zelenych popisku do jednotlivych snimku
        if popisky:
            snimek = cv2.putText(snimek, f"Original", (10,30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 1, cv2.LINE_AA)
            snimek_priblizeny = cv2.putText(snimek_priblizeny, f"{priblizeni}x", (10,30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 1, cv2.LINE_AA)
            snimek_negativ = cv2.putText(snimek_negativ, f"Negativ", (10,30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 1, cv2.LINE_AA)
            snimek_detekce_hran = cv2.putText(snimek_detekce_hran, f"Hrany", (10,30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 1, cv2.LINE_AA)

        # Pomoci knihovny NumPy pro operace s komplexnimi poli spojim vzdy
        # dva snimky (tedy pole pixelu) do jednoho radku (vetsiho pole)
        # pomoci metody hstack (https://numpy.org/doc/stable/reference/generated/numpy.hstack.html)
        radek1 = np.hstack((snimek, snimek_priblizeny))
        radek2 = np.hstack((snimek_negativ, snimek_detekce_hran))

        # Nyni analogicky spojim oba radky vertikalne do finaln isceny pomoci
        # metody vstack (https://numpy.org/doc/stable/reference/generated/numpy.vstack.html)
        scena = np.vstack((radek1, radek2))

        # ZObrazim primitivni GUI okno se scenou
        cv2.imshow("Živě.cz mikroskop", scena)
        
        # Detekce stisku klavesy
        klavesa = cv2.waitKey(1)
        if klavesa == 27: # ESC ukonci smycku
            break
        elif klavesa == ord("s"): # s ulozi scenu jako PNG s datem a casem v nazvu
            print("Ukladam snimek...")
            datum = datetime.today().strftime("%y%m%d_%H%M%S")
            cv2.imwrite(f"scena_{datum}.png", scena)
        elif klavesa == ord("f"): # f ulozi fotografii v plnem rozliseni a JPEG opet s datem a casem v nazvu
            print("Ukladam fotografii v plnem rozliseni")
            kamera.switch_mode(config_fotoaparat)
            kamera.capture_file(f"fotografie_{datum}.jpg")
            kamera.switch_mode(config_nahled)
        elif klavesa == ord("p"): # p priblizi snimek o 0.5 (max 10 kvuli narocne operaci)
            priblizeni += .5
            if priblizeni > 10:
                priblizeni = 10
        elif klavesa == ord("o"): # o oddali snimek o 0.5
            priblizeni -= .5
            if priblizeni < 1:
                priblizeni = 1
        elif klavesa == ord("t"): # t zobrazi/skryje textove popisky
            if popisky:
                popisky = False
            else:
                popisky = True


    kamera.stop()
    print("Ukončuji mikroskop...")
Diskuze (10) Další článek: Tajemné rádiové záblesky z vesmíru se opakují častěji, než jsme si mysleli. Je to stále záhada

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,