Spaces:
Sleeping
Sleeping
| """ | |
| Image Annotation Utility for RNA Structure Alignments | |
| Adds RMSD, reference/query names, and sequences directly to PNG/JPEG images | |
| """ | |
| from PIL import Image, ImageDraw, ImageFont | |
| import io | |
| def annotate_alignment_image(image_data, rmsd, ref_name, query_name, | |
| ref_sequence=None, query_sequence=None, | |
| output_format='PNG'): | |
| """ | |
| Add text annotations to an alignment image. | |
| Args: | |
| image_data: Either a file path (str), PIL Image object, or bytes | |
| rmsd: RMSD value (float) | |
| ref_name: Reference structure name (str) | |
| query_name: Query structure name (str) | |
| ref_sequence: Reference sequence (str, optional) | |
| query_sequence: Query sequence (str, optional) | |
| output_format: 'PNG' or 'JPEG' | |
| Returns: | |
| BytesIO object containing the annotated image | |
| """ | |
| # Load the image | |
| if isinstance(image_data, str): | |
| img = Image.open(image_data) | |
| elif isinstance(image_data, bytes): | |
| img = Image.open(io.BytesIO(image_data)) | |
| elif isinstance(image_data, Image.Image): | |
| img = image_data | |
| else: | |
| raise ValueError("image_data must be a file path, bytes, or PIL Image") | |
| # Convert to RGB if necessary (for JPEG compatibility) | |
| if img.mode != 'RGB' and output_format == 'JPEG': | |
| img = img.convert('RGB') | |
| # Create drawing context | |
| draw = ImageDraw.Draw(img) | |
| # Try to use a better font, fall back to default if not available | |
| try: | |
| # Try to load a TrueType font | |
| font_large = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20) | |
| font_medium = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 16) | |
| font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 14) | |
| except: | |
| # Fallback to default font | |
| font_large = ImageFont.load_default() | |
| font_medium = ImageFont.load_default() | |
| font_small = ImageFont.load_default() | |
| # Get image dimensions | |
| width, height = img.size | |
| # Define annotation box parameters | |
| margin = 15 | |
| padding = 12 | |
| line_spacing = 8 | |
| # Prepare text lines | |
| lines = [] | |
| lines.append(("RMSD:", f"{rmsd:.3f} Å", font_large, True)) # Bold RMSD | |
| lines.append(("", "", font_medium, False)) # Spacer | |
| lines.append(("Reference:", ref_name, font_medium, False)) | |
| if ref_sequence: | |
| lines.append((" Seq:", ref_sequence, font_small, False)) | |
| lines.append(("", "", font_medium, False)) # Spacer | |
| lines.append(("Query:", query_name, font_medium, False)) | |
| if query_sequence: | |
| lines.append((" Seq:", query_sequence, font_small, False)) | |
| # Calculate box dimensions | |
| max_width = 0 | |
| total_height = padding * 2 | |
| for label, value, font, is_bold in lines: | |
| if label or value: | |
| text = f"{label} {value}".strip() | |
| bbox = draw.textbbox((0, 0), text, font=font) | |
| text_width = bbox[2] - bbox[0] | |
| text_height = bbox[3] - bbox[1] | |
| max_width = max(max_width, text_width) | |
| total_height += text_height + line_spacing | |
| else: | |
| total_height += line_spacing // 2 | |
| box_width = max_width + padding * 2 | |
| box_height = total_height | |
| # Position box in bottom-left corner | |
| box_x = margin | |
| box_y = height - box_height - margin | |
| # Draw semi-transparent background box | |
| overlay = Image.new('RGBA', img.size, (255, 255, 255, 0)) | |
| overlay_draw = ImageDraw.Draw(overlay) | |
| # White background with 95% opacity | |
| overlay_draw.rounded_rectangle( | |
| [(box_x, box_y), (box_x + box_width, box_y + box_height)], | |
| radius=8, | |
| fill=(255, 255, 255, 242) | |
| ) | |
| # Add subtle border | |
| overlay_draw.rounded_rectangle( | |
| [(box_x, box_y), (box_x + box_width, box_y + box_height)], | |
| radius=8, | |
| outline=(200, 200, 200, 242), | |
| width=1 | |
| ) | |
| # Composite the overlay | |
| if img.mode == 'RGB': | |
| img = img.convert('RGBA') | |
| img = Image.alpha_composite(img, overlay) | |
| # Convert back to RGB if needed | |
| if output_format == 'JPEG': | |
| img = img.convert('RGB') | |
| # Recreate draw context after compositing | |
| draw = ImageDraw.Draw(img) | |
| # Draw text | |
| current_y = box_y + padding | |
| for label, value, font, is_bold in lines: | |
| if not label and not value: | |
| # Spacer | |
| current_y += line_spacing // 2 | |
| continue | |
| text = f"{label} {value}".strip() if value else label | |
| # Choose color based on content | |
| if "RMSD" in label: | |
| color = (233, 75, 60) # Red color for RMSD | |
| elif label.startswith(" "): | |
| color = (100, 100, 100) # Gray for sequences | |
| else: | |
| color = (51, 51, 51) # Dark gray for labels | |
| # Draw the text | |
| draw.text((box_x + padding, current_y), text, fill=color, font=font) | |
| # Get text height for next line | |
| bbox = draw.textbbox((0, 0), text, font=font) | |
| text_height = bbox[3] - bbox[1] | |
| current_y += text_height + line_spacing | |
| # Save to BytesIO | |
| output = io.BytesIO() | |
| img.save(output, format=output_format, quality=95 if output_format == 'JPEG' else None) | |
| output.seek(0) | |
| return output | |
| def annotate_alignment_image_file(input_path, output_path, rmsd, ref_name, query_name, | |
| ref_sequence=None, query_sequence=None): | |
| """ | |
| Annotate an image file and save to a new file. | |
| Args: | |
| input_path: Path to input image | |
| output_path: Path to save annotated image | |
| rmsd: RMSD value (float) | |
| ref_name: Reference structure name (str) | |
| query_name: Query structure name (str) | |
| ref_sequence: Reference sequence (str, optional) | |
| query_sequence: Query sequence (str, optional) | |
| """ | |
| output_format = 'JPEG' if output_path.lower().endswith('.jpg') or output_path.lower().endswith('.jpeg') else 'PNG' | |
| annotated = annotate_alignment_image( | |
| input_path, rmsd, ref_name, query_name, | |
| ref_sequence, query_sequence, output_format | |
| ) | |
| with open(output_path, 'wb') as f: | |
| f.write(annotated.getvalue()) | |
| # Example usage and testing | |
| if __name__ == "__main__": | |
| # Example: Create a test image with annotations | |
| from PIL import Image, ImageDraw | |
| # Create a simple test image | |
| test_img = Image.new('RGB', (800, 600), color='white') | |
| test_draw = ImageDraw.Draw(test_img) | |
| # Draw some simple shapes to simulate a structure | |
| test_draw.ellipse([300, 200, 500, 400], fill='lightblue', outline='blue', width=3) | |
| test_draw.ellipse([320, 220, 480, 380], fill='lightcoral', outline='red', width=3) | |
| # Annotate it | |
| annotated = annotate_alignment_image( | |
| test_img, | |
| rmsd=1.234, | |
| ref_name="6TNA_reference", | |
| query_name="1EHZ_query", | |
| ref_sequence="GCGGAU", | |
| query_sequence="GCGGAU" | |
| ) | |
| # Save test image | |
| with open('/tmp/test_annotated.png', 'wb') as f: | |
| f.write(annotated.getvalue()) | |
| print("Test image created: /tmp/test_annotated.png") | |