mosta02 commited on
Commit
df3ee3f
·
verified ·
1 Parent(s): f602454

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +1 -1
  2. endograft_script.py +453 -0
  3. server.py +25 -0
Dockerfile CHANGED
@@ -19,7 +19,7 @@ RUN conda run -n vmtk_env python -c "import vtk; print('VTK', vtk.vtkVersion.Get
19
  RUN conda run -n vmtk_env pip install --no-cache-dir flask flask-cors gunicorn
20
 
21
  # Copy the backend source code
22
- COPY centerline_extraction.py server.py ./
23
 
24
  # Hugging Face Spaces expects port 7860, server.py reads PORT from env
25
  ENV PORT=7860
 
19
  RUN conda run -n vmtk_env pip install --no-cache-dir flask flask-cors gunicorn
20
 
21
  # Copy the backend source code
22
+ COPY centerline_extraction.py endograft_script.py server.py ./
23
 
24
  # Hugging Face Spaces expects port 7860, server.py reads PORT from env
25
  ENV PORT=7860
endograft_script.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import vtk
2
+ import numpy as np
3
+ import tempfile
4
+
5
+ def arc_length(cl):
6
+ s = np.zeros(len(cl))
7
+ for i in range(1, len(cl)):
8
+ s[i] = s[i-1] + np.linalg.norm(cl[i] - cl[i-1])
9
+ return s
10
+
11
+ def compute_parallel_frames(centerline):
12
+ frames = []
13
+
14
+ t0 = centerline[1] - centerline[0]
15
+ t0 /= np.linalg.norm(t0)
16
+
17
+ ref = np.array([0, 0, 1])
18
+ if abs(np.dot(t0, ref)) > 0.9:
19
+ ref = np.array([1, 0, 0])
20
+
21
+ n0 = np.cross(t0, ref)
22
+ n0 /= np.linalg.norm(n0)
23
+ b0 = np.cross(t0, n0)
24
+
25
+ frames.append((t0, n0, b0))
26
+
27
+ for i in range(1, len(centerline)):
28
+ ti = centerline[i] - centerline[i-1]
29
+ ti /= np.linalg.norm(ti)
30
+
31
+ t_prev, n_prev, b_prev = frames[-1]
32
+ v = np.cross(t_prev, ti)
33
+
34
+ if np.linalg.norm(v) < 1e-6:
35
+ frames.append((ti, n_prev, b_prev))
36
+ continue
37
+
38
+ v /= np.linalg.norm(v)
39
+ angle = np.arccos(np.clip(np.dot(t_prev, ti), -1, 1))
40
+
41
+ def rot(vec):
42
+ return (vec * np.cos(angle) + np.cross(v, vec) * np.sin(angle) + v * np.dot(v, vec) * (1 - np.cos(angle)))
43
+
44
+ frames.append((ti, rot(n_prev), rot(b_prev)))
45
+
46
+ return frames
47
+
48
+ def project_point_to_surface(p, surface_poly):
49
+ locator = vtk.vtkCellLocator()
50
+ locator.SetDataSet(surface_poly)
51
+ locator.BuildLocator()
52
+
53
+ closest = [0.0, 0.0, 0.0]
54
+ cellId = vtk.mutable(0)
55
+ subId = vtk.mutable(0)
56
+ dist2 = vtk.mutable(0.0)
57
+
58
+ locator.FindClosestPoint(p, closest, cellId, subId, dist2)
59
+ return np.array(closest)
60
+
61
+ def sample_centerline_by_s(centerline, s_cl, frames, s):
62
+ idx = np.searchsorted(s_cl, s)
63
+
64
+ if idx <= 0:
65
+ return centerline[0], frames[0]
66
+ if idx >= len(centerline):
67
+ return centerline[-1], frames[-1]
68
+
69
+ s0, s1 = s_cl[idx-1], s_cl[idx]
70
+ P0, P1 = centerline[idx-1], centerline[idx]
71
+ t = (s - s0) / (s1 - s0 + 1e-12)
72
+
73
+ P = (1 - t) * P0 + t * P1
74
+
75
+ t0, N0, B0 = frames[idx-1]
76
+ t1, N1, B1 = frames[idx]
77
+
78
+ t_vec = (1 - t) * t0 + t * t1
79
+ N_vec = (1 - t) * N0 + t * N1
80
+ B_vec = (1 - t) * B0 + t * B1
81
+
82
+ t_vec /= np.linalg.norm(t_vec)
83
+ N_vec /= np.linalg.norm(N_vec)
84
+ B_vec /= np.linalg.norm(B_vec)
85
+
86
+ return P, (t_vec, N_vec, B_vec)
87
+
88
+ # Fabric
89
+ def create_component_fabric(centerline, frames, start_s, length, diameter, start_amp_pp=2.0, waves=8):
90
+ # shift centerline sampling
91
+ shifted_centerline = centerline.copy()
92
+
93
+ s_cl = arc_length(centerline)
94
+
95
+ pts = vtk.vtkPoints()
96
+ polys = vtk.vtkCellArray()
97
+ rings = []
98
+
99
+ A = start_amp_pp / 2.0
100
+ n_sections = 180
101
+ n_theta = 64
102
+
103
+ for i in range(n_sections):
104
+ s_base = start_s + length * i / (n_sections - 1)
105
+ P_base, (t, N, B) = sample_centerline_by_s(centerline, s_cl, frames, s_base)
106
+
107
+ ring = []
108
+
109
+ for j in range(n_theta):
110
+ theta = 2 * np.pi * j / n_theta
111
+ axial_offset = 0.0
112
+
113
+ if i == 0 and start_amp_pp > 0:
114
+ axial_offset = A * np.sin(waves * theta) - A
115
+
116
+ P = P_base + axial_offset * t
117
+ r = diameter / 2.0
118
+ pos = P + r * (np.cos(theta) * N + np.sin(theta) * B)
119
+
120
+ ring.append(pts.InsertNextPoint(pos))
121
+
122
+ rings.append(ring)
123
+
124
+ for i in range(len(rings)-1):
125
+ for j in range(n_theta):
126
+ q = vtk.vtkQuad()
127
+ q.GetPointIds().SetId(0, rings[i][j])
128
+ q.GetPointIds().SetId(1, rings[i][(j+1)%n_theta])
129
+ q.GetPointIds().SetId(2, rings[i+1][(j+1)%n_theta])
130
+ q.GetPointIds().SetId(3, rings[i+1][j])
131
+ polys.InsertNextCell(q)
132
+
133
+ poly = vtk.vtkPolyData()
134
+ poly.SetPoints(pts)
135
+ poly.SetPolys(polys)
136
+
137
+ return poly
138
+
139
+ # Struts
140
+
141
+ def create_proximal_bare_stent(centerline, frames, fabric_start_s, main_diam_mm, length, main_fabric, peak_to_peak=15.0,
142
+ peak_spacing=7.0, component_type="proximal", wire_radius=0.35, waves=8, samples=240
143
+ ):
144
+
145
+ s_cl = arc_length(centerline)
146
+ append = vtk.vtkAppendPolyData()
147
+
148
+ # --- build fabric normals & locator ONCE ---
149
+ normal_gen = vtk.vtkPolyDataNormals()
150
+ normal_gen.SetInputData(main_fabric)
151
+ normal_gen.ComputePointNormalsOn()
152
+ normal_gen.SplittingOff()
153
+ normal_gen.ConsistencyOn()
154
+ normal_gen.Update()
155
+ fabric_normals = normal_gen.GetOutput().GetPointData().GetNormals()
156
+
157
+ locator = vtk.vtkPointLocator()
158
+ locator.SetDataSet(main_fabric)
159
+ locator.BuildLocator()
160
+ # -----------------------------------------
161
+
162
+ A = peak_to_peak # peak-peak amplitude = 15
163
+ r = main_diam_mm / 2.0
164
+ gap = peak_spacing
165
+ step = A + gap
166
+
167
+ n_crowns = int(np.ceil(length / step))
168
+ s_peaks = [fabric_start_s + 1.0 + i * step for i in range(n_crowns)]
169
+ if ((component_type == "distal" or component_type == "extension") and (len(s_peaks) > 4)):
170
+ s_peaks = s_peaks[3:]
171
+
172
+ # =========================================================
173
+ # ===== EXTRA CLIPPED HALF sin =================
174
+ # =========================================================
175
+ A_extra = peak_to_peak / 2.0 # 7.5 mm height
176
+ extra_top = -1e-10 # peak at -7.5
177
+
178
+ if component_type == "proximal":
179
+ P0 = centerline[0]
180
+ t0, N0, B0 = frames[0]
181
+ P = P0 + extra_top * t0 # extrapolate backward
182
+ t, N, B = t0, N0, B0
183
+
184
+ elif component_type == "distal":
185
+ P, (t, N, B) = sample_centerline_by_s(centerline, s_cl, frames, fabric_start_s + length - extra_top)
186
+
187
+ else:
188
+ P, (t, N, B) = sample_centerline_by_s(centerline, s_cl, frames, extra_top)
189
+
190
+ pts = vtk.vtkPoints()
191
+ lines = vtk.vtkCellArray()
192
+
193
+ for i in range(samples + 1):
194
+ theta = 2*np.pi*i/samples
195
+
196
+ # keep ONLY negative or positive half of sine
197
+ if component_type == "proximal":
198
+ axial_offset = A_extra * min(0.0, np.sin(waves * theta))
199
+ raw_pos = (P
200
+ + (r + wire_radius) * (np.cos(theta)*N + np.sin(theta)*B)
201
+ + axial_offset * t)
202
+ elif (component_type == "distal" and length != 0):
203
+ axial_offset = A_extra * max(0.0, np.sin(waves * theta))
204
+
205
+ raw_pos = (P
206
+ + (r + wire_radius) * (np.cos(theta)*N + np.sin(theta)*B)
207
+ + axial_offset * t)
208
+ else:
209
+ continue
210
+
211
+ radial = (np.cos(theta)*N + np.sin(theta)*B)
212
+ radial /= np.linalg.norm(radial)
213
+
214
+ pos = P + (r + wire_radius) * radial + axial_offset * t
215
+ pts.InsertNextPoint(pos)
216
+
217
+ if i > 0:
218
+ l = vtk.vtkLine()
219
+ l.GetPointIds().SetId(0, i-1)
220
+ l.GetPointIds().SetId(1, i)
221
+ lines.InsertNextCell(l)
222
+
223
+ poly = vtk.vtkPolyData()
224
+ poly.SetPoints(pts)
225
+ poly.SetLines(lines)
226
+
227
+ tube = vtk.vtkTubeFilter()
228
+ tube.SetInputData(poly)
229
+ tube.SetRadius(wire_radius)
230
+ tube.SetNumberOfSides(18)
231
+ tube.CappingOff()
232
+ tube.Update()
233
+
234
+ append.AddInputData(tube.GetOutput())
235
+ # =========================================================
236
+ # ================= NORMAL FULL struts ====================
237
+ # =========================================================
238
+
239
+ for s0 in s_peaks:
240
+ P, (t, N, B) = sample_centerline_by_s(centerline, s_cl, frames, s0)
241
+
242
+ pts = vtk.vtkPoints()
243
+ lines = vtk.vtkCellArray()
244
+
245
+ for i in range(samples + 1):
246
+ theta = 2*np.pi*i/samples
247
+
248
+ axial_offset = A * (1 + np.sin(waves * theta)) / 2.0
249
+
250
+ raw_pos = (P
251
+ + (r + wire_radius) * (np.cos(theta)*N + np.sin(theta)*B)
252
+ + axial_offset * t)
253
+
254
+ surf_p = project_point_to_surface(raw_pos, main_fabric)
255
+
256
+ pid = locator.FindClosestPoint(surf_p)
257
+ n = np.array(fabric_normals.GetTuple(pid))
258
+ n /= np.linalg.norm(n)
259
+
260
+ pos = surf_p + wire_radius * n
261
+
262
+ pts.InsertNextPoint(pos)
263
+
264
+ if i > 0:
265
+ l = vtk.vtkLine()
266
+ l.GetPointIds().SetId(0, i-1)
267
+ l.GetPointIds().SetId(1, i)
268
+ lines.InsertNextCell(l)
269
+
270
+ poly = vtk.vtkPolyData()
271
+ poly.SetPoints(pts)
272
+ poly.SetLines(lines)
273
+
274
+ tube = vtk.vtkTubeFilter()
275
+ tube.SetInputData(poly)
276
+ tube.SetRadius(wire_radius)
277
+ tube.SetNumberOfSides(18)
278
+ tube.CappingOff()
279
+ tube.Update()
280
+
281
+ append.AddInputData(tube.GetOutput())
282
+
283
+ append.Update()
284
+ return append.GetOutput()
285
+
286
+ def create_component_stents(centerline, frames, fabric_start_s, length, diameter, fabric, peak_to_peak=15.0, peak_spacing=7.0, component_type="proximal"):
287
+
288
+ stents = create_proximal_bare_stent(centerline, frames, fabric_start_s, diameter, length, fabric, peak_to_peak, peak_spacing, component_type)
289
+
290
+ return stents
291
+
292
+ def merge_components(proximal, extension, distal):
293
+
294
+ merged_components = vtk.vtkAppendPolyData()
295
+ merged_components.AddInputData(proximal)
296
+ merged_components.AddInputData(extension)
297
+ merged_components.AddInputData(distal)
298
+ merged_components.Update()
299
+
300
+ clean = vtk.vtkCleanPolyData()
301
+ clean.SetInputData(merged_components.GetOutput())
302
+ clean.Update()
303
+
304
+ return clean
305
+
306
+ def create_endograft(centerline, diameter, prox_length,extension_length, distal_length, prox_gap, extension_gap, distal_gap):
307
+ frames = compute_parallel_frames(centerline)
308
+ peak_to_peak = 15.0
309
+
310
+ # -------------------------
311
+ # PROXIMAL COMPONENT
312
+ # -------------------------
313
+
314
+ prox_start = 0.0
315
+
316
+ prox_fabric = create_component_fabric(centerline, frames, prox_start, prox_length, diameter, start_amp_pp=2.0)
317
+
318
+ prox_stents = create_component_stents(centerline, frames, prox_start, prox_length, diameter, prox_fabric, peak_spacing=prox_gap, component_type="proximal")
319
+
320
+
321
+ # -------------------------
322
+ # EXTENSION COMPONENT
323
+ # -------------------------
324
+ if extension_length > 97:
325
+ prox_extension_overlap = 3*peak_to_peak + 2*extension_gap
326
+ else:
327
+ prox_extension_overlap = 2*peak_to_peak + extension_gap
328
+
329
+ extension_start = prox_length - prox_extension_overlap
330
+ extension_fabric = create_component_fabric(centerline, frames, extension_start, extension_length, diameter, start_amp_pp=0.0)
331
+
332
+ extension_stents = create_component_stents(centerline, frames, extension_start, extension_length, diameter, extension_fabric, peak_spacing=extension_gap, component_type="extension")
333
+
334
+ # -------------------------
335
+ # DISTAL COMPONENT
336
+ # -------------------------
337
+ distal_overlap = 3*peak_to_peak + 2*distal_gap
338
+
339
+ distal_start = prox_length + extension_length - prox_extension_overlap - distal_overlap
340
+
341
+ distal_fabric = create_component_fabric(centerline, frames, distal_start, distal_length, diameter, start_amp_pp=0.0)
342
+
343
+
344
+
345
+ distal_stents = create_component_stents(centerline, frames, distal_start, distal_length, diameter, distal_fabric, peak_spacing=distal_gap, component_type="distal")
346
+ # -------------------------
347
+ # MERGE COMPONENTS
348
+ # -------------------------
349
+ fabric_part = merge_components(prox_fabric, extension_fabric, distal_fabric)
350
+ stent_part = merge_components(prox_stents, extension_stents, distal_stents)
351
+
352
+ endograft = vtk.vtkAppendPolyData()
353
+
354
+ endograft.AddInputData(fabric_part.GetOutput())
355
+ endograft.AddInputData(stent_part.GetOutput())
356
+
357
+ endograft.Update()
358
+
359
+ clean = vtk.vtkCleanPolyData()
360
+ clean.SetInputData(endograft.GetOutput())
361
+ clean.Update()
362
+
363
+ return fabric_part.GetOutput(), stent_part.GetOutput(), clean.GetOutput()
364
+
365
+ def normalize_centerline_input(centerline):
366
+ if len(centerline) > 0 and isinstance(centerline[0], dict):
367
+ centerline = [[p["x"], p["y"], p["z"]] for p in centerline]
368
+
369
+ arr = np.asarray(centerline, dtype=np.float64)
370
+ if arr.ndim != 2 or arr.shape[1] != 3:
371
+ raise ValueError("centerline must have shape [N, 3]")
372
+ if arr.shape[0] < 2:
373
+ raise ValueError("centerline must contain at least 2 points")
374
+ return arr
375
+
376
+
377
+ # Returns generated VTK polydata parts as standalone objects.
378
+ def build_endograft_parts(
379
+ centerline,
380
+ diameter=22.0,
381
+ prox_length=105.0,
382
+ extension_length=0.0,
383
+ distal_length=0.0,
384
+ prox_gap=7.0,
385
+ extension_gap=0.0,
386
+ distal_gap=0.0,
387
+ ):
388
+ centerline_np = normalize_centerline_input(centerline)
389
+
390
+ endograft_poly, struts_poly, combined_poly = create_endograft(
391
+ centerline_np,
392
+ diameter=diameter,
393
+ prox_length=prox_length,
394
+ extension_length=extension_length,
395
+ distal_length=distal_length,
396
+ prox_gap=prox_gap,
397
+ extension_gap=extension_gap,
398
+ distal_gap=distal_gap,
399
+ )
400
+
401
+ return {
402
+ "endograft": endograft_poly,
403
+ "struts": struts_poly,
404
+ "combined": combined_poly,
405
+ }
406
+
407
+
408
+ def polydata_to_stl_bytes(polydata):
409
+ tmp = tempfile.NamedTemporaryFile(suffix=".stl", delete=False)
410
+ tmp_path = tmp.name
411
+ tmp.close()
412
+
413
+ try:
414
+ writer = vtk.vtkSTLWriter()
415
+ writer.SetFileName(tmp_path)
416
+ writer.SetInputData(polydata)
417
+ writer.Write()
418
+
419
+ with open(tmp_path, "rb") as f:
420
+ return f.read()
421
+ finally:
422
+ try:
423
+ import os
424
+
425
+ os.unlink(tmp_path)
426
+ except OSError:
427
+ pass
428
+
429
+
430
+ def build_endograft_stl_payload(centerline, **params):
431
+ parts = build_endograft_parts(centerline, **params)
432
+ return {
433
+ "endograft": polydata_to_stl_bytes(parts["endograft"]),
434
+ "struts": polydata_to_stl_bytes(parts["struts"]),
435
+ "combined": polydata_to_stl_bytes(parts["combined"]),
436
+ }
437
+
438
+
439
+ def generate_stl_files(centerline, output_prefix="endograft", **params):
440
+ parts = build_endograft_parts(centerline, **params)
441
+ outputs = {
442
+ "endograft": f"{output_prefix}-endograft.stl",
443
+ "struts": f"{output_prefix}-struts.stl",
444
+ "combined": f"{output_prefix}-combined.stl",
445
+ }
446
+
447
+ for key, path in outputs.items():
448
+ writer = vtk.vtkSTLWriter()
449
+ writer.SetFileName(path)
450
+ writer.SetInputData(parts[key])
451
+ writer.Write()
452
+
453
+ return outputs
server.py CHANGED
@@ -3,6 +3,7 @@
3
  import os
4
  import sys
5
  import gc
 
6
  import tempfile
7
  import traceback
8
  from flask import Flask, request, jsonify
@@ -25,6 +26,7 @@ _print_env_info()
25
 
26
  # Import the centerline extraction logic from the existing module
27
  from centerline_extraction import compute_centerlines_network, extract_centerline_data, load_nrrd_as_vtk_image
 
28
 
29
  app = Flask(__name__)
30
  CORS(app) # Allow cross-origin requests from the Next.js frontend
@@ -96,6 +98,29 @@ def extract():
96
  pass
97
 
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  if __name__ == "__main__":
100
  port = int(os.environ.get("PORT", 5050))
101
  app.run(host="0.0.0.0", port=port)
 
3
  import os
4
  import sys
5
  import gc
6
+ import base64
7
  import tempfile
8
  import traceback
9
  from flask import Flask, request, jsonify
 
26
 
27
  # Import the centerline extraction logic from the existing module
28
  from centerline_extraction import compute_centerlines_network, extract_centerline_data, load_nrrd_as_vtk_image
29
+ from endograft_script import build_endograft_stl_payload
30
 
31
  app = Flask(__name__)
32
  CORS(app) # Allow cross-origin requests from the Next.js frontend
 
98
  pass
99
 
100
 
101
+ @app.route("/endograft", methods=["POST"])
102
+ def endograft():
103
+ body = request.get_json(silent=True) or {}
104
+ centerline = body.get("centerline")
105
+ params = body.get("params") or {}
106
+
107
+ if not isinstance(centerline, list) or len(centerline) < 2:
108
+ return jsonify({"status": "error", "error": "centerline must be a list of at least 2 points"}), 400
109
+
110
+ try:
111
+ payload = build_endograft_stl_payload(centerline, **params)
112
+ result = {
113
+ "endograft_stl_b64": base64.b64encode(payload["endograft"]).decode("utf-8"),
114
+ "struts_stl_b64": base64.b64encode(payload["struts"]).decode("utf-8"),
115
+ "combined_stl_b64": base64.b64encode(payload["combined"]).decode("utf-8"),
116
+ }
117
+ gc.collect()
118
+ return jsonify({"status": "success", "data": result})
119
+ except Exception as e:
120
+ traceback.print_exc()
121
+ return jsonify({"status": "error", "error": str(e)}), 500
122
+
123
+
124
  if __name__ == "__main__":
125
  port = int(os.environ.get("PORT", 5050))
126
  app.run(host="0.0.0.0", port=port)