Commit 7c1411a5789343d8b58e30f083babdaabe482c88
Committed by
GitHub
1 parent
cc3c0dc3
Exists in
master
and in
22 other branches
Fill mask holes manually and remove connected mask parts (#47)
These tools use floodfill implemented using Cython c++.
Showing
11 changed files
with
569 additions
and
20 deletions
Show diff stats
invesalius/constants.py
@@ -481,6 +481,8 @@ ID_BOOLEAN_MASK = wx.NewId() | @@ -481,6 +481,8 @@ ID_BOOLEAN_MASK = wx.NewId() | ||
481 | ID_CLEAN_MASK = wx.NewId() | 481 | ID_CLEAN_MASK = wx.NewId() |
482 | 482 | ||
483 | ID_REORIENT_IMG = wx.NewId() | 483 | ID_REORIENT_IMG = wx.NewId() |
484 | +ID_FLOODFILL_MASK = wx.NewId() | ||
485 | +ID_REMOVE_MASK_PART = wx.NewId() | ||
484 | 486 | ||
485 | #--------------------------------------------------------- | 487 | #--------------------------------------------------------- |
486 | STATE_DEFAULT = 1000 | 488 | STATE_DEFAULT = 1000 |
@@ -498,6 +500,8 @@ SLICE_STATE_SCROLL = 3007 | @@ -498,6 +500,8 @@ SLICE_STATE_SCROLL = 3007 | ||
498 | SLICE_STATE_EDITOR = 3008 | 500 | SLICE_STATE_EDITOR = 3008 |
499 | SLICE_STATE_WATERSHED = 3009 | 501 | SLICE_STATE_WATERSHED = 3009 |
500 | SLICE_STATE_REORIENT = 3010 | 502 | SLICE_STATE_REORIENT = 3010 |
503 | +SLICE_STATE_MASK_FFILL = 3011 | ||
504 | +SLICE_STATE_REMOVE_MASK_PARTS = 3012 | ||
501 | 505 | ||
502 | VOLUME_STATE_SEED = 2001 | 506 | VOLUME_STATE_SEED = 2001 |
503 | # STATE_LINEAR_MEASURE = 3001 | 507 | # STATE_LINEAR_MEASURE = 3001 |
@@ -515,6 +519,8 @@ SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES | @@ -515,6 +519,8 @@ SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES | ||
515 | SLICE_STYLES.append(STATE_DEFAULT) | 519 | SLICE_STYLES.append(STATE_DEFAULT) |
516 | SLICE_STYLES.append(SLICE_STATE_EDITOR) | 520 | SLICE_STYLES.append(SLICE_STATE_EDITOR) |
517 | SLICE_STYLES.append(SLICE_STATE_WATERSHED) | 521 | SLICE_STYLES.append(SLICE_STATE_WATERSHED) |
522 | +SLICE_STYLES.append(SLICE_STATE_MASK_FFILL) | ||
523 | +SLICE_STYLES.append(SLICE_STATE_REMOVE_MASK_PARTS) | ||
518 | 524 | ||
519 | VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, | 525 | VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, |
520 | STATE_MEASURE_ANGLE] | 526 | STATE_MEASURE_ANGLE] |
@@ -523,6 +529,8 @@ VOLUME_STYLES.append(STATE_DEFAULT) | @@ -523,6 +529,8 @@ VOLUME_STYLES.append(STATE_DEFAULT) | ||
523 | 529 | ||
524 | STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, | 530 | STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, |
525 | SLICE_STATE_WATERSHED: 1, | 531 | SLICE_STATE_WATERSHED: 1, |
532 | + SLICE_STATE_MASK_FFILL: 2, | ||
533 | + SLICE_STATE_REMOVE_MASK_PARTS: 2, | ||
526 | SLICE_STATE_CROSS: 2, | 534 | SLICE_STATE_CROSS: 2, |
527 | SLICE_STATE_SCROLL: 2, | 535 | SLICE_STATE_SCROLL: 2, |
528 | SLICE_STATE_REORIENT: 2, | 536 | SLICE_STATE_REORIENT: 2, |
@@ -530,7 +538,7 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, | @@ -530,7 +538,7 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, | ||
530 | STATE_DEFAULT: 0, | 538 | STATE_DEFAULT: 0, |
531 | STATE_MEASURE_ANGLE: 2, | 539 | STATE_MEASURE_ANGLE: 2, |
532 | STATE_MEASURE_DISTANCE: 2, | 540 | STATE_MEASURE_DISTANCE: 2, |
533 | - STATE_WL: 2, | 541 | + STATE_WL: 3, |
534 | STATE_SPIN: 2, | 542 | STATE_SPIN: 2, |
535 | STATE_ZOOM: 2, | 543 | STATE_ZOOM: 2, |
536 | STATE_ZOOM_SL: 2, | 544 | STATE_ZOOM_SL: 2, |
invesalius/data/cy_my_types.pxd
@@ -2,4 +2,11 @@ import numpy as np | @@ -2,4 +2,11 @@ import numpy as np | ||
2 | cimport numpy as np | 2 | cimport numpy as np |
3 | cimport cython | 3 | cimport cython |
4 | 4 | ||
5 | -ctypedef np.int16_t image_t | 5 | +# ctypedef np.uint16_t image_t |
6 | + | ||
7 | +ctypedef fused image_t: | ||
8 | + np.float64_t | ||
9 | + np.int16_t | ||
10 | + np.uint8_t | ||
11 | + | ||
12 | +ctypedef np.uint8_t mask_t |
@@ -0,0 +1,232 @@ | @@ -0,0 +1,232 @@ | ||
1 | +import numpy as np | ||
2 | +cimport numpy as np | ||
3 | +cimport cython | ||
4 | + | ||
5 | +from collections import deque | ||
6 | + | ||
7 | +from libc.math cimport floor, ceil | ||
8 | +from libcpp.deque cimport deque as cdeque | ||
9 | +from libcpp.vector cimport vector | ||
10 | + | ||
11 | +from cy_my_types cimport image_t, mask_t | ||
12 | + | ||
13 | +cdef struct s_coord: | ||
14 | + int x | ||
15 | + int y | ||
16 | + int z | ||
17 | + | ||
18 | +ctypedef s_coord coord | ||
19 | + | ||
20 | + | ||
21 | +@cython.boundscheck(False) # turn of bounds-checking for entire function | ||
22 | +@cython.wraparound(False) | ||
23 | +@cython.nonecheck(False) | ||
24 | +@cython.cdivision(True) | ||
25 | +cdef inline void append_queue(cdeque[int]& stack, int x, int y, int z, int d, int h, int w) nogil: | ||
26 | + stack.push_back(z*h*w + y*w + x) | ||
27 | + | ||
28 | + | ||
29 | +@cython.boundscheck(False) # turn of bounds-checking for entire function | ||
30 | +@cython.wraparound(False) | ||
31 | +@cython.nonecheck(False) | ||
32 | +@cython.cdivision(True) | ||
33 | +cdef inline void pop_queue(cdeque[int]& stack, int* x, int* y, int* z, int d, int h, int w) nogil: | ||
34 | + cdef int i = stack.front() | ||
35 | + stack.pop_front() | ||
36 | + x[0] = i % w | ||
37 | + y[0] = (i / w) % h | ||
38 | + z[0] = i / (h * w) | ||
39 | + | ||
40 | + | ||
41 | +@cython.boundscheck(False) # turn of bounds-checking for entire function | ||
42 | +def floodfill(np.ndarray[image_t, ndim=3] data, int i, int j, int k, int v, int fill, np.ndarray[mask_t, ndim=3] out): | ||
43 | + | ||
44 | + cdef int to_return = 0 | ||
45 | + if out is None: | ||
46 | + out = np.zeros_like(data) | ||
47 | + to_return = 1 | ||
48 | + | ||
49 | + cdef int x, y, z | ||
50 | + cdef int w, h, d | ||
51 | + | ||
52 | + d = data.shape[0] | ||
53 | + h = data.shape[1] | ||
54 | + w = data.shape[2] | ||
55 | + | ||
56 | + stack = [(i, j, k), ] | ||
57 | + out[k, j, i] = fill | ||
58 | + | ||
59 | + while stack: | ||
60 | + x, y, z = stack.pop() | ||
61 | + | ||
62 | + if z + 1 < d and data[z + 1, y, x] == v and out[z + 1, y, x] != fill: | ||
63 | + out[z + 1, y, x] = fill | ||
64 | + stack.append((x, y, z + 1)) | ||
65 | + | ||
66 | + if z - 1 >= 0 and data[z - 1, y, x] == v and out[z - 1, y, x] != fill: | ||
67 | + out[z - 1, y, x] = fill | ||
68 | + stack.append((x, y, z - 1)) | ||
69 | + | ||
70 | + if y + 1 < h and data[z, y + 1, x] == v and out[z, y + 1, x] != fill: | ||
71 | + out[z, y + 1, x] = fill | ||
72 | + stack.append((x, y + 1, z)) | ||
73 | + | ||
74 | + if y - 1 >= 0 and data[z, y - 1, x] == v and out[z, y - 1, x] != fill: | ||
75 | + out[z, y - 1, x] = fill | ||
76 | + stack.append((x, y - 1, z)) | ||
77 | + | ||
78 | + if x + 1 < w and data[z, y, x + 1] == v and out[z, y, x + 1] != fill: | ||
79 | + out[z, y, x + 1] = fill | ||
80 | + stack.append((x + 1, y, z)) | ||
81 | + | ||
82 | + if x - 1 >= 0 and data[z, y, x - 1] == v and out[z, y, x - 1] != fill: | ||
83 | + out[z, y, x - 1] = fill | ||
84 | + stack.append((x - 1, y, z)) | ||
85 | + | ||
86 | + if to_return: | ||
87 | + return out | ||
88 | + | ||
89 | + | ||
90 | +@cython.boundscheck(False) # turn of bounds-checking for entire function | ||
91 | +@cython.wraparound(False) | ||
92 | +@cython.nonecheck(False) | ||
93 | +def floodfill_threshold(np.ndarray[image_t, ndim=3] data, list seeds, int t0, int t1, int fill, np.ndarray[mask_t, ndim=3] strct, np.ndarray[mask_t, ndim=3] out): | ||
94 | + | ||
95 | + cdef int to_return = 0 | ||
96 | + if out is None: | ||
97 | + out = np.zeros_like(data) | ||
98 | + to_return = 1 | ||
99 | + | ||
100 | + cdef int x, y, z | ||
101 | + cdef int dx, dy, dz | ||
102 | + cdef int odx, ody, odz | ||
103 | + cdef int xo, yo, zo | ||
104 | + cdef int i, j, k | ||
105 | + cdef int offset_x, offset_y, offset_z | ||
106 | + | ||
107 | + dz = data.shape[0] | ||
108 | + dy = data.shape[1] | ||
109 | + dx = data.shape[2] | ||
110 | + | ||
111 | + odz = strct.shape[0] | ||
112 | + ody = strct.shape[1] | ||
113 | + odx = strct.shape[2] | ||
114 | + | ||
115 | + cdef cdeque[coord] stack | ||
116 | + cdef coord c | ||
117 | + | ||
118 | + offset_z = odz / 2 | ||
119 | + offset_y = ody / 2 | ||
120 | + offset_x = odx / 2 | ||
121 | + | ||
122 | + for i, j, k in seeds: | ||
123 | + if data[k, j, i] >= t0 and data[k, j, i] <= t1: | ||
124 | + c.x = i | ||
125 | + c.y = j | ||
126 | + c.z = k | ||
127 | + stack.push_back(c) | ||
128 | + out[k, j, i] = fill | ||
129 | + | ||
130 | + with nogil: | ||
131 | + while stack.size(): | ||
132 | + c = stack.back() | ||
133 | + stack.pop_back() | ||
134 | + | ||
135 | + x = c.x | ||
136 | + y = c.y | ||
137 | + z = c.z | ||
138 | + | ||
139 | + out[z, y, x] = fill | ||
140 | + | ||
141 | + for k in xrange(odz): | ||
142 | + zo = z + k - offset_z | ||
143 | + for j in xrange(ody): | ||
144 | + yo = y + j - offset_y | ||
145 | + for i in xrange(odx): | ||
146 | + if strct[k, j, i]: | ||
147 | + xo = x + i - offset_x | ||
148 | + if 0 <= xo < dx and 0 <= yo < dy and 0 <= zo < dz and out[zo, yo, xo] != fill and t0 <= data[zo, yo, xo] <= t1: | ||
149 | + out[zo, yo, xo] = fill | ||
150 | + c.x = xo | ||
151 | + c.y = yo | ||
152 | + c.z = zo | ||
153 | + stack.push_back(c) | ||
154 | + | ||
155 | + if to_return: | ||
156 | + return out | ||
157 | + | ||
158 | + | ||
159 | +@cython.boundscheck(False) # turn of bounds-checking for entire function | ||
160 | +@cython.wraparound(False) | ||
161 | +@cython.nonecheck(False) | ||
162 | +def floodfill_auto_threshold(np.ndarray[image_t, ndim=3] data, list seeds, float p, int fill, np.ndarray[mask_t, ndim=3] out): | ||
163 | + | ||
164 | + cdef int to_return = 0 | ||
165 | + if out is None: | ||
166 | + out = np.zeros_like(data) | ||
167 | + to_return = 1 | ||
168 | + | ||
169 | + cdef cdeque[int] stack | ||
170 | + cdef int x, y, z | ||
171 | + cdef int w, h, d | ||
172 | + cdef int xo, yo, zo | ||
173 | + cdef int t0, t1 | ||
174 | + | ||
175 | + cdef int i, j, k | ||
176 | + | ||
177 | + d = data.shape[0] | ||
178 | + h = data.shape[1] | ||
179 | + w = data.shape[2] | ||
180 | + | ||
181 | + | ||
182 | + # stack = deque() | ||
183 | + | ||
184 | + x = 0 | ||
185 | + y = 0 | ||
186 | + z = 0 | ||
187 | + | ||
188 | + | ||
189 | + for i, j, k in seeds: | ||
190 | + append_queue(stack, i, j, k, d, h, w) | ||
191 | + out[k, j, i] = fill | ||
192 | + print i, j, k, d, h, w | ||
193 | + | ||
194 | + with nogil: | ||
195 | + while stack.size(): | ||
196 | + pop_queue(stack, &x, &y, &z, d, h, w) | ||
197 | + | ||
198 | + # print x, y, z, d, h, w | ||
199 | + | ||
200 | + xo = x | ||
201 | + yo = y | ||
202 | + zo = z | ||
203 | + | ||
204 | + t0 = <int>ceil(data[z, y, x] * (1 - p)) | ||
205 | + t1 = <int>floor(data[z, y, x] * (1 + p)) | ||
206 | + | ||
207 | + if z + 1 < d and data[z + 1, y, x] >= t0 and data[z + 1, y, x] <= t1 and out[zo + 1, yo, xo] != fill: | ||
208 | + out[zo + 1, yo, xo] = fill | ||
209 | + append_queue(stack, x, y, z+1, d, h, w) | ||
210 | + | ||
211 | + if z - 1 >= 0 and data[z - 1, y, x] >= t0 and data[z - 1, y, x] <= t1 and out[zo - 1, yo, xo] != fill: | ||
212 | + out[zo - 1, yo, xo] = fill | ||
213 | + append_queue(stack, x, y, z-1, d, h, w) | ||
214 | + | ||
215 | + if y + 1 < h and data[z, y + 1, x] >= t0 and data[z, y + 1, x] <= t1 and out[zo, yo + 1, xo] != fill: | ||
216 | + out[zo, yo + 1, xo] = fill | ||
217 | + append_queue(stack, x, y+1, z, d, h, w) | ||
218 | + | ||
219 | + if y - 1 >= 0 and data[z, y - 1, x] >= t0 and data[z, y - 1, x] <= t1 and out[zo, yo - 1, xo] != fill: | ||
220 | + out[zo, yo - 1, xo] = fill | ||
221 | + append_queue(stack, x, y-1, z, d, h, w) | ||
222 | + | ||
223 | + if x + 1 < w and data[z, y, x + 1] >= t0 and data[z, y, x + 1] <= t1 and out[zo, yo, xo + 1] != fill: | ||
224 | + out[zo, yo, xo + 1] = fill | ||
225 | + append_queue(stack, x+1, y, z, d, h, w) | ||
226 | + | ||
227 | + if x - 1 >= 0 and data[z, y, x - 1] >= t0 and data[z, y, x - 1] <= t1 and out[zo, yo, xo - 1] != fill: | ||
228 | + out[zo, yo, xo - 1] = fill | ||
229 | + append_queue(stack, x-1, y, z, d, h, w) | ||
230 | + | ||
231 | + if to_return: | ||
232 | + return out |
invesalius/data/mask.py
@@ -60,6 +60,8 @@ class EditionHistoryNode(object): | @@ -60,6 +60,8 @@ class EditionHistoryNode(object): | ||
60 | mvolume[1:, 1:, self.index+1] = array | 60 | mvolume[1:, 1:, self.index+1] = array |
61 | if self.clean: | 61 | if self.clean: |
62 | mvolume[0, 0, self.index+1] = 1 | 62 | mvolume[0, 0, self.index+1] = 1 |
63 | + elif self.orientation == 'VOLUME': | ||
64 | + mvolume[:] = array | ||
63 | 65 | ||
64 | print "applying to", self.orientation, "at slice", self.index | 66 | print "applying to", self.orientation, "at slice", self.index |
65 | 67 | ||
@@ -106,7 +108,15 @@ class EditionHistory(object): | @@ -106,7 +108,15 @@ class EditionHistory(object): | ||
106 | ##self.index -= 1 | 108 | ##self.index -= 1 |
107 | ##h[self.index].commit_history(mvolume) | 109 | ##h[self.index].commit_history(mvolume) |
108 | #self._reload_slice(self.index - 1) | 110 | #self._reload_slice(self.index - 1) |
109 | - if actual_slices and actual_slices[h[self.index - 1].orientation] != h[self.index - 1].index: | 111 | + if h[self.index - 1].orientation == 'VOLUME': |
112 | + self.index -= 1 | ||
113 | + print "================================" | ||
114 | + print mvolume.shape | ||
115 | + print "================================" | ||
116 | + h[self.index].commit_history(mvolume) | ||
117 | + self._reload_slice(self.index) | ||
118 | + Publisher.sendMessage("Enable redo", True) | ||
119 | + elif actual_slices and actual_slices[h[self.index - 1].orientation] != h[self.index - 1].index: | ||
110 | self._reload_slice(self.index - 1) | 120 | self._reload_slice(self.index - 1) |
111 | else: | 121 | else: |
112 | self.index -= 1 | 122 | self.index -= 1 |
@@ -129,7 +139,12 @@ class EditionHistory(object): | @@ -129,7 +139,12 @@ class EditionHistory(object): | ||
129 | ##h[self.index].commit_history(mvolume) | 139 | ##h[self.index].commit_history(mvolume) |
130 | #self._reload_slice(self.index + 1) | 140 | #self._reload_slice(self.index + 1) |
131 | 141 | ||
132 | - if actual_slices and actual_slices[h[self.index + 1].orientation] != h[self.index + 1].index: | 142 | + if h[self.index + 1].orientation == 'VOLUME': |
143 | + self.index += 1 | ||
144 | + h[self.index].commit_history(mvolume) | ||
145 | + self._reload_slice(self.index) | ||
146 | + Publisher.sendMessage("Enable undo", True) | ||
147 | + elif actual_slices and actual_slices[h[self.index + 1].orientation] != h[self.index + 1].index: | ||
133 | self._reload_slice(self.index + 1) | 148 | self._reload_slice(self.index + 1) |
134 | else: | 149 | else: |
135 | self.index += 1 | 150 | self.index += 1 |
invesalius/data/slice_.py
@@ -1402,7 +1402,8 @@ class Slice(object): | @@ -1402,7 +1402,8 @@ class Slice(object): | ||
1402 | buffer_slices = self.buffer_slices | 1402 | buffer_slices = self.buffer_slices |
1403 | actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, | 1403 | actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, |
1404 | "CORONAL": buffer_slices["CORONAL"].index, | 1404 | "CORONAL": buffer_slices["CORONAL"].index, |
1405 | - "SAGITAL": buffer_slices["SAGITAL"].index,} | 1405 | + "SAGITAL": buffer_slices["SAGITAL"].index, |
1406 | + "VOLUME": 0} | ||
1406 | self.current_mask.undo_history(actual_slices) | 1407 | self.current_mask.undo_history(actual_slices) |
1407 | for o in self.buffer_slices: | 1408 | for o in self.buffer_slices: |
1408 | self.buffer_slices[o].discard_mask() | 1409 | self.buffer_slices[o].discard_mask() |
@@ -1413,7 +1414,8 @@ class Slice(object): | @@ -1413,7 +1414,8 @@ class Slice(object): | ||
1413 | buffer_slices = self.buffer_slices | 1414 | buffer_slices = self.buffer_slices |
1414 | actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, | 1415 | actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, |
1415 | "CORONAL": buffer_slices["CORONAL"].index, | 1416 | "CORONAL": buffer_slices["CORONAL"].index, |
1416 | - "SAGITAL": buffer_slices["SAGITAL"].index,} | 1417 | + "SAGITAL": buffer_slices["SAGITAL"].index, |
1418 | + "VOLUME": 0} | ||
1417 | self.current_mask.redo_history(actual_slices) | 1419 | self.current_mask.redo_history(actual_slices) |
1418 | for o in self.buffer_slices: | 1420 | for o in self.buffer_slices: |
1419 | self.buffer_slices[o].discard_mask() | 1421 | self.buffer_slices[o].discard_mask() |
invesalius/data/styles.py
@@ -40,8 +40,11 @@ from scipy.ndimage import watershed_ift, generate_binary_structure | @@ -40,8 +40,11 @@ from scipy.ndimage import watershed_ift, generate_binary_structure | ||
40 | from skimage.morphology import watershed | 40 | from skimage.morphology import watershed |
41 | from skimage import filter | 41 | from skimage import filter |
42 | 42 | ||
43 | +from gui import dialogs | ||
43 | from .measures import MeasureData | 44 | from .measures import MeasureData |
44 | 45 | ||
46 | +from . import floodfill | ||
47 | + | ||
45 | import watershed_process | 48 | import watershed_process |
46 | 49 | ||
47 | import utils | 50 | import utils |
@@ -1747,6 +1750,170 @@ class ReorientImageInteractorStyle(DefaultInteractorStyle): | @@ -1747,6 +1750,170 @@ class ReorientImageInteractorStyle(DefaultInteractorStyle): | ||
1747 | buffer_.discard_vtk_image() | 1750 | buffer_.discard_vtk_image() |
1748 | buffer_.discard_image() | 1751 | buffer_.discard_image() |
1749 | 1752 | ||
1753 | + | ||
1754 | +class FFillConfig(object): | ||
1755 | + __metaclass__= utils.Singleton | ||
1756 | + def __init__(self): | ||
1757 | + self.dlg_visible = False | ||
1758 | + self.target = "2D" | ||
1759 | + self.con_2d = 4 | ||
1760 | + self.con_3d = 6 | ||
1761 | + | ||
1762 | + | ||
1763 | +class FloodFillMaskInteractorStyle(DefaultInteractorStyle): | ||
1764 | + def __init__(self, viewer): | ||
1765 | + DefaultInteractorStyle.__init__(self, viewer) | ||
1766 | + | ||
1767 | + self.viewer = viewer | ||
1768 | + self.orientation = self.viewer.orientation | ||
1769 | + | ||
1770 | + self.picker = vtk.vtkWorldPointPicker() | ||
1771 | + self.slice_actor = viewer.slice_data.actor | ||
1772 | + self.slice_data = viewer.slice_data | ||
1773 | + | ||
1774 | + self.config = FFillConfig() | ||
1775 | + self.dlg_ffill = None | ||
1776 | + | ||
1777 | + self.t0 = 0 | ||
1778 | + self.t1 = 1 | ||
1779 | + self.fill_value = 254 | ||
1780 | + | ||
1781 | + self._dlg_title = _(u"Fill holes") | ||
1782 | + | ||
1783 | + self.AddObserver("LeftButtonPressEvent", self.OnFFClick) | ||
1784 | + | ||
1785 | + def SetUp(self): | ||
1786 | + if not self.config.dlg_visible: | ||
1787 | + self.config.dlg_visible = True | ||
1788 | + self.dlg_ffill = dialogs.FFillOptionsDialog(self._dlg_title, self.config) | ||
1789 | + self.dlg_ffill.Show() | ||
1790 | + | ||
1791 | + def CleanUp(self): | ||
1792 | + if (self.dlg_ffill is not None) and (self.config.dlg_visible): | ||
1793 | + self.config.dlg_visible = False | ||
1794 | + self.dlg_ffill.Destroy() | ||
1795 | + self.dlg_ffill = None | ||
1796 | + | ||
1797 | + def OnFFClick(self, obj, evt): | ||
1798 | + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): | ||
1799 | + return | ||
1800 | + | ||
1801 | + viewer = self.viewer | ||
1802 | + iren = viewer.interactor | ||
1803 | + | ||
1804 | + mouse_x, mouse_y = iren.GetEventPosition() | ||
1805 | + render = iren.FindPokedRenderer(mouse_x, mouse_y) | ||
1806 | + slice_data = viewer.get_slice_data(render) | ||
1807 | + | ||
1808 | + self.picker.Pick(mouse_x, mouse_y, 0, render) | ||
1809 | + | ||
1810 | + coord = self.get_coordinate_cursor() | ||
1811 | + position = slice_data.actor.GetInput().FindPoint(coord) | ||
1812 | + | ||
1813 | + if position != -1: | ||
1814 | + coord = slice_data.actor.GetInput().GetPoint(position) | ||
1815 | + | ||
1816 | + if position < 0: | ||
1817 | + position = viewer.calculate_matrix_position(coord) | ||
1818 | + | ||
1819 | + mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] | ||
1820 | + x, y, z = self.calcultate_scroll_position(position) | ||
1821 | + if mask[z, y, x] < self.t0 or mask[z, y, x] > self.t1: | ||
1822 | + return | ||
1823 | + | ||
1824 | + if self.config.target == "3D": | ||
1825 | + bstruct = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') | ||
1826 | + self.viewer.slice_.do_threshold_to_all_slices() | ||
1827 | + cp_mask = self.viewer.slice_.current_mask.matrix.copy() | ||
1828 | + else: | ||
1829 | + _bstruct = generate_binary_structure(2, CON2D[self.config.con_2d]) | ||
1830 | + if self.orientation == 'AXIAL': | ||
1831 | + bstruct = np.zeros((1, 3, 3), dtype='uint8') | ||
1832 | + bstruct[0] = _bstruct | ||
1833 | + elif self.orientation == 'CORONAL': | ||
1834 | + bstruct = np.zeros((3, 1, 3), dtype='uint8') | ||
1835 | + bstruct[:, 0, :] = _bstruct | ||
1836 | + elif self.orientation == 'SAGITAL': | ||
1837 | + bstruct = np.zeros((3, 3, 1), dtype='uint8') | ||
1838 | + bstruct[:, :, 0] = _bstruct | ||
1839 | + | ||
1840 | + | ||
1841 | + floodfill.floodfill_threshold(mask, [[x, y, z]], self.t0, self.t1, self.fill_value, bstruct, mask) | ||
1842 | + | ||
1843 | + if self.config.target == '2D': | ||
1844 | + b_mask = self.viewer.slice_.buffer_slices[self.orientation].mask | ||
1845 | + index = self.viewer.slice_.buffer_slices[self.orientation].index | ||
1846 | + | ||
1847 | + if self.orientation == 'AXIAL': | ||
1848 | + p_mask = mask[index,:,:].copy() | ||
1849 | + elif self.orientation == 'CORONAL': | ||
1850 | + p_mask = mask[:, index, :].copy() | ||
1851 | + elif self.orientation == 'SAGITAL': | ||
1852 | + p_mask = mask[:, :, index].copy() | ||
1853 | + | ||
1854 | + self.viewer.slice_.current_mask.save_history(index, self.orientation, p_mask, b_mask) | ||
1855 | + else: | ||
1856 | + self.viewer.slice_.current_mask.save_history(0, 'VOLUME', self.viewer.slice_.current_mask.matrix.copy(), cp_mask) | ||
1857 | + | ||
1858 | + self.viewer.slice_.buffer_slices['AXIAL'].discard_mask() | ||
1859 | + self.viewer.slice_.buffer_slices['CORONAL'].discard_mask() | ||
1860 | + self.viewer.slice_.buffer_slices['SAGITAL'].discard_mask() | ||
1861 | + | ||
1862 | + self.viewer.slice_.buffer_slices['AXIAL'].discard_vtk_mask() | ||
1863 | + self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask() | ||
1864 | + self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() | ||
1865 | + | ||
1866 | + self.viewer.slice_.current_mask.was_edited = True | ||
1867 | + Publisher.sendMessage('Reload actual slice') | ||
1868 | + | ||
1869 | + def get_coordinate_cursor(self): | ||
1870 | + # Find position | ||
1871 | + x, y, z = self.picker.GetPickPosition() | ||
1872 | + bounds = self.viewer.slice_data.actor.GetBounds() | ||
1873 | + if bounds[0] == bounds[1]: | ||
1874 | + x = bounds[0] | ||
1875 | + elif bounds[2] == bounds[3]: | ||
1876 | + y = bounds[2] | ||
1877 | + elif bounds[4] == bounds[5]: | ||
1878 | + z = bounds[4] | ||
1879 | + return x, y, z | ||
1880 | + | ||
1881 | + def calcultate_scroll_position(self, position): | ||
1882 | + # Based in the given coord (x, y, z), returns a list with the scroll positions for each | ||
1883 | + # orientation, being the first position the sagital, second the coronal | ||
1884 | + # and the last, axial. | ||
1885 | + | ||
1886 | + if self.orientation == 'AXIAL': | ||
1887 | + image_width = self.slice_actor.GetInput().GetDimensions()[0] | ||
1888 | + axial = self.slice_data.number | ||
1889 | + coronal = position / image_width | ||
1890 | + sagital = position % image_width | ||
1891 | + | ||
1892 | + elif self.orientation == 'CORONAL': | ||
1893 | + image_width = self.slice_actor.GetInput().GetDimensions()[0] | ||
1894 | + axial = position / image_width | ||
1895 | + coronal = self.slice_data.number | ||
1896 | + sagital = position % image_width | ||
1897 | + | ||
1898 | + elif self.orientation == 'SAGITAL': | ||
1899 | + image_width = self.slice_actor.GetInput().GetDimensions()[1] | ||
1900 | + axial = position / image_width | ||
1901 | + coronal = position % image_width | ||
1902 | + sagital = self.slice_data.number | ||
1903 | + | ||
1904 | + return sagital, coronal, axial | ||
1905 | + | ||
1906 | + | ||
1907 | +class RemoveMaskPartsInteractorStyle(FloodFillMaskInteractorStyle): | ||
1908 | + def __init__(self, viewer): | ||
1909 | + FloodFillMaskInteractorStyle.__init__(self, viewer) | ||
1910 | + self.t0 = 254 | ||
1911 | + self.t1 = 255 | ||
1912 | + self.fill_value = 1 | ||
1913 | + | ||
1914 | + self._dlg_title = _(u"Remove parts") | ||
1915 | + | ||
1916 | + | ||
1750 | def get_style(style): | 1917 | def get_style(style): |
1751 | STYLES = { | 1918 | STYLES = { |
1752 | const.STATE_DEFAULT: DefaultInteractorStyle, | 1919 | const.STATE_DEFAULT: DefaultInteractorStyle, |
@@ -1762,5 +1929,9 @@ def get_style(style): | @@ -1762,5 +1929,9 @@ def get_style(style): | ||
1762 | const.SLICE_STATE_EDITOR: EditorInteractorStyle, | 1929 | const.SLICE_STATE_EDITOR: EditorInteractorStyle, |
1763 | const.SLICE_STATE_WATERSHED: WaterShedInteractorStyle, | 1930 | const.SLICE_STATE_WATERSHED: WaterShedInteractorStyle, |
1764 | const.SLICE_STATE_REORIENT: ReorientImageInteractorStyle, | 1931 | const.SLICE_STATE_REORIENT: ReorientImageInteractorStyle, |
1932 | + const.SLICE_STATE_MASK_FFILL: FloodFillMaskInteractorStyle, | ||
1933 | + const.SLICE_STATE_REMOVE_MASK_PARTS: RemoveMaskPartsInteractorStyle, | ||
1765 | } | 1934 | } |
1766 | return STYLES[style] | 1935 | return STYLES[style] |
1936 | + | ||
1937 | + |
invesalius/data/transforms.pyx
@@ -12,7 +12,7 @@ from cython.parallel import prange | @@ -12,7 +12,7 @@ from cython.parallel import prange | ||
12 | @cython.boundscheck(False) # turn of bounds-checking for entire function | 12 | @cython.boundscheck(False) # turn of bounds-checking for entire function |
13 | @cython.cdivision(True) | 13 | @cython.cdivision(True) |
14 | @cython.wraparound(False) | 14 | @cython.wraparound(False) |
15 | -cdef inline void mul_mat4_vec4(np.float64_t[:, :] M, | 15 | +cdef inline void mul_mat4_vec4(double[:, :] M, |
16 | double* coord, | 16 | double* coord, |
17 | double* out) nogil: | 17 | double* out) nogil: |
18 | 18 | ||
@@ -25,7 +25,7 @@ cdef inline void mul_mat4_vec4(np.float64_t[:, :] M, | @@ -25,7 +25,7 @@ cdef inline void mul_mat4_vec4(np.float64_t[:, :] M, | ||
25 | @cython.boundscheck(False) # turn of bounds-checking for entire function | 25 | @cython.boundscheck(False) # turn of bounds-checking for entire function |
26 | @cython.cdivision(True) | 26 | @cython.cdivision(True) |
27 | @cython.wraparound(False) | 27 | @cython.wraparound(False) |
28 | -cdef image_t coord_transform(image_t[:, :, :] volume, np.float64_t[:, :] M, int x, int y, int z, double sx, double sy, double sz, short minterpol, image_t cval) nogil: | 28 | +cdef image_t coord_transform(image_t[:, :, :] volume, double[:, :] M, int x, int y, int z, double sx, double sy, double sz, short minterpol, image_t cval) nogil: |
29 | 29 | ||
30 | cdef double coord[4] | 30 | cdef double coord[4] |
31 | coord[0] = z*sz | 31 | coord[0] = z*sz |
@@ -71,7 +71,7 @@ cdef image_t coord_transform(image_t[:, :, :] volume, np.float64_t[:, :] M, int | @@ -71,7 +71,7 @@ cdef image_t coord_transform(image_t[:, :, :] volume, np.float64_t[:, :] M, int | ||
71 | @cython.wraparound(False) | 71 | @cython.wraparound(False) |
72 | def apply_view_matrix_transform(image_t[:, :, :] volume, | 72 | def apply_view_matrix_transform(image_t[:, :, :] volume, |
73 | spacing, | 73 | spacing, |
74 | - np.float64_t[:, :] M, | 74 | + double[:, :] M, |
75 | unsigned int n, str orientation, | 75 | unsigned int n, str orientation, |
76 | int minterpol, | 76 | int minterpol, |
77 | image_t cval, | 77 | image_t cval, |
invesalius/data/viewer_slice.py
@@ -1344,7 +1344,7 @@ class Viewer(wx.Panel): | @@ -1344,7 +1344,7 @@ class Viewer(wx.Panel): | ||
1344 | actor = vtk.vtkImageActor() | 1344 | actor = vtk.vtkImageActor() |
1345 | # TODO: Create a option to let the user set if he wants to interpolate | 1345 | # TODO: Create a option to let the user set if he wants to interpolate |
1346 | # the slice images. | 1346 | # the slice images. |
1347 | - #actor.InterpolateOff() | 1347 | + actor.InterpolateOff() |
1348 | slice_data = sd.SliceData() | 1348 | slice_data = sd.SliceData() |
1349 | slice_data.SetOrientation(self.orientation) | 1349 | slice_data.SetOrientation(self.orientation) |
1350 | slice_data.renderer = renderer | 1350 | slice_data.renderer = renderer |
invesalius/gui/dialogs.py
@@ -1838,3 +1838,85 @@ def BitmapNotSameSize(): | @@ -1838,3 +1838,85 @@ def BitmapNotSameSize(): | ||
1838 | 1838 | ||
1839 | dlg.ShowModal() | 1839 | dlg.ShowModal() |
1840 | dlg.Destroy() | 1840 | dlg.Destroy() |
1841 | + | ||
1842 | + | ||
1843 | +class FFillOptionsDialog(wx.Dialog): | ||
1844 | + def __init__(self, title, config): | ||
1845 | + pre = wx.PreDialog() | ||
1846 | + pre.Create(wx.GetApp().GetTopWindow(), -1, title, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) | ||
1847 | + self.PostCreate(pre) | ||
1848 | + | ||
1849 | + self.config = config | ||
1850 | + | ||
1851 | + self._init_gui() | ||
1852 | + | ||
1853 | + def _init_gui(self): | ||
1854 | + sizer = wx.GridBagSizer(5, 6) | ||
1855 | + | ||
1856 | + flag_labels = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL | ||
1857 | + | ||
1858 | + # self.target = wx.RadioBox(self, -1, "", | ||
1859 | + # choices=[_(u"2D - Actual slice"), _(u"3D - Entire volume")], | ||
1860 | + # style=wx.NO_BORDER | wx.VERTICAL) | ||
1861 | + self.target_2d = wx.RadioButton(self, -1, _(u"2D - Actual slice")) | ||
1862 | + self.target_3d = wx.RadioButton(self, -1, _(u"3D - All slices")) | ||
1863 | + | ||
1864 | + if self.config.target == "2D": | ||
1865 | + self.target_2d.SetValue(1) | ||
1866 | + else: | ||
1867 | + self.target_3d.SetValue(1) | ||
1868 | + | ||
1869 | + choices2d = ["4", "8"] | ||
1870 | + choices3d = ["6", "18", "26"] | ||
1871 | + self.conect2D = wx.RadioBox(self, -1, _(u"2D Connectivity"), choices=choices2d, style=wx.NO_BORDER | wx.HORIZONTAL) | ||
1872 | + self.conect3D = wx.RadioBox(self, -1, _(u"3D Connectivity"), choices=choices3d, style=wx.NO_BORDER | wx.HORIZONTAL) | ||
1873 | + | ||
1874 | + try: | ||
1875 | + self.conect2D.SetSelection(choices2d.index(str(self.config.con_2d))) | ||
1876 | + except ValueError: | ||
1877 | + print "ERROR 2D" | ||
1878 | + self.conect2D.SetSelection(0) | ||
1879 | + self.config.con_2d = 4 | ||
1880 | + | ||
1881 | + try: | ||
1882 | + self.conect3D.SetSelection(choices3d.index(str(self.config.con_3d))) | ||
1883 | + except ValueError: | ||
1884 | + print "ERROR 3D" | ||
1885 | + self.conect3D.SetSelection(0) | ||
1886 | + self.config.con_3d = 6 | ||
1887 | + | ||
1888 | + sizer.Add(wx.StaticText(self, -1, _(u"Parameters")), (0, 0), flag=wx.TOP|wx.LEFT|wx.RIGHT, border=7) | ||
1889 | + sizer.AddStretchSpacer((0, 5)) | ||
1890 | + sizer.Add(self.target_2d, (1, 0), (1, 3), flag=wx.LEFT|wx.RIGHT, border=9) | ||
1891 | + sizer.Add(self.target_3d, (2, 0), (1, 3), flag=wx.LEFT|wx.RIGHT, border=9) | ||
1892 | + sizer.Add(self.conect2D, (3, 0), flag=wx.TOP|wx.LEFT|wx.RIGHT, border=9) | ||
1893 | + sizer.Add(self.conect3D, (4, 0), flag=wx.ALL, border=9) | ||
1894 | + | ||
1895 | + self.SetSizer(sizer) | ||
1896 | + sizer.Fit(self) | ||
1897 | + self.Layout() | ||
1898 | + | ||
1899 | + self.Bind(wx.EVT_RADIOBUTTON, self.OnSetTarget) | ||
1900 | + self.conect2D.Bind(wx.EVT_RADIOBOX, self.OnSetCon2D) | ||
1901 | + self.conect3D.Bind(wx.EVT_RADIOBOX, self.OnSetCon3D) | ||
1902 | + self.Bind(wx.EVT_CLOSE, self.OnClose) | ||
1903 | + | ||
1904 | + def OnSetTarget(self, evt): | ||
1905 | + if self.target_2d.GetValue(): | ||
1906 | + self.config.target = "2D" | ||
1907 | + else: | ||
1908 | + self.config.target = "3D" | ||
1909 | + | ||
1910 | + def OnSetCon2D(self, evt): | ||
1911 | + self.config.con_2d = int(self.conect2D.GetStringSelection()) | ||
1912 | + print self.config.con_2d | ||
1913 | + | ||
1914 | + def OnSetCon3D(self, evt): | ||
1915 | + self.config.con_3d = int(self.conect3D.GetStringSelection()) | ||
1916 | + print self.config.con_3d | ||
1917 | + | ||
1918 | + def OnClose(self, evt): | ||
1919 | + if self.config.dlg_visible: | ||
1920 | + Publisher.sendMessage('Disable style', const.SLICE_STATE_MASK_FFILL) | ||
1921 | + evt.Skip() | ||
1922 | + self.Destroy() |
invesalius/gui/frame.py
@@ -439,6 +439,12 @@ class Frame(wx.Frame): | @@ -439,6 +439,12 @@ class Frame(wx.Frame): | ||
439 | elif id == const.ID_REORIENT_IMG: | 439 | elif id == const.ID_REORIENT_IMG: |
440 | self.OnReorientImg() | 440 | self.OnReorientImg() |
441 | 441 | ||
442 | + elif id == const.ID_FLOODFILL_MASK: | ||
443 | + self.OnFillHolesManually() | ||
444 | + | ||
445 | + elif id == const.ID_REMOVE_MASK_PART: | ||
446 | + self.OnRemoveMaskParts() | ||
447 | + | ||
442 | def OnSize(self, evt): | 448 | def OnSize(self, evt): |
443 | """ | 449 | """ |
444 | Refresh GUI when frame is resized. | 450 | Refresh GUI when frame is resized. |
@@ -559,6 +565,12 @@ class Frame(wx.Frame): | @@ -559,6 +565,12 @@ class Frame(wx.Frame): | ||
559 | rdlg = dlg.ReorientImageDialog() | 565 | rdlg = dlg.ReorientImageDialog() |
560 | rdlg.Show() | 566 | rdlg.Show() |
561 | 567 | ||
568 | + def OnFillHolesManually(self): | ||
569 | + Publisher.sendMessage('Enable style', const.SLICE_STATE_MASK_FFILL) | ||
570 | + | ||
571 | + def OnRemoveMaskParts(self): | ||
572 | + Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS) | ||
573 | + | ||
562 | # ------------------------------------------------------------------ | 574 | # ------------------------------------------------------------------ |
563 | # ------------------------------------------------------------------ | 575 | # ------------------------------------------------------------------ |
564 | # ------------------------------------------------------------------ | 576 | # ------------------------------------------------------------------ |
@@ -578,7 +590,9 @@ class MenuBar(wx.MenuBar): | @@ -578,7 +590,9 @@ class MenuBar(wx.MenuBar): | ||
578 | self.enable_items = [const.ID_PROJECT_SAVE, | 590 | self.enable_items = [const.ID_PROJECT_SAVE, |
579 | const.ID_PROJECT_SAVE_AS, | 591 | const.ID_PROJECT_SAVE_AS, |
580 | const.ID_PROJECT_CLOSE, | 592 | const.ID_PROJECT_CLOSE, |
581 | - const.ID_REORIENT_IMG] | 593 | + const.ID_REORIENT_IMG, |
594 | + const.ID_FLOODFILL_MASK, | ||
595 | + const.ID_REMOVE_MASK_PART,] | ||
582 | self.__init_items() | 596 | self.__init_items() |
583 | self.__bind_events() | 597 | self.__bind_events() |
584 | 598 | ||
@@ -689,6 +703,12 @@ class MenuBar(wx.MenuBar): | @@ -689,6 +703,12 @@ class MenuBar(wx.MenuBar): | ||
689 | self.clean_mask_menu = mask_menu.Append(const.ID_CLEAN_MASK, _(u"Clean Mask\tCtrl+Shift+A")) | 703 | self.clean_mask_menu = mask_menu.Append(const.ID_CLEAN_MASK, _(u"Clean Mask\tCtrl+Shift+A")) |
690 | self.clean_mask_menu.Enable(False) | 704 | self.clean_mask_menu.Enable(False) |
691 | 705 | ||
706 | + self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill holes manually")) | ||
707 | + self.fill_hole_mask_menu.Enable(False) | ||
708 | + | ||
709 | + self.remove_mask_part_menu = mask_menu.Append(const.ID_REMOVE_MASK_PART, _(u"Remove parts")) | ||
710 | + self.remove_mask_part_menu.Enable(False) | ||
711 | + | ||
692 | tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu) | 712 | tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu) |
693 | 713 | ||
694 | # Image menu | 714 | # Image menu |
setup.py
@@ -25,6 +25,10 @@ if sys.platform == 'linux2': | @@ -25,6 +25,10 @@ if sys.platform == 'linux2': | ||
25 | include_dirs=[numpy.get_include()], | 25 | include_dirs=[numpy.get_include()], |
26 | extra_compile_args=['-fopenmp',], | 26 | extra_compile_args=['-fopenmp',], |
27 | extra_link_args=['-fopenmp',]), | 27 | extra_link_args=['-fopenmp',]), |
28 | + | ||
29 | + Extension("invesalius.data.floodfill", ["invesalius/data/floodfill.pyx"], | ||
30 | + include_dirs=[numpy.get_include()], | ||
31 | + language='c++',), | ||
28 | ]) | 32 | ]) |
29 | ) | 33 | ) |
30 | 34 | ||
@@ -32,18 +36,22 @@ elif sys.platform == 'win32': | @@ -32,18 +36,22 @@ elif sys.platform == 'win32': | ||
32 | setup( | 36 | setup( |
33 | cmdclass = {'build_ext': build_ext}, | 37 | cmdclass = {'build_ext': build_ext}, |
34 | ext_modules = cythonize([ Extension("invesalius.data.mips", ["invesalius/data/mips.pyx"], | 38 | ext_modules = cythonize([ Extension("invesalius.data.mips", ["invesalius/data/mips.pyx"], |
35 | - include_dirs = [numpy.get_include()], | ||
36 | - extra_compile_args=['/openmp'],), | 39 | + include_dirs = [numpy.get_include()], |
40 | + extra_compile_args=['/openmp'],), | ||
37 | 41 | ||
38 | - Extension("invesalius.data.interpolation", ["invesalius/data/interpolation.pyx"], | ||
39 | - include_dirs=[numpy.get_include()], | ||
40 | - extra_compile_args=['/openmp'],), | 42 | + Extension("invesalius.data.interpolation", ["invesalius/data/interpolation.pyx"], |
43 | + include_dirs=[numpy.get_include()], | ||
44 | + extra_compile_args=['/openmp'],), | ||
41 | 45 | ||
42 | - Extension("invesalius.data.transforms", ["invesalius/data/transforms.pyx"], | ||
43 | - include_dirs=[numpy.get_include()], | ||
44 | - extra_compile_args=['/openmp'],), | 46 | + Extension("invesalius.data.transforms", ["invesalius/data/transforms.pyx"], |
47 | + include_dirs=[numpy.get_include()], | ||
48 | + extra_compile_args=['/openmp'],), | ||
49 | + | ||
50 | + Extension("invesalius.data.floodfill", ["invesalius/data/floodfill.pyx"], | ||
51 | + include_dirs=[numpy.get_include()], | ||
52 | + language='c++',), | ||
45 | ]) | 53 | ]) |
46 | - ) | 54 | + ) |
47 | 55 | ||
48 | else: | 56 | else: |
49 | setup( | 57 | setup( |
@@ -63,5 +71,9 @@ else: | @@ -63,5 +71,9 @@ else: | ||
63 | include_dirs=[numpy.get_include()], | 71 | include_dirs=[numpy.get_include()], |
64 | extra_compile_args=['-fopenmp',], | 72 | extra_compile_args=['-fopenmp',], |
65 | extra_link_args=['-fopenmp',]), | 73 | extra_link_args=['-fopenmp',]), |
74 | + | ||
75 | + Extension("invesalius.data.floodfill", ["invesalius/data/floodfill.pyx"], | ||
76 | + include_dirs=[numpy.get_include()], | ||
77 | + language='c++',), | ||
66 | ]) | 78 | ]) |
67 | ) | 79 | ) |