diff --git a/.gitignore b/.gitignore index 577903d..91532e2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /electron-3d-app/ /express-app/ /react-app/ -/.vscode/ \ No newline at end of file +/.vscode/ +/3D_app.old/ \ No newline at end of file diff --git a/3D_app/.gitignore b/3D_app/.gitignore index 029519e..7ee7d8f 100644 --- a/3D_app/.gitignore +++ b/3D_app/.gitignore @@ -1,2 +1,5 @@ __pycache__/ -*.py[cod] \ No newline at end of file +*.py[cod] + +/cache/ +/Dataset/saves/ \ No newline at end of file diff --git a/3D_app/main.py b/3D_app/main.py index 39c19c7..e744d74 100644 --- a/3D_app/main.py +++ b/3D_app/main.py @@ -1,7 +1,10 @@ import dash -from dash import dcc, html -from dash.dependencies import Input, Output +from dash import dcc, html, ALL, DiskcacheManager +from dash.dependencies import Input, Output, State import dash_bootstrap_components as dbc +from os import listdir, mkdir +from os.path import isfile, join +import diskcache # on crée l'application @@ -11,8 +14,19 @@ app = dash.Dash( use_pages=True, ) +cache = diskcache.Cache("./cache") +background_callback_manager = DiskcacheManager(cache) + print("Reloading...") +file_to_load = None + +if "saves" not in listdir("Dataset"): + print("No saves folder, creating one for you...") + mkdir("Dataset/saves") + +files = listdir("Dataset/saves") + # on lit le fichier modal.md pour le tuto with open("assets/modal.md", "r") as f: @@ -35,7 +49,13 @@ modal_settings = dbc.Modal( [ dbc.Switch( id="use-real-values", - label="Use real values", + label="Use real values (not yet implemented)", + value=False, + className="me-2", + ), + dbc.Switch( + id="apply-filters-everywhere", + label="Apply filters everywhere", value=False, className="me-2", ), @@ -93,6 +113,64 @@ modal_settings = dbc.Modal( size="lg", ) +modal_open = dbc.Modal( + [ + dbc.ModalHeader("Open a file"), + dbc.ModalBody( + [ + dbc.ListGroup( + [ + dbc.ListGroupItem( + f"{file}", + action=True, + style={"cursor": "pointer"}, + id={"type": "file-item", "index": i}, + ) + for i, file in enumerate(files) + if isfile(join("Dataset/saves", file)) + ], + id="file-list", + ) + ] + ), + dbc.ModalFooter( + [ + dbc.Button("Refresh", id="open-refresh", color="success"), + dbc.Button("Close", id="open-close", className="open-bn"), + ] + ), + ], + id="open-modal", + size="lg", +) + +modal_save = dbc.Modal( + [ + dbc.ModalHeader("Save a file"), + dbc.ModalBody( + [ + dbc.Input(id="save-input", placeholder="Filename"), + dbc.RadioItems( + id="save-format", + options=[ + {"label": "Save raw dataset", "value": "raw"}, + {"label": "Save filtered dataset", "value": "filt"}, + ], + value="raw", + inline=True, + ), + ] + ), + dbc.ModalFooter( + [ + dbc.Button("Save", id="save-save", color="success"), + dbc.Button("Close", id="save-close", className="save-bn"), + ] + ), + ], + id="save-modal", +) + # on défini les boutons de la navbar button_gh = dbc.Button( "Learn more", @@ -116,6 +194,22 @@ button_settings = dbc.Button( outline=True, color="success", id="settings-open", + style={"textTransform": "none", "marginRight": "25px"}, +) + +button_open = dbc.Button( + "Open", + outline=True, + color="primary", + id="open-button", + style={"textTransform": "none", "marginRight": "10px"}, +) + +button_save = dbc.Button( + "Save", + outline=True, + color="success", + id="save-button", style={"textTransform": "none"}, ) @@ -202,6 +296,8 @@ nav_bar = dbc.Navbar( dbc.NavItem(button_howto), dbc.NavItem(button_gh), dbc.NavItem(button_settings), + dbc.NavItem(button_open), + dbc.NavItem(button_save), ], className="ml-auto", navbar=True, @@ -214,6 +310,8 @@ nav_bar = dbc.Navbar( ), modal_overlay, modal_settings, + modal_open, + modal_save, navmenu, ], align="center", @@ -235,11 +333,14 @@ app.layout = dbc.Container( id="store-settings", data={ "use_real_values": False, + "use_filters": False, "echantillonage_x": 1, "echantillonage_y": 32, "echantillonage_z": 1, }, ), + dcc.Store(id="store-filters", data={}), + dcc.Store(id="store-files"), ], fluid=True, ) @@ -280,6 +381,74 @@ def toggle_offcanvas(n, is_open): return is_open +@app.callback( + Output("open-modal", "is_open"), + [Input("open-button", "n_clicks"), Input("open-close", "n_clicks")], + [dash.dependencies.State("open-modal", "is_open")], +) +def toggle_open(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + +@app.callback( + Output("save-modal", "is_open"), + [Input("save-button", "n_clicks"), Input("save-close", "n_clicks")], + [dash.dependencies.State("save-modal", "is_open")], +) +def toggle_save(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + + +@app.callback( + Output("file-list", "children"), + [Input("open-refresh", "n_clicks")], +) +def refresh_files(n): + files = listdir("Dataset/saves") + return [ + dbc.ListGroupItem( + f"{file}", + action=True, + style={"cursor": "pointer"}, + id={"type": "file-item", "index": i}, + ) + for i, file in enumerate(files) + if isfile(join("Dataset/saves", file)) + ] + +@app.callback( + [Output("open-modal", "is_open", allow_duplicate=True), Output("store-files", "data")], + Input({"type": "file-item", "index": ALL}, "n_clicks"), + State({"type": "file-item", "index": ALL}, "children"), + prevent_initial_call=True, +) +def open_file(n, filenames): + ctx = dash.callback_context + if not ctx.triggered or all(click is None for click in n): + return [None, ""] + file_index = ctx.triggered[0]["prop_id"].split(".")[0] + file_index = eval(file_index) + filename = filenames[file_index['index']] + return [False, filename] + +@app.callback( + Output("save-format", "options"), + [Input("store-filters", "data")], +) +def update_save_format(filters): + if filters: + return [ + {"label": "Save raw dataset", "value": "raw"}, + {"label": "Save filtered dataset", "value": "filt"}, + ] + return [ + {"label": "Save raw dataset", "value": "raw"}, + {"label": "Save filtered dataset", "value": "filt", "disabled": True}, + ] + # on lance l'application if __name__ == "__main__": app.run(debug=True, port="8051", threaded=True) diff --git a/3D_app/pages/ascan.py b/3D_app/pages/ascan.py index c7be9f8..173a98b 100644 --- a/3D_app/pages/ascan.py +++ b/3D_app/pages/ascan.py @@ -1,5 +1,5 @@ import dash -from dash import html, callback, Input, Output, dcc +from dash import html, callback, Input, Output, dcc, State import dash_bootstrap_components as dbc import plotly.graph_objects as go import numpy as np @@ -308,6 +308,49 @@ layout = html.Div( style={"padding": "20px"}, ) +@callback( + Output("store-filters", "data"), + [ + Input("select-ascan-filter1", "value"), + Input("select-ascan-filter2", "value"), + Input("select-ascan-filter3", "value"), + Input("input-ascan-solo-fs", "value"), + Input("input-ascan-solo-cutoff", "value"), + Input("input-ascan-solo-order", "value"), + Input("input-ascan-solo-windowsize", "value"), + Input("input-ascan-solo-fs-2", "value"), + Input("input-ascan-solo-cutoff-2", "value"), + Input("input-ascan-solo-order-2", "value"), + Input("input-ascan-solo-windowsize-2", "value"), + ], +) +def store_settings( + select_filtre_1, + select_filtre_2, + select_filtre_3, + fs_filtre_1, + cutoff_filtre_1, + order_filtre_1, + windowsize_filtre_1, + fs_filtre_2, + cutoff_filtre_2, + order_filtre_2, + windowsize_filtre_2, +): + return { + "select_filtre_1": select_filtre_1, + "select_filtre_2": select_filtre_2, + "select_filtre_3": select_filtre_3, + "fs_filtre_1": fs_filtre_1, + "cutoff_filtre_1": cutoff_filtre_1, + "order_filtre_1": order_filtre_1, + "windowsize_filtre_1": windowsize_filtre_1, + "fs_filtre_2": fs_filtre_2, + "cutoff_filtre_2": cutoff_filtre_2, + "order_filtre_2": order_filtre_2, + "windowsize_filtre_2": windowsize_filtre_2, + } + # callback to update filter values @callback( @@ -368,31 +411,33 @@ def update_filter_values(select_filtre_1, select_filtre_2): Output("loading", "children"), ], [ - Input("select-ascan-filter1", "value"), - Input("select-ascan-filter2", "value"), - Input("select-ascan-filter3", "value"), Input("layer-slider-ascan-solo-x", "value"), Input("layer-slider-ascan-solo-y", "value"), Input("layer-slider-ascan-solo-z", "value"), Input("button-validate-filter", "n_clicks"), - Input("input-ascan-solo-fs", "value"), - Input("input-ascan-solo-cutoff", "value"), - Input("input-ascan-solo-order", "value"), - Input("input-ascan-solo-windowsize", "value"), - Input("input-ascan-solo-fs-2", "value"), - Input("input-ascan-solo-cutoff-2", "value"), - Input("input-ascan-solo-order-2", "value"), - Input("input-ascan-solo-windowsize-2", "value"), ], + [ + State("select-ascan-filter1", "value"), + State("select-ascan-filter2", "value"), + State("select-ascan-filter3", "value"), + State("input-ascan-solo-fs", "value"), + State("input-ascan-solo-cutoff", "value"), + State("input-ascan-solo-order", "value"), + State("input-ascan-solo-windowsize", "value"), + State("input-ascan-solo-fs-2", "value"), + State("input-ascan-solo-cutoff-2", "value"), + State("input-ascan-solo-order-2", "value"), + State("input-ascan-solo-windowsize-2", "value"), + ] ) def update_heatmap_ascan( - selec_transforme_hilbert, - select_filtre_1, - select_filtre_2, select_ascan_x, select_ascan_y, select_ascan_z, n_clicks, + selec_transforme_hilbert, + select_filtre_1, + select_filtre_2, fs_filtre_1, cutoff_filtre_1, order_filtre_1, diff --git a/3D_app/pages/home.py b/3D_app/pages/home.py index 65f3504..80b0b2d 100644 --- a/3D_app/pages/home.py +++ b/3D_app/pages/home.py @@ -1,11 +1,13 @@ import dash -from dash import html, callback, Input, Output, dcc +from dash import html, callback, Input, Output, dcc, State, DiskcacheManager import dash_bootstrap_components as dbc import plotly.graph_objects as go import numpy as np import plotly.express as px import plotly.io as pio from util import * +from os.path import join +import diskcache dash.register_page(__name__, path="/") @@ -15,6 +17,9 @@ fichiers_selectionnes = [ "Shear_x001-x101_y{:03d}_Rot00_transform.csv".format(i) for i in range(10, 14) ] +cache = diskcache.Cache("./cache") +background_callback_manager = DiskcacheManager(cache) + # on charge le fichier numpy # fichiers = np.load("Dataset/npy/export.npy") @@ -465,13 +470,16 @@ layout = html.Div( # on défini les callbacks # callback pour le plot 3D @callback( - [Output("3dplot", "figure"), Output("fade-3dplot", "is_in")], + [Output("3dplot", "figure")], [ Input("iso-slider", "value"), Input("y-slider", "value"), Input("store-settings", "data"), ], [dash.dependencies.State("fade-3dplot", "is_in")], + running=[ + (Output("fade-3dplot", "is_in"), False, True), + ], ) def update_3dplot(iso_value, y_values, settings, is_in): if settings["use_real_values"]: @@ -503,7 +511,7 @@ def update_3dplot(iso_value, y_values, settings, is_in): ) ) - return [fig, True] + return [fig] # callback pour le plot 3D en plein écran @@ -539,18 +547,18 @@ def update_3dplot_fullscreen(iso_value, y_values): # callback pour le A-scan @callback( - [ - Output("heatmap-ascan", "figure"), - Output("fade-ascan", "is_in"), - ], + [Output("heatmap-ascan", "figure")], [Input("layer-slider-bscan-zx", "value"), Input("layer-slider-bscan-xy", "value")], [dash.dependencies.State("fade-ascan", "is_in")], + running=[ + (Output("fade-ascan", "is_in"), False, True), + ], prevent_initial_call=True, ) def update_heatmap_ascan(layer, layer1, is_in): fig = px.line(y=volume[layer - 1, :, layer1], title="A-scan") - return [fig, True] + return [fig] # callback pour le A-scan en plein écran @@ -568,10 +576,12 @@ def update_heatmap_ascan_fullscreen(layer): [ Output("heatmap-bscan-zx", "figure"), Output("store-bscan-zx-layer", "data"), - Output("fade-bscan-zx", "is_in"), ], [Input("layer-slider-bscan-zx", "value")], [dash.dependencies.State("fade-bscan-zx", "is_in")], + running=[ + (Output("fade-bscan-xy", "is_in"), False, True), + ], prevent_initial_call=True, ) def update_heatmap_bscan_zx(layer, is_in): @@ -582,7 +592,7 @@ def update_heatmap_bscan_zx(layer, is_in): title="B-scan ZX", ) - return [fig, layer, True] + return [fig, layer] # callback pour les B-scan ZX en plein écran @@ -603,20 +613,19 @@ def update_heatmap_bscan_zx_fullscreen(layer): # callback pour les B-scan ZX @callback( - [ - Output("heatmap-bscan-xy", "figure"), - Output("store-bscan-xy-layer", "data"), - Output("fade-bscan-xy", "is_in"), - ], + [Output("heatmap-bscan-xy", "figure"), Output("store-bscan-xy-layer", "data")], [Input("layer-slider-bscan-xy", "value")], [dash.dependencies.State("fade-bscan-xy", "is_in")], + running=[ + (Output("fade-bscan-zx", "is_in"), False, True), + ], prevent_initial_call=True, ) def update_heatmap_bscan_xy(layer, is_in): fig = go.Figure(data=go.Heatmap(z=volume[:, :, layer], colorscale="Jet")) fig.update_layout(title="B-scan XY") - return [fig, layer, True] + return [fig, layer] # callback pour les B-scan ZX en plein écran @@ -772,25 +781,30 @@ def update_bscan_layers(bscan_xy, bscan_zx): @callback( [Output("store-settings", "data"), Output("settings-apply", "n_clicks")], [ - Input("use-real-values", "value"), - Input("echantillonage-x", "value"), - Input("echantillonage-y", "value"), - Input("echantillonage-z", "value"), Input("settings-apply", "n_clicks"), ], + [ + State("use-real-values", "value"), + State("apply-filters-everywhere", "value"), + State("echantillonage-x", "value"), + State("echantillonage-y", "value"), + State("echantillonage-z", "value"), + ], prevent_initial_call=True, ) def update_settings( + clicks, use_real_values, + apply_filters, echantillonage_x_value, echantillonage_y_value, echantillonage_z_value, - clicks, ): if clicks != None and clicks == 1: return [ { "use_real_values": use_real_values, + "use_filters": apply_filters, "echantillonage_x": echantillonage_x_value, "echantillonage_y": echantillonage_y_value, "echantillonage_z": echantillonage_z_value, @@ -805,6 +819,11 @@ def update_settings( Output("layer-slider-bscan-zx", "marks"), Output("layer-slider-bscan-xy", "max"), Output("layer-slider-bscan-xy", "marks"), + Output("iso-slider", "min"), + Output("iso-slider", "max"), + Output("iso-slider", "marks"), + Output("y-slider", "max"), + Output("y-slider", "marks"), Output("settings-spinner", "children"), ], Input("store-settings", "data"), @@ -825,5 +844,100 @@ def redef_data(data): {str(i): str(i) for i in range(0, dim_x, max(1, int(dim_x / 20)))}, dim_z - 1, {str(i): str(i) for i in range(1, dim_z + 1, max(1, int(dim_z / 20)))}, + volume.min(), + volume.max() / 2, + { + str(i): str(i) + for i in range( + int(volume.min()), + int(volume.max() / 2) + 1, + int((volume.max() / 2 - volume.min()) / 10), + ) + }, + dim_y, + {str(i): str(i) for i in range(0, int(dim_y) + 1, max(1, int(dim_y / 20)))}, "Apply", ] + + +@callback( + [Input("store-filters", "data"), Input("store-settings", "data")], +) +def apply_filters(data, settings): + global volume + if settings["use_filters"]: + select_filtre_1 = data["select_filtre_2"] + select_filtre_2 = data["select_filtre_3"] + fs_filtre_1 = data["fs_filtre_1"] + cutoff_filtre_1 = data["cutoff_filtre_1"] + order_filtre_1 = data["order_filtre_1"] + windowsize_filtre_1 = data["windowsize_filtre_1"] + fs_filtre_2 = data["fs_filtre_2"] + cutoff_filtre_2 = data["cutoff_filtre_2"] + order_filtre_2 = data["order_filtre_2"] + windowsize_filtre_2 = data["windowsize_filtre_2"] + selec_transforme_hilbert = data["select_filtre_1"] + + volume = pre_volume[ + :: settings["echantillonage_x"], + :: settings["echantillonage_y"], + :: settings["echantillonage_z"], + ] + + data_avec_traitement = switch_case(volume, int(selec_transforme_hilbert)) + + data_avec_traitement = switch_case( + data_avec_traitement, + int(select_filtre_1), + int(fs_filtre_1), + int(cutoff_filtre_1), + int(order_filtre_1), + int(windowsize_filtre_1), + ) + data_avec_traitement = switch_case( + data_avec_traitement, + int(select_filtre_2), + int(fs_filtre_2), + int(cutoff_filtre_2), + int(order_filtre_2), + int(windowsize_filtre_2), + ) + volume = data_avec_traitement + else: + volume = pre_volume[ + :: settings["echantillonage_x"], + :: settings["echantillonage_y"], + :: settings["echantillonage_z"], + ] + return None + + +@callback( + Input("store-files", "data"), + State("store-settings", "data"), + prevent_initial_call=True, +) +def update_files(data, settings): + global pre_volume, dim_y + if data is None or data == "": + return None + pre_volume = np.load(join("Dataset/saves", data)) + redef_data(settings) + update_3dplot(0, [0, dim_y / 2], settings, False) + update_heatmap_ascan(0, 0, False) + update_heatmap_bscan_zx(0, False) + update_heatmap_bscan_xy(0, False) + return None + +@callback( + Input("save-save", "n_clicks"), + [State("save-input", "value"), State("save-format", "value")], +) +def save_data(n_clicks, filename, format): + if n_clicks is None: + return None + if format == "raw": + np.save(join("Dataset/saves", filename), pre_volume) + else: + np.save(join("Dataset/saves", filename), volume) + return None \ No newline at end of file diff --git a/3D_app/requirements.txt b/3D_app/requirements.txt index 60c7b3d..efb2f55 100644 --- a/3D_app/requirements.txt +++ b/3D_app/requirements.txt @@ -1,8 +1,12 @@ dash==2.17.0 dash_bootstrap_components==1.6.0 +diskcache==5.6.3 matplotlib==3.8.4 numpy==1.26.4 pandas==2.2.2 plotly==5.22.0 +progress==1.6 +python_igraph==0.11.5 scikit_learn==1.5.0 scipy==1.13.1 +tqdm==4.66.4