✨ feat(data_processing.py): add raw_data parameter to get_advanced_data function to improve flexibility The L variable was not being computed in the get_advanced_data function, which is necessary for the calculation of the tortuosity. The raw_data parameter was added to the get_advanced_data function to improve flexibility and allow for more advanced calculations to be performed. 🐛 fix(input.py): add filename attribute to ScannedObject class The filename attribute was not being set in the ScannedObject class, which is necessary for generating the output file headers. ✨ feat(output.py): add function to generate headers for output file The generate_headers function was added to generate the headers for the output file. The headers include the filename, date, version, discretisation method, and whether the data was verticalised. 🚀 chore(MainWindow.py): enable process_advanced_data function 🎨 style(MainWindow.ui, UI_MainWindow.py): adjust GUI layout and label text for better user experience The GUI layout has been adjusted to improve the user experience. The window width has been reduced from 1419 to 1336 pixels to better fit the screen. The minimum and maximum sizes of the MainSettings widget have been increased from 518 to 600 pixels to allow for more space for the labels. The labels have been updated to include the units of measurement to improve clarity. 🐛 fix(AdvancedDataWorker.py): add raw_data parameter to __init__ method ✨ feat(AdvancedDataWorker.py): add support for raw data processing in get_advanced_data method The AdvancedDataWorker class now has a raw_data parameter in its __init__ method, which is used in the get_advanced_data method. This allows for raw data processing in addition to the existing discrete data processing. 🐛 fix(DiscreteDataWorker.py): generate headers before formatting data ✨ feat(DiscreteDataWorker.py): add support for generating headers in output file The DiscreteDataWorker class now generates headers for the output file if the add_headers setting is enabled. The headers are generated before formatting the data to ensure that the headers are included in the output file. 🐛 fix(PreProcessWorker.py): set filename of ScannedObject The PreProcessWorker class now sets the filename of the ScannedObject to the basename 🎨 style(UI_Settings.py): reorganize widgets in the settings UI for better readability 🐛 fix(data_extraction.py): fix standard deviation calculation to use unbiased estimator 🔧 chore(SettingManager.py): add "add_headers" setting with default value of True The changes in UI_Settings.py are purely cosmetic and do not affect the functionality of the code. The fix in data_extraction.py corrects the standard deviation calculation to use the unbiased estimator. The addition of the "add_headers" setting in SettingManager.py allows for the addition of headers to output files.
487 lines
17 KiB
Python
487 lines
17 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 struct
|
|
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
|
|
"""
|
|
|
|
authorised_extensions = [".obj",".stl",".xyz"]
|
|
|
|
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.filename = "N/A"
|
|
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)
|
|
elif os.path.splitext(file_path)[1].lower() == ".stl":
|
|
if open(file_path, 'rb').read(5) == b'solid':
|
|
return ScannedObject.from_ascii_stl_file(file_path, ratio, normalised)
|
|
else:
|
|
return ScannedObject.from_binary_stl_file(file_path, ratio, normalised)
|
|
else:
|
|
raise InvalidFileFormat("The file format is not supported.")
|
|
|
|
@staticmethod
|
|
def from_triangles(triangles:list, normalised:str = '')->'ScannedObject':
|
|
"""
|
|
Create an Object from a list of triangles.
|
|
|
|
:param triangles: List of triangles
|
|
:param vertices: List of vertices
|
|
:return: A ScannedObject
|
|
"""
|
|
obj = ScannedObject([], None)
|
|
obj.update_from_faces(triangles)
|
|
obj.normalise(normalised)
|
|
return obj
|
|
|
|
@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)))
|
|
|
|
@staticmethod
|
|
def from_binary_stl_file(file_path:str, ratio:float = 1, normalised:str = '')->'ScannedObject':
|
|
"""
|
|
Create an Object from a binary STL file.
|
|
|
|
:param file_path: Path to the STL file
|
|
:param ratio: Ratio to apply to the vertices
|
|
:param normalised: the axis to normalise
|
|
:return: A ScannedObject
|
|
"""
|
|
with open(file_path, 'rb') as f:
|
|
data = f.read(84)
|
|
n_triangles = int.from_bytes(data[80:84], byteorder='little')
|
|
triangles = []
|
|
|
|
for numero_triangles in range(n_triangles):
|
|
f.read(12)
|
|
current_triangle = []
|
|
for i in range(3):
|
|
vertex = list(struct.unpack('fff', f.read(12)))
|
|
for point in vertex:
|
|
point *= ratio
|
|
current_triangle.append(vertex)
|
|
triangles.append(current_triangle)
|
|
f.read(2)
|
|
|
|
return ScannedObject.from_triangles(triangles, normalised)
|
|
|
|
@staticmethod
|
|
def from_ascii_stl_file(file_path:str, ratio:float = 1, normalised:str = '')->'ScannedObject':
|
|
"""
|
|
Create an Object from an STL file.
|
|
|
|
:param file_path: Path to the STL 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:
|
|
triangles = []
|
|
vertex_buffer = []
|
|
data = f.readlines()
|
|
for line in data :
|
|
if line.startswith('vertex'):
|
|
x = float(line.split()[1]) * ratio
|
|
y = float(line.split()[2]) * ratio
|
|
z = float(line.split()[3]) * ratio
|
|
vertex_buffer.append([x,y,z])
|
|
if len(vertex_buffer) == 3:
|
|
triangles.append(vertex_buffer)
|
|
vertex_buffer = []
|
|
|
|
return ScannedObject.from_triangles(triangles, normalised)
|
|
|
|
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:
|
|
# if you split on the very last point, dont create an empty slice
|
|
if index + 1 < len(sorted_vertices):
|
|
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]
|
|
|
|
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 set_filename(self,filename):
|
|
"""
|
|
"""
|
|
self.filename = filename
|
|
|
|
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
|