Source code for pylidc.Contour

import sqlalchemy as sq
from sqlalchemy.orm import relationship
import numpy as np
from ._Base import Base
from .Scan import Scan
from .Annotation import Annotation

_off_limits = ['id','annotation_id','annotation',
               'inclusion','image_z_position','dicom_file_name','coords']

[docs]class Contour(Base): """ The Contour class holds the nodule boundary coordinate data of a :class:`pylidc.Annotation` object for a single slice in the CT volume. Attributes ---------- inclusion: bool If True, the area inside the contour is included as part of the nodule. If False, the area inside the contour is excluded from the nodule. image_z_position: float This is the `imageZposition` defined via the xml annnotations for this contour. It is the z-coordinate of DICOM attribute (0020,0032). dicom_file_name: string This is the name of the corresponding DICOM file for the scan to which this contour belongs, having the same `image_z_position`. coords: string These are the sequential (x,y) coordinates of the curve, stored as a string. It is better to access these coordinates using the `to_matrix` method, which returns a numpy array rather than a string. Example ------- Plotting a contour on top of the image volume:: import pylidc as pl import matplotlib.pyplot as plt ann = pl.query(pl.Annotation).first() vol = ann.scan.to_volume() con = ann.contours[3] k = con.image_k_position ii,jj = ann.contours[3].to_matrix(include_k=False).T plt.imshow(vol[:,:,46], cmap=plt.cm.gray) plt.plot(jj, ii, '-r', lw=1, label="Nodule Boundary") plt.legend() plt.show() """ __tablename__ = 'contours' id = sq.Column('id', sq.Integer, primary_key=True) annotation_id = sq.Column(sq.Integer, sq.ForeignKey('annotations.id')) annotation = relationship('Annotation', back_populates='contours') inclusion = sq.Column('inclusion', sq.Boolean) image_z_position = sq.Column('image_z_position', sq.Float) dicom_file_name = sq.Column('dicom_file_name', sq.String) coords = sq.Column('coords', sq.String) def __repr__(self): return "Contour(id=%d,annotation_id=%d)"%(self.id, self.annotation_id) def __setattr__(self, name, value): if name in _off_limits: msg = "Trying to assign read-only Contour object attribute \ `%s` a value of `%s`." % (name,value) raise ValueError(msg) else: super(Contour,self).__setattr__(name,value) @property def image_k_position(self): """ Similar to `Contour.image_z_position`, but returns the index instead of the z coordinate value. Note ---- This index may not be unique if the `slice_zvals` of the respective scan are not unique. """ zs = self.annotation.scan.slice_zvals k = np.abs(zs-self.image_z_position).argmin() return k
[docs] def to_matrix(self, include_k=True): """ Return the contour-annotation coordinates as a matrix where each row contains an (i,j,k) *index* coordinate into the image volume. Parameters ---------- include_k: bool, default=True Set `include_k=False` to omit the `k` axis coordinate. """ # The reversal [::-1] is because the coordinates from the LIDC XML # are stored as (x,y), not (i,j). ij = np.array([[int(cc) for cc in c.split(',')][::-1] for c in self.coords.split('\n')]) if not include_k: return ij else: k = np.ones(ij.shape[0])*self.image_k_position zs = self.annotation.contour_slice_zvals return np.c_[ij, k].astype(np.int)
Annotation.contours = relationship('Contour', order_by=Contour.id, back_populates='annotation')