Skip to content

Commit 3e0838d

Browse files
Update marimo dashboard
1 parent 30e02b7 commit 3e0838d

2 files changed

Lines changed: 191 additions & 44 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
run: |
2424
python -m pip install marimo
2525
- name: Export notebook
26-
run: marimo export html-wasm marimo_dashboard.py -o wasm --mode edit
26+
run: marimo export html-wasm marimo_dashboard.py -o wasm --mode run
2727
- name: Upload artifact
2828
uses: actions/upload-pages-artifact@v3
2929
with:

‎marimo_dashboard.py‎

Lines changed: 190 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@
99
import os
1010
import zipfile
1111
import requests
12+
import copy
1213
from pathlib import Path
1314

1415
import marimo as mo
1516

1617
import numpy as np
1718
import skimage.io
19+
from skimage.measure import find_contours
20+
import plotly.express as px
21+
import plotly.graph_objects as go
22+
from plotly.subplots import make_subplots
1823

1924

2025
def _object_count(mask: np.ndarray) -> int:
@@ -49,14 +54,38 @@ def binary_iou_score(gtruth_mask: np.ndarray, submission_mask: np.ndarray) -> fl
4954
return intersection / union
5055

5156

57+
def scatters_from_mask(mask):
58+
_labs = np.unique(mask)
59+
_labs = _labs[_labs != 0]
60+
_n_labs = len(_labs)
61+
62+
_rng = np.random.default_rng(42)
63+
_colors = {i: f"rgb({r},{g},{b})" for i, (r,g,b) in zip(_labs, _rng.integers(0, 255, (_n_labs, 3)))}
64+
65+
_mask_scatters = []
66+
67+
for _lab in _labs:
68+
for _contour in find_contours(mask == _lab, 0.5):
69+
_mask_scatters.append(
70+
go.Scatter(
71+
x=_contour[:, 1],
72+
y=_contour[:, 0],
73+
mode="lines",
74+
line=dict(color=_colors[int(_lab)], width=2),
75+
showlegend=False,
76+
)
77+
)
78+
79+
return _mask_scatters
80+
81+
5282
public_folder_path = Path("./public")
5383
images_path = public_folder_path / "Images"
5484
ground_truths_path = public_folder_path / "Ground_Truths"
5585

5686
if "pyodide" in sys.modules:
5787
import micropip
5888
await micropip.install("tifffile==2025.5.10")
59-
await micropip.install("anybioimage")
6089

6190
if not public_folder_path.exists():
6291
# Download and unzip the public folder from the repository
@@ -79,7 +108,8 @@ def binary_iou_score(gtruth_mask: np.ndarray, submission_mask: np.ndarray) -> fl
79108
for gt_file in _gt_challenge_folder.glob("*.tif"):
80109
_gt_mask = skimage.io.imread(gt_file)
81110
break # There should only be one ground truth
82-
ground_truths[_challenge] = _gt_mask
111+
_gt_scatter = scatters_from_mask(_gt_mask)
112+
ground_truths[_challenge] = (_gt_mask, _gt_scatter)
83113

84114

85115
@app.cell(hide_code=True)
@@ -90,8 +120,8 @@ def _(challenges_dropdown):
90120

91121
@app.cell(hide_code=True)
92122
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,)
123+
gt_mask, gt_scatter = ground_truths[challenges_dropdown.value] #skimage.io.imread(ground_truths_path / challenges_dropdown.value / f"{challenges_dropdown.value}-gt.tif")
124+
return gt_mask, gt_scatter
95125

96126

97127
@app.cell(hide_code=True)
@@ -125,27 +155,11 @@ def _():
125155
return
126156

127157

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-
143158
@app.cell(hide_code=True)
144159
def _():
145160
challenges_dropdown = mo.ui.dropdown(challenges, value=challenges[0], label="Challenge")
146161

147162
mo.callout(mo.hstack([challenges_dropdown], justify="center"), kind="neutral")
148-
149163
return (challenges_dropdown,)
150164

151165

@@ -160,11 +174,12 @@ def _():
160174
@app.cell
161175
def _(file_drop):
162176
if file_drop.contents():
163-
# TODO: add a little spinner
164-
mask = skimage.io.imread(io.BytesIO(file_drop.contents()))
177+
with mo.status.spinner(title=f"Loading mask...") as _spinner:
178+
mask = skimage.io.imread(io.BytesIO(file_drop.contents()))
179+
mask_scatters = scatters_from_mask(mask)
165180
else:
166181
mask = None
167-
return (mask,)
182+
return mask, mask_scatters
168183

169184

170185
@app.cell
@@ -175,6 +190,21 @@ def _(gt_mask, mask):
175190
return (valid_mask,)
176191

177192

193+
@app.cell(hide_code=True)
194+
def _():
195+
mo.md(r"""
196+
## Evaluation
197+
198+
The quality of your submissions is evaluated based on two metrics:
199+
200+
- **Object count** relative to the ground truth count.
201+
- **Intersection over union (IoU)** computed on the binary version of the mask.
202+
203+
We also average both scores to give an overall score.
204+
""")
205+
return
206+
207+
178208
@app.cell
179209
def _(challenges_dropdown, gt_mask, mask, valid_mask):
180210
mo.stop(not valid_mask, mo.callout(f"The uploaded mask file is invalid for the selected challenge ({challenges_dropdown.value}).", kind="danger"))
@@ -185,42 +215,159 @@ def _(challenges_dropdown, gt_mask, mask, valid_mask):
185215

186216
overall_score = np.mean([oc_score, iou_score])
187217

188-
# TODO: make a lovely print:
189218
mo.vstack([
190-
oc_score, iou_score, overall_score
191-
])
219+
mo.md("Your Submission Scores"),
220+
mo.hstack([mo.md(
221+
f"""| Object count | IoU | Average |
222+
| ------ | ------ | ------ |
223+
| {oc_score*100:.0f} % | {iou_score*100:.0f} % | {overall_score*100:.0f} % |
224+
"""
225+
)]),
226+
], align="center",
227+
)
228+
return
229+
230+
231+
@app.cell(hide_code=True)
232+
def _():
233+
mo.md(r"""
234+
Here is a comparison between the ground truth mask and your submission:
235+
""")
192236
return
193237

194238

195239
@app.cell
196-
def _(gt_mask, image):
197-
from anybioimage import BioImageViewer
240+
def _(gt_scatter, image, mask):
241+
viewer_size = 500
242+
243+
base = px.imshow(
244+
image,
245+
height=viewer_size,
246+
color_continuous_scale="gray",
247+
contrast_rescaling="minmax",
248+
aspect="equal",
249+
)
198250

199-
viewer = BioImageViewer()
251+
fig = make_subplots(
252+
rows=1,
253+
cols=2,
254+
column_widths=[0.5, 0.5],
255+
horizontal_spacing=0.02,
256+
subplot_titles=("Ground truth", "Submission"),
257+
)
200258

201-
viewer.set_image(image)
259+
fig.update_annotations(
260+
font=dict(size=16, color="white")
261+
)
202262

203-
viewer.add_mask(
204-
gt_mask,
205-
name="Ground truth",
206-
opacity=0.5,
207-
contours_only=True,
263+
for tr in base.data:
264+
fig.add_trace(copy.deepcopy(tr), row=1, col=1)
265+
266+
for tr in gt_scatter:
267+
fig.add_trace(copy.deepcopy(tr), row=1, col=1)
268+
269+
for ax in ["xaxis", "xaxis2"]:
270+
fig.layout[ax].update(
271+
showticklabels=False,
272+
showgrid=False,
273+
visible=False,
274+
zeroline=False,
275+
)
276+
277+
for ax, xanchor in [("yaxis", "x"), ("yaxis2", "x2")]:
278+
fig.layout[ax].update(
279+
showticklabels=False,
280+
showgrid=False,
281+
visible=False,
282+
zeroline=False,
283+
scaleanchor=xanchor,
284+
autorange="reversed",
285+
)
286+
287+
fig.update_layout(
288+
coloraxis_showscale=False,
289+
margin=dict(l=0, r=0, b=30, t=30, pad=0),
290+
plot_bgcolor="black",
291+
paper_bgcolor="black",
292+
showlegend=False,
208293
)
209294

210-
mo.ui.anywidget(viewer)
211-
return (viewer,)
295+
mo.ui.plotly(fig) if not isinstance(mask, np.ndarray) else None
296+
return (viewer_size,)
212297

213298

214299
@app.cell
215-
def _(mask, valid_mask, viewer):
300+
def _(gt_scatter, image, mask_scatters, valid_mask, viewer_size):
216301
mo.stop(not valid_mask)
217302

218-
viewer.add_mask(
219-
mask,
220-
name="Submission",
221-
color="#ff0000",
222-
opacity=0.5,
303+
304+
base2 = px.imshow(
305+
image,
306+
height=viewer_size,
307+
color_continuous_scale="gray",
308+
contrast_rescaling="minmax",
309+
aspect="equal",
223310
)
311+
312+
fig2 = make_subplots(
313+
rows=1,
314+
cols=2,
315+
column_widths=[0.5, 0.5],
316+
horizontal_spacing=0.02,
317+
subplot_titles=("Ground truth", "Submission"),
318+
)
319+
320+
fig2.update_annotations(
321+
font=dict(size=16, color="white")
322+
)
323+
324+
for _tr in base2.data:
325+
fig2.add_trace(copy.deepcopy(_tr), row=1, col=1)
326+
327+
for _tr in gt_scatter:
328+
fig2.add_trace(copy.deepcopy(_tr), row=1, col=1)
329+
330+
331+
# Added this:
332+
for _tr in base2.data:
333+
fig2.add_trace(copy.deepcopy(_tr), row=1, col=2)
334+
335+
for _tr in mask_scatters:
336+
fig2.add_trace(copy.deepcopy(_tr), row=1, col=2)
337+
# ----------
338+
339+
for _ax in ["xaxis", "xaxis2"]:
340+
fig2.layout[_ax].update(
341+
showticklabels=False,
342+
showgrid=False,
343+
visible=False,
344+
zeroline=False,
345+
)
346+
347+
for _ax, _xanchor in [("yaxis", "x"), ("yaxis2", "x2")]:
348+
fig2.layout[_ax].update(
349+
showticklabels=False,
350+
showgrid=False,
351+
visible=False,
352+
zeroline=False,
353+
scaleanchor=_xanchor,
354+
autorange="reversed",
355+
)
356+
357+
fig2.update_layout(
358+
coloraxis_showscale=False,
359+
margin=dict(l=0, r=0, b=30, t=30, pad=0),
360+
plot_bgcolor="black",
361+
paper_bgcolor="black",
362+
showlegend=False,
363+
)
364+
365+
mo.ui.plotly(fig2)
366+
return
367+
368+
369+
@app.cell
370+
def _():
224371
return
225372

226373

0 commit comments

Comments
 (0)