PaIRS-UniNa 0.2.5__cp313-cp313-win_amd64.whl → 0.2.9__cp313-cp313-win_amd64.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.
- PaIRS_UniNa/Calibration_Tab.py +15 -0
- PaIRS_UniNa/Changes.txt +54 -2
- PaIRS_UniNa/Explorer.py +118 -19
- PaIRS_UniNa/FolderLoop.py +196 -6
- PaIRS_UniNa/Input_Tab.py +167 -55
- PaIRS_UniNa/Input_Tab_CalVi.py +15 -17
- PaIRS_UniNa/Input_Tab_tools.py +9 -10
- PaIRS_UniNa/Output_Tab.py +2 -4
- PaIRS_UniNa/PaIRS_pypacks.py +227 -56
- PaIRS_UniNa/Process_Tab.py +2 -2
- PaIRS_UniNa/Process_Tab_Disp.py +1 -1
- PaIRS_UniNa/SPIVCalHelp.py +155 -0
- PaIRS_UniNa/Saving_tools.py +7 -7
- PaIRS_UniNa/TabTools.py +7 -4
- PaIRS_UniNa/Vis_Tab.py +129 -60
- PaIRS_UniNa/Whatsnew.py +15 -3
- PaIRS_UniNa/_PaIRS_PIV.pyd +0 -0
- PaIRS_UniNa/__init__.py +4 -4
- PaIRS_UniNa/addwidgets_ps.py +28 -20
- PaIRS_UniNa/calibView.py +7 -0
- PaIRS_UniNa/gPaIRS.py +179 -34
- PaIRS_UniNa/icons/flaticon_PaIRS_download_warning.png +0 -0
- PaIRS_UniNa/icons/folder_loop_cleanup.png +0 -0
- PaIRS_UniNa/icons/folder_loop_cleanup_off.png +0 -0
- PaIRS_UniNa/icons/information.png +0 -0
- PaIRS_UniNa/icons/information2.png +0 -0
- PaIRS_UniNa/icons/pencil_bw.png +0 -0
- PaIRS_UniNa/icons/scan_path_loop.png +0 -0
- PaIRS_UniNa/icons/scan_path_loop_off.png +0 -0
- PaIRS_UniNa/icons/spiv_setup_no.png +0 -0
- PaIRS_UniNa/icons/spiv_setup_ok.png +0 -0
- PaIRS_UniNa/pivParFor.py +1 -1
- PaIRS_UniNa/procTools.py +51 -3
- PaIRS_UniNa/rqrdpckgs.txt +6 -5
- PaIRS_UniNa/stereoPivParFor.py +1 -1
- PaIRS_UniNa/ui_Calibration_Tab.py +90 -57
- PaIRS_UniNa/ui_gPairs.py +9 -3
- PaIRS_UniNa/whatsnew.txt +4 -4
- {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/METADATA +32 -17
- {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/RECORD +42 -32
- {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/WHEEL +0 -0
- {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/top_level.txt +0 -0
PaIRS_UniNa/Calibration_Tab.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .ui_Calibration_Tab import*
|
|
2
2
|
from .TabTools import*
|
|
3
3
|
from .listLib import*
|
|
4
|
+
from .SPIVCalHelp import showSPIVCalHelp
|
|
4
5
|
|
|
5
6
|
spin_tips={
|
|
6
7
|
'ncam' : 'Number of cameras',
|
|
@@ -21,6 +22,7 @@ button_tips={
|
|
|
21
22
|
combo_tips={}
|
|
22
23
|
|
|
23
24
|
class CALpar(TABpar):
|
|
25
|
+
FlagSPIVCal = True
|
|
24
26
|
def __init__(self,Process=ProcessTypes.null,Step=StepTypes.null):
|
|
25
27
|
self.setup(Process,Step)
|
|
26
28
|
super().__init__('CALpar','Calibration')
|
|
@@ -61,6 +63,8 @@ class Calibration_Tab(gPaIRS_Tab):
|
|
|
61
63
|
self.app=app
|
|
62
64
|
setAppGuiPalette(self)
|
|
63
65
|
|
|
66
|
+
self.ui.button_info.setStyleSheet("border: none;")
|
|
67
|
+
|
|
64
68
|
#------------------------------------- Declaration of parameters
|
|
65
69
|
self.CALpar_base=CALpar()
|
|
66
70
|
self.CALpar:CALpar=self.TABpar
|
|
@@ -133,6 +137,7 @@ class Calibration_Tab(gPaIRS_Tab):
|
|
|
133
137
|
self.ui.button_paste_below.setEnabled(FlagNCal and FlagCuttedItems)
|
|
134
138
|
self.ui.button_paste_above.setEnabled(FlagNCal and FlagCuttedItems)
|
|
135
139
|
self.ui.button_clean.setEnabled(FlagFiles)
|
|
140
|
+
self.ui.button_info.setVisible(self.CALpar.Process==ProcessTypes.spiv)
|
|
136
141
|
|
|
137
142
|
self.ui.button_CalVi.setVisible(self.CALpar.flagRun==0)
|
|
138
143
|
|
|
@@ -166,13 +171,23 @@ class Calibration_Tab(gPaIRS_Tab):
|
|
|
166
171
|
#*************************************************** Buttons
|
|
167
172
|
#******************** Actions
|
|
168
173
|
def button_CalVi_action(self):
|
|
174
|
+
if self.CALpar.FlagSPIVCal and self.ui.button_info.isVisible() and self.ui.button_CalVi.isChecked():
|
|
175
|
+
showSPIVCalHelp(self,self.dontShowAgainSPIVCalHelp)
|
|
169
176
|
self.CALpar.FlagCalVi=self.ui.button_CalVi.isChecked()
|
|
170
177
|
|
|
178
|
+
def dontShowAgainSPIVCalHelp(self):
|
|
179
|
+
self.CALpar.FlagSPIVCal = False
|
|
180
|
+
|
|
181
|
+
def button_info_action(self):
|
|
182
|
+
showSPIVCalHelp(self)
|
|
183
|
+
|
|
171
184
|
def button_scan_list_action(self):
|
|
172
185
|
self.ui.calTree.setLists()
|
|
173
186
|
self.CALpar.calEx=deep_duplicate(self.ui.calTree.calEx)
|
|
174
187
|
|
|
175
188
|
def button_import_action(self):
|
|
189
|
+
if self.CALpar.FlagSPIVCal and self.ui.button_info.isVisible():
|
|
190
|
+
showSPIVCalHelp(self,self.dontShowAgainSPIVCalHelp)
|
|
176
191
|
filenames, _ = QFileDialog.getOpenFileNames(self,\
|
|
177
192
|
"Select calibration files from the current directory", filter='*.cal',\
|
|
178
193
|
options=optionNativeDialog)
|
PaIRS_UniNa/Changes.txt
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
|
-
********* Changes in version 0.2.
|
|
1
|
+
********* Changes in version 0.2.9 (2025.12.12) **********
|
|
2
|
+
Bug fixes:
|
|
3
|
+
- fixed a bug in the batch folder-copy tool enabling automatic removal of incomplete image pairs and a full re-scan of destination folders to detect and resolve any image-set mismatches.
|
|
4
|
+
|
|
5
|
+
New features:
|
|
6
|
+
- a new help dialog has been introduced to assist users during the stereoscopic PIV calibration process. The dialog provides a detailed explanation of the required coordinate convention (plate defining the x–y plane, z-axis normal to the plate and x-axis aligned with the stereoscopic baseline) and displays example images illustrating correct and incorrect configurations.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
********* Changes in version 0.2.8 (2025.11.14) **********
|
|
11
|
+
Bug fixes:
|
|
12
|
+
- corrected a bug in the z-vorticity computation: velocity gradients were not properly converted to physical units, causing an error in the magnitude scales though not in the qualitative distributions;
|
|
13
|
+
- corrected the display of maps of output variables and vector fields in Vis to ensure proper alignment across option changes;
|
|
14
|
+
- fixed issues related to path handling in the batch-folder copy and other modules: paths are no longer relativized, avoiding inconsistent behavior across different operating systems or different.
|
|
15
|
+
|
|
16
|
+
New features:
|
|
17
|
+
- added new options in the batch folder-copy tool to automatically remove image pairs where one or more files are missing in the destination folders and to fully re-scan the destination folders for cases in which mismatches in the image sets may occur;
|
|
18
|
+
- extended the existing functionality in Vis for loading and visualizing past results located under the specified output path and name root: when available, the corresponding saved log file is now automatically loaded and displayed in the Log tab.
|
|
19
|
+
|
|
20
|
+
User-interface enhancements:
|
|
21
|
+
- improved the naming logic for duplicated processes: copied processes are now assigned consistent incremental suffixes;
|
|
22
|
+
- improved the example-image tree and the behavior of the step spin box in the Image Import Tool (minimum step is now 1);
|
|
23
|
+
- refined the behavior of the import button: it is now always enabled, and when no changes in the image list are detected upon importing, the user simply receives a warning message;
|
|
24
|
+
- improved the behaviour of the list of path completers in the Input tabs;
|
|
25
|
+
- in Vis, resizing settings and automatic level-reset options are no longer global but apply individually per each step.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
********* Changes in version 0.2.7 (2025.10.13) **********
|
|
29
|
+
Bug fixes:
|
|
30
|
+
- resolved an issue related to visulazion of streamlines when the image is shown in pixel units and a custom resolution is specified (in previous versions, streamlines were always represented in physical units);
|
|
31
|
+
- corrected the update of reprojection errors in CalVi when the control point grid is expanded after a calibration. Each new control point now has its error recalculated and plotted, providing a correct view of the extrapolation error;
|
|
32
|
+
- fixed the automatic resize/reshape behavior specified in the Output tab: when an image transformation is assigned, it is now preserved even if the automatic reshape button is active.
|
|
33
|
+
|
|
34
|
+
User-interface enhancements:
|
|
35
|
+
- changed the behavior of the Vis button for restricting view on the interrogation window area: clicking it now opens a popup menu that allows users to select the desired interrogation-window size to focus on among all iterations of the process;
|
|
36
|
+
- names of processes duplicated in the process tree are now automatically updated to avoid conflicts: if a name already exists, the new one is appended with an incremental suffix;
|
|
37
|
+
- improved interaction with editable input fields: when hovering the highlighted style is displayed correctly, the text cursor now appears exactly where clicked and the background color is properly restored when leaving the field.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
********* Changes in version 0.2.6 (2025.09.06) **********
|
|
42
|
+
Bug fixes:
|
|
43
|
+
- fixed bugs related to process tree management.
|
|
44
|
+
|
|
45
|
+
User-interface enhancements:
|
|
46
|
+
- enhanced release check using SSL for reliable retrieval of the latest version.
|
|
47
|
+
|
|
48
|
+
Distribution:
|
|
49
|
+
- ready-to-use executables of PaIRS are now available!
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
********* Changes in version 0.2.5 (2025.07.28) **********
|
|
2
54
|
Bug fixes:
|
|
3
55
|
- fixed bugs in launching PIV and stereoscopic PIV processes in special cases;
|
|
4
56
|
- fixed tooltip crash introduced by PySide 6.9;
|
|
@@ -17,7 +69,7 @@ User-interface enhancements:
|
|
|
17
69
|
|
|
18
70
|
Distribution:
|
|
19
71
|
- Support for Python 3.9 has been discontinued;
|
|
20
|
-
- Python 3.13 builds have been successfully created and tested.
|
|
72
|
+
- Python 3.13 builds have been successfully created and tested in Windows.
|
|
21
73
|
|
|
22
74
|
|
|
23
75
|
|
PaIRS_UniNa/Explorer.py
CHANGED
|
@@ -1318,7 +1318,7 @@ class ProcessTree(PaIRSTree):
|
|
|
1318
1318
|
par.ind[1]=int(self.FlagBin)
|
|
1319
1319
|
par.ind[2]=i
|
|
1320
1320
|
except Exception as inst:
|
|
1321
|
-
pri.Error.red(inst)
|
|
1321
|
+
pri.Error.red(f"{inst}\n{traceback.format_exc()}")
|
|
1322
1322
|
pass
|
|
1323
1323
|
if hasattr(self.gui,'setLinks'):
|
|
1324
1324
|
inds_new_list=list(inds_new)
|
|
@@ -1383,7 +1383,7 @@ class ProcessTree(PaIRSTree):
|
|
|
1383
1383
|
if self.FlagExternalDrag:
|
|
1384
1384
|
self.FlagExternalDrag=False
|
|
1385
1385
|
self.dragged_items=None
|
|
1386
|
-
self.setStyleSheet(
|
|
1386
|
+
self.setStyleSheet(self.initialStyleSheet)
|
|
1387
1387
|
if self.parent(): self.setPalette(self.parent().palette())
|
|
1388
1388
|
self.hovered_item=None
|
|
1389
1389
|
|
|
@@ -1424,7 +1424,7 @@ class ProcessTree(PaIRSTree):
|
|
|
1424
1424
|
if not self.widgets: return
|
|
1425
1425
|
for w in self.widgets:
|
|
1426
1426
|
w:gPaIRS_Tab
|
|
1427
|
-
w.gen_TABpar(ITE.ind,FlagEmptyPrev=True,FlagInsert=
|
|
1427
|
+
w.gen_TABpar(ITE.ind,FlagEmptyPrev=True,FlagInsert=2,Process=ITE.Process,Step=ITE.Step)
|
|
1428
1428
|
pass
|
|
1429
1429
|
return
|
|
1430
1430
|
|
|
@@ -1434,11 +1434,11 @@ class ProcessTree(PaIRSTree):
|
|
|
1434
1434
|
if t in self.widgetNames:
|
|
1435
1435
|
k=self.widgetNames.index(t)
|
|
1436
1436
|
w:gPaIRS_Tab=self.widgets[k]
|
|
1437
|
-
w.gen_TABpar(ITE.ind,FlagInsert=
|
|
1437
|
+
w.gen_TABpar(ITE.ind,FlagInsert=3,Process=ITE.Process,Step=ITE.Step)
|
|
1438
1438
|
pass
|
|
1439
1439
|
for w, wn in zip(self.widgets,self.widgetNames):
|
|
1440
1440
|
if wn not in ITE.tabs+['TabArea']:
|
|
1441
|
-
w.gen_TABpar(ITE.ind,FlagNone=True,FlagInsert=
|
|
1441
|
+
w.gen_TABpar(ITE.ind,FlagNone=True,FlagInsert=3,Process=ITE.Process,Step=ITE.Step)
|
|
1442
1442
|
pass
|
|
1443
1443
|
return
|
|
1444
1444
|
|
|
@@ -1452,8 +1452,8 @@ class ProcessTree(PaIRSTree):
|
|
|
1452
1452
|
ITE.FlagQueue=self.TREpar.FlagQueue
|
|
1453
1453
|
ITE.ind=[self.TREpar.project,int(self.FlagBin),ind,0,0]
|
|
1454
1454
|
|
|
1455
|
+
processNames=[item[0].name for item in self.itemList[0]]+[item[0].name for item in self.restoreTree.itemList[0]]
|
|
1455
1456
|
if name is None:
|
|
1456
|
-
processNames=[item[0].name for item in self.itemList[0]]+[item[0].name for item in self.restoreTree.itemList[0]]
|
|
1457
1457
|
nameInd=1
|
|
1458
1458
|
ITE.name=f'{ITE.basename} {nameInd}'
|
|
1459
1459
|
while ITE.name in processNames:
|
|
@@ -1461,6 +1461,10 @@ class ProcessTree(PaIRSTree):
|
|
|
1461
1461
|
ITE.name=f'{ITE.basename} {nameInd}'
|
|
1462
1462
|
else:
|
|
1463
1463
|
ITE.name=name
|
|
1464
|
+
nameInd=1
|
|
1465
|
+
while ITE.name in processNames:
|
|
1466
|
+
nameInd+=1
|
|
1467
|
+
ITE.name=f'{name} ({nameInd})'
|
|
1464
1468
|
self.createParPrevs(ITE)
|
|
1465
1469
|
self.itemList[0].insert(ind,[ITE])
|
|
1466
1470
|
else:
|
|
@@ -1697,10 +1701,14 @@ class ProcessTree(PaIRSTree):
|
|
|
1697
1701
|
if errorString:
|
|
1698
1702
|
if '_data' in filename:
|
|
1699
1703
|
try:
|
|
1704
|
+
errorString=''
|
|
1700
1705
|
basename,ext=os.path.splitext(filename)
|
|
1701
1706
|
filename2=basename[:-5]+ext
|
|
1702
1707
|
data, errorMessage=loadList(filename2)
|
|
1703
|
-
errorString=errorString+errorMessage if errorMessage else ''
|
|
1708
|
+
#errorString=errorString+errorMessage if errorMessage else ''
|
|
1709
|
+
if errorMessage:
|
|
1710
|
+
WarningMessage="It was not possible to determine which process the selected data file belongs to.\nPlease, try again by loading the process output file directly."
|
|
1711
|
+
warningDialog(self,WarningMessage)
|
|
1704
1712
|
except Exception as inst2:
|
|
1705
1713
|
errorString+=str(inst2)
|
|
1706
1714
|
FlagError=True
|
|
@@ -1855,11 +1863,20 @@ class ProcessTree(PaIRSTree):
|
|
|
1855
1863
|
if not self.FlagCutted and not FlagPasteDeleted:
|
|
1856
1864
|
self.cutItems(new_items)
|
|
1857
1865
|
self.pasteLists(row,FlagPasteDeleted)
|
|
1858
|
-
for
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
self.
|
|
1862
|
-
|
|
1866
|
+
name_list=[i[0].name for i in self.itemList[0]]
|
|
1867
|
+
for pos,k in enumerate(indexes):
|
|
1868
|
+
item=self.topLevelItem(k)
|
|
1869
|
+
ITE=self.itemList[0][k][0]
|
|
1870
|
+
tail=set(indexes[pos:]);
|
|
1871
|
+
forbidden={name_list[j] for j in range(len(name_list)) if j not in tail}
|
|
1872
|
+
if ITE.name in forbidden:
|
|
1873
|
+
base=ITE.name
|
|
1874
|
+
n=1
|
|
1875
|
+
while f"{base} ({n})" in forbidden: n+=1
|
|
1876
|
+
ITE.name=f"{base} ({n})"
|
|
1877
|
+
name_list[k]=ITE.name
|
|
1878
|
+
self.setProcessItemWidget(item,ITE)
|
|
1879
|
+
self.setStepItemWidgets(item,ITE.ind[2])
|
|
1863
1880
|
|
|
1864
1881
|
self.scrollToItem(firstItemToScroll)
|
|
1865
1882
|
self.scrollToItem(lastItemToScroll)
|
|
@@ -1904,12 +1921,12 @@ class ProcessTree(PaIRSTree):
|
|
|
1904
1921
|
pixmap_list.append(icons_path+ITE.icon)
|
|
1905
1922
|
name_list.append(ITE.name)
|
|
1906
1923
|
flag_list.append(ITE.Step!=StepTypes.cal)
|
|
1907
|
-
func=lambda i, opts: self.process_loop(paths,processType,ind,ITEs[0].name,i,opts)
|
|
1924
|
+
func=lambda i, opts, cleanup_flag, rescan_flag: self.process_loop(paths,processType,ind,ITEs[0].name,i,opts,cleanup_flag,rescan_flag)
|
|
1908
1925
|
dialog = FolderLoopDialog(pixmap_list, name_list, flag_list, parent=self, paths=paths, func=func, process_name=ITEs[0].name)
|
|
1909
1926
|
dialog.exec()
|
|
1910
1927
|
return
|
|
1911
1928
|
|
|
1912
|
-
def process_loop(self,paths,processType,ind,name0,i,opts):
|
|
1929
|
+
def process_loop(self,paths,processType,ind,name0,i,opts,cleanup_flag,rescan_flag):
|
|
1913
1930
|
nProcess=self.topLevelItemCount()
|
|
1914
1931
|
path=paths[i]
|
|
1915
1932
|
name=name0+f" (.../{os.path.basename(path)}/)"
|
|
@@ -1920,6 +1937,7 @@ class ProcessTree(PaIRSTree):
|
|
|
1920
1937
|
ind_slave[2]=nProcess
|
|
1921
1938
|
ind_slave[-1]=0
|
|
1922
1939
|
|
|
1940
|
+
FlagWarning=False
|
|
1923
1941
|
for j in range(len(opts)):
|
|
1924
1942
|
ind_master[3]=j
|
|
1925
1943
|
ind_slave[3]=j
|
|
@@ -1931,13 +1949,23 @@ class ProcessTree(PaIRSTree):
|
|
|
1931
1949
|
if opts[j]==2:
|
|
1932
1950
|
INP: INPpar=self.gui.w_Input.TABpar_at(ind_new)
|
|
1933
1951
|
INP.path=myStandardPath(path)
|
|
1934
|
-
|
|
1952
|
+
if rescan_flag:
|
|
1953
|
+
flagWarning=self.rescanInputPath(ind_master,ind_new,cleanup_flag)
|
|
1954
|
+
else:
|
|
1955
|
+
flagWarning=self.gui.w_Input.scanImList(ind_new)
|
|
1956
|
+
if cleanup_flag: self.gui.w_Input.purgeImList(ind_new)
|
|
1957
|
+
INP.nimg=0
|
|
1958
|
+
if len(INP.imList[0]):
|
|
1959
|
+
if len(INP.imList[0][0]):
|
|
1960
|
+
INP.nimg=len(INP.imList[0][0])
|
|
1935
1961
|
self.gui.w_Input.checkINPpar(ind_new)
|
|
1936
1962
|
self.gui.w_Input.setINPwarn(ind_new)
|
|
1937
|
-
self.Explorer.setITElayout(ITE)
|
|
1938
1963
|
|
|
1939
1964
|
TABname=self.gui.w_Input.TABname
|
|
1940
1965
|
self.gui.bridge(TABname,ind_new)
|
|
1966
|
+
|
|
1967
|
+
FlagSettingPar=TABpar.FlagSettingPar
|
|
1968
|
+
TABpar.FlagSettingPar=True
|
|
1941
1969
|
for w in self.gui.tabWidgets:
|
|
1942
1970
|
w:gPaIRS_Tab
|
|
1943
1971
|
if w!=self.gui.w_Input:
|
|
@@ -1949,12 +1977,55 @@ class ProcessTree(PaIRSTree):
|
|
|
1949
1977
|
w.adjustTABparInd()
|
|
1950
1978
|
w.TABpar.copyfrom(currpar)
|
|
1951
1979
|
self.gui.bridge(w.TABname,ind_new)
|
|
1980
|
+
TABpar.FlagSettingPar=FlagSettingPar
|
|
1981
|
+
self.Explorer.setITElayout(ITE)
|
|
1982
|
+
FlagWarning=FlagWarning or flagWarning or ITE.OptionDone==0
|
|
1983
|
+
|
|
1952
1984
|
#item_child=item.child(j)
|
|
1953
1985
|
#item_child.setSelected(True)
|
|
1954
1986
|
#self.setCurrentItem(item_child)
|
|
1955
1987
|
else:
|
|
1956
1988
|
self.gui.link_pars(ind_slave,ind_master,FlagSet=False)
|
|
1957
|
-
return
|
|
1989
|
+
return FlagWarning
|
|
1990
|
+
|
|
1991
|
+
def rescanInputPath(self, ind_master, ind_new, FlagNoWarning):
|
|
1992
|
+
"""Rescan input on slave using master patterns, then rebuild imList/imEx."""
|
|
1993
|
+
INP: INPpar = self.gui.w_Input.TABpar_at(ind_new) # slave
|
|
1994
|
+
INPm: INPpar = self.gui.w_Input.TABpar_at(ind_master) # master
|
|
1995
|
+
|
|
1996
|
+
# Build A (for frame_1) and B (for frame_2) from master's pattern at master's frames
|
|
1997
|
+
patm = getattr(INPm.imSet, "pattern", [])
|
|
1998
|
+
ncam = INP.inp_ncam
|
|
1999
|
+
def _pat_list(frames,ind0=0):
|
|
2000
|
+
out=[];
|
|
2001
|
+
for k in range(ncam):
|
|
2002
|
+
f = frames[k]-ind0 if k < len(frames) else -1
|
|
2003
|
+
out.append(patm[f] if isinstance(f,int) and 0 <= f < len(patm) else None)
|
|
2004
|
+
return out
|
|
2005
|
+
A = _pat_list(INPm.frame_1)
|
|
2006
|
+
B = _pat_list(INPm.frame_2,1)
|
|
2007
|
+
|
|
2008
|
+
# Rescan slave input path with patterns=A,B (maps patterns -> frame indices on slave)
|
|
2009
|
+
self.gui.w_Input.scanInputPath(ind_new, patterns=[A, B], FlagNoWarning=FlagNoWarning)
|
|
2010
|
+
|
|
2011
|
+
# Rebuild imList/imEx from frames computed on slave
|
|
2012
|
+
INP.imList, INP.imEx = INP.imSet.genListsFromFrame(
|
|
2013
|
+
INP.frame_1, INP.frame_2, INP.ind_in, INP.npairs, INP.step, INP.FlagTR_Import
|
|
2014
|
+
)
|
|
2015
|
+
# Compare slave vs master lists
|
|
2016
|
+
def _lists_differ(a, b):
|
|
2017
|
+
if len(a) != len(b): return True
|
|
2018
|
+
for x, y in zip(a, b):
|
|
2019
|
+
if x != y: return True
|
|
2020
|
+
return False
|
|
2021
|
+
|
|
2022
|
+
FlagWarning = (
|
|
2023
|
+
_lists_differ(INP.imList, INPm.imList) or
|
|
2024
|
+
_lists_differ(INP.imEx, INPm.imEx)
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
return FlagWarning
|
|
2028
|
+
|
|
1958
2029
|
|
|
1959
2030
|
def button_delete_action(self):
|
|
1960
2031
|
self.blockSignals(True)
|
|
@@ -2940,9 +3011,16 @@ class StartingPage(QFrame):
|
|
|
2940
3011
|
for n, process in processes.items():
|
|
2941
3012
|
# Layout orizzontale per ogni processo
|
|
2942
3013
|
widget=QWidget()
|
|
3014
|
+
widget.setObjectName("process_item")
|
|
3015
|
+
widget.setStyleSheet("""
|
|
3016
|
+
QWidget#process_item:hover {
|
|
3017
|
+
background-color: rgba(64, 64, 255, 33);
|
|
3018
|
+
border-radius: 10px;
|
|
3019
|
+
}
|
|
3020
|
+
""")
|
|
2943
3021
|
process_layout = QHBoxLayout(widget)
|
|
2944
3022
|
process_layout.setSpacing(self.LAYOUT_SPACING)
|
|
2945
|
-
process_layout.setContentsMargins(
|
|
3023
|
+
process_layout.setContentsMargins(10, 10, 10, 0)
|
|
2946
3024
|
|
|
2947
3025
|
# Pulsante con icona
|
|
2948
3026
|
button_layout = QHBoxLayout()
|
|
@@ -2990,13 +3068,15 @@ class StartingPage(QFrame):
|
|
|
2990
3068
|
caption_text_edit.setFont(caption_font)
|
|
2991
3069
|
caption_text_edit.setAlignment(Qt.AlignmentFlag.AlignJustify)
|
|
2992
3070
|
caption_text_edit.setReadOnly(True)
|
|
3071
|
+
caption_text_edit.setTextInteractionFlags(Qt.NoTextInteraction)
|
|
3072
|
+
caption_text_edit.viewport().setCursor(Qt.PointingHandCursor)
|
|
2993
3073
|
caption_text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
2994
3074
|
caption_text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
2995
3075
|
caption_text_edit.setFrameStyle(QFrame.NoFrame)
|
|
2996
3076
|
#caption_text_edit.setFixedWidth(self.CAPTION_WIDTH)
|
|
2997
3077
|
caption_text_edit.setStyleSheet("background: transparent;")
|
|
2998
3078
|
caption_text_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
2999
|
-
|
|
3079
|
+
|
|
3000
3080
|
# Adjust height to content
|
|
3001
3081
|
caption_text_edit.document().setTextWidth(caption_text_edit.viewport().width())
|
|
3002
3082
|
caption_text_edit.setFixedHeight(CAPTION_HEIGHT)#caption_text_edit.document().size().height())
|
|
@@ -3012,6 +3092,25 @@ class StartingPage(QFrame):
|
|
|
3012
3092
|
# Aggiungi il layout orizzontale al layout principale
|
|
3013
3093
|
self.main_layout.addWidget(widget)
|
|
3014
3094
|
|
|
3095
|
+
# --- Make the whole row clickable (icon + labels + background) ---
|
|
3096
|
+
if buttonBar:
|
|
3097
|
+
|
|
3098
|
+
def make_clickable(w, callback):
|
|
3099
|
+
w.setCursor(Qt.PointingHandCursor)
|
|
3100
|
+
|
|
3101
|
+
def mouseReleaseEvent(event, cb=callback, ww=w):
|
|
3102
|
+
if event.button() == Qt.LeftButton:
|
|
3103
|
+
cb()
|
|
3104
|
+
# call base implementation to keep default behaviour
|
|
3105
|
+
QWidget.mouseReleaseEvent(ww, event)
|
|
3106
|
+
|
|
3107
|
+
w.mouseReleaseEvent = mouseReleaseEvent
|
|
3108
|
+
|
|
3109
|
+
# entire row + title + caption all trigger the same action
|
|
3110
|
+
make_clickable(widget, action(n))
|
|
3111
|
+
#make_clickable(name_label, click_callback)
|
|
3112
|
+
#make_clickable(caption_text_edit, click_callback)
|
|
3113
|
+
|
|
3015
3114
|
self.items[n]=widget
|
|
3016
3115
|
|
|
3017
3116
|
self.main_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
|
PaIRS_UniNa/FolderLoop.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from PySide6.QtWidgets import (
|
|
3
|
-
QApplication, QDialog, QVBoxLayout, QLabel, QHBoxLayout, QWidget, QSpacerItem,
|
|
3
|
+
QApplication, QDialog, QVBoxLayout, QLabel, QHBoxLayout, QWidget, QSpacerItem, QTreeWidget, QTreeWidgetItem,
|
|
4
4
|
QPushButton, QProgressBar, QSizePolicy,
|
|
5
5
|
QFileDialog, QListView, QAbstractItemView, QTreeView, QFileSystemModel
|
|
6
6
|
)
|
|
@@ -83,14 +83,18 @@ class FolderLoopDialog(QDialog):
|
|
|
83
83
|
self.tooltips = {
|
|
84
84
|
"copy": "Copy the item",
|
|
85
85
|
"link": "Link the item",
|
|
86
|
-
"change_folder": "Change the folder"
|
|
86
|
+
"change_folder": "Change the folder",
|
|
87
|
+
"auto_purge": "Auto-remove missing-image pairs",
|
|
88
|
+
"rescan": "Re-scan destination paths"
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
# Icons for the buttons
|
|
90
92
|
self.icons = {
|
|
91
93
|
"copy": [QIcon(icons_path + "copy_process.png"), QIcon(icons_path + "copy_process_off.png")],
|
|
92
94
|
"link": [QIcon(icons_path + "link.png"), QIcon(icons_path + "unlink.png")],
|
|
93
|
-
"change_folder": [QIcon(icons_path + "change_folder.png"), QIcon(icons_path + "change_folder_off.png")]
|
|
95
|
+
"change_folder": [QIcon(icons_path + "change_folder.png"), QIcon(icons_path + "change_folder_off.png")],
|
|
96
|
+
"auto_purge": [QIcon(icons_path + "folder_loop_cleanup.png"), QIcon(icons_path + "folder_loop_cleanup_off.png")],
|
|
97
|
+
"rescan": [QIcon(icons_path + "scan_path_loop.png"), QIcon(icons_path + "scan_path_loop_off.png")],
|
|
94
98
|
}
|
|
95
99
|
self.options_list=list(self.icons)
|
|
96
100
|
|
|
@@ -109,7 +113,8 @@ class FolderLoopDialog(QDialog):
|
|
|
109
113
|
self.header_icon.setFixedSize(self.icon_size)
|
|
110
114
|
header_layout.addWidget(self.header_icon)
|
|
111
115
|
|
|
112
|
-
|
|
116
|
+
header_text=process_name[:48]+f"{'...' if len(process_name)>50 else ''}"
|
|
117
|
+
self.header_label = QLabel(header_text)
|
|
113
118
|
self.header_label.setFont(self.title_font)
|
|
114
119
|
self.header_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
|
|
115
120
|
self.header_label.setFixedHeight(self.title_height)
|
|
@@ -191,6 +196,82 @@ class FolderLoopDialog(QDialog):
|
|
|
191
196
|
|
|
192
197
|
layout.addWidget(widget)
|
|
193
198
|
|
|
199
|
+
# --- Cleanup row ---
|
|
200
|
+
cleanup_spacing = 5
|
|
201
|
+
self.cleanup_widget = QWidget()
|
|
202
|
+
cw_layout = QHBoxLayout(self.cleanup_widget)
|
|
203
|
+
cw_layout.setContentsMargins(0, cleanup_spacing, 0, cleanup_spacing)
|
|
204
|
+
cw_layout.setSpacing(5)
|
|
205
|
+
|
|
206
|
+
self.cleanup_button = QPushButton(self)
|
|
207
|
+
self.cleanup_button.setCheckable(True)
|
|
208
|
+
self.cleanup_button.setChecked(False)
|
|
209
|
+
self.cleanup_button.setIcon(self.icons["auto_purge"][1])
|
|
210
|
+
self.cleanup_button.setFixedSize(self.button_size)
|
|
211
|
+
self.cleanup_button.setStyleSheet("QPushButton{border: none;}")
|
|
212
|
+
self.cleanup_button.setCursor(QCursor(Qt.PointingHandCursor))
|
|
213
|
+
self.cleanup_button.setToolTip(self.tooltips["auto_purge"])
|
|
214
|
+
self.cleanup_button.setIconSize(self.icon_size_off)
|
|
215
|
+
|
|
216
|
+
self.cleanup_label = QLabel("", self)
|
|
217
|
+
self.cleanup_label_font = parent.font()
|
|
218
|
+
self.cleanup_label_font.setPixelSize(fontPixelSize)
|
|
219
|
+
self.cleanup_label_font.setItalic(True)
|
|
220
|
+
self.cleanup_label.setFont(self.cleanup_label_font)
|
|
221
|
+
self.cleanup_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
222
|
+
|
|
223
|
+
self.cleanup_button.toggled.connect(self.on_cleanup_toggled)
|
|
224
|
+
cw_layout.addItem(QSpacerItem(10, 0, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
225
|
+
cw_layout.addWidget(self.cleanup_label)
|
|
226
|
+
cw_layout.addWidget(self.cleanup_button)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# --- Rescan row ---
|
|
230
|
+
rescan_spacing = 5
|
|
231
|
+
self.rescan_widget = QWidget()
|
|
232
|
+
rw_layout = QHBoxLayout(self.rescan_widget)
|
|
233
|
+
rw_layout.setContentsMargins(0, rescan_spacing, 0, rescan_spacing)
|
|
234
|
+
rw_layout.setSpacing(5)
|
|
235
|
+
|
|
236
|
+
self.rescan_button = QPushButton(self)
|
|
237
|
+
self.rescan_button.setCheckable(True)
|
|
238
|
+
self.rescan_button.setChecked(False)
|
|
239
|
+
self.rescan_button.setIcon(self.icons["rescan"][1])
|
|
240
|
+
self.rescan_button.setFixedSize(self.button_size)
|
|
241
|
+
self.rescan_button.setStyleSheet("QPushButton{border: none;}")
|
|
242
|
+
self.rescan_button.setCursor(QCursor(Qt.PointingHandCursor))
|
|
243
|
+
self.rescan_button.setToolTip(self.tooltips["rescan"])
|
|
244
|
+
self.rescan_button.setIconSize(self.icon_size_off)
|
|
245
|
+
|
|
246
|
+
self.rescan_label = QLabel("", self)
|
|
247
|
+
self.rescan_label_font = parent.font()
|
|
248
|
+
self.rescan_label_font.setPixelSize(fontPixelSize)
|
|
249
|
+
self.rescan_label_font.setItalic(True)
|
|
250
|
+
self.rescan_label.setFont(self.rescan_label_font)
|
|
251
|
+
self.rescan_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
252
|
+
|
|
253
|
+
self.rescan_button.toggled.connect(self.on_rescan_toggled)
|
|
254
|
+
rw_layout.addItem(QSpacerItem(10, 0, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
255
|
+
rw_layout.addWidget(self.rescan_label)
|
|
256
|
+
rw_layout.addWidget(self.rescan_button)
|
|
257
|
+
|
|
258
|
+
# --- Container for both rows ---
|
|
259
|
+
self.extra_opts_widget = QWidget()
|
|
260
|
+
eo_layout = QVBoxLayout(self.extra_opts_widget)
|
|
261
|
+
eo_layout.setContentsMargins(0, 5, 0, 50) # top=10, bottom=10
|
|
262
|
+
eo_layout.setSpacing(5)
|
|
263
|
+
eo_layout.addWidget(self.rescan_widget)
|
|
264
|
+
eo_layout.addWidget(self.cleanup_widget)
|
|
265
|
+
self.extra_opts_widget.setFixedHeight(self.button_size.height()*2+60)
|
|
266
|
+
|
|
267
|
+
self.extra_opts_widget.setVisible(False)
|
|
268
|
+
self.widgets.append(self.extra_opts_widget)
|
|
269
|
+
|
|
270
|
+
# Add container to parent layout
|
|
271
|
+
layout.addWidget(self.extra_opts_widget)
|
|
272
|
+
|
|
273
|
+
self.warnings=[False]*len(self.paths)
|
|
274
|
+
|
|
194
275
|
# Progress bar and final buttons (Cancel, Proceed)
|
|
195
276
|
progress_widget = QWidget()
|
|
196
277
|
progress_layout = QHBoxLayout()
|
|
@@ -228,8 +309,13 @@ class FolderLoopDialog(QDialog):
|
|
|
228
309
|
self.setLayout(layout)
|
|
229
310
|
|
|
230
311
|
# Set window maximum height and margins
|
|
312
|
+
self.on_cleanup_toggled(self.cleanup_button.isChecked())
|
|
313
|
+
self.on_rescan_toggled(self.rescan_button.isChecked())
|
|
314
|
+
self.update_extraopts_visibility(FlagAnimation=False)
|
|
231
315
|
self.setFixedHeight(self.max_height)
|
|
232
316
|
self.setContentsMargins(self.margin_size*2, self.margin_size, self.margin_size*2, self.margin_size)
|
|
317
|
+
self.setMinimumWidth(640)
|
|
318
|
+
self.setMaximumWidth(800)
|
|
233
319
|
|
|
234
320
|
# Timer for progress
|
|
235
321
|
self.iteration = 0
|
|
@@ -271,6 +357,51 @@ class FolderLoopDialog(QDialog):
|
|
|
271
357
|
button.setIconSize(self.icon_size)
|
|
272
358
|
button.setIcon(self.icons[self.options_list[k]][0]) # Checked state icon
|
|
273
359
|
self.step_options[index]=k
|
|
360
|
+
self.update_extraopts_visibility()
|
|
361
|
+
|
|
362
|
+
def update_extraopts_visibility(self,FlagAnimation=True):
|
|
363
|
+
"""Show extra row if any row has 'change_folder' checked; hide otherwise."""
|
|
364
|
+
try:
|
|
365
|
+
idx_change = self.options_list.index("change_folder")
|
|
366
|
+
except ValueError:
|
|
367
|
+
idx_change = -1
|
|
368
|
+
|
|
369
|
+
show = (idx_change >= 0) and any(
|
|
370
|
+
(opt == idx_change) for opt in self.step_options if isinstance(opt, int)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if self.extra_opts_widget.isVisible() != show:
|
|
374
|
+
self.extra_opts_widget.setVisible(show)
|
|
375
|
+
extra_opts_widget = self.extra_opts_widget.height()
|
|
376
|
+
maxHeight = self.nstep * (self.button_size.height() + self.row_spacing) + int(show)*extra_opts_widget + self.min_height
|
|
377
|
+
if FlagAnimation:
|
|
378
|
+
self.window_resize(maxHeight)
|
|
379
|
+
self.max_height = maxHeight
|
|
380
|
+
|
|
381
|
+
def on_cleanup_toggled(self, checked: bool):
|
|
382
|
+
"""Sync icon/size and label text when the extra toggle is changed."""
|
|
383
|
+
self.cleanup_button.setIcon(self.icons["auto_purge"][0 if checked else 1])
|
|
384
|
+
self.cleanup_button.setIconSize(self.icon_size if checked else self.icon_size_off)
|
|
385
|
+
self.cleanup_label.setText(
|
|
386
|
+
"Image-missing pairs will be removed automatically."
|
|
387
|
+
if checked else
|
|
388
|
+
"Image-missing pairs are kept; batch copy may raise warnings."
|
|
389
|
+
)
|
|
390
|
+
self.cleanup_label.setStyleSheet("color: none;" if checked else "color: rgb(125,125,125);")
|
|
391
|
+
self.cleanup_enabled = bool(checked)
|
|
392
|
+
|
|
393
|
+
def on_rescan_toggled(self, checked: bool):
|
|
394
|
+
"""Sync icon/size and label text when the rescan toggle is changed."""
|
|
395
|
+
self.rescan_button.setIcon(self.icons["rescan"][0 if checked else 1])
|
|
396
|
+
self.rescan_button.setIconSize(self.icon_size if checked else self.icon_size_off)
|
|
397
|
+
|
|
398
|
+
self.rescan_label.setText(
|
|
399
|
+
"The input folder will be re-scanned; input image list may differ or trigger warnings."
|
|
400
|
+
if checked else
|
|
401
|
+
"No folder scan; images identified in the master process will be used."
|
|
402
|
+
)
|
|
403
|
+
self.rescan_label.setStyleSheet("color: none;" if checked else "color: rgb(125,125,125);")
|
|
404
|
+
self.rescan_enabled = bool(checked)
|
|
274
405
|
|
|
275
406
|
def on_proceed(self):
|
|
276
407
|
"""Start or stop the progress loop depending on the button state."""
|
|
@@ -300,14 +431,17 @@ class FolderLoopDialog(QDialog):
|
|
|
300
431
|
"""Update the progress bar on each timer tick."""
|
|
301
432
|
if self.iteration < self.progress_bar.maximum():
|
|
302
433
|
timesleep(time_sleep_loop)
|
|
303
|
-
self.
|
|
304
|
-
self.
|
|
434
|
+
title_text=self.paths[self.iteration][:48]+f"{'...' if len(self.paths[self.iteration])>50 else ''}"
|
|
435
|
+
self.title_label.setText(title_text)
|
|
436
|
+
self.warnings[self.iteration]=self.func(self.iteration,self.step_options,self.cleanup_enabled,self.rescan_enabled)
|
|
305
437
|
self.progress_bar.setValue(self.iteration)
|
|
306
438
|
self.iteration += 1
|
|
307
439
|
if self.loop_running: self.timer.start(0)
|
|
308
440
|
else:
|
|
309
441
|
self.progress_bar.setValue(self.progress_bar.maximum())
|
|
310
442
|
self.stop_progress()
|
|
443
|
+
self.hide()
|
|
444
|
+
self.show_batch_issues_dialog()
|
|
311
445
|
if not self.animation: self.done(0) # Closes the dialog when complete
|
|
312
446
|
|
|
313
447
|
def cancel(self):
|
|
@@ -332,6 +466,62 @@ class FolderLoopDialog(QDialog):
|
|
|
332
466
|
|
|
333
467
|
def window_resize(self, h):
|
|
334
468
|
self.setFixedHeight(h)
|
|
469
|
+
|
|
470
|
+
def show_batch_issues_dialog(self):
|
|
471
|
+
"""Show a summary dialog listing all destination folders with warnings=True."""
|
|
472
|
+
issues=[(i,p) for i,p in enumerate(self.paths) if i<len(self.warnings) and self.warnings[i]]
|
|
473
|
+
if not issues: return
|
|
474
|
+
|
|
475
|
+
dlg=QDialog(self); dlg.setWindowTitle("Batch copy issues")
|
|
476
|
+
font=dlg.font()
|
|
477
|
+
font.setPixelSize(fontPixelSize)
|
|
478
|
+
dlg.setFont(font)
|
|
479
|
+
lay=QVBoxLayout(dlg)
|
|
480
|
+
|
|
481
|
+
# --- Warning header with icon ---
|
|
482
|
+
icon_width=96
|
|
483
|
+
warn_layout = QHBoxLayout()
|
|
484
|
+
warn_icon = QLabel()
|
|
485
|
+
warn_pix = QPixmap(icons_path + "warning.png").scaled(
|
|
486
|
+
icon_width, icon_width, Qt.KeepAspectRatio, Qt.SmoothTransformation
|
|
487
|
+
)
|
|
488
|
+
warn_icon.setPixmap(warn_pix)
|
|
489
|
+
warn_icon.setScaledContents(False)
|
|
490
|
+
#warn_icon.setAlignment(Qt.AlignTop)
|
|
491
|
+
warn_icon.setFixedWidth(icon_width)
|
|
492
|
+
|
|
493
|
+
head = QLabel(
|
|
494
|
+
f"{len(issues)} destination folder{'s' if len(issues)>1 else ''} could require attention: "
|
|
495
|
+
"potential warnings during batch copy or image-set mismatch!\n\n"
|
|
496
|
+
"Please note that the interface may not explicitly flag these cases. "
|
|
497
|
+
"Copy the paths below if you wish to inspect the corresponding processes.\n",
|
|
498
|
+
dlg
|
|
499
|
+
)
|
|
500
|
+
head.setWordWrap(True)
|
|
501
|
+
|
|
502
|
+
warn_layout.addWidget(warn_icon)
|
|
503
|
+
warn_layout.addWidget(head)
|
|
504
|
+
lay.addLayout(warn_layout)
|
|
505
|
+
|
|
506
|
+
tree=QTreeWidget(dlg); tree.setHeaderHidden(True)
|
|
507
|
+
tree.header().setStretchLastSection(True)
|
|
508
|
+
for _,p in issues:
|
|
509
|
+
item=QTreeWidgetItem([p])
|
|
510
|
+
item.setToolTip(0,p)
|
|
511
|
+
tree.addTopLevelItem(item)
|
|
512
|
+
lay.addWidget(tree)
|
|
513
|
+
|
|
514
|
+
# actions
|
|
515
|
+
btns=QHBoxLayout()
|
|
516
|
+
def _copy():
|
|
517
|
+
txt="\n".join(p for _,p in issues)
|
|
518
|
+
QApplication.clipboard().setText(txt)
|
|
519
|
+
copy_btn=QPushButton("Copy paths"); copy_btn.clicked.connect(_copy)
|
|
520
|
+
close_btn=QPushButton("Close"); close_btn.clicked.connect(dlg.accept)
|
|
521
|
+
btns.addWidget(copy_btn); btns.addStretch(1); btns.addWidget(close_btn)
|
|
522
|
+
lay.addLayout(btns)
|
|
523
|
+
|
|
524
|
+
dlg.resize(720, 360); dlg.exec()
|
|
335
525
|
|
|
336
526
|
if __name__ == "__main__":
|
|
337
527
|
import random
|