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_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
|
|
@@ -75,7 +77,7 @@ def load_app(root, app_name, app_func):
|
|
75
77
|
else:
|
76
78
|
proceed_with_app(root, app_name, app_func)
|
77
79
|
|
78
|
-
def
|
80
|
+
def parse_list_v1(value):
|
79
81
|
"""
|
80
82
|
Parses a string representation of a list and returns the parsed list.
|
81
83
|
|
@@ -96,6 +98,34 @@ def parse_list(value):
|
|
96
98
|
return parsed_value
|
97
99
|
elif all(isinstance(item, str) for item in parsed_value):
|
98
100
|
return parsed_value
|
101
|
+
elif all(isinstance(item, float) for item in parsed_value):
|
102
|
+
return parsed_value
|
103
|
+
else:
|
104
|
+
raise ValueError("List contains mixed types or unsupported types")
|
105
|
+
else:
|
106
|
+
raise ValueError(f"Expected a list but got {type(parsed_value).__name__}")
|
107
|
+
except (ValueError, SyntaxError) as e:
|
108
|
+
raise ValueError(f"Invalid format for list: {value}. Error: {e}")
|
109
|
+
|
110
|
+
def parse_list(value):
|
111
|
+
"""
|
112
|
+
Parses a string representation of a list and returns the parsed list.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
value (str): The string representation of the list.
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
list: The parsed list, which can contain integers, floats, or strings.
|
119
|
+
|
120
|
+
Raises:
|
121
|
+
ValueError: If the input value is not a valid list format or contains mixed types or unsupported types.
|
122
|
+
"""
|
123
|
+
try:
|
124
|
+
parsed_value = ast.literal_eval(value)
|
125
|
+
if isinstance(parsed_value, list):
|
126
|
+
# Check if all elements are homogeneous (either all int, float, or str)
|
127
|
+
if all(isinstance(item, (int, float, str)) for item in parsed_value):
|
128
|
+
return parsed_value
|
99
129
|
else:
|
100
130
|
raise ValueError("List contains mixed types or unsupported types")
|
101
131
|
else:
|
@@ -394,6 +424,7 @@ def convert_settings_dict_for_gui(settings):
|
|
394
424
|
'train_channels': ('combo', ["['r','g','b']", "['r','g']", "['r','b']", "['g','b']", "['r']", "['g']", "['b']"], "['r','g','b']"),
|
395
425
|
'channel_dims': ('combo', ['[0,1,2,3]', '[0,1,2]', '[0,1]', '[0]'], '[0,1,2,3]'),
|
396
426
|
'dataset_mode': ('combo', ['annotation', 'metadata', 'recruitment'], 'metadata'),
|
427
|
+
'cov_type': ('combo', ['HC0', 'HC1', 'HC2', 'HC3', None], None),
|
397
428
|
'cell_mask_dim': ('combo', chans, None),
|
398
429
|
'cell_chann_dim': ('combo', chans, None),
|
399
430
|
'nucleus_mask_dim': ('combo', chans, None),
|
@@ -486,12 +517,16 @@ def function_gui_wrapper(function=None, settings={}, q=None, fig_queue=None, imp
|
|
486
517
|
def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
|
487
518
|
|
488
519
|
from .gui_utils import process_stdout_stderr
|
489
|
-
from .core import generate_image_umap, preprocess_generate_masks
|
490
|
-
from .
|
520
|
+
from .core import generate_image_umap, preprocess_generate_masks
|
521
|
+
from .cellpose import identify_masks_finetune, check_cellpose_models, compare_cellpose_masks
|
522
|
+
from .submodules import analyze_recruitment
|
523
|
+
from .ml import generate_ml_scores, perform_regression
|
524
|
+
from .submodules import train_cellpose, analyze_plaques
|
525
|
+
from .io import process_non_tif_non_2D_images, generate_cellpose_train_test, generate_dataset
|
491
526
|
from .measure import measure_crop
|
492
527
|
from .sim import run_multiple_simulations
|
493
|
-
from .deep_spacr import deep_spacr
|
494
|
-
from .sequencing import generate_barecode_mapping
|
528
|
+
from .deep_spacr import deep_spacr, apply_model_to_tar
|
529
|
+
from .sequencing import generate_barecode_mapping
|
495
530
|
process_stdout_stderr(q)
|
496
531
|
|
497
532
|
print(f'run_function_gui settings_type: {settings_type}')
|
@@ -535,6 +570,9 @@ def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
|
|
535
570
|
elif settings_type == 'analyze_plaques':
|
536
571
|
function = analyze_plaques
|
537
572
|
imports = 1
|
573
|
+
elif settings_type == 'convert':
|
574
|
+
function = process_non_tif_non_2D_images
|
575
|
+
imports = 1
|
538
576
|
else:
|
539
577
|
raise ValueError(f"Invalid settings type: {settings_type}")
|
540
578
|
try:
|
@@ -697,4 +735,334 @@ def download_dataset(q, repo_id, subfolder, local_dir=None, retries=5, delay=5):
|
|
697
735
|
|
698
736
|
def ensure_after_tasks(frame):
|
699
737
|
if not hasattr(frame, 'after_tasks'):
|
700
|
-
frame.after_tasks = []
|
738
|
+
frame.after_tasks = []
|
739
|
+
|
740
|
+
def display_gif_in_plot_frame_v1(gif_path, parent_frame):
|
741
|
+
"""Display and zoom a GIF to fill the entire parent_frame, maintaining aspect ratio, with lazy resizing and caching."""
|
742
|
+
# Clear parent_frame if it contains any previous widgets
|
743
|
+
for widget in parent_frame.winfo_children():
|
744
|
+
widget.destroy()
|
745
|
+
|
746
|
+
# Load the GIF
|
747
|
+
gif = Image.open(gif_path)
|
748
|
+
|
749
|
+
# Get the aspect ratio of the GIF
|
750
|
+
gif_width, gif_height = gif.size
|
751
|
+
gif_aspect_ratio = gif_width / gif_height
|
752
|
+
|
753
|
+
# Create a label to display the GIF and configure it to fill the parent_frame
|
754
|
+
label = tk.Label(parent_frame, bg="black")
|
755
|
+
label.grid(row=0, column=0, sticky="nsew") # Expands in all directions (north, south, east, west)
|
756
|
+
|
757
|
+
# Configure parent_frame to stretch the label to fill available space
|
758
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
759
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
760
|
+
|
761
|
+
# Cache for storing resized frames (lazily filled)
|
762
|
+
resized_frames_cache = {}
|
763
|
+
|
764
|
+
# Last frame dimensions
|
765
|
+
last_frame_width = 0
|
766
|
+
last_frame_height = 0
|
767
|
+
|
768
|
+
def resize_and_crop_frame(frame_idx, frame_width, frame_height):
|
769
|
+
"""Resize and crop the current frame of the GIF to fit the parent_frame while maintaining the aspect ratio."""
|
770
|
+
# If the frame is already cached at the current size, return it
|
771
|
+
if (frame_idx, frame_width, frame_height) in resized_frames_cache:
|
772
|
+
return resized_frames_cache[(frame_idx, frame_width, frame_height)]
|
773
|
+
|
774
|
+
# Calculate the scaling factor to zoom in on the GIF
|
775
|
+
scale_factor = max(frame_width / gif_width, frame_height / gif_height)
|
776
|
+
|
777
|
+
# Calculate new dimensions while maintaining the aspect ratio
|
778
|
+
new_width = int(gif_width * scale_factor)
|
779
|
+
new_height = int(gif_height * scale_factor)
|
780
|
+
|
781
|
+
# Resize the GIF to fit the frame
|
782
|
+
gif.seek(frame_idx)
|
783
|
+
resized_gif = gif.copy().resize((new_width, new_height), Image.Resampling.LANCZOS)
|
784
|
+
|
785
|
+
# Calculate the cropping box to center the resized GIF in the frame
|
786
|
+
crop_left = (new_width - frame_width) // 2
|
787
|
+
crop_top = (new_height - frame_height) // 2
|
788
|
+
crop_right = crop_left + frame_width
|
789
|
+
crop_bottom = crop_top + frame_height
|
790
|
+
|
791
|
+
# Crop the resized GIF to exactly fit the frame
|
792
|
+
cropped_gif = resized_gif.crop((crop_left, crop_top, crop_right, crop_bottom))
|
793
|
+
|
794
|
+
# Convert the cropped frame to a Tkinter-compatible format
|
795
|
+
frame_image = ImageTk.PhotoImage(cropped_gif)
|
796
|
+
|
797
|
+
# Cache the resized frame
|
798
|
+
resized_frames_cache[(frame_idx, frame_width, frame_height)] = frame_image
|
799
|
+
|
800
|
+
return frame_image
|
801
|
+
|
802
|
+
def update_frame(frame_idx):
|
803
|
+
"""Update the GIF frame using lazy resizing and caching."""
|
804
|
+
# Get the current size of the parent_frame
|
805
|
+
frame_width = parent_frame.winfo_width()
|
806
|
+
frame_height = parent_frame.winfo_height()
|
807
|
+
|
808
|
+
# Only resize if the frame size has changed
|
809
|
+
nonlocal last_frame_width, last_frame_height
|
810
|
+
if frame_width != last_frame_width or frame_height != last_frame_height:
|
811
|
+
last_frame_width, last_frame_height = frame_width, frame_height
|
812
|
+
|
813
|
+
# Get the resized and cropped frame image
|
814
|
+
frame_image = resize_and_crop_frame(frame_idx, frame_width, frame_height)
|
815
|
+
label.config(image=frame_image)
|
816
|
+
label.image = frame_image # Keep a reference to avoid garbage collection
|
817
|
+
|
818
|
+
# Move to the next frame, or loop back to the beginning
|
819
|
+
next_frame_idx = (frame_idx + 1) % gif.n_frames
|
820
|
+
parent_frame.after(gif.info['duration'], update_frame, next_frame_idx)
|
821
|
+
|
822
|
+
# Start the GIF animation from frame 0
|
823
|
+
update_frame(0)
|
824
|
+
|
825
|
+
def display_gif_in_plot_frame(gif_path, parent_frame):
|
826
|
+
"""Display and zoom a GIF to fill the entire parent_frame, maintaining aspect ratio, with lazy resizing and caching."""
|
827
|
+
# Clear parent_frame if it contains any previous widgets
|
828
|
+
for widget in parent_frame.winfo_children():
|
829
|
+
widget.destroy()
|
830
|
+
|
831
|
+
# Load the GIF
|
832
|
+
gif = Image.open(gif_path)
|
833
|
+
|
834
|
+
# Get the aspect ratio of the GIF
|
835
|
+
gif_width, gif_height = gif.size
|
836
|
+
gif_aspect_ratio = gif_width / gif_height
|
837
|
+
|
838
|
+
# Create a label to display the GIF and configure it to fill the parent_frame
|
839
|
+
label = tk.Label(parent_frame, bg="black")
|
840
|
+
label.grid(row=0, column=0, sticky="nsew") # Expands in all directions (north, south, east, west)
|
841
|
+
|
842
|
+
# Configure parent_frame to stretch the label to fill available space
|
843
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
844
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
845
|
+
|
846
|
+
# Cache for storing resized frames (lazily filled)
|
847
|
+
resized_frames_cache = {}
|
848
|
+
|
849
|
+
# Store last frame size and aspect ratio
|
850
|
+
last_frame_width = 0
|
851
|
+
last_frame_height = 0
|
852
|
+
|
853
|
+
def resize_and_crop_frame(frame_idx, frame_width, frame_height):
|
854
|
+
"""Resize and crop the current frame of the GIF to fit the parent_frame while maintaining the aspect ratio."""
|
855
|
+
# If the frame is already cached at the current size, return it
|
856
|
+
if (frame_idx, frame_width, frame_height) in resized_frames_cache:
|
857
|
+
return resized_frames_cache[(frame_idx, frame_width, frame_height)]
|
858
|
+
|
859
|
+
# Calculate the scaling factor to zoom in on the GIF
|
860
|
+
scale_factor = max(frame_width / gif_width, frame_height / gif_height)
|
861
|
+
|
862
|
+
# Calculate new dimensions while maintaining the aspect ratio
|
863
|
+
new_width = int(gif_width * scale_factor)
|
864
|
+
new_height = int(gif_height * scale_factor)
|
865
|
+
|
866
|
+
# Resize the GIF to fit the frame using NEAREST for faster resizing
|
867
|
+
gif.seek(frame_idx)
|
868
|
+
resized_gif = gif.copy().resize((new_width, new_height), Image.Resampling.NEAREST if scale_factor > 2 else Image.Resampling.LANCZOS)
|
869
|
+
|
870
|
+
# Calculate the cropping box to center the resized GIF in the frame
|
871
|
+
crop_left = (new_width - frame_width) // 2
|
872
|
+
crop_top = (new_height - frame_height) // 2
|
873
|
+
crop_right = crop_left + frame_width
|
874
|
+
crop_bottom = crop_top + frame_height
|
875
|
+
|
876
|
+
# Crop the resized GIF to exactly fit the frame
|
877
|
+
cropped_gif = resized_gif.crop((crop_left, crop_top, crop_right, crop_bottom))
|
878
|
+
|
879
|
+
# Convert the cropped frame to a Tkinter-compatible format
|
880
|
+
frame_image = ImageTk.PhotoImage(cropped_gif)
|
881
|
+
|
882
|
+
# Cache the resized frame
|
883
|
+
resized_frames_cache[(frame_idx, frame_width, frame_height)] = frame_image
|
884
|
+
|
885
|
+
return frame_image
|
886
|
+
|
887
|
+
def update_frame(frame_idx):
|
888
|
+
"""Update the GIF frame using lazy resizing and caching."""
|
889
|
+
# Get the current size of the parent_frame
|
890
|
+
frame_width = parent_frame.winfo_width()
|
891
|
+
frame_height = parent_frame.winfo_height()
|
892
|
+
|
893
|
+
# Only resize if the frame size has changed
|
894
|
+
nonlocal last_frame_width, last_frame_height
|
895
|
+
if frame_width != last_frame_width or frame_height != last_frame_height:
|
896
|
+
last_frame_width, last_frame_height = frame_width, frame_height
|
897
|
+
|
898
|
+
# Get the resized and cropped frame image
|
899
|
+
frame_image = resize_and_crop_frame(frame_idx, frame_width, frame_height)
|
900
|
+
label.config(image=frame_image)
|
901
|
+
label.image = frame_image # Keep a reference to avoid garbage collection
|
902
|
+
|
903
|
+
# Move to the next frame, or loop back to the beginning
|
904
|
+
next_frame_idx = (frame_idx + 1) % gif.n_frames
|
905
|
+
parent_frame.after(gif.info['duration'], update_frame, next_frame_idx)
|
906
|
+
|
907
|
+
# Start the GIF animation from frame 0
|
908
|
+
update_frame(0)
|
909
|
+
|
910
|
+
def display_media_in_plot_frame(media_path, parent_frame):
|
911
|
+
"""Display an MP4, AVI, or GIF and play it on repeat in the parent_frame, fully filling the frame while maintaining aspect ratio."""
|
912
|
+
# Clear parent_frame if it contains any previous widgets
|
913
|
+
for widget in parent_frame.winfo_children():
|
914
|
+
widget.destroy()
|
915
|
+
|
916
|
+
# Check file extension to decide between video (mp4/avi) or gif
|
917
|
+
file_extension = os.path.splitext(media_path)[1].lower()
|
918
|
+
|
919
|
+
if file_extension in ['.mp4', '.avi']:
|
920
|
+
# Handle video formats (mp4, avi) using OpenCV
|
921
|
+
video = cv2.VideoCapture(media_path)
|
922
|
+
|
923
|
+
# Create a label to display the video
|
924
|
+
label = tk.Label(parent_frame, bg="black")
|
925
|
+
label.grid(row=0, column=0, sticky="nsew")
|
926
|
+
|
927
|
+
# Configure the parent_frame to expand
|
928
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
929
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
930
|
+
|
931
|
+
def update_frame():
|
932
|
+
"""Update function for playing video."""
|
933
|
+
ret, frame = video.read()
|
934
|
+
if ret:
|
935
|
+
# Get the frame dimensions
|
936
|
+
frame_height, frame_width, _ = frame.shape
|
937
|
+
|
938
|
+
# Get parent frame dimensions
|
939
|
+
parent_width = parent_frame.winfo_width()
|
940
|
+
parent_height = parent_frame.winfo_height()
|
941
|
+
|
942
|
+
# Ensure dimensions are greater than 0
|
943
|
+
if parent_width > 0 and parent_height > 0:
|
944
|
+
# Calculate the aspect ratio of the media
|
945
|
+
frame_aspect_ratio = frame_width / frame_height
|
946
|
+
parent_aspect_ratio = parent_width / parent_height
|
947
|
+
|
948
|
+
# Determine whether to scale based on width or height to cover the parent frame
|
949
|
+
if parent_aspect_ratio > frame_aspect_ratio:
|
950
|
+
# The parent frame is wider than the video aspect ratio
|
951
|
+
# Fit to width, crop height
|
952
|
+
new_width = parent_width
|
953
|
+
new_height = int(parent_width / frame_aspect_ratio)
|
954
|
+
else:
|
955
|
+
# The parent frame is taller than the video aspect ratio
|
956
|
+
# Fit to height, crop width
|
957
|
+
new_width = int(parent_height * frame_aspect_ratio)
|
958
|
+
new_height = parent_height
|
959
|
+
|
960
|
+
# Resize the frame to the new dimensions (cover the parent frame)
|
961
|
+
resized_frame = cv2.resize(frame, (new_width, new_height))
|
962
|
+
|
963
|
+
# Crop the frame to fit exactly within the parent frame
|
964
|
+
x_offset = (new_width - parent_width) // 2
|
965
|
+
y_offset = (new_height - parent_height) // 2
|
966
|
+
cropped_frame = resized_frame[y_offset:y_offset + parent_height, x_offset:x_offset + parent_width]
|
967
|
+
|
968
|
+
# Convert the frame to RGB (OpenCV uses BGR by default)
|
969
|
+
cropped_frame = cv2.cvtColor(cropped_frame, cv2.COLOR_BGR2RGB)
|
970
|
+
|
971
|
+
# Convert the frame to a Tkinter-compatible format
|
972
|
+
frame_image = ImageTk.PhotoImage(Image.fromarray(cropped_frame))
|
973
|
+
|
974
|
+
# Update the label with the new frame
|
975
|
+
label.config(image=frame_image)
|
976
|
+
label.image = frame_image # Keep a reference to avoid garbage collection
|
977
|
+
|
978
|
+
# Call update_frame again after a delay to match the video's frame rate
|
979
|
+
parent_frame.after(int(1000 / video.get(cv2.CAP_PROP_FPS)), update_frame)
|
980
|
+
else:
|
981
|
+
# Restart the video if it reaches the end
|
982
|
+
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
983
|
+
update_frame()
|
984
|
+
|
985
|
+
# Start the video playback
|
986
|
+
update_frame()
|
987
|
+
|
988
|
+
elif file_extension == '.gif':
|
989
|
+
# Handle GIF format using PIL
|
990
|
+
gif = Image.open(media_path)
|
991
|
+
|
992
|
+
# Create a label to display the GIF
|
993
|
+
label = tk.Label(parent_frame, bg="black")
|
994
|
+
label.grid(row=0, column=0, sticky="nsew")
|
995
|
+
|
996
|
+
# Configure the parent_frame to expand
|
997
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
998
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
999
|
+
|
1000
|
+
def update_gif_frame(frame_idx):
|
1001
|
+
"""Update function for playing GIF."""
|
1002
|
+
try:
|
1003
|
+
gif.seek(frame_idx) # Move to the next frame
|
1004
|
+
|
1005
|
+
# Get the frame dimensions
|
1006
|
+
gif_width, gif_height = gif.size
|
1007
|
+
|
1008
|
+
# Get parent frame dimensions
|
1009
|
+
parent_width = parent_frame.winfo_width()
|
1010
|
+
parent_height = parent_frame.winfo_height()
|
1011
|
+
|
1012
|
+
# Ensure dimensions are greater than 0
|
1013
|
+
if parent_width > 0 and parent_height > 0:
|
1014
|
+
# Calculate the aspect ratio of the GIF
|
1015
|
+
gif_aspect_ratio = gif_width / gif_height
|
1016
|
+
parent_aspect_ratio = parent_width / parent_height
|
1017
|
+
|
1018
|
+
# Determine whether to scale based on width or height to cover the parent frame
|
1019
|
+
if parent_aspect_ratio > gif_aspect_ratio:
|
1020
|
+
# Fit to width, crop height
|
1021
|
+
new_width = parent_width
|
1022
|
+
new_height = int(parent_width / gif_aspect_ratio)
|
1023
|
+
else:
|
1024
|
+
# Fit to height, crop width
|
1025
|
+
new_width = int(parent_height * gif_aspect_ratio)
|
1026
|
+
new_height = parent_height
|
1027
|
+
|
1028
|
+
# Resize the GIF frame to cover the parent frame
|
1029
|
+
resized_gif = gif.copy().resize((new_width, new_height), Image.Resampling.LANCZOS)
|
1030
|
+
|
1031
|
+
# Crop the resized GIF to fit the exact parent frame dimensions
|
1032
|
+
x_offset = (new_width - parent_width) // 2
|
1033
|
+
y_offset = (new_height - parent_height) // 2
|
1034
|
+
cropped_gif = resized_gif.crop((x_offset, y_offset, x_offset + parent_width, y_offset + parent_height))
|
1035
|
+
|
1036
|
+
# Convert the frame to a Tkinter-compatible format
|
1037
|
+
frame_image = ImageTk.PhotoImage(cropped_gif)
|
1038
|
+
|
1039
|
+
# Update the label with the new frame
|
1040
|
+
label.config(image=frame_image)
|
1041
|
+
label.image = frame_image # Keep a reference to avoid garbage collection
|
1042
|
+
frame_idx += 1
|
1043
|
+
except EOFError:
|
1044
|
+
frame_idx = 0 # Restart the GIF if at the end
|
1045
|
+
|
1046
|
+
# Schedule the next frame update
|
1047
|
+
parent_frame.after(gif.info['duration'], update_gif_frame, frame_idx)
|
1048
|
+
|
1049
|
+
# Start the GIF animation from frame 0
|
1050
|
+
update_gif_frame(0)
|
1051
|
+
|
1052
|
+
else:
|
1053
|
+
raise ValueError("Unsupported file format. Only .mp4, .avi, and .gif are supported.")
|
1054
|
+
|
1055
|
+
def print_widget_structure(widget, indent=0):
|
1056
|
+
"""Recursively print the widget structure."""
|
1057
|
+
# Print the widget's name and class
|
1058
|
+
print(" " * indent + f"{widget}: {widget.winfo_class()}")
|
1059
|
+
|
1060
|
+
# Recursively print all child widgets
|
1061
|
+
for child_name, child_widget in widget.children.items():
|
1062
|
+
print_widget_structure(child_widget, indent + 2)
|
1063
|
+
|
1064
|
+
def get_screen_dimensions():
|
1065
|
+
monitor = get_monitors()[0] # Get the primary monitor
|
1066
|
+
screen_width = monitor.width
|
1067
|
+
screen_height = monitor.height
|
1068
|
+
return screen_width, screen_height
|