MotifAlign / image_annotator.py
jiehou's picture
Upload 2 files
91fbffc verified
"""
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")