Aplikace pro tribologickou laboratoř postavená v .NETu: automatická analýza smáčivosti povrchu — detekce kapek a počítání úhlů z fotek a videí.
“Jirko, neměl bys čas, náladu a chuť na aplikaci pro analýzu smáčivosti povrchu?” Vůbec jsem netušil, co to je…o to větší byla chuť. Co je smáčivost povrchu jsem tušil jen intuitivně, o to větší byla nálada se o tom něco dozvědět. Po krátké debatě o podmínkách spolupráce se našel i ten čas.
Práce pro tribologa
Tribologie (z řeckého τρίβω — třít) je vědecký obor, zabývající se procesy tření, opotřebení a mazání. Při vzájemném pohybu dvou nebo více materiálů, popřípadě při interakci materiálu s prostředím dochází ke ztrátě materiálu z povrchu, což je proces známý jako opotřebení. Wiki
Laicky řečeno, výzkumníci v tribologické laboratoři potřebují zkoumat přilnavost povrchu. Funguje to zhruba tak, že na povrch umístí kapku tekutiny a zkoumají, jak se bude chovat — jak moc a jak rychle se rozplizne. Když to hodně zjednoduším, čím kulatější kapka zůstane, tím je povrch odpudivější. Takové výzkumy pak slouží k technologickým inovacím v průmyslu — všude tam, kde se dochází ke tření. Typickým příkladem mohou být nejrůznější ložiska (nejen) ve spalovacích motorech. Udělat povrch daného materiálu více či měně odpudivým pochopitelně něco stojí, takže ne pro všechny aplikace a podmínky jsou takové úpravy ekonomické.
Zkoumaný povrch není jeden jediný, existuje jich spousta a liší se jednak použitým materiálem, jeho povrchovou úpravou a taky technologií povrchové úpravy. Popsané měření samozřejmě neprobíhá jednou, ale jsou jich stovky až tisíce, takže následné analýzy prováděné lidskou silou jsou časově náročné a tudíž drahé. Proto bylo žádoucí takové analýzy automatizovat.
Pokud takové měření chcete dělat, je žádoucí, aby bylo (pokud možno) opakovatelné. Takže nemůžete jen tak někde házet kapky. Potřebujete, aby kapky byly stejně velké (tj. aby bylo aplikováno stejné množství tekutiny), aby byly aplikovány stejně (tj. aby byly kapnuty ze stejné vzdálenosti od povrchu), aby byly umístěny stejně (tj. na stejné místo povrchu). Nechcete tedy kapku jednou vylít z flašky a příště z injekční stříkačky, nechcete kapky jednou pustit z metrové výšky a příště ji na povrch položit, nechcete kapku jednou umístit do středu povrchu a příště na jeho hranu.
Pzn. výše uvedené berte s nadsázkou, je to jen to málo, co jsem během řešení pochopil, možná to ani úplně (možná ani zdaleka) neodpovídá realitě — jsou to čistě subjektivní asociace, které mi slouží k tomu, abych aspoň trochu tušil, o co jde.
Celý tento experiment potřebujete nějak zaznamenat. Protože kapky jsou maličké, určitě je nebudete fotit mobilem, ale použijete mikroskop. Na kvalitu záznamu jsou kladeny obdobné požadavky jako na kapku samotnou (tj. chcete vždy snímat ze stejné vzdálenosti a pokud možno kolmo k povrchu). Kdo někdy trochu fotil, tak tuší, že při tak malých vzdálenostech hraje velkou roli ostření (tj. už nemůžete mít ostrý celý snímek, ale hloubka ostrosti dosahuje maximálně několika milimetrů, zbytek je neostrý). To celé chcete nejen fotit, ale pokud možno zaznamenat na video, aby bylo možné zkoumat smáčivost v závislosti na čase (tj. jak rychle se kapka rozplizne).
Proces měření se dá částečně automatizovat, ale to není úkol až tak pro vývojáře, ale spíše pro strojaře. Řeší se tedy různé přípravky na polohování povrchu, jehly pro aplikaci kapky, stabilizaci mikroskopu, ale také klimatické podmínky v laboratoři (tj. teplota, vlhkost, prašnost, vibrace,…).
Práce pro vývojáře
Z úvodu článku je patrné, že měření generuje poměrně velké množství dat (poměrně vzhledem k tomu, co a za jakou dobu je člověk schopen analyzovat ručně). Výsledkem analýzy (alespoň v této první verzi) jsou 4 hodnoty (dva úhly mezi kapkou a podložkou, jejich průměr a směrodatná odchylka) pro každý snímek plus nějaká metadata. Aplikace by teda měla být nic víc a nic míň, než černá skříňka, do které proudí záznamy z mikroskopu (ať už fotky nebo videa) a z druhé strany z ní vypadávají výsledky analýzy.
Co musí aplikace umět:
Vybrat soubory ke zpracování (fotky, videa)
Analyzovat — počítat úhly
Uložit výsledky
Po několika konzultacích jsme došli k pár dalším vylepšením
Automatický a manuální režim: V automatickém režimu nahrajete fotku a dál se o nic nestaráte, v ideálním případě dostanete výsledky analýzy. Občas se ale nezadaří a automatika selže, případně se netrefí úplně přesně. To se stane zejména v situacích, kdy jsou vstupní data defektní — typicky buď příliš neostré nebo zaznamenány z nevhodné vzdálenosti (tj. příliš blízko znamená málo informací pro extrakci kapky od povrchu, příliš daleko znamená mnoho nežádoucích artefaktů na pozadí, nebo v nevhodném úhlu vůči povrchu). V manuálním režimu má uživatel možnost vypočtené hodnoty interaktivně modifikovat, doladit.
Undo/Redo: Když už dochází k manuální úpravě, je vhodné dát uživateli možnost kroku zpět/vpřed — tedy ne kroku, ale kroků. Každý se někdy uklikne a udělá úpravu, kterou vlastně nechtěl.
ROI — rectangle of interest: Je velká škoda, když už proběhlo měření, které generuje správně ostré fotky, ale jsou pořízeny z příliš velké vzdálenosti. V takovém případě je škoda měření zahodit a provádět znovu. Proto vznikla možnost interaktivně z obrázku vybrat oblast ke zpracování. Zbytek fotky ležící mimo tuto oblast je pro účely analýzy ignorován.
Rotace: Někdy vznikne záznam otočený nebo záznam vzhůru nohama — to závisí na tom, jak je umístěn mikroskop ve stojanu. Jedna možnost byla komplikovat analýzu tak, aby si s otočeným videem poradila, nicméně jednodušší bylo přidat tlačítko rotace o +/- 90 stupňů — aplikaci ovládá uživatel a jeden klik navíc nikoho nezabije. Další možnost byla udělat automatickou detekci rotace, ale je asi jasné, že jádro aplikace leží někde jinde...
Ukládání a mazání: Přestože výsledkem analýzy jsou čísla (nebo tabulka čísel), ukázalo se jako vhodné mít možnost vybrané zpracované snímky uložit (pro dokumentační účely) nebo smazat (protože nejsou vhodné).
Zpracování videa: Typický záznam videa z mikroskopu má délku někde kolem 15–25 sekund. To dává smysl, výzkumník si připraví, co je potřeba, zapne záznam a pak teprve začne s aplikací kapky. Reálně se ale to zajímavé na videu odehraje během několika málo sekund (typicky 2–3s) a zpracovávat celé video je zbytečné — o data mimo zajímavý časový rámec nikdo nestojí (buď na nich kapka ještě není, nebo se už nijak nemění). Proto bylo vhodné dát uživateli možnost definovat, od kterého okamžiku je vhodné data analyzovat.
Mikroposun ve videu: Hledání počátečního okamžiku pro zpracování videa se na myši dá trefit celkem snadno (plus/mínus pár snímků). Občas je ale vhodné mnohem přesnější ovládání pozice ve videu — třeba když chcete najít ten nejlepsi snímek a ten si uložit. Na to se osvědčily tradiční tlačítka pro posun o jeden snímek vpřed/vzad.
Další spousty drobných vylepšení, třeba rychlá analalýza, který spočítá jen jeden snímek za sekundu a dá uživateli rychle informaci, v které sekundě záznamu se něco zajímavého začne dít.
Algoritmus a implementace
Pro jednoduchost se budeme bavit jen analýzou jednoho snímku. Zpracování videa je pak obdobné (video není nic jiného než sekvence snímků) — kontext mezi jednotlivými snímky videa ignorujeme (aspoň pro tuto verzi).
Vstup: Snímek kapky (případně videozáznam) Výstup: Dva úhly, průměr, směrodatná odchylka, čas a číslo snímku v sekvenci.
Postup
Načíst snímek z disku
Fáze předzpracování
Snímky z mikroskopu jsou sice hezké pro člověka, ale nejsou zrovna ideální pro počítačové zpracování)
Převzorkování (resizing/resampling): Snímky z mikroskopu mají rozlišení v řádu tisíců pixelů. Pro analýzu nám stačí snímek v menším rozlišení (velikost úhlů nezávisí na to, jestli mám 15Mpx snímek nebo jen 5MPx).
image
.Rotate(RotateValue, new Bgr(Color.Black), true)
.Resize(DetectorSettings.ResizeWidth, DetectorSettings.ResizeHeight, Inter.Cubic, true);
Definovat ROI (oblast zájmu): Nemusíme zpracovávat všechno, pokud uživatel nechce.
Stupně šedé (gray scaling): Převod do šedé škály. Jednak proto, že barevná informace je nám k ničemu (velikost úhlů nezávisí na tom, jestli je kapka červená, zelená nebo modrá), jednak je pak následná analýza jednodušší (každý pixel má jednu hodnotu a ne tři).
var grayImage = origImg.Convert<Gray, byte>();
Redukce šumu: Ani tak nejde o to, že by snímky byly nějak významně zašuměné, ale jak známo aplikací Gaussian Pyramid Decomposition oběma směry (tedy dolů a pak hned nahoru) se můžeme zbavit některých artefaktů v obraze. V principu snímek trochu rozostříme a pak zase zaostříme. S tímto jsem hodně experimentoval, v tomto konkrétním algoritmu se to ukázalo jako přínosné.
var pyrDown = new UMat();
CvInvoke.PyrDown(grayImage, pyrDown);
CvInvoke.PyrUp(pyrDown, grayImage);
Úrovně (thresholding): Aby snímek kapky na podložce mohl být dostatečně ostrý, je potřeba spousta světla. V důsledku toho je pozadí snímku skoro světlé (záměrně neříkám bílé) a podložka je tmavá (nechci říct černá). Kapka na snímku není ani světlá, ani tmavá — část je tmavá (tam kde kvůli lomu světla odráží podložka), část je světlá (tam kde se odráží pozadí), část je plná artefaktů (tam kde se odráží všechno ostatní). Jako vhodné se zde ukázalo provést thresholding, tj. převést snímek na binární (tj. každý pixel je buď černý, nebo bílý, nic mezi tím).
grayImage.ThresholdBinary(new Gray(maxValue / Math.Sqrt(2)), new Gray(maxValue));
Dilatace/Eroze: Po mnoha experimentech se zde ukázalo vhodné provést pár morfologických operací — konkrétně dilataci a následnou erozi. Tím odstraníme případný další šum, který buď ve snímku byl nebo vznikl jako důsledek binarizace (thresholdingu) v předchozím kroku.
CvInvoke.Dilate(grayImage, grayImage, null, new Point(-1, -1), 1, BorderType.Default,new MCvScalar(255, 255, 255));
CvInvoke.Erode(grayImage, grayImage, null, new Point(-1, -1), 1, BorderType.Default, new MCvScalar(255, 255, 255));
Zpracování obrazu
Teprve takto upravené snímky pustíme do analýzy.
Detekce hran: Celým účelem předzpracování snímku je připravit snímek tak, aby detekce hran proběhla s co nejlepšími výsledky. V ideálním případě (který nenastává zas tak často, viz kapitola o rozšířeních aplikace) bude výsledkem tohoto kroku jedna hrana začínající na levém okraji snímku, pokračující po podložce, přes kapku, vracející se zpět k podložce a končící na pravém okraji snímku. V praktických případech je hran násobně víc, někdy klidně desítky — nejvíc problémů zde způsobují hrany detekované uvnitř kapky jako důsledek mnoha a mnoha různých odrazů světla uvnitř kapky. Ať tak či onak, hrany musíme zpracovat všechny, protože v tomto kroku ještě nevíme, která je ta chtěná a která je matoucí.
CvInvoke.Canny(grayImage, edges, DetectorSettings.CannyThreshold, DetectorSettings.CannyThresholdLinking);
Zpracování hran. Hrana je nyní popsána seznamem bodů (pixelů). Hranu proložíme křivkou (polygonem), tím počet bodů výrazně redukujeme. Toto vypadá jednoduše, v reálu to ale znamenalo dlouhé bádání nad nastavením vhodných parametrů aproximace.
CvInvoke.FindContours(edges, contours, null, RetrType.List, ChainApproxMethod.ChainApproxSimple);
CvInvoke.ApproxPolyDP(contour, polyContour, lengthContour * DetectorSettings.ApproxPolyDpDivider, false);
Filtrace. Na základě hran jsme získali polygony, teď můžeme snadno odfiltrovat ty, které nás nezajímají. Jsou to například polygony celé ležící v rozích snímku (empiricky víme, že kapka leží plus minus uprostřed), polygony uzavřené samy do sebe (malé artefakty, které i přes provedený preprocesing ještě zbyly zejména uvnitř kapky), polygony ležící u hran snímku (podložka prochází skrz celý snímek zleva doprava vždy), polygony ležící u horního nebo spodního okraje (tady podložka ani kapka nikdy neleží), polygony ležící ve specifickém tvaru někde uprostřed (to je typicky aplikační jehla zachycená na snímku).
Elipsy. Předchozími kroky jsme se zbavili pokud možno maximálního počtu nechtěných hran. V hranách, které celý tento eliminační preprocesing přežily se pokusíme detekovat kapku a podložku. Základem je rozdělit hranu (polygon) na tři části — levou část podložky, kapku, pravou část podložky.
Tři části polygonu z předchozí části zredukujeme na dvě — část polygonu náležející podložce a část polygonu náležející kapce (zkrátka první a poslední část tvoří podložku, prostřední část je kapka). Rozdělení jednoho polygonu na tyto tři části je kritickým bodem celého algoritmu. Vyzkoušel jsem desítky různých přístupů jak bezpečně poznat, který bod ještě patří do podložky a který už do kapky. Nejlepší výsledky plynuly z nejjednoduššího postupu — jak podložka, tak kapka jsou vhodně kulaté (lépe řečeno šišaté), ale každá jinak a toho se dá krásně využít. Celé rozdělení polygonu na tyto části tedy spoléhá na konkávnost vs. konvexnost — v jednoduchosti je asi fakt krása.
Aproximace: Obě vzniklé části se pokusíme proložit elipsou (ne, aproximace kapky kružnicí se neukázala jako vhodná, přestože následné analytické výpočty by byly výrazně jednodušší).
Aproximace podložky elipsou většinou nepředstavuje problém. Je to zejména proto, že polygon aproximující podložku obsahuje dostatečné množství vhodně rozložených bodů.
Aproximace kapky elipsou je poněkud složitější problém. V zásadě vždy polygon kapky sestává z dostatečného množství bodů. Pokud jsou ale kapky příliš ploché, pak výškové souřadnice těchto bodů jsou si podobné (jinak řečeno směrodatná odchylka jejich výškových je příliš malá), tudíž algoritmus proložení elipsy někdy nevrací správné výsledky (ne že by byly špatné, ale — když to přeženu — pokud máte 20 bodů plus mínus na přímce, těžko říct, která proložená elipsa je ta správná). Tady se ukázalo jako přínosné (z hlediska poměru cena výkon) v určitých případech kdy polygon splňuje specifické vlastnosti, pár bodů do polygonu vhodně doplnit (v zásadě posunutý zrcadlový obraz kapky přes spojnici bodů dotyku). Zní to možná složitě, ale nejde o nic jiného než přidat pár virtuálních bodů pod podložku — tyto body slouží pouze jako “nápověda” pro aproximační algoritmus, v podstatě mu napovídá, kudy by proložená elipsa zhruba měla procházet.
ellipse = CvInvoke.FitEllipse(new VectorOfPoint(poly));
Analytická geometrie
Máme dvě elipsy. Jedna popisuje kapku, druhá podložku. Elipsa je v tomto případě definována svým středem (souřadnice středu), velikostí (šířka a výška) a rotací (úhel).
Počítáme tečny k elipse v bodě dotyku s podložkou. To není nic jiného než jsme počítali křídou na tabuli na střední škole — není to nic víc než manipulace se souřadnicemi.
Spočítáme ohniska elipsy. Ohniska elipsy leží na průsečíku horizontální osy elipsy a kružnice o poloměru poloviny šířky elipsy se středem ve ležícím na průsečíku elipsy se svou vertikální osou. Složitě řečeno, ale je to jen průsečík přímky a kružnice. Jo, toto je ta část, která by byla mírně jednodušší, kdyby elipsa byla kružnice — bylo by jen jedno ohnisko (střed kružnice).
public static void FindLineCircleIntersections(
float cx,
float cy,
float radius,
PointF point1,
PointF point2,
out PointF intersection1,
out PointF intersection2)
{
float t;
var dx = point2.X - point1.X;
var dy = point2.Y - point1.Y;
var a = dx * dx + dy * dy;
var b = 2 * (dx * (point1.X - cx) + dy * (point1.Y - cy));
var c = (point1.X - cx) * (point1.X - cx) + (point1.Y - cy) * (point1.Y - cy) - radius * radius;
var det = b * b - 4 * a * c;
if (a <= 0.0000001 || det < 0)
{
// No real solutions.
intersection1 = new PointF(float.NaN, float.NaN);
intersection2 = new PointF(float.NaN, float.NaN);
return;
}
if (Math.Abs(det) < 0.001)
{
// One solution.
t = -b / (2 * a);
intersection1 = new PointF(point1.X + t * dx, point1.Y + t * dy);
intersection2 = new PointF(float.NaN, float.NaN);
return;
}
// Two solutions.
t = (float) ((-b + Math.Sqrt(det)) / (2 * a));
intersection1 = new PointF(point1.X + t * dx, point1.Y + t * dy);
t = (float) ((-b - Math.Sqrt(det)) / (2 * a));
intersection2 = new PointF(point1.X + t * dx, point1.Y + t * dy);
}
Máme tedy elipsu, její ohniska (dva body) a potřebujeme spočítat tečny v bodě dotyku. Toto je druhé místo, kde by aproximace kružnicí byla jednodušší — tečna ke kružnici je kolmice k jejímu poloměru. Tečna k elipse je složitější — spočítá se jako kolmice k ose úhlu mezi levým ohniskem, bodem dotyku a pravým ohniskem. Napřed je teda potřeba spočítat osu úhlu, pak k ní vypočítat kolmici a tou je právě naše hledaná tečna. To stejné provedeme s druhým bodem dotyku, máme teda dvě tečny ke kapce.
Totéž jako v předchozím bodě provedeme pro elipsu popisující podložku. Algoritmus je úplně stejný, pouze vstupem je jiná elipsa. Výsledkem jsou teda další dvě tečny, celkem máme čtyři.
Zbývá vypočítat úhly — vezmeme jednu tečnu od kapky a jednu od podložky a spočteme úhel mezi dvěma přímkami. Totéž uděláme s druhou dvojicí tečen. Vypadnou nám dvě čísla (dvě velikosti úhlů).
Spočítat průměr — teoreticky by měly být oba úhly stejné, ale vlivem prostředí nejsou, proto je zprůměrujeme.
Spočítat směrodatnou odchylku. Ta nám říká něco, co se z průměru nedozvíme. Z praktického hlediska nám říká, jak moc špatně jsme úhlu změřili. Pokud je směrodatná odchylka malá, znamená to, že oba úhlu jsou skoro stejně velké. Pokud je velká, znamená to, že jeden z úhlů je výrazně větší, než druhý.
Hotovo.
Pzn. Všechno ostatní jsou jen drobnosti nijak neovlivňující pricip algoritmu.
Poznámky vývojáře
Mikroskop ani nic jiného v laboratoři není připojeno k internetu, často ani k lokální síti. Veškeré svoje plány s automatickou distribucí nových verzí aplikace jsem mohl rovnou pustit z hlavy. Stejně tak veškerý monitoring se smrskl na logování do souboru. Dalším nepěkným důsledkem je, že počítač ovládající mikroskop je z cca 5 let neaktualizovaný, tudíž ne vše na něm bylo tak, jako na vývojářské mašině.
Nikde jsem nepsal o tom, na čem je to postavené. GUI není podstatné (šáhl jsem po starých dobrých winformech). GUI není podstatné, že je to v zásadě jeden obrázek a pár tlačítek, přepsat ho pro web či cokoliv jiného není žádný problém.
Implementaci analýzy bych rozdělil do dvou oblastí. Jednou je zpracování obrazu, redukce barevného prostoru (škála šedé, binarizace), filtrace (pyramid, dilatace, eroze) a detekce hran (canny edges detektor). Na to jsem použil OpenCV, teda jeho port do .NETu EmguCV. Druhou částí je analytická geometrie a výpočty s tím spojené (průsečíky, kružnice, ohniska, osy, úhly, elipsy, stupně, radiány, vzdálenosti, rotace, …), tam jsem použil vlastní implementaci — jak jsem zmiňoval, je to relativně přímočará manipulace s vektory.
Bodů, úseček, kružnic a elips už bylo v obrázcích hodně, nezbývalo než ne odlišit barivčkama. Třeba náhodně:
public static Color GetRandomColor()
{
return Color.FromKnownColor(Names[RandomGen.Next(Names.Length)]);
}
Výpočet průměru je snadný, směrodatnou odchylku jsem potřeboval implementovat.
public static double CalculateStdDev(this IEnumerable<double> values)
{
double res = 0;
var array = values as double[] ?? values.ToArray();
if (!array.Any())
return res;
var avg = array.Average();
var sum = array.Sum(d => Math.Pow(d - avg, 2));
res = Math.Sqrt(sum / (array.Length - 1));
return res;
}
Další vývoj
Výsledky na dostupných datech jsou poměrně stabilní, aplikace tedy může do reálného provozu. Tam bude používána pro analýzu záznamů z experimentů.
Cílem je zjistit jak moc dobré/špatné výsledky analýza reálných dat produkuje. Něco se dá poladit kvalitou kapek, něco kvalitou záznamu a popsaný algoritmus má taky spoustu míst pro vylepšení a zdokonalení. Většina míst, u kterých tuším prostor pro vylepšení, ale vyžaduje udělat rozhodnutí — a ty bude dobré udělat až po zkušenostech z praktického provozu.
Pokud jde o rychlost výpočtu analýzy, nejsou na ni kladeny žádné zvláštní požadavky — fáze měření a následných analýz jsou odděleny fyzicky (měření probíhá v laboratoři, zpracování analýz v kancelářích). Realtime zpracování tady nic zajímavého nepřináší. Zpracování jednoho snímku v současné implementace na běžném notebooku (i5/8GB RAM) je hotové za časy kolem 0.1s. To je prozatím dostatečné.
Při zpracování videa občas nastane situace, kdy u ojedinělých snímků je kapka detekována mírně (o pár pixelů) jinde, než u předchozích/následujících snímků. To je způsobeno obvykle drobnými artefakty v záznamu pohybující se kapky. Tady se nabízí využít kontextové informace v záznamu a do detekce kapky zavést korekci.
Když už máme změřeny úhly, nabízí se v vzhledem k nějakému (na snímku zachycenému) měřítku odhadnout velikost kapky a její objem.
Dále pak může následovat celá řada dalších tribologických výpočtů, které je možné z analytických výstupů počítat. To už je pole fyziky.
Dává to smysl? :)