dbouget commited on
Commit
6b70d16
1 Parent(s): 62a50d3

Airways segmentation running locally on a resampled version of the test CT[skip ci]

Browse files
Files changed (7) hide show
  1. AeroPath/__init__.py +0 -0
  2. AeroPath/gui.py +171 -0
  3. AeroPath/inference.py +103 -0
  4. Dockerfile +1 -0
  5. README.md +3 -3
  6. app.py +41 -0
  7. demo/requirements.txt +3 -3
AeroPath/__init__.py ADDED
File without changes
AeroPath/gui.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import gradio as gr
4
+
5
+ from .inference import run_model
6
+ from .utils import load_ct_to_numpy
7
+ from .utils import load_pred_volume_to_numpy
8
+ from .utils import nifti_to_glb
9
+
10
+
11
+ class WebUI:
12
+ def __init__(
13
+ self,
14
+ model_name: str = None,
15
+ cwd: str = "/home/user/app/",
16
+ share: int = 1,
17
+ ):
18
+ # global states
19
+ self.images = []
20
+ self.pred_images = []
21
+
22
+ # @TODO: This should be dynamically set based on chosen volume size
23
+ self.nb_slider_items = 415
24
+
25
+ self.model_name = model_name
26
+ self.cwd = cwd
27
+ self.share = share
28
+
29
+ self.class_name = "airways" # default
30
+ self.class_names = {
31
+ "airways": "CT_Airways",
32
+ "lungs": "CT_Lungs",
33
+ }
34
+
35
+ self.result_names = {
36
+ "airways": "Airways",
37
+ "lungs": "Lungs",
38
+ }
39
+
40
+ # define widgets not to be rendered immediately, but later on
41
+ self.slider = gr.Slider(
42
+ 1,
43
+ self.nb_slider_items,
44
+ value=1,
45
+ step=1,
46
+ label="Which 2D slice to show",
47
+ )
48
+ self.volume_renderer = gr.Model3D(
49
+ clear_color=[0.0, 0.0, 0.0, 0.0],
50
+ label="3D Model",
51
+ visible=True,
52
+ elem_id="model-3d",
53
+ ).style(height=512)
54
+
55
+ def set_class_name(self, value):
56
+ print("Changed task to:", value)
57
+ self.class_name = value
58
+
59
+ def combine_ct_and_seg(self, img, pred):
60
+ return (img, [(pred, self.class_name)])
61
+
62
+ def upload_file(self, file):
63
+ return file.name
64
+
65
+ def process(self, mesh_file_name):
66
+ path = mesh_file_name.name
67
+ run_model(
68
+ path,
69
+ model_path=os.path.join(self.cwd, "resources/models/"),
70
+ task=self.class_names[self.class_name],
71
+ name=self.result_names[self.class_name],
72
+ )
73
+ nifti_to_glb("prediction.nii.gz")
74
+
75
+ self.images = load_ct_to_numpy(path)
76
+ self.pred_images = load_pred_volume_to_numpy("./prediction.nii.gz")
77
+ return "./prediction.obj"
78
+
79
+ def get_img_pred_pair(self, k):
80
+ k = int(k) - 1
81
+ out = [gr.AnnotatedImage.update(visible=False)] * self.nb_slider_items
82
+ out[k] = gr.AnnotatedImage.update(
83
+ self.combine_ct_and_seg(self.images[k], self.pred_images[k]),
84
+ visible=True,
85
+ )
86
+ return out
87
+
88
+ def run(self):
89
+ css = """
90
+ #model-3d {
91
+ height: 512px;
92
+ }
93
+ #model-2d {
94
+ height: 512px;
95
+ margin: auto;
96
+ }
97
+ #upload {
98
+ height: 120px;
99
+ }
100
+ """
101
+ with gr.Blocks(css=css) as demo:
102
+ with gr.Row():
103
+ file_output = gr.File(file_count="single", elem_id="upload")
104
+ file_output.upload(self.upload_file, file_output, file_output)
105
+
106
+ model_selector = gr.Dropdown(
107
+ list(self.class_names.keys()),
108
+ label="Task",
109
+ info="Which task to perform - one model for"
110
+ "airways and lungs extraction",
111
+ multiselect=False,
112
+ size="sm",
113
+ )
114
+ model_selector.input(
115
+ fn=lambda x: self.set_class_name(x),
116
+ inputs=model_selector,
117
+ outputs=None,
118
+ )
119
+
120
+ run_btn = gr.Button("Run analysis").style(
121
+ full_width=False, size="lg"
122
+ )
123
+ run_btn.click(
124
+ fn=lambda x: self.process(x),
125
+ inputs=file_output,
126
+ outputs=self.volume_renderer,
127
+ )
128
+
129
+ with gr.Row():
130
+ gr.Examples(
131
+ examples=[
132
+ os.path.join(self.cwd, "test_thorax_CT_ds.nii"),
133
+ os.path.join(self.cwd, "test_thorax_CT_ds.nii"),
134
+ ],
135
+ inputs=file_output,
136
+ outputs=file_output,
137
+ fn=self.upload_file,
138
+ cache_examples=True,
139
+ )
140
+
141
+ with gr.Row():
142
+ with gr.Box():
143
+ with gr.Column():
144
+ image_boxes = []
145
+ for i in range(self.nb_slider_items):
146
+ visibility = True if i == 1 else False
147
+ t = gr.AnnotatedImage(
148
+ visible=visibility, elem_id="model-2d"
149
+ ).style(
150
+ color_map={self.class_name: "#ffae00"},
151
+ height=512,
152
+ width=512,
153
+ )
154
+ image_boxes.append(t)
155
+
156
+ self.slider.input(
157
+ self.get_img_pred_pair, self.slider, image_boxes
158
+ )
159
+
160
+ self.slider.render()
161
+
162
+ with gr.Box():
163
+ self.volume_renderer.render()
164
+
165
+ # sharing app publicly -> share=True:
166
+ # https://gradio.app/sharing-your-app/
167
+ # inference times > 60 seconds -> need queue():
168
+ # https://github.com/tloen/alpaca-lora/issues/60#issuecomment-1510006062
169
+ demo.queue().launch(
170
+ server_name="0.0.0.0", server_port=7860, share=self.share
171
+ )
AeroPath/inference.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import configparser
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import traceback
6
+
7
+
8
+ def run_model(
9
+ input_path: str,
10
+ model_path: str,
11
+ verbose: str = "info",
12
+ task: str = "CT_Airways",
13
+ name: str = "Airways",
14
+ ):
15
+ logging.basicConfig()
16
+ logging.getLogger().setLevel(logging.WARNING)
17
+
18
+ if verbose == "debug":
19
+ logging.getLogger().setLevel(logging.DEBUG)
20
+ elif verbose == "info":
21
+ logging.getLogger().setLevel(logging.INFO)
22
+ elif verbose == "error":
23
+ logging.getLogger().setLevel(logging.ERROR)
24
+ else:
25
+ raise ValueError("Unsupported verbose value provided:", verbose)
26
+
27
+ # delete patient/result folder if they exist
28
+ if os.path.exists("./patient/"):
29
+ shutil.rmtree("./patient/")
30
+ if os.path.exists("./result/"):
31
+ shutil.rmtree("./result/")
32
+
33
+ patient_directory = ''
34
+ output_path = ''
35
+ try:
36
+ # setup temporary patient directory
37
+ filename = input_path.split("/")[-1]
38
+ splits = filename.split(".")
39
+ extension = ".".join(splits[1:])
40
+ patient_directory = "./patient/"
41
+ os.makedirs(patient_directory + "T0/", exist_ok=True)
42
+ shutil.copy(
43
+ input_path,
44
+ patient_directory + "T0/" + splits[0] + "-t1gd." + extension,
45
+ )
46
+
47
+ # define output directory to save results
48
+ output_path = "./result/prediction-" + splits[0] + "/"
49
+ os.makedirs(output_path, exist_ok=True)
50
+
51
+ # Setting up the configuration file
52
+ rads_config = configparser.ConfigParser()
53
+ rads_config.add_section("Default")
54
+ rads_config.set("Default", "task", "mediastinum_diagnosis")
55
+ rads_config.set("Default", "caller", "")
56
+ rads_config.add_section("System")
57
+ rads_config.set("System", "gpu_id", "-1")
58
+ rads_config.set("System", "input_folder", patient_directory)
59
+ rads_config.set("System", "output_folder", output_path)
60
+ rads_config.set("System", "model_folder", model_path)
61
+ rads_config.set(
62
+ "System",
63
+ "pipeline_filename",
64
+ os.path.join(model_path, task, "pipeline.json"),
65
+ )
66
+ rads_config.add_section("Runtime")
67
+ rads_config.set(
68
+ "Runtime", "reconstruction_method", "thresholding"
69
+ ) # thresholding, probabilities
70
+ rads_config.set("Runtime", "reconstruction_order", "resample_first")
71
+ rads_config.set("Runtime", "use_preprocessed_data", "False")
72
+
73
+ with open("rads_config.ini", "w") as f:
74
+ rads_config.write(f)
75
+
76
+ # finally, run inference
77
+ from raidionicsrads.compute import run_rads
78
+
79
+ run_rads(config_filename="rads_config.ini")
80
+
81
+ # rename and move final result
82
+ os.rename(
83
+ "./result/prediction-"
84
+ + splits[0]
85
+ + "/T0/"
86
+ + splits[0]
87
+ + "-t1gd_annotation-"
88
+ + name
89
+ + ".nii.gz",
90
+ "./prediction.nii.gz",
91
+ )
92
+ # Clean-up
93
+ if os.path.exists(patient_directory):
94
+ shutil.rmtree(patient_directory)
95
+ if os.path.exists(output_path):
96
+ shutil.rmtree(output_path)
97
+ except Exception as e:
98
+ print(traceback.format_exc())
99
+ # Clean-up
100
+ if os.path.exists(patient_directory):
101
+ shutil.rmtree(patient_directory)
102
+ if os.path.exists(output_path):
103
+ shutil.rmtree(output_path)
Dockerfile CHANGED
@@ -61,6 +61,7 @@ RUN wget "https://github.com/raidionics/Raidionics-models/releases/download/1.2.
61
  RUN rm -r *.zip
62
 
63
  # Download test sample
 
64
  RUN wget "https://github.com/andreped/neukit/releases/download/test-data/test_thorax_CT.nii.gz"
65
 
66
  # CMD ["/bin/bash"]
 
61
  RUN rm -r *.zip
62
 
63
  # Download test sample
64
+ # @TODO: I have resampled the volume to 1mm isotropic for faster computation
65
  RUN wget "https://github.com/andreped/neukit/releases/download/test-data/test_thorax_CT.nii.gz"
66
 
67
  # CMD ["/bin/bash"]
README.md CHANGED
@@ -51,12 +51,12 @@ It is also possible to run the app locally without Docker. Just setup a virtual
51
  Note that the current working directory would need to be adjusted based on where `AeroPath` is located on disk.
52
 
53
  ```
54
- git clone https://github.com/andreped/AeroPath.git
55
  cd AeroPath/
56
 
57
- virtualenv -ppython3 venv --clear
58
  source venv/bin/activate
59
- pip install -r requirements.txt
60
 
61
  python app.py --cwd ./
62
  ```
 
51
  Note that the current working directory would need to be adjusted based on where `AeroPath` is located on disk.
52
 
53
  ```
54
+ git clone https://github.com/raidionics/AeroPath.git
55
  cd AeroPath/
56
 
57
+ virtualenv -python3 venv --clear
58
  source venv/bin/activate
59
+ pip install -r ./demo/requirements.txt
60
 
61
  python app.py --cwd ./
62
  ```
app.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from argparse import ArgumentParser
3
+
4
+ from AeroPath.gui import WebUI
5
+
6
+
7
+ def main():
8
+ parser = ArgumentParser()
9
+ parser.add_argument(
10
+ "--cwd",
11
+ type=str,
12
+ default="/home/user/app/",
13
+ help="Set current working directory (path to app.py).",
14
+ )
15
+ parser.add_argument(
16
+ "--share",
17
+ type=int,
18
+ default=1,
19
+ help="Whether to enable the app to be accessible online"
20
+ "-> setups a public link which requires internet access.",
21
+ )
22
+ args = parser.parse_args()
23
+
24
+ print("Current working directory:", args.cwd)
25
+
26
+ if not os.path.exists(args.cwd):
27
+ raise ValueError("Chosen 'cwd' is not a valid path!")
28
+ if args.share not in [0, 1]:
29
+ raise ValueError(
30
+ "The 'share' argument can only be set to 0 or 1, but was:",
31
+ args.share,
32
+ )
33
+
34
+ # initialize and run app
35
+ print("Launching demo...")
36
+ app = WebUI(cwd=args.cwd, share=args.share)
37
+ app.run()
38
+
39
+
40
+ if __name__ == "__main__":
41
+ main()
demo/requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
- raidionicsrads@git+https://github.com/andreped/raidionics_rads_lib.git
2
- gradio==3.44.4
3
- pandas==2.0.0
 
1
+ raidionicsrads@git+https://github.com/dbouget/raidionics_rads_lib
2
+ gradio==3.36.1
3
+ gradio_client==0.2.7