Spaces:
Sleeping
Sleeping
Improve hosted upload-only CXR retrieval page
Browse files
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("
|
| 1485 |
|
| 1486 |
if xrv is None:
|
| 1487 |
-
st.sidebar.warning(
|
|
|
|
|
|
|
| 1488 |
|
| 1489 |
if cooler is None:
|
| 1490 |
-
st.sidebar.info(
|
| 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 |
-
"
|
| 2606 |
-
"Upload
|
|
|
|
| 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
|
| 2672 |
show_missing_index_message()
|
| 2673 |
-
st.caption("
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 2941 |
-
|
| 2942 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2960 |
return
|
| 2961 |
|
| 2962 |
-
encoder_key = canonical_encoder_name(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2969 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 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 |
|