Diff of /dosma/gui/ims.py [000000] .. [030aeb]

Switch to unified view

a b/dosma/gui/ims.py
1
import logging
2
import os
3
import sys
4
import tkinter as tk
5
from tkinter import IntVar, Radiobutton, filedialog, messagebox, ttk
6
from typing import Dict
7
8
import numpy as np
9
import Pmw
10
from skimage.color import label2rgb
11
from skimage.measure import label
12
13
from dosma.cli import SUPPORTED_QUANTITATIVE_VALUES, SUPPORTED_SCAN_TYPES, parse_args
14
from dosma.core.io import format_io_utils as fio_utils
15
from dosma.core.orientation import AXIAL, CORONAL, SAGITTAL
16
from dosma.gui.dosma_gui import ScanReader
17
from dosma.gui.gui_utils.filedialog_reader import FileDialogReader
18
from dosma.gui.im_viewer import IndexTracker
19
from dosma.gui.preferences_viewer import PreferencesManager
20
from dosma.msk import knee
21
22
import matplotlib
23
import matplotlib.pyplot as plt
24
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
25
26
matplotlib.use("TkAgg")
27
LARGE_FONT = ("Verdana", 12)
28
29
_logger = logging.getLogger(__name__)
30
31
32
class DosmaViewer(tk.Tk):
33
    def __init__(self, *args, **kwargs):
34
        tk.Tk.__init__(self, *args, **kwargs)
35
36
        container = tk.Frame(self)
37
        container.pack(side="top", fill="both", expand=True)
38
        container.grid_rowconfigure(0, weight=1)
39
        container.grid_columnconfigure(0, weight=1)
40
41
        self.frames = {}
42
        self.protocol("WM_DELETE_WINDOW", self.on_closing)
43
        for F in (StartPage, DosmaFrame, PageThree, AnalysisFrame):
44
            frame = F(container, self)
45
46
            self.frames[F] = frame
47
48
            frame.grid(row=0, column=0, sticky="nsew")
49
50
        self.show_frame(StartPage)
51
52
        self.pref = PreferencesManager()
53
54
    def on_closing(self):
55
        if messagebox.askokcancel("Quit", "Do you want to quit?"):
56
            sys.exit()
57
58
    def show_frame(self, cont):
59
        frame = self.frames[cont]
60
        frame.tkraise()
61
62
    def show_preferences(self):
63
        self.pref.show_window(self)
64
65
66
class StartPage(tk.Frame):
67
    def __init__(self, parent, controller):
68
        tk.Frame.__init__(self, parent)
69
        # photo = tk.PhotoImage(file="./defaults/skel-rotate.gif")
70
        # label1 = tk.Label(image=photo)
71
        # label1.pack()
72
73
        label = tk.Label(self, text="Start Page", font=LARGE_FONT)
74
        label.pack(pady=10, padx=10)
75
76
        button2 = ttk.Button(self, text="Scan", command=lambda: controller.show_frame(DosmaFrame))
77
        button2.pack()
78
79
        button3 = ttk.Button(
80
            self, text="Knee Analysis", command=lambda: controller.show_frame(AnalysisFrame)
81
        )
82
        button3.pack()
83
84
        button3 = ttk.Button(
85
            self, text="Image Viewer", command=lambda: controller.show_frame(PageThree)
86
        )
87
        button3.pack()
88
89
        button3 = ttk.Button(
90
            self, text="Preferences", command=lambda: controller.show_preferences()
91
        )
92
        button3.pack()
93
94
95
class AnalysisFrame(tk.Frame):
96
    __TISSUES_KEY = "Tissues"
97
    __QUANTITATIVE_VALUES_KEY = "Quantitative values"
98
    __LOAD_PATH_KEY = "Load data"
99
100
    __PID_KEY = "pid"
101
    __MEDIAL_TO_LATERAL_ORIENTATION_KEY = "ml"
102
103
    def __init__(self, parent, controller):
104
        tk.Frame.__init__(self, parent)
105
106
        self.manager: Dict = {}
107
        self.gui_manager: Dict = {}
108
        self.balloon = Pmw.Balloon()
109
110
        self.__init_manager()
111
112
        self.__base_gui()
113
        self.preferences = PreferencesManager()
114
        self.file_dialog_reader = FileDialogReader()
115
        self.scan_reader = ScanReader(self)
116
117
        button1 = ttk.Button(self, text="Home", command=lambda: controller.show_frame(StartPage))
118
        button1.pack(anchor="se", side="right")
119
120
        button1 = ttk.Button(self, text="Run", command=lambda: self.execute())
121
        button1.pack(anchor="sw", side="left")
122
123
    def execute(self):
124
        try:
125
            load_path = self.manager[self.__LOAD_PATH_KEY].get()
126
            if not load_path:
127
                raise ValueError("Load path not defined")
128
129
            preferences_str = self.preferences.get_cmd_line_str().strip()
130
131
            tissue_str = ""
132
            for c, t in enumerate(self.manager[self.__TISSUES_KEY]):
133
                if t.get():
134
                    tissue_str += "--%s " % knee.SUPPORTED_TISSUES[c].STR_ID
135
            tissue_str = tissue_str.strip()
136
137
            if not tissue_str:
138
                raise ValueError("No tissues selected")
139
140
            qv_str = ""
141
            for c, qv in enumerate(self.manager[self.__QUANTITATIVE_VALUES_KEY]):
142
                if qv.get():
143
                    qv_str += "--%s " % SUPPORTED_QUANTITATIVE_VALUES[c].name.lower()
144
            qv_str = qv_str.strip()
145
146
            if not qv_str:
147
                raise ValueError("No quantitative values selected")
148
149
            pid = self.manager[self.__PID_KEY].get()
150
            medial_to_lateral = self.manager[self.__MEDIAL_TO_LATERAL_ORIENTATION_KEY].get()
151
152
            if not pid:
153
                raise ValueError("No PID was provided")
154
155
            # analysis string
156
            str_f = "--l %s %s knee %s --pid %s %s %s" % (
157
                load_path,
158
                preferences_str,
159
                tissue_str,
160
                pid,
161
                "--ml" if medial_to_lateral else "",
162
                qv_str,
163
            )
164
            str_f = str_f.strip()
165
            parse_args(str_f.split())
166
        except Exception as e:
167
            tk.messagebox.showerror(str(type(e)), e.__str__())
168
169
    def __init_manager(self):
170
        self.manager[self.__LOAD_PATH_KEY] = tk.StringVar()
171
        self.manager[self.__TISSUES_KEY] = [
172
            tk.BooleanVar() for i in range(len(knee.SUPPORTED_TISSUES))
173
        ]
174
        self.manager[self.__QUANTITATIVE_VALUES_KEY] = [
175
            tk.BooleanVar() for i in range(len(SUPPORTED_QUANTITATIVE_VALUES))
176
        ]
177
178
        self.manager[self.__PID_KEY] = tk.StringVar()
179
        self.manager[self.__MEDIAL_TO_LATERAL_ORIENTATION_KEY] = tk.BooleanVar()
180
181
    def __display_pid_info(self):
182
        hb = tk.Frame(self)
183
        hb.pack(side="top", anchor="nw")
184
        _label = tk.Label(hb, text=self.__PID_KEY.upper())
185
        _label.pack(side="left", anchor="w", pady=10)
186
        t = tk.Entry(hb, textvariable=self.manager[self.__PID_KEY])
187
        t.pack(side="left", anchor="w", pady=10)
188
        self.balloon.bind(_label, "Patient id")
189
190
    def __display_data_loader(self):
191
        hb = tk.Frame(self)
192
193
        filedialog = FileDialogReader(self.manager[self.__LOAD_PATH_KEY])
194
        b = tk.Button(
195
            hb,
196
            text=self.__LOAD_PATH_KEY,
197
            command=lambda fd=filedialog: self.manager[self.__LOAD_PATH_KEY].set(
198
                fd.get_save_dirpath()
199
            ),
200
        )
201
        b.pack(side="left", anchor="nw", pady=10)
202
203
        _label = tk.Label(hb, textvariable=self.manager[self.__LOAD_PATH_KEY])
204
        _label.pack(side="left", anchor="nw", pady=10)
205
206
        hb.pack(side="top", anchor="nw")
207
208
    def __display_multi_option(self, label, options_list, boolvar_list):
209
        hb = tk.Frame(self)
210
        _label = tk.Label(hb, text="%s:" % label)
211
        _label.pack(side="left", anchor="w")
212
        hb.pack(side="top", anchor="nw")
213
        frames = [tk.Frame(hb)] * (len(options_list) // 3 + 1)
214
        for ind, option in enumerate(options_list):
215
            f = frames[ind // 3]
216
            b = tk.Checkbutton(f, text=option, variable=boolvar_list[ind])
217
            b.pack(side="top", anchor="nw", pady=5)
218
219
        for f in frames:
220
            f.pack(side="left", anchor="nw")
221
222
        return hb
223
224
    def __display_tissues(self):
225
        tissue_names = [x.FULL_NAME for x in knee.SUPPORTED_TISSUES]
226
        _label = self.__display_multi_option(
227
            self.__TISSUES_KEY, tissue_names, self.manager[self.__TISSUES_KEY]
228
        )
229
        self.balloon.bind(_label, "Tissues to analyze")
230
231
    def __display_quant_vals(self):
232
        quantitative_value_names = [x.name for x in SUPPORTED_QUANTITATIVE_VALUES]
233
        _label = self.__display_multi_option(
234
            self.__QUANTITATIVE_VALUES_KEY,
235
            quantitative_value_names,
236
            self.manager[self.__QUANTITATIVE_VALUES_KEY],
237
        )
238
        self.balloon.bind(_label, "Quantitative values to analyze")
239
240
    def __display_knee_info(self):
241
        hb = tk.Frame(self)
242
        hb.pack(side="top", anchor="nw")
243
        _label = tk.Label(hb, text="Medial -> Lateral: ")
244
        _label.pack(side="left", anchor="w", pady=10)
245
        t = tk.Checkbutton(hb, variable=self.manager[self.__MEDIAL_TO_LATERAL_ORIENTATION_KEY])
246
        t.pack(side="left", anchor="w", pady=10)
247
248
        self.balloon.bind(_label, "Select if Dicoms proceed in medial->lateral direction")
249
250
    def __base_gui(self):
251
        self.__display_data_loader()
252
        self.__display_pid_info()
253
        self.__display_tissues()
254
        self.__display_knee_info()
255
        self.__display_quant_vals()
256
257
258
class DosmaFrame(tk.Frame):
259
    __SCAN_KEY = "Scan"
260
    __TISSUES_KEY = "Tissues"
261
262
    __DICOM_PATH_KEY = "Read dicoms"
263
    __LOAD_PATH_KEY = "Load data"
264
265
    __SAVE_PATH_KEY = "Save path"
266
267
    __DATA_KEY = "data"  # Track option menu for dicom/load path
268
    __DATA_PATH_KEY = "datapath"  # Track filepath associated with option menu
269
270
    __IGNORE_EXTENSION_KEY = "Ignore extension"
271
272
    def __init__(self, parent, controller):
273
        tk.Frame.__init__(self, parent)
274
275
        self.file_dialog_reader = FileDialogReader()
276
277
        self.manager: Dict = {}
278
        self.gui_manager: Dict = {}
279
        self.balloon = Pmw.Balloon()
280
281
        self.__init_manager()
282
283
        self.__base_gui()
284
        self.preferences = PreferencesManager()
285
        self.scan_reader = ScanReader(self)
286
287
        button1 = ttk.Button(self, text="Home", command=lambda: controller.show_frame(StartPage))
288
        button1.pack(anchor="se", side="right")
289
290
        button1 = ttk.Button(self, text="Run", command=lambda: self.execute())
291
        button1.pack(anchor="sw", side="left")
292
293
        self.InitUI()
294
295
    def execute(self):
296
        try:
297
            save_path = self.manager[self.__SAVE_PATH_KEY].get()
298
            if not save_path:
299
                raise ValueError("Save path not defined")
300
301
            action_str = self.scan_reader.get_cmd_line_str().strip()
302
303
            if not action_str:
304
                raise ValueError("No action selected")
305
306
            preferences_str = self.preferences.get_cmd_line_str().strip()
307
308
            source = "d"
309
            if self.manager[self.__DATA_KEY].get() == self.__LOAD_PATH_KEY:
310
                source = "l"
311
312
            tissue_str = ""
313
            for c, t in enumerate(self.manager[self.__TISSUES_KEY]):
314
                if t.get():
315
                    tissue_str += "--%s " % knee.SUPPORTED_TISSUES[c].STR_ID
316
            tissue_str = tissue_str.strip()
317
318
            if not tissue_str:
319
                raise ValueError("No tissues selected")
320
321
            ignore_ext = self.manager[self.__IGNORE_EXTENSION_KEY].get()
322
323
            str_f = "--%s %s --s %s %s %s %s %s %s" % (
324
                source,
325
                self.manager[self.__DATA_PATH_KEY].get(),
326
                save_path,
327
                preferences_str,
328
                "--ignore_ext" if ignore_ext else "",
329
                self.manager[self.__SCAN_KEY].get(),
330
                tissue_str,
331
                action_str,
332
            )
333
334
            _logger.info("CMD LINE INPUT: %s" % str_f)
335
336
            parse_args(str_f.split())
337
        except Exception as e:
338
            tk.messagebox.showerror(str(type(e)), e.__str__())
339
340
    def __init_manager(self):
341
        self.manager[self.__SCAN_KEY] = tk.StringVar()
342
        self.manager[self.__TISSUES_KEY] = [
343
            tk.BooleanVar() for i in range(len(knee.SUPPORTED_TISSUES))
344
        ]
345
        self.manager[self.__DATA_KEY] = tk.StringVar()
346
        self.manager[self.__DATA_PATH_KEY] = tk.StringVar()
347
348
        self.manager[self.__SCAN_KEY].trace_add("write", self.__on_scan_change)
349
        self.manager[self.__SAVE_PATH_KEY] = tk.StringVar()
350
        self.manager[self.__IGNORE_EXTENSION_KEY] = tk.BooleanVar()
351
352
    def __on_scan_change(self, *args):
353
        scan_id = self.manager[self.__SCAN_KEY].get()
354
        scan = None
355
        for x in SUPPORTED_SCAN_TYPES:
356
            if x.NAME == scan_id:
357
                scan = x
358
359
        self.scan_reader.load_scan(scan)
360
361
        assert scan is not None, "No scan selected"
362
363
    def __update_svar(self, *args):
364
        svar = self.manager[self.__DATA_PATH_KEY]
365
        selected_option = self.manager[self.__DATA_KEY].get()
366
        if selected_option == self.__DICOM_PATH_KEY:
367
            fp = self.file_dialog_reader.get_volume_filepath(
368
                selected_option, im_type=fio_utils.ImageDataFormat.dicom
369
            )
370
        elif selected_option == self.__LOAD_PATH_KEY:
371
            fp = self.file_dialog_reader.get_dirpath(selected_option)
372
        else:
373
            raise ValueError("%s key not found" % self.__DATA_KEY)
374
375
        if not fp:
376
            svar.set("")
377
            return
378
379
        svar.set(fp)
380
381
        if selected_option == self.__LOAD_PATH_KEY:
382
            self.manager[self.__SAVE_PATH_KEY].set(fp)
383
384
    def __display_data_loader(self):
385
        s_var = self.manager[self.__DATA_PATH_KEY]
386
387
        hb = tk.Frame(self)
388
389
        label = tk.Label(hb, text="Data source: ")
390
        label.pack(side="left", anchor="nw", pady=10)
391
392
        options = [self.__DICOM_PATH_KEY, self.__LOAD_PATH_KEY]
393
        menu = tk.OptionMenu(
394
            hb, self.manager[self.__DATA_KEY], *options, command=self.__update_svar
395
        )
396
        menu.pack(side="left", anchor="nw", pady=10)
397
398
        label = tk.Label(hb, textvariable=s_var)
399
        label.pack(side="left", anchor="nw", pady=10)
400
401
        hb.pack(side="top", anchor="nw")
402
        self.balloon.bind(hb, "Read dicoms or load data")
403
404
        hb = tk.Frame(self)
405
406
        # filedialog = FileDialogReader(self.manager[self.__SAVE_PATH_KEY])
407
        b = tk.Button(
408
            hb,
409
            text=self.__SAVE_PATH_KEY,
410
            command=lambda fd=self.file_dialog_reader: self.manager[self.__SAVE_PATH_KEY].set(
411
                fd.get_save_dirpath()
412
            ),
413
        )
414
        b.pack(side="left", anchor="nw", pady=10)
415
416
        label = tk.Label(hb, textvariable=self.manager[self.__SAVE_PATH_KEY])
417
        label.pack(side="left", anchor="nw", pady=10)
418
419
        hb.pack(side="top", anchor="nw")
420
421
        hb = tk.Frame(self)
422
423
        b = tk.Checkbutton(
424
            hb, text=self.__IGNORE_EXTENSION_KEY, variable=self.manager[self.__IGNORE_EXTENSION_KEY]
425
        )
426
        b.pack(side="left", anchor="nw", pady=10)
427
        self.balloon.bind(b, "Ignore '.dcm' extension when loading dicoms")
428
429
        hb.pack(side="top", anchor="nw")
430
431
    def __display_tissues(self):
432
        hb = tk.Frame(self)
433
        _label = tk.Label(hb, text="Tissues:")
434
        _label.pack(side="left", anchor="w")
435
        hb.pack(side="top", anchor="nw")
436
        frames = [tk.Frame(hb)] * (len(knee.SUPPORTED_TISSUES) // 3 + 1)
437
        for ind, tissue in enumerate(knee.SUPPORTED_TISSUES):
438
            f = frames[ind // 3]
439
            b = tk.Checkbutton(
440
                f, text=tissue.FULL_NAME, variable=self.manager[self.__TISSUES_KEY][ind]
441
            )
442
            b.pack(side="top", anchor="nw", pady=5)
443
444
        for f in frames:
445
            f.pack(side="left", anchor="nw")
446
447
        self.balloon.bind(_label, "Tissues to analyze")
448
449
    def __base_gui(self):
450
        self.__display_data_loader()
451
        self.__display_tissues()
452
453
        hb = tk.Frame(self)
454
        scan_label = tk.Label(hb, text="Scan:")
455
        scan_label.pack(side="left", anchor="nw", pady=10)
456
        options = [x.NAME for x in SUPPORTED_SCAN_TYPES]
457
        scan_dropdown = tk.OptionMenu(hb, self.manager[self.__SCAN_KEY], *options)
458
        scan_dropdown.pack(side="left", anchor="nw", pady=10)
459
        hb.pack(side="top", anchor="nw")
460
461
    def InitUI(self):
462
        self.text_box = tk.Text(self, wrap="word", height=11, width=50)
463
        self.text_box.pack(anchor="s", side="bottom")
464
465
466
class PageThree(tk.Frame):
467
    SUPPORTED_FORMATS = (("nifti files", "*.nii\.gz"), ("dicom files", "*.dcm"))  # noqa: W605
468
    __base_filepath = "../"
469
470
    _ORIENTATIONS = [("sagittal", SAGITTAL), ("coronal", CORONAL), ("axial", AXIAL)]
471
472
    def __init__(self, parent, controller):
473
        tk.Frame.__init__(self, parent)
474
        self._im_display = None
475
        self.binding_vars: Dict = {}
476
        fig, ax = plt.subplots(1, 1)
477
        X = np.random.rand(20, 20, 40)
478
479
        self.tracker = IndexTracker(ax, X)
480
481
        canvas = FigureCanvasTkAgg(fig, self)
482
        canvas.draw()
483
        canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
484
        canvas.mpl_connect("scroll_event", self.tracker.onscroll)
485
486
        toolbar = NavigationToolbar2Tk(canvas, self)
487
        toolbar.update()
488
        canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
489
490
        self.im = None
491
        self.mask = None
492
        self._im_display = None
493
494
        button1 = ttk.Button(
495
            self, text="Back to Home", command=lambda: controller.show_frame(StartPage)
496
        )
497
        button1.pack(side=tk.BOTTOM, anchor="sw")
498
499
        button2 = ttk.Button(self, text="Load main image", command=self.load_volume_callback)
500
        button2.pack()
501
502
        button3 = ttk.Button(self, text="Load mask", command=self.load_mask_callback)
503
        button3.pack()
504
505
        self.init_reformat_display()
506
507
    def __reformat_callback(self, *args):
508
        self.im_update()
509
510
    def init_reformat_display(self):
511
        orientation_var = IntVar(0)
512
        orientation_var.trace_add("write", self.__reformat_callback)
513
        count = 0
514
        for text, _value in self._ORIENTATIONS:
515
            b = Radiobutton(self, text=text, variable=orientation_var, value=count)
516
            b.pack(side=tk.TOP, anchor="w")
517
            count += 1
518
        self._orientation = orientation_var
519
520
    def load_volume_callback(self):
521
        im = self.load_volume()
522
        if not im:
523
            return
524
        self.im = im
525
        self.mask = None
526
527
        self.im_update()
528
529
    def load_mask_callback(self):
530
        if not self.im:
531
            messagebox.showerror("Loading mask failed", "Main image must be loaded prior to mask")
532
            return
533
534
        mask = self.load_volume("Load mask")
535
        mask.reformat(self.im.orientation, inplace=True)
536
        try:
537
            self.__verify_mask_size(self.im.volume, mask.volume)
538
        except Exception as e:
539
            messagebox.showerror("Loading mask failed", str(e))
540
            return
541
542
        self.mask = mask
543
        self.im_update()
544
545
    def __verify_mask_size(self, im: np.ndarray, mask: np.ndarray):
546
        if mask.ndim != 3:
547
            raise ValueError("Dimension mismatch. Mask must be 3D")
548
        if im.shape != mask.shape:
549
            raise ValueError(
550
                "Dimension mismatch. Image of shape %s, but mask of shape %s"
551
                % (str(im.shape), str(mask.shape))
552
            )
553
554
    def im_update(self):
555
        orientation = self.orientation
556
        self.im.reformat(orientation, inplace=True)
557
        im = self.im.volume
558
        im = im / np.max(im)
559
        if self.mask:
560
            self.mask.reformat(orientation, inplace=True)
561
            label_image = label(self.mask.volume)
562
            im = self.__labeltorgb_3d__(im, label_image, 0.3)
563
564
        self.im_display = im
565
566
    def __labeltorgb_3d__(self, im: np.ndarray, labels: np.ndarray, alpha: float = 0.3):
567
        im_rgb = np.zeros(im.shape + (3,))  # rgb channel
568
        for s in range(im.shape[2]):
569
            im_slice = im[..., s]
570
            labels_slice = labels[..., s]
571
            im_rgb[..., s, :] = label2rgb(labels_slice, image=im_slice, bg_label=0, alpha=alpha)
572
        return im_rgb
573
574
    def load_volume(self, title="Select volume file(s)"):
575
        files = filedialog.askopenfilenames(initialdir=self.__base_filepath, title=title)
576
        if len(files) == 0:
577
            return
578
579
        filepath = files[0]
580
        self.__base_filepath = os.path.dirname(filepath)
581
582
        if filepath.endswith(".dcm"):
583
            filepath = os.path.dirname(filepath)
584
585
        im = fio_utils.generic_load(filepath, 1)
586
587
        return im
588
589
    @property
590
    def orientation(self):
591
        ind = self._orientation.get()
592
        return self._ORIENTATIONS[ind][1]
593
594
    @property
595
    def im_display(self):
596
        return self._im_display
597
598
    @im_display.setter
599
    def im_display(self, value):
600
        self._im_display = value
601
        self.tracker.x = self._im_display