spacr 0.3.1__py3-none-any.whl → 0.3.3__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 +335 -163
  5. spacr/gui.py +2 -0
  6. spacr/gui_core.py +85 -65
  7. spacr/gui_elements.py +110 -5
  8. spacr/gui_utils.py +375 -7
  9. spacr/io.py +680 -141
  10. spacr/logger.py +28 -9
  11. spacr/measure.py +108 -133
  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 +181 -50
  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 +776 -182
  27. {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/METADATA +31 -22
  28. {spacr-0.3.1.dist-info → spacr-0.3.3.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.3.dist-info}/LICENSE +0 -0
  39. {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/WHEEL +0 -0
  40. {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/entry_points.txt +0 -0
  41. {spacr-0.3.1.dist-info → spacr-0.3.3.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)
@@ -397,10 +379,13 @@ def set_globals(thread_control_var, q_var, console_output_var, parent_frame_var,
397
379
  index_control = index_control_var
398
380
 
399
381
  def import_settings(settings_type='mask'):
400
- from .gui_utils import convert_settings_dict_for_gui, hide_all_settings
401
382
  global vars_dict, scrollable_frame, button_scrollable_frame
402
- from .settings import generate_fields, set_default_settings_preprocess_generate_masks, get_measure_crop_settings, set_default_train_test_model, set_default_generate_barecode_mapping, set_default_umap_image_settings, get_analyze_recruitment_default_settings
403
383
 
384
+ from .gui_utils import convert_settings_dict_for_gui, hide_all_settings
385
+ from .settings import generate_fields, set_default_settings_preprocess_generate_masks, get_measure_crop_settings, set_default_train_test_model
386
+ from .settings import set_default_generate_barecode_mapping, set_default_umap_image_settings, get_analyze_recruitment_default_settings
387
+ from .settings import get_default_generate_activation_map_settings
388
+ #activation
404
389
  def read_settings_from_csv(csv_file_path):
405
390
  settings = {}
406
391
  with open(csv_file_path, newline='') as csvfile:
@@ -440,8 +425,12 @@ def import_settings(settings_type='mask'):
440
425
  settings = set_default_umap_image_settings(settings={})
441
426
  elif settings_type == 'recruitment':
442
427
  settings = get_analyze_recruitment_default_settings(settings={})
428
+ elif settings_type == 'activation':
429
+ settings = get_default_generate_activation_map_settings(settings={})
443
430
  elif settings_type == 'analyze_plaques':
444
431
  settings = {}
432
+ elif settings_type == 'convert':
433
+ settings = {}
445
434
  else:
446
435
  raise ValueError(f"Invalid settings type: {settings_type}")
447
436
 
@@ -452,7 +441,10 @@ def import_settings(settings_type='mask'):
452
441
 
453
442
  def setup_settings_panel(vertical_container, settings_type='mask'):
454
443
  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
444
+ from .settings import get_identify_masks_finetune_default_settings, set_default_analyze_screen, set_default_settings_preprocess_generate_masks
445
+ from .settings import get_measure_crop_settings, deep_spacr_defaults, set_default_generate_barecode_mapping, set_default_umap_image_settings
446
+ from .settings import get_map_barcodes_default_settings, get_analyze_recruitment_default_settings, get_check_cellpose_models_default_settings
447
+ from .settings import generate_fields, get_perform_regression_default_settings, get_train_cellpose_default_settings, get_default_generate_activation_map_settings
456
448
  from .gui_utils import convert_settings_dict_for_gui
457
449
  from .gui_elements import set_element_size
458
450
 
@@ -495,8 +487,12 @@ def setup_settings_panel(vertical_container, settings_type='mask'):
495
487
  settings = get_perform_regression_default_settings(settings={})
496
488
  elif settings_type == 'recruitment':
497
489
  settings = get_analyze_recruitment_default_settings(settings={})
490
+ elif settings_type == 'activation':
491
+ settings = get_default_generate_activation_map_settings(settings={})
498
492
  elif settings_type == 'analyze_plaques':
499
- settings = {}
493
+ settings = {'src':'path to images'}
494
+ elif settings_type == 'convert':
495
+ settings = {'src':'path to images'}
500
496
  else:
501
497
  raise ValueError(f"Invalid settings type: {settings_type}")
502
498
 
@@ -515,7 +511,7 @@ def setup_settings_panel(vertical_container, settings_type='mask'):
515
511
  def setup_console(vertical_container):
516
512
  global console_output
517
513
  from .gui_elements import set_dark_style
518
-
514
+
519
515
  # Apply dark style and get style output
520
516
  style = ttk.Style()
521
517
  style_out = set_dark_style(style)
@@ -546,9 +542,27 @@ def setup_console(vertical_container):
546
542
  def on_leave(event):
547
543
  top_border.config(bg=style_out['bg_color'])
548
544
 
545
+ #def on_enter_key(event):
546
+ # user_input = console_output.get("1.0", "end-1c").strip() # Get the user input from the console
547
+ # if user_input:
548
+ # # Print the user input with the (user) tag
549
+ # console_output.insert("end", f"\n(user): {user_input}\n")
550
+ #
551
+ # # Get the AI response from the chatbot
552
+ # response = chatbot.ask_question(user_input)
553
+ #
554
+ # # Print the AI response with the (ai) tag
555
+ # console_output.insert("end", f"(ai): {response}\n")
556
+ #
557
+ # console_output.see("end") # Scroll to the end
558
+ # #console_output.delete("1.0", "end") # Clear the input field
559
+ # return "break" # Prevent the default behavior of inserting a new line
560
+
549
561
  console_output.bind("<Enter>", on_enter)
550
562
  console_output.bind("<Leave>", on_leave)
551
563
 
564
+ #console_output.bind("<Return>", on_enter_key)
565
+
552
566
  return console_output, console_frame
553
567
 
554
568
  def setup_button_section(horizontal_container, settings_type='mask', run=True, abort=True, download=True, import_btn=True):
@@ -755,7 +769,7 @@ def initiate_abort():
755
769
  def start_process(q=None, fig_queue=None, settings_type='mask'):
756
770
  global thread_control, vars_dict, parent_frame
757
771
  from .settings import check_settings, expected_types
758
- from .gui_utils import run_function_gui, set_high_priority, set_cpu_affinity, initialize_cuda
772
+ from .gui_utils import run_function_gui, set_cpu_affinity, initialize_cuda, display_gif_in_plot_frame, print_widget_structure
759
773
 
760
774
  if q is None:
761
775
  q = Queue()
@@ -778,16 +792,14 @@ def start_process(q=None, fig_queue=None, settings_type='mask'):
778
792
 
779
793
  process_args = (settings_type, settings, q, fig_queue, stop_requested)
780
794
  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']:
795
+ 'cellpose_dataset', 'train_cellpose', 'ml_analyze', 'cellpose_masks', 'cellpose_all',
796
+ 'map_barcodes', 'regression', 'recruitment', 'cellpose_compare', 'vision_scores',
797
+ 'vision_dataset', 'convert']:
783
798
 
784
799
  # Start the process
785
800
  process = Process(target=run_function_gui, args=process_args)
786
801
  process.start()
787
802
 
788
- # Set high priority for the process
789
- #set_high_priority(process)
790
-
791
803
  # Set CPU affinity if necessary
792
804
  set_cpu_affinity(process)
793
805
 
@@ -889,10 +901,14 @@ def initiate_root(parent, settings_type='mask'):
889
901
 
890
902
  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
903
 
892
- from .gui_utils import setup_frame
904
+ from .gui_utils import setup_frame, get_screen_dimensions
893
905
  from .settings import descriptions
906
+ #from .openai import Chatbot
894
907
 
895
908
  uppdate_frequency = 500
909
+ num_cores = os.cpu_count()
910
+
911
+ #chatbot = Chatbot(api_key="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")
896
912
 
897
913
  # Start tracemalloc and initialize global variables
898
914
  tracemalloc.start()
@@ -930,10 +946,14 @@ def initiate_root(parent, settings_type='mask'):
930
946
  else:
931
947
  scrollable_frame, vars_dict = setup_settings_panel(settings_container, settings_type)
932
948
  print('setup_settings_panel')
933
- canvas, canvas_widget = setup_plot_section(vertical_container)
934
- console_output, _ = setup_console(vertical_container)
949
+ canvas, canvas_widget = setup_plot_section(vertical_container, settings_type)
950
+ console_output, _ = setup_console(vertical_container) #, chatbot)
935
951
  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)
952
+
953
+ if num_cores > 12:
954
+ _, usage_bars, btn_col = setup_usage_panel(horizontal_container, btn_col, uppdate_frequency)
955
+ else:
956
+ usage_bars = []
937
957
 
938
958
  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
959
  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)