jiehou commited on
Commit
9687e90
·
verified ·
1 Parent(s): 8062cf7

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +7 -203
  2. visualization.py +132 -26
app.py CHANGED
@@ -1032,210 +1032,14 @@ def main():
1032
  import traceback
1033
  st.code(traceback.format_exc())
1034
 
1035
- # Direct Download Annotated Image Button
1036
  st.markdown("---")
1037
- st.markdown("### 📸 Download Annotated Structure Image")
1038
-
1039
- col1, col2 = st.columns([3, 1])
1040
-
1041
- with col1:
1042
- st.info("💡 Generate a structure visualization with RMSD and sequence information embedded")
1043
-
1044
- with col2:
1045
- if st.button("🖼️ Generate Annotated Image", use_container_width=True, type="primary"):
1046
- with st.spinner("Generating annotated image..."):
1047
- try:
1048
- from visualization import extract_window_pdb, transform_pdb_string
1049
-
1050
- # Create a simple structure visualization using matplotlib
1051
- # Since we can't capture py3Dmol directly, we'll create a matplotlib-based view
1052
-
1053
- # Extract structures
1054
- ref_pdb = extract_window_pdb(
1055
- selected_row['Ref_Path'],
1056
- selected_row['Ref_Window']
1057
- )
1058
-
1059
- query_pdb = extract_window_pdb(
1060
- selected_row['Query_Path'],
1061
- selected_row['Query_Window']
1062
- )
1063
-
1064
- query_aligned_pdb = transform_pdb_string(
1065
- query_pdb,
1066
- selected_row['Rotation_Matrix'],
1067
- selected_row['Query_COM'],
1068
- selected_row['Ref_COM']
1069
- )
1070
-
1071
- # Parse coordinates for visualization
1072
- from rmsd_utils import parse_residue_atoms
1073
-
1074
- # Create a matplotlib-based 3D visualization
1075
- import matplotlib.pyplot as plt
1076
- from mpl_toolkits.mplot3d import Axes3D
1077
-
1078
- fig = plt.figure(figsize=(12, 9), dpi=150)
1079
- ax = fig.add_subplot(111, projection='3d')
1080
-
1081
- # Function to extract coordinates from PDB string
1082
- def get_coords_from_pdb_string(pdb_string):
1083
- coords = []
1084
- for line in pdb_string.split('\n'):
1085
- if line.startswith(('ATOM', 'HETATM')):
1086
- try:
1087
- x = float(line[30:38].strip())
1088
- y = float(line[38:46].strip())
1089
- z = float(line[46:54].strip())
1090
- atom_name = line[12:16].strip()
1091
- coords.append((x, y, z, atom_name))
1092
- except:
1093
- continue
1094
- return coords
1095
-
1096
- # Get coordinates
1097
- ref_coords = get_coords_from_pdb_string(ref_pdb)
1098
- query_coords = get_coords_from_pdb_string(query_aligned_pdb)
1099
-
1100
- # Plot reference structure (blue)
1101
- if ref_coords:
1102
- ref_x = [c[0] for c in ref_coords]
1103
- ref_y = [c[1] for c in ref_coords]
1104
- ref_z = [c[2] for c in ref_coords]
1105
- ax.scatter(ref_x, ref_y, ref_z, c='#4A90E2', s=40, alpha=0.8, label='Reference')
1106
-
1107
- # Connect backbone atoms
1108
- backbone_atoms = ['P', "C4'", "C3'", "O3'"]
1109
- ref_backbone = [(c[0], c[1], c[2]) for c in ref_coords if c[3] in backbone_atoms]
1110
- if len(ref_backbone) > 1:
1111
- bb_x = [c[0] for c in ref_backbone]
1112
- bb_y = [c[1] for c in ref_backbone]
1113
- bb_z = [c[2] for c in ref_backbone]
1114
- ax.plot(bb_x, bb_y, bb_z, c='#4A90E2', linewidth=2, alpha=0.6)
1115
-
1116
- # Plot query structure (red)
1117
- if query_coords:
1118
- query_x = [c[0] for c in query_coords]
1119
- query_y = [c[1] for c in query_coords]
1120
- query_z = [c[2] for c in query_coords]
1121
- ax.scatter(query_x, query_y, query_z, c='#E94B3C', s=40, alpha=0.8, label='Query (Aligned)')
1122
-
1123
- # Connect backbone atoms
1124
- query_backbone = [(c[0], c[1], c[2]) for c in query_coords if c[3] in backbone_atoms]
1125
- if len(query_backbone) > 1:
1126
- bb_x = [c[0] for c in query_backbone]
1127
- bb_y = [c[1] for c in query_backbone]
1128
- bb_z = [c[2] for c in query_backbone]
1129
- ax.plot(bb_x, bb_y, bb_z, c='#E94B3C', linewidth=2, alpha=0.6)
1130
-
1131
- # Set labels and title
1132
- ax.set_xlabel('X (Å)', fontsize=10)
1133
- ax.set_ylabel('Y (Å)', fontsize=10)
1134
- ax.set_zlabel('Z (Å)', fontsize=10)
1135
- ax.legend(fontsize=10, loc='upper right')
1136
-
1137
- # Set viewing angle
1138
- ax.view_init(elev=20, azim=45)
1139
-
1140
- # Equal aspect ratio
1141
- if ref_coords or query_coords:
1142
- all_coords = ref_coords + query_coords
1143
- all_x = [c[0] for c in all_coords]
1144
- all_y = [c[1] for c in all_coords]
1145
- all_z = [c[2] for c in all_coords]
1146
-
1147
- max_range = max(
1148
- max(all_x) - min(all_x),
1149
- max(all_y) - min(all_y),
1150
- max(all_z) - min(all_z)
1151
- ) / 2.0
1152
-
1153
- mid_x = (max(all_x) + min(all_x)) / 2
1154
- mid_y = (max(all_y) + min(all_y)) / 2
1155
- mid_z = (max(all_z) + min(all_z)) / 2
1156
-
1157
- ax.set_xlim(mid_x - max_range, mid_x + max_range)
1158
- ax.set_ylim(mid_y - max_range, mid_y + max_range)
1159
- ax.set_zlim(mid_z - max_range, mid_z + max_range)
1160
-
1161
- plt.tight_layout()
1162
-
1163
- # Save to temporary buffer
1164
- buf = io.BytesIO()
1165
- plt.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='white')
1166
- plt.close()
1167
- buf.seek(0)
1168
-
1169
- # Now annotate this image
1170
- annotated_img = annotate_alignment_image(
1171
- image_data=buf.read(),
1172
- rmsd=selected_row['RMSD'],
1173
- ref_name=selected_row['Reference'],
1174
- query_name=selected_row['Query'],
1175
- ref_sequence=selected_row['Ref_Sequence'],
1176
- query_sequence=selected_row['Query_Sequence'],
1177
- output_format='JPEG'
1178
- )
1179
-
1180
- # Generate filename
1181
- ref_clean = selected_row['Reference'].replace('.pdb', '')
1182
- query_clean = selected_row['Query'].replace('.pdb', '')
1183
- filename = f"annotated_{ref_clean}_{query_clean}_RMSD_{selected_row['RMSD']:.3f}.jpg"
1184
-
1185
- # Show preview and download button
1186
- st.success("✅ Annotated image generated!")
1187
- st.image(annotated_img, caption="Annotated Structure Alignment", use_column_width=True)
1188
-
1189
- st.download_button(
1190
- label="📥 Download Annotated JPEG",
1191
- data=annotated_img.getvalue(),
1192
- file_name=filename,
1193
- mime="image/jpeg",
1194
- use_container_width=True,
1195
- help="Download JPEG with RMSD and sequence information"
1196
- )
1197
-
1198
- except Exception as e:
1199
- st.error(f"Error generating annotated image: {str(e)}")
1200
- import traceback
1201
- st.code(traceback.format_exc())
1202
-
1203
- # Fallback: offer the upload option
1204
- st.info("💡 Alternatively, you can download a screenshot from the 3D viewer above using the '📷 Download PNG' button, then upload it below:")
1205
-
1206
- uploaded_screenshot = st.file_uploader(
1207
- "Upload screenshot (PNG/JPG)",
1208
- type=['png', 'jpg', 'jpeg'],
1209
- key=f"screenshot_upload_fallback_{selected_viz_idx}"
1210
- )
1211
-
1212
- if uploaded_screenshot is not None:
1213
- try:
1214
- annotated_img = annotate_alignment_image(
1215
- image_data=uploaded_screenshot.read(),
1216
- rmsd=selected_row['RMSD'],
1217
- ref_name=selected_row['Reference'],
1218
- query_name=selected_row['Query'],
1219
- ref_sequence=selected_row['Ref_Sequence'],
1220
- query_sequence=selected_row['Query_Sequence'],
1221
- output_format='JPEG'
1222
- )
1223
-
1224
- st.image(annotated_img, use_column_width=True)
1225
-
1226
- ref_clean = selected_row['Reference'].replace('.pdb', '')
1227
- query_clean = selected_row['Query'].replace('.pdb', '')
1228
- filename = f"annotated_{ref_clean}_{query_clean}_RMSD_{selected_row['RMSD']:.3f}.jpg"
1229
-
1230
- st.download_button(
1231
- label="📥 Download Annotated JPEG",
1232
- data=annotated_img.getvalue(),
1233
- file_name=filename,
1234
- mime="image/jpeg",
1235
- use_container_width=True
1236
- )
1237
- except Exception as e2:
1238
- st.error(f"Error annotating uploaded image: {str(e2)}")
1239
 
1240
  # Show transformation details
1241
  with st.expander("🔧 Transformation Details"):
 
1032
  import traceback
1033
  st.code(traceback.format_exc())
1034
 
1035
+ # Automatic Annotation Info
1036
  st.markdown("---")
1037
+ st.success(" **Automatic Annotation:** When you click 'Download PNG' in the 3D viewer above, the image automatically includes RMSD, structure names, and sequences!")
1038
+ st.info("💡 **Customize font size:** Use the 'Annotation Font Size' dropdown in the viewer controls (top-right) to choose from Small, Medium, Large (default), or Extra Large fonts!")
1039
+
1040
+
1041
+
1042
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
 
1044
  # Show transformation details
1045
  with st.expander("🔧 Transformation Details"):
visualization.py CHANGED
@@ -315,6 +315,16 @@ def create_structure_visualization(ref_path, query_path, ref_window_indices, que
315
  <option value="gray">Gray</option>
316
  </select>
317
  </div>
 
 
 
 
 
 
 
 
 
 
318
  </div>
319
 
320
  <div class="rmsd-info">
@@ -607,14 +617,41 @@ def create_structure_visualization(ref_path, query_path, ref_window_indices, que
607
  var refName = "{ref_name}".replace('.pdb', '');
608
  var queryName = "{query_name}".replace('.pdb', '');
609
  var rmsdValue = "{f'{rmsd:.3f}' if rmsd is not None else 'NA'}";
610
- var filename = 'alignment_' + refName + '_' + queryName + '_RMSD_' + rmsdValue + '.png';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
  // Ensure viewer is rendered
613
  if (viewer) {{
614
  viewer.render();
615
  }}
616
 
617
- // Get the container element (includes canvas + all overlays)
618
  const container = document.getElementById('container');
619
 
620
  if (!container) {{
@@ -630,34 +667,103 @@ def create_structure_visualization(ref_path, query_path, ref_window_indices, que
630
  useCORS: true,
631
  allowTaint: true
632
  }}).then(function(canvas) {{
633
- // Convert to PNG
634
- const dataURL = canvas.toDataURL('image/png');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
 
636
- // Create download link
637
- const link = document.createElement('a');
638
- link.download = filename;
639
- link.href = dataURL;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
 
641
- // Trigger download
642
- document.body.appendChild(link);
643
- link.click();
644
- document.body.removeChild(link);
645
  }}).catch(function(error) {{
646
  console.error('html2canvas error:', error);
647
-
648
- // Fallback: just capture the canvas without overlays
649
- const canvas = document.querySelector('#container canvas');
650
- if (canvas) {{
651
- const dataURL = canvas.toDataURL('image/png');
652
- const link = document.createElement('a');
653
- link.download = filename;
654
- link.href = dataURL;
655
- document.body.appendChild(link);
656
- link.click();
657
- document.body.removeChild(link);
658
- }} else {{
659
- alert('Download failed. Please try again.');
660
- }}
661
  }});
662
 
663
  }} catch (error) {{
 
315
  <option value="gray">Gray</option>
316
  </select>
317
  </div>
318
+
319
+ <div class="control-section">
320
+ <div class="section-title">Annotation Font Size</div>
321
+ <select id="annotationFontSize">
322
+ <option value="small">Small (18pt/16pt/14pt)</option>
323
+ <option value="medium">Medium (22pt/18pt/16pt)</option>
324
+ <option value="large" selected>Large (28pt/22pt/18pt)</option>
325
+ <option value="xlarge">Extra Large (36pt/28pt/22pt)</option>
326
+ </select>
327
+ </div>
328
  </div>
329
 
330
  <div class="rmsd-info">
 
617
  var refName = "{ref_name}".replace('.pdb', '');
618
  var queryName = "{query_name}".replace('.pdb', '');
619
  var rmsdValue = "{f'{rmsd:.3f}' if rmsd is not None else 'NA'}";
620
+ var refSeq = "{ref_sequence if ref_sequence else ''}";
621
+ var querySeq = "{query_sequence if query_sequence else ''}";
622
+
623
+ var filenameOriginal = 'alignment_' + refName + '_' + queryName + '_RMSD_' + rmsdValue + '.png';
624
+ var filenameAnnotated = 'annotated_' + refName + '_' + queryName + '_RMSD_' + rmsdValue + '.png';
625
+
626
+ // Get selected font size
627
+ const fontSizeSelect = document.getElementById('annotationFontSize');
628
+ const fontSizeOption = fontSizeSelect ? fontSizeSelect.value : 'large';
629
+
630
+ // Define font sizes based on selection (all values are at 2x scale for high resolution)
631
+ let fontSizes;
632
+ switch(fontSizeOption) {{
633
+ case 'small':
634
+ fontSizes = {{ rmsd: 36, name: 32, seq: 28 }}; // 18pt/16pt/14pt at 2x
635
+ break;
636
+ case 'medium':
637
+ fontSizes = {{ rmsd: 44, name: 36, seq: 32 }}; // 22pt/18pt/16pt at 2x
638
+ break;
639
+ case 'large':
640
+ fontSizes = {{ rmsd: 56, name: 44, seq: 36 }}; // 28pt/22pt/18pt at 2x
641
+ break;
642
+ case 'xlarge':
643
+ fontSizes = {{ rmsd: 72, name: 56, seq: 44 }}; // 36pt/28pt/22pt at 2x
644
+ break;
645
+ default:
646
+ fontSizes = {{ rmsd: 56, name: 44, seq: 36 }}; // Default to large
647
+ }}
648
 
649
  // Ensure viewer is rendered
650
  if (viewer) {{
651
  viewer.render();
652
  }}
653
 
654
+ // Get the container element
655
  const container = document.getElementById('container');
656
 
657
  if (!container) {{
 
667
  useCORS: true,
668
  allowTaint: true
669
  }}).then(function(canvas) {{
670
+ // Create ANNOTATED version
671
+ const annotatedCanvas = document.createElement('canvas');
672
+ annotatedCanvas.width = canvas.width;
673
+ annotatedCanvas.height = canvas.height;
674
+ const ctx = annotatedCanvas.getContext('2d');
675
+
676
+ // Draw the original image onto new canvas
677
+ ctx.drawImage(canvas, 0, 0);
678
+
679
+ // Add annotations
680
+ const margin = 30; // Scaled for 2x resolution
681
+ const padding = 24;
682
+ const lineSpacing = 16;
683
+
684
+ // Prepare annotation text with selected font sizes
685
+ const annotations = [
686
+ {{ text: 'RMSD: ' + rmsdValue + ' Å', fontSize: fontSizes.rmsd, fontFamily: 'bold Arial', color: '#E94B3C' }},
687
+ {{ text: '', fontSize: 20, fontFamily: 'Arial', color: '#333' }}, // Spacer
688
+ {{ text: 'Reference: ' + refName, fontSize: fontSizes.name, fontFamily: 'Arial', color: '#333' }},
689
+ {{ text: ' Seq: ' + refSeq, fontSize: fontSizes.seq, fontFamily: 'Courier New, monospace', color: '#666' }},
690
+ {{ text: '', fontSize: 20, fontFamily: 'Arial', color: '#333' }}, // Spacer
691
+ {{ text: 'Query: ' + queryName, fontSize: fontSizes.name, fontFamily: 'Arial', color: '#333' }},
692
+ {{ text: ' Seq: ' + querySeq, fontSize: fontSizes.seq, fontFamily: 'Courier New, monospace', color: '#666' }}
693
+ ];
694
+
695
+ // Calculate box dimensions
696
+ let maxWidth = 0;
697
+ let totalHeight = padding * 2;
698
+ const textMetrics = [];
699
 
700
+ annotations.forEach(ann => {{
701
+ if (ann.text) {{
702
+ ctx.font = ann.fontSize + 'px ' + ann.fontFamily;
703
+ const metrics = ctx.measureText(ann.text);
704
+ const height = ann.fontSize * 1.2; // Approximate height
705
+ textMetrics.push({{ width: metrics.width, height: height }});
706
+ maxWidth = Math.max(maxWidth, metrics.width);
707
+ totalHeight += height + lineSpacing;
708
+ }} else {{
709
+ textMetrics.push({{ width: 0, height: lineSpacing / 2 }});
710
+ totalHeight += lineSpacing / 2;
711
+ }}
712
+ }});
713
+
714
+ const boxWidth = maxWidth + padding * 2;
715
+ const boxHeight = totalHeight;
716
+
717
+ // Position box in bottom-left
718
+ const boxX = margin;
719
+ const boxY = annotatedCanvas.height - boxHeight - margin;
720
+
721
+ // Draw semi-transparent white background with rounded corners
722
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
723
+ const radius = 16;
724
+ ctx.beginPath();
725
+ ctx.moveTo(boxX + radius, boxY);
726
+ ctx.lineTo(boxX + boxWidth - radius, boxY);
727
+ ctx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + radius);
728
+ ctx.lineTo(boxX + boxWidth, boxY + boxHeight - radius);
729
+ ctx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - radius, boxY + boxHeight);
730
+ ctx.lineTo(boxX + radius, boxY + boxHeight);
731
+ ctx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - radius);
732
+ ctx.lineTo(boxX, boxY + radius);
733
+ ctx.quadraticCurveTo(boxX, boxY, boxX + radius, boxY);
734
+ ctx.closePath();
735
+ ctx.fill();
736
+
737
+ // Draw border
738
+ ctx.strokeStyle = 'rgba(200, 200, 200, 0.95)';
739
+ ctx.lineWidth = 2;
740
+ ctx.stroke();
741
+
742
+ // Draw text
743
+ let currentY = boxY + padding;
744
+ annotations.forEach((ann, idx) => {{
745
+ if (ann.text) {{
746
+ ctx.font = ann.fontSize + 'px ' + ann.fontFamily;
747
+ ctx.fillStyle = ann.color;
748
+ ctx.fillText(ann.text, boxX + padding, currentY + textMetrics[idx].height * 0.8);
749
+ currentY += textMetrics[idx].height + lineSpacing;
750
+ }} else {{
751
+ currentY += textMetrics[idx].height;
752
+ }}
753
+ }});
754
+
755
+ // Download ONLY the annotated PNG
756
+ const annotatedDataURL = annotatedCanvas.toDataURL('image/png');
757
+ const linkAnnotated = document.createElement('a');
758
+ linkAnnotated.download = filenameAnnotated;
759
+ linkAnnotated.href = annotatedDataURL;
760
+ document.body.appendChild(linkAnnotated);
761
+ linkAnnotated.click();
762
+ document.body.removeChild(linkAnnotated);
763
 
 
 
 
 
764
  }}).catch(function(error) {{
765
  console.error('html2canvas error:', error);
766
+ alert('Error creating images. Please try again.');
 
 
 
 
 
 
 
 
 
 
 
 
 
767
  }});
768
 
769
  }} catch (error) {{