def test_cylinder_xaxis(): # test single axis cylinder, parallel to the x axis shape = (64, 64, 64) halfshape = tuple(s // 2 for s in shape) p1 = (1, 0, 0) p2 = (-1, 0, 0) r = 0.5 # in base coordinates; radius will be 1/4 the shape volume = phantom_base.cylinder(shape, p1, p2, r) # check the centroid and radius xslice = volume[halfshape[0], :, :] row = xslice[halfshape[1], :] col = xslice[:, halfshape[2]] # given a radius in real numbers, calculate that expected radius in pixels expected_diameters = tuple(int(s * r) for s in shape) # assert that a row slice through the object centroid has the correct radius nose.tools.assert_equals(_find_diameter(row), expected_diameters[0]) # assert that a column slice through the object centroid has the correct radius nose.tools.assert_equals(_find_diameter(col), expected_diameters[1]) # make sure that the center line of the cylinder exists across the volume expected_line = np.ones(shape[0], dtype=bool) np.testing.assert_array_equal(volume[:, halfshape[1], halfshape[2]], expected_line) # check that cap == "none" is identical with full axis p1 = (-0.5, 0, 0) p2 = (0.5, 0, 0) r = 0.5 # in base coordinates; radius will be 1/4 the shape vol_nocap = phantom_base.cylinder(shape, p1, p2, r, cap="none") np.testing.assert_array_equal(vol_nocap, volume)
def test_cylinder_tiny_radius(): # check that we are logging a warning for radii smaller than grid resolution shape = (10, 10, 10) p1 = (-0.5, 0, 0) p2 = (0.5, 0, 0) r = 0.05 # use an epsilon-sized radius, should be smaller than the grid size volume = phantom_base.cylinder(shape, p1, p2, r) # volume should be all zeros np.testing.assert_array_equal(np.unique(volume), np.zeros(1)) r = 1 / min(shape) # use the grid size, see what happens volume = phantom_base.cylinder(shape, p1, p2, r) # volume should be all zeros np.testing.assert_array_equal(np.unique(volume), np.zeros(1))
def test_cylinder_big_radius(): shape = (10, 10, 10) p1 = (0, 0, -1) p2 = (0, 0, 1) r = 2 volume = phantom_base.cylinder(shape, p1, p2, r) np.testing.assert_array_equal(np.unique(volume), np.ones(1))
def test_cylinder_xyz_inf(): # make sure that we can generate an arbitrary cylinder across 3 dimensions shape = (64, 64, 64) p1 = (-1, -1, -1) p2 = (1, 1, 1) r = 0.5 # in base coordinates; radius will be 1/4 the shape volume = phantom_base.cylinder(shape, p1, p2, r, cap="none") np.testing.assert_array_equal(volume, volume[::-1, ::-1, ::-1])
def test_cylinder_xaxis_flatcap(): # cylinder along the xaxis, but with flat caps shape = (64, 64, 64) halfshape = tuple(s // 2 for s in shape) p1 = (-0.5, 0, 0) p2 = (0.5, 0, 0) r = 0.5 # in base coordinates; radius will be 1/4 the shape volume = phantom_base.cylinder(shape, p1, p2, r, cap="flat") # get the expected centerline of the cylinder, with a planar cap # Parts of the line outside of the planes defined by the two points # through the center line should not be turned on. expected_line = np.zeros(shape[0], dtype=bool) expected_line[(halfshape[0] // 2):(halfshape[0] + halfshape[0] // 2)] = 1 np.testing.assert_array_equal(volume[:, 31, 31], expected_line)
def test_cylinder_xyz_flatcap(): # test the planar cut on generating a cylinder across 3 dimensions shape = (64, 64, 64) quarter = np.array([s // 4 for s in shape]) p1 = (-0.5, -0.5, -0.5) p2 = (0.5, 0.5, 0.5) r = 0.25 # in base coordinates; radius will be 1/4 the shape volume = phantom_base.cylinder(shape, p1, p2, r) np.testing.assert_array_equal(volume, volume[::-1, ::-1, ::-1]) # make sure we have flattened at the edge of the cylinder # get the diagonal line through the cylinder, then check bounds line = np.array([volume[x, x, x] for x in range(shape[0])]) nose.tools.assert_equal(phantom_base.nonzero_bounds(line), (quarter[0], shape[0] - quarter[0]))
def test_cylinder_xaxis_spherecap(): # cylinder along the xaxis, but with flat caps shape = (64, 64, 64) halfshape = tuple(s // 2 for s in shape) p1 = (-0.5, 0, 0) p2 = (0.5, 0, 0) r = 0.25 # in base coordinates; radius will be 1/4 the shape # radius in pixels; divide by 2 since space is [-1, 1] rpix = int(shape[0] * r / 2) volume = phantom_base.cylinder(shape, p1, p2, r, cap="sphere") center_line = volume[:, halfshape[1], halfshape[2]] # create the expected centerline between point1 and point2, plus the # spherical radius of each of the endcaps at both endpoints expected_line = np.zeros(shape[0], dtype=bool) expected_line[(halfshape[0] // 2 - rpix):(halfshape[0] + halfshape[0] // 2 + rpix)] = 1 np.testing.assert_array_equal(center_line, expected_line)
def add_cylinder_basecoord(volume, point1: (float, float, float), point2: (float, float, float), radius: float, cap="sphere"): """ Given two points and a radius in pixel space, allocate a volume with the target cylinder Internally, radius is normalized to the minimum dimension of the volume shape Inputs: volume: ndarray, grayscale volume (unboxed) point1: tuple, describes starting point of cylinder in volume, in base volume floats [-1, 1] eg, (-1, 0.56, 0.98) point2: tuple, describes starting point of cylinder in volume, in base volume floats [-1, 1] radius: float, the cylinder radius in base volume coordinates cap: type of cap on the cylinder, eg "flat", "sphere", or None. Types are defined in phantoms.base.CAP_TYPES Return: ndarray, dtype=bool, same shape as input `volume` TODO: Make this smarter: 1) only allocate volume for minimum bounding box for the cylinder we are adding 2) update original input `volume` slice with allocated cylinder volume Even better, mkae a cylinder as a primitive, instead of making cylinder out of spheres """ # TODO add isotropy back to base cylinder call # # scale the isotropy # shape_min = min(volume.shape) # used as scaling factor # isotropy = tuple(shape_min / s for s in volume.shape) # A smarter way to do this would be to allocate the minimum bounding box # necessary to enclose the cylinder, and insert that volume into a slice # of the original volume. Hard part is getting the scale units correct. newvolume = phantom_base.cylinder(volume.shape, point1, point2, radius, cap=cap) return np.logical_or(volume, newvolume)
def test_cylinder_offset_from_axis(): # test cylinder line in negative quadrant shape = (64, 64, 64) quartershape = tuple(s // 4 for s in shape) p1 = (-.9, -0.5, -0.5) p2 = (-0.2, -0.5, -0.5) r = 0.2 volume = phantom_base.cylinder(shape, p1, p2, r, cap="flat") expected_line = np.zeros(shape[0], dtype=bool) expected_line[4:26] = 1 np.testing.assert_array_equal(volume[:, quartershape[1], quartershape[2]], expected_line) xindex = quartershape[0] xslice = volume[xindex, :, :] row = xslice[quartershape[1], :] col = xslice[:, quartershape[2]] expected_diameters = tuple(int(s * r) for s in shape) nose.tools.assert_almost_equal(_find_diameter(row), expected_diameters[0], delta=1) nose.tools.assert_almost_equal(_find_diameter(col), expected_diameters[1], delta=1)
def test_cylinder_cap_strmatch_assertion(): # invalid cap types should fail shape = (4, 4, 4) cyl = ((0, 0, 0), (1, 1, 1), .5) with nose.tools.assert_raises(AssertionError): phantom_base.cylinder(shape, *cyl, cap="foo")