Skip to content

Commit bae6f7e

Browse files
Add marimo notebook (preliminary)
1 parent 0323bdf commit bae6f7e

12 files changed

Lines changed: 264 additions & 8 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Deploy Marimo Notebook to GitHub Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- '*'
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
- name: Setup Python
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: '3.13'
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install -r requirements.txt
25+
- name: Export notebook
26+
run: marimo export html-wasm marimo_dashboard.py -o wasm --mode edit
27+
- name: Upload artifact
28+
uses: actions/upload-pages-artifact@v3
29+
with:
30+
path: ./wasm
31+
32+
deploy:
33+
needs: build
34+
runs-on: ubuntu-latest
35+
environment:
36+
name: github-pages
37+
url: ${{ steps.deployment.outputs.page_url }}
38+
39+
if: github.ref == 'refs/heads/main'
40+
permissions:
41+
pages: write
42+
id-token: write
43+
44+
steps:
45+
- name: Deploy to GitHub Pages
46+
id: deployment
47+
uses: actions/deploy-pages@v4
48+
with:
49+
artifact_name: github-pages

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
__pycache__/
2+
__marimo__/
23
*.py[cod]
34
*$py.class
45
old/

‎README.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
# Segmentation Challenge
22

3-
The goal of this workshop is to let you practice your segmentation skills by developing automated segmentation workflows for a selection of challenge images.
3+
The goal of this workshop is to let you practice your segmentation skills by developing automated segmentation workflows for a selection of "challenge" images.
44

5-
You can use any tool you want (Fiji, Python, etc.) to generate your segmentation masks. The idea is to test and figure out which methods work best for each image.
6-
7-
Example tools: [Fiji](https://fiji.sc/), [MorpholibJ](https://imagej.net/plugins/morpholibj), [Scikit-image](https://scikit-image.org/), [CellPose](https://www.cellpose.org/), [SAM](https://github.com/facebookresearch/segment-anything), [Ilastik](https://www.ilastik.org/)...
5+
You can use any tool you want ([Fiji](https://fiji.sc/), [MorpholibJ](https://imagej.net/plugins/morpholibj), [Scikit-image](https://scikit-image.org/), [CellPose](https://www.cellpose.org/), [SAM](https://github.com/facebookresearch/segment-anything), [Ilastik](https://www.ilastik.org/)...) to generate your segmentation masks. The idea is to test and figure out which methods work best for each image.
86

97
## Images
108

@@ -22,13 +20,18 @@ The challenge images are located in the [`Images`](./Images/) folder.
2220

2321
## Ground truths
2422

25-
For each image, a segmentation mask representing the *ground truth* is available in the [`Ground_Truths`](./Ground_Truths/) folder. These ground truth masks have been carefully edited to be as close as possible to an ideal result.
23+
For each image, a *ground truth* segmentation mask is available in the [`Ground_Truths`](./Ground_Truths/) folder. These ground truth masks have been carefully edited to be as close as possible to an ideal result.
2624

2725
## Submissions
2826

2927
You can share your segmentation masks to see how they compare to the “ground truthˮ and other submissions.
3028

31-
Upload your segmentation mask files (e.g., in `TIFF` format) by drag and dropping them into the [`Submissions`](./Submissions/) folder of the shared Jupyter lab session provided by workshop organizers. The segmentation mask should be a labeled array with values representing instances. It should have the same size (same number of pixels in X and Y) as the original image.
29+
Upload your segmentation mask files (in **TIFF** format) by drag and dropping them into
30+
31+
- the Marimo thing I'll make (TODO) or
32+
- the [`Submissions`](./Submissions/) folder of the shared Jupyter lab session provided by workshop organizers.
33+
34+
The segmentation mask should be a **labeled array** with values representing instances. It should have the same size (same number of pixels in X and Y) as the original image.
3235

3336
## Evaluation
3437

@@ -43,7 +46,7 @@ These metrics are used to rank submissions in the **leaderboards** displayed on
4346

4447
## Solutions
4548

46-
A few reference solutions are available in the [Solutions](./Solutions/) folder. We encourage you to look at them *after* the workshop!
49+
A few reference solutions are available in the [Solutions](./Solutions/) folder. We encourage you to take a look at them *after* the workshop!
4750

4851
---
4952
## Setup (for workshop organizers)

‎marimo_dashboard.py‎

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import marimo
2+
3+
__generated_with = "0.23.8"
4+
app = marimo.App()
5+
6+
with app.setup:
7+
import io
8+
import sys
9+
import os
10+
import zipfile
11+
import requests
12+
from pathlib import Path
13+
14+
import marimo as mo
15+
16+
import numpy as np
17+
import skimage.io
18+
19+
20+
def _object_count(mask: np.ndarray) -> int:
21+
uniques = np.unique(mask)
22+
n_objects = len(uniques)
23+
if 0 in uniques: # 0 = background by convention; not counted
24+
n_objects -= 1
25+
26+
return n_objects
27+
28+
29+
def object_count_score(gtruth_mask: np.ndarray, submission_mask: np.ndarray) -> float:
30+
"""
31+
Returns a score in the range [0-1] based on the relative error on object count.
32+
"""
33+
n_gt = _object_count(gtruth_mask)
34+
n_sub = _object_count(submission_mask)
35+
count_score = np.abs(n_gt - n_sub) / n_gt
36+
37+
return 1 - count_score
38+
39+
40+
def binary_iou_score(gtruth_mask: np.ndarray, submission_mask: np.ndarray) -> float:
41+
"""
42+
Intersection over union between the binary ground truth and submitted mask.
43+
"""
44+
binary_gt = gtruth_mask > 0
45+
binary_sub = submission_mask > 0
46+
intersection = np.logical_and(binary_gt, binary_sub).sum()
47+
union = np.logical_or(binary_gt, binary_sub).sum()
48+
49+
return intersection / union
50+
51+
52+
public_folder_path = Path("./public")
53+
images_path = public_folder_path / "Images"
54+
ground_truths_path = public_folder_path / "Ground_Truths"
55+
56+
if "pyodide" in sys.modules:
57+
# Create the `public` folder
58+
if not public_folder_path.exists():
59+
os.mkdir(public_folder_path)
60+
61+
# Download and unzip the data from the repository
62+
zip_path = Path("public") / "data.zip"
63+
url = mo.notebook_location() / "public" / "data.zip"
64+
r = requests.get(str(url))
65+
r.raise_for_status()
66+
zip_path.write_bytes(r.content)
67+
with zipfile.ZipFile(zip_path, "r") as zf:
68+
zf.extractall(public_folder_path)
69+
zip_path.unlink(missing_ok=True) # Delete the zipped dataset
70+
71+
print(f"Extracted data in: {public_folder_path}")
72+
73+
challenges = [d.name for d in ground_truths_path.iterdir() if d.is_dir()]
74+
75+
# Read all ground truth masks
76+
ground_truths = {}
77+
for _challenge in challenges:
78+
_gt_challenge_folder = ground_truths_path / _challenge
79+
for gt_file in _gt_challenge_folder.glob("*.tif"):
80+
_gt_mask = skimage.io.imread(gt_file)
81+
break # There should only be one ground truth
82+
ground_truths[_challenge] = _gt_mask
83+
84+
85+
@app.cell(hide_code=True)
86+
def _(challenges_dropdown):
87+
image = skimage.io.imread(images_path / f"{challenges_dropdown.value}.tif")
88+
return
89+
90+
91+
@app.cell(hide_code=True)
92+
def _(challenges_dropdown):
93+
gt_mask = skimage.io.imread(ground_truths_path / challenges_dropdown.value / f"{challenges_dropdown.value}-gt.tif")
94+
return (gt_mask,)
95+
96+
97+
@app.cell(hide_code=True)
98+
def _():
99+
mo.md(r"""
100+
![logo](https://imaging.epfl.ch/resources/logo-for-gitlab.svg)
101+
""")
102+
return
103+
104+
105+
@app.cell(hide_code=True)
106+
def _():
107+
mo.hstack(
108+
[
109+
mo.md("# Segmentation Challenge"),
110+
mo.md("[➡️ GitHub repository](https://github.com/EPFL-Center-for-Imaging/segmentation-challenge)")
111+
]
112+
)
113+
return
114+
115+
116+
@app.cell(hide_code=True)
117+
def _():
118+
mo.md(r"""
119+
Evaluate your submissions. Your submitted files should be
120+
121+
- in TIFF format
122+
- a labeled array with values representing instances
123+
- it should have the same size (same number of pixels in X and Y) than the original image
124+
""")
125+
return
126+
127+
128+
@app.cell(hide_code=True)
129+
def _():
130+
mo.md(r"""
131+
## Evaluation
132+
133+
The quality of your submissions is evaluated based on two metrics:
134+
135+
- **Object count** relative to the ground truth count (`oc_score`).
136+
- **Intersection over union (IoU)** computed on the binary version of the mask (`iou_score`).
137+
138+
We also average both scores to give an overall score (`overall_score`).
139+
""")
140+
return
141+
142+
143+
@app.cell(hide_code=True)
144+
def _():
145+
challenges_dropdown = mo.ui.dropdown(challenges, value=challenges[0], label="Challenge")
146+
147+
challenges_dropdown
148+
return (challenges_dropdown,)
149+
150+
151+
@app.cell
152+
def _():
153+
file_drop = mo.ui.file(filetypes=[".tif"], kind="area", label="Drag and drop your submission file here (.tif)")
154+
155+
file_drop
156+
return (file_drop,)
157+
158+
159+
@app.cell
160+
def _(file_drop):
161+
if file_drop.contents():
162+
# TODO: add a little spinner
163+
mask = skimage.io.imread(io.BytesIO(file_drop.contents()))
164+
else:
165+
mask = None
166+
return (mask,)
167+
168+
169+
@app.cell
170+
def _(gt_mask, mask):
171+
mo.stop(not isinstance(mask, np.ndarray), mo.callout("Upload a mask file to evaluate it against the ground truth.", kind="info"))
172+
173+
valid_mask = gt_mask.shape == mask.shape
174+
return (valid_mask,)
175+
176+
177+
@app.cell
178+
def _(challenges_dropdown, gt_mask, mask, valid_mask):
179+
mo.stop(not valid_mask, mo.callout(f"The uploaded mask file is invalid for the selected challenge ({challenges_dropdown.value}).", kind="danger"))
180+
181+
oc_score = object_count_score(gt_mask, mask)
182+
183+
iou_score = binary_iou_score(gt_mask, mask)
184+
185+
overall_score = np.mean([oc_score, iou_score])
186+
187+
# TODO: make a lovely print:
188+
mo.vstack([
189+
oc_score, iou_score, overall_score
190+
])
191+
return
192+
193+
194+
@app.cell
195+
def _():
196+
# display_nicely(gt_mask, mask, image)
197+
return
198+
199+
200+
if __name__ == "__main__":
201+
app.run()
File renamed without changes.

0 commit comments

Comments
 (0)