drap 0.0.4.post8__py3-none-any.whl → 0.0.4.post11__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.
drap/utils.py CHANGED
@@ -1,6 +1,8 @@
1
- from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton, QFileDialog, QMessageBox, QLineEdit, QHBoxLayout, QGroupBox, QCheckBox
1
+ from PyQt5.QtWidgets import (QApplication, QProgressDialog, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton, QFileDialog,
2
+ QMessageBox, QLineEdit, QHBoxLayout, QGroupBox, QCheckBox, QSlider, QDialog, QDialogButtonBox,
3
+ QComboBox, QGridLayout, QSpinBox)
2
4
  from PyQt5.QtGui import QPixmap, QPainter, QPen, QImage, QMouseEvent, QColor
3
- from PyQt5.QtCore import Qt, QPoint, QRect, QFileInfo
5
+ from PyQt5.QtCore import Qt, QPoint, QRect, QFileInfo, QTimer, QEvent, QSize
4
6
  from fabio.edfimage import EdfImage
5
7
  import tkinter as tk
6
8
  from tkinter import filedialog
@@ -24,32 +26,81 @@ import re
24
26
  import argparse
25
27
  import cv2
26
28
  import matplotlib
29
+ import subprocess
30
+ import shutil
31
+ import tempfile
32
+ from typing import Iterable, Set, Tuple, Optional, Callable, List
33
+ from textwrap import dedent
27
34
 
28
35
  import matplotlib.pyplot as plt
29
36
 
30
37
  matplotlib.use('Agg')
31
38
 
32
39
 
33
- from PyQt5.QtWidgets import QApplication
34
- import sys
35
40
 
36
41
 
42
+ # Mapeamento codec → extensão recomendada
43
+ CODEC_EXTENSIONS = {
44
+ "mp4v": ".mp4",
45
+ "avc1": ".mp4",
46
+ "H264": ".mp4",
47
+ "XVID": ".avi",
48
+ "MJPG": ".avi",
49
+ "DIVX": ".avi",
50
+ "WMV1": ".avi",
51
+ "WMV2": ".avi",
52
+ }
53
+
54
+ class CodecDialog(QDialog):
55
+ def __init__(self, available_codecs, parent=None):
56
+ super().__init__(parent)
57
+ self.setWindowTitle("Escolher Codec de Vídeo")
58
+ self.setMinimumWidth(300)
59
+
60
+ layout = QVBoxLayout()
61
+
62
+ # Texto de instrução
63
+ layout.addWidget(QLabel("Selecione o codec e a extensão para salvar o vídeo:"))
64
+
65
+ # Combobox para codecs
66
+ self.codec_combo = QComboBox()
67
+ for codec in available_codecs:
68
+ ext = CODEC_EXTENSIONS.get(codec, ".avi")
69
+ self.codec_combo.addItem(f"{codec} → {ext}", (codec, ext))
70
+ layout.addWidget(self.codec_combo)
71
+
72
+ # Botões OK/Cancel
73
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
74
+ buttons.accepted.connect(self.accept)
75
+ buttons.rejected.connect(self.reject)
76
+ layout.addWidget(buttons)
37
77
 
78
+ self.setLayout(layout)
79
+
80
+ def get_selection(self):
81
+ """Retorna (codec, extensão) escolhido pelo usuário"""
82
+ return self.codec_combo.currentData()
38
83
 
39
84
 
40
85
  class ImageCropper(QMainWindow):
86
+
87
+
41
88
  def __init__(self):
89
+
42
90
  super().__init__()
43
- self.initUI()
44
-
45
- # Atributos para desenho
46
91
  self.drawing = False
47
92
  self.rect_start = QPoint()
48
93
  self.current_rect = QRect()
49
- self.image = None
50
- self.pixmap = None
51
94
  self.original_image = None
95
+ self.image = None
96
+ self.pixmap = None
52
97
  self.result_image = None
98
+ self.ret = None
99
+
100
+
101
+ self.initUI()
102
+
103
+
53
104
 
54
105
 
55
106
  def initUI(self):
@@ -72,23 +123,146 @@ class ImageCropper(QMainWindow):
72
123
  self.image_layout = QVBoxLayout()
73
124
  self.main_layout.addLayout(self.image_layout)
74
125
 
75
- # Layout to controls
76
- self.controls_layout = QVBoxLayout()
77
- self.main_layout.addLayout(self.controls_layout)
78
126
 
79
127
  # Create a QLabel to display the image
80
128
  self.image_label = QLabel(self)
129
+ self.image_label.setAlignment(Qt.AlignCenter) # centraliza o pixmap
130
+ self.image_label.setMinimumSize(320, 240)
81
131
  self.image_layout.addWidget(self.image_label)
132
+ self.image_label.setMouseTracking(True)
133
+
134
+
135
+ # --- Video Player Controls ---
136
+ # Layout de controles do player de vídeo (abaixo do vídeo)
137
+ self.video_bar_time_layout = QVBoxLayout()
138
+ self.image_layout.addLayout(self.video_bar_time_layout)
139
+
140
+ # Layout de controles do player de vídeo (abaixo do vídeo)
141
+ self.video_controls_layout = QVBoxLayout()
142
+ self.image_layout.addLayout(self.video_controls_layout)
143
+
144
+ # progress bar
145
+ self.video_slider = QSlider(Qt.Horizontal)
146
+ self.video_bar_time_layout.addWidget(self.video_slider)
147
+ self.video_slider.sliderMoved.connect(self.seek_video)
148
+
149
+ # show time
150
+ self.time_label = QLabel('00:00 / 00:00')
151
+ self.video_bar_time_layout.addWidget(self.time_label)
152
+
153
+ self.info_labels_layout = QHBoxLayout()
154
+ self.mouse_label = QLabel('Mouse: (0,0)')
155
+ self.image_size_label = QLabel("Image size: 0 x 0")
156
+
157
+ self.info_labels_layout.addWidget(self.mouse_label)
158
+ self.info_labels_layout.addWidget(self.image_size_label)
159
+ self.info_labels_layout.addStretch()
82
160
 
83
- # Create a QLabel to display the resulting image
84
- self.result_label = QLabel(self)
85
- self.image_layout.addWidget(self.result_label)
86
161
 
162
+ self.video_bar_time_layout.addLayout(self.info_labels_layout)
163
+
164
+
165
+ # self.video_bar_time_layout.addWidget(self.image_size_label)
166
+
167
+ # Timer para reprodução de vídeo
168
+ self.video_timer = QTimer()
169
+ self.video_timer.timeout.connect(self.next_frame)
170
+
171
+ # Variáveis de controle de vídeo
172
+ self.video = None # cv2.VideoCapture
173
+ self.fps = 0
174
+ self.total_frames = 0
175
+ self.current_frame = 0
176
+ self.playing = False
177
+
178
+ # botons line: Play, Pause, Stop
179
+ self.video_buttons_layout = QHBoxLayout()
180
+ self.video_controls_layout.addLayout(self.video_buttons_layout)
181
+
87
182
  # Create a button to load the image
88
183
  self.load_button = QPushButton('Load Video', self)
89
- self.controls_layout.addWidget(self.load_button)
184
+ self.video_buttons_layout.addWidget(self.load_button)
90
185
  self.load_button.clicked.connect(self.load_image)
91
186
 
187
+ # Botões de controle
188
+ self.play_button = QPushButton('Play')
189
+ self.pause_button = QPushButton('Pause')
190
+ self.stop_button = QPushButton('Stop')
191
+ self.export_button = QPushButton('Export Video', self)
192
+
193
+
194
+ self.video_buttons_layout.addWidget(self.play_button)
195
+ self.video_buttons_layout.addWidget(self.pause_button)
196
+ self.video_buttons_layout.addWidget(self.stop_button)
197
+ self.video_buttons_layout.addWidget(self.export_button)
198
+
199
+ self.play_button.clicked.connect(self.play_video)
200
+ self.pause_button.clicked.connect(self.pause_video)
201
+ self.stop_button.clicked.connect(self.stop_video)
202
+ self.export_button.clicked.connect(self.export_video_dialog)
203
+
204
+
205
+ # Slider de velocidade (0.25x a 2.0x)
206
+ self.speed_label = QLabel("Speed: 1.0x")
207
+ self.video_controls_layout.addWidget(self.speed_label)
208
+ self.speed_slider = QSlider(Qt.Horizontal)
209
+ self.speed_slider.setMinimum(25) # 0.25x
210
+ self.speed_slider.setMaximum(200) # 2.0x
211
+ self.speed_slider.setValue(100) # 1.0x
212
+ self.speed_slider.setTickInterval(25)
213
+ self.speed_slider.setTickPosition(QSlider.TicksBelow)
214
+ self.video_controls_layout.addWidget(self.speed_slider)
215
+
216
+ self.speed_slider.valueChanged.connect(self.update_speed)
217
+
218
+
219
+ # === Painel de coordenadas do retângulo ===
220
+ self.rect_group = QGroupBox("Rectangle Position and Size")
221
+ self.rect_layout = QGridLayout()
222
+ self.rect_group.setLayout(self.rect_layout)
223
+
224
+ # Cria os 4 campos (x, y, largura, altura)
225
+ self.rect_x_spin = QSpinBox()
226
+ self.rect_y_spin = QSpinBox()
227
+ self.rect_w_spin = QSpinBox()
228
+ self.rect_h_spin = QSpinBox()
229
+
230
+ # Rótulos
231
+ self.rect_layout.addWidget(QLabel("X:"), 0, 0)
232
+ self.rect_layout.addWidget(self.rect_x_spin, 0, 1)
233
+ self.rect_layout.addWidget(QLabel("Y:"), 0, 2)
234
+ self.rect_layout.addWidget(self.rect_y_spin, 0, 3)
235
+ self.rect_layout.addWidget(QLabel("Width:"), 1, 0)
236
+ self.rect_layout.addWidget(self.rect_w_spin, 1, 1)
237
+ self.rect_layout.addWidget(QLabel("Height:"), 1, 2)
238
+ self.rect_layout.addWidget(self.rect_h_spin, 1, 3)
239
+
240
+ # Adiciona ao layout principal lateral
241
+ self.video_controls_layout.addWidget(self.rect_group)
242
+
243
+ # Limites padrão (atualizados quando o vídeo é carregado)
244
+ for spin in [self.rect_x_spin, self.rect_y_spin, self.rect_w_spin, self.rect_h_spin]:
245
+ spin.setRange(0, 9999)
246
+ spin.setSingleStep(1)
247
+
248
+ # Conectar mudanças dos campos ao redesenho do retângulo
249
+ self.rect_x_spin.valueChanged.connect(self.update_rect_from_spinboxes)
250
+ self.rect_y_spin.valueChanged.connect(self.update_rect_from_spinboxes)
251
+ self.rect_w_spin.valueChanged.connect(self.update_rect_from_spinboxes)
252
+ self.rect_h_spin.valueChanged.connect(self.update_rect_from_spinboxes)
253
+
254
+
255
+
256
+ # Layout to controls
257
+ self.controls_layout = QVBoxLayout()
258
+ self.main_layout.addLayout(self.controls_layout)
259
+
260
+ # Create a QLabel to display the resulting image
261
+ self.result_label = QLabel(self)
262
+ self.image_layout.addWidget(self.result_label)
263
+
264
+
265
+
92
266
 
93
267
  # Create a button to crop the image
94
268
  self.crop_button = QPushButton('Crop Image', self)
@@ -187,9 +361,7 @@ class ImageCropper(QMainWindow):
187
361
  self.check_option = QCheckBox("Print a PDF with images", self)
188
362
  self.check_option.setChecked(False) # desmarcado por padrão
189
363
  self.controls_layout.addWidget(self.check_option)
190
-
191
-
192
-
364
+
193
365
  # Hook mouse events
194
366
  self.image_label.installEventFilter(self)
195
367
 
@@ -197,8 +369,8 @@ class ImageCropper(QMainWindow):
197
369
  if self.test:
198
370
  self.load_image()
199
371
  self.int_input1.setText("45.")
200
- self.int_input2.setText("10")
201
- self.int_input3.setText("5000")
372
+ self.int_input2.setText("1")
373
+ self.int_input3.setText("25")
202
374
  self.int_input4.setText("1.0")
203
375
 
204
376
  self.show()
@@ -206,8 +378,13 @@ class ImageCropper(QMainWindow):
206
378
  def load_image(self):
207
379
 
208
380
 
381
+ if hasattr(self, "video") and self.video and self.video.isOpened():
382
+ self.video.release()
383
+
384
+
209
385
  if self.test:
210
- self.file_path = "/media/standard02/Linux_sync_2020_02/standart/Documents/programming/python/files/15-SY-50cm/water-without-absolute-intensity2.flv"
386
+ self.file_path = "/home/standard02/Documents/programming/python/bolhas/PyPI/drap/teste.mp4"
387
+ # "/home/standard02/Documents/programming/python/bolhas/PyPI/drap/teste_completo.mp4" teste_bolha.mp4
211
388
  # self.file_path, _ = QFileDialog.getOpenFileName(self, 'Open Video', '', 'Videos (*.avi *.mp4 *.mov *.mkv *.wmv *.flv *.mpg *.mpeg *.3gp *.ogv .webm)')
212
389
  else:
213
390
  self.file_path, _ = QFileDialog.getOpenFileName(self, 'Open Video', '', 'Videos (*.avi *.mp4 *.mov *.mkv *.wmv *.flv *.mpg *.mpeg *.3gp *.ogv .webm)')
@@ -217,54 +394,563 @@ class ImageCropper(QMainWindow):
217
394
  return
218
395
 
219
396
  self.file_path = os.path.normpath(str(Path(self.file_path).expanduser().resolve()))
397
+ self.video = cv2.VideoCapture(self.file_path)
398
+ self.ret = None
399
+
220
400
 
401
+ if not self.video.isOpened():
402
+ QMessageBox.critical(self, 'Error', f'Could not open video:\n{self.file_path}')
403
+ return
404
+
221
405
  # self.file_path = os.path.normpath(self.file_path)
222
406
  # self.file_path = QFileInfo(self.file_path).fileName();
223
407
  # self.file_path = Path(self.file_path);
224
408
  # self.file_path = self.file_path.resolve();
225
409
  # self.file_path = os.path.normpath(self.file_path);
226
-
227
- video = cv2.VideoCapture(self.file_path);
410
+
411
+ rval, frame = self.video.read();
228
412
 
229
- if not video.isOpened():
230
- QMessageBox.critical(self, 'Error', f'Could not open video:\n{self.file_path}')
413
+ if not rval or frame is None:
414
+ QMessageBox.critical(self, 'Error', 'Could not read first frame from the video.')
415
+ self.video.release()
231
416
  return
417
+
418
+ # Converte o frame OpenCV (BGR → RGB) para QImage/QPixmap
419
+ rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
420
+ h, w, ch = rgb.shape
421
+ bytes_per_line = ch * w
422
+ qimg = QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888).copy()
423
+
424
+ # Atualiza atributos de imagem
425
+ self.original_image = qimg
426
+ self.image = qimg.copy()
427
+ self.pixmap = QPixmap.fromImage(self.image)
428
+ self.image_label.setPixmap(self.pixmap) #show image
429
+ self.image_label.adjustSize()
430
+ self.original_image = self.pixmap.toImage()
431
+ # self.image_label.setScaledContents(True)
432
+ self.image_label.setScaledContents(False)
433
+ self.image_label.setAlignment(Qt.AlignCenter)
434
+
232
435
 
233
- rval, frame = video.read();
436
+ self.image_width = self.pixmap.width()
437
+ self.image_height = self.pixmap.height()
438
+ self.update_rect_limits()
439
+
440
+ self.current_rect = QRect()
441
+ self.update_image()
442
+ self.img_size = [frame.shape[1], frame.shape[0]]
234
443
 
235
- if not rval or frame is None:
236
- QMessageBox.critical(self, 'Error', 'Could not read first frame from the video.')
237
- video.release()
444
+ self.image_size_label.setText(f"Image size: {self.img_size[1]} × {self.img_size[0]}")
445
+
446
+ # Limitar X e Y dentro dos limites da imagem
447
+ self.rect_x_spin.setRange(0, self.img_size[0] - 1 )
448
+ self.rect_y_spin.setRange(0, self.img_size[1] - 1 )
449
+
450
+ # Limitar largura e altura para não ultrapassar o tamanho da imagem
451
+ self.rect_w_spin.setRange(0, self.img_size[0] - self.rect_x_spin.value())
452
+ self.rect_h_spin.setRange(0, self.img_size[1] - self.rect_y_spin.value())
453
+
454
+ # Reseta variáveis de estado
455
+ self.fps = int(self.video.get(cv2.CAP_PROP_FPS))
456
+ self.total_frames = int(self.video.get(cv2.CAP_PROP_FRAME_COUNT))
457
+ self.duration_sec = self.total_frames / self.fps if self.fps else 0
458
+ self.current_frame = 0
459
+ self.video.set(cv2.CAP_PROP_POS_FRAMES, 0)
460
+ self.video_slider.setMaximum(max(0, self.total_frames - 1))
461
+ self.playing = False
462
+
463
+ # Atualiza UI
464
+ # self.update_time_label()
465
+ # self.display_frame()
466
+ # self.image = self.original_image.copy()
467
+ # self.display_cv2_frame(frame)
468
+
469
+
470
+
471
+ def play_video(self):
472
+
473
+ if self.video is None:
238
474
  return
475
+ self.playing = True
476
+ # self.video_timer.start(int(1000 / self.fps)) # chama a cada frame
477
+ speed_factor = self.speed_slider.value() / 100.0
478
+ interval = max(1, int(1000 / self.fps / speed_factor))
479
+ self.video_timer.start(interval)
480
+
481
+
482
+ def pause_video(self):
239
483
 
240
- file_path = 'data/';
241
- file_path = Path(file_path);
242
- file_path = file_path.resolve();
243
- os.makedirs(file_path, exist_ok=True)
244
- file_path = os.path.normpath(file_path);
245
- file_path = os.path.join(file_path, 'sample_frame.jpg');
246
- file_path = os.path.normpath(file_path);
484
+ self.playing = False
485
+ self.video_timer.stop()
486
+
487
+
488
+ def stop_video(self):
489
+
490
+ if self.video is None:
491
+ return
492
+ self.pause_video()
493
+ self.current_frame = 0
494
+ self.video.set(cv2.CAP_PROP_POS_FRAMES, 0)
495
+ self.display_frame()
496
+ self.video_slider.setValue(0)
497
+ self.update_time_label()
498
+
499
+
500
+ def keyPressEvent(self, event):
501
+
502
+ if event.key() == Qt.Key_Space:
503
+ if self.playing:
504
+ self.pause_video()
505
+ else:
506
+ self.play_video()
507
+
508
+
509
+ def next_frame(self):
510
+
511
+ if self.video is None or not self.playing:
512
+ return
513
+
514
+ self.video.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame)
515
+ ret, frame = self.video.read()
516
+
517
+ if not ret:
518
+ self.pause_video()
519
+ return
520
+
521
+ self.display_cv2_frame(frame)
522
+ self.video_slider.setValue(self.current_frame)
523
+ self.update_time_label()
524
+
525
+ self.current_frame += 1
526
+
527
+ if self.current_frame >= self.total_frames:
528
+ self.stop_video()
529
+
530
+
531
+
532
+
533
+ def display_frame(self):
534
+
535
+
536
+ if self.video is None:
537
+ return
538
+ self.video.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame)
539
+ ret, frame = self.video.read()
540
+ if ret:
541
+ self.display_cv2_frame(frame)
542
+
543
+ def display_cv2_frame(self, frame):
544
+
545
+ rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
546
+ h, w, ch = rgb_image.shape
547
+ bytes_per_line = ch * w
548
+ q_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888).copy()
549
+
550
+
551
+ # GUARDA a imagem atual
552
+ self.original_image = q_image
553
+ self.image = q_image.copy()
554
+ self.pixmap = QPixmap.fromImage(self.image)
555
+ self.image_label.setPixmap(self.pixmap)
556
+ self.image_label.adjustSize()
557
+ self.image_label.setPixmap(self.pixmap)
558
+ self.image_width = w
559
+ self.image_height = h
560
+ self.update_rect_limits()
561
+ # self.update_image()
562
+
563
+
564
+
565
+
566
+ def seek_video(self, frame_number):
567
+
568
+ if self.video is None:
569
+ return
570
+ self.pause_video()
571
+ self.current_frame = frame_number
572
+ self.video.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
573
+ self.display_frame()
574
+ self.update_time_label()
575
+
576
+ def update_time_label(self):
577
+
578
+ current_time = self.current_frame / self.fps if self.fps else 0
579
+ total_time = self.total_frames / self.fps if self.fps else 0
580
+ time_str = f"{self.format_time(current_time)} min / {self.format_time(total_time)} min ({current_time:.2f} s - frame: {self.current_frame})"
581
+ self.time_label.setText(time_str)
582
+
583
+ def format_time(self, seconds):
584
+
585
+ m, s = divmod(int(seconds), 60)
586
+ return f"{m:02d}:{s:02d}"
587
+
588
+ def update_speed(self):
589
+
590
+ speed_factor = self.speed_slider.value() / 100.0
591
+ self.speed_label.setText(f"Speed: {speed_factor:.2f}x")
592
+ if self.playing:
593
+ interval = max(1, int(1000 / self.fps / speed_factor))
594
+ self.video_timer.setInterval(interval)
595
+
596
+
597
+ def export_video_dialog(self):
598
+
599
+ if self.video is None:
600
+ print(f"Error: input file not found: {self.video}", file=sys.stderr)
601
+ return
602
+
603
+ available_codecs = self.detect_codecs()
604
+ dlg = CodecDialog(available_codecs)
605
+ if dlg.exec_() == QDialog.Accepted:
606
+ codec, ext = dlg.get_selection()
607
+ # print(f"✅ Codec escolhido: {codec}, Extensão: {ext}")
608
+
609
+
610
+ # fourcc = cv2.VideoWriter_fourcc(*codec)
611
+ # out = cv2.VideoWriter("saida" + ext, fourcc, fps, (width, height))
612
+
613
+ # Diálogo para escolher onde salvar o vídeo
614
+ save_path, _ = QFileDialog.getSaveFileName(self, "Save Video As", "", f"Video Files {codec}")
615
+ base, ext = os.path.splitext(self.file_path)
616
+ out_ext = out_ext = os.path.splitext(save_path)[1].lower()
617
+
618
+ if not save_path:
619
+ return
620
+
621
+ if not out_ext:
622
+ out_ext = '.mp4'
623
+ save_path = (save_path + out_ext)
624
+
625
+
626
+ # Widgets para escolher parâmetros
627
+ dialog = QDialog(self)
628
+ dialog.setWindowTitle("Export Video Settings")
629
+ layout = QVBoxLayout(dialog)
630
+
631
+ # Frame inicial
632
+ start_label = QLabel("Start Frame:")
633
+ start_input = QLineEdit(str(self.current_frame))
634
+ layout.addWidget(start_label)
635
+ layout.addWidget(start_input)
636
+
637
+ # Frame final
638
+ end_label = QLabel("End Frame:")
639
+ end_input = QLineEdit(str(self.total_frames - 1))
640
+ layout.addWidget(end_label)
641
+ layout.addWidget(end_input)
642
+
643
+ # keep decider when the frame will save
644
+ keep_decider_label = QLabel("Keep frames each second:")
645
+ keep_decider_input = QLineEdit(str(self.fps))
646
+ layout.addWidget(keep_decider_label)
647
+ layout.addWidget(keep_decider_input)
648
+
649
+
650
+ # Botões
651
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
652
+ layout.addWidget(button_box)
653
+
654
+
655
+ button_box.accepted.connect(lambda: self.export_video(
656
+ self.file_path,
657
+ save_path,
658
+ codec,
659
+ int(start_input.text()),
660
+ int(end_input.text()),
661
+ int(keep_decider_input.text()),
662
+ self.ret,
663
+ None
664
+ ))
665
+ # crop_rect=(100, 50, 400, 300)
666
+ button_box.accepted.connect(dialog.accept)
667
+ button_box.rejected.connect(dialog.reject)
668
+
669
+ dialog.exec_()
670
+
671
+
672
+
673
+ def export_video (self, in_path, out_path, codec, cut_first=0, cut_last=0, keep_decider=None, crop_rect=None, to_drop=None,):
674
+
675
+ video_in = cv2.VideoCapture(in_path)
676
+
677
+
678
+ if not video_in.isOpened():
679
+ raise RuntimeError(f"Not was possible open {in_path}")
680
+
681
+ total_frames = int(video_in.get(cv2.CAP_PROP_FRAME_COUNT))
682
+ fps_in = int(video_in.get(cv2.CAP_PROP_FPS))
683
+ if fps_in <= 0:
684
+ raise ValueError(f"Invalid FPS: {fps_in}")
685
+
686
+
687
+ # crop limits
688
+ start = max(0, cut_first)
689
+ end = cut_last if cut_last > 0 else total_frames
690
+ end = max(start, end) - 1
691
+ # print(start, end, total_frames)
692
+
693
+
694
+ w_in = int(video_in.get(cv2.CAP_PROP_FRAME_WIDTH))
695
+ h_in = int(video_in.get(cv2.CAP_PROP_FRAME_HEIGHT))
696
+
697
+
698
+ if crop_rect is not None:
699
+ x, y, w_out, h_out = crop_rect
700
+ x = max(0, min(x, w_in - 1))
701
+ y = max(0, min(y, h_in - 1))
702
+ w_out = min(w_out, w_in - x)
703
+ h_out = min(h_out, h_in - y)
704
+
705
+ else:
706
+ x, y = 0, 0
707
+ w_out, h_out = w_in, h_in
708
+
709
+ size = (abs(w_out), abs(h_out))
710
+
711
+
712
+
713
+ kept = 0
714
+ for idx in range(start, end):
715
+ if to_drop and idx in to_drop:
716
+ continue
717
+ if keep_decider and idx % keep_decider != 0:
718
+ continue
719
+ kept += 1
720
+
721
+ if kept == 0:
722
+ raise RuntimeError("No frames selected for export!")
723
+
724
+ total_considered = end - start
725
+ fps_out = fps_in * (kept / total_considered)
726
+ # print(f"FPS adjusted: {fps_in:.2f} → {fps_out:.2f} (keeped {kept}/{total_considered})")
727
+
728
+
729
+ if size[0] <= 0 or size[1] <= 0:
730
+ raise ValueError(f"Invalid size: {size}")
731
+
732
+ # temporary file (backup)
733
+ out_ext = os.path.splitext(out_path)[1] or ".mp4"
734
+ os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True)
735
+ with tempfile.NamedTemporaryFile(prefix="cut_", suffix=out_ext, delete=False,
736
+ dir=os.path.dirname(os.path.abspath(out_path))) as tmp:
737
+ tmp_out_path = tmp.name
738
+
739
+ # Inicializar VideoWriter (tentando codecs candidatos, como você já fazia)
740
+ fourcc = cv2.VideoWriter_fourcc(*codec)
741
+ writer = cv2.VideoWriter(tmp_out_path, fourcc, fps_out, size)
742
+
743
+ if not writer.isOpened():
744
+ raise RuntimeError("Failed to open VideoWriter with codec {codec}")
745
+
746
+ progress = QProgressDialog("Exporting video...", "Cancel", 0, kept, self)
747
+ progress.setWindowTitle("Please wait")
748
+ progress.setWindowModality(Qt.WindowModal)
749
+ progress.setMinimumDuration(0)
750
+ progress.setValue(0)
751
+
752
+
753
+
754
+ # Loop in frames
755
+ idx = 0
756
+ written = 0
757
+ current_kept = 0
758
+
759
+ while True:
760
+
761
+ ret, frame = video_in.read()
762
+ if not ret:
763
+ break
764
+
765
+ if idx < start: # crop first N frames
766
+ idx += 1
767
+ continue
768
+ if idx >= end: # crop last N frames
769
+ break
770
+ if to_drop and idx in to_drop: # descartar manualmente
771
+ idx += 1
772
+ continue
773
+ if keep_decider and idx % keep_decider != 0: # functions decide if keep
774
+ idx += 1
775
+ continue
776
+
777
+ # Crop if exist rect
778
+ if crop_rect is not None:
779
+ frame = frame[y:y+h_out, x:x+w_out]
780
+
781
+
782
+
783
+
784
+ # save frame
785
+ writer.write(frame)
786
+ written += 1
787
+ idx += 1
788
+ current_kept += 1
789
+
790
+ progress.setValue(current_kept)
791
+ QApplication.processEvents()
792
+ if progress.wasCanceled():
793
+ print("Export canceled by user.")
794
+ break
795
+
796
+
797
+ video_in.release()
798
+ writer.release()
799
+
800
+ if not progress.wasCanceled():
801
+ # change the last file
802
+ shutil.move(tmp_out_path, out_path)
803
+ # print(f"Video exported in {out_path}, ({written} saves frames)")
804
+ progress.setValue(kept)
805
+ else:
806
+ os.remove(tmp_out_path)
807
+
808
+ def draw_square(self, event, x, y, flags, param, imagem):
809
+
810
+
811
+
812
+ if event == cv2.EVENT_LBUTTONDOWN:
813
+ # Primeiro clique → guarda o ponto inicial
814
+ self.vertices = [(x, y)]
815
+ self.drawing = True
816
+
817
+ elif event == cv2.EVENT_MOUSEMOVE and self.drawing:
818
+ # Se estiver arrastando, mostra o quadrado "dinâmico"
819
+ img_copy = param.copy()
820
+ cv2.rectangle(img_copy, self.vertices[0], (x, y), (255, 0, 0), 2)
821
+ cv2.imshow("Video", img_copy)
822
+
823
+ elif event == cv2.EVENT_LBUTTONUP:
824
+ # Segundo clique → fecha o quadrado
825
+ self.vertices.append((x, y))
826
+ self.drawing = False
827
+ cv2.rectangle(param, vertices[0], self.vertices[1], (255, 0, 0), 2)
828
+ cv_imshow_safe("Video", param)
829
+
830
+ # print(f"Rectangle of {self.vertices[0]} up to {self.vertices[1]}")
831
+
832
+
833
+
834
+ def label_pos_to_image_pos(self, pos: QPoint):
835
+
836
+
837
+ if self.pixmap is None or self.image is None:
838
+ return None
839
+
840
+ label_size = self.image_label.size()
841
+ pixmap_size = self.pixmap.size()
842
+
843
+ # Calcula o offset (bordas pretas) se a imagem estiver centralizada
844
+ x_offset = max((label_size.width() - pixmap_size.width()) // 2, 0)
845
+ y_offset = max((label_size.height() - pixmap_size.height()) // 2, 0)
846
+
847
+ # Remove o deslocamento
848
+ x = pos.x() - x_offset
849
+ y = pos.y() - y_offset
850
+
851
+ # Garante que está dentro da imagem
852
+ if 0 <= x < pixmap_size.width() and 0 <= y < pixmap_size.height():
853
+ return QPoint(x, y)
854
+ else:
855
+ return None
856
+
857
+
858
+
859
+ def detect_codecs(self):
860
+
861
+ codecs = self.list_ffmpeg_codecs()
862
+ if codecs:
863
+ return codecs
864
+ else:
865
+ return self.test_opencv_codecs(["mp4v", "XVID", "MJPG", "H264", "avc1", "DIVX"])
866
+
867
+
868
+ def list_ffmpeg_codecs(self):
869
+ try:
870
+ result = subprocess.run(["ffmpeg", "-codecs"],
871
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
872
+ text=True)
873
+ codecs = []
874
+ for line in result.stdout.splitlines():
875
+ if line.startswith(" "): # linhas úteis
876
+ parts = line.split()
877
+ if len(parts) >= 2:
878
+ codecs.append(parts[1])
879
+ return codecs
880
+ except FileNotFoundError:
881
+ # print("⚠️ FFmpeg não encontrado no sistema.")
882
+ return []
883
+
884
+ def test_opencv_codecs(self,codecs, output_dir="test_codecs"):
885
+
886
+ os.makedirs(output_dir, exist_ok=True)
887
+ fps = 10
888
+ frame_size = (320, 240)
889
+ frame = np.zeros((frame_size[1], frame_size[0], 3), dtype=np.uint8)
890
+
891
+ available = []
892
+ for codec in codecs:
893
+ filename = os.path.join(output_dir, f"test_{codec}.avi")
894
+ fourcc = cv2.VideoWriter_fourcc(*codec)
895
+ writer = cv2.VideoWriter(filename, fourcc, fps, frame_size)
896
+ if writer.isOpened():
897
+ writer.write(frame)
898
+ writer.release()
899
+ if os.path.exists(filename) and os.path.getsize(filename) > 0:
900
+ available.append(codec)
901
+ return available
902
+
903
+
904
+
905
+
906
+
907
+
908
+
909
+
910
+
911
+
912
+
913
+
914
+
915
+
916
+
917
+
918
+
919
+
920
+
921
+
922
+
923
+
924
+
925
+
926
+
927
+
928
+
929
+
930
+
931
+
932
+
933
+
934
+
935
+
936
+
937
+
938
+
939
+
940
+
941
+
942
+
943
+
944
+
945
+
946
+
947
+
948
+
949
+
950
+
951
+
952
+
247
953
 
248
- ok = cv2.imwrite(file_path,frame);
249
- if not ok:
250
- QMessageBox.critical(self, 'Error', f'Failed to save first frame to:\n{file_path}')
251
- video.release()
252
- return
253
-
254
- if file_path:
255
- self.pixmap = QPixmap(file_path)
256
- if self.pixmap.isNull():
257
- QMessageBox.critical(self, 'Error', f'Failed to load image into QPixmap:\n{file_path}')
258
- video.release()
259
- return
260
- self.original_image = self.pixmap.toImage()
261
- self.image = self.pixmap.toImage()
262
- self.image_label.setPixmap(self.pixmap)
263
- self.image_label.setScaledContents(True)
264
- self.fps = video.get(cv2.CAP_PROP_FPS);
265
- self.total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
266
- self.max_frames = self.total_frames;
267
- video.release();
268
954
 
269
955
  def sort_images_edf_time(self):
270
956
 
@@ -365,42 +1051,63 @@ class ImageCropper(QMainWindow):
365
1051
  concatene_files_scat_back(set_file_1, set_file_2);
366
1052
 
367
1053
  def eventFilter(self, obj, event):
368
- if obj == self.image_label:
369
- if event.type() == QMouseEvent.MouseButtonPress:
370
- if event.button() == Qt.LeftButton:
1054
+
1055
+ if obj == self.image_label and (self.original_image is not None):
1056
+
1057
+ # Atualiza posição do mouse (mesmo sem clicar)
1058
+ if event.type() == QEvent.MouseMove:
1059
+ pos = event.pos()
1060
+ mapped = self.label_pos_to_image_pos(pos)
1061
+ if mapped:
1062
+ self.mouse_label.setText(f"Mouse: ({mapped.x()}, {mapped.y()})")
1063
+
1064
+ # Início do desenho do retângulo
1065
+ if event.type() == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
1066
+ mapped = self.label_pos_to_image_pos(event.pos())
1067
+ if mapped is not None:
371
1068
  self.drawing = True
372
- self.rect_start = event.pos()
373
- elif event.type() == QMouseEvent.MouseMove:
374
- if self.drawing:
375
- self.current_rect = QRect(self.rect_start, event.pos()).normalized()
376
- self.update_image()
377
- elif event.type() == QMouseEvent.MouseButtonRelease:
378
- if event.button() == Qt.LeftButton:
379
- self.drawing = False
380
- if not self.current_rect.isNull():
381
- self.update_image()
1069
+ self.rect_start = mapped
1070
+ self.current_rect = QRect(self.rect_start, QSize())
1071
+ self.update_image() # desenha imediatamente, sem atualizar limites
1072
+ self.update_spinboxes_from_rect()
1073
+
1074
+ # Movimento do mouse durante o desenho
1075
+ elif event.type() == QEvent.MouseMove and getattr(self, "drawing", False):
1076
+ mapped = self.label_pos_to_image_pos(event.pos())
1077
+ if mapped is not None:
1078
+ self.current_rect = QRect(self.rect_start, mapped).normalized()
1079
+ self.update_image() # redesenha em tempo real (não mexe nos spinboxes ainda)
1080
+
1081
+ # Soltar o botão: finalize o desenho
1082
+ elif event.type() == QEvent.MouseButtonRelease and event.button() == Qt.LeftButton:
1083
+ if getattr(self, "drawing", False):
1084
+ # self.drawing = False
1085
+ mapped = self.label_pos_to_image_pos(event.pos())
1086
+ if mapped is not None:
1087
+ self.current_rect = QRect(self.rect_start, mapped).normalized()
1088
+ self.update_image() # mantém o desenho visível
1089
+ self.update_spinboxes_from_rect() # atualiza campos
1090
+ self.update_rect_limits() # aplica os limites só no final
1091
+ self.drawing = False
1092
+
382
1093
  return super().eventFilter(obj, event)
383
1094
 
1095
+
1096
+
384
1097
  def crop_image(self):
385
1098
 
386
- if self.current_rect.isNull() or not self.original_image:
387
- QMessageBox.warning(self, 'Warning', 'No rectangle drawn for cropping. Please draw rectangle first.')
1099
+ if not hasattr(self, "original_image") or self.original_image is None:
1100
+ QMessageBox.warning(self, "Warning", "No image loaded.")
1101
+ return
1102
+ if not hasattr(self, "current_rect") or self.current_rect.isNull():
1103
+ QMessageBox.warning(self, "Warning", "No rectangle drawn.")
388
1104
  return
389
1105
 
390
- # Crop the image
391
1106
  cropped_image = self.original_image.copy(self.current_rect)
392
-
393
- # Saving the cropped image
394
- save_path = 'data/';
395
- save_path = Path(save_path);
396
- save_path = save_path.resolve();
397
- save_path = os.path.normpath(save_path);
398
- save_path = os.path.join(save_path, 'image_croped.png');
399
- save_path = os.path.normpath(save_path);
400
- os.makedirs(os.path.dirname(save_path), exist_ok=True)
401
- if save_path:
402
- cropped_image.save(save_path)
403
- self.save_rectangle_coordinates(self.current_rect)
1107
+ save_path = Path("data/image_cropped.png").resolve()
1108
+ os.makedirs(save_path.parent, exist_ok=True)
1109
+ cropped_image.save(str(save_path))
1110
+ self.save_rectangle_coordinates(self.current_rect)
404
1111
 
405
1112
 
406
1113
  def show_image_info(self):
@@ -419,6 +1126,15 @@ class ImageCropper(QMainWindow):
419
1126
  f"Time Total (s): {round(self.total_frames/self.fps)} ")
420
1127
 
421
1128
  QMessageBox.information(self, 'Information of Image', info)
1129
+
1130
+
1131
+ def show_error_message(self, title, message):
1132
+
1133
+ msg = QMessageBox(self)
1134
+ msg.setIcon(QMessageBox.Critical)
1135
+ msg.setWindowTitle(title)
1136
+ msg.setText(message)
1137
+ msg.exec_()
422
1138
 
423
1139
  def calcule_size_drop(self):
424
1140
 
@@ -428,7 +1144,7 @@ class ImageCropper(QMainWindow):
428
1144
  else:
429
1145
  print_pdf = False
430
1146
 
431
- if hasattr(self, 'ret'):
1147
+ if hasattr(self, 'ret') and self.ret is not None:
432
1148
  pass;
433
1149
  else:
434
1150
  QMessageBox.warning(self, '', 'No rectangle drawn for cropping. Please draw rectangle first and cut the image.')
@@ -440,38 +1156,191 @@ class ImageCropper(QMainWindow):
440
1156
  for output_field, number in zip([self.int_output1, self.int_output2, self.int_output3], numbers):
441
1157
  output_field.setText(f'Number: {number}')
442
1158
 
443
- set_file_1 = conc_scat_video(3, file_video =self.file_path, px_mm = float(numbers[0]) , step = int(numbers[1]), time_limit = int(numbers[2]), Co = float(numbers[3]) , retangulo = self.ret, print_pdf = print_pdf);
1159
+ set_file_1 = conc_scat_video(3, file_video =self.file_path, px_mm = float(numbers[0]) , step = int(numbers[1]), time_limit = int(numbers[2]), Co = float(numbers[3]), retangulo = self.ret, print_pdf = print_pdf);
444
1160
  result_image = set_file_1.read_video();
445
1161
  if result_image:
446
1162
  self.result_image = QPixmap(result_image)
447
- self.original_image = self.result_image.toImage()
448
1163
  self.image = self.result_image.toImage()
449
- self.result_label.setPixmap(QPixmap.fromImage(self.original_image))
450
- self.result_label.setScaledContents(True)
1164
+ self.result_label.setPixmap(QPixmap.fromImage(self.image))
1165
+ # self.result_label.setScaledContents(True)
451
1166
  else:
452
1167
  QMessageBox.warning(self, 'Warning', 'Please Fill the forms.')
453
1168
  return;
454
1169
 
1170
+ def update_rect_from_spinboxes(self):
1171
+ """Atualiza o retângulo quando o usuário muda os valores manualmente."""
1172
+ if not hasattr(self, "current_rect"):
1173
+ self.current_rect = QRect()
455
1174
 
456
- def update_image(self):
1175
+ # Se o usuário estiver desenhando, ignore alterações manuais temporariamente
1176
+ if getattr(self, "drawing", False):
1177
+ return
457
1178
 
458
- if self.original_image and self.pixmap:
459
- self.image = self.original_image.copy() # Restore the original image
460
- painter = QPainter(self.image)
461
- painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
462
- if not self.current_rect.isNull():
463
- painter.drawRect(self.current_rect)
464
- painter.end()
465
- self.pixmap = QPixmap.fromImage(self.image)
466
- self.image_label.setPixmap(self.pixmap)
1179
+ if not hasattr(self, "pixmap") or self.pixmap.isNull():
1180
+ return
1181
+
1182
+ img_width = self.pixmap.width()
1183
+ img_height = self.pixmap.height()
1184
+
1185
+ x = self.rect_x_spin.value()
1186
+ y = self.rect_y_spin.value()
1187
+ w = self.rect_w_spin.value()
1188
+ h = self.rect_h_spin.value()
1189
+
1190
+ # Ajusta se ultrapassar a borda direita ou inferior
1191
+ if x + w > img_width:
1192
+ w = img_width - x
1193
+ self.rect_w_spin.blockSignals(True)
1194
+ self.rect_w_spin.setValue(w)
1195
+ self.rect_w_spin.blockSignals(False)
1196
+
1197
+ if y + h > img_height:
1198
+ h = img_height - y
1199
+ self.rect_h_spin.blockSignals(True)
1200
+ self.rect_h_spin.setValue(h)
1201
+ self.rect_h_spin.blockSignals(False)
1202
+
1203
+ # 🔹 Garante que x, y não fiquem fora da imagem
1204
+ if x < 0:
1205
+ x = 0
1206
+ self.rect_x_spin.blockSignals(True)
1207
+ self.rect_x_spin.setValue(0)
1208
+ self.rect_x_spin.blockSignals(False)
1209
+
1210
+ if y < 0:
1211
+ y = 0
1212
+ self.rect_y_spin.blockSignals(True)
1213
+ self.rect_y_spin.setValue(0)
1214
+ self.rect_y_spin.blockSignals(False)
1215
+
1216
+ # 🔹 Atualiza o retângulo e redesenha
1217
+ self.current_rect = QRect(x, y, w, h)
1218
+ self.update_image()
1219
+
1220
+
1221
+
1222
+ def update_spinboxes_from_rect(self):
1223
+ """Atualiza os campos (x, y, largura, altura) com base no retângulo atual desenhado."""
1224
+ if not hasattr(self, "current_rect") or self.current_rect.isNull():
1225
+ return
1226
+
1227
+ rect = self.current_rect
1228
+
1229
+ # Evita loops infinitos de sinal: desliga os sinais temporariamente
1230
+ self.rect_x_spin.blockSignals(True)
1231
+ self.rect_y_spin.blockSignals(True)
1232
+ self.rect_w_spin.blockSignals(True)
1233
+ self.rect_h_spin.blockSignals(True)
1234
+
1235
+ # Atualiza os valores dos campos com o retângulo atual
1236
+ self.rect_x_spin.setValue(rect.x())
1237
+ self.rect_y_spin.setValue(rect.y())
1238
+ self.rect_w_spin.setValue(rect.width())
1239
+ self.rect_h_spin.setValue(rect.height())
1240
+
1241
+ # Reativa os sinais
1242
+ self.rect_x_spin.blockSignals(False)
1243
+ self.rect_y_spin.blockSignals(False)
1244
+ self.rect_w_spin.blockSignals(False)
1245
+ self.rect_h_spin.blockSignals(False)
1246
+
1247
+ def update_rect_limits(self):
1248
+
1249
+
1250
+ if not hasattr(self, "pixmap") or self.pixmap.isNull():
1251
+ return
1252
+
1253
+ # Se o usuário está desenhando, não interfere
1254
+ if getattr(self, "drawing", False):
1255
+ return
1256
+
1257
+ img_width = getattr(self, "image_width", self.pixmap.width())
1258
+ img_height = getattr(self, "image_height", self.pixmap.height())
1259
+
1260
+ x = self.rect_x_spin.value()
1261
+ y = self.rect_y_spin.value()
1262
+ w = self.rect_w_spin.value()
1263
+ h = self.rect_h_spin.value()
1264
+
1265
+ # Define novos limites
1266
+ self.rect_x_spin.setRange(0, img_width - 1)
1267
+ self.rect_y_spin.setRange(0, img_height - 1)
1268
+ self.rect_w_spin.setRange(1, img_width - x)
1269
+ self.rect_h_spin.setRange(1, img_height - y)
1270
+
1271
+ # Se os valores atuais de largura/altura ultrapassarem os limites, reduza automaticamente
1272
+ if x + w > img_width:
1273
+ self.rect_w_spin.setValue(img_width - x)
1274
+ self.rect_w_spin.blockSignals(True)
1275
+ self.rect_w_spin.setValue(img_width - x)
1276
+ self.rect_w_spin.blockSignals(False)
1277
+
1278
+ if y + h > img_height:
1279
+ self.rect_h_spin.setValue(img_height - y)
1280
+ self.rect_h_spin.blockSignals(True)
1281
+ self.rect_h_spin.setValue(img_height - y)
1282
+ self.rect_h_spin.blockSignals(False)
1283
+
1284
+ # Só atualiza o retângulo se o usuário não estiver desenhando com o mouse
1285
+ if not getattr(self, "drawing", False):
1286
+ self.current_rect = QRect(x, y, self.rect_w_spin.value(), self.rect_h_spin.value())
1287
+ self.update_image()
1288
+
1289
+
1290
+
1291
+ def update_image(self, frame=None):
1292
+
1293
+ if frame is not None:
1294
+ self.image = cv2_to_qimage(frame)
1295
+ self.original_image = self.image.copy()
1296
+ elif self.image is None and self.original_image is not None:
1297
+ self.image = self.original_image.copy()
1298
+ elif self.original_image is not None:
1299
+ self.image = self.original_image.copy()
1300
+
1301
+ else:
1302
+ return
1303
+
1304
+ painter = QPainter(self.image)
1305
+ painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
1306
+
1307
+
1308
+ if hasattr(self, "current_rect") and not self.current_rect.isNull():
1309
+ painter.drawRect(self.current_rect)
1310
+ painter.end()
1311
+
1312
+ self.pixmap = QPixmap.fromImage(self.image)
1313
+ self.image_label.setPixmap(self.pixmap)
1314
+
1315
+
1316
+
1317
+ # if self.original_image and self.pixmap:
1318
+ #
1319
+ # self.image = self.original_image.copy() # Restore the original image
1320
+ # painter = QPainter(self.image)
1321
+ # painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
1322
+ #
1323
+ # if not self.current_rect.isNull():
1324
+ # painter.drawRect(self.current_rect)
1325
+ #
1326
+ # painter.end()
1327
+ # self.pixmap = QPixmap.fromImage(self.image)
1328
+ # self.image_label.setPixmap(self.pixmap)
467
1329
 
468
1330
  def save_rectangle_coordinates(self, rect):
469
1331
 
470
1332
  # Save vertex coordinates to a file
471
- x1, y1 = rect.topLeft().x(), rect.topLeft().y()
472
- x2, y2 = rect.bottomRight().x(), rect.bottomRight().y()
473
- self.ret = [x1, x2, y1,y2]
474
- coordinates = f"Vértices do Retângulo: ({x1}, {y1}), ({x2}, {y2})"
1333
+ # x1, y1 = rect.topLeft().x(), rect.topLeft().y()
1334
+ # x2, y2 = rect.bottomRight().x(), rect.bottomRight().y()
1335
+ # self.ret = [x1, x2, y1,y2]
1336
+
1337
+
1338
+ x = self.current_rect.x()
1339
+ y = self.current_rect.y()
1340
+ w = self.current_rect.width()
1341
+ h = self.current_rect.height()
1342
+ self.ret = [x, y, w,h]
1343
+ coordinates = f"Vértice do Retângulo: ({x}, {y}), width ({w}, height {h})"
475
1344
 
476
1345
  # Save the coordinates to a text file
477
1346
  save_path = "data/";
@@ -784,6 +1653,15 @@ def cv_destroy_all_windows_safe():
784
1653
  pass # headless
785
1654
 
786
1655
 
1656
+ def cv2_to_qimage(frame):
1657
+
1658
+ """Converte frame do OpenCV (BGR) para QImage (RGB)."""
1659
+ rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
1660
+ h, w, ch = rgb_image.shape
1661
+ bytes_per_line = ch * w
1662
+
1663
+ return QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888).copy()
1664
+
787
1665
  def get_dir_paths( **kwargs):
788
1666
 
789
1667
  # Create a hidden Tkinter window
@@ -836,10 +1714,10 @@ def get_info_video():
836
1714
 
837
1715
  questions = [
838
1716
  "Type the interval between frames to get the drop size: ",
839
- "Type the START pixel value, in the image in X, to select the drop region: ",
840
- "Type the END pixel value, in the image in X, to select the drop region: ",
841
- "Type the START pixel value, in the image in Y, to select the drop region: ",
842
- "Type the END pixel value, in the image in Y, to select the drop region: ",
1717
+ "Type the START pixel value (left bottom), in the image in X EDGE, to select the drop region: ",
1718
+ "Type the START pixel value (left bottom), in the image in Y EDGE, to select the drop region: ",
1719
+ "Type the WIDTH value, in the image, to select the drop region: ",
1720
+ "Type the HEIGHT value, in the image, to select the drop region: ",
843
1721
  "Type the value of pixel by millimeters: ",
844
1722
  "Type the maximum video analysis time (s): "
845
1723
  ]
@@ -929,14 +1807,14 @@ class conc_scat_video:
929
1807
  self.Co = (float(str(line[line.index(':')+1:])));
930
1808
  if line.find('step_1:') != -1:
931
1809
  self.step = round(float(str(line[line.index(':')+1:])));
932
- if line.find('start pixel x_1:') != -1:
1810
+ if line.find('left bottom pixel x_1:') != -1:
933
1811
  self.start_x = round(float(str(line[line.index(':')+1:])));
934
- if line.find('end pixel x_1:') != -1:
935
- self.end_x = round(float(str(line[line.index(':')+1:])));
936
- if line.find('start pixel y_1:') != -1:
1812
+ if line.find('left bottom pixel y_1:') != -1:
937
1813
  self.start_y = round(float(str(line[line.index(':')+1:])));
938
- if line.find('end pixel y_1:') != -1:
939
- self.end_y = round(float(str(line[line.index(':')+1:])));
1814
+ if line.find('width_1:') != -1:
1815
+ self.width = round(float(str(line[line.index(':')+1:])));
1816
+ if line.find('height_1:') != -1:
1817
+ self.height = round(float(str(line[line.index(':')+1:])));
940
1818
  if line.find('pixel/mm_1:') != -1:
941
1819
  self.px_mm = (float(str(line[line.index(':')+1:])));
942
1820
  self.px_mm_inv = 1. / self.px_mm
@@ -974,14 +1852,14 @@ class conc_scat_video:
974
1852
  self.Co = (float(str(line[line.index(':')+1:])));
975
1853
  if line.find('step_2:') != -1:
976
1854
  self.step = round(float(str(line[line.index(':')+1:])));
977
- if line.find('start pixel x_2:') != -1:
1855
+ if line.find('left bottom pixel x_2:') != -1:
978
1856
  self.start_x = round(float(str(line[line.index(':')+1:])));
979
- if line.find('end pixel x_2:') != -1:
980
- self.end_x = round(float(str(line[line.index(':')+1:])));
981
- if line.find('start pixel y_2:') != -1:
1857
+ if line.find('left bottom pixel y_2:') != -1:
982
1858
  self.start_y = round(float(str(line[line.index(':')+1:])));
983
- if line.find('end pixel y_2:') != -1:
984
- self.end_y = round(float(str(line[line.index(':')+1:])));
1859
+ if line.find('width_2:') != -1:
1860
+ self.width = round(float(str(line[line.index(':')+1:])));
1861
+ if line.find('height_2:') != -1:
1862
+ self.height = round(float(str(line[line.index(':')+1:])));
985
1863
  if line.find('pixel/mm_2:') != -1:
986
1864
  self.px_mm = (float(str(line[line.index(':')+1:])));
987
1865
  self.px_mm_inv = 1. / self.px_mm
@@ -1029,9 +1907,9 @@ class conc_scat_video:
1029
1907
  if 'retangulo' in kwargs:
1030
1908
  ret = kwargs['retangulo'];
1031
1909
  self.start_x = ret[0];
1032
- self.end_x = ret[1];
1033
- self.start_y = ret[2];
1034
- self.end_y= ret[3];
1910
+ self.start_y = ret[1];
1911
+ self.width = ret[2];
1912
+ self.height = ret[3];
1035
1913
  if 'print_pdf' in kwargs:
1036
1914
  self.print_pdf = kwargs['print_pdf'];
1037
1915
  else:
@@ -1200,6 +2078,8 @@ class conc_scat_video:
1200
2078
 
1201
2079
 
1202
2080
  # self.video_c = os.path.getctime(self.file_video);
2081
+ # print(self.file_video)
2082
+
1203
2083
 
1204
2084
  self.video_m = os.path.getmtime(self.file_video);
1205
2085
 
@@ -1256,14 +2136,16 @@ class conc_scat_video:
1256
2136
  # crop image to restrict background
1257
2137
  new_start_x = self.start_x;
1258
2138
  new_start_y = self.start_y;
1259
- new_end_x = self.end_x;
1260
- new_end_y = self.end_y;
1261
- ref_width = abs(self.end_x - self.start_x);
1262
- ref_height = abs(self.end_y - self.start_y);
2139
+ ref_end_x = new_end_x = self.start_x + self.width;
2140
+ ref_end_y = new_end_y = self.start_y + self.height;
2141
+ ref_width = abs(self.width);
2142
+ ref_height = abs(self.height);
1263
2143
 
1264
2144
  amplie = False;
1265
2145
  factor = 1;
1266
2146
  start_time = timelib.time()
2147
+
2148
+ progress = ProgressHandler(self, label="Reading frames...", maximum=total_frames)
1267
2149
 
1268
2150
  while has_frame: # take frame just end of video
1269
2151
 
@@ -1294,18 +2176,18 @@ class conc_scat_video:
1294
2176
  _w = temp_size_window[data_i-window:data_i,0]; _h = temp_size_window[data_i-window:data_i,1];
1295
2177
  avg_w = numpy.mean(_w) ;
1296
2178
  avg_h = numpy.mean(_h) ;
1297
- if avg_w < 0.15* abs(self.end_x - self.start_x): avg_w = 0.15 * abs(self.end_x - self.start_x);
1298
- if avg_h < 0.15* abs(self.end_y - self.start_y): avg_h =0.15 *abs(self.end_y - self.start_y);
2179
+ if avg_w < 0.15* abs(ref_end_x - self.start_x): avg_w = 0.15 * abs(ref_end_x - self.start_x);
2180
+ if avg_h < 0.15* abs(ref_end_y - self.start_y): avg_h =0.15 *abs(ref_end_y - self.start_y);
1299
2181
  factor_exp = 0.15;
1300
2182
  new_start_x = int(( x_center - avg_w/2) - (factor_exp * avg_w));
1301
2183
  new_end_x = int(( x_center + avg_w/2) + (factor_exp * avg_w));
1302
2184
  if new_start_x < self.start_x: new_start_x = self.start_x;
1303
- if new_end_x > self.end_x: new_end_x = self.end_x;
2185
+ if new_end_x > ref_end_x: new_end_x = ref_end_x;
1304
2186
  ref_width = abs(new_end_x - new_start_x);
1305
2187
  new_start_y = int(( y_center - avg_h/2) - (factor_exp * avg_h));
1306
2188
  new_end_y = int(( y_center + avg_h/2) + (factor_exp * avg_h));
1307
2189
  if new_start_y < self.start_y: new_start_y = self.start_y;
1308
- if new_end_y > self.end_y: new_end_y = self.end_y;
2190
+ if new_end_y > ref_end_y: new_end_y = ref_end_y;
1309
2191
  ref_height = abs(new_end_y - new_start_y);
1310
2192
  amplie = True;
1311
2193
 
@@ -1314,7 +2196,9 @@ class conc_scat_video:
1314
2196
  # cv2.imwrite("teste.png",frame); #exit();
1315
2197
  #crop image
1316
2198
  imagem = frame[new_start_y:new_end_y, new_start_x:new_end_x];
1317
- # cv2.imwrite("teste1.png",imagem); #exit();
2199
+ # cv2.imwrite("teste1.png",imagem); #exit();
2200
+ # cv2.imshow("teste1",imagem)
2201
+
1318
2202
 
1319
2203
  img_h, img_w = imagem.shape[:2];
1320
2204
  if data_i >= 1 or amplie:
@@ -1322,10 +2206,13 @@ class conc_scat_video:
1322
2206
  factor = 12;
1323
2207
  new_w = int(img_w * factor)
1324
2208
  new_h = int(img_h * factor)
2209
+
1325
2210
 
1326
2211
  if new_w <= 1 or new_h <= 1:
1327
- print(f"Error, check the video; it seems probably there is no droplet image starting from {int(time)} s.")
1328
- break
2212
+ message = f"Error, check the video; it seems probably there is no droplet image starting from {int(time)} s."
2213
+ show_message(self, "Check the video", message, details=None, level="error")
2214
+ # print(f"Error, check the video; it seems probably there is no droplet image starting from {int(time)} s.")
2215
+ return None
1329
2216
 
1330
2217
 
1331
2218
 
@@ -1446,6 +2333,7 @@ class conc_scat_video:
1446
2333
  data_time_size[data_i][0] = time; # time
1447
2334
  data_time_size[data_i][1] = (width / 2.) / (self.px_mm ); # width -> semi axes
1448
2335
  data_time_size[data_i][2] = (height/ 2.) / (self.px_mm); # height -> semi axes
2336
+
1449
2337
  if data_i == 0:
1450
2338
  self.Vo = calcule_vol_spheroide(data_time_size[data_i][1], data_time_size[data_i][2] ) / 1000. # use in mL
1451
2339
  temp = datetime.fromtimestamp(self.video_m) + timedelta(seconds=time);
@@ -1472,7 +2360,7 @@ class conc_scat_video:
1472
2360
  # temp_m[flag_temp,1] = temp_m[flag_temp,1] - 1
1473
2361
  # if temp_m[flag_temp,1] < 1: flag_temp = flag_temp + 1;
1474
2362
 
1475
-
2363
+
1476
2364
 
1477
2365
  data_i = data_i +1;
1478
2366
  # cv2.imshow("image fim", imagem)
@@ -1480,18 +2368,27 @@ class conc_scat_video:
1480
2368
 
1481
2369
  frame_count += 1
1482
2370
  elapsed_time = timelib.time() - start_time;
1483
- print(f"Iteration {frame_count + 1}/{(self.time_limit*fps)}, Elapsed time: {elapsed_time:.2f} seconds", end='\r')
2371
+
2372
+ # print(f"Iteration {frame_count + 1}/{(self.time_limit*fps)}, Elapsed time: {elapsed_time:.2f} seconds", end='\r')
2373
+ progress.update(frame_count, elapsed_time)
2374
+ if progress.was_canceled():
2375
+ progress.finish()
2376
+ # print("Process canceled by user.")
2377
+ return None
1484
2378
 
1485
2379
  has_frame, frame = video.read()
1486
2380
 
1487
-
2381
+ progress.finish()
1488
2382
  file_data_imgs.close();
1489
-
2383
+
2384
+
1490
2385
  new_data_time_size = delete_value_extrem(data_time_size);
2386
+
1491
2387
  self.coef_pol_w = numpy.polyfit(new_data_time_size[:, 0],new_data_time_size[:, 1],12);
1492
2388
  self.coef_pol_h = numpy.polyfit(new_data_time_size[:, 0],new_data_time_size[:, 2],12);
1493
2389
  self.coef_pol_area = numpy.polyfit(new_data_time_size[:, 0],new_data_time_size[:, 6],12);
1494
2390
  self.coef_pol_conc = numpy.polyfit(new_data_time_size[:, 0],new_data_time_size[:, 7],12);
2391
+
1495
2392
 
1496
2393
 
1497
2394
 
@@ -1499,6 +2396,7 @@ class conc_scat_video:
1499
2396
  file_out = os.path.join(path_dir_imgs,self.name_file+'_Video_time_size.csv');
1500
2397
  file_out = os.path.normpath(file_out);
1501
2398
  save_data_video(new_data_time_size,self.coef_pol_w, self.coef_pol_h, self.coef_pol_area, self.coef_pol_conc, file_out);
2399
+
1502
2400
 
1503
2401
 
1504
2402
  file_out = os.path.join(path_dir_imgs,self.name_file+'_sizes.png');
@@ -1513,11 +2411,76 @@ class conc_scat_video:
1513
2411
 
1514
2412
  if self.print_pdf:
1515
2413
  self.print_frames_pdf(path_dir_imgs, file_image_str)
1516
-
2414
+
1517
2415
 
1518
2416
  return file_out
1519
2417
 
1520
2418
 
2419
+
2420
+ class ProgressHandler:
2421
+
2422
+ def __init__(self, parent=None, label="Processing...", maximum=100):
2423
+
2424
+ self.parent = parent
2425
+ self.maximum = maximum
2426
+ self.current = 0
2427
+ self.use_gui = False
2428
+ self.progress = None
2429
+
2430
+ # Detecta se GUI está ativa
2431
+ app = QApplication.instance()
2432
+ if app is not None:
2433
+ try:
2434
+ # Tenta criar mesmo sem parent QWidget
2435
+ if isinstance(parent, QWidget):
2436
+ self.progress = QProgressDialog(label, "Cancel", 0, maximum, parent)
2437
+ else:
2438
+ self.progress = QProgressDialog(label, "Cancel", 0, maximum)
2439
+ self.progress.setWindowTitle("Please wait")
2440
+ self.progress.setWindowModality(Qt.WindowModal)
2441
+ self.progress.setMinimumDuration(0)
2442
+ self.progress.setValue(0)
2443
+ self.use_gui = True
2444
+ except Exception as e:
2445
+ print(f"[ProgressHandler] ⚠️ Falling back to terminal mode: {e}")
2446
+ self.use_gui = False
2447
+ else:
2448
+ self.use_gui = False
2449
+
2450
+ def update(self, value, elapsed=None):
2451
+ """Atualiza o progresso (GUI ou terminal)"""
2452
+ self.current = value
2453
+ if self.use_gui and self.progress:
2454
+ self.progress.setValue(value)
2455
+ QApplication.processEvents()
2456
+ else:
2457
+ if elapsed is not None:
2458
+ print(
2459
+ f"Iteration {value}/{self.maximum}, Elapsed time: {elapsed:.2f} seconds",
2460
+ end="\r"
2461
+ )
2462
+ else:
2463
+ print(f"Progress: {value}/{self.maximum}", end="\r")
2464
+
2465
+ def was_canceled(self):
2466
+ """Verifica se o usuário cancelou (apenas GUI)"""
2467
+ if self.use_gui and self.progress:
2468
+ return self.progress.wasCanceled()
2469
+ return False
2470
+
2471
+ def finish(self):
2472
+ """Finaliza o progresso"""
2473
+ if self.use_gui and self.progress:
2474
+ self.progress.setValue(self.maximum)
2475
+ QApplication.processEvents() # 🔹 força atualização final
2476
+ self.progress.close() # 🔹 fecha explicitamente o diálogo
2477
+ QApplication.processEvents() # 🔹 garante que o fechamento seja processado
2478
+ else:
2479
+ print()
2480
+
2481
+
2482
+
2483
+
1521
2484
  def menu():
1522
2485
  print("\n Options:")
1523
2486
  print("1. Video analysis")
@@ -1528,25 +2491,6 @@ def menu():
1528
2491
 
1529
2492
 
1530
2493
 
1531
- def draw_square(event, x, y, flags, param, imagem):
1532
-
1533
-
1534
- vertices = []
1535
-
1536
- imagem = cv2.imread('sample.jpg')
1537
-
1538
- if event == cv2.EVENT_LBUTTONDOWN:
1539
- vertices.append((x, y))
1540
-
1541
- if len(vertices) == 2:
1542
- # Draw the square on the original image
1543
- cv2.rectangle(imagem, vertices[0], vertices[1], (255, 0, 0), 5) # Blue with thickness 2
1544
- cv_imshow_safe("Imagem", imagem) # cv2.imshow('Imagem', imagem)
1545
- vertices.clear()
1546
-
1547
-
1548
- for i, vertice in enumerate(vertices):
1549
- print(f"Vértice {i + 1}: {vertice}")
1550
2494
 
1551
2495
  def save_data_video(data_in, coef_w, coef_h, coef_area, coef_conc, output_file):
1552
2496
 
@@ -1559,9 +2503,11 @@ def save_data_video(data_in, coef_w, coef_h, coef_area, coef_conc, output_file):
1559
2503
  file_op.write(f"Coeficient concentration: {', '.join([f'{i_coef:.7e}' for i_coef in coef_conc])}\n")
1560
2504
  file_op.write("Frame,dropDX(mm),dropDY(mm),surface(mm^2),Volume(\u03bcL),RelativeConcentration(%),date,time(s),time(min)\n")
1561
2505
 
2506
+
1562
2507
  for i_data in range(0, len(data_in)):
1563
2508
 
1564
- str_ = f"{int(data_in[i_data,4]):>5d}, {data_in[i_data,1]:.2f}, {data_in[i_data,2]:.3e}, {data_in[i_data,6]:.3e}, {data_in[i_data,8]:.3e}, {data_in[i_data,7]:.3e}, {datetime.fromtimestamp(data_in[i_data,3]).strftime('%Y-%m-%d %H:%M:%S')}, {data_in[i_data,0]:.2f}, {data_in[i_data,4]:.2f} \n";
2509
+
2510
+ str_ = f"{int(data_in[i_data,4]):>5d}, {data_in[i_data,1]:.3e}, {data_in[i_data,2]:.3e}, {data_in[i_data,6]:.3e}, {data_in[i_data,8]:.3e}, {data_in[i_data,7]:.3e}, {datetime.fromtimestamp(data_in[i_data,3]).strftime('%Y-%m-%d %H:%M:%S')}, {data_in[i_data,0]:.2f}, {data_in[i_data,4]:.2f} \n";
1565
2511
 
1566
2512
  file_op.write(str_);
1567
2513
  file_op.close()
@@ -1585,6 +2531,42 @@ def save_data_edf(data_in, output_file, option):
1585
2531
  str_ = f"{i_data['file']}, {i_data['date']}, {float(i_data['start_time']):.2f} \n";
1586
2532
  file_op.write(str_);
1587
2533
  file_op.close()
2534
+
2535
+
2536
+ def show_message(self, title, message, details=None, level="error"):
2537
+
2538
+ import traceback
2539
+ import sys
2540
+ from PyQt5.QtWidgets import QMessageBox, QApplication
2541
+
2542
+ app = QApplication.instance() # verifica se a GUI está ativa
2543
+
2544
+ if app is not None:
2545
+
2546
+ msg = QMessageBox(self if hasattr(self, "windowTitle") else None)
2547
+ if level.lower() == "error":
2548
+ msg.setIcon(QMessageBox.Critical)
2549
+ elif level.lower() == "warning":
2550
+ msg.setIcon(QMessageBox.Warning)
2551
+ else:
2552
+ msg.setIcon(QMessageBox.Information)
2553
+
2554
+ msg.setWindowTitle(title)
2555
+ msg.setText(message)
2556
+ if details:
2557
+ msg.setDetailedText(details)
2558
+ msg.exec_()
2559
+ else:
2560
+
2561
+ print(f"\n{'='*60}")
2562
+ print(f"[{level.upper()}] {title}")
2563
+ print(f"→ {message}")
2564
+ if details:
2565
+ print("-" * 60)
2566
+ print(details)
2567
+ print("-" * 60)
2568
+ print(f"{'='*60}\n")
2569
+
1588
2570
 
1589
2571
  def read_file_video(input_file):
1590
2572
 
@@ -1642,4 +2624,108 @@ def calcule_surface_spheroide(edge_1, edge_2):
1642
2624
  e = np.sqrt(1.0 - (edge_1*edge_1)/(edge_2*edge_2)) # 0 < e < 1
1643
2625
  return 2.0 * np.pi * edge_1*edge_1 * (1.0 + (edge_2/(edge_1*e)) * np.arcsin(e))
1644
2626
 
1645
-
2627
+
2628
+
2629
+ def _int_to_fourcc(v: int) -> str:
2630
+ if not v:
2631
+ return ""
2632
+ chars = []
2633
+ for i in range(4):
2634
+ chars.append(chr((v >> (8 * i)) & 0xFF))
2635
+ s = "".join(chars)
2636
+ if not s.isprintable():
2637
+ return ""
2638
+ return s
2639
+
2640
+
2641
+ def _default_fourcc_candidates_for_ext(ext: str) -> List[str]:
2642
+ ext = ext.lower()
2643
+ # Reasonable candidates given typical OpenCV/FFmpeg builds (no guarantee)
2644
+ if ext in (".mp4", ".m4v", ".mov"):
2645
+ return ["mp4v", "avc1", "h264"] # mp4v is most portable in OpenCV wheels
2646
+ if ext == ".avi":
2647
+ return ["MJPG", "XVID", "mp4v"]
2648
+ if ext == ".mkv":
2649
+ return ["mp4v", "MJPG", "XVID"]
2650
+ # Very uncommon/unsupported for writing via OpenCV:
2651
+ if ext == ".flv":
2652
+ return [] # force user to change container
2653
+ return ["mp4v"]
2654
+
2655
+
2656
+ def _pick_writer_fourcc(cap: cv2.VideoCapture, out_path: str, user_codec: Optional[str]) -> List[str]:
2657
+ ext = os.path.splitext(out_path)[1].lower()
2658
+ # If user forces a codec, try it first
2659
+ candidates: List[str] = []
2660
+ if user_codec:
2661
+ candidates.append(user_codec)
2662
+
2663
+ # Try to reuse detected codec (rarely usable for writing, but try)
2664
+ detected = _int_to_fourcc(int(cap.get(cv2.CAP_PROP_FOURCC)))
2665
+ if detected and detected.strip("\x00").strip():
2666
+ candidates.append(detected)
2667
+
2668
+ # Add common candidates for the chosen extension
2669
+ candidates += _default_fourcc_candidates_for_ext(ext)
2670
+
2671
+ # Finally, add a few generic fallbacks
2672
+ for fallback in ("mp4v", "MJPG", "XVID", "avc1"):
2673
+ if fallback not in candidates:
2674
+ candidates.append(fallback)
2675
+
2676
+ # Remove empties/dupes while preserving order
2677
+ seen = set()
2678
+ out = []
2679
+ for c in candidates:
2680
+ c = (c or "").strip()
2681
+ if not c:
2682
+ continue
2683
+ if c not in seen:
2684
+ seen.add(c)
2685
+ out.append(c)
2686
+ return out
2687
+
2688
+
2689
+ def parse_drop_spec(spec: str, total_frames: int) -> Set[int]:
2690
+ if not spec:
2691
+ return set()
2692
+ result: Set[int] = set()
2693
+ for chunk in spec.split(","):
2694
+ chunk = chunk.strip()
2695
+ if not chunk:
2696
+ continue
2697
+ if "-" in chunk:
2698
+ a, b = chunk.split("-", 1)
2699
+ a = a.strip()
2700
+ b = b.strip()
2701
+ if not a.isdigit() or not b.isdigit():
2702
+ raise ValueError(f"Invalid range '{chunk}' in --drop spec")
2703
+ start = int(a)
2704
+ end = int(b)
2705
+ if start > end:
2706
+ start, end = end, start
2707
+ for i in range(start, end + 1):
2708
+ if 0 <= i < total_frames:
2709
+ result.add(i)
2710
+ else:
2711
+ if not chunk.isdigit():
2712
+ raise ValueError(f"Invalid index '{chunk}' in --drop spec")
2713
+ i = int(chunk)
2714
+ if 0 <= i < total_frames:
2715
+ result.add(i)
2716
+ return result
2717
+
2718
+ def _open_writer_any(tmp_out_path: str, fps: float, size: Tuple[int, int], candidates: List[str]) -> Tuple[Optional[cv2.VideoWriter], Optional[str]]:
2719
+
2720
+
2721
+ for c in candidates:
2722
+ try:
2723
+ fourcc = cv2.VideoWriter_fourcc(*c)
2724
+ w = cv2.VideoWriter(tmp_out_path, fourcc, fps, size)
2725
+ if w.isOpened():
2726
+ return w, c
2727
+ # release and try next
2728
+ w.release()
2729
+ except Exception:
2730
+ pass
2731
+ return None, None