skarugu commited on
Commit
e2a4bdf
Β·
verified Β·
1 Parent(s): e766e4e

Edited streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +130 -73
src/streamlit_app.py CHANGED
@@ -1107,20 +1107,20 @@ def build_mask_batch_zip(selected_names, artifacts: dict) -> bytes:
1107
  return buf.getvalue()
1108
 
1109
 
1110
- def _zip_png_mask(zf: zipfile.ZipFile, member: str) -> np.ndarray:
1111
- with zf.open(member) as f:
1112
- return (np.array(Image.open(f).convert("L")) > 0).astype(np.uint8)
1113
-
1114
-
1115
- def _pick_mask_member(files: dict, base_name: str):
1116
- for ext in (".png", ".tif", ".tiff"):
1117
- member = files.get(f"{base_name}{ext}")
1118
- if member:
1119
- return member
1120
- return None
1121
-
1122
-
1123
- def import_correction_zip(zip_file, artifacts: dict) -> tuple[int, list[str]]:
1124
  submitted = 0
1125
  errors = []
1126
  artifact_by_safe = {safe_zip_stem(name): name for name in artifacts}
@@ -1137,14 +1137,14 @@ def import_correction_zip(zip_file, artifacts: dict) -> tuple[int, list[str]]:
1137
 
1138
  # Preferred format: folders produced by build_mask_batch_zip.
1139
  folders = sorted({str(Path(m).parent).replace("\\", "/") for m in members if "/" in m})
1140
- for folder in folders:
1141
- if folder in ("", "."):
1142
- continue
1143
- files = {Path(m).name.lower(): m for m in members if str(Path(m).parent).replace("\\", "/") == folder}
1144
- nuc_member = _pick_mask_member(files, "nuclei_mask")
1145
- myo_member = _pick_mask_member(files, "myotube_mask")
1146
- if not (nuc_member and myo_member):
1147
- continue
1148
  source = artifact_by_safe.get(Path(folder).name)
1149
  meta_member = files.get("meta.json")
1150
  if meta_member:
@@ -1154,12 +1154,12 @@ def import_correction_zip(zip_file, artifacts: dict) -> tuple[int, list[str]]:
1154
  source = meta.get("source_image", source)
1155
  except Exception:
1156
  pass
1157
- if source not in artifacts:
1158
- errors.append(f"Skipped {folder}: source image is not in current analysis.")
1159
- continue
1160
- try:
1161
- nuc_arr = _zip_png_mask(zf, nuc_member)
1162
- myo_arr = _zip_png_mask(zf, myo_member)
1163
  orig_rgb = artifacts[source]["rgb_u8"]
1164
  add_to_queue(orig_rgb, nuc_mask=nuc_arr, myo_mask=myo_arr,
1165
  reason="user_correction_batch",
@@ -1212,6 +1212,79 @@ def import_correction_zip(zip_file, artifacts: dict) -> tuple[int, list[str]]:
1212
  # Model (architecture identical to training script)
1213
  # ─────────────────────────────────────────────────────────────────────────────
1214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1215
  class DoubleConv(nn.Module):
1216
  def __init__(self, in_ch, out_ch):
1217
  super().__init__()
@@ -1927,38 +2000,11 @@ if not private_mode and names:
1927
  if enable_al and not private_mode:
1928
  st.subheader("🧠 Submit corrected labels (Active Learning)")
1929
  st.caption(
1930
- "Download the predicted masks, correct them in ImageJ/FIJI or any "
1931
  "image editor (white = foreground, black = background), then upload "
1932
- "the corrected versions below."
1933
- )
1934
-
1935
- st.download_button(
1936
- "Download batch correction ZIP",
1937
- data=build_mask_batch_zip(names, st.session_state.artifacts),
1938
- file_name=f"myoseg_correction_batch_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip",
1939
- mime="application/zip",
1940
- key="download_correction_batch",
1941
- help="Includes one folder per image with image.png, nuclei_mask.png, myotube_mask.png, and meta.json.",
1942
  )
1943
 
1944
- batch_zip = st.file_uploader(
1945
- "Upload corrected batch ZIP",
1946
- type=["zip"],
1947
- key="batch_corr_zip",
1948
- help="Re-upload the ZIP after editing each folder's nuclei_mask.png and myotube_mask.png.",
1949
- )
1950
- if st.button("Submit batch corrections", type="secondary"):
1951
- if batch_zip is None:
1952
- st.error("Please upload a corrected ZIP batch first.")
1953
- else:
1954
- n_submitted, batch_errors = import_correction_zip(batch_zip, st.session_state.artifacts)
1955
- if n_submitted:
1956
- st.success(f"Saved {n_submitted} corrected image pair(s) to corrections/.")
1957
- if batch_errors:
1958
- st.warning("Some files were skipped:\n" + "\n".join(f"- {e}" for e in batch_errors[:10]))
1959
- if not n_submitted and not batch_errors:
1960
- st.error("No correction pairs were found in the ZIP.")
1961
-
1962
  al_pick = st.selectbox("Correct masks for image", names, key="al_pick")
1963
 
1964
  # Download buttons for current masks β€” lets users edit and re-upload
@@ -2004,27 +2050,38 @@ if enable_al and not private_mode:
2004
 
2005
  acol1, acol2 = st.columns(2)
2006
  with acol1:
2007
- corr_nuc = st.file_uploader("Upload corrected NUCLEI mask",
2008
- type=["png", "tif", "tiff"], key="nuc_corr")
 
 
 
 
 
2009
  with acol2:
2010
- corr_myo = st.file_uploader("Upload corrected MYOTUBE mask",
2011
- type=["png", "tif", "tiff"], key="myo_corr")
 
 
 
 
 
2012
 
2013
  if st.button("βœ… Submit corrections", type="primary"):
2014
- if corr_nuc is None or corr_myo is None:
2015
- st.error("Please upload BOTH a nuclei mask and a myotube mask.")
2016
  else:
2017
- orig_rgb = st.session_state.artifacts[al_pick]["rgb_u8"]
2018
- nuc_arr = (np.array(Image.open(corr_nuc).convert("L")) > 0).astype(np.uint8)
2019
- myo_arr = (np.array(Image.open(corr_myo).convert("L")) > 0).astype(np.uint8)
2020
- add_to_queue(orig_rgb, nuc_mask=nuc_arr, myo_mask=myo_arr,
2021
- reason="user_correction",
2022
- metadata={"source_image": al_pick,
2023
- "timestamp": datetime.now().isoformat()})
2024
- st.success(
2025
- f"βœ… Corrections for **{al_pick}** saved to `corrections/`. "
2026
- "The model will retrain at the next scheduled cycle."
2027
  )
 
 
 
 
 
 
 
 
 
2028
  elif enable_al and private_mode:
2029
  st.info(
2030
  "πŸ”’ Active learning and training submissions are disabled in Private mode. "
 
1107
  return buf.getvalue()
1108
 
1109
 
1110
+ def _zip_png_mask(zf: zipfile.ZipFile, member: str) -> np.ndarray:
1111
+ with zf.open(member) as f:
1112
+ return (np.array(Image.open(f).convert("L")) > 0).astype(np.uint8)
1113
+
1114
+
1115
+ def _pick_mask_member(files: dict, base_name: str):
1116
+ for ext in (".png", ".tif", ".tiff"):
1117
+ member = files.get(f"{base_name}{ext}")
1118
+ if member:
1119
+ return member
1120
+ return None
1121
+
1122
+
1123
+ def import_correction_zip(zip_file, artifacts: dict) -> tuple[int, list[str]]:
1124
  submitted = 0
1125
  errors = []
1126
  artifact_by_safe = {safe_zip_stem(name): name for name in artifacts}
 
1137
 
1138
  # Preferred format: folders produced by build_mask_batch_zip.
1139
  folders = sorted({str(Path(m).parent).replace("\\", "/") for m in members if "/" in m})
1140
+ for folder in folders:
1141
+ if folder in ("", "."):
1142
+ continue
1143
+ files = {Path(m).name.lower(): m for m in members if str(Path(m).parent).replace("\\", "/") == folder}
1144
+ nuc_member = _pick_mask_member(files, "nuclei_mask")
1145
+ myo_member = _pick_mask_member(files, "myotube_mask")
1146
+ if not (nuc_member and myo_member):
1147
+ continue
1148
  source = artifact_by_safe.get(Path(folder).name)
1149
  meta_member = files.get("meta.json")
1150
  if meta_member:
 
1154
  source = meta.get("source_image", source)
1155
  except Exception:
1156
  pass
1157
+ if source not in artifacts:
1158
+ errors.append(f"Skipped {folder}: source image is not in current analysis.")
1159
+ continue
1160
+ try:
1161
+ nuc_arr = _zip_png_mask(zf, nuc_member)
1162
+ myo_arr = _zip_png_mask(zf, myo_member)
1163
  orig_rgb = artifacts[source]["rgb_u8"]
1164
  add_to_queue(orig_rgb, nuc_mask=nuc_arr, myo_mask=myo_arr,
1165
  reason="user_correction_batch",
 
1212
  # Model (architecture identical to training script)
1213
  # ─────────────────────────────────────────────────────────────────────────────
1214
 
1215
+
1216
+ def correction_stem(upload_name: str) -> str:
1217
+ stem = Path(upload_name).stem
1218
+ lower = stem.lower()
1219
+ for suffix in (
1220
+ "_nuclei_mask", "-nuclei-mask", "_nuclei", "-nuclei", "_nuc", "-nuc",
1221
+ "_myotube_mask", "-myotube-mask", "_myotubes_mask", "-myotubes-mask",
1222
+ "_myotube", "-myotube", "_myotubes", "-myotubes", "_myo", "-myo",
1223
+ ):
1224
+ if lower.endswith(suffix):
1225
+ return stem[: -len(suffix)].strip()
1226
+ return stem.strip()
1227
+
1228
+
1229
+ def uploaded_mask_to_array(uploaded_file) -> np.ndarray:
1230
+ return (np.array(Image.open(uploaded_file).convert("L")) > 0).astype(np.uint8)
1231
+
1232
+
1233
+ def submit_uploaded_correction_pairs(nuc_files, myo_files, artifacts: dict, selected_name: str):
1234
+ nuc_files = nuc_files or []
1235
+ myo_files = myo_files or []
1236
+ if len(nuc_files) != len(myo_files):
1237
+ return 0, [f"Upload the same number of nuclei and myotube masks ({len(nuc_files)} nuclei, {len(myo_files)} myotube)."]
1238
+
1239
+ by_stem = {Path(name).stem: name for name in artifacts}
1240
+ by_safe = {safe_zip_stem(name): name for name in artifacts}
1241
+ nuc_by_stem = {correction_stem(f.name): f for f in nuc_files}
1242
+ myo_by_stem = {correction_stem(f.name): f for f in myo_files}
1243
+ common_stems = sorted(set(nuc_by_stem) & set(myo_by_stem))
1244
+
1245
+ if len(nuc_files) == len(myo_files) == 1 and not common_stems:
1246
+ common_stems = [selected_name]
1247
+ nuc_by_stem[selected_name] = nuc_files[0]
1248
+ myo_by_stem[selected_name] = myo_files[0]
1249
+
1250
+ submitted = 0
1251
+ errors = []
1252
+ missing_nuc = sorted(set(myo_by_stem) - set(nuc_by_stem))
1253
+ missing_myo = sorted(set(nuc_by_stem) - set(myo_by_stem))
1254
+ for stem in missing_nuc:
1255
+ errors.append(f"Skipped {stem}: no matching nuclei mask.")
1256
+ for stem in missing_myo:
1257
+ errors.append(f"Skipped {stem}: no matching myotube mask.")
1258
+
1259
+ for stem in common_stems:
1260
+ source = by_stem.get(stem) or by_safe.get(stem)
1261
+ if source is None and len(common_stems) == 1:
1262
+ source = selected_name
1263
+ if source not in artifacts:
1264
+ errors.append(f"Skipped {stem}: no matching analyzed image is loaded.")
1265
+ continue
1266
+
1267
+ try:
1268
+ nuc_arr = uploaded_mask_to_array(nuc_by_stem[stem])
1269
+ myo_arr = uploaded_mask_to_array(myo_by_stem[stem])
1270
+ add_to_queue(
1271
+ artifacts[source]["rgb_u8"],
1272
+ nuc_mask=nuc_arr,
1273
+ myo_mask=myo_arr,
1274
+ reason="user_correction_batch" if len(common_stems) > 1 else "user_correction",
1275
+ metadata={
1276
+ "source_image": source,
1277
+ "nuclei_file": nuc_by_stem[stem].name,
1278
+ "myotube_file": myo_by_stem[stem].name,
1279
+ "timestamp": datetime.now().isoformat(),
1280
+ },
1281
+ )
1282
+ submitted += 1
1283
+ except Exception as exc:
1284
+ errors.append(f"Skipped {stem}: {exc}")
1285
+
1286
+ return submitted, errors
1287
+
1288
  class DoubleConv(nn.Module):
1289
  def __init__(self, in_ch, out_ch):
1290
  super().__init__()
 
2000
  if enable_al and not private_mode:
2001
  st.subheader("🧠 Submit corrected labels (Active Learning)")
2002
  st.caption(
2003
+ "Download predicted masks, correct them in ImageJ/FIJI, napari, or any "
2004
  "image editor (white = foreground, black = background), then upload "
2005
+ "one corrected pair or many corrected pairs below."
 
 
 
 
 
 
 
 
 
2006
  )
2007
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2008
  al_pick = st.selectbox("Correct masks for image", names, key="al_pick")
2009
 
2010
  # Download buttons for current masks β€” lets users edit and re-upload
 
2050
 
2051
  acol1, acol2 = st.columns(2)
2052
  with acol1:
2053
+ corr_nuc = st.file_uploader(
2054
+ "Upload corrected NUCLEI mask(s)",
2055
+ type=["png", "tif", "tiff"],
2056
+ accept_multiple_files=True,
2057
+ key="nuc_corr",
2058
+ help="Upload one nuclei mask for the selected image, or select many nuclei masks for batch correction.",
2059
+ )
2060
  with acol2:
2061
+ corr_myo = st.file_uploader(
2062
+ "Upload corrected MYOTUBE mask(s)",
2063
+ type=["png", "tif", "tiff"],
2064
+ accept_multiple_files=True,
2065
+ key="myo_corr",
2066
+ help="Upload the matching myotube mask(s). Batch files are paired by filename stem.",
2067
+ )
2068
 
2069
  if st.button("βœ… Submit corrections", type="primary"):
2070
+ if not corr_nuc or not corr_myo:
2071
+ st.error("Please upload BOTH nuclei and myotube mask files.")
2072
  else:
2073
+ n_submitted, correction_errors = submit_uploaded_correction_pairs(
2074
+ corr_nuc, corr_myo, st.session_state.artifacts, al_pick
 
 
 
 
 
 
 
 
2075
  )
2076
+ if n_submitted == 1:
2077
+ st.success("Saved 1 corrected image pair to corrections/.")
2078
+ elif n_submitted > 1:
2079
+ st.success(f"Saved {n_submitted} corrected image pairs to corrections/.")
2080
+ if correction_errors:
2081
+ st.warning("Some files were skipped:\n" + "\n".join(f"- {e}" for e in correction_errors[:12]))
2082
+ if not n_submitted and not correction_errors:
2083
+ st.error("No matching correction pairs were found.")
2084
+
2085
  elif enable_al and private_mode:
2086
  st.info(
2087
  "πŸ”’ Active learning and training submissions are disabled in Private mode. "