🐛 fix(data_test.py): change variable name from teta_diffs to theta_diffs to improve semantics

 feat(data_processing.py): add function to calculate morphological indicators from discrete data
The variable name teta_diffs was changed to theta_diffs to improve semantics. A new function was added to calculate morphological indicators from discrete data. The function calculates Tortuosity, Volume, Surface, Mean radius, Standard deviation of radius, Sigma r tot, MI_l, and MI_p.

🔥 refactor(input.py): remove unused result_file_path parameter from ScannedObject constructor and from_xyz_file method
 feat(input.py): add encoding parameter to open method in from_obj_file and from_xyz_file methods
The result_file_path parameter was not being used in the ScannedObject constructor and from_xyz_file method, so it was removed to simplify the code. The encoding parameter was added to the open method in the from_obj_file and from_xyz_file methods to ensure that the files are opened with the correct encoding.

🐛 fix(output.py): add utf-8 encoding when writing to output file
 feat(output.py): remove unused import and function argument, improve code readability
The fix adds the utf-8 encoding when writing to the output file to avoid encoding issues. The feat removes the unused import and function argument to improve code readability. The function format_data now only takes the necessary arguments and the unused import is removed.

🐛 fix(main_window.py): fix typo in function name
 feat(main_window.py): add persistence to pre-processed data
The fix corrects a typo in the function name get_true_theta_from_x_y. The feat adds persistence to the pre-processed data by storing the raw data, discrete data, and advanced data in the main window. This avoids re-computation of the data when switching between tabs.

🎨 style(MainWindow.ui): add export_advanced_metrics button to the UI
🎨 style(UI_MainWindow.py): add export_advanced_metrics button to the UI
🎨 style(ressources_rc.py): update the resource file
🐛 fix(data_extraction.py): fix typo in function name get_mean_teta to get_mean_theta
The changes add a new button to the UI named "export_advanced_metrics" which allows the user to export variables. The resource file is updated to reflect the changes. The typo in the function name get_mean_teta is fixed to get_mean_theta.
This commit is contained in:
Djalim Simaila 2023-05-09 10:56:43 +02:00
parent 4402085620
commit fb2bb6e9ce
14 changed files with 219 additions and 137 deletions

View File

@ -104,7 +104,7 @@ def check_raw_data(expected_file, actual_file, ndigits=7, eps=0.00001, silent: b
minimal_output_data = ""
output_data = ""
expected = []
list_x_diffs, list_y_diffs, list_z_diffs, list_teta_diffs, list_radius_diffs, list_xmoy_diffs, list_ymoy_diffs = [], [], [], [], [], [], []
list_x_diffs, list_y_diffs, list_z_diffs, list_theta_diffs, list_radius_diffs, list_xmoy_diffs, list_ymoy_diffs = [], [], [], [], [], [], []
with open(expected_file, "r") as f:
expected = f.readlines()[1:]
@ -124,7 +124,7 @@ def check_raw_data(expected_file, actual_file, ndigits=7, eps=0.00001, silent: b
list_x_diffs.append(x_diff)
list_y_diffs.append(y_diff)
list_z_diffs.append(z_diff)
list_teta_diffs.append(t_diff)
list_theta_diffs.append(t_diff)
list_radius_diffs.append(r_diff)
list_xmoy_diffs.append(xmoy_diff)
list_ymoy_diffs.append(ymoy_diff)
@ -141,7 +141,7 @@ def check_raw_data(expected_file, actual_file, ndigits=7, eps=0.00001, silent: b
if z_diff > eps:
minimal_output_data += f"{i} : Z diff {z_diff}\n"
if t_diff > eps:
minimal_output_data += f"{i} : teta diff{ t_diff}\n"
minimal_output_data += f"{i} : theta diff{ t_diff}\n"
if r_diff > eps:
minimal_output_data += f"{i} : R diff {r_diff}\n"
if xmoy_diff > eps:
@ -158,7 +158,7 @@ def check_raw_data(expected_file, actual_file, ndigits=7, eps=0.00001, silent: b
sum_x_diff = round(sum(list_x_diffs)/len(expected), ndigits)
sum_y_diff = round(sum(list_y_diffs)/len(expected), ndigits)
sum_z_diff = round(sum(list_z_diffs)/len(expected), ndigits)
sum_teta_diff = round(sum(list_teta_diffs)/len(expected), ndigits)
sum_theta_diff = round(sum(list_theta_diffs)/len(expected), ndigits)
sum_radius_diff = round(sum(list_radius_diffs)/len(expected), ndigits)
sum_xmoy_diff = round(sum(list_xmoy_diffs)/len(expected), ndigits)
sum_ymoy_diffs = round(sum(list_ymoy_diffs)/len(expected), ndigits)
@ -167,12 +167,12 @@ def check_raw_data(expected_file, actual_file, ndigits=7, eps=0.00001, silent: b
print(f"diff moyenne de x : {sum_x_diff}")
print(f"diff moyenne de y : {sum_y_diff}")
print(f"diff moyenne de z : {sum_z_diff}")
print(f"diff moyenne de t : {sum_teta_diff}")
print(f"diff moyenne de t : {sum_theta_diff}")
print(f"diff moyenne de r : {sum_radius_diff}")
print(f"diff moyenne de xmoy : {sum_xmoy_diff}")
print(f"diff moyenne de ymoy : {sum_ymoy_diffs}")
print(
f"diff gloabale des fichiers : {(sum_x_diff+sum_y_diff+sum_z_diff+sum_teta_diff+sum_radius_diff+sum_xmoy_diff+sum_ymoy_diffs)/7}")
f"diff gloabale des fichiers : {(sum_x_diff+sum_y_diff+sum_z_diff+sum_theta_diff+sum_radius_diff+sum_xmoy_diff+sum_ymoy_diffs)/7}")
print(f"Voir {output_file} pour plus de détails")
print(f"Voir {minimal_output_file} pour les différences significatives")
print("_"*80)
@ -180,7 +180,7 @@ def check_raw_data(expected_file, actual_file, ndigits=7, eps=0.00001, silent: b
return {"x": list_x_diffs,
"y": list_y_diffs,
"z": list_z_diffs,
"teta": list_teta_diffs,
"theta": list_theta_diffs,
"radius": list_radius_diffs,
"xmoy": list_xmoy_diffs,
"ymoy": list_ymoy_diffs}
@ -236,7 +236,7 @@ def test_get_raw_data(obj: ScannedObject, expected_file: str, ndigits: int = 6,
print()
print("Time to calculate raw data: ", time.time() - now)
output.save_output_file('tmp_analyse_brute.txt', output.format_data(data, '\t', [
"X (en mm)", "Y (en mm)", "Z (en mm)", "teta (en rad)", "rayon (en mm)", "Xi-Xmoy", "Yi-Ymoy"]))
"X (en mm)", "Y (en mm)", "Z (en mm)", "theta (en rad)", "rayon (en mm)", "Xi-Xmoy", "Yi-Ymoy"]))
check_raw_data(expected_file, 'tmp_analyse_brute.txt', ndigits, eps)
os.remove('tmp_analyse_brute.txt')

View File

@ -8,7 +8,6 @@ Created on Mon Apr 24 2023
from utils.math import data_extraction as de
import numpy as np
from utils.files.input import ScannedObject
from utils.settings.SettingManager import SettingManager
def progressbar_placeholder(percent:int):
"""
@ -27,14 +26,20 @@ def get_raw_data(obj:ScannedObject, ndigits:int,delta_z:float=1,update_progress_
- X (en mm) : list of x values
- Y (en mm) : list of y values
- Z (en mm) : list of z values
- teta (en rad) : list of teta values
- theta (en rad) : list of theta values
- rayon (en mm) : list of radius values
- Xi-Xmoy : list of Xi-Xmoy values
- Yi-Ymoy : list of Yi-Ymoy values
"""
# Create the data dict
colones = ["X (en mm)", "Y (en mm)", "Z (en mm)", "teta (en rad)", "rayon (en mm)","Xi-Xmoy","Yi-Ymoy"]
colones = ["X (en mm)",
"Y (en mm)",
"Z (en mm)",
"theta (en rad)",
"rayon (en mm)",
"Xi-Xmoy",
"Yi-Ymoy"]
data = {}
for colone in colones:
data[colone] = []
@ -50,7 +55,7 @@ def get_raw_data(obj:ScannedObject, ndigits:int,delta_z:float=1,update_progress_
data["X (en mm)"].append(round(x, ndigits))
data["Y (en mm)"].append(round(y, ndigits))
data["Z (en mm)"].append(round(z, ndigits))
data["teta (en rad)"].append(round(de.get_teta_from_x_y(x,y,mean_x,mean_y), ndigits))
data["theta (en rad)"].append(round(de.get_theta_from_x_y(x,y,mean_x,mean_y), ndigits))
data["rayon (en mm)"].append(round(de.get_radius_from_x_y(x,y,mean_x,mean_y), ndigits))
data["Xi-Xmoy"].append(round(x-mean_x, ndigits))
data["Yi-Ymoy"].append(round(y-mean_y, ndigits))
@ -73,8 +78,14 @@ def get_discrete_data(obj:ScannedObject, ndigits:int, delta_z:float=1, update_pr
- Rayon moyen (en mm) : list of mean radius values
- Rayon ecart type (en mm) : list of radius standard deviation values
"""
# Create the data dict
colones = ["X moy (en mm)", "Y moy (en mm)", "Z moy (en mm)","Discretisation(en mm)","Rayon moyen (en mm)","Rayon ecart type (en mm)"]
colones = ["X moy (en mm)",
"Y moy (en mm)",
"Z moy (en mm)",
"Discretisation(en mm)",
"Rayon moyen (en mm)",
"Rayon ecart type (en mm)"]
data = {}
for colone in colones:
data[colone] = []
@ -96,25 +107,49 @@ def get_discrete_data(obj:ScannedObject, ndigits:int, delta_z:float=1, update_pr
progress += 1
return data
def get_advanced_data(discrete_data:dict, update_progress_bar= progressbar_placeholder):
def get_advanced_data(discrete_data:dict, update_progress_bar= progressbar_placeholder)->dict:
"""
Calculates morphological indicators from the given discrete data
:param discrete_data: dict(str:list) with the following keys:
- X moy (en mm) : list of x mean values
- Y moy (en mm) : list of y mean values
- Z moy (en mm) : list of z mean values
- Rayon moyen (en mm) : list of mean radius values
- Rayon ecart type (en mm) : list of radius standard deviation values
:param update_progress_bar: Function to update the progress bar
:return: dict with the following keys:
- Tortuosite
- Volume
- Surface
- Moyenne des rayons moyens
- Ecart-type des rayons moyens
- Sigma r tot
- MI_l
- MI_p
"""
# Tortusity
l = 0
L = 0
vertices = list(zip(discrete_data["X moy (en mm)"], discrete_data["Y moy (en mm)"], discrete_data["Z moy (en mm)"]))
for i in range(len(vertices)-1):
l += de.get_distance_between_two_vertices(vertices[i], vertices[i+1])
vertices = list(zip(discrete_data["X moy (en mm)"],
discrete_data["Y moy (en mm)"],
discrete_data["Z moy (en mm)"]))
for index in range(len(vertices)-1):
l += de.get_distance_between_two_vertices(vertices[index], vertices[index+1])
L = de.get_distance_between_two_vertices(vertices[0], vertices[-1])
T = l/L
update_progress_bar(10)
# Volume and surface
H = discrete_data["Z moy (en mm)"][-1] - discrete_data["Z moy (en mm)"][0]
R = de.get_mean([np.power(r,2) for r in discrete_data["Rayon moyen (en mm)"]])
V = np.pi * R * H
S = 2 * np.pi * R * H
update_progress_bar(30)
#
# Morphological indicators
R_mean = de.get_mean(discrete_data["Rayon moyen (en mm)"])
R_mean_std = de.get_standard_deviation(discrete_data["Rayon moyen (en mm)"])
mean_sigma_r_squared = de.get_mean([np.power(r,2) for r in discrete_data["Rayon ecart type (en mm)"]])

View File

@ -26,11 +26,9 @@ class ScannedObject:
:param vertices: List of verticesm Ndarray of shape (n,2)
:param faces: List of faces, Ndarray of shape (n,2)
:param result_file_path: Path to the result file (deprecated, used for the bruteforce discretization)
:ivar vertices: List of vertices, Ndarray of shape (n,2)
:ivar faces: List of faces, Ndarray of shape (n,2)
:ivar result_file_path: Path to the result file (deprecated, used for the bruteforce discretization)
: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
@ -46,31 +44,10 @@ class ScannedObject:
: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
:Example:
>>> from utils.files.input import ScannedObject
>>> vertices = [(0,0,0), (1,0,0), (1,1,0), (0,1,0), (0,0,1), (1,0,1), (1,1,1), (0,1,1)]
>>> faces = [(0,1,2), (0,2,3), (4,5,6), (4,6,7), (0,1,5), (0,4,5), (1,2,6), (1,5,6), (2,3,7), (2,6,7), (3,0,4), (3,7,4)]
>>> obj = ScannedObject(vertices, faces)
>>> obj.get_x()
[0, 1, 1, 0, 0, 1, 1, 0]
>>> obj.get_y()
[0, 0, 1, 1, 0, 0, 1, 1]
>>> obj.get_z()
[0, 0, 0, 0, 1, 1, 1, 1]
>>> obj.get_vertices()
[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0), (0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)]
>>> obj.get_faces()
[(0, 1, 2), (0, 2, 3), (4, 5, 6), (4, 6, 7), (0, 1, 5), (0, 4, 5), (1, 2, 6), (1, 5, 6), (2, 3, 7), (2, 6, 7), (3, 0, 4), (3, 7, 4)]
>>> obj.get_discrete_vertices()
[[(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]],[ (0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)]]
"""
def __init__(self, vertices, faces=None, result_file_path=None):
def __init__(self, vertices, faces=None):
self.vertices = np.asarray(vertices)
self.faces = np.asarray(faces)
self.old_delta = None
@ -82,17 +59,16 @@ class ScannedObject:
@staticmethod
def from_obj_file(file_path:str, result_file_path:str = None, ratio:float = 1,normalised:str = '')->'ScannedObject':
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 result_file_path: Path to the result file
:param ratio: Ratio to apply to the vertices
:param normalised: the axis to normalise
:return: A ScannedObject
"""
with open(file_path, 'r') as f:
with open(file_path, 'r', encoding='utf-8') as f:
x, y, z = [], [], []
triangles = []
data = f.readlines()
@ -105,6 +81,10 @@ class ScannedObject:
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)
@ -112,55 +92,54 @@ class ScannedObject:
if 'x' in normalised:
xmin = min(x)
for count, value in enumerate(x):
for count,_ in enumerate(x):
x[count] -= xmin
if 'y' in normalised:
ymin = min(y)
for count, value in enumerate(y):
for count,_ in enumerate(y):
y[count] -= ymin
if 'z' in normalised:
zmin = min(z)
for count, value in enumerate(z):
for count,_ in enumerate(z):
z[count] -= zmin
return ScannedObject(list(zip(x,y,z)), triangles, result_file_path)
return ScannedObject(list(zip(x,y,z)), triangles, )
@staticmethod
def from_xyz_file(file_path: str, result_file_path:str = None, delimiter: str = ' ',normalised:str = '')->'ScannedObject':
def from_xyz_file(file_path:str, delimiter:str = ' ', normalised:str = '')->'ScannedObject':
"""
Create an Object from an XYZ file.
:param file_path: Path to the XYZ file
:param result_file_path: Path to the result 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') as f:
with open(file_path, 'r',encoding='utf-8') as f:
data = f.readlines()
for line in data:
x.append(float(line.split(delimiter)[0]))
y.append(float(line.split(delimiter)[1]))
z.append(float(line.split(delimiter)[2]))
if 'x' in normalised:
if 'x' in normalised:
xmin = min(x)
for count, value in enumerate(x):
for count,_ in enumerate(x):
x[count] -= xmin
if 'y' in normalised:
if 'y' in normalised:
ymin = min(y)
for count, value in enumerate(y):
for count,_ in enumerate(y):
y[count] -= ymin
if 'z' in normalised:
if 'z' in normalised:
zmin = min(z)
for count, value in enumerate(z):
for count,_ in enumerate(z):
z[count] -= zmin
return ScannedObject(list(zip(x,y,z)), result_file_path=result_file_path)
return ScannedObject(list(zip(x,y,z)))
def get_x(self)->list:
"""
@ -194,6 +173,12 @@ class ScannedObject:
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)
@ -206,10 +191,14 @@ class ScannedObject:
: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):
@ -223,43 +212,53 @@ class ScannedObject:
def get_discrete_vertices_2(self, step:float = 1)->list:
"""
Discretize the vertices of the object using a lenght method.
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
L = [[]]
splitted_data = [[]]
z = min(self.get_z())
sorted_vertices = self.get_vertices(sort=True)
for index,_ in enumerate(sorted_vertices):
L[-1].append(sorted_vertices[index])
splitted_data[-1].append(sorted_vertices[index])
if sorted_vertices[index][2] - z > step:
z = sorted_vertices[index+1][2]
L.append([])
self.old_discrete = L
return L
splitted_data.append([])
self.old_discrete = splitted_data
return splitted_data
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 was given')
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.
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
"""
@ -304,7 +303,12 @@ class ScannedObject:
:return: Data of the object
"""
return {'verticies': self.vertices, 'faces': self.faces, 'x': self.x, 'y': self.y, 'z': self.z}
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"):
"""
@ -314,12 +318,11 @@ class ScannedObject:
:param separator: chars used to separate the values
"""
string = ''
with open(file_path, "w") as f:
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"
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):
@ -329,17 +332,16 @@ class ScannedObject:
: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"
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)
@ -347,7 +349,7 @@ 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 and the bruteforce_discretisation
check.
:param file_path: Path of the file
:param separator: chars used to separate the values

View File

@ -5,10 +5,8 @@ Created on Mon Apr 17 2023
@auth: Djalim Simaila
@e-mail: djalim.simaila@inrae.fr
"""
from utils.settings.SettingManager import SettingManager
def format_data(data:dict, separator:str, selected_columns:list = None) -> str:
"""
Format the data to be saved in the output file.
@ -41,7 +39,7 @@ def format_data(data:dict, separator:str, selected_columns:list = None) -> str:
# Write the columns headers
for column_name in selected_columns:
if is_prettier:
output += column_name.ljust(len(column_name) if len(column_name) > 8 else 9 ) + separator
output += column_name.ljust(len(column_name) if len(column_name) > 8 else 9 ) + separator
else:
output += column_name + separator
output += '\n'
@ -62,5 +60,5 @@ def save_output_file(output_file:str, content:str):
:param output_file: Path to the output file
:param content: Content of the output file
"""
with open(output_file, 'w') as f:
f.write(content)
with open(output_file, 'w',encoding='utf-8') as file:
file.write(content)

View File

@ -1,7 +1,8 @@
"""
Created on Fri Apr 21 2023
@name: mpl_render.py
@desc: A module to render a 2D data using matplotlib
@desc: A module to render a 2D data using matplotlib,
thoses functions cant be integrated into pyqt gui yet
@auth: Djalim Simaila
@e-mail: djalim.simaila@inrae.fr
"""

View File

@ -16,7 +16,7 @@ def render2D(values:list,title:str,xlabel="",ylabel="",show:bool=True)->vp.Fig|N
:param title: Title of the plot
:param xlabel: Label of the x axis
:param ylabel: Label of the y axis
:param show: If True, show the plot, else return the canvas
:param show: If True, show the plot in its own window, else return the canvas
"""
fig = vp.Fig(size=(600, 500), show=False)
plotwidget = fig[0, 0]
@ -41,7 +41,7 @@ def cross_section(x_values:list, y_values:list,title:str,xlabel="",ylabel="",sho
:param title: Title of the plot
:param xlabel: Label of the x axis
:param ylabel: Label of the y axis
:param show: If True, show the plot, else return the canvas
:param show: If True, show the plot in its own window, else return the canvas
"""
color = (0.3, 0.5, 0.8,.8)
fig = vp.Fig(show=False)

View File

@ -1,7 +1,8 @@
"""
Created on Fri Apr 21 2023
@name: mpl_render.py
@desc: A module to render a 3D data using matplotlib
@desc: A module to render a 3D data using matplotlib,
thoses functions cant be integrated into pyqt gui yet
@auth: Djalim Simaila
@e-mail: djalim.simaila@inrae.fr
"""

View File

@ -10,6 +10,8 @@ from utils.gui.pyqt.about.UI_AboutThis import Ui_AboutThis
from utils.settings.SettingManager import SettingManager
class AboutThis(QtWidgets.QMainWindow,Ui_AboutThis):
"""
"""
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setupUi(self)

View File

@ -30,8 +30,12 @@ class ErrorPopup(object):
self.button_callback = button_callback
self.details = details
def show_popup(self):
"""
Show the error popup with the specified error message.
If a button label and a button callback are specified, the popup will
have a button with the specified label and the specified callback.
"""
msg = QMessageBox()
msg.setWindowTitle("Erreur")
msg.setText("Erreur: " + self.error_text)

View File

@ -13,7 +13,7 @@ from utils.files.input import ScannedObject
from utils.gui.pyqt.main_window.Workers.AdvancedDataWorker import AdvancedDataWorker
from utils.gui.pyqt.settings.Settings import Settings
from utils.gui.pyqt.about.AboutThis import AboutThis
from utils.math.data_extraction import get_mean, get_radius_from_x_y, get_true_teta_from_x_y
from utils.math.data_extraction import get_mean, get_radius_from_x_y, get_true_theta_from_x_y
from utils.settings.SettingManager import SettingManager
from utils.graph2D.visplot_render import cross_section, render2D
from utils.graph3D.visplot_render import render3D
@ -25,11 +25,17 @@ from utils.gui.pyqt.error_popup.ErrorPopup import ErrorPopup
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
Main window of the application, it contains all the UI elements
"""
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
# Persist variable to avoid re-computation
self.obj = None
self.raw_data= None
self.discrete_data = None
self.advanced_data = None
# Retrieve the UI
self.setupUi(self)
# Setup buttons listeners
@ -42,10 +48,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.actionQuitter.triggered.connect(self.close)
self.actionQ_propos_de_ce_logiciel.triggered.connect(self.show_about)
# add default layer combobox value and setup the listenerr when index change
self.layer_ComboBox.addItems(['Aucune couche'])
self.layer_ComboBox.currentIndexChanged.connect(self.layer_changed)
self.graphType = [
# Prepare available graph type list for the slots combobox
self.graph_type = [
"Aucun",
"Mesh3D",
"Coupe XZ",
@ -55,15 +63,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"Coupe de la couche",
"Difference entre le rayon de chaque points et le rayon moyen de la couche"
]
self.obj = None
self.raw_data= None
self.discrete_data = None
self.advanced_data = None
self.completed_tasks = 0
self.total_tasks = 3
# put all slots combo boxes in a list for conveniance
self.combo_boxes = [
self.slot0ComboBox,
self.slot1ComboBox,
@ -78,10 +79,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.slot10ComboBox
]
# Setup all combo boxes with values and listener
for combo_box in self.combo_boxes:
combo_box.addItems(self.graphType)
combo_box.addItems(self.graph_type)
combo_box.currentIndexChanged.connect(self.graph_type_changed)
# put all slots in a list for conveniance and store their state in a string
self.slots = [
[self.slot0,"Aucun"],
[self.slot1,"Aucun"],
@ -96,19 +99,32 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
[self.slot10,"Aucun"]
]
# Retrieve the slot previous value from the settings
for slot_nb,slot in enumerate(self.slots):
slot[1] = SettingManager.get_instance().get_last_graph(slot_nb)
self.combo_boxes[slot_nb].setCurrentText(slot[1])
# Graph number indicator
self.graph_nb =0
self.graph_type_changed()
# Construct sub windows
self.settings_window = Settings()
self.about_window = AboutThis()
# Variable to check if parametters has changed to avoid re-computation
self.has_changed = True
self.old_discretisation_value = None
# Task counter and task number, used to know when the analysis
# is considered finished
# Current tasks are
# - process raw data
# - process discrete data
# - process morphological indicators (advanced data)
self.completed_tasks = 0
self.total_tasks = 3
###############################################################################
# #
# #
@ -118,7 +134,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
###############################################################################
def select_file(self):
"""
Open a file dialog to select the input file
Open a file dialog to select the input file,
if the folder_path is empty, it fills it with the files folder.
"""
file = QFileDialog.getOpenFileName()[0]
self.input_file_path.setPlainText(file)
@ -136,19 +153,30 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def check_input_file(self):
"""
Check if the input file is valid
Check if the input file is valid by checking:
- if it exists
- if its extension is .obj
"""
if not os.path.isfile(self.input_file_path.toPlainText()):
ErrorPopup("Fichier d'entrée invalide",button_label="Choisir un fichier d'entrée",button_callback=self.select_file).show_popup()
if not os.path.isfile(self.input_file_path.toPlainText()) :
ErrorPopup("Fichier d'entrée invalide: Aucun fichier selectionné, ou le fichier n'existe pas",
button_label="Choisir un fichier d'entrée",
button_callback=self.select_file).show_popup()
return False
if os.path.splitext(self.input_file_path.toPlainText())[1].lower() != ".obj":
ErrorPopup("Fichier d'entrée invalide: l'extension du fichier est incorrecte ",
button_label="Choisir un fichier d'entrée",
button_callback=self.select_file).show_popup()
return False
return True
def check_output_folder(self):
"""
Check if the output folder is valid
Check if the output folder is valid by cheking if it exists.
"""
if not os.path.isdir(self.output_folder_path.toPlainText()):
ErrorPopup("Dossier de sortie invalide",button_label="Choisir un dossier de sortie",button_callback=self.select_folder).show_popup()
ErrorPopup("Dossier de sortie invalide",
button_label="Choisir un dossier de sortie",
button_callback=self.select_folder).show_popup()
return False
return True
@ -284,13 +312,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def set_obj(self,obj:ScannedObject):
"""
Set the obj to the main window
Persists the pre-processed obj
"""
self.obj = obj
def set_discrete_data(self,discrete_data:dict):
"""
Set the discrete data to the main window
Persists the calculated discrete data
"""
self.discrete_data = discrete_data
layer = [str(i) for i in range(len(discrete_data["X moy (en mm)"]))]
@ -302,13 +330,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def set_raw_data(self,raw_data:dict):
"""
Set the raw data to the main window
Persists the calculated raw data
"""
self.raw_data = raw_data
def set_advanced_data(self,advanced_data:dict):
"""
Set the advanced data to the main window
Persists the calculated raw data and show the values
"""
self.advanced_data = advanced_data
self.tortuosity.setValue(advanced_data["Tortuosité"])
@ -433,15 +461,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
y_mean = discrete_data["Y moy (en mm)"][layer_nb]
r_mean = discrete_data["Rayon moyen (en mm)"][layer_nb]
rs = [get_radius_from_x_y(x,y,x_mean,y_mean) for x,y,z in vertices]
θs = [get_true_teta_from_x_y(x,y,x_mean,y_mean) for x,y,z in vertices]
θs = [get_true_theta_from_x_y(x,y,x_mean,y_mean) for x,y,z in vertices]
min_θ = min(θs)
current_slot.addWidget(cross_section([θ-min_θ for θ in θs],
[r-r_mean for r in rs],
"Difference entre le rayon de chaque points et le rayon moyen de la couche",
"Theta en rad",
"R - <R>",
"r - <r> en mm",
False).native)
self.set_status("Graphs rendered!")
self.set_status("Graphs rendered!")
def clear_slot(self,slot):

View File

@ -567,6 +567,13 @@
</property>
</widget>
</item>
<item row="16" column="0">
<widget class="QPushButton" name="export_advanced_metrics">
<property name="text">
<string>Exporter les variables</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View File

@ -277,6 +277,9 @@ class Ui_MainWindow(object):
self.line.setFrameShadow(QtWidgets.QFrame.Sunken)
self.line.setObjectName("line")
self.formLayout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.line)
self.export_advanced_metrics = QtWidgets.QPushButton(self.tab_6)
self.export_advanced_metrics.setObjectName("export_advanced_metrics")
self.formLayout.setWidget(16, QtWidgets.QFormLayout.LabelRole, self.export_advanced_metrics)
self.gridLayout_15.addLayout(self.formLayout, 0, 0, 1, 1)
self.SettingsTab.addTab(self.tab_6, "")
self.gridLayout_2.addWidget(self.SettingsTab, 1, 0, 1, 1)
@ -519,6 +522,7 @@ class Ui_MainWindow(object):
self.sigma_r_tot_label.setText(_translate("MainWindow", "σ<R>tot"))
self.MI_p_label.setText(_translate("MainWindow", "MI_p"))
self.MI_l_label.setText(_translate("MainWindow", "MI_l"))
self.export_advanced_metrics.setText(_translate("MainWindow", "Exporter les variables"))
self.SettingsTab.setTabText(self.SettingsTab.indexOf(self.tab_6), _translate("MainWindow", "Valeurs"))
self.GraphTabs.setTabText(self.GraphTabs.indexOf(self.tab), _translate("MainWindow", "1"))
self.GraphTabs.setTabText(self.GraphTabs.indexOf(self.tab_2), _translate("MainWindow", "2"))

View File

@ -751,7 +751,7 @@ qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x87\xdb\xa2\x85\x9e\
\x00\x00\x01\x87\xdc\x71\xcf\xb8\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]

View File

@ -101,51 +101,51 @@ def get_radius_std(discrete_values:list):
radius.append(get_radius_from_x_y(x,y,x_mean,y_mean))
return get_standard_deviation(radius)
def get_mean_teta(discrete_values:list):
def get_mean_theta(discrete_values:list):
"""
Get the mean of the teta in the discrete range.
Get the mean of the theta in the discrete range.
:param discrete_values: discrete values
:return: mean of the teta in the discrete range
:return: mean of the theta in the discrete range
:Example:
>>> get_mean_teta([(1,2,3),(4,5,6),(7,8,9)])
>>> get_mean_theta([(1,2,3),(4,5,6),(7,8,9)])
0.7853981633974483
"""
x_mean, y_mean, z_mean = get_x_y_z_mean(discrete_values)
teta = []
theta = []
for x,y,z in discrete_values:
teta.append(get_teta_from_x_y(x,y,x_mean,y_mean))
return get_mean(teta)
theta.append(get_theta_from_x_y(x,y,x_mean,y_mean))
return get_mean(theta)
def get_teta_from_x_y(xi:float, yi:float, x_mean:float, y_mean:float):
def get_theta_from_x_y(xi:float, yi:float, x_mean:float, y_mean:float):
"""
Get the teta from the x and y coordinates.
Get the theta from the x and y coordinates.
:param xi: x coordinate
:param yi: y coordinate
:param x_mean: mean of x coordinates in the discrete range
:param y_mean: mean of y coordinates in the discrete range
:return: teta for this point
:return: theta for this point
:Example:
>>> get_teta_from_x_y(1,2,3,4)
>>> get_theta_from_x_y(1,2,3,4)
0.7853981633974483
"""
return math.atan((yi - y_mean)/(xi-x_mean))
def get_true_teta_from_x_y(xi:float, yi:float, x_mean:float, y_mean:float):
def get_true_theta_from_x_y(xi:float, yi:float, x_mean:float, y_mean:float):
"""
Get the teta from the x and y coordinates.
Get the theta from the x and y coordinates.
:param xi: x coordinate
:param yi: y coordinate
:param x_mean: mean of x coordinates in the discrete range
:param y_mean: mean of y coordinates in the discrete range
:return: teta for this point
:return: theta for this point
:Example:
>>> get_true_teta_from_x_y(1,2,3,4)
>>> get_true_theta_from_x_y(1,2,3,4)
0.7853981633974483
"""
return math.atan2((xi-x_mean),(yi-y_mean))