From e7cd5aa568701feac931b396cf03a25f84bafd74 Mon Sep 17 00:00:00 2001
From: Guillaume  Bodart <guillaume.bodart@inrae.fr>
Date: Mon, 5 Jul 2021 14:10:18 +0200
Subject: [PATCH] MAJ latest dev

---
 project.py | 269 ++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 236 insertions(+), 33 deletions(-)

diff --git a/project.py b/project.py
index 43ea412..6246b7a 100644
--- a/project.py
+++ b/project.py
@@ -25,6 +25,8 @@ import matplotlib as mpl
 import plotlib 
 import cv2 as cv
 from tkinter.filedialog import askdirectory
+import ctypes
+from numpy.ctypeslib import ndpointer
 
 if 'monStyle' in plt.style.available:
     plt.style.use('monStyle')
@@ -770,7 +772,19 @@ class dataCl():
             lblClbr = param_dict["lblClbr"]
             del param_dict["lblClbr"]
         else:
-            lblClbr = "Velocity difference [m/s]"
+            lblClbr = "Velocity [m/s]"
+        # label of colorbar
+        if "vmin" in param_dict.keys():
+            vmin = param_dict["vmin"]
+            del param_dict["vmin"]
+        else:
+            vmin = None
+        # label of colorbar
+        if "vmax" in param_dict.keys():
+            vmax = param_dict["vmax"]
+            del param_dict["vmax"]
+        else:
+            vmax = None
         
         # ---- PLOT ----
         if mtype == "vel":
@@ -809,7 +823,11 @@ class dataCl():
                                     scale_units     = "xy",
                                     **param_dict)
                     
-                    mapp = mpl.cm.ScalarMappable(mpl.colors.Normalize(vmin = norm.min(),vmax = norm.max()),cmap = cmap)
+                    if vmin == None:
+                        vmin = norm.min()
+                    if vmax == None:
+                        vmax = norm.min()
+                    mapp = mpl.cm.ScalarMappable(mpl.colors.Normalize(vmin = vmin,vmax = vmax),cmap = cmap)
                     # Add colorbar 
                     plt.colorbar(mapp,ax=ax,label = lblClbr)
             
@@ -1068,6 +1086,11 @@ class dataCl():
             - showCdf [int], optional
                 Flag that indicates whether the cumulative density function (cdf) has to be plotted or not 
                 Default is False
+        
+            --- ADDITIONARY ARGUMENT FOR HIST
+            
+            - legend [bool], optionnal
+                Indicates if legend has to be plotted, mainly for histograms and statistics. 
                 
              --- ADDITIONARY ARGUMENT FOR EE_PIX : 
 
@@ -1104,7 +1127,9 @@ class dataCl():
             camera = param_dict["camera"]
             del param_dict["camera"]
         else:
-            camera = None
+            if method in ["EE_pix","AE_pix","AE_2D_pix"]:
+                print("\nERROR: camera data is mandatory for this method ! Please add it to kwargs")
+                return 1
             
         # ---- PLOT ----
         dictPlotFct = {"dist"               : plotlib.dist,
@@ -1130,8 +1155,12 @@ class dataCl():
             selfDsp         = self.copy()
             compDsp         = compData.copy()
             # Converts now to pixel displacement 
-            selfDsp.vel     = velSpace2dispImage(selfDsp.vel, camera)
-            compDsp.vel     = velSpace2dispImage(compDsp.vel, camera)
+            try:
+                selfDsp.vel     = camera.velSpace2dispImage(selfDsp.vel)
+                compDsp.vel     = camera.velSpace2dispImage(compDsp.vel)
+            except Exception as e:
+                print("\nERROR: Problem while changing from velocity to pixels displacement... \n Exception: " + str(e))
+                return 1
             # Plot 
             meth = method[:-4]
             mplt = dictPlotFct[meth](ax,selfDsp.vel,compDsp.vel,**param_dict)
@@ -1286,8 +1315,12 @@ class dataCl():
             selfDsp         = self.copy()
             compDsp         = compData.copy()
             # Converts now to pixel displacement 
-            selfDsp.vel     = velSpace2dispImage(selfDsp.vel, camera)
-            compDsp.vel     = velSpace2dispImage(compDsp.vel, camera)
+            try:
+                selfDsp.vel     = camera.velSpace2dispImage(selfDsp.vel)
+                compDsp.vel     = camera.velSpace2dispImage(compDsp.vel)
+            except Exception as e:
+                print("\nERROR: Problem while changing from velocity to pixels displacement... \n Exception: " + str(e))
+                return 1
             # compute diff
             ee = compDsp.vel - selfDsp.vel 
             # Compute norm 
@@ -1636,10 +1669,10 @@ class dataCl():
             self.vel[Flspiv_data.vel == 0]          = np.nan
             Flspiv_data.vel[Flspiv_data.vel == 0]   = np.nan
         elif case ==2:
-            isFiltre =  np.array([(Flspiv_data.vx == 0),(Flspiv_data.vy == 0)]).T
+            isFiltre =  np.logical_and(Flspiv_data.vx == 0,Flspiv_data.vy == 0)
             self.vel[isFiltre]              = np.nan
-            Flspiv_data.vx[isFiltre[:,0]]   = np.nan
-            Flspiv_data.vy[isFiltre[:,0]]   = np.nan
+            Flspiv_data.vx[isFiltre]        = np.nan
+            Flspiv_data.vy[isFiltre]        = np.nan
             
         return   
         
@@ -1780,7 +1813,109 @@ class dataCl():
                 mapp = mpl.cm.ScalarMappable(mpl.colors.Normalize(vmin = vmin,vmax = vmax),cmap = cmap)
                 # Add colorbar 
             fig.colorbar(mapp,ax=ax,label = lblClbr)
+    
+    def filter(self):
+            """ Remove filtered velocities from list """
+            
+            f           = np.logical_not(np.isfinite(self.vel))
+            self.pos    = np.array([np.delete(self.pos[:,0],f[:,0]),np.delete(self.pos[:,1],f[:,1])]).T
+            self.vel    = np.array([np.delete(self.vel[:,0],f[:,0]),np.delete(self.vel[:,1],f[:,1])]).T
+            if self.normals != []:
+                self.normals = np.array([np.delete(self.normals[:,0],f[:,0]),np.delete(self.normals[:,1],f[:,1])]).T
+            
+            return
+            
+    def warpImg(self,
+                camera,
+                im1,
+                im2,
+                outRMS = True):  
+            """
+            Apply velocity field to warp image2 back into position of image1. 
+            This tool is used for Backend Error Projection. 
+            Output is the warped image and the RMS error computed over the velocity field 
+            
+            Parameters
+            ----------
+                - im1 : [npArray]
+                    First image (could be ndg or color image)
+                - im2 : [npArray]
+                    Second image image (could be ndg or color image)
+                - outRMS : [bool]
+                    If True then the RMS error between im1 and im1_warp is returned. 
+                    Error is computed only within the warped area. 
+            Returns
+            -------
+                - warpedImage : [npArray]
+                    Image 2 "warped" with the velocity field of image 1  
+                - RMS   : [float]
+                    RMS error computed over all image 
+                - errno : [int]
+                    - 1 - Wrong format for inputs 
+
+            """
+            #### Checkings 
+            if np.shape(im1) != np.shape(im2):
+                print("\nERROR: Shape of images mismatch")
+                return 1
+            # image format -> ndg or color 
+            if len(np.shape(im1)) != 2:
+                print("\nERROR: Wrong image format")
+                return 1
             
+            #### Get data
+            # shape 
+            ny,nx       = np.shape(im1)[:2]
+            # Remove nan from arrays
+            self.filter()
+            # indexes
+            tmp         = space2img(self.pos, camera)
+            ind         = np.array(tmp,dtype = int)
+            # displacement [pixels]
+            disp        = camera.velSpace2dispImage(self.vel) 
+            # Prepare grid for interpolation
+            ij          = np.meshgrid(np.arange(ny), np.arange(nx),indexing = "ij") # indexes of image (/!\ meshgrid gives [0] -> x (j) and [1] -> y (i))
+            gridij      = np.array([ij[0],ij[1]])                                   # Create 2D grid containing i and j indexes (stored [i,j] -> [y,x])
+            gridij      = np.moveaxis(gridij,0,2)                                   # Reshape the data for compatibility -> [ni,nj,2] instead of [2,ni,nj]
+            # Interpolate displacement on all image 
+            uv_i        = griddata(ind, disp, gridij, method="cubic")
+            # Put velocities in grid and change nan to 0 (for interpolation function)
+            u           = np.nan_to_num(uv_i[:,:,1], copy=True, nan=0.0, posinf=None, neginf=None)
+            v           = np.nan_to_num(uv_i[:,:,0], copy=True, nan=0.0, posinf=None, neginf=None)
+            #### Warp image 
+            # Data have to be double for interpolation function
+            imout       = np.array(im1,dtype = np.float64)
+            imin        = np.array(im2,dtype = np.float64)
+            # Use ctypes to wrap C function to Python (much much faster!!)
+            lib = ctypes.CDLL('/home/guillaume.bodart/Documents/04_Dev/03_Lib/Image/01_bicubic_c_func/bicubic_interpolation.so')
+            # Select the function in the dyn. lib
+            bicint = lib.bicubic_interpolation_warp
+            # Arrange input and outputs 
+            bicint.restype = None
+            bicint.argtypes = [ndpointer(ctypes.c_double),
+                               ndpointer(ctypes.c_double),
+                               ndpointer(ctypes.c_double),
+                               ndpointer(ctypes.c_double),
+                               ctypes.c_int,
+                               ctypes.c_int,
+                               ctypes.c_bool]
+            try:
+                bicint(imin,u,v,imout,nx,ny,True)
+            except Exception as e:
+                print("\nERROR: Problem while warping image... \n Exception : " + str(e))
+                return 2
+            # Compute difference between im1 and backprojection 
+            # First apply dynamic expansion based on the data within the warped area
+            ppout   = dynExpans(imout[np.isfinite(uv_i[:,:,0])])
+            pp1     = dynExpans(im1[np.isfinite(uv_i[:,:,0])].astype(float))
+            # comput diff
+            diff    = ppout - pp1
+            RMS       = np.sqrt(np.sum([x**2 for x in diff])/len(diff))
+            
+            if outRMS:       
+                return np.array(np.clip(imout,0,255),dtype = np.uint8),RMS
+            else:
+                return np.array(np.clip(imout,0,255),dtype = np.uint8)
         
         
 class filteredCl(dataCl):
@@ -1990,15 +2125,17 @@ def image2space(imgIndx,camera,xy0 = np.nan):
                 - 1 - Information missing on the camera : no resolution and no scaling
                 - 2 - Image isn't squarred, problem with scaling...
                 - 3 - ResolutionInfo of the camera isn't recognize
+                - 4 - Shape of grid isn't correct'
     """
+    
     if camera.resolutionInfo == "":
         # Case no resolution is entered, use simple scaling (typical blender case with othographic camera)
         if camera.scale == -99:
-            print("ERROR : Information missing on the camera : no resolution and no scaling")
+            print("ERROR: Information missing on the camera : no resolution and no scaling")
             return 1
         # Image size is assumed to be squared 
         if camera.imagesize[0]!=camera.imagesize[1]:
-            print("ERROR : Image isn't squarred, problem with scaling...")
+            print("ERROR: Image isn't squarred, problem with scaling...")
             return 1
         res = camera.imagesize[0] / camera.scale 
     elif camera.resolutionInfo == "m/pix":
@@ -2008,23 +2145,25 @@ def image2space(imgIndx,camera,xy0 = np.nan):
         # Case resolution is given in pixels/meter  
         res = camera.resolution 
     else: 
-        print("ERROR : resolution information of the camera isn't recognize. Expected : 'm/pix' or 'pix/m'")
+        print("ERROR: resolution information of the camera isn't recognize. Expected : 'm/pix' or 'pix/m'")
         return 3
-    
+    # Checkings 
+    if len(np.shape(imgIndx)) > 2:
+        print("ERROR: Shape of grid isn't correct. Expected : [;,2]")
+        return 4
     # If no xy0 are passed to the function they're calculated from the camera data
     if np.isnan(xy0):
         xy0 = (camera.position - (0.5*camera.scale))[:2]
        
     if len(imgIndx)==0:
-        print("WARNING : imgIndx is empty...")
-    # Swap column to get (j,i) as j is x and i is y     
-    JIimgIndx = np.array([imgIndx[:,1],imgIndx[:,0]]).T
+        print("WARNING: imgIndx is empty...")
+    # Swap column to get (j,i) as j is x and i is y and inverse i ax to be in mathematical coordinate space    
+    JIimgIndx = np.array([imgIndx[:,1],camera.imagesize[0] - imgIndx[:,0]]).T    
     out = (JIimgIndx/res) + xy0 
     
     return out
    
-
-def velSpace2dispImage(spaceVel,camera,xy0 = np.nan):
+def space2img(spaceIndx,camera,xy0 = np.nan):
     """
     Converts image indexes to world units with information of the camera and xy0. 
     Assume that the input image is already ortho-rectified (i.e. image can be assumed to be a plan XY)
@@ -2052,17 +2191,14 @@ def velSpace2dispImage(spaceVel,camera,xy0 = np.nan):
                 - 2 - Image isn't squarred, problem with scaling...
                 - 3 - ResolutionInfo of the camera isn't recognize
     """
-    # if isinstance(camera,transform.camera):
-    #     print("\nERROR : wrong format of camera passed to the function. Expected : lspiv.transform.camera ")
-    #     return 1
     if camera.resolutionInfo == "":
         # Case no resolution is entered, use simple scaling (typical blender case with othographic camera)
         if camera.scale == -99:
-            print("\nERROR : Information missing on the camera : no resolution and no scaling")
+            print("ERROR : Information missing on the camera : no resolution and no scaling")
             return 1
         # Image size is assumed to be squared 
         if camera.imagesize[0]!=camera.imagesize[1]:
-            print("\nERROR : Image isn't squarred, problem with scaling...")
+            print("ERROR : Image isn't squarred, problem with scaling...")
             return 1
         res = camera.imagesize[0] / camera.scale 
     elif camera.resolutionInfo == "m/pix":
@@ -2072,23 +2208,90 @@ def velSpace2dispImage(spaceVel,camera,xy0 = np.nan):
         # Case resolution is given in pixels/meter  
         res = camera.resolution 
     else: 
-        print("\nERROR : resolution information of the camera isn't recognize. Expected : 'm/pix' or 'pix/m'")
+        print("ERROR : resolution information of the camera isn't recognize. Expected : 'm/pix' or 'pix/m'")
         return 3
     
     # If no xy0 are passed to the function they're calculated from the camera data
     if np.isnan(xy0):
         xy0 = (camera.position - (0.5*camera.scale))[:2]
        
-    if len(spaceVel)==0:
-        print("\nWARNING : Values are empty...")
+    if len(spaceIndx)==0:
+        print("WARNING : imgIndx is empty...")
+    # Swap column to get (x,y) as j is x and i is y     
+    JIimgIndx = (spaceIndx - xy0) * res # Because here the resolution is in pix/meters
+    out = np.array([camera.imagesize[0]-JIimgIndx[:,1],JIimgIndx[:,0]]).T
+    
+    return out
+
+def dynExpans(val):
+    mx, mn = val.max(),val.min()
+    return 255*((val-mn)/(mx-mn))
+
+# def velSpace2dispImage(spaceVel,camera,xy0 = np.nan):
+#     """
+#     Converts image indexes to world units with information of the camera and xy0. 
+#     Assume that the input image is already ortho-rectified (i.e. image can be assumed to be a plan XY)
+
+#     Parameters
+#     ----------
+#         - imgIndx : [npArrayInt]
+#             Indexes of the images to be transformed. --> shape : [(ind),(i,j)]
+#             WARNING : image coordinate are expected. 
+#             Values from Fudaa-LSPIV are usually given in a mathematical coordinate space with (0,0) located down left
+#             So the i index need to be "invert" --> correctiIndex = imageSize(i) - iIndexFudaa
+            
+#         - camera : [lspiv.transform.camera()]
+#             Contains the information of the camera 
+#         - xyz0 : [npArrayFloat]
+#             Offset (in meters) apply on the values X, Y for transformation
+
+#     Returns
+#     -------
+#         - SpaceCoord [npArrayFloat]
+#             X,Y values in meters
+#         - errno [int]
+#             In case of an error this function returns an integer 
+#                 - 1 - Information missing on the camera : no resolution and no scaling
+#                 - 2 - Image isn't squarred, problem with scaling...
+#                 - 3 - ResolutionInfo of the camera isn't recognize
+#     """
+#     # if isinstance(camera,transform.camera):
+#     #     print("\nERROR : wrong format of camera passed to the function. Expected : lspiv.transform.camera ")
+#     #     return 1
+#     if camera.resolutionInfo == "":
+#         # Case no resolution is entered, use simple scaling (typical blender case with othographic camera)
+#         if camera.scale == -99:
+#             print("\nERROR : Information missing on the camera : no resolution and no scaling")
+#             return 1
+#         # Image size is assumed to be squared 
+#         if camera.imagesize[0]!=camera.imagesize[1]:
+#             print("\nERROR : Image isn't squarred, problem with scaling...")
+#             return 1
+#         res = camera.imagesize[0] / camera.scale 
+#     elif camera.resolutionInfo == "m/pix":
+#         # Case resolution is given in meter/pixels (typical units from Fudaa-LSPIV project) 
+#         res = 1/camera.resolution 
+#     elif camera.resolutionInfo == "pix/m":
+#         # Case resolution is given in pixels/meter  
+#         res = camera.resolution 
+#     else: 
+#         print("\nERROR : resolution information of the camera isn't recognize. Expected : 'm/pix' or 'pix/m'")
+#         return 3
+    
+#     # If no xy0 are passed to the function they're calculated from the camera data
+#     if np.isnan(xy0):
+#         xy0 = (camera.position - (0.5*camera.scale))[:2]
+       
+#     if len(spaceVel)==0:
+#         print("\nWARNING : Values are empty...")
     
-    # Compute displacement based on camera info 
-    XYdisp = spaceVel / camera.fps
-    JIspaceV = (XYdisp)*res  
-    # Swap column to get (i,j) as x is j and y is i     
-    IJindex = np.array([JIspaceV[:,1],JIspaceV[:,0]]).T
+#     # Compute displacement based on camera info 
+#     XYdisp = spaceVel / camera.fps
+#     JIspaceV = (XYdisp)*res  
+#     # Swap column to get (i,j) as x is j and y is i     
+#     IJindex = np.array([JIspaceV[:,1],JIspaceV[:,0]]).T
     
-    return IJindex
+#     return IJindex
    
 
 
-- 
GitLab