diff --git a/Common/geometry.py b/Common/geometry.py
index 235b30ed8b5ef60d0855417fac3c6943b807accd..365ab8ac0b625d39434f2109aee34cc090a76a49 100644
--- a/Common/geometry.py
+++ b/Common/geometry.py
@@ -13,6 +13,7 @@ from datetime import *
 import multiprocessing as mp
 import pickle
 import rasterio
+from scipy.stats import gaussian_kde
 
 def compute_overlap_matrix(_lst: List[otb.Application], out_param='out'):
     masks = []
@@ -49,11 +50,24 @@ def local_shift(src, tgt):
 
     return shifts
 
+def compute_mode_2d(arr):
+    sm = gaussian_kde(arr.transpose())
+    mx, Mx = min(arr[:,0]), max(arr[:,0])
+    my, My = min(arr[:,1]), max(arr[:,1])
+    X, Y = np.mgrid[mx:Mx:0.1, my:My:0.1]
+    positions = np.vstack([X.ravel(), Y.ravel()])
+    Z = np.reshape(sm(positions).T, X.shape)
+    idx = np.unravel_index(Z.argmax(), Z.shape)
+    out = X[idx[0],0], Y[0,idx[1]]
+    return np.array(out)
+
+
+
 def compute_displacement(_src: otb.Application, _tgt: otb.Application,
                          src_band=2, tgt_band=2,
                          out_param_src='out', out_param_tgt='out',
                          geobin_size=32, geobin_spacing=256, margin=32,
-                         filter=5, n_proc=6):
+                         filter=5, n_proc=6, use_kde=False):
 
     sz = _tgt.GetImageSize(out_param_tgt)
     W,H = sz[0], sz[1]
@@ -102,7 +116,10 @@ def compute_displacement(_src: otb.Application, _tgt: otb.Application,
         shifts = shifts[nrm < filter]
 
     if len(shifts) > 0:
-        return np.mean(np.array(shifts), axis=0), len(shifts)
+        if not use_kde:
+            return np.mean(np.array(shifts), axis=0), len(shifts)
+        else:
+            return compute_mode_2d(shifts), len(shifts)
     else:
         return None, 0
 
diff --git a/Common/otb_numpy_proc.py b/Common/otb_numpy_proc.py
index b784ce42a0a0b891495e33b2ccc824af43e18114..94adf8feb42f58c4e8bebbd48aafa20f6166bec3 100644
--- a/Common/otb_numpy_proc.py
+++ b/Common/otb_numpy_proc.py
@@ -5,6 +5,8 @@ import os, psutil
 from functools import partial
 import string, random
 from osgeo.gdal import BuildVRT
+from osgeo.osr import SpatialReference
+from pyproj import Transformer as T
 import otbApplication as otb
 
 def randomword(length):
@@ -205,6 +207,26 @@ def to_otb_pipeline(obj):
         sys.exit("Impossible to convert object ot OTB pipeline")
     return ppl
 
+def ppl_output_extent(img, out_srs=None, param='out'):
+    og = list(img.GetImageOrigin(param))
+    sp = list(img.GetImageSpacing(param))
+    sz = list(img.GetImageSize(param))
+    xt = [og[0], og[0]+sp[0]*sz[0], og[1], og[1]+sp[1]*sz[1]]
+    if xt[0] > xt[1]:
+        xt[0], xt[1] = xt[1], xt[0]
+    if xt[2] > xt[3]:
+        xt[2], xt[3] = xt[3], xt[2]
+    if out_srs==None:
+        return xt
+    else:
+        i_srs = SpatialReference()
+        i_srs.ImportFromWkt(img.GetImageProjection(param))
+        tr = T.from_crs(i_srs.GetAuthorityCode(None), out_srs, always_xy=True)
+        lon_min, lat_min = tr.transform(xt[0], xt[2])
+        lon_max, lat_max = tr.transform(xt[1], xt[3])
+        bbox = [lon_min, lon_max, lat_min, lat_max]
+        return bbox
+
 def extract_roi_as_numpy(img: otb.Application, startx=None, starty=None, sizex=None, sizey=None, bands=None, out_param='out'):
     ers = otb.Registry.CreateApplication('ExtractROI')
     ers.SetParameterInputImage('in', img.GetParameterOutputImage(out_param))
diff --git a/VHR/googlesat.py b/VHR/googlesat.py
index 9aecd1b9548e6ee5c620da3a9135240232fc6947..799261210b85721cd389a1d00c209134f40fe919 100644
--- a/VHR/googlesat.py
+++ b/VHR/googlesat.py
@@ -4,7 +4,7 @@ import os
 from tqdm import tqdm
 from osgeo import gdal
 import otbApplication as otb
-from Common.otb_numpy_proc import to_otb_pipeline
+from Common.otb_numpy_proc import to_otb_pipeline, ppl_output_extent
 
 class GoogleSatScene:
 
@@ -31,6 +31,9 @@ class GoogleSatScene:
             "{y}", str(y)).replace(
             "{z}", str(z))
         path = f'{temp_dir}/{x}_{y}_{z}.png'
+        opener = urllib.request.build_opener()
+        opener.addheaders = [('User-agent', 'Mozilla/5.0')]
+        urllib.request.install_opener(opener)
         urllib.request.urlretrieve(url, path)
         return(path)
 
@@ -67,7 +70,15 @@ class GoogleSatScene:
                     outputBounds=bounds)
         return filename + '.tif'
         
-    def __init__(self, bbox, out, zoom=18):
+    def __init__(self, roi, out, zoom=18):
+
+        if isinstance(roi, list):
+            bbox = roi
+        elif isinstance(roi, otb.Application):
+            #if ROI given with otbApplication object, image parameter must be 'out'
+            bbox = ppl_output_extent(roi, out_srs=4326)
+        
+        print(bbox)
 
         fld = os.path.dirname(out)
         s = os.path.basename(out)
@@ -85,14 +96,28 @@ class GoogleSatScene:
                 tile_lst.append(self.georeference_raster_tile(x, y, zoom, png_path))
                 os.remove(png_path)
                 pbar.update()
+        
+        pbar.close()
 
         mos = otb.Registry.CreateApplication('Mosaic')
         mos.SetParameterStringList('il', tile_lst)
-        mos.SetParameterString('out', out)
-        mos.SetParameterOutputImagePixelType('out', otb.ImagePixelType_uint8)
-        mos.ExecuteAndWriteOutput()
+        
+        
+        if isinstance(roi, list):
+            mos.SetParameterString('out', out)
+            mos.SetParameterOutputImagePixelType('out', otb.ImagePixelType_uint8)
+            mos.ExecuteAndWriteOutput()
+        elif isinstance(roi, otb.Application):
+            mos.Execute()
+            si = otb.Registry.CreateApplication('Superimpose')
+            si.SetParameterInputImage('inr', roi.GetParameterOutputImage('out'))
+            si.SetParameterInputImage('inm', mos.GetParameterOutputImage('out'))
+            si.SetParameterString('out', out)
+            si.SetParameterOutputImagePixelType('out', otb.ImagePixelType_uint8)
+            si.ExecuteAndWriteOutput()
+
         ds = gdal.Open(out,1)
         [ds.GetRasterBand(i).DeleteNoDataValue() for i in range(1, ds.RasterCount+1)]
-
-        [os.remove(x) for x in tile_lst]
         self.scene = to_otb_pipeline(out)
+        [os.remove(x) for x in tile_lst]
+        return
\ No newline at end of file
diff --git a/VHR/vhrbase.py b/VHR/vhrbase.py
index 89f58ed20f02f3720a028fa9ccc142bbd6c7657e..88efccdc2828826b9a2feeec7dbcc9094eb0289b 100644
--- a/VHR/vhrbase.py
+++ b/VHR/vhrbase.py
@@ -7,6 +7,8 @@ import re
 from osgeo import ogr, gdal
 from Common.geometry import compute_displacement
 from math import ceil, floor, sqrt
+import rasterio
+import pickle
 
 class SPOT67RasterPipeline:
     # BEGIN SPOT6/7 VHR PROTOTYPE
@@ -272,4 +274,37 @@ class PleiadesOrthoAutoMosaicPipeline:
             return fn
         else:
             print('Preprocess scenes first.')
-            return None
\ No newline at end of file
+            return None
+        
+class PreprocessedVHR:
+    def __init__(self, path, red_channel=2):
+        self.path = path
+        self.scene = to_otb_pipeline(path)
+        self.spacing = self.scene.GetImageSpacing('out')
+        self.red_channel = red_channel
+
+    def shift_from_reference(self, ref, red_channel=2, 
+                             geobin_size=32, geobin_spacing=32, margin=32, filter=20):
+        rfp = otb.Registry.CreateApplication('Superimpose')
+        rfp.SetParameterInputImage('inr', self.scene.GetParameterOutputImage('out'))
+        rfp.SetParameterString('inm', ref)
+        rfp.Execute()
+        d, n = compute_displacement(self.scene, rfp, self.red_channel, red_channel,
+                                    geobin_size=geobin_size, geobin_spacing=geobin_spacing, 
+                                    margin=margin, filter=filter,
+                                    use_kde=True)
+        return d[1]*self.spacing[0], d[0]*self.spacing[1] 
+    
+    def shift_from_googlesat(self):
+        return
+    
+    def apply_shift_inplace(self, shift):
+        self.scene = None
+        with rasterio.open(self.path, 'r+') as ds:
+            tr = ds.transform
+            with open(os.path.splitext(self.path)[0]+'_transform_backup.pkl', 'wb') as f:
+                pickle.dump(tr, f)
+            ds.transform = rasterio.transform.Affine(tr[0], tr[1], tr[2] + shift[0], tr[3], tr[4], tr[5] + shift[1])
+        self.scene = to_otb_pipeline(self.path)
+        self.spacing = self.scene.GetImageSpacing('out')
+        return