Mahrufa commited on
Commit
b3fff3b
·
1 Parent(s): c9a761c

Improve hosted upload-only CXR retrieval page

Browse files
Files changed (1) hide show
  1. app.py +150 -33
app.py CHANGED
@@ -1478,17 +1478,21 @@ def show_disclaimer() -> None:
1478
 
1479
  def show_startup_checks() -> None:
1480
  available_indexes = available_encoder_indexes()
 
1481
  if available_indexes:
1482
  st.sidebar.success("Local CXR index available.")
1483
  else:
1484
- st.sidebar.info("No local CXR index found. Upload-only mode is available.")
1485
 
1486
  if xrv is None:
1487
- st.sidebar.warning("TorchXRayVision is unavailable. Uploaded CXR embeddings fall back to ResNet18.")
 
 
1488
 
1489
  if cooler is None:
1490
- st.sidebar.info("cooler is unavailable. .cool/.mcool upload is disabled; .csv and .npy matrices still work.")
1491
-
 
1492
 
1493
  def sidebar_controls(image_paths: np.ndarray, labels: np.ndarray) -> tuple[int, str, int | None, object | None]:
1494
  st.sidebar.markdown("## CXR Retrieval Settings")
@@ -2602,11 +2606,11 @@ def show_retrieval_explanation_cards() -> None:
2602
 
2603
  def show_missing_index_message() -> None:
2604
  st.info(
2605
- "A matching saved CXR embedding index is not available for the current selection. "
2606
- "Upload-first mode is available below, or build a local index with `python build_index.py`."
 
2607
  )
2608
 
2609
-
2610
  def show_uploaded_gallery_results(
2611
  results: pd.DataFrame,
2612
  gallery_items: list[dict],
@@ -2668,9 +2672,9 @@ def show_uploaded_gallery_results(
2668
 
2669
 
2670
  def show_uploaded_retrieval_mode() -> None:
2671
- st.markdown('<span class="badge badge-prototype">Upload-only mode</span>', unsafe_allow_html=True)
2672
  show_missing_index_message()
2673
- st.caption("Upload a query image and a small reference gallery to build temporary in-session embeddings.")
2674
 
2675
  encoder_options = encoder_options_for_uploads()
2676
  selected_encoder_label = st.selectbox("Encoder for uploaded images", list(encoder_options.keys()))
@@ -2689,7 +2693,10 @@ def show_uploaded_retrieval_mode() -> None:
2689
  )
2690
 
2691
  if query_file is None or not gallery_files:
2692
- st.info("Upload one query image and at least one reference image to run temporary retrieval.")
 
 
 
2693
  return
2694
 
2695
  try:
@@ -2937,62 +2944,172 @@ def render_retrieval_dashboard(
2937
 
2938
 
2939
  def render_retrieval_tab(index_state: tuple[np.ndarray, np.ndarray, np.ndarray] | None) -> None:
2940
- st.markdown('<div class="section-title">CXR Retrieval Visualization</div>', unsafe_allow_html=True)
2941
- encoder_label_to_key = {str(config["label"]): encoder_name for encoder_name, config in ENCODER_CONFIGS.items()}
2942
- selected_encoder_label = st.sidebar.selectbox("CXR encoder index", list(encoder_label_to_key.keys()))
 
 
 
 
 
 
 
 
 
 
 
2943
  selected_encoder_key = encoder_label_to_key[selected_encoder_label]
2944
  selected_index_dir = encoder_index_dir(selected_encoder_key)
2945
 
 
 
 
2946
  if selected_index_dir is None:
2947
- st.warning(f"Index not found for this encoder. Run `python build_index.py --encoder {selected_encoder_key}`.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2948
  show_uploaded_retrieval_mode()
 
 
 
 
 
 
 
 
 
 
 
 
2949
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2950
 
2951
- st.markdown('<span class="badge badge-normal">Local CXR index available</span>', unsafe_allow_html=True)
2952
  try:
2953
  image_paths, labels, embeddings, metadata = load_index(str(selected_index_dir))
2954
  except (OSError, ValueError, json.JSONDecodeError) as error:
2955
  st.error(f"Could not load embeddings: {error}")
 
 
 
 
2956
  return
2957
 
2958
  if len(image_paths) == 0:
2959
- st.error("The embeddings index is empty. Add images to data/chest_xray and run python build_index.py.")
 
 
 
 
 
2960
  return
2961
 
2962
- encoder_key = canonical_encoder_name(str(metadata.get("encoder_key", selected_encoder_key)))
 
 
 
2963
  if encoder_key == "torchxrayvision_densenet121" and xrv is None:
2964
  st.warning(
2965
- "TorchXRayVision embeddings are selected, but torchxrayvision is not installed. "
2966
  "Dataset queries still work from saved embeddings; uploaded queries will fall back to ResNet18."
2967
  )
2968
- top_k, query_mode, selected_index, uploaded_file = sidebar_controls(image_paths, labels)
2969
- query = prepare_query(query_mode, selected_index, uploaded_file, image_paths, labels, embeddings, encoder_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2970
  if query is None:
2971
  return
2972
 
2973
  query_image, query_embedding, query_index, filename, query_label, query_source = query
 
2974
  if query_embedding.shape[0] != embeddings.shape[1]:
2975
  st.warning(
2976
  "Uploaded query embedding dimension does not match the selected saved index. "
2977
- "Select a matching index or rebuild embeddings for the desired encoder."
2978
  )
 
 
 
 
2979
  return
2980
 
2981
  render_retrieval_dashboard(
2982
- query_image,
2983
- query_embedding,
2984
- query_index,
2985
- filename,
2986
- query_label,
2987
- query_source,
2988
- embeddings,
2989
- image_paths,
2990
- labels,
2991
- top_k,
2992
- metadata,
2993
  )
2994
 
2995
-
2996
  def sr_image_options(image_paths: np.ndarray, labels: np.ndarray) -> list[str]:
2997
  return [f"{idx}: {Path(str(path)).name} ({labels[idx]})" for idx, path in enumerate(image_paths)]
2998
 
 
1478
 
1479
  def show_startup_checks() -> None:
1480
  available_indexes = available_encoder_indexes()
1481
+
1482
  if available_indexes:
1483
  st.sidebar.success("Local CXR index available.")
1484
  else:
1485
+ st.sidebar.info("Hosted demo: upload-only mode available.")
1486
 
1487
  if xrv is None:
1488
+ st.sidebar.warning(
1489
+ "TorchXRayVision is unavailable. Uploaded CXR embeddings can fall back to ResNet18."
1490
+ )
1491
 
1492
  if cooler is None:
1493
+ st.sidebar.info(
1494
+ "cooler is unavailable. CSV/NPY contact maps are supported; .cool/.mcool upload is disabled."
1495
+ )
1496
 
1497
  def sidebar_controls(image_paths: np.ndarray, labels: np.ndarray) -> tuple[int, str, int | None, object | None]:
1498
  st.sidebar.markdown("## CXR Retrieval Settings")
 
2606
 
2607
  def show_missing_index_message() -> None:
2608
  st.info(
2609
+ "Local CXR embeddings are not bundled with the hosted demo. "
2610
+ "Upload a query image and a small reference gallery below to run temporary retrieval. "
2611
+ "For full local-index benchmarks, run `build_index.py` locally."
2612
  )
2613
 
 
2614
  def show_uploaded_gallery_results(
2615
  results: pd.DataFrame,
2616
  gallery_items: list[dict],
 
2672
 
2673
 
2674
  def show_uploaded_retrieval_mode() -> None:
2675
+ st.markdown('<span class="badge badge-prototype">Upload-only retrieval workbench</span>', unsafe_allow_html=True)
2676
  show_missing_index_message()
2677
+ st.caption("Build a temporary reference gallery during this session. No dataset is stored or bundled in the hosted demo.")
2678
 
2679
  encoder_options = encoder_options_for_uploads()
2680
  selected_encoder_label = st.selectbox("Encoder for uploaded images", list(encoder_options.keys()))
 
2693
  )
2694
 
2695
  if query_file is None or not gallery_files:
2696
+ st.info(
2697
+ "Upload one query image and at least one reference image. "
2698
+ "The app will then show top-k visual matches, similarity scores, and a downloadable retrieval report."
2699
+ )
2700
  return
2701
 
2702
  try:
 
2944
 
2945
 
2946
  def render_retrieval_tab(index_state: tuple[np.ndarray, np.ndarray, np.ndarray] | None) -> None:
2947
+ st.markdown(
2948
+ '<h2 class="section-title">CXR Retrieval Visualization</h2>',
2949
+ unsafe_allow_html=True,
2950
+ )
2951
+
2952
+ encoder_label_to_key = {
2953
+ str(config["label"]): encoder_name
2954
+ for encoder_name, config in ENCODER_CONFIGS.items()
2955
+ }
2956
+
2957
+ selected_encoder_label = st.sidebar.selectbox(
2958
+ "CXR encoder index",
2959
+ list(encoder_label_to_key.keys()),
2960
+ )
2961
  selected_encoder_key = encoder_label_to_key[selected_encoder_label]
2962
  selected_index_dir = encoder_index_dir(selected_encoder_key)
2963
 
2964
+ # ------------------------------------------------------------------
2965
+ # Hosted / upload-only mode
2966
+ # ------------------------------------------------------------------
2967
  if selected_index_dir is None:
2968
+ st.markdown(
2969
+ """
2970
+ <div class="info-callout">
2971
+ <strong>Hosted upload-only mode</strong><br>
2972
+ This public demo does not bundle the full Kaggle CXR dataset or prebuilt embeddings.
2973
+ You can still test retrieval by uploading one query image and a small reference gallery.
2974
+ </div>
2975
+ """,
2976
+ unsafe_allow_html=True,
2977
+ )
2978
+
2979
+ st.markdown(
2980
+ """
2981
+ <div class="card">
2982
+ <h3>How to test this demo</h3>
2983
+ <p>
2984
+ Upload one biomedical image as the query, then upload several reference images
2985
+ as the temporary gallery. The app will compute embeddings during the session and
2986
+ retrieve the most visually similar gallery images.
2987
+ </p>
2988
+ <ul>
2989
+ <li><strong>For CXR retrieval:</strong> upload one chest X-ray query and several chest X-ray reference images.</li>
2990
+ <li><strong>For non-CXR images:</strong> use a custom gallery from the same image type.</li>
2991
+ <li><strong>For full benchmarks:</strong> run locally and build the CXR index with <code>build_index.py</code>.</li>
2992
+ </ul>
2993
+ </div>
2994
+ """,
2995
+ unsafe_allow_html=True,
2996
+ )
2997
+
2998
  show_uploaded_retrieval_mode()
2999
+
3000
+ st.markdown(
3001
+ """
3002
+ <div class="warning-callout">
3003
+ <strong>Important:</strong> Uploaded images are not classified by this app.
3004
+ Retrieval results are nearest-neighbor matches from the uploaded reference gallery or local index.
3005
+ They should not be interpreted as diagnosis.
3006
+ </div>
3007
+ """,
3008
+ unsafe_allow_html=True,
3009
+ )
3010
+
3011
  return
3012
+ #with st.expander("What is available in hosted mode?", expanded=True):
3013
+ # st.markdown(
3014
+ #"""
3015
+ #Hosted mode supports:
3016
+
3017
+ #- Uploading a query image
3018
+ #- Uploading a small reference gallery
3019
+ #- Temporary in-session embedding extraction
3020
+ #- Top-k visual similarity retrieval
3021
+ #- Similarity score inspection
3022
+ #- Super-resolution upload workflows
3023
+ - Synthetic and uploaded contact-map visualization
3024
+
3025
+ #Full CXR batch benchmarks and encoder comparison require local prebuilt indexes.
3026
+ #"""
3027
+ #)
3028
+
3029
+ #return
3030
+
3031
+ # ------------------------------------------------------------------
3032
+ # Local-index mode
3033
+ # ------------------------------------------------------------------
3034
+ st.markdown(
3035
+ '<span class="badge badge-normal">Local CXR index available</span>',
3036
+ unsafe_allow_html=True,
3037
+ )
3038
 
 
3039
  try:
3040
  image_paths, labels, embeddings, metadata = load_index(str(selected_index_dir))
3041
  except (OSError, ValueError, json.JSONDecodeError) as error:
3042
  st.error(f"Could not load embeddings: {error}")
3043
+ st.info(
3044
+ "You can still use upload-only mode below without local embeddings."
3045
+ )
3046
+ show_uploaded_retrieval_mode()
3047
  return
3048
 
3049
  if len(image_paths) == 0:
3050
+ st.error(
3051
+ "The embeddings index is empty. Add images to `data/chest_xray` "
3052
+ "and run `python build_index.py`."
3053
+ )
3054
+ st.info("Upload-only mode is still available below.")
3055
+ show_uploaded_retrieval_mode()
3056
  return
3057
 
3058
+ encoder_key = canonical_encoder_name(
3059
+ str(metadata.get("encoder_key", selected_encoder_key))
3060
+ )
3061
+
3062
  if encoder_key == "torchxrayvision_densenet121" and xrv is None:
3063
  st.warning(
3064
+ "TorchXRayVision embeddings are selected, but `torchxrayvision` is not installed. "
3065
  "Dataset queries still work from saved embeddings; uploaded queries will fall back to ResNet18."
3066
  )
3067
+
3068
+ top_k, query_mode, selected_index, uploaded_file = sidebar_controls(
3069
+ image_paths,
3070
+ labels,
3071
+ )
3072
+
3073
+ query = prepare_query(
3074
+ query_mode,
3075
+ selected_index,
3076
+ uploaded_file,
3077
+ image_paths,
3078
+ labels,
3079
+ embeddings,
3080
+ encoder_key,
3081
+ )
3082
+
3083
  if query is None:
3084
  return
3085
 
3086
  query_image, query_embedding, query_index, filename, query_label, query_source = query
3087
+
3088
  if query_embedding.shape[0] != embeddings.shape[1]:
3089
  st.warning(
3090
  "Uploaded query embedding dimension does not match the selected saved index. "
3091
+ "Use the matching encoder or use upload-only gallery retrieval."
3092
  )
3093
+
3094
+ with st.expander("Use upload-only gallery retrieval instead", expanded=True):
3095
+ show_uploaded_retrieval_mode()
3096
+
3097
  return
3098
 
3099
  render_retrieval_dashboard(
3100
+ query_image=query_image,
3101
+ query_embedding=query_embedding,
3102
+ query_index=query_index,
3103
+ filename=filename,
3104
+ query_label=query_label,
3105
+ query_source=query_source,
3106
+ embeddings=embeddings,
3107
+ image_paths=image_paths,
3108
+ labels=labels,
3109
+ top_k=top_k,
3110
+ metadata=metadata,
3111
  )
3112
 
 
3113
  def sr_image_options(image_paths: np.ndarray, labels: np.ndarray) -> list[str]:
3114
  return [f"{idx}: {Path(str(path)).name} ({labels[idx]})" for idx, path in enumerate(image_paths)]
3115