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 +1246 -160
- {drap-0.0.4.post8.dist-info → drap-0.0.4.post11.dist-info}/METADATA +1 -1
- {drap-0.0.4.post8.dist-info → drap-0.0.4.post11.dist-info}/RECORD +7 -7
- {drap-0.0.4.post8.dist-info → drap-0.0.4.post11.dist-info}/WHEEL +0 -0
- {drap-0.0.4.post8.dist-info → drap-0.0.4.post11.dist-info}/entry_points.txt +0 -0
- {drap-0.0.4.post8.dist-info → drap-0.0.4.post11.dist-info}/licenses/LICENSE +0 -0
- {drap-0.0.4.post8.dist-info → drap-0.0.4.post11.dist-info}/top_level.txt +0 -0
drap/utils.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
-
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton, QFileDialog,
|
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.
|
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("
|
201
|
-
self.int_input3.setText("
|
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 = "/
|
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
|
-
|
410
|
+
|
411
|
+
rval, frame = self.video.read();
|
228
412
|
|
229
|
-
if not
|
230
|
-
QMessageBox.critical(self, 'Error',
|
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
|
-
|
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
|
-
|
236
|
-
|
237
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
369
|
-
|
370
|
-
|
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 =
|
373
|
-
|
374
|
-
|
375
|
-
self.
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
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
|
387
|
-
QMessageBox.warning(self,
|
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
|
-
|
394
|
-
save_path
|
395
|
-
|
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])
|
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.
|
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
|
-
|
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
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
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
|
-
|
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
|
841
|
-
"Type the
|
842
|
-
"Type the
|
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('
|
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('
|
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('
|
939
|
-
self.
|
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('
|
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('
|
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('
|
984
|
-
self.
|
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.
|
1033
|
-
self.
|
1034
|
-
self.
|
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.
|
1260
|
-
new_end_y = self.
|
1261
|
-
ref_width = abs(self.
|
1262
|
-
ref_height = abs(self.
|
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(
|
1298
|
-
if avg_h < 0.15* abs(
|
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 >
|
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 >
|
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
|
-
|
1328
|
-
|
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
|
-
|
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
|
-
|
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
|