HOME/Articles/

pil example d3 (snippet)

Article Outline

Python pil example 'd3'

Functions in program:

  • def dicom_ask_for_dir():

Modules used in program:

  • import pyqtgraph.console as pgcon
  • import pyqtgraph.opengl as gl
  • import pyqtgraph as pg
  • import numpy as np
  • import dicom
  • import glob
  • import os

python d3

Python pil example: d3


"3-d visualization tools"

import os
import glob
import dicom 
import numpy as np
import pyqtgraph as pg
import pyqtgraph.opengl as gl
import pyqtgraph.console as pgcon

from pyqtgraph import QtCore, QtGui


def dicom_ask_for_dir():
    cfg = pg.QtCore.QSettings('INS', 'IC/DC')
    path = cfg.value('dicomdir')
    if type(path) not in (str, unicode):
        path = unicode(path.toString())
    print(path, type(path), len(path)==0)
    """
    if len(path) == 0:
        path = os.path.expanduser('~')
    """
    newpath = unicode(pg.QtGui.QFileDialog.getExistingDirectory(
        directory=path,
        caption='Select folder containing DICOM files'
    ))
    if len(newpath)>0:
        cfg.setValue('dicomdir', newpath)
        return newpath
    return ''

# refactor to separate module
class XImageViewBox(pg.ViewBox):

    sigPosChanged = pg.QtCore.Signal(object)

    def __init__(self, image=None):
        pg.ViewBox.__init__(self)
        self.image = image or np.random.randn(255, 255)
        self.item = pg.ImageItem()
        self.item.setImage(self.image)
        self.addItem(self.item)
        self.il_x = pg.InfiniteLine(pos=self.image.shape[0]/2, angle=90)
        self.il_y = pg.InfiniteLine(pos=self.image.shape[1]/2, angle=0)
        self.addItem(self.il_x)
        self.addItem(self.il_y)

    def tr_pixels_data(self, px, normed=False):
        x, y = px
        ((vxlo, vxhi), (vylo, vyhi)) = self.viewRange()
        r = self.rect()
        rxlo, rxhi, rylo, ryhi = r.left(), r.right(), r.bottom(), r.top()
        nx = (x - rxlo)/(rxhi - rxlo)
        ny = (y - rylo)/(ryhi - rylo)
        if normed:
            return nx, ny
        return nx*(vxhi - vxlo) + vxlo, ny*(vyhi - vylo) + vylo

    def mouseClickEvent(self, ev):
        pos = ev.x(), ev.y()
        dx, dy = self.tr_pixels_data(pos)
        self.il_x.setValue(dx)
        self.il_y.setValue(dy)
        self.sigPosChanged.emit(self.tr_pixels_data(pos, normed=True))

    # disable other interactions
    def mouseDragEvent(self, ev):
        pass

    def wheelEvent(self, ev):
        pass

class XImageWidget(pg.GraphicsView):

    def __init__(self, image=None):
        pg.GraphicsView.__init__(self)
        self.xi = XImageViewBox(image=image)
        self.setCentralItem(self.xi)

    # mouse dragging continously sets position

    _pressed = False
    def mousePressEvent(self, ev):
        self._pressed = True
        self.xi.mouseClickEvent(ev)

    def mouseReleaseEvent(self, ev):
        self._pressed = False

    def mouseMoveEvent(self, ev):
        if self._pressed:
            self.xi.mouseClickEvent(ev)

class Slice(XImageWidget):

    sigXChanged = pg.QtCore.Signal(object)
    sigYChanged = pg.QtCore.Signal(object)

    def __init__(self, axis, data, T=False, fx=False, fy=False):
        XImageWidget.__init__(self)
        self.axis = axis
        self.data = data
        self.T = T
        self.fx = fx
        self.fy = fy
        self.shape = list(self.data.shape)
        del self.shape[axis]
        self.xi.sigPosChanged.connect(self.pos_changed)
        self.ignore_pos_changes = False
        self.idx = data.shape[axis]/2

    def pos_changed(self, pos):
        x, y = pos
        # ignore changes when calling set_pos
        if not self.ignore_pos_changes:
            self.sigXChanged.emit(x)
            self.sigYChanged.emit(y)

    def set_pos(self, x=None, y=None):
        self.ignore_pos_changes = True
        xa, ya = (1, 0) if self.T else (0, 1)
        if x:
            self.xi.il_x.setValue(x * self.shape[xa])
        if y:
            self.xi.il_y.setValue(y * self.shape[ya])
        self.ignore_pos_changes = False

    _idx = 0

    def _get_idx(self):
        return self._idx

    def _set_idx(self, idx):
        self._idx = idx = int(idx)
        self.update_image()

    def update_image(self):
        sl = [slice(None) for i in range(self.axis)] + [self.idx]
        try:
            ary = self.data[tuple(sl)]
        except Exception as exc:
            print('no slice', exc, sl)
            return
        jx = -1 if self.fx else 1
        jy = -1 if self.fy else 1
        self.xi.item.setImage((ary.T if self.T else ary)[::jx, ::jy])

    idx = property(_get_idx, _set_idx)

    _slice = 0.0

    def _get_slice(self):
        return self._slice

    def _set_slice(self, val):
        self.idx = val * self.data.shape[self.axis]

    slice = property(_get_slice, _set_slice)


class ImageContrastControl():
    pass


class SlicesVolume(gl.GLViewWidget):

    def __init__(self, images, datasets=None, parent=None):
        gl.GLViewWidget.__init__(self, parent=parent)
        self.images = np.transpose(images, (2, 1, 0)).copy()
        self.datasets = datasets
        self.volume = np.zeros(self.images.shape + (4,), np.ubyte)

        self.images  = (self.images - self.images.min())*255.0/self.images.ptp()

        print(self.images.min(), self.images.max())

        p10, p90 = np.percentile(self.images.flat[:], [1, 99])
        self.images = np.clip((self.images - p10)/(p90-p10)*256, 0, 255).astype(np.ubyte)
        self.volume[..., 0] = self.images
        self.volume[..., 1] = self.images
        self.volume[..., 2] = self.images
        #self.volume[..., 1] = 255 - self.images
        #self.volume[..., 2] = 255 - 2*np.abs(self.images - 128)
        self.volume[..., 3] = self.images/10

        self.item = gl.GLVolumeItem(self.volume)
        self.item.scale(*[1.0/s for s in self.images.shape])
        self.item.translate(-0.5, -0.5, -0.5)
        self.addItem(self.item)

    def keyPressEvent(self, ev):
        k = str(ev.text())
        print(k, k.lower(), k.lower() in 's')
        if k and k.lower() in 'xcv':
            tr = [0, 0, 0]
            tr['xcv'.index(k.lower())] = 10*(-1 if k==k.lower() else 1)
            self.item.translate(*tr)
        if k and k.lower() in 'sdf':
            sc = [1, 1, 1]
            sc['sdf'.index(k.lower())] = 1 + (0.2 if k==k.lower() else -0.2)
            self.item.scale(*sc)
        if k and k.lower() in 'Aa':
            self.item.data[..., -1] *= 0.5 if k=='a' else 2
            self.item.initializeGL()
            self.item.scale(1,1,1)


class FlipBoard(pg.QtGui.QWidget):
    def __init__(self, slices):
        pg.QtGui.QWidget.__init__(self)
        self.lay = pg.QtGui.QGridLayout()
        self.setLayout(self.lay)
        self.refs = []
        for i, ax in enumerate('xyz'):
            for j, attr in enumerate('T fx fy'.split()):
                cb = pg.QtGui.QCheckBox(ax + ' ' + attr)
                def _(obj, attr):
                    def __(state):
                        setattr(obj, attr, state>0)
                        obj.update_image()
                    return __
                ij = _(slices[i], attr)
                cb.stateChanged.connect(ij)
                self.lay.addWidget(cb, j, i)
                self.refs.append((cb, ij))


class Controls(pg.QtGui.QWidget):
    def __init__(self, datasets):
        pg.QtGui.QWidget.__init__(self)
        self.datasets = datasets
        self.lay = pg.QtGui.QGridLayout()
        self.setLayout(self.lay)
        self.pbs = []
        for k in dir(self):
            if k.startswith('pb_'):
                f = getattr(self, k)
                k = k.split('_')
                pb = pg.QtGui.QPushButton(' '.join(k[3:]).title())
                pb.clicked.connect(f)
                self.lay.addWidget(pb, int(k[1]), int(k[2]))
                self.pbs.append(pb)

    dups = []
    def pb_0_0_duplicate(self):
        self.dups.append(MultiXImage.from_datasets(self.datasets))
        self.dups[-1].show()

    lastpath = [os.path.expanduser('~')]
    def pb_1_0_open(self):
        path = dicom_ask_for_dir()
        if path:
            self.newmxi = MultiXImage.from_glob(os.path.join(path, '*'))
            self.newmxi.show()

    def pb_0_1_show_volume(self):
        image = np.array([d.pixel_array for d in self.datasets])
        self.volwin = SlicesVolume(image, self.datasets)
        self.volwin.show()


    def pb_0_2_metadata(self):
        self.metadatatw = pg.TableWidget()
        ds = self.datasets[0]
        self.metadata = []
        for k in ds.dir():
            v = ds.data_element(k)
            if v=='PixelData' or v is None:
                continue
            self.metadata.append((k, repr(v.value).encode('ascii', 'ignore')[:20]))
        self.metadatatw.setData(np.array(self.metadata, dtype=object))
        self.metadatatw.show()


class MultiXImage(pg.QtGui.QWidget):

    sigXChanged, sigYChanged, sigZChanged = [pg.QtCore.Signal(object) for _ in range(3)]

    def __init__(self, image, datasets=None, parent=None):
        pg.QtGui.QWidget.__init__(self, parent=parent)
        self.lay = pg.QtGui.QGridLayout()
        self.setLayout(self.lay)
        self.image = image
        self.setup_image()
        self.setup_signals()
        self.datasets = datasets
        if datasets is not None:
            self.ctls = Controls(datasets)
            self.lay.addWidget(self.ctls)

    def setup_image(self):

        im = self.image

        self.sl_x = Slice(0, im, fy=False)
        self.sl_y = Slice(1, im)
        self.sl_z = Slice(2, im, T=True)

        self.lay.addWidget(self.sl_x, 0, 0)
        self.lay.addWidget(self.sl_y, 0, 1)
        self.lay.addWidget(self.sl_z, 1, 0)

    def change_x(self, x):
        self.sl_y.slice = x
        self.sl_z.set_pos(x=x)
        self.sl_x.set_pos(x=x)
        self.sigXChanged.emit(x)

    def change_y(self, y):
        self.sl_x.slice = y
        self.sl_z.set_pos(y=y)
        self.sl_y.set_pos(x=y)
        self.sigYChanged.emit(y)

    def change_z(self, z):
        self.sl_z.slice = z
        self.sl_x.set_pos(y=z)
        self.sl_y.set_pos(y=z)
        self.sigZChanged.emit(z)

    def setup_signals(self):

        # connect x-y changes of slices to x-y-z slots
        self.sl_x.sigXChanged.connect(self.change_x)
        self.sl_x.sigYChanged.connect(self.change_z)

        self.sl_y.sigXChanged.connect(self.change_y)
        self.sl_y.sigYChanged.connect(self.change_z)

        self.sl_z.sigXChanged.connect(self.change_x)
        self.sl_z.sigYChanged.connect(self.change_y)


    @classmethod
    def from_glob(cls, pathglob):
        return cls.from_files(sorted(glob.glob(pathglob)))

    @classmethod
    def from_files(cls, files):
        return cls.from_datasets(map(dicom.read_file, files))

    @classmethod
    def from_datasets(cls, datasets):
        return cls(np.array([d.pixel_array for d in datasets]), datasets)

    @classmethod
    def from_file_dialog(cls):
        path = dicom_ask_for_dir()
        if path:
            return cls.from_glob(os.path.join(path, '*'))

if __name__ == '__main__':
    app = QtGui.QApplication([])
    mxi = MultiXImage.from_file_dialog()
    mxi.show()
    app.exec_()