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 )
92122def _ (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 )
144159def _ ():
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
161175def _ (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
179209def _ (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