diff --git a/integration_tests/data_test.py b/integration_tests/data_test.py
index 0e3db873d53cf9943a8d3b4b1aae5eb4759d7f7b..cb833a3f6cf2ee55c593bdacccaeb6937af1d2d9 100644
--- a/integration_tests/data_test.py
+++ b/integration_tests/data_test.py
@@ -6,7 +6,7 @@ import time
 import numpy as np
 from main import get_discrete_data, get_raw_data
 from utils.files import output
-from utils.files.input import ScannedObject
+from utils.files.input import ScannedObject, parse_result_file
 
 def check_discrete_data(expected_file: str, actual_file: str, ndigits=7, eps=0.00001, silent: bool = False) -> dict:
     """
@@ -193,7 +193,7 @@ def check_consistency(obj: ScannedObject, discretised_file_path: str):
     :param obj: The object to check
     :param discretised_file_path: The discetised file output
     """
-    obj.export("verification.txt")
+    obj.export_xyz("verification.txt")
     mean_list = []
     moyx, moyy, moyz = parse_result_file(discretised_file_path)
     moy = moyy
diff --git a/main.py b/main.py
index 3eec56a1bc3b74cc86bba793908ee097cb1f94c8..6a757a5d5c56755c43a20f8c9245181a6ae2bc01 100644
--- a/main.py
+++ b/main.py
@@ -1,6 +1,8 @@
 from utils.math import data_extraction
 from utils.files import output
 from utils.files.input import ScannedObject
+from utils.math.position_manipulation import verticalise
+from utils.graph3D.visplot_render import render3D
 
 def get_raw_data(obj:ScannedObject, ndigits:int)->dict:
     """
@@ -46,7 +48,7 @@ def get_discrete_data(obj:ScannedObject, ndigits:int)->dict:
         - Rayon moyen (en mm)       : list of mean radius values
         - Rayon ecart type (en mm)  : list of radius standard deviation values
     """
-    colones = ["X moy (en mm)", "Y moy (en mm)", "Z moy (en mm)","Rayon moyen (en mm)","Rayon ecart type (en mm)"]
+    colones = ["X moy (en mm)", "Y moy (en mm)", "Z moy (en mm)","Delta z(en mm)","Rayon moyen (en mm)","Rayon ecart type (en mm)"]
     data = {}
     for colone in colones:
         data[colone] = []
@@ -55,15 +57,19 @@ def get_discrete_data(obj:ScannedObject, ndigits:int)->dict:
         data["X moy (en mm)"].append(round(x, ndigits))
         data["Y moy (en mm)"].append(round(y, ndigits))
         data["Z moy (en mm)"].append(round(z, ndigits))
+        first = discrete_values[0]
+        last = discrete_values[-1]
+        data["Delta z(en mm)"].append(round(last[2]-first[2],ndigits))
         data["Rayon moyen (en mm)"].append(round(data_extraction.get_mean_radius(discrete_values), ndigits))
         data["Rayon ecart type (en mm)"].append(round(data_extraction.get_radius_std(discrete_values), ndigits))
     return data
 
-
 def main():
     # Create an object from the given file
-    obj = ScannedObject.from_xyz_file("test_cylindre.xyz",normalised='z')
-    
+    obj = ScannedObject.from_obj_file("datasets/Barette/1 - BARETTE.obj")
+    verticalise(obj)
+    obj.normalise()
+
     # Calculate raw data and save it in a file
     data = get_raw_data(obj, 6)
     output.save_output_file('analyse_brute.txt',
@@ -85,6 +91,7 @@ def main():
                                                 ["X moy (en mm)",
                                                 "Y moy (en mm)",
                                                 "Z moy (en mm)",
+                                                "Delta z(en mm)",
                                                 "Rayon moyen (en mm)",
                                                 "Rayon ecart type (en mm)"] ))
 
diff --git a/requirement.txt b/requirement.txt
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..239f8828713c028c29fcf705bbfebfa77d49d119 100644
--- a/requirement.txt
+++ b/requirement.txt
@@ -0,0 +1,3 @@
+vispy==0.12.2
+PyQt5==5.15.9
+numpy==1.24.2
\ No newline at end of file
diff --git a/utils/files/input.py b/utils/files/input.py
index 0da7d3759c172c8ca6ed25f71421b8acc8deaa0b..94412c881209295a6bbee935cdc30d2e3aad5199 100644
--- a/utils/files/input.py
+++ b/utils/files/input.py
@@ -60,15 +60,15 @@ class ScannedObject:
     #TODO Add and test exemples
     """
     def __init__(self, vertices, faces=None, result_file_path=None):
-        self.vertices = vertices
-        self.faces = faces
+        self.vertices = np.asarray(vertices)
+        self.faces = np.asarray(faces)
         # Deprecated
         self.result_file_path = result_file_path
         self.bruteforce_discretization_result = None
         #
-        self.x = [vertex[0] for vertex in vertices]
-        self.y = [vertex[1] for vertex in vertices]
-        self.z = [vertex[2] for vertex in vertices]
+        self.x = np.asarray([vertex[0] for vertex in vertices])
+        self.y = np.asarray([vertex[1] for vertex in vertices])
+        self.z = np.asarray([vertex[2] for vertex in vertices])
 
 
     @staticmethod
@@ -88,8 +88,10 @@ class ScannedObject:
             data = f.readlines()
             for line in data :
                 if line.startswith('f'):
-                    # Face indices start at 1, not 0
-                    triangles.append([int(line.split()[1])-1, int(line.split()[2])-1, int(line.split()[3])-1])
+                    if "//" in line:
+                        triangles.append([int(line.split()[1].split("//")[0])-1, int(line.split()[2].split("//")[0])-1, int(line.split()[3].split("//")[0])-1])
+                    else:
+                        triangles.append([int(line.split()[1])-1, int(line.split()[2])-1, int(line.split()[3])-1])
                 elif line.startswith('v'):
                     x.append(float(line.split()[1]) * ratio)
                     y.append(float(line.split()[2]) * ratio)
@@ -223,16 +225,56 @@ class ScannedObject:
                 L.append([])
         return L
     
-
-    def get_faces(self)->list:
+    def get_faces(self,resolved:bool = False)->list:
         """
         Get the faces of the object.
         :return: faces
         """
         if self.faces is None:
             raise FacesNotGiven('No faces was given')
+        if resolved:
+            return self.vertices[self.faces]
         return self.faces
 
+    def update_from_faces(self,faces:list):
+        """
+        Update the object from the faces.
+        :param faces: Faces to update the object from
+        """
+        cpt = 0
+        vertex_dict = {}
+        new_vertices = []
+        new_faces = []
+        for face in faces:
+            new_faces.append([])
+            for vertex in face:
+                vertex = tuple(vertex)
+                if vertex not in vertex_dict:
+                    vertex_dict[vertex] = cpt
+                    cpt += 1
+                    new_vertices.append(vertex)
+                new_faces[-1].append(vertex_dict[vertex])
+
+        self.vertices = np.asarray(new_vertices)
+        self.faces = np.asarray(new_faces)
+        self.x = self.vertices[:,0]
+        self.y = self.vertices[:,1]
+        self.z = self.vertices[:,2]
+        self.normalise()
+    
+    def normalise(self, axis:str = 'z'):
+        """
+        Normalise the object.
+        :param axis: Axis to normalise
+        """
+        if 'x' in axis:
+            self.x -= min(self.x)
+        if 'y' in axis:
+            self.y -= min(self.y)
+        if 'z' in axis:
+            self.z -= min(self.z)
+        self.vertices = np.asarray(list(zip(self.x,self.y,self.z))) 
+
     def get_data(self)->dict:
         """
         Get the data of the object.
@@ -256,7 +298,7 @@ class ScannedObject:
         verticies = self.get_vertices(sort=True)
         position = 0
         while position < len(verticies):
-            print(position/len(verticies)*100,end="\r")
+            print('progression :',position/len(verticies)*100,end="\r")
             x = verticies[position][0]
             y = verticies[position][1]
             z = verticies[position][2]
@@ -275,7 +317,7 @@ class ScannedObject:
         self.bruteforce_discretization_result = splitted_data
         return splitted_data
 
-    def export(self, file_path:str,separator:str="\t"):
+    def export_xyz(self, file_path:str,separator:str="\t"):
         """
         Export the object in a file.
         :param file_path: Path of the file
@@ -290,6 +332,25 @@ class ScannedObject:
                 string+=f"{x}{separator}{y}{separator}{z}\n"
         save_output_file(file_path,string)
 
+    def export_obj(self,file_path):
+        """
+        Export the object in a file.
+        :param file_path: Path of the file
+        """
+        string = ''
+        with open(file_path, "w") as f:
+            for vertex in self.get_vertices():
+                x = round(vertex[0], 6)
+                y = round(vertex[1], 6)
+                z = round(vertex[2], 6)
+                string+=f"v {x} {y} {z}\n"
+            for face in self.get_faces():
+                string+="f "
+                for vertex in face:
+                    string+=f"{vertex+1} "
+                string+="\n"
+        save_output_file(file_path,string)
+
 
 def parse_result_file(file_path: str, separator: str = "\t")-> tuple:
     """
diff --git a/utils/3d/mpl_render.py b/utils/graph3D/mpl_render.py
similarity index 72%
rename from utils/3d/mpl_render.py
rename to utils/graph3D/mpl_render.py
index 316474e385fe15609e760f0d57500b044b2d740c..c4757fe36e03a2d20fa218cff61b668960bfcdda 100644
--- a/utils/3d/mpl_render.py
+++ b/utils/graph3D/mpl_render.py
@@ -1,16 +1,17 @@
 from mpl_toolkits.mplot3d.art3d import Poly3DCollection
 import matplotlib.pyplot as plt
 import numpy as np
+from utils.files.input import ScannedObject
 
-def render3D(data:dict):
+def render3D(obj:ScannedObject):
     """
     Render a 3D model using matplotlib poly3dcollection
     :param data: A dict with the vertices and faces
     """
     fig = plt.figure()
     ax = fig.add_subplot(projection='3d')
-    faces = np.array(data['faces'])
-    verts = np.array(list(zip(data['x'], data['y'], data['z'])))
+    faces = np.array(obj.get_faces())
+    verts = np.array(obj.get_vertices())
     mesh = Poly3DCollection(verts[faces], alpha=0.25, edgecolor='none')
     ax.add_collection3d(mesh)
     plt.show()
\ No newline at end of file
diff --git a/utils/graph3D/visplot_render.py b/utils/graph3D/visplot_render.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2cd0f24021018cb49a43ee2424a12b8c7382ec8
--- /dev/null
+++ b/utils/graph3D/visplot_render.py
@@ -0,0 +1,33 @@
+import numpy as np
+from vispy import app, scene
+from vispy.scene.visuals import Mesh
+from vispy.scene import transforms
+from vispy.visuals.filters import ShadingFilter, WireframeFilter
+from utils.files.input import ScannedObject
+
+
+def render3D(obj:ScannedObject):
+    vertices = np.asarray(obj.get_vertices())
+    faces = np.asarray(obj.get_faces())
+    canvas = scene.SceneCanvas(keys='interactive', bgcolor='white')
+    view = canvas.central_widget.add_view()
+    view.camera = 'arcball'
+    view.camera.depth_value = 1e3
+    mesh = Mesh(vertices, faces, color=(.5, .7, .5, 1))
+    view.add(mesh)
+    wireframe_filter = WireframeFilter(width=0)
+    shading_filter = ShadingFilter(shininess=0)
+    mesh.attach(wireframe_filter)
+    mesh.attach(shading_filter)
+
+    def attach_headlight(view):
+        light_dir = (0, 1, 0, 0)
+        shading_filter.light_dir = light_dir[:3]
+        initial_light_dir = view.camera.transform.imap(light_dir)
+        @view.scene.transform.changed.connect
+        def on_transform_change(event):
+            transform = view.camera.transform
+            shading_filter.light_dir = transform.map(initial_light_dir)[:3]
+    attach_headlight(view)
+    canvas.show()
+    app.run()
\ No newline at end of file
diff --git a/utils/math/position_manipulation.py b/utils/math/position_manipulation.py
new file mode 100644
index 0000000000000000000000000000000000000000..53af77c58c384a20cf78291f9daf687b8b451734
--- /dev/null
+++ b/utils/math/position_manipulation.py
@@ -0,0 +1,90 @@
+import numpy as np
+from utils.files.input import ScannedObject
+from utils.math.data_extraction import get_mean
+
+
+def get_mass_properties(obj:ScannedObject):
+    '''
+    Evaluate and return a tuple with the following elements:
+        - the volume
+        - the position of the center of gravity (COG)
+        - the inertia matrix expressed at the COG
+
+    Documentation can be found here:
+    http://www.geometrictools.com/Documentation/PolyhedralMassProperties.pdf
+    '''
+
+    verts = np.asarray(obj.get_vertices())
+    faces = np.asarray(obj.get_faces())
+    faces = verts[faces]
+    x = np.asarray([[point[0] for point in faces] for faces in faces])
+    y = np.asarray([[point[1] for point in faces] for faces in faces])
+    z = np.asarray([[point[2] for point in faces] for faces in faces])
+
+    def subexpression(x):
+        w0, w1, w2 = x[:, 0], x[:, 1], x[:, 2]
+        temp0 = w0 + w1
+        f1 = temp0 + w2
+        temp1 = w0 * w0
+        temp2 = temp1 + w1 * temp0
+        f2 = temp2 + w2 * f1
+        f3 = w0 * temp1 + w1 * temp2 + w2 * f2
+        g0 = f2 + w0 * (f1 + w0)
+        g1 = f2 + w1 * (f1 + w1)
+        g2 = f2 + w2 * (f1 + w2)
+        return f1, f2, f3, g0, g1, g2
+
+    x0, x1, x2 = x[:, 0], x[:, 1], x[:, 2]
+    y0, y1, y2 = y[:, 0], y[:, 1], y[:, 2]
+    z0, z1, z2 = z[:, 0], z[:, 1], z[:, 2]
+    a1, b1, c1 = x1 - x0, y1 - y0, z1 - z0
+    a2, b2, c2 = x2 - x0, y2 - y0, z2 - z0
+    d0, d1, d2 = b1 * c2 - b2 * c1, a2 * c1 - a1 * c2, a1 * b2 - a2 * b1
+
+    f1x, f2x, f3x, g0x, g1x, g2x = subexpression(x)
+    f1y, f2y, f3y, g0y, g1y, g2y = subexpression(y)
+    f1z, f2z, f3z, g0z, g1z, g2z = subexpression(z)
+
+    intg = np.zeros((10))
+    intg[0] = sum(d0 * f1x)
+    intg[1:4] = sum(d0 * f2x), sum(d1 * f2y), sum(d2 * f2z)
+    intg[4:7] = sum(d0 * f3x), sum(d1 * f3y), sum(d2 * f3z)
+    intg[7] = sum(d0 * (y0 * g0x + y1 * g1x + y2 * g2x))
+    intg[8] = sum(d1 * (z0 * g0y + z1 * g1y + z2 * g2y))
+    intg[9] = sum(d2 * (x0 * g0z + x1 * g1z + x2 * g2z))
+    intg /= np.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120])
+    volume = intg[0]
+    cog = intg[1:4] / volume
+    cogsq = cog ** 2
+    inertia = np.zeros((3, 3))
+    inertia[0, 0] = intg[5] + intg[6] - volume * (cogsq[1] + cogsq[2])
+    inertia[1, 1] = intg[4] + intg[6] - volume * (cogsq[2] + cogsq[0])
+    inertia[2, 2] = intg[4] + intg[5] - volume * (cogsq[0] + cogsq[1])
+    inertia[0, 1] = inertia[1, 0] = -(intg[7] - volume * cog[0] * cog[1])
+    inertia[1, 2] = inertia[2, 1] = -(intg[8] - volume * cog[1] * cog[2])
+    inertia[0, 2] = inertia[2, 0] = -(intg[9] - volume * cog[2] * cog[0])
+    return volume, cog, inertia
+
+def verticalise(obj:ScannedObject):
+    cog = get_mass_properties(obj)
+    cog, inertia = get_mass_properties(obj)[1:]
+    [val,vect] = np.linalg.eig(inertia)
+
+    if np.linalg.det(vect) < 0:
+        vect[:,2] = - vect[:,2]
+
+    rot = vect.T
+    faces = obj.get_faces(True)
+
+    theta = -np.pi/2
+
+    R_y = np.array([[np.cos(theta), 0, np.sin(theta)],
+                    [0, 1, 0],
+                    [-np.sin(theta), 0, np.cos(theta)]])
+    
+    for face,_ in enumerate(faces):
+        for vertex in range(3):
+            faces[face][vertex] = rot@(faces[face][vertex] - cog)
+            faces[face][vertex] = R_y@(faces[face][vertex])
+
+    obj.update_from_faces(faces)