spacr 0.3.1__py3-none-any.whl → 0.3.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. spacr/__init__.py +19 -3
  2. spacr/cellpose.py +311 -0
  3. spacr/core.py +245 -2494
  4. spacr/deep_spacr.py +316 -48
  5. spacr/gui.py +1 -0
  6. spacr/gui_core.py +74 -63
  7. spacr/gui_elements.py +110 -5
  8. spacr/gui_utils.py +346 -6
  9. spacr/io.py +680 -141
  10. spacr/logger.py +28 -9
  11. spacr/measure.py +107 -95
  12. spacr/mediar.py +0 -3
  13. spacr/ml.py +1051 -0
  14. spacr/openai.py +37 -0
  15. spacr/plot.py +707 -20
  16. spacr/resources/data/lopit.csv +3833 -0
  17. spacr/resources/data/toxoplasma_metadata.csv +8843 -0
  18. spacr/resources/icons/convert.png +0 -0
  19. spacr/resources/{models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model → icons/dna_matrix.mp4} +0 -0
  20. spacr/sequencing.py +241 -1311
  21. spacr/settings.py +134 -47
  22. spacr/sim.py +0 -2
  23. spacr/submodules.py +349 -0
  24. spacr/timelapse.py +0 -2
  25. spacr/toxo.py +238 -0
  26. spacr/utils.py +419 -180
  27. {spacr-0.3.1.dist-info → spacr-0.3.22.dist-info}/METADATA +31 -22
  28. {spacr-0.3.1.dist-info → spacr-0.3.22.dist-info}/RECORD +32 -33
  29. spacr/chris.py +0 -50
  30. spacr/graph_learning.py +0 -340
  31. spacr/resources/MEDIAR/.git +0 -1
  32. spacr/resources/MEDIAR_weights/.DS_Store +0 -0
  33. spacr/resources/icons/.DS_Store +0 -0
  34. spacr/resources/icons/spacr_logo_rotation.gif +0 -0
  35. spacr/resources/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model_settings.csv +0 -23
  36. spacr/resources/models/cp/toxo_pv_lumen.CP_model +0 -0
  37. spacr/sim_app.py +0 -0
  38. {spacr-0.3.1.dist-info → spacr-0.3.22.dist-info}/LICENSE +0 -0
  39. {spacr-0.3.1.dist-info → spacr-0.3.22.dist-info}/WHEEL +0 -0
  40. {spacr-0.3.1.dist-info → spacr-0.3.22.dist-info}/entry_points.txt +0 -0
  41. {spacr-0.3.1.dist-info → spacr-0.3.22.dist-info}/top_level.txt +0 -0
spacr/gui_core.py CHANGED
@@ -1,9 +1,9 @@
1
- import traceback, ctypes, csv, re, platform, time
1
+ import os, traceback, ctypes, csv, re, platform
2
2
  import tkinter as tk
3
3
  from tkinter import ttk
4
4
  from tkinter import filedialog
5
5
  from multiprocessing import Process, Value, Queue, set_start_method
6
- from tkinter import ttk, scrolledtext
6
+ from tkinter import ttk
7
7
  from matplotlib.figure import Figure
8
8
  from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
9
9
  import numpy as np
@@ -11,15 +11,13 @@ import psutil
11
11
  import GPUtil
12
12
  from collections import deque
13
13
  import tracemalloc
14
- from tkinter import Menu
15
- import io
16
14
 
17
15
  try:
18
16
  ctypes.windll.shcore.SetProcessDpiAwareness(True)
19
17
  except AttributeError:
20
18
  pass
21
19
 
22
- from .gui_elements import spacrProgressBar, spacrButton, spacrLabel, spacrFrame, spacrDropdownMenu , spacrSlider, set_dark_style, standardize_figure
20
+ from .gui_elements import spacrProgressBar, spacrButton, spacrFrame, spacrDropdownMenu , spacrSlider, set_dark_style
23
21
 
24
22
  # Define global variables
25
23
  q = None
@@ -35,6 +33,7 @@ figures = None
35
33
  figure_index = None
36
34
  progress_bar = None
37
35
  usage_bars = None
36
+ index_control = None
38
37
 
39
38
  thread_control = {"run_thread": None, "stop_requested": False}
40
39
 
@@ -170,39 +169,6 @@ def display_figure(fig):
170
169
  #flash_feedback("right")
171
170
  show_next_figure()
172
171
 
173
- def zoom_v1(event):
174
- nonlocal scale_factor
175
-
176
- zoom_speed = 0.1 # Adjust the zoom speed for smoother experience
177
-
178
- # Adjust zoom factor based on the operating system and mouse event
179
- if event.num == 4 or event.delta > 0: # Scroll up
180
- scale_factor *= (1 + zoom_speed)
181
- elif event.num == 5 or event.delta < 0: # Scroll down
182
- scale_factor /= (1 + zoom_speed)
183
-
184
- # Get mouse position relative to the figure
185
- x_mouse, y_mouse = event.x, event.y
186
- x_ratio = x_mouse / canvas_widget.winfo_width()
187
- y_ratio = y_mouse / canvas_widget.winfo_height()
188
-
189
- for ax in fig.get_axes():
190
- xlim = ax.get_xlim()
191
- ylim = ax.get_ylim()
192
-
193
- # Calculate the new limits
194
- x_center = xlim[0] + x_ratio * (xlim[1] - xlim[0])
195
- y_center = ylim[0] + (1 - y_ratio) * (ylim[1] - ylim[0])
196
-
197
- x_range = (xlim[1] - xlim[0]) * scale_factor
198
- y_range = (ylim[1] - ylim[0]) * scale_factor
199
-
200
- ax.set_xlim([x_center - x_range * x_ratio, x_center + x_range * (1 - x_ratio)])
201
- ax.set_ylim([y_center - y_range * (1 - y_ratio), y_center + y_range * y_ratio])
202
-
203
- # Redraw the figure
204
- fig.canvas.draw_idle()
205
-
206
172
  def zoom(event):
207
173
  nonlocal scale_factor
208
174
 
@@ -282,7 +248,7 @@ def show_next_figure():
282
248
  figures.append(fig)
283
249
  figure_index += 1
284
250
  display_figure(fig)
285
-
251
+
286
252
  def process_fig_queue():
287
253
  global canvas, fig_queue, canvas_widget, parent_frame, uppdate_frequency, figures, figure_index, index_control
288
254
 
@@ -329,44 +295,60 @@ def update_figure(value):
329
295
  index_control.set_to(len(figures) - 1)
330
296
  index_control.set(figure_index)
331
297
 
332
- def setup_plot_section(vertical_container):
298
+ def setup_plot_section(vertical_container, settings_type):
333
299
  global canvas, canvas_widget, figures, figure_index, index_control
300
+ from .gui_utils import display_media_in_plot_frame
301
+
302
+ style_out = set_dark_style(ttk.Style())
303
+ bg = style_out['bg_color']
304
+ fg = style_out['fg_color']
334
305
 
335
306
  # Initialize deque for storing figures and the current index
336
307
  figures = deque()
337
308
 
338
309
  # Create a frame for the plot section
339
310
  plot_frame = tk.Frame(vertical_container)
311
+ plot_frame.configure(bg=bg)
340
312
  vertical_container.add(plot_frame, stretch="always")
341
313
 
342
- # Set up the plot
314
+ # Clear the plot_frame (optional, to handle cases where it may already have content)
315
+ for widget in plot_frame.winfo_children():
316
+ widget.destroy()
317
+
318
+ # Create a figure and plot
343
319
  figure = Figure(figsize=(30, 4), dpi=100)
344
320
  plot = figure.add_subplot(111)
345
321
  plot.plot([], [])
346
322
  plot.axis('off')
323
+
324
+ if settings_type == 'map_barcodes':
325
+ # Load and display GIF
326
+ current_dir = os.path.dirname(__file__)
327
+ resources_path = os.path.join(current_dir, 'resources', 'icons')
328
+ gif_path = os.path.join(resources_path, 'dna_matrix.mp4')
329
+
330
+ display_media_in_plot_frame(gif_path, plot_frame)
331
+ canvas = FigureCanvasTkAgg(figure, master=plot_frame)
332
+ canvas.get_tk_widget().configure(cursor='arrow', highlightthickness=0)
333
+ canvas_widget = canvas.get_tk_widget()
334
+ return canvas, canvas_widget
347
335
 
348
336
  canvas = FigureCanvasTkAgg(figure, master=plot_frame)
349
337
  canvas.get_tk_widget().configure(cursor='arrow', highlightthickness=0)
350
338
  canvas_widget = canvas.get_tk_widget()
351
339
  canvas_widget.grid(row=0, column=0, sticky="nsew")
352
-
353
340
  plot_frame.grid_rowconfigure(0, weight=1)
354
341
  plot_frame.grid_columnconfigure(0, weight=1)
355
-
356
342
  canvas.draw()
357
- canvas.figure = figure # Ensure that the figure is linked to the canvas
358
- style_out = set_dark_style(ttk.Style())
359
- bg = style_out['bg_color']
360
- fg = style_out['fg_color']
361
-
343
+ canvas.figure = figure
362
344
  figure.patch.set_facecolor(bg)
363
345
  plot.set_facecolor(bg)
364
346
  containers = [plot_frame]
365
347
 
366
348
  # Create slider
367
- control_frame = tk.Frame(plot_frame, height=15*2, bg=bg) # Fixed height based on knob_radius
349
+ control_frame = tk.Frame(plot_frame, height=15*2, bg=bg)
368
350
  control_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=5)
369
- control_frame.grid_propagate(False) # Prevent the frame from resizing
351
+ control_frame.grid_propagate(False)
370
352
 
371
353
  # Pass the update_figure function as the command to spacrSlider
372
354
  index_control = spacrSlider(control_frame, from_=0, to=0, value=0, thickness=2, knob_radius=10, position="center", show_index=True, command=update_figure)
@@ -442,6 +424,8 @@ def import_settings(settings_type='mask'):
442
424
  settings = get_analyze_recruitment_default_settings(settings={})
443
425
  elif settings_type == 'analyze_plaques':
444
426
  settings = {}
427
+ elif settings_type == 'convert':
428
+ settings = {}
445
429
  else:
446
430
  raise ValueError(f"Invalid settings type: {settings_type}")
447
431
 
@@ -452,7 +436,8 @@ def import_settings(settings_type='mask'):
452
436
 
453
437
  def setup_settings_panel(vertical_container, settings_type='mask'):
454
438
  global vars_dict, scrollable_frame
455
- from .settings import get_identify_masks_finetune_default_settings, set_default_analyze_screen, set_default_settings_preprocess_generate_masks, get_measure_crop_settings, deep_spacr_defaults, set_default_generate_barecode_mapping, set_default_umap_image_settings, generate_fields, get_perform_regression_default_settings, get_train_cellpose_default_settings, get_map_barcodes_default_settings, get_analyze_recruitment_default_settings, get_check_cellpose_models_default_settings
439
+ from .settings import get_identify_masks_finetune_default_settings, set_default_analyze_screen, set_default_settings_preprocess_generate_masks, get_measure_crop_settings, deep_spacr_defaults, set_default_generate_barecode_mapping, set_default_umap_image_settings
440
+ from .settings import get_map_barcodes_default_settings, get_analyze_recruitment_default_settings, get_check_cellpose_models_default_settings, generate_fields, get_perform_regression_default_settings, get_train_cellpose_default_settings
456
441
  from .gui_utils import convert_settings_dict_for_gui
457
442
  from .gui_elements import set_element_size
458
443
 
@@ -496,7 +481,9 @@ def setup_settings_panel(vertical_container, settings_type='mask'):
496
481
  elif settings_type == 'recruitment':
497
482
  settings = get_analyze_recruitment_default_settings(settings={})
498
483
  elif settings_type == 'analyze_plaques':
499
- settings = {}
484
+ settings = {'src':'path to images'}
485
+ elif settings_type == 'convert':
486
+ settings = {'src':'path to images'}
500
487
  else:
501
488
  raise ValueError(f"Invalid settings type: {settings_type}")
502
489
 
@@ -515,7 +502,7 @@ def setup_settings_panel(vertical_container, settings_type='mask'):
515
502
  def setup_console(vertical_container):
516
503
  global console_output
517
504
  from .gui_elements import set_dark_style
518
-
505
+
519
506
  # Apply dark style and get style output
520
507
  style = ttk.Style()
521
508
  style_out = set_dark_style(style)
@@ -546,9 +533,27 @@ def setup_console(vertical_container):
546
533
  def on_leave(event):
547
534
  top_border.config(bg=style_out['bg_color'])
548
535
 
536
+ #def on_enter_key(event):
537
+ # user_input = console_output.get("1.0", "end-1c").strip() # Get the user input from the console
538
+ # if user_input:
539
+ # # Print the user input with the (user) tag
540
+ # console_output.insert("end", f"\n(user): {user_input}\n")
541
+ #
542
+ # # Get the AI response from the chatbot
543
+ # response = chatbot.ask_question(user_input)
544
+ #
545
+ # # Print the AI response with the (ai) tag
546
+ # console_output.insert("end", f"(ai): {response}\n")
547
+ #
548
+ # console_output.see("end") # Scroll to the end
549
+ # #console_output.delete("1.0", "end") # Clear the input field
550
+ # return "break" # Prevent the default behavior of inserting a new line
551
+
549
552
  console_output.bind("<Enter>", on_enter)
550
553
  console_output.bind("<Leave>", on_leave)
551
554
 
555
+ #console_output.bind("<Return>", on_enter_key)
556
+
552
557
  return console_output, console_frame
553
558
 
554
559
  def setup_button_section(horizontal_container, settings_type='mask', run=True, abort=True, download=True, import_btn=True):
@@ -755,7 +760,7 @@ def initiate_abort():
755
760
  def start_process(q=None, fig_queue=None, settings_type='mask'):
756
761
  global thread_control, vars_dict, parent_frame
757
762
  from .settings import check_settings, expected_types
758
- from .gui_utils import run_function_gui, set_high_priority, set_cpu_affinity, initialize_cuda
763
+ from .gui_utils import run_function_gui, set_cpu_affinity, initialize_cuda, display_gif_in_plot_frame, print_widget_structure
759
764
 
760
765
  if q is None:
761
766
  q = Queue()
@@ -778,16 +783,14 @@ def start_process(q=None, fig_queue=None, settings_type='mask'):
778
783
 
779
784
  process_args = (settings_type, settings, q, fig_queue, stop_requested)
780
785
  if settings_type in ['mask', 'umap', 'measure', 'simulation', 'sequencing', 'classify', 'analyze_plaques',
781
- 'cellpose_dataset', 'train_cellpose', 'ml_analyze', 'cellpose_masks', 'cellpose_all', 'map_barcodes',
782
- 'regression', 'recruitment', 'cellpose_compare', 'vision_scores', 'vision_dataset']:
786
+ 'cellpose_dataset', 'train_cellpose', 'ml_analyze', 'cellpose_masks', 'cellpose_all',
787
+ 'map_barcodes', 'regression', 'recruitment', 'cellpose_compare', 'vision_scores',
788
+ 'vision_dataset', 'convert']:
783
789
 
784
790
  # Start the process
785
791
  process = Process(target=run_function_gui, args=process_args)
786
792
  process.start()
787
793
 
788
- # Set high priority for the process
789
- #set_high_priority(process)
790
-
791
794
  # Set CPU affinity if necessary
792
795
  set_cpu_affinity(process)
793
796
 
@@ -889,10 +892,14 @@ def initiate_root(parent, settings_type='mask'):
889
892
 
890
893
  global q, fig_queue, thread_control, parent_frame, scrollable_frame, button_frame, vars_dict, canvas, canvas_widget, button_scrollable_frame, progress_bar, uppdate_frequency, figures, figure_index, index_control, usage_bars
891
894
 
892
- from .gui_utils import setup_frame
895
+ from .gui_utils import setup_frame, get_screen_dimensions
893
896
  from .settings import descriptions
897
+ #from .openai import Chatbot
894
898
 
895
899
  uppdate_frequency = 500
900
+ num_cores = os.cpu_count()
901
+
902
+ #chatbot = Chatbot(api_key="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")
896
903
 
897
904
  # Start tracemalloc and initialize global variables
898
905
  tracemalloc.start()
@@ -930,10 +937,14 @@ def initiate_root(parent, settings_type='mask'):
930
937
  else:
931
938
  scrollable_frame, vars_dict = setup_settings_panel(settings_container, settings_type)
932
939
  print('setup_settings_panel')
933
- canvas, canvas_widget = setup_plot_section(vertical_container)
934
- console_output, _ = setup_console(vertical_container)
940
+ canvas, canvas_widget = setup_plot_section(vertical_container, settings_type)
941
+ console_output, _ = setup_console(vertical_container) #, chatbot)
935
942
  button_scrollable_frame, btn_col = setup_button_section(horizontal_container, settings_type)
936
- _, usage_bars, btn_col = setup_usage_panel(horizontal_container, btn_col, uppdate_frequency)
943
+
944
+ if num_cores > 12:
945
+ _, usage_bars, btn_col = setup_usage_panel(horizontal_container, btn_col, uppdate_frequency)
946
+ else:
947
+ usage_bars = []
937
948
 
938
949
  set_globals(thread_control, q, console_output, parent_frame, vars_dict, canvas, canvas_widget, scrollable_frame, fig_queue, figures, figure_index, index_control, progress_bar, usage_bars)
939
950
  description_text = descriptions.get(settings_type, "No description available for this module.")
spacr/gui_elements.py CHANGED
@@ -1,4 +1,4 @@
1
- import os, threading, time, sqlite3, webbrowser, pyautogui
1
+ import os, threading, time, sqlite3, webbrowser, pyautogui, random, cv2
2
2
  import tkinter as tk
3
3
  from tkinter import ttk
4
4
  import tkinter.font as tkFont
@@ -7,7 +7,7 @@ from tkinter import font
7
7
  from queue import Queue
8
8
  from tkinter import Label, Frame, Button
9
9
  import numpy as np
10
- from PIL import Image, ImageOps, ImageTk
10
+ from PIL import Image, ImageOps, ImageTk, ImageDraw, ImageFont, ImageEnhance
11
11
  from concurrent.futures import ThreadPoolExecutor
12
12
  from skimage.exposure import rescale_intensity
13
13
  from IPython.display import display, HTML
@@ -16,7 +16,8 @@ from collections import deque
16
16
  from skimage.draw import polygon, line
17
17
  from skimage.transform import resize
18
18
  from scipy.ndimage import binary_fill_holes, label
19
- from tkinter import ttk, scrolledtext
19
+ from tkinter import ttk, scrolledtext
20
+
20
21
  fig = None
21
22
 
22
23
  def set_element_size():
@@ -2512,7 +2513,6 @@ def create_menu_bar(root):
2512
2513
  "Annotate": lambda: initiate_root(root, settings_type='annotate'),
2513
2514
  "Make Masks": lambda: initiate_root(root, settings_type='make_masks'),
2514
2515
  "Classify": lambda: initiate_root(root, settings_type='classify'),
2515
- "Sequencing": lambda: initiate_root(root, settings_type='sequencing'),
2516
2516
  "Umap": lambda: initiate_root(root, settings_type='umap'),
2517
2517
  "Train Cellpose": lambda: initiate_root(root, settings_type='train_cellpose'),
2518
2518
  "ML Analyze": lambda: initiate_root(root, settings_type='ml_analyze'),
@@ -2842,4 +2842,109 @@ def modify_figure(fig):
2842
2842
 
2843
2843
  # Apply button
2844
2844
  apply_button = tk.Button(modify_window, text="Apply", command=apply_modifications, bg="#2E2E2E", fg="white")
2845
- apply_button.grid(row=len(options) + len(checkboxes), column=0, columnspan=2, pady=10)
2845
+ apply_button.grid(row=len(options) + len(checkboxes), column=0, columnspan=2, pady=10)
2846
+
2847
+ def generate_dna_matrix(output_path='dna_matrix.gif', canvas_width=1500, canvas_height=1000, duration=30, fps=20, base_size=20, transition_frames=30, font_type='arial.ttf', enhance=[1.1, 1.5, 1.2, 1.5], lowercase_prob=0.3):
2848
+ """
2849
+ Generate a DNA matrix animation and save it as GIF, MP4, or AVI using OpenCV for videos.
2850
+ """
2851
+
2852
+ def save_output(frames, output_path, fps, output_format):
2853
+ """Save the animation based on output format."""
2854
+ if output_format in ['.mp4', '.avi']:
2855
+ images = [np.array(img.convert('RGB')) for img in frames]
2856
+ fourcc = cv2.VideoWriter_fourcc(*('mp4v' if output_format == '.mp4' else 'XVID'))
2857
+ out = cv2.VideoWriter(output_path, fourcc, fps, (canvas_width, canvas_height))
2858
+ for img in images:
2859
+ out.write(cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
2860
+ out.release()
2861
+ elif output_format == '.gif':
2862
+ frames[0].save(output_path, save_all=True, append_images=frames[1:], duration=int(1000/fps), loop=0)
2863
+
2864
+ def draw_base(draw, col_idx, base_position, base, font, alpha=255, fill_color=None):
2865
+ """Draws a DNA base at the specified position."""
2866
+ draw.text((col_idx * base_size, base_position * base_size), base, fill=(*fill_color, alpha), font=font)
2867
+
2868
+ # Setup variables
2869
+ num_frames = duration * fps
2870
+ num_columns = canvas_width // base_size
2871
+ bases = ['A', 'T', 'C', 'G']
2872
+ active_color = (155, 55, 155)
2873
+ color = (255, 255, 255)
2874
+ base_colors = {'A': color, 'T': color, 'C': color, 'G': color}
2875
+
2876
+ _, output_format = os.path.splitext(output_path)
2877
+
2878
+ # Initialize font
2879
+ try:
2880
+ font = ImageFont.truetype(font_type, base_size)
2881
+ except IOError:
2882
+ font = ImageFont.load_default()
2883
+
2884
+ # DNA string and positions
2885
+ string_lengths = [random.randint(10, 100) for _ in range(num_columns)]
2886
+ visible_bases = [0] * num_columns
2887
+ base_positions = [random.randint(-canvas_height // base_size, 0) for _ in range(num_columns)]
2888
+ column_strings = [[''] * 100 for _ in range(num_columns)]
2889
+ random_white_sequences = [None] * num_columns
2890
+
2891
+ frames = []
2892
+ end_frame_start = int(num_frames * 0.8)
2893
+
2894
+ for frame_idx in range(num_frames):
2895
+ img = Image.new('RGBA', (canvas_width, canvas_height), color=(0, 0, 0, 255))
2896
+ draw = ImageDraw.Draw(img)
2897
+
2898
+ for col_idx in range(num_columns):
2899
+ if base_positions[col_idx] >= canvas_height // base_size and frame_idx < end_frame_start:
2900
+ string_lengths[col_idx] = random.randint(10, 100)
2901
+ base_positions[col_idx] = -string_lengths[col_idx]
2902
+ visible_bases[col_idx] = 0
2903
+ # Randomly choose whether to make each base lowercase
2904
+ column_strings[col_idx] = [
2905
+ random.choice([base.lower(), base]) if random.random() < lowercase_prob else base
2906
+ for base in [random.choice(bases) for _ in range(string_lengths[col_idx])]
2907
+ ]
2908
+ if string_lengths[col_idx] > 8:
2909
+ random_start = random.randint(0, string_lengths[col_idx] - 8)
2910
+ random_white_sequences[col_idx] = range(random_start, random_start + 8)
2911
+
2912
+ last_10_percent_start = max(0, int(string_lengths[col_idx] * 0.9))
2913
+
2914
+ for row_idx in range(min(visible_bases[col_idx], string_lengths[col_idx])):
2915
+ base_position = base_positions[col_idx] + row_idx
2916
+ if 0 <= base_position * base_size < canvas_height:
2917
+ base = column_strings[col_idx][row_idx]
2918
+ if base:
2919
+ if row_idx == visible_bases[col_idx] - 1:
2920
+ draw_base(draw, col_idx, base_position, base, font, fill_color=active_color)
2921
+ elif row_idx >= last_10_percent_start:
2922
+ alpha = 255 - int(((row_idx - last_10_percent_start) / (string_lengths[col_idx] - last_10_percent_start)) * 127)
2923
+ draw_base(draw, col_idx, base_position, base, font, alpha=alpha, fill_color=base_colors[base.upper()])
2924
+ elif random_white_sequences[col_idx] and row_idx in random_white_sequences[col_idx]:
2925
+ draw_base(draw, col_idx, base_position, base, font, fill_color=active_color)
2926
+ else:
2927
+ draw_base(draw, col_idx, base_position, base, font, fill_color=base_colors[base.upper()])
2928
+
2929
+ if visible_bases[col_idx] < string_lengths[col_idx]:
2930
+ visible_bases[col_idx] += 1
2931
+ base_positions[col_idx] += 2
2932
+
2933
+ # Convert the image to numpy array to check unique pixel values
2934
+ img_array = np.array(img)
2935
+ if len(np.unique(img_array)) > 2: # Only append frames with more than two unique pixel values (avoid black frames)
2936
+ # Enhance contrast and saturation
2937
+ if enhance:
2938
+ img = ImageEnhance.Brightness(img).enhance(enhance[0]) # Slightly increase brightness
2939
+ img = ImageEnhance.Sharpness(img).enhance(enhance[1]) # Sharpen the image
2940
+ img = ImageEnhance.Contrast(img).enhance(enhance[2]) # Enhance contrast
2941
+ img = ImageEnhance.Color(img).enhance(enhance[3]) # Boost color saturation
2942
+
2943
+ frames.append(img)
2944
+
2945
+ for i in range(transition_frames):
2946
+ alpha = i / float(transition_frames)
2947
+ transition_frame = Image.blend(frames[-1], frames[0], alpha)
2948
+ frames.append(transition_frame)
2949
+
2950
+ save_output(frames, output_path, fps, output_format)