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.
Files changed (42) hide show
  1. PaIRS_UniNa/Calibration_Tab.py +15 -0
  2. PaIRS_UniNa/Changes.txt +54 -2
  3. PaIRS_UniNa/Explorer.py +118 -19
  4. PaIRS_UniNa/FolderLoop.py +196 -6
  5. PaIRS_UniNa/Input_Tab.py +167 -55
  6. PaIRS_UniNa/Input_Tab_CalVi.py +15 -17
  7. PaIRS_UniNa/Input_Tab_tools.py +9 -10
  8. PaIRS_UniNa/Output_Tab.py +2 -4
  9. PaIRS_UniNa/PaIRS_pypacks.py +227 -56
  10. PaIRS_UniNa/Process_Tab.py +2 -2
  11. PaIRS_UniNa/Process_Tab_Disp.py +1 -1
  12. PaIRS_UniNa/SPIVCalHelp.py +155 -0
  13. PaIRS_UniNa/Saving_tools.py +7 -7
  14. PaIRS_UniNa/TabTools.py +7 -4
  15. PaIRS_UniNa/Vis_Tab.py +129 -60
  16. PaIRS_UniNa/Whatsnew.py +15 -3
  17. PaIRS_UniNa/_PaIRS_PIV.pyd +0 -0
  18. PaIRS_UniNa/__init__.py +4 -4
  19. PaIRS_UniNa/addwidgets_ps.py +28 -20
  20. PaIRS_UniNa/calibView.py +7 -0
  21. PaIRS_UniNa/gPaIRS.py +179 -34
  22. PaIRS_UniNa/icons/flaticon_PaIRS_download_warning.png +0 -0
  23. PaIRS_UniNa/icons/folder_loop_cleanup.png +0 -0
  24. PaIRS_UniNa/icons/folder_loop_cleanup_off.png +0 -0
  25. PaIRS_UniNa/icons/information.png +0 -0
  26. PaIRS_UniNa/icons/information2.png +0 -0
  27. PaIRS_UniNa/icons/pencil_bw.png +0 -0
  28. PaIRS_UniNa/icons/scan_path_loop.png +0 -0
  29. PaIRS_UniNa/icons/scan_path_loop_off.png +0 -0
  30. PaIRS_UniNa/icons/spiv_setup_no.png +0 -0
  31. PaIRS_UniNa/icons/spiv_setup_ok.png +0 -0
  32. PaIRS_UniNa/pivParFor.py +1 -1
  33. PaIRS_UniNa/procTools.py +51 -3
  34. PaIRS_UniNa/rqrdpckgs.txt +6 -5
  35. PaIRS_UniNa/stereoPivParFor.py +1 -1
  36. PaIRS_UniNa/ui_Calibration_Tab.py +90 -57
  37. PaIRS_UniNa/ui_gPairs.py +9 -3
  38. PaIRS_UniNa/whatsnew.txt +4 -4
  39. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/METADATA +32 -17
  40. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/RECORD +42 -32
  41. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/WHEEL +0 -0
  42. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/top_level.txt +0 -0
@@ -15,10 +15,12 @@ basefold_DEBUG='./'
15
15
  basefold_DEBUG_VIS=''
16
16
  #basefold='B:/dl/apairs/jetcross'
17
17
 
18
+
18
19
  developerIDs={
19
20
  'GP_Win_Office': '231128824800632', #'0x7824af430781',
20
21
  'GP_Win_Office_New': '140626882900161', #'0x7824af430781',
21
22
  'GP_Mac_Laptop': 'V94LRP93FV', #'0xa275dd445ab0',
23
+ 'GP_WSL' : 'b44ec1c0e5a74ffd97bb050c39ef6cb1',
22
24
  'TA_Win_Office': '160983906000941', #'0xccb0da8c896e'
23
25
  'TA_Win_Office_New': '231128824801036', #??
24
26
  }
@@ -29,9 +31,32 @@ def getCurrentID():
29
31
  serial_number=None
30
32
  try:
31
33
  if psutil.LINUX:
34
+ def get_linux_serial():
35
+ from pathlib import Path
36
+ candidates = [
37
+ Path("/sys/class/dmi/id/board_serial"),
38
+ Path("/sys/class/dmi/id/product_uuid"),
39
+ Path("/etc/machine-id"), Path("/var/lib/dbus/machine-id") #WSL
40
+ ]
41
+ for p in candidates:
42
+ try:
43
+ if p.is_file():
44
+ val = p.read_text(errors="ignore").strip()
45
+ if val and val.lower() not in {
46
+ "none", "unknown", "not specified", "to be filled by o.e.m."
47
+ }:
48
+ return val
49
+ except Exception as e:
50
+ if Flag_DEBUG:
51
+ print(f"Error while retrieving motherboard serial number: {e}")
52
+ continue
53
+ return None
54
+ serial_number = get_linux_serial()
55
+ """"
32
56
  # On Linux, the motherboard serial number can be obtained from the /sys/class/dmi/id/board_serial file
33
57
  with open('/sys/class/dmi/id/board_serial', 'r') as f:
34
58
  serial_number = f.read().strip()
59
+ """
35
60
  elif psutil.WINDOWS:
36
61
  # On Windows, the motherboard serial number can be obtained using WMI
37
62
  output = subprocess.check_output(["wmic", "baseboard", "get", "SerialNumber"]).decode('utf-8')
@@ -43,7 +68,8 @@ def getCurrentID():
43
68
  if b'Serial Number (system)' in line:
44
69
  serial_number = line.split(b':')[1].strip().decode('utf-8')
45
70
  except Exception as e:
46
- print(f"Error while retrieving motherboard serial number: {e}")
71
+ if Flag_DEBUG:
72
+ print(f"Error while retrieving motherboard serial number: {e}")
47
73
  return serial_number
48
74
 
49
75
  currentID=getCurrentID()
@@ -58,6 +84,16 @@ if currentID in (developerIDs['GP_Win_Office'],developerIDs['GP_Win_Office_New']
58
84
  'C:/desk/PIV_Img/_data/Calibration_data/cylinder/',
59
85
  ]
60
86
  basefold_DEBUG_VIS='C:/desk/PIV_Img/_data/PIV_data/real_case/'
87
+ elif currentID==developerIDs['GP_WSL']:
88
+ basefold_DEBUG='/mnt/c/desk/PIV_Img/_data/PIV_data/virtual_case/'
89
+ basefold_DEBUGOptions=[
90
+ '/mnt/c/desk/PIV_Img/img1/',
91
+ '/mnt/c/desk/PIV_Img/_data/PIV_data/virtual_case/',
92
+ '/mnt/c/desk/PIV_Img/_data/PIV_data/real_case/',
93
+ '/mnt/c/desk/PIV_Img/_data/Calibration_data/pinhole/',
94
+ '/mnt/c/desk/PIV_Img/_data/Calibration_data/cylinder/',
95
+ ]
96
+ basefold_DEBUG_VIS='/mnt/c/desk/PIV_Img/_data/PIV_data/real_case/'
61
97
  elif currentID==developerIDs['GP_Mac_Laptop']: #gerardo mac
62
98
  basefold_DEBUG='/Users/gerardo/Desktop/PIV_Img/swirler_png/' #'/Users/gerardo/Desktop/PIV_Img/img1/'
63
99
  basefold_DEBUGOptions=[
@@ -72,12 +108,12 @@ elif currentID==developerIDs['GP_Mac_Laptop']: #gerardo mac
72
108
  basefold_DEBUG_VIS='/Users/gerardo/Desktop/PaIRS_examples/PIV_data/real_case/'
73
109
  basefold_DEBUG_VIS='/Users/gerardo/Desktop/PIV_Img/img1/'
74
110
  elif currentID in (developerIDs['TA_Win_Office'],developerIDs['TA_Win_Office_New']): #TA windows
75
- basefold_DEBUG='C:\desk\Attuali\PythonLibC\PIV\img'
111
+ basefold_DEBUG=r'C:\desk\Attuali\PythonLibC\PIV\img'
76
112
  basefold_DEBUGOptions=[
77
113
  'C:/desk/PIV_Img/img1/',
78
114
  'C:/desk/PIV_Img/swirler_png/',
79
115
  '../../img/calib/',
80
- 'C:\desk\Attuali\PythonLibC\PIV\img',
116
+ r'C:\desk\Attuali\PythonLibC\PIV\img',
81
117
  ]
82
118
  basefold_DEBUG_VIS=''
83
119
  else:
@@ -106,6 +142,7 @@ f_empty_width=250 #blank space in scrollable area within the main window
106
142
  time_ScrollBar=250 #time of animation of scroll area
107
143
  time_callback2_async=0 #time to test async callbacks
108
144
  time_showSplashOnTop=250
145
+ pathCompleterLength=10
109
146
 
110
147
  fileChanges='Changes.txt'
111
148
  fileWhatsNew=['whatsnew.txt','whatwasnew.txt']
@@ -149,6 +186,37 @@ import sys
149
186
  import concurrent.futures
150
187
  import asyncio
151
188
 
189
+ _old_init_QAction = QAction.__init__
190
+ def _new_init_QAction(self, *args, **kwargs):
191
+ _old_init_QAction(self, *args, **kwargs)
192
+ try:
193
+ self.setIconVisibleInMenu(True)
194
+ except Exception:
195
+ pass
196
+ QAction.__init__ = _new_init_QAction
197
+ _old_init_QMenu = QMenu.__init__
198
+ def _new_init_QMenu(self, *args, **kwargs):
199
+ _old_init_QMenu(self, *args, **kwargs)
200
+ try:
201
+ self.menuAction().setIconVisibleInMenu(True)
202
+ except Exception:
203
+ pass
204
+ QMenu.__init__ = _new_init_QMenu
205
+
206
+ # --- Patch dei metodi QMenu che CREANO/AGGIUNGONO azioni (copre gli overload C++) ---
207
+ _old_addAction = QMenu.addAction
208
+ def _new_addAction(self, *args, **kwargs):
209
+ act:QAction = _old_addAction(self, *args, **kwargs) # può essere creato lato C++
210
+ try:
211
+ if isinstance(act, QAction):
212
+ act.setIconVisibleInMenu(True)
213
+ except Exception:
214
+ pass
215
+ return act
216
+ QMenu.addAction = _new_addAction
217
+
218
+ Flag_ISEXE=getattr(sys, 'frozen', False) #made by pyInstaller
219
+ EXEurl='https://www.pairs.unina.it/#download'
152
220
 
153
221
  class ColorPrint:
154
222
  def __init__(self,flagTime=False,prio=PrintTAPriority.medium,faceStd=PrintTA.faceStd,flagFullDebug=False):
@@ -184,6 +252,7 @@ class GPaIRSPrint:
184
252
  self.Info=ColorPrint(prio=PrintTAPriority.medium)
185
253
  self.Time=ColorPrint(prio=PrintTAPriority.medium if FlagPrintTime else PrintTAPriority.veryLow,flagTime=True,faceStd=PrintTA.faceUnderline)
186
254
  self.Error=ColorPrint(prio=PrintTAPriority.medium,faceStd=PrintTA.faceBold)
255
+ self.IOError=ColorPrint(prio=PrintTAPriority.veryLow,faceStd=PrintTA.faceBold)
187
256
  self.Process=ColorPrint(prio=PrintTAPriority.veryLow)
188
257
  self.Callback=ColorPrint(prio=PrintTAPriority.veryLow)
189
258
  self.TABparDiff=ColorPrint(prio=PrintTAPriority.veryLow)
@@ -237,7 +306,6 @@ if __package__ or "." in __name__:
237
306
  foldPaIRS = foldPaIRS.replace('\\', '/')
238
307
  else:
239
308
  foldPaIRS='./'
240
-
241
309
  class ProcessTypes:
242
310
  null=None
243
311
  min=0
@@ -247,7 +315,7 @@ class ProcessTypes:
247
315
  cal=10
248
316
 
249
317
  singleCamera=[piv]
250
- threeCameras=[tpiv]
318
+ threeCameras=[min,tpiv]
251
319
 
252
320
  class StepTypes:
253
321
  null=None
@@ -291,15 +359,24 @@ class outExt:
291
359
  cfg_calvi='.calvi_cfg'
292
360
  pla='.pairs_pla'
293
361
 
294
-
295
362
 
296
363
  lastcfgname='lastWorkSpace'+outExt.wksp
297
-
298
364
  fileChanges=foldPaIRS+'Changes.txt'
299
- fileWhatsNew=[foldPaIRS+f for f in fileWhatsNew]
300
365
  icons_path=foldPaIRS+icons_path
301
- lastcfgname=foldPaIRS+lastcfgname
302
- pro_path=foldPaIRS+"pro/"
366
+
367
+ if not Flag_ISEXE:
368
+ fileWhatsNew=[foldPaIRS+f for f in fileWhatsNew]
369
+
370
+ lastcfgname=foldPaIRS+lastcfgname
371
+ pro_path=foldPaIRS+"pro/"
372
+ else:
373
+ from pathlib import Path
374
+ exe_dir = str(Path(sys.argv[0]).resolve().parent)+'/'
375
+ fileWhatsNew=[foldPaIRS+fileWhatsNew[0],exe_dir+fileWhatsNew[1]]
376
+ lastcfgname = exe_dir + lastcfgname
377
+ pro_path = exe_dir + "pro/"
378
+
379
+
303
380
  if not os.path.exists(pro_path):
304
381
  try:
305
382
  os.mkdir(pro_path)
@@ -558,6 +635,13 @@ def myStandardRoot(root):
558
635
  currroot = re.sub('/+', '/', currroot) # Reduce consecutive slashes to a single slash
559
636
  return currroot
560
637
 
638
+ def relativizePath(currpath:str):
639
+ return currpath
640
+ directory_path = myStandardPath(os.getcwd())
641
+ if directory_path in currpath:
642
+ currpath=currpath.replace(directory_path,'./')
643
+ return currpath
644
+
561
645
  def findFiles_sorted(pattern):
562
646
  list_files=glob.glob(pattern)
563
647
  files=sorted([re.sub(r'\\+',r'/',f) for f in list_files],key=str.lower)
@@ -682,6 +766,43 @@ def identifierName(typeObject:str='proc'):
682
766
  name=version_user_info+'_'+typeObject+'_'+id
683
767
  return name, username, __version__
684
768
 
769
+ def fileIdenitifierCheck(id: str, filename: str) -> bool:
770
+ """
771
+ Extract the date/time from the identifier 'name' and check whether 'filename'
772
+ has been modified after that timestamp.
773
+ Returns True if file is newer, False otherwise.
774
+ """
775
+
776
+ # --- Extract timestamp from name ---
777
+ # Expected pattern: ..._<date>-<time>_...
778
+ # Example: 'PaIRS-v0.2.8_Linux-user_2025/11/07-12:45:31_proc_...'
779
+ try:
780
+ parts = id.split('_')
781
+ date_str, time_str = parts[2].split('-')[0], parts[2].split('-')[1]
782
+ except Exception:
783
+ pri.Error.red("Identifier format not recognized: cannot extract date and time.")
784
+ return False
785
+
786
+ # Convert date/time strings into QDateTime
787
+ qdate = QDate.fromString(date_str, 'yyyy/MM/dd')
788
+ qtime = QTime.fromString(time_str, 'HH:mm:ss')
789
+ qdt_identifier = QDateTime(qdate, qtime)
790
+ qdt_identifier = qdt_identifier.addSecs(-1) #to be safe
791
+
792
+ if not qdt_identifier.isValid():
793
+ pri.Error.red("Parsed QDateTime is not valid. Check identifier format.")
794
+ return False
795
+
796
+ # --- File timestamp ---
797
+ if not os.path.exists(filename):
798
+ return False
799
+
800
+ file_mtime = os.path.getmtime(filename)
801
+ qdt_file = QDateTime.fromSecsSinceEpoch(int(file_mtime))
802
+
803
+ # True if file was modified after the timestamp stored in name
804
+ return qdt_file > qdt_identifier
805
+
685
806
  PlainTextConverter=QtGui.QTextDocument()
686
807
  def toPlainText(text):
687
808
  PlainTextConverter.setHtml(text) #for safety
@@ -782,12 +903,16 @@ def runPaIRS(self,command='',flagQuestion=True):
782
903
  def run(self):
783
904
  try:
784
905
  import subprocess
785
- if Flag: #launched from package
786
- pri.Info.white(sys.executable+' -m PaIRS_UniNa '+command)
787
- subprocess.call(sys.executable+' -m PaIRS_UniNa '+command,shell=True)
906
+ if Flag_ISEXE:
907
+ pri.Info.white(sys.executable+' '+command)
908
+ subprocess.call(sys.executable+' '+command,shell=True)
788
909
  else:
789
- pri.Info.white(sys.executable+' -c '+'"'+f"import os; os.chdir('{os.getcwd()}'); {pyCommands[command]}"+'"')
790
- subprocess.call(sys.executable+' -c '+'"'+f"import os; os.chdir('{os.getcwd()}'); {pyCommands[command]}"+'"',shell=True)
910
+ if Flag: #launched from package
911
+ pri.Info.white(sys.executable+' -m PaIRS_UniNa '+command)
912
+ subprocess.call(sys.executable+' -m PaIRS_UniNa '+command,shell=True)
913
+ else:
914
+ pri.Info.white(sys.executable+' -c '+'"'+f"import os; os.chdir('{os.getcwd()}'); {pyCommands[command]}"+'"')
915
+ subprocess.call(sys.executable+' -c '+'"'+f"import os; os.chdir('{os.getcwd()}'); {pyCommands[command]}"+'"',shell=True)
791
916
  self.isRunning=False
792
917
  except Exception as inst:
793
918
  pri.Error.red(inst)
@@ -816,12 +941,12 @@ def showSplash(filename=''+ icons_path +'logo_PaIRS_completo.png'):
816
941
  splash.show()
817
942
  return splash
818
943
 
819
- def checkLatestVersion(self,version,app:QApplication=None,splash:QLabel=None):
944
+ def checkLatestVersion(self,version,app:QApplication=None,splash:QLabel=None,flagWarning=1):
820
945
  flagStopAndDownload=False
821
946
  var=self.TABpar
822
947
  #var.FlagOutDated=0 if currentVersion==var.latestVersion else var.FlagOutDated
823
948
  if abs(var.FlagOutDated)==1:
824
- warningLatestVersion(self,app,flagExit=0,flagWarning=1,FlagStayOnTop=True)
949
+ warningLatestVersion(self,app,flagExit=0,flagWarning=flagWarning,FlagStayOnTop=True)
825
950
  var.FlagOutDated=2 if var.FlagOutDated==1 else -2
826
951
  """
827
952
  flagStopAndDownload=questionDialog(self,f'A new version of the PaIRS_UniNa package is available. Do you want to download it before starting the current istance of {self.name}?')
@@ -852,7 +977,7 @@ def checkLatestVersion(self,version,app:QApplication=None,splash:QLabel=None):
852
977
  var.FlagOutDated=-2 if var.FlagOutDated==-2 else -1
853
978
  elif flagOutDated==-1000:
854
979
  sOut=f'Error from pip: it was not possible to check for a new version of the {packageName} package!'
855
- var.FlagOutDated=0
980
+ var.FlagOutDated=-1000
856
981
  else:
857
982
  sOut=f'{packageName} The current version ({currentVersion}) of {packageName} is up-to-date! Enjoy it!'
858
983
  var.FlagOutDated=0
@@ -872,13 +997,18 @@ def warningLatestVersion(self,app,flagExit=0,flagWarning=0,time_milliseconds=0,F
872
997
  py=myStandardRoot(sys.executable).split('/')[-1].split('.')[0]
873
998
  command=f'{py} -m pip install --upgrade PaIRS_UniNa'
874
999
  if self.TABpar.FlagOutDated>0:
875
- Message=f'A new version of the PaIRS_UniNa package is available (current: {self.TABpar.currentVersion}, latest: {self.TABpar.latestVersion}).\nPlease, {exitSuggestion}install it with the following command:\n{command}'
1000
+ if Flag_ISEXE:
1001
+ Message=f'A new version of the PaIRS_UniNa package is available (current: {self.TABpar.currentVersion}, latest: {self.TABpar.latestVersion}).\nPlease, download it from the following link:\n{EXEurl}'
1002
+ else:
1003
+ Message=f'A new version of the PaIRS_UniNa package is available (current: {self.TABpar.currentVersion}, latest: {self.TABpar.latestVersion}).\nPlease, {exitSuggestion}install it with the following command:\n{command}'
1004
+ elif self.TABpar.FlagOutDated==-1000:
1005
+ Message = ("Unable to check for the latest official release of PaIRS_UniNa. Please check the PyPI page manually for updates:\n""https://pypi.org/project/PaIRS-UniNa/")
876
1006
  else:
877
1007
  Message=f'The version of the current instance of PaIRS_UniNa ({self.TABpar.currentVersion}) is newer than the latest official releas ({self.TABpar.latestVersion})!\nYou should contact Tommaso and Gerardo if you are a developer and some relevant change is made by yourself!\nIf you are a user, enjoy this beta version and please report any issue!'
878
1008
  if flagExit:
879
1009
  print(f"\n{'*'*100}\n"+Message+f"\n{'*'*100}\n")
880
1010
  if flagWarning:
881
- warningDialog(self,Message,time_milliseconds=time_milliseconds,flagScreenCenter=True,pixmap=''+ icons_path +'flaticon_PaIRS_download.png' if self.TABpar.FlagOutDated>0 else ''+ icons_path +'flaticon_PaIRS_beta.png',FlagStayOnTop=FlagStayOnTop,addButton={"See what's new!": lambda: QDesktopServices.openUrl(QUrl("https://pypi.org/project/PaIRS-UniNa/"))} if self.TABpar.FlagOutDated>0 else {})
1011
+ warningDialog(self,Message,time_milliseconds=time_milliseconds,flagScreenCenter=True,pixmap=''+ icons_path +'flaticon_PaIRS_download.png' if self.TABpar.FlagOutDated>0 else ''+ icons_path +'flaticon_PaIRS_download_warning.png' if self.TABpar.FlagOutDated==-1000 else ''+ icons_path +'flaticon_PaIRS_beta.png',FlagStayOnTop=FlagStayOnTop,addButton={"Go to the download page!": lambda: QDesktopServices.openUrl(QUrl(EXEurl))} if Flag_ISEXE else {"See what's new!": lambda: QDesktopServices.openUrl(QUrl("https://pypi.org/project/PaIRS-UniNa/"))} if self.TABpar.FlagOutDated>0 else {})
882
1012
 
883
1013
  def downloadLatestVersion(self,app):
884
1014
  try:
@@ -902,6 +1032,7 @@ def downloadLatestVersion(self,app):
902
1032
 
903
1033
  def button_download_PaIRS_action(self,app):
904
1034
  warningLatestVersion(self,app,flagExit=0,flagWarning=1)
1035
+ checkLatestVersion(self,__version__,self.app,splash=None,flagWarning=0)
905
1036
  return
906
1037
  flagStopAndDownload=questionDialog(self,f'A new version of the PaIRS_UniNa package is available. Do you want to close the current instance of {self.name} and download it?')
907
1038
  if not flagStopAndDownload: return
@@ -913,6 +1044,20 @@ def button_download_PaIRS_action(self,app):
913
1044
  else: command=''
914
1045
  subprocess.call(sys.executable+' -m PaIRS_UniNa '+command,shell=True)
915
1046
  #runPaIRS(self,flagQuestion=False)
1047
+
1048
+ import urllib.request, json, ssl
1049
+
1050
+ def get_package_version_urllib(package_name):
1051
+ """Get package version using only standard library"""
1052
+ try:
1053
+ import certifi
1054
+ url = f"https://pypi.org/pypi/{package_name}/json"
1055
+ context = ssl.create_default_context(cafile=certifi.where())
1056
+ with urllib.request.urlopen(url, context=context, timeout=10) as response:
1057
+ data = json.loads(response.read().decode())
1058
+ return True, data['info']['version']
1059
+ except Exception as e:
1060
+ return False, f"Error: {e}"
916
1061
 
917
1062
  def checkOutDated(packageName:str,printOutDated):
918
1063
  '''
@@ -944,48 +1089,60 @@ def checkOutDated(packageName:str,printOutDated):
944
1089
  currentVersion='none'
945
1090
  latestVersion=''
946
1091
  try:
947
- if Flag_DEBUG:
1092
+ if Flag_ISEXE:
948
1093
  currentVersion=__version__+'.'+__subversion__ if int(__subversion__) else __version__
949
1094
  else:
950
- command=[sys.executable, '-m', 'pip', 'show', packageName]
951
- reqs = subprocess.run(command,capture_output=True)
952
- if reqs.returncode:
953
- pri.Error.red('Error in command:\n'+' '.join(command)+'\n'+reqs.stderr.decode("utf-8") )
954
- return flagOutDated,currentVersion,latestVersion
955
- printing=reqs.stdout.decode("utf-8")
956
- pri.Info.cyan( printing )
957
- r=reqs.stdout.decode("utf-8").replace('\r','').split('\n')
958
- currentVersion='none'
959
- for s in r:
960
- if 'Version: ' in s:
961
- currentVersion=s.replace('Version: ','')
962
- break
1095
+ if Flag_DEBUG:
1096
+ currentVersion=__version__+'.'+__subversion__ if int(__subversion__) else __version__
1097
+ else:
1098
+ command=[sys.executable, '-m', 'pip', 'show', packageName]
1099
+ reqs = subprocess.run(command,capture_output=True)
1100
+ if reqs.returncode:
1101
+ pri.Error.red('Error in command:\n'+' '.join(command)+'\n'+reqs.stderr.decode("utf-8") )
1102
+ return flagOutDated,currentVersion,latestVersion
1103
+ printing=reqs.stdout.decode("utf-8")
1104
+ pri.Info.cyan( printing )
1105
+ r=reqs.stdout.decode("utf-8").replace('\r','').split('\n')
1106
+ currentVersion='none'
1107
+ for s in r:
1108
+ if 'Version: ' in s:
1109
+ currentVersion=s.replace('Version: ','')
1110
+ break
963
1111
  if currentVersion!=__version__:
964
1112
  message=f'Greetings, developer!\nThe version of the current instance of PaIRS_UniNa ({__version__}) is different from that installed in the present Python environment ({currentVersion})!\nYou should contact Tommaso and Gerardo if some relevant change is made by yourself!'
965
1113
  pri.Info.yellow(f'{"-"*50}\n{message}\n{"-"*50}\n')
966
- command=[sys.executable, '-m', 'pip', 'index','versions',packageName]
967
- reqs = subprocess.run(command,capture_output=True)
968
- if not reqs.returncode:
969
- printing=reqs.stdout.decode("utf-8")
970
- pri.Info.cyan( printing )
971
- r=reqs.stdout.decode("utf-8").replace('\r','').split('\n')
972
- #currentVersion=r[0].replace(packageName,'').replace('(','').replace(')','').replace(' ','')
973
- latestVersion=r[1].replace('Available versions: ','').split(',')[0]
1114
+ if Flag_ISEXE:
1115
+ _, latestVersion = get_package_version_urllib("PaIRS_UniNa")
974
1116
  else:
975
- pri.Error.red('Error in command:\n'+' '.join(command)+'\n'+reqs.stderr.decode("utf-8") )
976
-
977
- command=[sys.executable, '-m', 'pip', 'list','--outdated']
1117
+ command=[sys.executable, '-m', 'pip', 'index', 'versions', packageName]
978
1118
  reqs = subprocess.run(command,capture_output=True)
979
- if reqs.returncode:
980
- pri.Error.red('Error in command:\n'+' '.join(command)+'\n'+reqs.stderr.decode("utf-8") )
981
- return flagOutDated,currentVersion,latestVersion
982
- outDated = [r.decode().split('==')[0] for r in reqs.stdout.split()]
983
- if packageName in outDated:
984
- i=outDated.index(packageName)
985
- latestVersion=outDated[i+2]
1119
+ if not reqs.returncode:
1120
+ printing=reqs.stdout.decode("utf-8")
1121
+ pri.Info.cyan( printing )
1122
+ r=reqs.stdout.decode("utf-8").replace('\r','').split('\n')
1123
+ #currentVersion=r[0].replace(packageName,'').replace('(','').replace(')','').replace(' ','')
1124
+ latestVersion=r[1].replace('Available versions: ','').split(',')[0]
986
1125
  else:
987
- latestVersion=currentVersion
988
- pri.Info.cyan(f'{packageName} ({currentVersion}). Latest version available: {latestVersion}')
1126
+ flagOk,latestVersion=get_package_version_urllib(packageName)
1127
+ if not flagOk:
1128
+ pri.Error.red('Error in command:\n'+' '.join(command)+'\n'+reqs.stderr.decode("utf-8") )
1129
+ pri.Error.red(latestVersion)
1130
+ latestVersion='none'
1131
+
1132
+ """
1133
+ command=[sys.executable, '-m', 'pip', 'list','--outdated']
1134
+ reqs = subprocess.run(command,capture_output=True)
1135
+ if reqs.returncode:
1136
+ pri.Error.red('Error in command:\n'+' '.join(command)+'\n'+reqs.stderr.decode("utf-8") )
1137
+ return flagOutDated,currentVersion,latestVersion
1138
+ outDated = [r.decode().split('==')[0] for r in reqs.stdout.split()]
1139
+ if packageName in outDated:
1140
+ i=outDated.index(packageName)
1141
+ latestVersion=outDated[i+2]
1142
+ else:
1143
+ latestVersion=currentVersion
1144
+ pri.Info.cyan(f'{packageName} ({currentVersion}). Latest version available: {latestVersion}')
1145
+ """
989
1146
  #flagOutDated=1 if currentVersion!=latestVersion else 0
990
1147
  cV_parts=[int(c) for c in currentVersion.split('.')]
991
1148
  lV_parts=[int(c) for c in latestVersion.split('.')]
@@ -1165,6 +1322,19 @@ def resetRequiredPackagesFile(filename=rqrdpckgs_filename):
1165
1322
  else:
1166
1323
  pri.Error.red(f"resetRequiredPackagesFile: Skipping malformed line: {line}")
1167
1324
 
1325
+ def to_triplet(v: Version) -> tuple[int,int,int]:
1326
+ r = v.release or (0,)
1327
+ return (r[0], r[1] if len(r) > 1 else 0, r[2] if len(r) > 2 else 0)
1328
+
1329
+ def le_ver(a: Version, b: Version) -> bool:
1330
+ a1,a2,a3 = to_triplet(a)
1331
+ b1,b2,b3 = to_triplet(b)
1332
+ if a1 > b1: return False
1333
+ if a1 < b1: return True
1334
+ if a2 > b2: return False
1335
+ if a2 < b2: return True
1336
+ return a3 <= b3
1337
+
1168
1338
  def checkRequiredPackages(self, filename=rqrdpckgs_filename, FlagDisplay=False, FlagForcePrint=False):
1169
1339
  required_packages = []
1170
1340
  vmin_list = []
@@ -1199,7 +1369,7 @@ def checkRequiredPackages(self, filename=rqrdpckgs_filename, FlagDisplay=False,
1199
1369
  Flag = True
1200
1370
 
1201
1371
  # Check if within [vmin, vmax]
1202
- if not (vmin_list[i] <= installed_version <= vmax_list[i]):
1372
+ if not (le_ver(vmin_list[i],installed_version) and le_ver(installed_version,vmax_list[i])):
1203
1373
  """
1204
1374
  warnings.append(
1205
1375
  f"- {pkg}: installed = {installed_version}, target range = [{vmin_list[i]}, {vmax_list[i]}]"
@@ -1216,7 +1386,8 @@ def checkRequiredPackages(self, filename=rqrdpckgs_filename, FlagDisplay=False,
1216
1386
  "Some installed packages have a version outside the target range used to develop "
1217
1387
  "the current release of the PaIRS_UniNa package.\n\n"
1218
1388
  "This may lead to compatibility issues. If you experience unexpected behavior, "
1219
- "it is recommended to either reinstall the last tested compatible versions."
1389
+ "it is recommended to either reinstall the last tested compatible versions or "
1390
+ f"download the executable at {EXEurl}."
1220
1391
  f" If any issue occurs, please contact the authors at {__mail__}.\n\n"
1221
1392
  #"or use the standalone executable available at:\n"
1222
1393
  #"https://pairs.unina.it/#download\n\n"
@@ -724,7 +724,7 @@ class Process_Tab(gPaIRS_Tab):
724
724
  #******************** Actions
725
725
  def edit_Wind_vectors(self,wedit:QLineEdit,wlab:QLabel):
726
726
  text=wedit.text()
727
- split_text=re.split('(\d+)', text)[1:-1:2]
727
+ split_text=re.split(r'(\d+)', text)[1:-1:2]
728
728
  vect=[int(i) for i in split_text]
729
729
  FlagEmpty=len(vect)==0
730
730
  if FlagEmpty: FlagError=True
@@ -1291,7 +1291,7 @@ class Process_Tab(gPaIRS_Tab):
1291
1291
 
1292
1292
  def line_edit_IW_action(self):
1293
1293
  text=self.ui.line_edit_IW.text()
1294
- split_text=re.split('(\d+)', text)[1:-1:2]
1294
+ split_text=re.split(r'(\d+)', text)[1:-1:2]
1295
1295
  vect=[int(split_text[i]) for i in (0,2,1,3)]
1296
1296
  if len(vect)==4:
1297
1297
  k=self.PROpar.row
@@ -133,7 +133,7 @@ class Process_Tab_Disp(gPaIRS_Tab):
133
133
  #******************** Actions
134
134
  def line_edit_IW_action(self):
135
135
  text=self.ui.line_edit_IW.text()
136
- split_text=re.split('(\d+)', text)[1:-1:2]
136
+ split_text=re.split(r'(\d+)', text)[1:-1:2]
137
137
  split_num=[int(t) for t in split_text]
138
138
  if len(split_num)<4: split_num+=[split_num[-1]]*(4-len(split_num))
139
139
  vect=[int(split_num[i]) for i in (0,2,1,3)]
@@ -0,0 +1,155 @@
1
+ from PySide6 import QtWidgets, QtGui, QtCore
2
+ import sys
3
+ from .PaIRS_pypacks import icons_path, fontPixelSize
4
+
5
+ def showSPIVCalHelp(parent=None,disable_callback=None):
6
+ """
7
+ Shows an informational dialog explaining the correct calibration setup
8
+ for stereoscopic PIV in PaIRS, ensuring compatibility with disparity
9
+ correction and full stereoscopic reconstruction.
10
+ """
11
+ dlg = QtWidgets.QDialog(parent)
12
+ dlg.setWindowTitle("Guidelines for stereoscopic PIV calibration")
13
+ #dlg.resize(900, 750)
14
+ dlg.setMinimumWidth(900)
15
+ dlg.setMinimumHeight(750)
16
+
17
+ main_layout = QtWidgets.QVBoxLayout(dlg)
18
+
19
+ # --- Explanatory text (English, corrected axes) ---
20
+ text = (
21
+ "For stereoscopic PIV, PaIRS assumes that:<br><br>"
22
+
23
+ "&nbsp;&nbsp;• the <b>calibration plate defines "
24
+ "the x–y plane</b> of the calibration coordinate system;<br>"
25
+
26
+ "&nbsp;&nbsp;• the <b>x-axis</b> is <b>aligned with the stereoscopic baseline</b>, i.e. "
27
+ "the direction along which the projections of the two camera viewing rays diverge "
28
+ "on the calibration plate (the dominant disparity direction);<br>"
29
+
30
+ "&nbsp;&nbsp;• the <b>y-axis</b> is then defined as the axis <b>perpendicular to the plane containing "
31
+ "the two cameras</b> (i.e. perpendicular to the triangulation plane formed by the two "
32
+ "optical axes);<br>"
33
+
34
+ "&nbsp;&nbsp;• the <b>z-axis is normal to the plate</b> (typically pointing towards the cameras).<br><br>"
35
+
36
+ "To ensure full compatibility with the operations performed in the disparity correction step and the stereoscopic reconstruction,"
37
+ " the calibration procedure must always adhere to the above coordinate convention.<br>"
38
+ " The example below shows a <b>correct</b> configuration (left) and an <b>incorrect</b> one (right).<br>"
39
+ )
40
+
41
+ text_label = QtWidgets.QLabel()
42
+ text_label.setWordWrap(True)
43
+ text_label.setTextFormat(QtCore.Qt.RichText)
44
+ text_label.setText(f"<div>{text}</div>")
45
+ main_layout.addWidget(text_label)
46
+ font=dlg.font()
47
+ font.setPixelSize(fontPixelSize+4)
48
+ text_label.setFont(font)
49
+
50
+ # --- Side-by-side images ---
51
+ img_layout = QtWidgets.QHBoxLayout()
52
+ img_layout.setSpacing(10)
53
+ main_layout.addLayout(img_layout)
54
+
55
+ # Paths to images (adjust to match PaIRS resources folder)
56
+ img_ok_path = icons_path+"spiv_setup_ok.png"
57
+ img_no_path = icons_path+"spiv_setup_no.png"
58
+
59
+ # --- Correct configuration image ---
60
+ ok_widget = QtWidgets.QVBoxLayout()
61
+ ok_caption = QtWidgets.QLabel()
62
+ caption_text = "<b>Correct configuration (x–z stereo plane)</b>"
63
+ ok_caption.setText(f"<div>{caption_text}</div>")
64
+ ok_caption.setFont(font)
65
+ ok_caption.setTextFormat(QtCore.Qt.RichText)
66
+ ok_caption.setAlignment(QtCore.Qt.AlignCenter)
67
+
68
+ ok_label_img = QtWidgets.QLabel()
69
+ ok_label_img.setAlignment(QtCore.Qt.AlignCenter)
70
+ ok_label_img.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
71
+ ok_label_img.setScaledContents(True)
72
+
73
+ ok_pix = QtGui.QPixmap(img_ok_path)
74
+ if not ok_pix.isNull():
75
+ ok_pix = ok_pix.scaledToWidth(400, QtCore.Qt.SmoothTransformation)
76
+ ok_label_img.setPixmap(ok_pix)
77
+ ok_label_img.setFixedSize(ok_pix.width(),ok_pix.height())
78
+
79
+ ok_widget.addWidget(ok_caption)
80
+ ok_widget.addWidget(ok_label_img)
81
+ img_layout.addLayout(ok_widget)
82
+
83
+ # --- Incorrect configuration image ---
84
+ no_widget = QtWidgets.QVBoxLayout()
85
+ no_caption = QtWidgets.QLabel()
86
+ caption_text = "<b>Incorrect configuration</b>"
87
+ no_caption.setText(f"<div'>{caption_text}</div>")
88
+ no_caption.setFont(font)
89
+ no_caption.setTextFormat(QtCore.Qt.RichText)
90
+ no_caption.setAlignment(QtCore.Qt.AlignCenter)
91
+
92
+ no_label_img = QtWidgets.QLabel()
93
+ no_label_img.setAlignment(QtCore.Qt.AlignCenter)
94
+ no_label_img.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
95
+ no_label_img.setScaledContents(True)
96
+
97
+ no_pix = QtGui.QPixmap(img_no_path)
98
+ if not no_pix.isNull():
99
+ no_pix = no_pix.scaledToWidth(400, QtCore.Qt.SmoothTransformation)
100
+ no_label_img.setPixmap(no_pix)
101
+ no_label_img.setFixedSize(no_pix.width(),no_pix.height())
102
+
103
+ no_widget.addWidget(no_caption)
104
+ no_widget.addWidget(no_label_img)
105
+ img_layout.addLayout(no_widget)
106
+
107
+ # Prevent bottom clipping: allow images row to shrink/expand as needed
108
+ main_layout.setStretch(0, 1)
109
+ main_layout.setStretch(1, 0)
110
+
111
+ # Create a horizontal layout for buttons
112
+ button_layout = QtWidgets.QHBoxLayout()
113
+
114
+ # Spacer pushes OK to the right
115
+ button_layout.addStretch(1)
116
+
117
+ # Left button
118
+ if disable_callback is not None:
119
+ button_disable = QtWidgets.QPushButton("Don't show this message again")
120
+ def on_disable():
121
+ disable_callback()
122
+ dlg.accept()
123
+ button_disable.clicked.connect(on_disable)
124
+ button_layout.addWidget(button_disable)
125
+
126
+ # Right button (OK)
127
+ button_ok = QtWidgets.QPushButton("OK")
128
+ def on_ok():
129
+ dlg.accept()
130
+ button_ok.clicked.connect(on_ok)
131
+ button_layout.addWidget(button_ok)
132
+
133
+ # Add the layout to the dialog
134
+ main_layout.addSpacing(12)
135
+ main_layout.addLayout(button_layout)
136
+
137
+ button_ok.setDefault(True)
138
+ button_ok.setFocus()
139
+
140
+ main_layout.setContentsMargins(20, 25, 20, 25) # slightly larger bottom margin to avoid macOS clipping
141
+
142
+ dlg.exec()
143
+
144
+ if __name__ == "__main__":
145
+ # QApplication MUST be created before any QWidget
146
+ app = QtWidgets.QApplication(sys.argv)
147
+
148
+ # Parent window (optional)
149
+ main_window = QtWidgets.QMainWindow()
150
+ main_window.show()
151
+
152
+ # Show the SPIV calibration dialog
153
+ showSPIVCalHelp(parent=main_window)
154
+
155
+ sys.exit(app.exec())