The first letter of the title "Analyse morphologique" was not capitalized, which is inconsistent with the usual capitalization rules for titles. 🖼️ chore(README.md): update image and fix typo in rotation instructions The image in the README.md file was updated to reflect the correct rotation instructions. A typo in the rotation instructions was also fixed. 👥 chore(README.md): fix typo in contributor's role A typo in the role of one of the contributors was fixed. 🐛 fix(data_processing.py): add missing half discretisation values to L calculation The calculation of L in the get_advanced_data function of data_processing.py was missing half the discretisation values at the beginning and end of the object. 🐛 fix(input.py): remove print statement A print statement was accidentally left in the from_xyz_file method of the ScannedObject class in input
405 lines
14 KiB
Python
405 lines
14 KiB
Python
"""
|
|
Created on Thu Apr 20 2023
|
|
@name: input.py
|
|
@desc: This module contains the functions to parse the input files, and create a ScannedObject.
|
|
@auth: Djalim Simaila
|
|
@e-mail: djalim.simaila@inrae.fr
|
|
"""
|
|
import numpy as np
|
|
import os
|
|
from utils.files.output import save_output_file
|
|
from utils.settings.SettingManager import SettingManager
|
|
|
|
|
|
class FacesNotGiven(Exception):
|
|
"""
|
|
Exception raised when no faces was given.
|
|
"""
|
|
|
|
class ResultFileNotGiven(Exception):
|
|
"""
|
|
Exception raised when no faces was given.
|
|
"""
|
|
|
|
class InvalidFileFormat(Exception):
|
|
"""
|
|
Exception raised when the file format is not supported.
|
|
"""
|
|
|
|
class ScannedObject:
|
|
"""
|
|
This class is used to manage the data of the 3D object.
|
|
|
|
:param vertices: List of verticesm Ndarray of shape (n,2)
|
|
:param faces: List of faces, Ndarray of shape (n,2)
|
|
|
|
:ivar vertices: List of vertices, Ndarray of shape (n,2)
|
|
:ivar faces: List of faces, Ndarray of shape (n,2)
|
|
:ivar x: List of x values of the vertices
|
|
:ivar y: List of y values of the vertices
|
|
:ivar z: List of z values of the vertices
|
|
|
|
:static method from_xyz_file(): Creates a ScannedObject from a .xyz file
|
|
:static method from_obj_file(): Creates a ScannedObject from a .obj file
|
|
:method get_x(): Returns the x values of the vertices
|
|
:method get_y(): Returns the y values of the vertices
|
|
:method get_z(): Returns the z values of the vertices
|
|
:method get_vertices(): Returns the vertices
|
|
:method get_faces(): Returns the faces
|
|
:method get_discrete_vertices(): Returns the discrete vertices
|
|
:method get_data(): Returns the data
|
|
:method export: Exports the data to a file
|
|
|
|
:raises FacesNotGiven: If no faces was given
|
|
:raises ResultFileNotGiven: If no result file was given
|
|
"""
|
|
def __init__(self, vertices, faces=None):
|
|
self.vertices = np.asarray(vertices)
|
|
self.faces = np.asarray(faces) if faces is not None else None
|
|
self.old_delta = None
|
|
self.old_discrete = None
|
|
self.old_discrete_type = None
|
|
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
|
|
def from_file(file_path:str, ratio:float = 1,normalised:str = '')->'ScannedObject':
|
|
"""
|
|
Create an Object from a file.
|
|
|
|
:param file_path: Path to the file
|
|
:param ratio: Ratio to apply to the vertices
|
|
:param normalised: the axis to normalise
|
|
:return: A ScannedObject
|
|
"""
|
|
if os.path.splitext(file_path)[1].lower() == ".xyz":
|
|
return ScannedObject.from_xyz_file(file_path, ' ',ratio, normalised)
|
|
elif os.path.splitext(file_path)[1].lower() == ".obj":
|
|
return ScannedObject.from_obj_file(file_path, ratio, normalised)
|
|
else:
|
|
raise InvalidFileFormat("The file format is not supported.")
|
|
|
|
|
|
@staticmethod
|
|
def from_obj_file(file_path:str, ratio:float = 1,normalised:str = '')->'ScannedObject':
|
|
"""
|
|
Create an Object from an OBJ file.
|
|
|
|
:param file_path: Path to the OBJ file
|
|
:param ratio: Ratio to apply to the vertices
|
|
:param normalised: the axis to normalise
|
|
:return: A ScannedObject
|
|
"""
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
x, y, z = [], [], []
|
|
triangles = []
|
|
data = f.readlines()
|
|
for line in data :
|
|
if line.startswith('f'):
|
|
# Ignore the normals and textures
|
|
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])
|
|
elif "/" 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])
|
|
|
|
# if it is a vertex, the line starts with a 'v ',
|
|
# taking only 'v' would cause to take the textures coordinates('vt'),
|
|
# vertex normals ('vn') and space vertices ('vp')
|
|
elif line.startswith('v '):
|
|
x.append(float(line.split()[1]) * ratio)
|
|
y.append(float(line.split()[2]) * ratio)
|
|
z.append(float(line.split()[3]) * ratio)
|
|
|
|
if 'x' in normalised:
|
|
xmin = min(x)
|
|
for count,_ in enumerate(x):
|
|
x[count] -= xmin
|
|
|
|
if 'y' in normalised:
|
|
ymin = min(y)
|
|
for count,_ in enumerate(y):
|
|
y[count] -= ymin
|
|
|
|
if 'z' in normalised:
|
|
zmin = min(z)
|
|
for count,_ in enumerate(z):
|
|
z[count] -= zmin
|
|
|
|
return ScannedObject(list(zip(x,y,z)), triangles, )
|
|
|
|
@staticmethod
|
|
def from_xyz_file(file_path:str, delimiter:str = ' ', ratio:float=1, normalised:str = '')->'ScannedObject':
|
|
"""
|
|
Create an Object from an XYZ file.
|
|
|
|
:param file_path: Path to the XYZ file
|
|
:param delimiter: The delimiter used in the xyz file.
|
|
:param normalised: the axis to normalise
|
|
:return: A ScannedObject
|
|
"""
|
|
x , y , z = [], [], []
|
|
with open(file_path, 'r',encoding='utf-8') as f:
|
|
data = f.readlines()
|
|
for line in data:
|
|
x.append(float(line.split(delimiter)[0])* ratio)
|
|
y.append(float(line.split(delimiter)[1])* ratio)
|
|
z.append(float(line.split(delimiter)[2])* ratio)
|
|
|
|
if 'x' in normalised:
|
|
xmin = min(x)
|
|
for count,_ in enumerate(x):
|
|
x[count] -= xmin
|
|
|
|
if 'y' in normalised:
|
|
ymin = min(y)
|
|
for count,_ in enumerate(y):
|
|
y[count] -= ymin
|
|
|
|
if 'z' in normalised:
|
|
zmin = min(z)
|
|
for count,_ in enumerate(z):
|
|
z[count] -= zmin
|
|
return ScannedObject(list(zip(x,y,z)))
|
|
|
|
def get_x(self)->list:
|
|
"""
|
|
Get the x coordinates of the object.
|
|
return: x coordinates
|
|
"""
|
|
return self.x
|
|
|
|
def get_y(self)->list:
|
|
"""
|
|
Get the y coordinates of the object.
|
|
return: y coordinates
|
|
"""
|
|
return self.y
|
|
|
|
def get_z(self)->list:
|
|
"""
|
|
Get the z coordinates of the object.
|
|
return: z coordinates
|
|
"""
|
|
return self.z
|
|
|
|
def get_vertices(self, sort:bool = False)->list:
|
|
"""
|
|
Get the vertices of the object.
|
|
:param sort: Sort the vertices by z coordinate
|
|
|
|
:return: vertices
|
|
"""
|
|
vertices = self.vertices if not sort else sorted(self.vertices, key=lambda vertex: vertex[2])
|
|
return vertices
|
|
|
|
def get_discrete_vertices(self, step:float = 1)->list:
|
|
"""
|
|
Discretize the vertices using the method specified in the settings.
|
|
|
|
:param step: Step of the discretization
|
|
:return: Discretized vertices
|
|
"""
|
|
if SettingManager.get_instance().get_setting("discretisation_method") == "Z0-Zi < DeltaZ":
|
|
return self.get_discrete_vertices_1(step)
|
|
return self.get_discrete_vertices_2(step)
|
|
|
|
def get_discrete_vertices_1(self, step:float = 1)->list:
|
|
"""
|
|
Discretize the vertices of the object using a split method.
|
|
This implementation will split the object at every step interval.
|
|
|
|
:param step: Step of the discretization
|
|
:return: Discretized vertices
|
|
"""
|
|
|
|
# if it has already been calculated with the same method and parametters
|
|
# dont do it again
|
|
if self.old_delta == step and self.old_discrete_type == 0:
|
|
return self.old_discrete
|
|
self.old_delta = step
|
|
self.old_discrete_type = 0
|
|
|
|
current_interval = int(min(self.get_z()))
|
|
splitted_data = [[]]
|
|
for line in self.get_vertices(sort=True):
|
|
# TODO check distance instead of equality
|
|
if line[2] >= current_interval + step:
|
|
splitted_data.append([])
|
|
current_interval += step
|
|
splitted_data[-1].append(line)
|
|
self.old_discrete = splitted_data
|
|
return splitted_data
|
|
|
|
def get_discrete_vertices_2(self, step:float = 1)->list:
|
|
"""
|
|
Discretize the vertices of the object using a length method.
|
|
This implementation will split the object when difference between the
|
|
first and last point of a slice is greater or equal then the step interval.
|
|
|
|
:param step: Step of the discretization
|
|
:return: Discretized vertices
|
|
"""
|
|
|
|
# if it has already been calculated with the same method and parametters
|
|
# dont do it again
|
|
if self.old_delta == step and self.old_discrete_type == 1:
|
|
return self.old_discrete
|
|
self.old_delta = step
|
|
self.old_discrete_type = 1
|
|
|
|
splitted_data = [[]]
|
|
z = min(self.get_z())
|
|
sorted_vertices = self.get_vertices(sort=True)
|
|
for index,_ in enumerate(sorted_vertices):
|
|
splitted_data[-1].append(sorted_vertices[index])
|
|
if sorted_vertices[index][2] - z > step:
|
|
z = sorted_vertices[index+1][2]
|
|
splitted_data.append([])
|
|
self.old_discrete = splitted_data
|
|
return splitted_data
|
|
|
|
def has_faces(self)->bool:
|
|
"""
|
|
Check if the object has faces.
|
|
|
|
:return: True if the object has faces, False otherwise
|
|
"""
|
|
return self.faces is not None
|
|
|
|
def get_faces(self,resolved:bool = False)->list:
|
|
"""
|
|
Get the faces of the object.
|
|
If the faces are not resolved, the faces will be returned as a list of
|
|
indices of the vertices. else, the faces will be returned as a list of
|
|
vertices.
|
|
|
|
:param resolved: If the faces should be resolved
|
|
:return: faces
|
|
"""
|
|
if self.faces is None:
|
|
raise FacesNotGiven('No faces were given')
|
|
if resolved:
|
|
return self.vertices[self.faces]
|
|
return self.faces
|
|
|
|
def update_from_faces(self,faces:list):
|
|
"""
|
|
Update the object from the faces. This will reconstruct the vertices
|
|
from the faces, it is assumed that the faces are given as a list of
|
|
vertices.
|
|
|
|
: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.
|
|
|
|
:return: Data of the object
|
|
"""
|
|
return {'verticies': self.vertices,
|
|
'faces': self.faces,
|
|
'x': self.x,
|
|
'y': self.y,
|
|
'z': self.z
|
|
}
|
|
|
|
def export_xyz(self, file_path:str,separator:str="\t"):
|
|
"""
|
|
Export the object in a file.
|
|
|
|
:param file_path: Path of the file
|
|
:param separator: chars used to separate the values
|
|
"""
|
|
string = ''
|
|
for vertex in self.get_vertices(sort=True):
|
|
x = round(vertex[0], 6)
|
|
y = round(vertex[1], 6)
|
|
z = round(vertex[2], 6)
|
|
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 = ''
|
|
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:
|
|
"""
|
|
This functions parses the discretised output file to retreive the first
|
|
three colunms. It is used to extract the means of x y z for the consistency
|
|
check.
|
|
|
|
:param file_path: Path of the file
|
|
:param separator: chars used to separate the values
|
|
:return: x, y, z
|
|
|
|
:Example:
|
|
>>> parse_result_file("test.txt")
|
|
([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0])
|
|
"""
|
|
lines = []
|
|
x, y, z = [], [], []
|
|
with open(file_path, "r") as f:
|
|
lines = f.readlines()[1:]
|
|
for line in lines:
|
|
line = line.replace(",", ".")
|
|
values = line.split(separator)
|
|
x.append(float(values[0]))
|
|
y.append(float(values[1]))
|
|
z.append(float(values[2]))
|
|
return x, y, z
|