1
0

preview.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. """Preview module for generating image previews of patterns.
  2. Uses ProcessPoolExecutor to run CPU-intensive preview generation in separate
  3. processes, completely eliminating Python GIL contention with the motion control thread.
  4. """
  5. import os
  6. import math
  7. import asyncio
  8. import logging
  9. from io import BytesIO
  10. from modules.core import process_pool as pool_module
  11. logger = logging.getLogger(__name__)
  12. def _generate_preview_in_process(pattern_file, format='WEBP'):
  13. """Generate preview for a pattern file, optimized for 300x300 view.
  14. Runs in a separate process with its own GIL, avoiding contention
  15. with the motion control thread.
  16. All imports are done inside the function to ensure they happen
  17. in the worker process, not the main process.
  18. Note: Worker CPU affinity/priority is configured once at pool init via initializer.
  19. """
  20. # Import dependencies in the worker process
  21. from PIL import Image, ImageDraw
  22. from modules.core.pattern_manager import parse_theta_rho_file, THETA_RHO_DIR
  23. file_path = os.path.join(THETA_RHO_DIR, pattern_file)
  24. coordinates = parse_theta_rho_file(file_path)
  25. # Image generation parameters
  26. RENDER_SIZE = 2048 # Use 2048x2048 for high quality rendering
  27. DISPLAY_SIZE = 512 # Final display size
  28. if not coordinates:
  29. # Create an image with "No pattern data" text
  30. img = Image.new('RGBA', (DISPLAY_SIZE, DISPLAY_SIZE), (255, 255, 255, 0)) # Transparent background
  31. draw = ImageDraw.Draw(img)
  32. text = "No pattern data"
  33. try:
  34. bbox = draw.textbbox((0, 0), text)
  35. text_width = bbox[2] - bbox[0]
  36. text_height = bbox[3] - bbox[1]
  37. text_x = (DISPLAY_SIZE - text_width) / 2
  38. text_y = (DISPLAY_SIZE - text_height) / 2
  39. except:
  40. text_x = DISPLAY_SIZE / 4
  41. text_y = DISPLAY_SIZE / 2
  42. draw.text((text_x, text_y), text, fill="black")
  43. img_byte_arr = BytesIO()
  44. img.save(img_byte_arr, format=format)
  45. img_byte_arr.seek(0)
  46. return img_byte_arr.getvalue()
  47. # Create image and draw pattern
  48. img = Image.new('RGBA', (RENDER_SIZE, RENDER_SIZE), (255, 255, 255, 0)) # Transparent background
  49. draw = ImageDraw.Draw(img)
  50. # Image drawing parameters
  51. CENTER = RENDER_SIZE / 2.0
  52. SCALE_FACTOR = (RENDER_SIZE / 2.0) - 10.0
  53. LINE_COLOR = "black"
  54. STROKE_WIDTH = 2 # Increased stroke width for better visibility after scaling
  55. points_to_draw = []
  56. for theta, rho in coordinates:
  57. x = CENTER - rho * SCALE_FACTOR * math.cos(theta)
  58. y = CENTER - rho * SCALE_FACTOR * math.sin(theta)
  59. points_to_draw.append((x, y))
  60. if len(points_to_draw) > 1:
  61. draw.line(points_to_draw, fill=LINE_COLOR, width=STROKE_WIDTH, joint="curve")
  62. elif len(points_to_draw) == 1:
  63. r = 4 # Larger radius for single point to remain visible after scaling
  64. x, y = points_to_draw[0]
  65. draw.ellipse([(x-r, y-r), (x+r, y+r)], fill=LINE_COLOR)
  66. # Scale down to display size with high-quality resampling
  67. img = img.resize((DISPLAY_SIZE, DISPLAY_SIZE), Image.Resampling.LANCZOS)
  68. # Rotate the image 180 degrees
  69. img = img.rotate(180)
  70. # Save to bytes
  71. img_byte_arr = BytesIO()
  72. img.save(img_byte_arr, format=format, lossless=False, alpha_quality=20, method=0)
  73. img_byte_arr.seek(0)
  74. return img_byte_arr.getvalue()
  75. async def generate_preview_image(pattern_file, format='WEBP'):
  76. """Generate a preview for a pattern file.
  77. Runs in a separate process via ProcessPoolExecutor to completely
  78. eliminate GIL contention with the motion control thread.
  79. """
  80. loop = asyncio.get_running_loop()
  81. pool = pool_module.get_pool()
  82. try:
  83. # Run preview generation in a separate process (separate GIL)
  84. result = await loop.run_in_executor(
  85. pool,
  86. _generate_preview_in_process,
  87. pattern_file,
  88. format
  89. )
  90. return result
  91. except Exception as e:
  92. logger.error(f"Error generating preview for {pattern_file}: {e}")
  93. return None