Skip to content
Merged

Dev #35

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
9aee523
feat(imports): [WIP] Added a PIPELINE_REGISTRY
Drdaag Jan 28, 2026
0983a01
feat(imports): [WIP] Added the decorator to all pipelines
Drdaag Jan 28, 2026
367c04b
feat: Added a first draft for the pyproject.toml
Drdaag Jan 28, 2026
c7c4210
feat: Added some optionnal dependencies (pipeline & dev)
Drdaag Jan 28, 2026
83ff84c
feat: [WIP] Linted && Added Scripts + Tools options
Drdaag Jan 28, 2026
c8107e6
chore: Removed the requirements.txt
Drdaag Jan 28, 2026
9df661f
Zip input format for cli + zip output format for batch processing.
MaxBA1602 Jan 29, 2026
a35e2df
Merge pull request #5 from DigitalHolography/4-pyproject-migrate-proj…
MaxBA1602 Jan 29, 2026
5567c68
Merge pull request #6 from DigitalHolography/pipeline_dev
MaxBA1602 Jan 29, 2026
66d1f35
feat: [WIP] Updated a first version of the README.md
Drdaag Jan 29, 2026
5c68df4
feat: Added some details and a TIP
Drdaag Jan 29, 2026
cb1943c
chore: Small change in look of the README.md
Drdaag Jan 29, 2026
32053c0
chore: Typo fix README.md
Drdaag Jan 29, 2026
6a8791e
feat: Add CI workflow for linting and testing
Drdaag Jan 29, 2026
dc17724
chore: Update CI workflow to remove dev branch from push
Drdaag Jan 29, 2026
bd191b3
chore: Modified the ci and renamed to ruff-lint-check workflow file
Drdaag Jan 30, 2026
511348b
chore: Add 'dev' branch to lint check workflow
Drdaag Jan 30, 2026
c7af884
feat: Added a linter script and moved folder
Drdaag Jan 30, 2026
88c537e
Merge branch 'dev' of github.com:DigitalHolography/AngioEye into dev
Drdaag Jan 30, 2026
af02968
Merge remote-tracking branch 'origin/dev' into improve-imports-global
Drdaag Jan 30, 2026
8b1dd4a
feat: Updated the pyproject.toml to add new lint (ruff) rules
Drdaag Jan 31, 2026
0754ff1
chore: Linted the whole project using the ruff config (and updated ty…
Drdaag Jan 31, 2026
155116a
Merge pull request #9 from DigitalHolography/3-typing-can-we-use-stan…
Drdaag Jan 31, 2026
7ea5b09
feat: Added the pre-commit config yaml and updated the pyproject
Drdaag Jan 31, 2026
39b3ca1
chore: Updated the README.md
Drdaag Jan 31, 2026
eeefb66
chore: Added some details on pre-commit
Drdaag Jan 31, 2026
5cc3e17
Merge pull request #11 from DigitalHolography/10-pre-commit-adding-a-…
Drdaag Jan 31, 2026
836bf91
chore: fixed a typo
Drdaag Jan 31, 2026
454c1fb
chore: fixed phrasing inside README
Drdaag Jan 31, 2026
8f4ba8f
Merge remote-tracking branch 'origin/dev' into improve-imports-global
Drdaag Jan 31, 2026
a370490
chore: Fixed some linting
Drdaag Jan 31, 2026
12edfe6
chore: Renamed "register_pipeline" -> "registerPipeline"
Drdaag Jan 31, 2026
8290bf7
chore: Updated the README.md with the registerPipeline
Drdaag Jan 31, 2026
16ecb56
chore: Fixed a missing import inside README.md example
Drdaag Jan 31, 2026
d5ed5d5
feat: Changed the registry to a dict
Drdaag Feb 1, 2026
a51fac1
feat: Added the PipelineDescriptor dataclass
Drdaag Feb 1, 2026
832eeac
Merge pull request #13 from DigitalHolography/12-pipeline-possible-da…
Drdaag Feb 1, 2026
7654d7a
Merge pull request #14 from DigitalHolography/improve-imports-global
MaxBA1602 Feb 2, 2026
3dd101a
First draft
Gabaali Feb 3, 2026
2cc1a17
arterial velocity add
Gabaali Feb 4, 2026
0aa89fc
centroid time, RI, RTVI
chloepaquet Feb 4, 2026
816b6a5
Remove dead code and outdated helper functions
MaxBA1602 Feb 4, 2026
cc0063c
lint
MaxBA1602 Feb 4, 2026
e2e8690
Merge pull request #17 from DigitalHolography/Remove_dead_code_and_ou…
MaxBA1602 Feb 4, 2026
bf2515b
Remove Single file tab
MaxBA1602 Feb 4, 2026
2a77e2f
Merge pull request #18 from DigitalHolography/remove_single_file_tab
MaxBA1602 Feb 4, 2026
ae312a6
remove intermediate output folders
MaxBA1602 Feb 4, 2026
bcde44f
Merge pull request #19 from DigitalHolography/remove_intermediate_out…
MaxBA1602 Feb 4, 2026
d158c87
fix typo in h5 output
MaxBA1602 Feb 4, 2026
2502562
TMI, RI,RVTI
chloepaquet Feb 4, 2026
3ef5742
lint
MaxBA1602 Feb 4, 2026
91ad496
Merge pull request #20 from DigitalHolography/dev
chloepaquet Feb 4, 2026
da0ed41
Merge pull request #21 from DigitalHolography/dev
Gabaali Feb 4, 2026
23958c0
metrics per branch pipeline added
Gabaali Feb 4, 2026
779e132
TMI, RI, RTVI for each branch
chloepaquet Feb 5, 2026
980b007
Core pulse-shape metrics from complex harmonics
Gabaali Feb 5, 2026
0b26af7
Tier-1 pulse waveform time metrics
Gabaali Feb 5, 2026
2517642
lint-tool passed
Gabaali Feb 5, 2026
d3e9903
first draft
MaxBA1602 Feb 6, 2026
6fefecd
new error message and export logs
MaxBA1602 Feb 6, 2026
9a0f6fe
lint
MaxBA1602 Feb 6, 2026
f6a5a64
Merge pull request #24 from DigitalHolography/Error_handler
MaxBA1602 Feb 6, 2026
84330f8
Merge pull request #25 from DigitalHolography/dev
chloepaquet Feb 6, 2026
da21597
Adding of the last relevant metrics (VTI metrics mainly)
Gabaali Feb 6, 2026
233fb5a
lint-tool
Gabaali Feb 6, 2026
6ecaca3
pulse waveform shape metrics
chloepaquet Feb 6, 2026
c38b356
Subfolders_in_h5_outputs
MaxBA1602 Feb 9, 2026
968e2b4
Merge pull request #26 from DigitalHolography/subfolders_in_h5_outputs
MaxBA1602 Feb 9, 2026
63337f7
Merge pull request #27 from DigitalHolography/dev
chloepaquet Feb 10, 2026
c24b5ba
Merge pull request #28 from DigitalHolography/dev
Gabaali Feb 10, 2026
b102ab4
clean version
Gabaali Feb 10, 2026
5655c59
new metrics
chloepaquet Feb 10, 2026
21f6b93
extrapolation of velocity profiles using sides of the profile
Gabaali Feb 10, 2026
ff6c071
Remove artefact and file attr + remove metrics subfolder
MaxBA1602 Feb 11, 2026
aa40b40
lint
MaxBA1602 Feb 11, 2026
157b041
Merge pull request #29 from DigitalHolography/new_h5_format_output
MaxBA1602 Feb 11, 2026
99db1e3
clean R_VTI tau_M1 and RI both global and bandlimited metrics pipelin…
Gabaali Feb 12, 2026
663b1d9
lint
MaxBA1602 Feb 12, 2026
2f0fd03
Merge pull request #22 from DigitalHolography/ArterialVelocity
MaxBA1602 Feb 12, 2026
36f6921
add new metrics RI, tau M1 and RTVI for each branch
chloepaquet Feb 12, 2026
0917e1d
Merge pull request #30 from DigitalHolography/dev
Gabaali Feb 13, 2026
6a062a6
new version : metrics R_VTI , RI, TauM1 for each branch
chloepaquet Feb 13, 2026
fe1a36a
Merge branch 'ArterialVelocity' into chloe
Gabaali Feb 13, 2026
defb0cf
Merge pull request #31 from DigitalHolography/chloe
Gabaali Feb 13, 2026
55433c6
metrics evalutaion on a factor 2 reduced sampling frequency with to …
Gabaali Feb 13, 2026
a216c55
segment reconstruction with factor 2
Gabaali Feb 16, 2026
a5d2b46
M0 and sqrt(M2/M0) temporal(U reshaped to 512*512) and spatial (Vt) m…
Gabaali Feb 16, 2026
603a063
deploy codex version of segmented metrics computing
Gabaali Feb 16, 2026
3db0396
fusion global and computed by segment of the metrics R_VTI RI and TauM1
Gabaali Feb 16, 2026
029a5f4
adding and correction of m2 over M0 spatial and temporal modes
Gabaali Feb 17, 2026
72c155d
rectification of global metrics computing on files missing velocity p…
Gabaali Feb 17, 2026
0cb69d5
veloctiy_waveform_shape_metrics adding PI
Gabaali Feb 17, 2026
829b0c4
Refactor waveform shape metrics for segments and globals
micatlan Feb 17, 2026
1e0673b
Merge pull request #33 from DigitalHolography/micatlan-patch-2
Gabaali Feb 18, 2026
80d1f90
clean version of metrics computation globally and by segment
Gabaali Feb 18, 2026
ca7d813
lint-tool
Gabaali Feb 18, 2026
5f53e17
Merge pull request #34 from DigitalHolography/ArterialVelocity
MaxBA1602 Feb 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ruff-lint-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Ruff Lint Check

on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev

jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install Ruff
run: |
pip install ruff

- name: Check Linting (Ruff)
run: ruff check .

- name: Check Formatting (Ruff Format)
run: ruff format --check .
16 changes: 16 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-added-large-files

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.14
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,135 @@
# AngioEye

AngioEye is the cohort-analysis engine for retinal Doppler holography. It browses EyeFlow .h5 outputs, reads per-segment metrics, applies QC, compares models, and aggregates results at eye/cohort level (including artery–vein summaries) to help design biomarkers. It exports clean CSV reports for stats, figures, and clinical models.

---

## Setup

### Prerequisites

- Python 3.10 or higher.
- It is highly recommended to use a virtual environment.

This project uses a `pyproject.toml` to describe all requirements needed. To start using it, **it is better to use a Python virtual environment (venv)**.

```sh
# Creates the venv
python -m venv .venv

# To enter the venv
# If you are using Windows PowerShell, you might need to activate the "Exceution" policy
./.venv/Scripts/activate
```

You can easily exit it with the command

```sh
deactivate
```

### 1. Basic Installation (User)

```sh
pip install -e .

# Installs pipeline-specific dependencies (optional)
pip install -e ".[pipelines]"
```

### 2. Development Setup (Contributor)

```sh
# Install all dependencies including dev tools (ruff, pre-commit, pyinstaller)
pip install -e ".[dev,pipelines]"

# Initialize pre-commit hooks (optionnal)
pre-commit install
```

> [!NOTE]
> The pre-commit is really usefull to run automatic checks before pushing code, reducing chances of ugly code being pushed.
>
> If a pre-commit hook fails, it will try to fix all needed files, **so you will need to add them again before recreating the commit**.

> [!TIP]
> You can run the linter easily, once the `dev` dependencies are installed, with the command:
>
> ```sh
> # To only run the checks
> lint-tool
>
> # To let the linter try to fix as much as possible
> lint-tool --fix
> ```

---

## Usage

Launch the main application to process files interactively:

### GUI

The GUI handles batch processing for folders, single .h5/.hdf5 files, or .zip archives and lets you run multiple pipelines at once. Batch outputs are written directly into the chosen output directory (one combined `.h5` per input file).

```sh
# Via the entry point
angioeye

# Or via the script
python src/angio_eye.py
```

### CLI

The CLI is designed for batch processing in headless environments or clusters.

```sh
# Via the entry point
angioeye-cli

# Or via the script
python src/cli.py
```

---

## Pipeline System

Pipelines are the heart of AngioEye. To add a new analysis, create a file in `src/pipelines/` with a class inheriting from `ProcessPipeline`.

To register it to the app, add the decorator `@register_pipeline`. You can define any needed imports inside, as well as some more info.

To see more complete examples, check out `src/pipelines/basic_stats.py` and `src/pipelines/dummy_heavy.py`.

### Simple Pipeline Structure

```python
from pipelines import ProcessPipeline, ProcessResult, registerPipeline

@registerPipeline(
name="My Analysis",
description="Calculates a custom clinical metric.",
required_deps=["torch>=2.2"],
)
class MyAnalysis(ProcessPipeline):
def run(self, h5file):
import torch
# 1. Read data using h5py
# 2. Perform calculations
# 3. Return metrics

metrics={"peak_flow": 12.5}

# Optional attributes applied to the pipeline group.
attrs = {
"pipeline_version": "1.0",
"author": "StaticExample"
}

return ProcessResult(
metrics=metrics,
attrs=attrs
)
```
80 changes: 58 additions & 22 deletions Viewer/viewer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from typing import Dict, List, Optional, Tuple, Union

import h5py
import numpy as np
Expand All @@ -14,11 +13,11 @@ def __init__(self) -> None:
super().__init__()
self.title("HDF5 Viewer")
self.geometry("1200x800")
self.h5_file: Optional[h5py.File] = None
self.current_dataset: Optional[h5py.Dataset] = None
self.current_dataset_path: Optional[str] = None
self.axis_label_to_index: Dict[str, int] = {}
self.slider_vars: Dict[int, Tuple[tk.IntVar, ttk.Label]] = {}
self.h5_file: h5py.File | None = None
self.current_dataset: h5py.Dataset | None = None
self.current_dataset_path: str | None = None
self.axis_label_to_index: dict[str, int] = {}
self.slider_vars: dict[int, tuple[tk.IntVar, ttk.Label]] = {}
self.colorbar = None

self._build_ui()
Expand Down Expand Up @@ -47,8 +46,12 @@ def _build_ui(self) -> None:
tree_frame.columnconfigure(0, weight=1)
tree_frame.rowconfigure(0, weight=1)

self.tree = ttk.Treeview(tree_frame, columns=("path",), show="tree", selectmode="browse")
tree_scroll = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
self.tree = ttk.Treeview(
tree_frame, columns=("path",), show="tree", selectmode="browse"
)
tree_scroll = ttk.Scrollbar(
tree_frame, orient="vertical", command=self.tree.yview
)
self.tree.configure(yscrollcommand=tree_scroll.set)
self.tree.grid(row=0, column=0, sticky="nsew")
tree_scroll.grid(row=0, column=1, sticky="ns")
Expand All @@ -65,17 +68,23 @@ def _build_ui(self) -> None:
self.y_axis_var = tk.StringVar()

ttk.Label(axis_frame, text="X axis").grid(row=0, column=0, sticky="w")
self.x_combo = ttk.Combobox(axis_frame, textvariable=self.x_axis_var, state="readonly", width=24)
self.x_combo = ttk.Combobox(
axis_frame, textvariable=self.x_axis_var, state="readonly", width=24
)
self.x_combo.grid(row=0, column=1, sticky="ew", padx=4, pady=2)

ttk.Label(axis_frame, text="Y axis").grid(row=1, column=0, sticky="w")
self.y_combo = ttk.Combobox(axis_frame, textvariable=self.y_axis_var, state="readonly", width=24)
self.y_combo = ttk.Combobox(
axis_frame, textvariable=self.y_axis_var, state="readonly", width=24
)
self.y_combo.grid(row=1, column=1, sticky="ew", padx=4, pady=2)

self.x_combo.bind("<<ComboboxSelected>>", self.on_axis_change)
self.y_combo.bind("<<ComboboxSelected>>", self.on_axis_change)

self.slider_frame = ttk.LabelFrame(sidebar, text="Other axes sliders", padding=8)
self.slider_frame = ttk.LabelFrame(
sidebar, text="Other axes sliders", padding=8
)
self.slider_frame.grid(row=4, column=0, sticky="nsew", pady=(8, 8))
self.slider_frame.columnconfigure(1, weight=1)

Expand All @@ -93,10 +102,14 @@ def _build_ui(self) -> None:
self.canvas.get_tk_widget().grid(row=1, column=0, sticky="nsew")

def _axis_label(self, axis: int) -> str:
size = self.current_dataset.shape[axis] if self.current_dataset is not None else "?"
size = (
self.current_dataset.shape[axis]
if self.current_dataset is not None
else "?"
)
return f"Dim {axis} (size {size})"

def _selected_axis(self, label: str) -> Optional[int]:
def _selected_axis(self, label: str) -> int | None:
if label == "(none)":
return None
return self.axis_label_to_index.get(label)
Expand Down Expand Up @@ -138,17 +151,32 @@ def _populate_tree(self) -> None:
self.tree.delete(*self.tree.get_children())
if self.h5_file is None:
return
root_id = self.tree.insert("", "end", text=os.path.basename(self.h5_file.filename), open=True, values=("/"))
root_id = self.tree.insert(
"",
"end",
text=os.path.basename(self.h5_file.filename),
open=True,
values=("/"),
)
self._add_tree_items(root_id, self.h5_file)

def _add_tree_items(self, parent: str, obj: h5py.Group) -> None:
for key, item in obj.items():
if isinstance(item, h5py.Group):
node_id = self.tree.insert(parent, "end", text=key, open=False, values=(item.name,), tags=("group",))
node_id = self.tree.insert(
parent,
"end",
text=key,
open=False,
values=(item.name,),
tags=("group",),
)
self._add_tree_items(node_id, item)
else:
label = f"{key} {item.shape}"
self.tree.insert(parent, "end", text=label, values=(item.name,), tags=("dataset",))
self.tree.insert(
parent, "end", text=label, values=(item.name,), tags=("dataset",)
)

def on_tree_select(self, _event: tk.Event) -> None:
item_id = self.tree.focus()
Expand All @@ -166,7 +194,9 @@ def load_dataset(self, path: str) -> None:
dataset = self.h5_file[path]
self.current_dataset = dataset
self.current_dataset_path = path
self.info_label.config(text=f"{path}\nshape={dataset.shape} dtype={dataset.dtype}")
self.info_label.config(
text=f"{path}\nshape={dataset.shape} dtype={dataset.dtype}"
)

dims = dataset.ndim
labels = [self._axis_label(i) for i in range(dims)]
Expand Down Expand Up @@ -215,7 +245,9 @@ def refresh_sliders(self) -> None:
if axis == x_axis or axis == y_axis:
continue
row = len(self.slider_vars)
ttk.Label(self.slider_frame, text=self._axis_label(axis)).grid(row=row, column=0, sticky="w")
ttk.Label(self.slider_frame, text=self._axis_label(axis)).grid(
row=row, column=0, sticky="w"
)
var = tk.IntVar(value=0)
scale = tk.Scale(
self.slider_frame,
Expand All @@ -231,7 +263,7 @@ def refresh_sliders(self) -> None:
value_label.grid(row=row, column=2, sticky="e")
self.slider_vars[axis] = (var, value_label)

def on_axis_change(self, _event: Optional[tk.Event] = None) -> None:
def on_axis_change(self, _event: tk.Event | None = None) -> None:
if self.current_dataset is None:
return
self.refresh_sliders()
Expand Down Expand Up @@ -262,7 +294,9 @@ def update_plot(self) -> None:
dims = ds.ndim
x_axis = self._selected_axis(self.x_axis_var.get())
y_axis = self._selected_axis(self.y_axis_var.get())
slider_indices = {axis: int(var.get()) for axis, (var, _) in self.slider_vars.items()}
slider_indices = {
axis: int(var.get()) for axis, (var, _) in self.slider_vars.items()
}

if dims == 0:
value = ds[()]
Expand All @@ -277,7 +311,7 @@ def update_plot(self) -> None:
if x_axis == y_axis:
self._show_placeholder("Choose two different axes")
return
slices: List[Union[int, slice]] = []
slices: list[int | slice] = []
for axis in range(dims):
if axis == x_axis or axis == y_axis:
slices.append(slice(None))
Expand All @@ -289,7 +323,9 @@ def update_plot(self) -> None:
self.ax.set_xlabel(self._axis_label(x_axis))
self.ax.set_ylabel(ds.name)
else:
kept_axes = [idx for idx, slc in enumerate(slices) if isinstance(slc, slice)]
kept_axes = [
idx for idx, slc in enumerate(slices) if isinstance(slc, slice)
]
order = [kept_axes.index(y_axis), kept_axes.index(x_axis)]
if order != [0, 1]:
data = np.transpose(data, order)
Expand Down
Loading