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.
- spacr/__init__.py +19 -3
- spacr/cellpose.py +311 -0
- spacr/core.py +245 -2494
- spacr/deep_spacr.py +335 -163
- spacr/gui.py +2 -0
- spacr/gui_core.py +85 -65
- spacr/gui_elements.py +110 -5
- spacr/gui_utils.py +375 -7
- spacr/io.py +680 -141
- spacr/logger.py +28 -9
- spacr/measure.py +108 -133
- spacr/mediar.py +0 -3
- spacr/ml.py +1051 -0
- spacr/openai.py +37 -0
- spacr/plot.py +707 -20
- spacr/resources/data/lopit.csv +3833 -0
- spacr/resources/data/toxoplasma_metadata.csv +8843 -0
- spacr/resources/icons/convert.png +0 -0
- spacr/resources/{models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model → icons/dna_matrix.mp4} +0 -0
- spacr/sequencing.py +241 -1311
- spacr/settings.py +181 -50
- spacr/sim.py +0 -2
- spacr/submodules.py +349 -0
- spacr/timelapse.py +0 -2
- spacr/toxo.py +238 -0
- spacr/utils.py +776 -182
- {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/METADATA +31 -22
- {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/RECORD +32 -33
- spacr/chris.py +0 -50
- spacr/graph_learning.py +0 -340
- spacr/resources/MEDIAR/.git +0 -1
- spacr/resources/MEDIAR_weights/.DS_Store +0 -0
- spacr/resources/icons/.DS_Store +0 -0
- spacr/resources/icons/spacr_logo_rotation.gif +0 -0
- spacr/resources/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model_settings.csv +0 -23
- spacr/resources/models/cp/toxo_pv_lumen.CP_model +0 -0
- spacr/sim_app.py +0 -0
- {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/LICENSE +0 -0
- {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/WHEEL +0 -0
- {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
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,
|
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
|
-
#
|
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
|
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)
|
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)
|
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
|
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,
|
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',
|
782
|
-
'regression', 'recruitment', 'cellpose_compare', 'vision_scores',
|
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
|
-
|
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)
|