| """Dataset E viewer — browse images with labels and filters.""" |
| import os, io, math, pyarrow.parquet as pq |
| from flask import Flask, send_file, request, render_template_string |
| from PIL import Image |
| from collections import Counter |
|
|
| ROOT = "/home/kccitadmin/jupyterlab/sandbox/data/E" |
| app = Flask(__name__) |
|
|
| |
| table = pq.read_table(os.path.join(ROOT, "data", "data-00000-of-00001.parquet")) |
| N = table.num_rows |
| DATA = { |
| "cancer": table.column("cancer_label").to_pylist(), |
| "grade": table.column("grade_label").to_pylist(), |
| "subclass": table.column("subclass_label").to_pylist(), |
| "source": table.column("source_dataset").to_pylist(), |
| "filename": table.column("original_filename").to_pylist(), |
| "pid": table.column("patient_id").to_pylist(), |
| "imaging": table.column("imaging_type").to_pylist(), |
| "img_paths": [v["path"] for v in table.column("image").to_pylist()], |
| } |
|
|
| LABELS = { |
| "cancer": {0: "non_cancer", 1: "cancer"}, |
| "grade": {0: "low_grade", 1: "high_grade", 2: "not_applicable"}, |
| "subclass": {0: "malignant", 1: "non_malignant", 2: "normal", 3: "landmark", 4: "foreign_body"}, |
| "imaging": {0: "WLI", 1: "NBI", 2: "BLC"}, |
| } |
|
|
| HTML = """ |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>Dataset E Viewer</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #e0e0e0; padding: 20px; } |
| h1 { color: #0ff; margin-bottom: 10px; } |
| .stats { background: #16213e; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; display: flex; gap: 20px; flex-wrap: wrap; } |
| .stats span { color: #0fa; } |
| .filters { background: #16213e; padding: 16px; border-radius: 8px; margin-bottom: 16px; display: flex; gap: 12px; flex-wrap: wrap; align-items: end; } |
| .filters label { font-size: 12px; color: #aaa; display: block; margin-bottom: 4px; } |
| .filters select { background: #0f3460; color: #e0e0e0; border: 1px solid #0fa; border-radius: 4px; padding: 6px 10px; font-size: 13px; } |
| .filters button { background: #0fa; color: #000; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; font-weight: bold; } |
| .filters button:hover { background: #0fc; } |
| .pagination { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; } |
| .pagination a { background: #0f3460; color: #0fa; padding: 6px 14px; border-radius: 4px; text-decoration: none; font-size: 13px; } |
| .pagination a:hover { background: #1a4a80; } |
| .pagination a.active { background: #0fa; color: #000; } |
| .pagination span { color: #888; font-size: 13px; } |
| .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; } |
| .card { background: #16213e; border-radius: 8px; overflow: hidden; cursor: pointer; transition: transform 0.15s; border: 2px solid transparent; } |
| .card:hover { transform: scale(1.03); border-color: #0fa; } |
| .card img { width: 100%; height: 180px; object-fit: cover; display: block; } |
| .card .info { padding: 8px; font-size: 11px; } |
| .card .info .src { font-weight: bold; color: #0fa; } |
| .card .info .pid { color: #888; } |
| .card .info .labels { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; } |
| .card .badge { padding: 2px 6px; border-radius: 3px; font-size: 10px; } |
| .badge.cancer { background: #e74c3c; color: #fff; } |
| .badge.non_cancer { background: #2ecc71; color: #000; } |
| .badge.grade { background: #3498db; color: #fff; } |
| .badge.na { background: #555; color: #ccc; } |
| .badge.subclass { background: #9b59b6; color: #fff; } |
| .badge.imaging { background: #f39c12; color: #000; } |
| .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 1000; justify-content: center; align-items: center; } |
| .modal-overlay.show { display: flex; } |
| .modal { background: #16213e; border-radius: 12px; max-width: 800px; max-height: 90vh; overflow: auto; padding: 20px; } |
| .modal img { max-width: 100%; max-height: 500px; border-radius: 8px; display: block; margin: 0 auto 16px; } |
| .modal .meta { font-size: 14px; line-height: 1.8; } |
| .modal .meta .row { display: flex; gap: 8px; } |
| .modal .meta .key { color: #888; min-width: 140px; } |
| .modal .meta .val { color: #0fa; } |
| .modal .close { position: absolute; top: 20px; right: 30px; font-size: 28px; color: #fff; cursor: pointer; } |
| </style> |
| </head> |
| <body> |
| <h1>Dataset E Viewer</h1> |
| <div class="stats"> |
| <span>Total: {{ total }}</span> |
| <span>Showing: {{ filtered_count }}</span> |
| <span>Page: {{ page }}/{{ pages }}</span> |
| <span>Cancer: {{ stats.cancer }} | Non-cancer: {{ stats.non_cancer }}</span> |
| <span>WLI: {{ stats.wli }} | NBI: {{ stats.nbi }} | BLC: {{ stats.blc }}</span> |
| </div> |
| |
| <form class="filters" method="GET"> |
| <div><label>Source</label><select name="source"><option value="">All</option>{% for s in sources %}<option {{ 'selected' if filters.source==s }}>{{ s }}</option>{% endfor %}</select></div> |
| <div><label>Cancer</label><select name="cancer"><option value="">All</option><option value="0" {{ 'selected' if filters.cancer=='0' }}>non_cancer</option><option value="1" {{ 'selected' if filters.cancer=='1' }}>cancer</option></select></div> |
| <div><label>Grade</label><select name="grade"><option value="">All</option><option value="0" {{ 'selected' if filters.grade=='0' }}>low_grade</option><option value="1" {{ 'selected' if filters.grade=='1' }}>high_grade</option><option value="2" {{ 'selected' if filters.grade=='2' }}>not_applicable</option></select></div> |
| <div><label>Subclass</label><select name="subclass"><option value="">All</option><option value="0" {{ 'selected' if filters.subclass=='0' }}>malignant</option><option value="1" {{ 'selected' if filters.subclass=='1' }}>non_malignant</option><option value="2" {{ 'selected' if filters.subclass=='2' }}>normal</option><option value="3" {{ 'selected' if filters.subclass=='3' }}>landmark</option><option value="4" {{ 'selected' if filters.subclass=='4' }}>foreign_body</option></select></div> |
| <div><label>Imaging</label><select name="imaging"><option value="">All</option><option value="0" {{ 'selected' if filters.imaging=='0' }}>WLI</option><option value="1" {{ 'selected' if filters.imaging=='1' }}>NBI</option><option value="2" {{ 'selected' if filters.imaging=='2' }}>BLC</option></select></div> |
| <div><label>Patient ID</label><input type="text" name="pid" value="{{ filters.pid }}" style="background:#0f3460;color:#e0e0e0;border:1px solid #0fa;border-radius:4px;padding:6px;width:80px;"></div> |
| <button type="submit">Filter</button> |
| <a href="/" style="background:#555;color:#fff;padding:8px 16px;border-radius:4px;text-decoration:none;">Reset</a> |
| </form> |
| |
| <div class="pagination"> |
| {% if page > 1 %}<a href="?{{ qs }}&page={{ page-1 }}">← Prev</a>{% endif %} |
| <span>Page {{ page }} of {{ pages }} ({{ filtered_count }} results)</span> |
| {% if page < pages %}<a href="?{{ qs }}&page={{ page+1 }}">Next →</a>{% endif %} |
| </div> |
| |
| <div class="grid"> |
| {% for i in indices %} |
| <div class="card" onclick="showModal({{ i }})"> |
| <img src="/img/{{ i }}/thumb" loading="lazy"> |
| <div class="info"> |
| <div><span class="src">{{ items[loop.index0].source }}</span> <span class="pid">pid={{ items[loop.index0].pid }}</span></div> |
| <div class="labels"> |
| <span class="badge {{ 'cancer' if items[loop.index0].cancer==1 else 'non_cancer' }}">{{ items[loop.index0].cancer_label }}</span> |
| <span class="badge {{ 'grade' if items[loop.index0].grade!=2 else 'na' }}">{{ items[loop.index0].grade_label }}</span> |
| <span class="badge subclass">{{ items[loop.index0].subclass_label }}</span> |
| <span class="badge imaging">{{ items[loop.index0].imaging_label }}</span> |
| </div> |
| <div style="color:#666;margin-top:2px;font-size:10px;">{{ items[loop.index0].filename }}</div> |
| </div> |
| </div> |
| {% endfor %} |
| </div> |
| |
| <div class="modal-overlay" id="modal" onclick="closeModal()"> |
| <div class="modal" onclick="event.stopPropagation()"> |
| <span class="close" onclick="closeModal()">×</span> |
| <img id="modal-img" src=""> |
| <div class="meta" id="modal-meta"></div> |
| </div> |
| </div> |
| |
| <script> |
| const allData = {{ all_data|tojson }}; |
| function showModal(i) { |
| const d = allData[i]; |
| document.getElementById('modal-img').src = '/img/' + i + '/full'; |
| let html = ''; |
| for (const [k, v] of Object.entries(d)) { |
| html += '<div class="row"><span class="key">' + k + '</span><span class="val">' + v + '</span></div>'; |
| } |
| document.getElementById('modal-meta').innerHTML = html; |
| document.getElementById('modal').classList.add('show'); |
| } |
| function closeModal() { document.getElementById('modal').classList.remove('show'); } |
| document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); }); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| PER_PAGE = 48 |
|
|
| @app.route("/") |
| def index(): |
| |
| f = { |
| "source": request.args.get("source", ""), |
| "cancer": request.args.get("cancer", ""), |
| "grade": request.args.get("grade", ""), |
| "subclass": request.args.get("subclass", ""), |
| "imaging": request.args.get("imaging", ""), |
| "pid": request.args.get("pid", ""), |
| } |
| page = max(1, int(request.args.get("page", 1))) |
|
|
| |
| indices = list(range(N)) |
| if f["source"]: |
| indices = [i for i in indices if DATA["source"][i] == f["source"]] |
| if f["cancer"] != "": |
| indices = [i for i in indices if DATA["cancer"][i] == int(f["cancer"])] |
| if f["grade"] != "": |
| indices = [i for i in indices if DATA["grade"][i] == int(f["grade"])] |
| if f["subclass"] != "": |
| indices = [i for i in indices if DATA["subclass"][i] == int(f["subclass"])] |
| if f["imaging"] != "": |
| indices = [i for i in indices if DATA["imaging"][i] == int(f["imaging"])] |
| if f["pid"]: |
| indices = [i for i in indices if str(DATA["pid"][i]) == f["pid"]] |
|
|
| filtered_count = len(indices) |
| pages = max(1, math.ceil(filtered_count / PER_PAGE)) |
| page = min(page, pages) |
| start = (page - 1) * PER_PAGE |
| page_indices = indices[start:start + PER_PAGE] |
|
|
| |
| items = [] |
| for i in page_indices: |
| items.append({ |
| "source": DATA["source"][i], |
| "pid": DATA["pid"][i], |
| "cancer": DATA["cancer"][i], |
| "cancer_label": LABELS["cancer"][DATA["cancer"][i]], |
| "grade": DATA["grade"][i], |
| "grade_label": LABELS["grade"][DATA["grade"][i]], |
| "subclass": DATA["subclass"][i], |
| "subclass_label": LABELS["subclass"][DATA["subclass"][i]], |
| "imaging": DATA["imaging"][i], |
| "imaging_label": LABELS["imaging"][DATA["imaging"][i]], |
| "filename": DATA["filename"][i], |
| }) |
|
|
| |
| all_data = {} |
| for idx, i in enumerate(page_indices): |
| all_data[str(i)] = { |
| "index": str(i), |
| "source": DATA["source"][i], |
| "patient_id": str(DATA["pid"][i]), |
| "cancer_label": LABELS["cancer"][DATA["cancer"][i]], |
| "grade_label": LABELS["grade"][DATA["grade"][i]], |
| "subclass_label": LABELS["subclass"][DATA["subclass"][i]], |
| "imaging_type": LABELS["imaging"][DATA["imaging"][i]], |
| "original_filename": DATA["filename"][i], |
| "image_path": DATA["img_paths"][i].split("data/E/")[-1], |
| } |
|
|
| |
| fc = Counter(DATA["cancer"][i] for i in indices) |
| fi = Counter(DATA["imaging"][i] for i in indices) |
|
|
| |
| qs_parts = [] |
| for k, v in f.items(): |
| if v: |
| qs_parts.append(f"{k}={v}") |
| qs = "&".join(qs_parts) |
|
|
| return render_template_string(HTML, |
| total=N, filtered_count=filtered_count, page=page, pages=pages, |
| indices=page_indices, items=items, all_data=all_data, |
| sources=["B", "C", "D"], filters=f, |
| stats={"cancer": fc[1], "non_cancer": fc[0], "wli": fi[0], "nbi": fi[1], "blc": fi[2]}, |
| qs=qs) |
|
|
| @app.route("/img/<int:i>/<size>") |
| def serve_img(i, size): |
| path = DATA["img_paths"][i] |
| img = Image.open(path) |
| if img.mode in ("RGBA", "P"): |
| img = img.convert("RGB") |
| if size == "thumb": |
| img.thumbnail((300, 300)) |
| else: |
| img.thumbnail((1000, 1000)) |
| buf = io.BytesIO() |
| img.save(buf, format="JPEG", quality=85) |
| buf.seek(0) |
| return send_file(buf, mimetype="image/jpeg") |
|
|
| if __name__ == "__main__": |
| print(f"Dataset E viewer: {N} images loaded") |
| print("Starting server at http://localhost:5173") |
| app.run(host="0.0.0.0", port=5173, debug=False) |
|
|