spacr 0.3.1__py3-none-any.whl → 0.3.2__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 +140 -2493
- spacr/deep_spacr.py +151 -29
- spacr/gui.py +1 -0
- spacr/gui_core.py +74 -63
- spacr/gui_elements.py +110 -5
- spacr/gui_utils.py +346 -6
- spacr/io.py +624 -44
- spacr/logger.py +28 -9
- spacr/measure.py +107 -95
- spacr/mediar.py +0 -3
- spacr/ml.py +964 -0
- spacr/openai.py +37 -0
- spacr/plot.py +280 -15
- 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 +129 -43
- spacr/sim.py +0 -2
- spacr/submodules.py +348 -0
- spacr/timelapse.py +0 -2
- spacr/toxo.py +233 -0
- spacr/utils.py +271 -171
- {spacr-0.3.1.dist-info → spacr-0.3.2.dist-info}/METADATA +7 -1
- {spacr-0.3.1.dist-info → spacr-0.3.2.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.2.dist-info}/LICENSE +0 -0
- {spacr-0.3.1.dist-info → spacr-0.3.2.dist-info}/WHEEL +0 -0
- {spacr-0.3.1.dist-info → spacr-0.3.2.dist-info}/entry_points.txt +0 -0
- {spacr-0.3.1.dist-info → spacr-0.3.2.dist-info}/top_level.txt +0 -0
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)
|
spacr/gui_utils.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import os, io, sys, ast, ctypes, ast, sqlite3, requests, time, traceback, torch
|
1
|
+
import os, io, sys, ast, ctypes, ast, sqlite3, requests, time, traceback, torch, cv2
|
2
2
|
import tkinter as tk
|
3
3
|
from tkinter import ttk
|
4
4
|
import matplotlib
|
@@ -6,6 +6,8 @@ import matplotlib.pyplot as plt
|
|
6
6
|
matplotlib.use('Agg')
|
7
7
|
from huggingface_hub import list_repo_files
|
8
8
|
import psutil
|
9
|
+
from PIL import Image, ImageTk
|
10
|
+
from screeninfo import get_monitors
|
9
11
|
|
10
12
|
from .gui_elements import AnnotateApp, spacrEntry, spacrCheck, spacrCombo
|
11
13
|
|
@@ -394,6 +396,7 @@ def convert_settings_dict_for_gui(settings):
|
|
394
396
|
'train_channels': ('combo', ["['r','g','b']", "['r','g']", "['r','b']", "['g','b']", "['r']", "['g']", "['b']"], "['r','g','b']"),
|
395
397
|
'channel_dims': ('combo', ['[0,1,2,3]', '[0,1,2]', '[0,1]', '[0]'], '[0,1,2,3]'),
|
396
398
|
'dataset_mode': ('combo', ['annotation', 'metadata', 'recruitment'], 'metadata'),
|
399
|
+
'cov_type': ('combo', ['HC0', 'HC1', 'HC2', 'HC3', None], None),
|
397
400
|
'cell_mask_dim': ('combo', chans, None),
|
398
401
|
'cell_chann_dim': ('combo', chans, None),
|
399
402
|
'nucleus_mask_dim': ('combo', chans, None),
|
@@ -486,12 +489,16 @@ def function_gui_wrapper(function=None, settings={}, q=None, fig_queue=None, imp
|
|
486
489
|
def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
|
487
490
|
|
488
491
|
from .gui_utils import process_stdout_stderr
|
489
|
-
from .core import generate_image_umap, preprocess_generate_masks
|
490
|
-
from .
|
492
|
+
from .core import generate_image_umap, preprocess_generate_masks
|
493
|
+
from .cellpose import identify_masks_finetune, check_cellpose_models, compare_cellpose_masks
|
494
|
+
from .submodules import analyze_recruitment
|
495
|
+
from .ml import generate_ml_scores, perform_regression
|
496
|
+
from .submodules import train_cellpose, analyze_plaques
|
497
|
+
from .io import process_non_tif_non_2D_images, generate_cellpose_train_test, generate_dataset
|
491
498
|
from .measure import measure_crop
|
492
499
|
from .sim import run_multiple_simulations
|
493
|
-
from .deep_spacr import deep_spacr
|
494
|
-
from .sequencing import generate_barecode_mapping
|
500
|
+
from .deep_spacr import deep_spacr, apply_model_to_tar
|
501
|
+
from .sequencing import generate_barecode_mapping
|
495
502
|
process_stdout_stderr(q)
|
496
503
|
|
497
504
|
print(f'run_function_gui settings_type: {settings_type}')
|
@@ -535,6 +542,9 @@ def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
|
|
535
542
|
elif settings_type == 'analyze_plaques':
|
536
543
|
function = analyze_plaques
|
537
544
|
imports = 1
|
545
|
+
elif settings_type == 'convert':
|
546
|
+
function = process_non_tif_non_2D_images
|
547
|
+
imports = 1
|
538
548
|
else:
|
539
549
|
raise ValueError(f"Invalid settings type: {settings_type}")
|
540
550
|
try:
|
@@ -697,4 +707,334 @@ def download_dataset(q, repo_id, subfolder, local_dir=None, retries=5, delay=5):
|
|
697
707
|
|
698
708
|
def ensure_after_tasks(frame):
|
699
709
|
if not hasattr(frame, 'after_tasks'):
|
700
|
-
frame.after_tasks = []
|
710
|
+
frame.after_tasks = []
|
711
|
+
|
712
|
+
def display_gif_in_plot_frame_v1(gif_path, parent_frame):
|
713
|
+
"""Display and zoom a GIF to fill the entire parent_frame, maintaining aspect ratio, with lazy resizing and caching."""
|
714
|
+
# Clear parent_frame if it contains any previous widgets
|
715
|
+
for widget in parent_frame.winfo_children():
|
716
|
+
widget.destroy()
|
717
|
+
|
718
|
+
# Load the GIF
|
719
|
+
gif = Image.open(gif_path)
|
720
|
+
|
721
|
+
# Get the aspect ratio of the GIF
|
722
|
+
gif_width, gif_height = gif.size
|
723
|
+
gif_aspect_ratio = gif_width / gif_height
|
724
|
+
|
725
|
+
# Create a label to display the GIF and configure it to fill the parent_frame
|
726
|
+
label = tk.Label(parent_frame, bg="black")
|
727
|
+
label.grid(row=0, column=0, sticky="nsew") # Expands in all directions (north, south, east, west)
|
728
|
+
|
729
|
+
# Configure parent_frame to stretch the label to fill available space
|
730
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
731
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
732
|
+
|
733
|
+
# Cache for storing resized frames (lazily filled)
|
734
|
+
resized_frames_cache = {}
|
735
|
+
|
736
|
+
# Last frame dimensions
|
737
|
+
last_frame_width = 0
|
738
|
+
last_frame_height = 0
|
739
|
+
|
740
|
+
def resize_and_crop_frame(frame_idx, frame_width, frame_height):
|
741
|
+
"""Resize and crop the current frame of the GIF to fit the parent_frame while maintaining the aspect ratio."""
|
742
|
+
# If the frame is already cached at the current size, return it
|
743
|
+
if (frame_idx, frame_width, frame_height) in resized_frames_cache:
|
744
|
+
return resized_frames_cache[(frame_idx, frame_width, frame_height)]
|
745
|
+
|
746
|
+
# Calculate the scaling factor to zoom in on the GIF
|
747
|
+
scale_factor = max(frame_width / gif_width, frame_height / gif_height)
|
748
|
+
|
749
|
+
# Calculate new dimensions while maintaining the aspect ratio
|
750
|
+
new_width = int(gif_width * scale_factor)
|
751
|
+
new_height = int(gif_height * scale_factor)
|
752
|
+
|
753
|
+
# Resize the GIF to fit the frame
|
754
|
+
gif.seek(frame_idx)
|
755
|
+
resized_gif = gif.copy().resize((new_width, new_height), Image.Resampling.LANCZOS)
|
756
|
+
|
757
|
+
# Calculate the cropping box to center the resized GIF in the frame
|
758
|
+
crop_left = (new_width - frame_width) // 2
|
759
|
+
crop_top = (new_height - frame_height) // 2
|
760
|
+
crop_right = crop_left + frame_width
|
761
|
+
crop_bottom = crop_top + frame_height
|
762
|
+
|
763
|
+
# Crop the resized GIF to exactly fit the frame
|
764
|
+
cropped_gif = resized_gif.crop((crop_left, crop_top, crop_right, crop_bottom))
|
765
|
+
|
766
|
+
# Convert the cropped frame to a Tkinter-compatible format
|
767
|
+
frame_image = ImageTk.PhotoImage(cropped_gif)
|
768
|
+
|
769
|
+
# Cache the resized frame
|
770
|
+
resized_frames_cache[(frame_idx, frame_width, frame_height)] = frame_image
|
771
|
+
|
772
|
+
return frame_image
|
773
|
+
|
774
|
+
def update_frame(frame_idx):
|
775
|
+
"""Update the GIF frame using lazy resizing and caching."""
|
776
|
+
# Get the current size of the parent_frame
|
777
|
+
frame_width = parent_frame.winfo_width()
|
778
|
+
frame_height = parent_frame.winfo_height()
|
779
|
+
|
780
|
+
# Only resize if the frame size has changed
|
781
|
+
nonlocal last_frame_width, last_frame_height
|
782
|
+
if frame_width != last_frame_width or frame_height != last_frame_height:
|
783
|
+
last_frame_width, last_frame_height = frame_width, frame_height
|
784
|
+
|
785
|
+
# Get the resized and cropped frame image
|
786
|
+
frame_image = resize_and_crop_frame(frame_idx, frame_width, frame_height)
|
787
|
+
label.config(image=frame_image)
|
788
|
+
label.image = frame_image # Keep a reference to avoid garbage collection
|
789
|
+
|
790
|
+
# Move to the next frame, or loop back to the beginning
|
791
|
+
next_frame_idx = (frame_idx + 1) % gif.n_frames
|
792
|
+
parent_frame.after(gif.info['duration'], update_frame, next_frame_idx)
|
793
|
+
|
794
|
+
# Start the GIF animation from frame 0
|
795
|
+
update_frame(0)
|
796
|
+
|
797
|
+
def display_gif_in_plot_frame(gif_path, parent_frame):
|
798
|
+
"""Display and zoom a GIF to fill the entire parent_frame, maintaining aspect ratio, with lazy resizing and caching."""
|
799
|
+
# Clear parent_frame if it contains any previous widgets
|
800
|
+
for widget in parent_frame.winfo_children():
|
801
|
+
widget.destroy()
|
802
|
+
|
803
|
+
# Load the GIF
|
804
|
+
gif = Image.open(gif_path)
|
805
|
+
|
806
|
+
# Get the aspect ratio of the GIF
|
807
|
+
gif_width, gif_height = gif.size
|
808
|
+
gif_aspect_ratio = gif_width / gif_height
|
809
|
+
|
810
|
+
# Create a label to display the GIF and configure it to fill the parent_frame
|
811
|
+
label = tk.Label(parent_frame, bg="black")
|
812
|
+
label.grid(row=0, column=0, sticky="nsew") # Expands in all directions (north, south, east, west)
|
813
|
+
|
814
|
+
# Configure parent_frame to stretch the label to fill available space
|
815
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
816
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
817
|
+
|
818
|
+
# Cache for storing resized frames (lazily filled)
|
819
|
+
resized_frames_cache = {}
|
820
|
+
|
821
|
+
# Store last frame size and aspect ratio
|
822
|
+
last_frame_width = 0
|
823
|
+
last_frame_height = 0
|
824
|
+
|
825
|
+
def resize_and_crop_frame(frame_idx, frame_width, frame_height):
|
826
|
+
"""Resize and crop the current frame of the GIF to fit the parent_frame while maintaining the aspect ratio."""
|
827
|
+
# If the frame is already cached at the current size, return it
|
828
|
+
if (frame_idx, frame_width, frame_height) in resized_frames_cache:
|
829
|
+
return resized_frames_cache[(frame_idx, frame_width, frame_height)]
|
830
|
+
|
831
|
+
# Calculate the scaling factor to zoom in on the GIF
|
832
|
+
scale_factor = max(frame_width / gif_width, frame_height / gif_height)
|
833
|
+
|
834
|
+
# Calculate new dimensions while maintaining the aspect ratio
|
835
|
+
new_width = int(gif_width * scale_factor)
|
836
|
+
new_height = int(gif_height * scale_factor)
|
837
|
+
|
838
|
+
# Resize the GIF to fit the frame using NEAREST for faster resizing
|
839
|
+
gif.seek(frame_idx)
|
840
|
+
resized_gif = gif.copy().resize((new_width, new_height), Image.Resampling.NEAREST if scale_factor > 2 else Image.Resampling.LANCZOS)
|
841
|
+
|
842
|
+
# Calculate the cropping box to center the resized GIF in the frame
|
843
|
+
crop_left = (new_width - frame_width) // 2
|
844
|
+
crop_top = (new_height - frame_height) // 2
|
845
|
+
crop_right = crop_left + frame_width
|
846
|
+
crop_bottom = crop_top + frame_height
|
847
|
+
|
848
|
+
# Crop the resized GIF to exactly fit the frame
|
849
|
+
cropped_gif = resized_gif.crop((crop_left, crop_top, crop_right, crop_bottom))
|
850
|
+
|
851
|
+
# Convert the cropped frame to a Tkinter-compatible format
|
852
|
+
frame_image = ImageTk.PhotoImage(cropped_gif)
|
853
|
+
|
854
|
+
# Cache the resized frame
|
855
|
+
resized_frames_cache[(frame_idx, frame_width, frame_height)] = frame_image
|
856
|
+
|
857
|
+
return frame_image
|
858
|
+
|
859
|
+
def update_frame(frame_idx):
|
860
|
+
"""Update the GIF frame using lazy resizing and caching."""
|
861
|
+
# Get the current size of the parent_frame
|
862
|
+
frame_width = parent_frame.winfo_width()
|
863
|
+
frame_height = parent_frame.winfo_height()
|
864
|
+
|
865
|
+
# Only resize if the frame size has changed
|
866
|
+
nonlocal last_frame_width, last_frame_height
|
867
|
+
if frame_width != last_frame_width or frame_height != last_frame_height:
|
868
|
+
last_frame_width, last_frame_height = frame_width, frame_height
|
869
|
+
|
870
|
+
# Get the resized and cropped frame image
|
871
|
+
frame_image = resize_and_crop_frame(frame_idx, frame_width, frame_height)
|
872
|
+
label.config(image=frame_image)
|
873
|
+
label.image = frame_image # Keep a reference to avoid garbage collection
|
874
|
+
|
875
|
+
# Move to the next frame, or loop back to the beginning
|
876
|
+
next_frame_idx = (frame_idx + 1) % gif.n_frames
|
877
|
+
parent_frame.after(gif.info['duration'], update_frame, next_frame_idx)
|
878
|
+
|
879
|
+
# Start the GIF animation from frame 0
|
880
|
+
update_frame(0)
|
881
|
+
|
882
|
+
def display_media_in_plot_frame(media_path, parent_frame):
|
883
|
+
"""Display an MP4, AVI, or GIF and play it on repeat in the parent_frame, fully filling the frame while maintaining aspect ratio."""
|
884
|
+
# Clear parent_frame if it contains any previous widgets
|
885
|
+
for widget in parent_frame.winfo_children():
|
886
|
+
widget.destroy()
|
887
|
+
|
888
|
+
# Check file extension to decide between video (mp4/avi) or gif
|
889
|
+
file_extension = os.path.splitext(media_path)[1].lower()
|
890
|
+
|
891
|
+
if file_extension in ['.mp4', '.avi']:
|
892
|
+
# Handle video formats (mp4, avi) using OpenCV
|
893
|
+
video = cv2.VideoCapture(media_path)
|
894
|
+
|
895
|
+
# Create a label to display the video
|
896
|
+
label = tk.Label(parent_frame, bg="black")
|
897
|
+
label.grid(row=0, column=0, sticky="nsew")
|
898
|
+
|
899
|
+
# Configure the parent_frame to expand
|
900
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
901
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
902
|
+
|
903
|
+
def update_frame():
|
904
|
+
"""Update function for playing video."""
|
905
|
+
ret, frame = video.read()
|
906
|
+
if ret:
|
907
|
+
# Get the frame dimensions
|
908
|
+
frame_height, frame_width, _ = frame.shape
|
909
|
+
|
910
|
+
# Get parent frame dimensions
|
911
|
+
parent_width = parent_frame.winfo_width()
|
912
|
+
parent_height = parent_frame.winfo_height()
|
913
|
+
|
914
|
+
# Ensure dimensions are greater than 0
|
915
|
+
if parent_width > 0 and parent_height > 0:
|
916
|
+
# Calculate the aspect ratio of the media
|
917
|
+
frame_aspect_ratio = frame_width / frame_height
|
918
|
+
parent_aspect_ratio = parent_width / parent_height
|
919
|
+
|
920
|
+
# Determine whether to scale based on width or height to cover the parent frame
|
921
|
+
if parent_aspect_ratio > frame_aspect_ratio:
|
922
|
+
# The parent frame is wider than the video aspect ratio
|
923
|
+
# Fit to width, crop height
|
924
|
+
new_width = parent_width
|
925
|
+
new_height = int(parent_width / frame_aspect_ratio)
|
926
|
+
else:
|
927
|
+
# The parent frame is taller than the video aspect ratio
|
928
|
+
# Fit to height, crop width
|
929
|
+
new_width = int(parent_height * frame_aspect_ratio)
|
930
|
+
new_height = parent_height
|
931
|
+
|
932
|
+
# Resize the frame to the new dimensions (cover the parent frame)
|
933
|
+
resized_frame = cv2.resize(frame, (new_width, new_height))
|
934
|
+
|
935
|
+
# Crop the frame to fit exactly within the parent frame
|
936
|
+
x_offset = (new_width - parent_width) // 2
|
937
|
+
y_offset = (new_height - parent_height) // 2
|
938
|
+
cropped_frame = resized_frame[y_offset:y_offset + parent_height, x_offset:x_offset + parent_width]
|
939
|
+
|
940
|
+
# Convert the frame to RGB (OpenCV uses BGR by default)
|
941
|
+
cropped_frame = cv2.cvtColor(cropped_frame, cv2.COLOR_BGR2RGB)
|
942
|
+
|
943
|
+
# Convert the frame to a Tkinter-compatible format
|
944
|
+
frame_image = ImageTk.PhotoImage(Image.fromarray(cropped_frame))
|
945
|
+
|
946
|
+
# Update the label with the new frame
|
947
|
+
label.config(image=frame_image)
|
948
|
+
label.image = frame_image # Keep a reference to avoid garbage collection
|
949
|
+
|
950
|
+
# Call update_frame again after a delay to match the video's frame rate
|
951
|
+
parent_frame.after(int(1000 / video.get(cv2.CAP_PROP_FPS)), update_frame)
|
952
|
+
else:
|
953
|
+
# Restart the video if it reaches the end
|
954
|
+
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
955
|
+
update_frame()
|
956
|
+
|
957
|
+
# Start the video playback
|
958
|
+
update_frame()
|
959
|
+
|
960
|
+
elif file_extension == '.gif':
|
961
|
+
# Handle GIF format using PIL
|
962
|
+
gif = Image.open(media_path)
|
963
|
+
|
964
|
+
# Create a label to display the GIF
|
965
|
+
label = tk.Label(parent_frame, bg="black")
|
966
|
+
label.grid(row=0, column=0, sticky="nsew")
|
967
|
+
|
968
|
+
# Configure the parent_frame to expand
|
969
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
970
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
971
|
+
|
972
|
+
def update_gif_frame(frame_idx):
|
973
|
+
"""Update function for playing GIF."""
|
974
|
+
try:
|
975
|
+
gif.seek(frame_idx) # Move to the next frame
|
976
|
+
|
977
|
+
# Get the frame dimensions
|
978
|
+
gif_width, gif_height = gif.size
|
979
|
+
|
980
|
+
# Get parent frame dimensions
|
981
|
+
parent_width = parent_frame.winfo_width()
|
982
|
+
parent_height = parent_frame.winfo_height()
|
983
|
+
|
984
|
+
# Ensure dimensions are greater than 0
|
985
|
+
if parent_width > 0 and parent_height > 0:
|
986
|
+
# Calculate the aspect ratio of the GIF
|
987
|
+
gif_aspect_ratio = gif_width / gif_height
|
988
|
+
parent_aspect_ratio = parent_width / parent_height
|
989
|
+
|
990
|
+
# Determine whether to scale based on width or height to cover the parent frame
|
991
|
+
if parent_aspect_ratio > gif_aspect_ratio:
|
992
|
+
# Fit to width, crop height
|
993
|
+
new_width = parent_width
|
994
|
+
new_height = int(parent_width / gif_aspect_ratio)
|
995
|
+
else:
|
996
|
+
# Fit to height, crop width
|
997
|
+
new_width = int(parent_height * gif_aspect_ratio)
|
998
|
+
new_height = parent_height
|
999
|
+
|
1000
|
+
# Resize the GIF frame to cover the parent frame
|
1001
|
+
resized_gif = gif.copy().resize((new_width, new_height), Image.Resampling.LANCZOS)
|
1002
|
+
|
1003
|
+
# Crop the resized GIF to fit the exact parent frame dimensions
|
1004
|
+
x_offset = (new_width - parent_width) // 2
|
1005
|
+
y_offset = (new_height - parent_height) // 2
|
1006
|
+
cropped_gif = resized_gif.crop((x_offset, y_offset, x_offset + parent_width, y_offset + parent_height))
|
1007
|
+
|
1008
|
+
# Convert the frame to a Tkinter-compatible format
|
1009
|
+
frame_image = ImageTk.PhotoImage(cropped_gif)
|
1010
|
+
|
1011
|
+
# Update the label with the new frame
|
1012
|
+
label.config(image=frame_image)
|
1013
|
+
label.image = frame_image # Keep a reference to avoid garbage collection
|
1014
|
+
frame_idx += 1
|
1015
|
+
except EOFError:
|
1016
|
+
frame_idx = 0 # Restart the GIF if at the end
|
1017
|
+
|
1018
|
+
# Schedule the next frame update
|
1019
|
+
parent_frame.after(gif.info['duration'], update_gif_frame, frame_idx)
|
1020
|
+
|
1021
|
+
# Start the GIF animation from frame 0
|
1022
|
+
update_gif_frame(0)
|
1023
|
+
|
1024
|
+
else:
|
1025
|
+
raise ValueError("Unsupported file format. Only .mp4, .avi, and .gif are supported.")
|
1026
|
+
|
1027
|
+
def print_widget_structure(widget, indent=0):
|
1028
|
+
"""Recursively print the widget structure."""
|
1029
|
+
# Print the widget's name and class
|
1030
|
+
print(" " * indent + f"{widget}: {widget.winfo_class()}")
|
1031
|
+
|
1032
|
+
# Recursively print all child widgets
|
1033
|
+
for child_name, child_widget in widget.children.items():
|
1034
|
+
print_widget_structure(child_widget, indent + 2)
|
1035
|
+
|
1036
|
+
def get_screen_dimensions():
|
1037
|
+
monitor = get_monitors()[0] # Get the primary monitor
|
1038
|
+
screen_width = monitor.width
|
1039
|
+
screen_height = monitor.height
|
1040
|
+
return screen_width, screen_height
|