The Annotation class

A pylidc.Annotation object belongs to a pylidc.Scan object:

import pylidc as pl

ann = pl.query(pl.Annotation).first()
print(ann.scan.patient_id)
# => LIDC-IDRI-0078

Jump to:

Querying annotation feature values

The queryable attributes for Annotation objects are the characteristic values assigned by the particular annotating radiologist:

anns = pl.query(pl.Annotation).filter(pl.Annotation.spiculation == 5,
                                      pl.Annotation.malignancy == 5)
print(anns.count())
# => 91

Note

Not all of the class attributes are queryable! Some attributes are actually computed properties. You can determine this by, for example, examining the object type for the class:

print(type(pl.Annotation.lobulation))
# => <class 'sqlalchemy.orm.attributes.InstrumentedAttribute'>
# => ^^^ queryable because it's an sqlalchemy attribute.

# Whereas ...
print(type(pl.Annotation.Lobulation))
# => <type 'property'>
# ^^^ not queryable because it's a computed property

The lobulation attribute is the numerical value assigned by the annotating radiologist, whereas the Lobulation attribute is a property that encodes the numerical value to the semantic string representation.

Each of the characteristic values are the numerical values assigned by the annotating radiologist. Alongside each of these attributes is a corresponding computed property that gives the semantic interpretation of the numerical value for a given characteristic:

ann = pl.query(pl.Annotation)\
        .filter(pl.Annotation.malignancy == 5).first()

print(ann.malignancy, ann.Malignancy)
# => 5, 'Highly Suspicious'

print(ann.margin, ann.Margin)
# => 2, 'Near Poorly Defined'

The names of these characteristics are found in pl.annotation_feature_names:

('subtlety',
 'internalStructure',
 'calcification',
 'sphericity',
 'margin',
 'lobulation',
 'spiculation',
 'texture',
 'malignancy')

All of these characteristics values and strings can be quickly viewed by:

ann.print_formatted_feature_table()
# => Feature              Meaning                    #
# => -                    -                          -
# => Subtlety           | Obvious                  | 5
# => Internalstructure  | Soft Tissue              | 1
# => Calcification      | Absent                   | 6
# => Sphericity         | Ovoid/Round              | 4
# => Margin             | Near Poorly Defined      | 2
# => Lobulation         | Near Marked Lobulation   | 4
# => Spiculation        | No Spiculation           | 1
# => Texture            | Solid                    | 5
# => Malignancy         | Highly Suspicious        | 5

We can also query directly for the attributes themselves, rather than the entire Annotation object:

svals = pl.query(pl.Annotation.spiculation)\
          .filter(pl.Annotation.spiculation > 3)

print(svals[0])
# => (4,)

print(all([s[0] > 3 for s in svals]))
# => True

Contour-derived data

The pylidc.Contour class is almost never used directly, but only through the pylidc.Annotation object to which the contours belong. They can be accessed directly, however, via:

ann = pl.query(pl.Annotation).first()
contours = ann.contours

print(contours[0])
# => Contour(id=21,annotation_id=1)

The diameter, surface_area, and volume attributes are all, for example, computed properties that use the contour data for a particular Annotation:

print("%.2f mm, %.2f mm^2, %.2f mm^3" % (ann.diameter,
                                         ann.surface_area,
                                         ann.volume))
# => 20.84 mm, 1242.74 mm^2, 2439.30 mm^3

A boolean-valued volume can be obtained that is 1 to indicate nodule and 0 to indicate non-nodule:

mask = ann.boolean_mask()
print(mask.shape, mask.dtype)
# => (34, 27, 6), dtype('bool')

Note by the shape of the volume that the boolean-valued mask does not occupy the entire extent of the CT image volume (which is 512 x 512 x num slices). Rather, the boolean mask sits within the computed “bounding box” of the nodule, which is the computed extent of the contour indices of the annotation.

The pylidc.Annotation.bbox() method returns a tuple of slices corresponding to the nodule bounding box indices. This can be used to easily index into the NumPy CT image volume:

bbox = ann.bbox()
print(bbox)
# => (slice(151, 185, None), slice(349, 376, None), slice(44, 50, None))

vol = ann.scan.to_volume()
print(vol[bbox].shape)
# => (34, 27, 6)

Note that both the pylidc.Annotation.boolean_mask() and the pylidc.Annotation.bbox() methods accept a pad argument which can be used for adding context about the nodule.

We can also get the physical dimensions of the bounding box:

print(ann.bbox_dims())
# => [21.45, 16.90, 15.0]

Annotation visualization

The pylidc.Annotation class provides two direct convenience functions for visualization annotation data which we will show below; however, let’s first continue the pylidc.Annotation.boolean_mask() and pylidc.Annotation.bbox() examples from above to provide a visualization of the annotated nodule:

import pylidc as pl
import matplotlib.pyplot as plt

ann = pl.query(pl.Annotation).first()
vol = ann.scan.to_volume()

padding = [(30,10), (10,25), (0,0)]

mask = ann.boolean_mask(pad=padding)
bbox = ann.bbox(pad=padding)

fig,ax = plt.subplots(1,2,figsize=(5,3))

ax[0].imshow(vol[bbox][:,:,2], cmap=plt.cm.gray)
ax[0].axis('off')

ax[1].imshow(mask[:,:,2], cmap=plt.cm.gray)
ax[1].axis('off')

plt.tight_layout()
#plt.savefig("../images/mask_bbox.png", bbox_inches="tight")
plt.show()
../_images/mask_bbox.png

Surface visualization

The nodule surface (as obtained by a single annotator) can be visualized in three dimensions by invoking the following:

ann = pl.query(pl.Annotation)\
        .filter(pl.Annotation.lobulation == 5).first()
ann.visualize_in_3d()

which appears as:

../_images/annotation3d.png

CT visualization

The contours can be visualized interactively on top of the CT image data:

ann = pl.query(pl.Annotation).first()
ann.visualize_in_scan()

which appears as:

../_images/annotation_viz_in_scan.png