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.
Files changed (41) hide show
  1. spacr/__init__.py +19 -3
  2. spacr/cellpose.py +311 -0
  3. spacr/core.py +140 -2493
  4. spacr/deep_spacr.py +151 -29
  5. spacr/gui.py +1 -0
  6. spacr/gui_core.py +74 -63
  7. spacr/gui_elements.py +110 -5
  8. spacr/gui_utils.py +346 -6
  9. spacr/io.py +624 -44
  10. spacr/logger.py +28 -9
  11. spacr/measure.py +107 -95
  12. spacr/mediar.py +0 -3
  13. spacr/ml.py +964 -0
  14. spacr/openai.py +37 -0
  15. spacr/plot.py +280 -15
  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 +129 -43
  22. spacr/sim.py +0 -2
  23. spacr/submodules.py +348 -0
  24. spacr/timelapse.py +0 -2
  25. spacr/toxo.py +233 -0
  26. spacr/utils.py +271 -171
  27. {spacr-0.3.1.dist-info → spacr-0.3.2.dist-info}/METADATA +7 -1
  28. {spacr-0.3.1.dist-info → spacr-0.3.2.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.2.dist-info}/LICENSE +0 -0
  39. {spacr-0.3.1.dist-info → spacr-0.3.2.dist-info}/WHEEL +0 -0
  40. {spacr-0.3.1.dist-info → spacr-0.3.2.dist-info}/entry_points.txt +0 -0
  41. {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, generate_ml_scores, identify_masks_finetune, check_cellpose_models, analyze_recruitment, train_cellpose, analyze_plaques, compare_cellpose_masks, generate_dataset, apply_model_to_tar
490
- from .io import generate_cellpose_train_test
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, perform_regression
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