SmartHeal commited on
Commit
014af55
·
verified ·
1 Parent(s): bdd7e15

Update src/ui_components_original.py

Browse files
Files changed (1) hide show
  1. src/ui_components_original.py +465 -198
src/ui_components_original.py CHANGED
@@ -9,6 +9,8 @@ from datetime import datetime
9
  from PIL import Image
10
  import html
11
  from typing import Optional, Dict, Any
 
 
12
 
13
  # ---- Safe imports for local vs package execution ----
14
  try:
@@ -456,46 +458,57 @@ button.gr-button:hover, button.gr-button-primary:hover {
456
  }
457
 
458
  .status-warning {
459
- background: linear-gradient(135deg, #FFFAF0 0%, #FEEBC8 100%) !important;
460
- border: 2px solid #DD6B20 !important;
461
- color: #9C4221 !important;
462
  padding: 20px 24px !important;
463
  border-radius: 16px !important;
464
  font-weight: 600 !important;
465
  margin: 16px 0 !important;
466
- box-shadow: 0 8px 24px rgba(221, 107, 32, 0.2) !important;
467
  backdrop-filter: blur(10px) !important;
468
  }
469
 
470
- /* Image gallery styling */
471
  .image-gallery {
472
- display: grid;
473
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
474
- gap: 20px;
475
- margin: 20px 0;
476
  }
477
- .image-item { background: #f8f9fa; border-radius: 12px; padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); text-align: center; }
478
- .image-item img { max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
479
- .image-item h4 { margin: 15px 0 5px 0; color: #2d3748; font-weight: 600; }
480
- .image-item p { margin: 0; color: #666; font-size: 0.9em; }
481
-
482
- /* Analyze button */
483
- #analyze-btn {
484
- background: linear-gradient(135deg, #1B5CF3 0%, #1E3A8A 100%) !important;
485
- color: #FFFFFF !important;
486
- border: none !important;
487
- border-radius: 8px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  font-weight: 700 !important;
489
- padding: 14px 28px !important;
490
- font-size: 1.1rem !important;
491
- letter-spacing: 0.5px !important;
492
- text-align: center !important;
493
- transition: all 0.2s ease-in-out !important;
494
  }
495
- #analyze-btn:hover {
496
- background: linear-gradient(135deg, #174ea6 0%, #123b82 100%) !important;
497
- box-shadow: 0 4px 14px rgba(27, 95, 193, 0.4) !important;
498
- transform: translateY(-2px) !important;
 
 
499
  }
500
 
501
  /* Responsive */
@@ -675,13 +688,7 @@ button.gr-button:hover, button.gr-button-primary:hover {
675
  with gr.Column(scale=1):
676
  gr.HTML("<h3>📸 Wound Image</h3>")
677
  wound_image = gr.Image(label="Upload Wound Image", type="filepath")
678
- # Optional manual segmentation mask provided by the clinician. This allows clinicians
679
- # to manually mark the wound boundary after the automatic segmentation has run.
680
- manual_mask_input = gr.Image(
681
- label="Manual Segmentation Mask (optional)",
682
- type="filepath",
683
- interactive=True,
684
- )
685
  # Slider to adjust the automatic segmentation mask. Positive values dilate
686
  # (expand) the mask, negative values erode (shrink) it. The value represents
687
  # roughly percentage change where each 5 units corresponds to one iteration.
@@ -693,6 +700,7 @@ button.gr-button:hover, button.gr-button-primary:hover {
693
  label="Segmentation Adjustment",
694
  info="Adjust the automatic segmentation (negative shrinks, positive expands)"
695
  )
 
696
  gr.HTML("<h3>📝 Medical History</h3>")
697
  previous_treatment = gr.Textbox(label="Previous Treatment", lines=3)
698
  medical_history = gr.Textbox(label="Medical History", lines=3)
@@ -700,7 +708,43 @@ button.gr-button:hover, button.gr-button-primary:hover {
700
  allergies = gr.Textbox(label="Known Allergies", lines=2)
701
  additional_notes = gr.Textbox(label="Additional Notes", lines=3)
702
 
703
- analyze_btn = gr.Button("🔬 Analyze Wound", variant="primary", elem_id="analyze-btn")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  analysis_output = gr.HTML("")
705
 
706
  # ------------------- PATIENT HISTORY -------------------
@@ -792,213 +836,436 @@ button.gr-button:hover, button.gr-button-primary:hover {
792
  return {
793
  auth_panel: gr.update(visible=True),
794
  practitioner_panel: gr.update(visible=False),
795
- organization_panel: gr.update(visible=False)
 
 
 
 
796
  }
797
 
798
- def on_patient_mode_change(mode):
799
  return {
800
- new_patient_group: gr.update(visible=(mode == "New patient")),
801
- existing_patient_dd: gr.update(interactive=(mode == "Existing patient"))
802
  }
803
 
804
- def load_history():
 
 
 
 
 
 
 
 
 
 
 
805
  try:
806
- uid = int(self.current_user.get("id", 0) or 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
807
  if not uid:
808
  return "<div class='status-error'>❌ Please login first.</div>"
809
- rows = self.patient_history_manager.get_user_patient_history(uid) or []
810
- # inline images
811
- out = []
812
- for r in rows or []:
813
- r = dict(r)
814
- if r.get("image_url"):
815
- r["image_url"] = _to_data_url_if_local(r["image_url"])
816
- out.append(r)
817
- return self.patient_history_manager.format_history_for_display(out)
818
  except Exception as e:
819
- logging.error(f"load_history error: {e}")
820
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
821
 
822
- def do_search(name):
823
  try:
824
- uid = int(self.current_user.get("id", 0) or 0)
825
  if not uid:
826
  return "<div class='status-error'>❌ Please login first.</div>"
827
- if not (name or "").strip():
828
- return "<div class='status-warning'>⚠️ Enter a name to search.</div>"
829
- rows = self.patient_history_manager.search_patient_by_name(uid, name.strip()) or []
830
- out = []
831
- for r in rows or []:
832
- r = dict(r)
833
- if r.get("image_url"):
834
- r["image_url"] = _to_data_url_if_local(r["image_url"])
835
- out.append(r)
836
- return self.patient_history_manager.format_patient_data_for_display(out)
837
  except Exception as e:
838
- logging.error(f"search error: {e}")
839
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
840
 
841
- def view_details(existing_label):
842
  try:
843
- uid = int(self.current_user.get("id", 0) or 0)
844
  if not uid:
845
  return "<div class='status-error'>❌ Please login first.</div>"
846
- pid = _label_to_id(existing_label)
 
847
  if not pid:
848
- return "<div class='status-warning'>⚠️ Select a patient.</div>"
849
- rows = self.patient_history_manager.get_wound_progression_by_id(uid, pid) or []
850
- out = []
851
- for r in rows or []:
852
- r = dict(r)
853
- if r.get("image_url"):
854
- r["image_url"] = _to_data_url_if_local(r["image_url"])
855
- out.append(r)
856
- return self.patient_history_manager.format_patient_progress_for_display(out)
857
  except Exception as e:
858
- logging.error(f"view_details error: {e}")
859
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
860
 
861
- # ----------------------- wiring -----------------------
862
- signup_role.change(
863
- toggle_role_fields,
864
- inputs=[signup_role],
865
- outputs=[org_fields, prac_fields]
866
- )
867
-
868
  signup_btn.click(
869
  handle_signup,
870
- inputs=[signup_username, signup_email, signup_password, signup_name, signup_role,
871
- org_name, phone, country_code, department, location, organization_dropdown],
872
- outputs=[signup_status]
873
  )
874
-
875
  login_btn.click(
876
  handle_login,
877
- inputs=[login_username, login_password],
878
- outputs=[login_status, auth_panel, practitioner_panel, organization_panel,
879
- user_info, existing_patient_dd, view_details_dd]
880
  )
881
-
882
- logout_btn_prac.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
883
- logout_btn_org.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
884
-
885
- patient_mode.change(
886
- on_patient_mode_change,
887
- inputs=[patient_mode],
888
- outputs=[new_patient_group, existing_patient_dd]
889
  )
890
-
891
- # --- IMPORTANT: call standalone GPU function via lambda to pass instance/ctx ---
 
 
 
 
 
 
 
 
 
892
  analyze_btn.click(
893
- fn=lambda mode, ex_lbl, np_n, np_a, np_g, wl, wd, p, m, i, d, pt, mh, med, al, nt,
894
- img, manual_mask, seg_adj: \
895
- standalone_run_analysis(
896
- self, self.current_user, self.database_manager, self.wound_analyzer,
897
- mode, ex_lbl, np_n, np_a, np_g, wl, wd, p, m, i, d, pt, mh, med, al, nt,
898
- img, seg_adj, manual_mask
899
- ),
900
- inputs=[
901
- patient_mode, existing_patient_dd,
902
- new_patient_name, new_patient_age, new_patient_gender,
903
- wound_location, wound_duration, pain_level, moisture_level, infection_signs, diabetic_status,
904
- previous_treatment, medical_history, medications, allergies, additional_notes,
905
- wound_image, manual_mask_input, seg_adjust_slider
906
- ],
907
- outputs=[analysis_output]
908
  )
909
-
910
- history_btn.click(load_history, outputs=[patient_history_output])
911
- search_patient_btn.click(do_search, inputs=[search_patient_name], outputs=[specific_patient_output])
912
- view_details_btn.click(view_details, inputs=[view_details_dd], outputs=[view_details_output])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
 
914
  return app
915
 
916
- # ----------------------- formatting & risk logic -----------------------
917
- def _format_comprehensive_analysis_results(self, analysis_result, image_url=None, questionnaire_data=None):
918
- """Format comprehensive analysis results with all visualization images from AIProcessor."""
919
  try:
920
- success = analysis_result.get('success', False)
921
- if not success:
922
- error_msg = analysis_result.get('error', 'Unknown error')
923
- return f"<div class='status-error'>❌ Analysis failed: {error_msg}</div>"
924
-
925
- visual_analysis = analysis_result.get('visual_analysis', {})
926
- report = analysis_result.get('report', '')
927
- saved_image_path = analysis_result.get('saved_image_path', '')
928
-
929
- wound_type = visual_analysis.get('wound_type', 'Unknown')
930
- # New AI insights
931
- skin_tone_label = visual_analysis.get('skin_tone_label', 'Unknown')
932
- ita_deg = visual_analysis.get('ita_degrees', None)
933
- tissue_type = visual_analysis.get('tissue_type', 'Unknown')
934
- length_cm = visual_analysis.get('length_cm', 0)
935
- breadth_cm = visual_analysis.get('breadth_cm', 0)
936
- area_cm2 = visual_analysis.get('surface_area_cm2', 0)
937
- detection_confidence = visual_analysis.get('detection_confidence', 0)
938
-
939
- detection_image_path = visual_analysis.get('detection_image_path', '')
940
- segmentation_image_path = visual_analysis.get('segmentation_image_path', '')
941
- original_image_path = visual_analysis.get('original_image_path', '')
942
-
943
- original_image_base64 = None
944
- detection_image_base64 = None
945
- segmentation_image_base64 = None
946
-
947
- if image_url and os.path.exists(image_url):
948
- original_image_base64 = self.image_to_base64(image_url)
949
- elif original_image_path and os.path.exists(original_image_path):
950
- original_image_base64 = self.image_to_base64(original_image_path)
951
- elif saved_image_path and os.path.exists(saved_image_path):
952
- original_image_base64 = self.image_to_base64(saved_image_path)
953
-
954
- if detection_image_path and os.path.exists(detection_image_path):
955
- detection_image_base64 = self.image_to_base64(detection_image_path)
956
-
957
- if segmentation_image_path and os.path.exists(segmentation_image_path):
958
- segmentation_image_base64 = self.image_to_base64(segmentation_image_path)
959
-
960
  risk_assessment = self._generate_risk_assessment(questionnaire_data)
961
- risk_level = risk_assessment['risk_level']
962
- risk_score = risk_assessment['risk_score']
963
- risk_factors = risk_assessment['risk_factors']
964
-
965
- risk_class = "low"
966
- if risk_level.lower() == "moderate":
967
- risk_class = "moderate"
968
- elif risk_level.lower() in ["high", "very high"]:
969
- risk_class = "high"
970
-
971
- risk_factors_html = "<ul>" + "".join(f"<li>{factor}</li>" for factor in risk_factors) + "</ul>" if risk_factors else "<p>No specific risk factors identified.</p>"
972
-
973
- image_gallery_html = ""
974
- if original_image_base64 or detection_image_base64 or segmentation_image_base64:
975
- image_gallery_html = '<div class="image-gallery">'
976
- if original_image_base64:
977
- image_gallery_html += f'''
 
 
 
 
 
978
  <div class="image-item">
979
- <img src="{original_image_base64}" alt="Original Wound Image">
980
- <h4>📸 Original Wound Image</h4>
981
- <p>Uploaded image for analysis</p>
982
  </div>
983
- '''
984
- if detection_image_base64:
985
- image_gallery_html += f'''
 
 
 
 
 
986
  <div class="image-item">
987
- <img src="{detection_image_base64}" alt="Wound Detection">
988
  <h4>🎯 Wound Detection</h4>
989
- <p>AI-detected wound boundaries</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
990
  </div>
991
- '''
992
- if segmentation_image_base64:
993
- image_gallery_html += f'''
 
 
 
 
 
994
  <div class="image-item">
995
- <img src="{segmentation_image_base64}" alt="Wound Segmentation">
996
- <h4>📏 Wound Segmentation</h4>
997
- <p>Detailed wound area measurement and analysis</p>
998
  </div>
999
- '''
1000
- image_gallery_html += '</div>'
 
1001
 
 
1002
  report_html = self.markdown_to_html(report) if report else ""
1003
 
1004
  html_output = f"""
 
9
  from PIL import Image
10
  import html
11
  from typing import Optional, Dict, Any
12
+ import numpy as np
13
+ import cv2
14
 
15
  # ---- Safe imports for local vs package execution ----
16
  try:
 
458
  }
459
 
460
  .status-warning {
461
+ background: linear-gradient(135deg, #FFFBEB 0%, #FEF3C7 100%) !important;
462
+ border: 2px solid #F59E0B !important;
463
+ color: #92400E !important;
464
  padding: 20px 24px !important;
465
  border-radius: 16px !important;
466
  font-weight: 600 !important;
467
  margin: 16px 0 !important;
468
+ box-shadow: 0 8px 24px rgba(245, 158, 11, 0.2) !important;
469
  backdrop-filter: blur(10px) !important;
470
  }
471
 
472
+ /* Image Gallery */
473
  .image-gallery {
474
+ display: grid !important;
475
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)) !important;
476
+ gap: 20px !important;
477
+ margin: 20px 0 !important;
478
  }
479
+
480
+ .image-item {
481
+ background: white !important;
482
+ border-radius: 16px !important;
483
+ padding: 20px !important;
484
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1) !important;
485
+ transition: transform 0.3s ease !important;
486
+ }
487
+
488
+ .image-item:hover {
489
+ transform: translateY(-5px) !important;
490
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.15) !important;
491
+ }
492
+
493
+ .image-item img {
494
+ width: 100% !important;
495
+ height: auto !important;
496
+ border-radius: 12px !important;
497
+ margin-bottom: 12px !important;
498
+ }
499
+
500
+ .image-item h4 {
501
+ color: #2D3748 !important;
502
+ margin: 0 0 8px 0 !important;
503
+ font-size: 1.2rem !important;
504
  font-weight: 700 !important;
 
 
 
 
 
505
  }
506
+
507
+ .image-item p {
508
+ color: #4A5568 !important;
509
+ margin: 0 !important;
510
+ font-size: 0.95rem !important;
511
+ line-height: 1.5 !important;
512
  }
513
 
514
  /* Responsive */
 
688
  with gr.Column(scale=1):
689
  gr.HTML("<h3>📸 Wound Image</h3>")
690
  wound_image = gr.Image(label="Upload Wound Image", type="filepath")
691
+
 
 
 
 
 
 
692
  # Slider to adjust the automatic segmentation mask. Positive values dilate
693
  # (expand) the mask, negative values erode (shrink) it. The value represents
694
  # roughly percentage change where each 5 units corresponds to one iteration.
 
700
  label="Segmentation Adjustment",
701
  info="Adjust the automatic segmentation (negative shrinks, positive expands)"
702
  )
703
+
704
  gr.HTML("<h3>📝 Medical History</h3>")
705
  previous_treatment = gr.Textbox(label="Previous Treatment", lines=3)
706
  medical_history = gr.Textbox(label="Medical History", lines=3)
 
708
  allergies = gr.Textbox(label="Known Allergies", lines=2)
709
  additional_notes = gr.Textbox(label="Additional Notes", lines=3)
710
 
711
+ # Initial analysis button
712
+ analyze_btn = gr.Button("🔬 Preview Segmentation", variant="primary", elem_id="analyze-btn")
713
+
714
+ # Segmentation preview section (initially hidden)
715
+ with gr.Group(visible=False) as segmentation_preview_group:
716
+ gr.HTML("<h3>🎯 Segmentation Preview</h3>")
717
+ segmentation_preview = gr.Image(label="Automatic Segmentation", interactive=False)
718
+
719
+ with gr.Row():
720
+ accept_segmentation_btn = gr.Button("✅ Accept & Generate Full Report", variant="primary")
721
+ manual_edit_btn = gr.Button("✏️ Manual Edit", variant="secondary")
722
+
723
+ # Manual editing section (initially hidden)
724
+ with gr.Group(visible=False) as manual_edit_group:
725
+ gr.HTML("""
726
+ <div style="background: #e6f3ff; padding: 15px; border-radius: 8px; margin: 10px 0;">
727
+ <h4 style="margin: 0 0 10px 0; color: #1a365d;">📝 Manual Segmentation Instructions</h4>
728
+ <p style="margin: 0; color: #2c5282;">
729
+ Use the drawing tool below to manually mark the wound area.
730
+ Select the pen tool and draw over the wound region to create your mask.
731
+ </p>
732
+ </div>
733
+ """)
734
+
735
+ # Manual mask input using ImageMask component
736
+ manual_mask_input = gr.ImageMask(
737
+ sources=["upload"],
738
+ layers=False,
739
+ transforms=[],
740
+ format="png",
741
+ label="Manual Segmentation - Draw on the image to mark wound area",
742
+ show_label=True,
743
+ interactive=True
744
+ )
745
+
746
+ process_manual_btn = gr.Button("🔬 Generate Report with Manual Mask", variant="primary")
747
+
748
  analysis_output = gr.HTML("")
749
 
750
  # ------------------- PATIENT HISTORY -------------------
 
836
  return {
837
  auth_panel: gr.update(visible=True),
838
  practitioner_panel: gr.update(visible=False),
839
+ organization_panel: gr.update(visible=False),
840
+ login_status: "<div class='status-warning'>Please sign in.</div>",
841
+ segmentation_preview_group: gr.update(visible=False),
842
+ manual_edit_group: gr.update(visible=False),
843
+ analysis_output: ""
844
  }
845
 
846
+ def toggle_patient_mode(mode):
847
  return {
848
+ existing_patient_dd: gr.update(visible=(mode == "Existing patient")),
849
+ new_patient_group: gr.update(visible=(mode == "New patient"))
850
  }
851
 
852
+ def process_image_for_segmentation(
853
+ mode, existing_label, np_name, np_age, np_gender,
854
+ w_loc, w_dur, pain, moist, infect, diabetic,
855
+ prev_tx, med_hist, meds, alls, notes, img_path, seg_adjust
856
+ ):
857
+ """Process image and show segmentation preview"""
858
+ if not img_path:
859
+ return {
860
+ segmentation_preview_group: gr.update(visible=False),
861
+ analysis_output: "<div class='status-error'>❌ Please upload a wound image.</div>"
862
+ }
863
+
864
  try:
865
+ # Run initial analysis to get segmentation
866
+ user_id = int(self.current_user.get("id", 0) or 0)
867
+ if not user_id:
868
+ return {
869
+ segmentation_preview_group: gr.update(visible=False),
870
+ analysis_output: "<div class='status-error'>❌ Please login first.</div>"
871
+ }
872
+
873
+ # Prepare questionnaire data for AI
874
+ if mode == "Existing patient":
875
+ pid = _label_to_id(existing_label)
876
+ if not pid:
877
+ return {
878
+ segmentation_preview_group: gr.update(visible=False),
879
+ analysis_output: "<div class='status-warning'>⚠️ Select an existing patient.</div>"
880
+ }
881
+ # Fetch patient data
882
+ row = self.database_manager.execute_query_one(
883
+ "SELECT id, name, age, gender FROM patients WHERE id=%s LIMIT 1", (pid,)
884
+ )
885
+ pcore = row or {}
886
+ patient_name_v = pcore.get("name")
887
+ patient_age_v = pcore.get("age")
888
+ patient_gender_v = pcore.get("gender")
889
+ else:
890
+ patient_name_v = np_name
891
+ patient_age_v = np_age
892
+ patient_gender_v = np_gender
893
+
894
+ q_for_ai = {
895
+ 'age': patient_age_v,
896
+ 'diabetic': 'Yes' if diabetic != 'Non-diabetic' else 'No',
897
+ 'allergies': alls,
898
+ 'date_of_injury': 'Unknown',
899
+ 'professional_care': 'Yes',
900
+ 'oozing_bleeding': 'Minor Oozing' if infect != 'None' else 'None',
901
+ 'infection': 'Yes' if infect != 'None' else 'No',
902
+ 'moisture': moist,
903
+ 'patient_name': patient_name_v,
904
+ 'patient_gender': patient_gender_v,
905
+ 'wound_location': w_loc,
906
+ 'wound_duration': w_dur,
907
+ 'pain_level': pain,
908
+ 'previous_treatment': prev_tx,
909
+ 'medical_history': med_hist,
910
+ 'medications': meds,
911
+ 'additional_notes': notes
912
+ }
913
+
914
+ # Run visual analysis only to get segmentation
915
+ image_pil = Image.open(img_path)
916
+ visual_results = self.wound_analyzer.perform_visual_analysis(image_pil)
917
+
918
+ if not visual_results:
919
+ return {
920
+ segmentation_preview_group: gr.update(visible=False),
921
+ analysis_output: "<div class='status-error'>❌ Failed to analyze image.</div>"
922
+ }
923
+
924
+ # Get segmentation image path
925
+ seg_path = visual_results.get("segmentation_image_path")
926
+ if not seg_path or not os.path.exists(seg_path):
927
+ return {
928
+ segmentation_preview_group: gr.update(visible=False),
929
+ analysis_output: "<div class='status-error'>❌ Segmentation failed.</div>"
930
+ }
931
+
932
+ return {
933
+ segmentation_preview_group: gr.update(visible=True),
934
+ segmentation_preview: seg_path,
935
+ manual_edit_group: gr.update(visible=False),
936
+ analysis_output: "<div class='status-success'>✅ Segmentation preview ready. Review and choose to accept or manually edit.</div>"
937
+ }
938
+
939
+ except Exception as e:
940
+ logging.error(f"Segmentation preview error: {e}")
941
+ return {
942
+ segmentation_preview_group: gr.update(visible=False),
943
+ analysis_output: f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
944
+ }
945
+
946
+ def show_manual_edit_interface(img_path):
947
+ """Show manual editing interface with the original image"""
948
+ if not img_path or not os.path.exists(img_path):
949
+ return {
950
+ manual_edit_group: gr.update(visible=False),
951
+ analysis_output: "<div class='status-error'>❌ Original image not available for editing.</div>"
952
+ }
953
+
954
+ return {
955
+ manual_edit_group: gr.update(visible=True),
956
+ manual_mask_input: img_path, # Load the original image for manual editing
957
+ analysis_output: "<div class='status-warning'>⚠️ Use the drawing tool to manually mark the wound area, then click 'Generate Report with Manual Mask'.</div>"
958
+ }
959
+
960
+ def process_manual_mask(mask_data):
961
+ """Process the manual mask from ImageMask component"""
962
+ if not mask_data:
963
+ return "<div class='status-error'>❌ No manual mask provided.</div>"
964
+
965
+ try:
966
+ # Extract the mask from the ImageMask component
967
+ # The mask_data contains both the background image and the drawn mask
968
+ if isinstance(mask_data, dict) and "layers" in mask_data:
969
+ # Get the alpha channel from the first layer (the drawn mask)
970
+ alpha_channel = mask_data["layers"][0][:, :, 3]
971
+ # Convert to binary mask
972
+ mask = np.where(alpha_channel == 0, 0, 255).astype(np.uint8)
973
+
974
+ # Save the mask temporarily
975
+ import tempfile
976
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
977
+ cv2.imwrite(tmp.name, mask)
978
+ manual_mask_path = tmp.name
979
+
980
+ return manual_mask_path
981
+ else:
982
+ return None
983
+
984
+ except Exception as e:
985
+ logging.error(f"Manual mask processing error: {e}")
986
+ return None
987
+
988
+ def run_full_analysis_with_manual_mask(
989
+ mode, existing_label, np_name, np_age, np_gender,
990
+ w_loc, w_dur, pain, moist, infect, diabetic,
991
+ prev_tx, med_hist, meds, alls, notes, img_path, seg_adjust, mask_data
992
+ ):
993
+ """Run full analysis with manual mask"""
994
+ try:
995
+ # Process manual mask
996
+ manual_mask_path = process_manual_mask(mask_data)
997
+
998
+ # Run the full analysis with manual mask
999
+ result_html = standalone_run_analysis(
1000
+ self, self.current_user, self.database_manager, self.wound_analyzer,
1001
+ mode, existing_label, np_name, np_age, np_gender,
1002
+ w_loc, w_dur, pain, moist, infect, diabetic,
1003
+ prev_tx, med_hist, meds, alls, notes, img_path,
1004
+ seg_adjust, manual_mask_path
1005
+ )
1006
+
1007
+ # Clean up temporary file
1008
+ if manual_mask_path and os.path.exists(manual_mask_path):
1009
+ try:
1010
+ os.unlink(manual_mask_path)
1011
+ except:
1012
+ pass
1013
+
1014
+ return {
1015
+ analysis_output: result_html,
1016
+ segmentation_preview_group: gr.update(visible=False),
1017
+ manual_edit_group: gr.update(visible=False)
1018
+ }
1019
+
1020
+ except Exception as e:
1021
+ logging.error(f"Manual analysis error: {e}")
1022
+ return {
1023
+ analysis_output: f"<div class='status-error'>❌ Analysis failed: {html.escape(str(e))}</div>"
1024
+ }
1025
+
1026
+ def run_full_analysis_accept_segmentation(
1027
+ mode, existing_label, np_name, np_age, np_gender,
1028
+ w_loc, w_dur, pain, moist, infect, diabetic,
1029
+ prev_tx, med_hist, meds, alls, notes, img_path, seg_adjust
1030
+ ):
1031
+ """Run full analysis accepting the automatic segmentation"""
1032
+ try:
1033
+ result_html = standalone_run_analysis(
1034
+ self, self.current_user, self.database_manager, self.wound_analyzer,
1035
+ mode, existing_label, np_name, np_age, np_gender,
1036
+ w_loc, w_dur, pain, moist, infect, diabetic,
1037
+ prev_tx, med_hist, meds, alls, notes, img_path,
1038
+ seg_adjust, None # No manual mask
1039
+ )
1040
+
1041
+ return {
1042
+ analysis_output: result_html,
1043
+ segmentation_preview_group: gr.update(visible=False),
1044
+ manual_edit_group: gr.update(visible=False)
1045
+ }
1046
+
1047
+ except Exception as e:
1048
+ logging.error(f"Analysis error: {e}")
1049
+ return {
1050
+ analysis_output: f"<div class='status-error'>❌ Analysis failed: {html.escape(str(e))}</div>"
1051
+ }
1052
+
1053
+ def load_patient_history():
1054
+ try:
1055
+ uid = int(self.current_user.get("id", 0))
1056
  if not uid:
1057
  return "<div class='status-error'>❌ Please login first.</div>"
1058
+
1059
+ history_data = self.patient_history_manager.get_patient_history(uid)
1060
+ if not history_data:
1061
+ return "<div class='status-warning'>⚠️ No patient history found.</div>"
1062
+
1063
+ html_report = self.report_generator.generate_history_report(history_data)
1064
+ return html_report
 
 
1065
  except Exception as e:
1066
+ logging.error(f"History load error: {e}")
1067
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
1068
 
1069
+ def search_patient_by_name(name):
1070
  try:
1071
+ uid = int(self.current_user.get("id", 0))
1072
  if not uid:
1073
  return "<div class='status-error'>❌ Please login first.</div>"
1074
+
1075
+ if not name or not name.strip():
1076
+ return "<div class='status-warning'>⚠️ Enter a patient name to search.</div>"
1077
+
1078
+ results = self.patient_history_manager.search_patients_by_name(uid, name.strip())
1079
+ if not results:
1080
+ return f"<div class='status-warning'>⚠️ No patients found matching '{html.escape(name)}'.</div>"
1081
+
1082
+ html_report = self.report_generator.generate_search_results(results, name)
1083
+ return html_report
1084
  except Exception as e:
1085
+ logging.error(f"Patient search error: {e}")
1086
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
1087
 
1088
+ def view_patient_details(selected_label):
1089
  try:
1090
+ uid = int(self.current_user.get("id", 0))
1091
  if not uid:
1092
  return "<div class='status-error'>❌ Please login first.</div>"
1093
+
1094
+ pid = _label_to_id(selected_label)
1095
  if not pid:
1096
+ return "<div class='status-warning'>⚠️ Select a patient to view details.</div>"
1097
+
1098
+ details = self.patient_history_manager.get_patient_details(uid, pid)
1099
+ if not details:
1100
+ return "<div class='status-warning'>⚠️ No details found for selected patient.</div>"
1101
+
1102
+ html_report = self.report_generator.generate_patient_timeline(details)
1103
+ return html_report
 
1104
  except Exception as e:
1105
+ logging.error(f"Patient details error: {e}")
1106
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
1107
 
1108
+ # ----------------------- Event bindings -----------------------
1109
+ signup_role.change(toggle_role_fields, [signup_role], [org_fields, prac_fields])
1110
+
 
 
 
 
1111
  signup_btn.click(
1112
  handle_signup,
1113
+ [signup_username, signup_email, signup_password, signup_name, signup_role,
1114
+ org_name, phone, country_code, department, location, organization_dropdown],
1115
+ [signup_status]
1116
  )
1117
+
1118
  login_btn.click(
1119
  handle_login,
1120
+ [login_username, login_password],
1121
+ [login_status, auth_panel, practitioner_panel, organization_panel, user_info,
1122
+ existing_patient_dd, view_details_dd]
1123
  )
1124
+
1125
+ logout_btn_prac.click(
1126
+ handle_logout,
1127
+ [],
1128
+ [auth_panel, practitioner_panel, organization_panel, login_status,
1129
+ segmentation_preview_group, manual_edit_group, analysis_output]
 
 
1130
  )
1131
+
1132
+ logout_btn_org.click(
1133
+ handle_logout,
1134
+ [],
1135
+ [auth_panel, practitioner_panel, organization_panel, login_status,
1136
+ segmentation_preview_group, manual_edit_group, analysis_output]
1137
+ )
1138
+
1139
+ patient_mode.change(toggle_patient_mode, [patient_mode], [existing_patient_dd, new_patient_group])
1140
+
1141
+ # Segmentation preview workflow
1142
  analyze_btn.click(
1143
+ process_image_for_segmentation,
1144
+ [patient_mode, existing_patient_dd, new_patient_name, new_patient_age, new_patient_gender,
1145
+ wound_location, wound_duration, pain_level, moisture_level, infection_signs, diabetic_status,
1146
+ previous_treatment, medical_history, medications, allergies, additional_notes, wound_image, seg_adjust_slider],
1147
+ [segmentation_preview_group, segmentation_preview, manual_edit_group, analysis_output]
 
 
 
 
 
 
 
 
 
 
1148
  )
1149
+
1150
+ # Accept segmentation and generate full report
1151
+ accept_segmentation_btn.click(
1152
+ run_full_analysis_accept_segmentation,
1153
+ [patient_mode, existing_patient_dd, new_patient_name, new_patient_age, new_patient_gender,
1154
+ wound_location, wound_duration, pain_level, moisture_level, infection_signs, diabetic_status,
1155
+ previous_treatment, medical_history, medications, allergies, additional_notes, wound_image, seg_adjust_slider],
1156
+ [analysis_output, segmentation_preview_group, manual_edit_group]
1157
+ )
1158
+
1159
+ # Show manual edit interface
1160
+ manual_edit_btn.click(
1161
+ show_manual_edit_interface,
1162
+ [wound_image],
1163
+ [manual_edit_group, manual_mask_input, analysis_output]
1164
+ )
1165
+
1166
+ # Process manual mask and generate report
1167
+ process_manual_btn.click(
1168
+ run_full_analysis_with_manual_mask,
1169
+ [patient_mode, existing_patient_dd, new_patient_name, new_patient_age, new_patient_gender,
1170
+ wound_location, wound_duration, pain_level, moisture_level, infection_signs, diabetic_status,
1171
+ previous_treatment, medical_history, medications, allergies, additional_notes, wound_image, seg_adjust_slider, manual_mask_input],
1172
+ [analysis_output, segmentation_preview_group, manual_edit_group]
1173
+ )
1174
+
1175
+ history_btn.click(load_patient_history, [], [patient_history_output])
1176
+ search_patient_btn.click(search_patient_by_name, [search_patient_name], [specific_patient_output])
1177
+ view_details_btn.click(view_patient_details, [view_details_dd], [view_details_output])
1178
 
1179
  return app
1180
 
1181
+ def _format_comprehensive_analysis_results(self, analysis_result, image_path, questionnaire_data):
1182
+ """Format comprehensive analysis results with enhanced visual presentation"""
 
1183
  try:
1184
+ visual_analysis = analysis_result.get("visual_analysis", {})
1185
+ report = analysis_result.get("report", "")
1186
+
1187
+ # Extract key metrics
1188
+ wound_type = visual_analysis.get("wound_type", "Unknown")
1189
+ length_cm = visual_analysis.get("length_cm", 0)
1190
+ breadth_cm = visual_analysis.get("breadth_cm", 0)
1191
+ area_cm2 = visual_analysis.get("surface_area_cm2", 0)
1192
+ skin_tone_label = visual_analysis.get("skin_tone_label", "Unknown")
1193
+ ita_deg = visual_analysis.get("ita_degrees")
1194
+ tissue_type = visual_analysis.get("tissue_type", "Unknown")
1195
+
1196
+ # Generate risk assessment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1197
  risk_assessment = self._generate_risk_assessment(questionnaire_data)
1198
+ risk_level = risk_assessment.get("risk_level", "Unknown")
1199
+ risk_score = risk_assessment.get("risk_score", 0)
1200
+ risk_factors = risk_assessment.get("risk_factors", [])
1201
+ risk_class = risk_level.lower().replace(" ", "_")
1202
+
1203
+ # Format risk factors
1204
+ if risk_factors:
1205
+ risk_factors_html = "<ul style='margin: 10px 0; padding-left: 20px;'>"
1206
+ for factor in risk_factors:
1207
+ risk_factors_html += f"<li style='margin: 5px 0; color: #2d3748;'>{html.escape(str(factor))}</li>"
1208
+ risk_factors_html += "</ul>"
1209
+ else:
1210
+ risk_factors_html = "<p style='color: #4a5568; font-style: italic;'>No specific risk factors identified.</p>"
1211
+
1212
+ # Create image gallery
1213
+ image_gallery_html = "<div class='image-gallery'>"
1214
+
1215
+ # Original image
1216
+ if image_path and os.path.exists(image_path):
1217
+ img_b64 = self.image_to_base64(image_path)
1218
+ if img_b64:
1219
+ image_gallery_html += f"""
1220
  <div class="image-item">
1221
+ <img src="{img_b64}" alt="Original Wound Image">
1222
+ <h4>📸 Original Image</h4>
1223
+ <p>Uploaded wound photograph for analysis</p>
1224
  </div>
1225
+ """
1226
+
1227
+ # Detection visualization
1228
+ detection_path = visual_analysis.get("detection_image_path")
1229
+ if detection_path and os.path.exists(detection_path):
1230
+ img_b64 = self.image_to_base64(detection_path)
1231
+ if img_b64:
1232
+ image_gallery_html += f"""
1233
  <div class="image-item">
1234
+ <img src="{img_b64}" alt="Wound Detection">
1235
  <h4>🎯 Wound Detection</h4>
1236
+ <p>AI-powered wound boundary detection with confidence: {visual_analysis.get('detection_confidence', 0):.1%}</p>
1237
+ </div>
1238
+ """
1239
+
1240
+ # Segmentation visualization
1241
+ seg_path = visual_analysis.get("segmentation_image_path")
1242
+ if seg_path and os.path.exists(seg_path):
1243
+ img_b64 = self.image_to_base64(seg_path)
1244
+ if img_b64:
1245
+ image_gallery_html += f"""
1246
+ <div class="image-item">
1247
+ <img src="{img_b64}" alt="Wound Segmentation">
1248
+ <h4>🔍 Wound Segmentation</h4>
1249
+ <p>Precise wound boundary identification and tissue analysis</p>
1250
  </div>
1251
+ """
1252
+
1253
+ # Annotated measurements
1254
+ annotated_path = visual_analysis.get("segmentation_annotated_path")
1255
+ if annotated_path and os.path.exists(annotated_path):
1256
+ img_b64 = self.image_to_base64(annotated_path)
1257
+ if img_b64:
1258
+ image_gallery_html += f"""
1259
  <div class="image-item">
1260
+ <img src="{img_b64}" alt="Annotated Measurements">
1261
+ <h4>📏 Measurements</h4>
1262
+ <p>Calibrated dimensional analysis with length and width indicators</p>
1263
  </div>
1264
+ """
1265
+
1266
+ image_gallery_html += "</div>"
1267
 
1268
+ # Convert report markdown to HTML
1269
  report_html = self.markdown_to_html(report) if report else ""
1270
 
1271
  html_output = f"""