diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5632604 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +iris/data/ diff --git a/README.md b/README.md index 45d5744..7f2cc79 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ # Laboratorium Biometrii -To repozytorium pozwala zbudowac obraz Docker uzywany podczas cwiczenia z biometerii poczas laboratium Optoelektronika w diagnostyce i zabezpieczeniach obiektow. \ No newline at end of file +To repozytorium pozwala zbudowac obrazy Docker używane podczas ćwiczenia z biometerii podczas laboratium Optoelektronika w diagnostyce i zabezpieczeniach obiektów. + +## Rozpoznawanie tęczówki +Budowanie obrazu: +``` +docker build -t iris:latest ./iris +``` +Uruchomienie obrazu: +``` +docker run --rm -p 8888:8888 --mount type=bind,src=./iris/data,dst=/src/data iris:latest +``` \ No newline at end of file diff --git a/iris/Dockerfile b/iris/Dockerfile new file mode 100644 index 0000000..d150f01 --- /dev/null +++ b/iris/Dockerfile @@ -0,0 +1,27 @@ +FROM debian:13.3 + +# Install system dependencies +RUN apt-get update && apt-get install -y wget libgl1 libglib2.0-0 patchelf + +# Copy project files +COPY ./src /src + +# Create a non-root user +RUN useradd -m student +RUN chown -R student:student /src +USER student + +# Install uv +RUN wget -qO- https://astral.sh/uv/install.sh | sh +ENV PATH="/home/student/.local/bin:$PATH" + +# Create main env for Jupyter Notebook +WORKDIR /src +RUN uv venv --python 3.11 +ENV PATH="/src/.venv/bin:$PATH" +RUN uv pip install notebook open-iris +RUN patchelf --clear-execstack /src/.venv/lib/python3.11/site-packages/onnxruntime/capi/*.so + +# Launch Jupyter Notebook +EXPOSE 8888 +CMD ["python", "-m", "jupyter", "notebook", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--IdentityProvider.token=''"] \ No newline at end of file diff --git a/iris/Iris recognition inference system.pdf b/iris/Iris recognition inference system.pdf new file mode 100644 index 0000000..95b68a3 Binary files /dev/null and b/iris/Iris recognition inference system.pdf differ diff --git a/iris/notes.txt b/iris/notes.txt new file mode 100644 index 0000000..e69de29 diff --git a/iris/src/__init__.py b/iris/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iris/src/img/filtered.png b/iris/src/img/filtered.png new file mode 100644 index 0000000..b04729e Binary files /dev/null and b/iris/src/img/filtered.png differ diff --git a/iris/src/img/gabor.png b/iris/src/img/gabor.png new file mode 100644 index 0000000..61f7682 Binary files /dev/null and b/iris/src/img/gabor.png differ diff --git a/iris/src/img/geometry.png b/iris/src/img/geometry.png new file mode 100644 index 0000000..2601be0 Binary files /dev/null and b/iris/src/img/geometry.png differ diff --git a/iris/src/img/hamming.png b/iris/src/img/hamming.png new file mode 100644 index 0000000..eac0f05 Binary files /dev/null and b/iris/src/img/hamming.png differ diff --git a/iris/src/img/norm.png b/iris/src/img/norm.png new file mode 100644 index 0000000..53d8cd5 Binary files /dev/null and b/iris/src/img/norm.png differ diff --git a/iris/src/img/norm_explanation.png b/iris/src/img/norm_explanation.png new file mode 100644 index 0000000..7b92d24 Binary files /dev/null and b/iris/src/img/norm_explanation.png differ diff --git a/iris/src/img/sample.png b/iris/src/img/sample.png new file mode 100644 index 0000000..c365f4c Binary files /dev/null and b/iris/src/img/sample.png differ diff --git a/iris/src/img/segmentation.png b/iris/src/img/segmentation.png new file mode 100644 index 0000000..2210d1d Binary files /dev/null and b/iris/src/img/segmentation.png differ diff --git a/iris/src/main.ipynb b/iris/src/main.ipynb new file mode 100644 index 0000000..75be8bb --- /dev/null +++ b/iris/src/main.ipynb @@ -0,0 +1,330 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "70737cde", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "# Skrypt do laboratorium z biometrii tęczówki" + ] + }, + { + "cell_type": "markdown", + "id": "87df3026", + "metadata": {}, + "source": [ + "## Zadanie 1:\n", + "\n", + "W celach przyszłej analizy należy wykonać zdjęcia tęczówki w różnych warunkach.\n", + "\n", + "Domyślnie zdjęcia powinny być robione dla **prawego** oka, w podczerwieni, patrząc prosto w kamerę, bez włączonego oświetlacza i wykadrowane w następujący sposób:\n", + "\n", + "![](./img/sample.png)\n", + "\n", + "Należy wykonać następujące zdjęcia oraz odpowiednio je nazwać. Zdjęcia będą usunięte na koniec laboratorium.\n", + "W każdym podpunkcie można wykonać dowolną ilość zestawów zdjęć (w nazwie x zastąpić numerem zestawu), ale w jednym zestawie powinny być zdjęcia tego samego oka.\n", + "\n", + "* Co najmniej dwa zdjęcia oka każdego z członków grupy (w tym przypadku x oznacza numer zdjęcia tego samego oka):\n", + " nazwa pliku: `_normal_x.png`\n", + "* Oko w świetle widzialnym i podczerwonym\n", + " nazwa pliku: `visible_x.png`, `infrared_x.png`\n", + "* Oko w ciemości, przy normalnym oświetleniu i przy włączonym oświetlaczu\n", + " nazwa pliku: `dark_x.png`, `dim_x.png`, `bright_x.png`\n", + "* Oko przymróżone, standardowo otwarte i maksymalnie otwarte (powieki przytrzymane palcami)\n", + " nazwa pliku: `squint_x.png`, `open_x.png`, `fully_open_x.png`\n", + "* Oko patrzące na 5 różnych celów za kamerą.\n", + " nazwa pliku: `-45_x.png`, `-20_x.png`, `0_x.png`, `20_x.png`, `45_x.png`\n", + "* Oko w różnych odległościach od kamery: standardowa, pół-metra, metr\n", + " nazwa pliku: `20cm_x.png`, `50cm_x.png`, `1m_x.png`\n", + "* Zdjęcie lewego i prawego oka tej samej osoby:\n", + " nazwa pliku: `left_x.png`, `right_x.png`\n", + "* Dowolne inne zdjęcia oka:\n", + " nazwa pliku: `other_x.png`\n", + "\n", + "Wszystkie zdjęcia należy umieścić w folderze `./data/input/`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc47a2e9", + "metadata": {}, + "outputs": [], + "source": [ + "# Uruchom aby przeprocesować wszystkie zdjęcia\n", + "# Po dodaniu nowych zdjęć, uruchom ponownie ten skrypt, aby zaktualizować dane. Jedynie nowe i zmienione zdjęcia zostaną przetworzone, więc proces będzie szybki.\n", + "\n", + "from .process import process\n", + "output = process(\"data\")" + ] + }, + { + "cell_type": "markdown", + "id": "1459dd9e", + "metadata": {}, + "source": [ + "## Rozpoznawanie geometrii oka\n", + "Program używa sieci neuronowej aby przydzielić każdy z pikseli obrazu do następujących kategorii: `źrenica`, `tęczówka`, `oko`, `zakłócenia` (włosy, rzęsy itd.).\n", + "Przykładowy przydział został przedstawiony na zdjęciu poniżej. (Przydziały wszystkich zdjęć znajdują się w folderze `./data/segmentation`)\n", + "\n", + "![](./img/segmentation.png)\n", + "\n", + "Dane te użyte są następnie do rozpoznania geometrii oka na zdjęciu. Program znajduje najlepiej dopasowany okrąg bądź elipsę do kształtu pikseli z kategorii `źrenica` i `tęczówka`.\n", + "Za pomocą pikseli z kategorii `oko` program rozpoznaje orientację oka (poziomo/pionowo i wszystko pomiędzy) poprzez znalezienie w którym kierunku \"wydłużony\" jest kształt oka.\n", + "Przykładowa znaleziona geometria przedstawiona jest poniżej (a wszyskie pozostałe zdjęcia znajdują się w folderze `./data/geometry`):\n", + "\n", + "![](./img/geometry.png)\n", + "\n", + "Program potrafii również ocenić czy oko patrzy w kierunku kamery poprzez ocenę eliptyczności tęczówki.\n", + "Im tęczówka jest bliższa okręgu tym bardziej osoba bezpośrednio patrzy w kamerę, a im bardziej eliptyczna, tym bardziej na bok." + ] + }, + { + "cell_type": "markdown", + "id": "386ad177", + "metadata": {}, + "source": [ + "## Zadanie 2:\n", + "Na podstawie wykonanych zdjęć i wyników z `./data/geometry` oceńcie przy jakich warunkach program nie radzi sobie z rozpoznawaniem geometrii oka." + ] + }, + { + "cell_type": "markdown", + "id": "68d1f14d", + "metadata": {}, + "source": [ + "## Zadanie 3:\n", + "Zbadajcie jak kierunek patrzenia wpływa na wynik zwracany przez program.\n", + "Aby otrzymać wynik należy użyć poniższego skryptu uzupełniając uprzednio nazwę zdjęcia.\n", + "Zaproponujcie wartość progową dla której program powinien przyjąć że oko patrzy bezpośrednio w kierunku kamery.\n", + "Spróbujcie wykonać takie zdjęcie aby \"oszukać\" program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2456c1f9", + "metadata": {}, + "outputs": [], + "source": [ + "image_names = [\"-45_1.png\", \"-20_1.png\", \"0_1.png\", \"20_1.png\", \"45_1.png\"] # Tutaj dodaj nazwy zdjęć, które chcesz przetestować, np. \"other_1.png\"\n", + "# TODO: Return offgaze score from image" + ] + }, + { + "cell_type": "markdown", + "id": "505d2288", + "metadata": {}, + "source": [ + "## Normalizacja tęczówki\n", + "Na każdym zdjęciu tęczówka znajduje się w trochę innym miejscu, wzrok skierowany jest w innym kierunku, oraz oko jest przystosowane do innej ilości światła.\n", + "W celu porównania różnych zdjęć geometria tęczówki musi zostać znormalizowana.\n", + "Jest to wykonywane na podstawie geometrii znalezionej w poprzednim kroku. Tęczówka jest \"rozwijana\" z okręgu na linię prostą oraz skalowana do standardowych wymiarów.\n", + "Schemat działania tej operacji przedstawiono na rysunku:\n", + "\n", + "![](./img/norm_explanation.png)\n", + "\n", + "Przykładowe zdjęcie tak rozwiniętej tęczówki przedstawiono poniżej:\n", + "\n", + "![](./img/norm.png)\n", + "\n", + "W tym momencie zdjęcie ma już standardowe wymiary w pikselach, w naszym przypadku: 512x128 px." + ] + }, + { + "cell_type": "markdown", + "id": "25451080", + "metadata": {}, + "source": [ + "## Zadanie 4:\n", + "Następny skrypt losuje dwa zdjęcia oczu i rysuje oryginalne zdjęcie oraz znormalizowaną tęczówkę.\n", + "Zróbcie zrzut ekranu a następnie zaznaczcie w programie graficznym (np. gimp) jak tęczówka została rozwinięta.\n", + "Można to zrobić np. zaznaczając kilka punktów szczególnych na obu reprezentacjach." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a08aabfe", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Return two random images and their normalized irises" + ] + }, + { + "cell_type": "markdown", + "id": "90d1e6b5", + "metadata": {}, + "source": [ + "## Zadanie 5:\n", + "Tęczówka reaguje na poziom oświetlenia w otoczeniu tak aby do oka wpadała zbliżona ilość światła.\n", + "W tym celu tęczówka kurczy się albo rozluźnia - zmienia się jej szerokość.\n", + "Jest to jeden z powodów dla których przeprowadzana jest normalizacja.\n", + "Poniższy skrypt wyświetli znormalizowane zdjęcia tęczówki tego samego oka.\n", + "Spróbujcie ocenić które zdjęcie było wykonane przy najmniejszej ilości światła a które przy największej.\n", + "Po decyzji uruchomcie kolejny skrypt aby zweryfikować waszą decyzję.\n", + "W jaki sposób poziom oświetlenia może wpływać na jakość rozpoznawania tęczówki?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a71342a0", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Return normalized images with different lighting conditions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6fefb47", + "metadata": {}, + "outputs": [], + "source": [ + "dark = 0 # Wpisz numer zdjęcia odpowiadającemu jasności\n", + "normal = 0\n", + "bright = 0\n", + "# TODO: Verify" + ] + }, + { + "cell_type": "markdown", + "id": "81ecf2fe", + "metadata": {}, + "source": [ + "## Zadanie 6:\n", + "Poniższy skrypt wylosuje jedno znormalizowane zdjęcie tęczówki, oraz pokaże 5 innych zdjęć.\n", + "Spróbujcie dopasować tęczówkę do opisanych zdjęć. W programie graficznym zaznaczcie dlaczego wybraliście dane zdjęcie.\n", + "Po wybraniu zdjęcia możecie uruchomić następny skrypt aby zweryfikować wasz wybór." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "783c6763", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Return one random normalized iris and 5 other images with names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebeffd14", + "metadata": {}, + "outputs": [], + "source": [ + "selected = 0 # Wpisz numer wybranego zdjęcia (1-5) \n", + "# TODO: Verify" + ] + }, + { + "cell_type": "markdown", + "id": "a69fd482", + "metadata": {}, + "source": [ + "## Filtr Gabora\n", + "Ostatnim krokiem w przetwarzaniu obrazu tęczówki jest użycie tzw filtru Gabora.\n", + "Jest to specjalistyczny filtr to przetwarzania obrazów 2D, wykorzystywany do analizy tekstur.\n", + "W dużym uproszczeniu znajduje on wybraną częstotliwość na obrazie w postaci zmieniających się pasów oraz znajduje kierunek tych zmian.\n", + "\n", + "![](./img/gabor.png)\n", + "\n", + "Filtr ten jest użyty aby znaleźć punkty szczególne na obrazie a następnie jego wynik jest progowany i zamieniany na wartość 0 albo 1.\n", + "W ten sposób dla każdego filtru i oka uzyskuje się 512X128 bitów (x2 ze względu na część rzeczywistą i urojonę) które służą jako skompresowany opis danej tęczówki.\n", + "W tym przypadku używane są dwie różne częstotliwości. Cały obraz został w ten sposób skompresowany do 32KB danych które są łatwo porównywalne z innymi zdjęciami.\n", + "\n", + "Przykładowy wynik filtru i progowania został przedstawiony poniżej. Czerwone fragmenty to takie które zostały odrzucone ze względu na zakłócenia takie jak np. powieka czy rzęsy:\n", + "\n", + "![](./img/filtered.png)" + ] + }, + { + "cell_type": "markdown", + "id": "54fa38ce", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## Zadanie 7:\n", + "Poniższy skrypt wylosuje jeden wynik filtrowania, oraz pokaże 5 innych podpisanych wyników.\n", + "Spróbujcie dopasować tęczówkę do opisanych zdjęć. Do tego celu możecie np użyć warstw w programie graficznym.\n", + "Po wybraniu zdjęcia możecie uruchomić następny skrypt aby zweryfikować wasz wybór." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5afd9590", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Return one random iris template and 5 other templates with names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5063ad6b", + "metadata": {}, + "outputs": [], + "source": [ + "selected = 0 # Wpisz numer wybranego zdjęcia (1-5) \n", + "# TODO: Verify" + ] + }, + { + "cell_type": "markdown", + "id": "08d5d94d", + "metadata": {}, + "source": [ + "## Dopasowanie tęczówki\n", + "Program w celu dopasowania tęczówki porównuje wynik przetwarzania zdjęcia nowej tęczówki z uprzednio przetworzonymi zdjęciami zapisanymi w bazie.\n", + "Do tego celu wykorzystuje tzw. odległość Hamminga czyli proporcję ilości bitów nie zgodnych do ilości wszystkich bitów.\n", + "Ze względu na wybrany sposób przetwarzania obrazu, średnia proporcja dla dwóch różnych tęczówek będzie równa 0.5 ponieważ szansa na wartość piksela równą 0 albo 1 jest taka sama.\n", + "Natomiast dla zdjęć tej samej tęczówki odległość Hamminga będzie znacząco mniejsza.\n", + "Na poniższym rysunku przedstawiono rozkłady dla pasujących oraz niepasujących zdjęć dla 2.5 miliona par zdjęć.\n", + "\n", + "![](./img/hamming.png)" + ] + }, + { + "cell_type": "markdown", + "id": "049caf5e", + "metadata": {}, + "source": [ + "## Zadanie 8:\n", + "Używając poniższego skryptu porównaj różne pary zdjęć a następnie oszacuj jaka wartość odległości Hamminga mogła by służyć jako próg odróżniający pasujące i nie pasujące zdjęcia" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bbae8b2", + "metadata": {}, + "outputs": [], + "source": [ + "photo_a = \"other1.png\"\n", + "photo_b = \"other1.png\"\n", + "# TODO: Compare and return hamming distance" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/iris/src/process.py b/iris/src/process.py new file mode 100644 index 0000000..af49510 --- /dev/null +++ b/iris/src/process.py @@ -0,0 +1,60 @@ +import pathlib + +import cv2 +import matplotlib.pyplot as plt + +import iris + + +def process(path: str) -> None: + path = pathlib.Path(path) + + iris_pipeline = iris.IRISPipeline(env=iris.IRISPipeline.DEBUGGING_ENVIRONMENT) + iris_visualizer = iris.visualisation.IRISVisualizer() + + outputs = [] + for image_path in (path / "input").iterdir(): + if image_path.suffix != ".png": + continue + + print(f"Processing {image_path}...") + + eye_side = "right" if "left" not in image_path.stem else "left" + + image_pixels = cv2.imread(str(image_path), cv2.IMREAD_GRAYSCALE) + img = iris.IRImage( + img_data=image_pixels, + eye_side=eye_side, + ) + + output = iris_pipeline(image_pixels, eye_side=eye_side) + + iris_visualizer.plot_segmentation_map( + ir_image=img, + segmap=iris_pipeline.call_trace["segmentation"], + ) + (path / "segmentation").mkdir(parents=True, exist_ok=True) + plt.savefig(path / "segmentation" / f"{image_path.stem}.png") + + iris_visualizer.plot_all_geometry( + ir_image=img, + geometry_polygons=iris_pipeline.call_trace["geometry_estimation"], + eye_orientation=iris_pipeline.call_trace["eye_orientation"], + eye_center=iris_pipeline.call_trace["eye_center_estimation"], + ) + (path / "geometry").mkdir(parents=True, exist_ok=True) + plt.savefig(path / "geometry" / f"{image_path.stem}.png") + + iris_visualizer.plot_normalized_iris( + normalized_iris=iris_pipeline.call_trace["normalization"], + ) + (path / "normalized").mkdir(parents=True, exist_ok=True) + plt.savefig(path / "normalized" / f"{image_path.stem}.png") + + iris_visualizer.plot_iris_template( + iris_template=iris_pipeline.call_trace["encoder"], + ) + (path / "template").mkdir(parents=True, exist_ok=True) + plt.savefig(path / "template" / f"{image_path.stem}.png") + + outputs.append((image_path, img, output))