diff --git a/.gitignore b/.gitignore index eba74f4..4ea05a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -venv/ \ No newline at end of file +venv/ +__pycache__/ \ No newline at end of file diff --git a/app.py b/app.py index e07fd05..96172d0 100644 --- a/app.py +++ b/app.py @@ -47,7 +47,8 @@ st.set_page_config( page_title="IVUS合併症アノテーションツール", page_icon="🫀", - layout="wide" + layout="wide", + initial_sidebar_state="expanded" # Hide sidebar to prevent scrolling ) @@ -275,6 +276,10 @@ st.session_state.annotator ) + # Initialize zoom level (persistent across cases) + if 'zoom_percent' not in st.session_state: + st.session_state.zoom_percent = 80 # Default zoom level + def get_current_case_id() -> Optional[Union[int, float]]: """現在の症例IDを取得。""" @@ -284,69 +289,67 @@ return None -def render_header(): - """進捗情報を含むヘッダーを表示。""" - st.title("IVUS合併症アノテーションツール") - - total_cases = len(st.session_state.case_ids) - annotated_count = len(st.session_state.annotated_cases) - - col1, col2, col3, col4 = st.columns(4) - with col1: - st.metric("アノテーター", st.session_state.annotator) - with col2: - st.metric("全症例数", total_cases) - with col3: - st.metric("完了数", annotated_count) - with col4: - progress = annotated_count / total_cases if total_cases > 0 else 0 - st.metric("進捗", f"{progress:.1%}") - - -def render_case_selector(container): - """症例ナビゲーションを表示(右側に配置)。 - - Args: - container: Streamlit container to render in +def render_sidebar_ui(): """ - container.header("症例ナビゲーション") - - current_case_id = get_current_case_id() - - # Case selector dropdown - case_display = [ - f"症例 {cid}" + (" ✓" if cid in st.session_state.annotated_cases else "") - for cid in st.session_state.case_ids - ] - - selected_idx = container.selectbox( - "症例を選択", - range(len(st.session_state.case_ids)), - index=st.session_state.current_case_idx, - format_func=lambda i: case_display[i] - ) - - if selected_idx != st.session_state.current_case_idx: - st.session_state.current_case_idx = selected_idx - st.rerun() - - # Previous/Next buttons - col1, col2 = container.columns(2) - with col1: - if st.button("← 前へ", disabled=st.session_state.current_case_idx == 0): - st.session_state.current_case_idx -= 1 - st.rerun() - with col2: - if st.button( - "次へ →", - disabled=st.session_state.current_case_idx >= len(st.session_state.case_ids) - 1 - ): - st.session_state.current_case_idx += 1 + サイドバーに進捗、ナビゲーション、設定を集約。 + メイン画面の縦スペースを確保するために使用。 + """ + with st.sidebar: + st.header(f"担当: {st.session_state.annotator}") + + # --- 進捗状況 --- + total_cases = len(st.session_state.case_ids) + annotated_count = len(st.session_state.annotated_cases) + progress = annotated_count / total_cases if total_cases > 0 else 0 + st.progress(progress) + st.caption(f"進捗: {progress:.1%} ({annotated_count}/{total_cases})") + + st.markdown("---") + + # --- 症例ナビゲーション --- + st.subheader("症例選択") + current_case_id = get_current_case_id() + case_display = [ + f"症例 {cid}" + (" ✓" if cid in st.session_state.annotated_cases else "") + for cid in st.session_state.case_ids + ] + + selected_idx = st.selectbox( + "症例リスト", + range(len(st.session_state.case_ids)), + index=st.session_state.current_case_idx, + format_func=lambda i: case_display[i], + label_visibility="collapsed" + ) + + if selected_idx != st.session_state.current_case_idx: + st.session_state.current_case_idx = selected_idx st.rerun() - # Display current case info - if current_case_id: - container.info(f"**現在の症例:** {current_case_id}") + col1, col2 = st.columns(2) + with col1: + if st.button("← 前へ", disabled=st.session_state.current_case_idx == 0, use_container_width=True): + st.session_state.current_case_idx -= 1 + st.rerun() + with col2: + if st.button("次へ →", disabled=st.session_state.current_case_idx >= len(st.session_state.case_ids) - 1, use_container_width=True): + st.session_state.current_case_idx += 1 + st.rerun() + + st.markdown("---") + + # --- 画像拡大率 --- + st.subheader("表示設定") + zoom_percent = st.select_slider( + "画像拡大率", + options=[30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150], + value=st.session_state.zoom_percent, + format_func=lambda x: f"{x}%", + key="global_zoom" + ) + if zoom_percent != st.session_state.zoom_percent: + st.session_state.zoom_percent = zoom_percent + st.rerun() def render_image_viewer(case_id: Union[int, float], container): @@ -368,16 +371,6 @@ if frame_key not in st.session_state: st.session_state[frame_key] = 0 - # Image zoom slider (拡大率の調整) - zoom_percent = container.slider( - "画像拡大率 (%)", - min_value=30, - max_value=150, - value=80, # デフォルト値を少し縮小 - step=10, - key=f"zoom_{case_id}" - ) - # Image slider frame_idx = container.slider( "フレーム番号", @@ -392,12 +385,12 @@ if frame_idx != st.session_state[frame_key]: st.session_state[frame_key] = frame_idx - # Display image with custom width + # Display image with custom width based on global zoom level try: img = Image.open(images[frame_idx]) - # Calculate display width based on zoom percentage - display_width = int(DEFAULT_IMAGE_WIDTH * zoom_percent / 100) + # Calculate display width based on zoom percentage from session state + display_width = int(DEFAULT_IMAGE_WIDTH * st.session_state.zoom_percent / 100) container.image( img, @@ -426,55 +419,46 @@ def render_annotation_form(container, case_id: Union[int, float]) -> dict: - """ - アノテーション入力フォームを表示。 - - Args: - container: Streamlit container to render in - case_id: 現在の症例ID - - Returns: - アノテーションデータの辞書 - """ - container.markdown("---") - container.header("アノテーション") - - # Display annotator name (read-only) - container.text(f"アノテーター: {st.session_state.annotator}") - container.markdown("---") - - # Q1: Prediction + """アノテーション入力フォームを表示 - コンパクト版""" + + # Q1: Prediction (横並び) + container.markdown("**Q1: 合併症予測**") prediction = container.radio( - "Q1: 合併症予測 (No reflow/Slow flow)", + "Q1: 合併症予測", PREDICTION_OPTIONS, - key=f"prediction_{case_id}" + key=f"prediction_{case_id}", + horizontal=True, # 変更: 横並びにして省スペース化 + label_visibility="collapsed" ) - # Q2: Confidence (5段階、横並び) - container.markdown("**Q2: 確信度 (%)**") + # Q2: Confidence (横並び) + container.markdown("**Q2: 確信度**") confidence = container.radio( - "確信度を選択", + "確信度", CONFIDENCE_OPTIONS, - index=2, # デフォルトは50% + index=2, # 50% format_func=lambda x: f"{x}%", key=f"confidence_{case_id}", label_visibility="collapsed", - horizontal=True # 横並びに表示 + horizontal=True # 変更: 横並び ) # Q3: Reasons - container.markdown("**Q3: 判断根拠** (複数選択可)") + container.markdown("**Q3: 判断根拠**") reasons = [] + # チェックボックスは項目の長さによるため縦並びのまま for reason in REASONS_OPTIONS: if container.checkbox(reason, key=f"reason_{case_id}_{reason}"): reasons.append(reason) - # Q4: Free text comment + # Q4: Comment (高さを最小限に) + container.markdown("**Q4: 自由記述**") comment = container.text_area( - "Q4: 自由記述欄", + "Q4: 自由記述", value="", key=f"comment_{case_id}", - height=100 + height=68, # 変更: 高さを約2行分に縮小 + label_visibility="collapsed" ) return { @@ -573,7 +557,7 @@ st.info("アプリケーションを再起動して、データディレクトリパスを確認してください。") return - render_header() + render_sidebar_ui() current_case_id = get_current_case_id() @@ -599,9 +583,6 @@ # Right side: Navigation and Annotation with right_col: - # Navigation section - render_case_selector(right_col) - # Annotation form section annotation_data = render_annotation_form(right_col, current_case_id)