def get_heartbeat_movie(frames, fps=60., bpm_limits=(40, 200),
                        pyramid_type='laplacian', pyramid_nlevels=4):

    nt, nr, nc = frames.shape
    frames = np.array(frames, dtype=np.float, copy=True)
    frames -= frames.min()
    frames /= frames.max()
    if pyramid_type == 'laplacian':
        downsamp = laplacian_downsample(frames, pyramid_nlevels)
    elif pyramid_type == 'gaussian':
        downsamp = gaussian_downsample(frames, pyramid_nlevels)

    lowcut, highcut = (ll / 60. for ll in bpm_limits)
    bandpassed = butter_bandpass(downsamp, lowcut, highcut, fps)

    # for ii, us in enumerate(bandpassed):
    #     for jj in xrange(pyramid_nlevels):
    #         us = cv2.pyrUp(us)
    #     frames[ii] = us[:nr, :nc]
    for ii, us in enumerate(bandpassed):
        for jj in xrange(pyramid_nlevels):
            us = transform.pyramid_expand(us)
        frames[ii] = us[:nr, :nc]

    return frames
Example #2
0
def main():
	args = vars(parser.parse_args())
	filename = os.path.join(os.getcwd(), args["image"][0])

	image = skimage.img_as_uint(color.rgb2gray(io.imread(filename)))

	subsample = 1

	if (not args["subsample"] == 1):
		subsample = args["subsample"][0]

		image = transform.downscale_local_mean(image, (subsample, subsample))
		image = transform.pyramid_expand(image, subsample, 0, 0)

	image = exposure.rescale_intensity(image, out_range=(0,args["depth"][0]))

	if (args["visualize"]):
		io.imshow(image)
		io.show()

	source = generate_face(image, subsample, args["depth"][0], FLICKER_SPEED)

	if source:
		with open(args["output"][0], 'w') as file_:
			file_.write(source)
	else:
		print "Attempted to generate source code, failed."
def scale_images(input_folder = '../train-256', output_folder = '../train-128', pixel_size = 128.):
    input_list = set([file for file in os.listdir(input_folder) if file.endswith('.jpeg')])
    output_list = set([file for file in os.listdir(output_folder) if file.endswith('.jpeg')])
    files_to_process = list(input_list - output_list)
    
    for fn in files_to_process:        
        original_image = io.imread(input_folder + '/' + fn)    
        scale = original_image.shape[0] / pixel_size
        if scale > 1:
            output_image = transform.pyramid_reduce(original_image, downscale=scale)
        elif scale < 1:
            output_image = transform.pyramid_expand(original_image, upscale=1/scale)
        else:
            output_image = original_image

        if output_image.shape != (pixel_size, pixel_size):
            x = pixel_size - output_image.shape[0]
            y = pixel_size - output_image.shape[1]
            if x < 0:
                image = output_image[:x, :]
            if y < 0:
                image = output_image[:, :y]
    
        output_image = (output_image - output_image.min()) / output_image.max()
    
        try:
            io.imsave(output_folder + '/' + fn, output_image)
        except ValueError:
            print fn + ' not saved!'
Example #4
0
def _process_img_morph(img, threshold=.5, scale=1):
    if scale > 1:
        up_img = transform.pyramid_expand(img, upscale=scale, order=3)  # type: np.ndarray
        img = (255. * up_img).astype(img.dtype)
    img_min, img_max = img.min(), img.max()
    bin_img = (img >= img_min + (img_max - img_min) * threshold)
    skel, dist_map = morphology.medial_axis(bin_img, return_distance=True)
    return img, bin_img, skel, dist_map
def _properties2d(image, dim):
    """
    Compute shape property of the input 2D image. Accounts for partial volume information.
    :param image: 2D input image in uint8 or float (weighted for partial volume) that has a single object.
    :param dim: [px, py]: Physical dimension of the image (in mm). X,Y respectively correspond to AP,RL.
    :return:
    """
    upscale = 5  # upscale factor for resampling the input image (for better precision)
    pad = 3  # padding used for cropping
    # Check if slice is empty
    if not image.any():
        logging.debug('The slice is empty.')
        return None
    # Normalize between 0 and 1 (also check if slice is empty)
    image_norm = (image - image.min()) / (image.max() - image.min())
    # Convert to float64
    image_norm = image_norm.astype(np.float64)
    # Binarize image using threshold at 0. Necessary input for measure.regionprops
    image_bin = np.array(image_norm > 0.5, dtype='uint8')
    # Get all closed binary regions from the image (normally there is only one)
    regions = measure.regionprops(image_bin, intensity_image=image_norm)
    # Check number of regions
    if len(regions) > 1:
        logging.debug('There is more than one object on this slice.')
        return None
    region = regions[0]
    # Get bounding box of the object
    minx, miny, maxx, maxy = region.bbox
    # Use those bounding box coordinates to crop the image (for faster processing)
    image_crop = image_norm[np.clip(minx-pad, 0, image_bin.shape[0]): np.clip(maxx+pad, 0, image_bin.shape[0]),
                 np.clip(miny-pad, 0, image_bin.shape[1]): np.clip(maxy+pad, 0, image_bin.shape[1])]
    # Oversample image to reach sufficient precision when computing shape metrics on the binary mask
    image_crop_r = transform.pyramid_expand(image_crop, upscale=upscale, sigma=None, order=1)
    # Binarize image using threshold at 0. Necessary input for measure.regionprops
    image_crop_r_bin = np.array(image_crop_r > 0.5, dtype='uint8')
    # Get all closed binary regions from the image (normally there is only one)
    regions = measure.regionprops(image_crop_r_bin, intensity_image=image_crop_r)
    region = regions[0]
    # Compute area with weighted segmentation and adjust area with physical pixel size
    area = np.sum(image_crop_r) * dim[0] * dim[1] / upscale ** 2
    # Compute ellipse orientation, rotated by 90deg because image axis are inverted, modulo pi, in deg, and between [0, 90]
    orientation = _fix_orientation(region.orientation)
    # Find RL and AP diameter based on major/minor axes and cord orientation=
    [diameter_AP, diameter_RL] = \
        _find_AP_and_RL_diameter(region.major_axis_length, region.minor_axis_length, orientation,
                                 [i / upscale for i in dim])
    # TODO: compute major_axis_length/minor_axis_length by summing weighted voxels along axis
    # Fill up dictionary
    properties = {'area': area,
                  'diameter_AP': diameter_AP,
                  'diameter_RL': diameter_RL,
                  'centroid': region.centroid,
                  'eccentricity': region.eccentricity,
                  'orientation': orientation,
                  'solidity': region.solidity  # convexity measure
    }

    return properties
Example #6
0
def upsample(data: np.ndarray) -> np.array:
    """
    :type data: array
    :param data: data to transform

    apply pyramid expand with params upscale 2 and order 1, mode reflect.
    """
    return transform.pyramid_expand(data, upscale=2, sigma=None, order=1, 
                                    mode='reflect', cval=0)
Example #7
0
def weight_pyramid(generator, weights=[1, 1, 1]):
    nb_layers = len(weights) - 1
    for batch in generator:
        batch_merged = []
        for img in batch:
            img = img[0]
            lap_pyr = []
            prev = img
            for i in range(nb_layers):
                gauss = pyramid_reduce(prev)
                lap_pyr.append(prev - pyramid_expand(gauss))
                prev = gauss

            merged = gauss*weights[0]
            for i, lap in enumerate(reversed(lap_pyr)):
                merged = pyramid_expand(merged) + weights[i+1]*lap
            batch_merged.append(merged)
        yield np.stack(batch_merged).reshape(batch.shape)
def preprocess_images(input_folder = '../train', output_folder = '../train-256', pixel_size = 256.):
    input_list = set([file for file in os.listdir(input_folder) if file.endswith('.jpeg')])
    output_list = set([file for file in os.listdir(output_folder) if file.endswith('.jpeg')])
    files_to_process = list(input_list - output_list)
    
    for fn in files_to_process:
        original_image = io.imread(input_folder + '/' + fn)
        shape = original_image.shape[1] - original_image.shape[0]
        if shape < 0:
            shape = -shape
            if shape % 2 == 0:
                shape += 1
            kernel = np.ones((shape, 1))
        else:
            if shape % 2 == 0:
                shape += 1
            kernel = np.ones((1, shape))
        output_image = remove_padding(original_image, kernel)
        grayed_image = color.rgb2gray(padded_image)
        blurred_image = gaussian_filter(grayed_image,sigma=6, multichannel=False)
        difference_image = equalize_hist(grayed_image - blurred_image)

        scale = difference_image.shape[0] / pixel_size
        if scale > 1:
            output_image = transform.pyramid_reduce(difference_image, downscale=scale)
        elif scale < 1:
            output_image = transform.pyramid_expand(difference_image, upscale=1/scale)
        else:
            output_image = difference_image

        if output_image.shape != (pixel_size, pixel_size):
            x = pixel_size - output_image.shape[0]
            y = pixel_size - output_image.shape[1]
            if x < 0:
                image = output_image[:x, :]
            if y < 0:
                image = output_image[:, :y]
    
        output_image = (output_image - output_image.min()) / output_image.max()
        
        try:
            io.imsave(output_folder + '/' + fn, output_image)
        except ValueError:
            print fn + ' not saved!'
comps = detector.mixture.mixture_components()

#theta = kernels.reshape((kernels.shape[0], -1))

llhs = [[] for i in xrange(detector.num_mixtures)] 

print 'Iterating CAD images'

for cad_i, cad_filename in enumerate(cad_files):
    cad = gv.img.load_image(cad_filename)
    f = cad.shape[0]/size[0]
    if f > 1:
        cad = pyramid_reduce(cad, downscale=f)
    elif f < 1:
        cad = pyramid_expand(cad, upscale=1/f)
    mixcomp = comps[cad_i]

    alpha = cad[...,3]
    gray_cad = gv.img.asgray(cad)
    bkg_img = bkg_generator.next()
    
    # Notice, gray_cad is not multiplied by alpha, since most files use premultiplied alpha 
    composite = gray_cad + bkg_img * (1 - alpha)

    # Get features
    X_full = descriptor.extract_features(composite, settings=dict(spread_radii=radii))

    X = gv.sub.subsample(X_full, subsize)

def test_pyramid_expand():
    rows, cols, dim = image.shape
    out = pyramid_expand(image, upscale=2)
    assert_array_equal(out.shape, (rows * 2, cols * 2, dim))
Example #11
0
def to_RLE(mask):
        """Convert mask to run length encoded format"""
        dm = np.diff(mask.T.flatten().astype('int16'))
        start = np.nonzero(dm>0)[0]
        stop = np.nonzero(dm<0)[0]
        RLE = np.vstack((start+2, stop-start))
        return ' '.join(str(n) for n in RLE.flatten(order='F'))


RLE = []


for i, file in enumerate(orderedfnames, 1):
    # Load the prediction file
    raw = np.load(file)[0,:,:]
    upsized = transform.pyramid_expand(raw,2)
    # Apply masking function
    prediction = upsized > DECISION_BOUNDARY
    if i==1:
        print prediction.shape
    area = prediction.sum()
    # Apply size-based cutoff and output RLE
    if area < AREA_CUTOFF:
        RLE.append((i,''))
    else:
        RLE.append((i,to_RLE(prediction)))


out = '\n'.join('{i},{rle}'.format(i=r[0],rle=r[1]) for r in RLE)
with open('output.csv','w') as f:
    f.write('img,pixels\n')
def _process_file(settings, bkg_stack, bkg_stack_num, fn, mixcomp):
    ag.info("Processing file", fn)
    seed = np.abs(hash(fn) % 123124)
    descriptor_name = settings["detector"]["descriptor"]
    img_size = settings["detector"]["image_size"]
    part_size = settings[descriptor_name]["part_size"]
    psize = settings["detector"]["subsample_size"]

    # The 4 is for the edge border that falls off
    # orig_sh = (img_size[0] - part_size[0] - 4 + 1, img_size[1] - part_size[1] - 4 + 1)
    orig_sh = img_size
    sh = gv.sub.subsample_size(np.ones(orig_sh), psize)

    # We need the descriptor to generate and manipulate images
    descriptor = gv.load_descriptor(settings)

    counts = np.zeros(
        (settings["detector"]["num_mixtures"], sh[0], sh[1], descriptor.num_parts + 1, descriptor.num_parts),
        dtype=np.uint16,
    )

    prnds = [np.random.RandomState(seed + i) for i in xrange(5)]

    # Binarize support and Extract alpha
    # color_img, alpha = gv.img.load_image_binarized_alpha(fn)
    color_img = gv.img.load_image(fn)

    from skimage.transform import pyramid_reduce, pyramid_expand

    f = color_img.shape[0] / settings["detector"]["image_size"][0]
    if f > 1:
        color_img = pyramid_reduce(color_img, downscale=f)
    elif f < 1:
        color_img = pyramid_expand(color_img, upscale=1 / f)

    alpha = color_img[..., 3]
    img = gv.img.asgray(color_img)

    # Resize it
    # TODO: This only looks at the first axis

    assert img.shape == settings["detector"]["image_size"], "Target size not achieved: {0} != {1}".format(
        img.shape, settings["detector"]["image_size"]
    )

    # Settings
    bsettings = settings["edges"].copy()
    radius = bsettings["radius"]
    bsettings["radius"] = 0

    # offsets = gv.sub.subsample_offset_shape(sh, psize)

    # locations0 = xrange(offsets[0], sh[0], psize[0])
    # locations1 = xrange(offsets[1], sh[1], psize[1])
    locations0 = xrange(sh[0])
    locations1 = xrange(sh[1])
    # locations0 = xrange(10-4, 10+5)
    # locations1 = xrange(10-4, 10+5)

    # locations0 = xrange(10, 11)
    # locations1 = xrange(10, 11)

    # padded_theta = descriptor.unspread_parts_padded

    # pad = 10
    pad = 5
    size = settings[descriptor_name]["part_size"]
    X_pad_size = (size[0] + pad * 2, size[1] + pad * 2)

    img_pad = ag.util.zeropad(img, pad)

    alpha_pad = ag.util.zeropad(alpha, pad)

    # Iterate every duplicate

    dups = settings["detector"].get("duplicates", 1)

    bkgs = np.empty(((descriptor.num_parts + 1) * dups,) + X_pad_size)
    # cads = np.empty((descriptor.num_parts,) + X_pad_size)
    # alphas = np.empty((descriptor.num_parts,) + X_pad_size, dtype=np.bool)

    radii = settings["detector"]["spread_radii"]
    psize = settings["detector"]["subsample_size"]
    cb = settings["detector"].get("crop_border")
    sett = dict(spread_radii=radii, subsample_size=psize, crop_border=cb)

    plt.clf()
    plt.imshow(img)
    plt.savefig("output/img.png")

    if 0:
        # NEW{
        totfeats = np.zeros(sh + (descriptor.num_parts,) * 2)
        for f in xrange(descriptor.num_parts):
            num = bkg_stack_num[f]

            for d in xrange(dups):
                feats = np.zeros(sh + (descriptor.num_parts,), dtype=np.uint8)

                for i, j in itr.product(locations0, locations1):
                    x = i * psize[0]
                    y = i * psize[1]

                    bkg_i = prnds[4].randint(num)
                    bkg = bkg_stack[f, bkg_i]

                    selection = [slice(x, x + X_pad_size[0]), slice(y, y + X_pad_size[1])]
                    # X_pad = edges_pad[selection].copy()
                    patch = img_pad[selection]
                    alpha_patch = alpha_pad[selection]

                    # patch = np.expand_dims(patch, 0)
                    # alpha_patch = np.expand_dims(alpha_patch, 0)

                    # TODO: Which one?
                    # img_with_bkg = patch + bkg * (1 - alpha_patch)
                    img_with_bkg = patch * alpha_patch + bkg * (1 - alpha_patch)

                    edges_pads = ag.features.bedges(img_with_bkg, **bsettings)
                    X_pad_spreads = ag.features.bspread(edges_pads, spread=bsettings["spread"], radius=radius)

                    padding = pad - 2
                    X_spreads = X_pad_spreads[padding:-padding:, padding:-padding]

                    partprobs = ag.features.code_parts(
                        X_spreads,
                        descriptor._log_parts,
                        descriptor._log_invparts,
                        descriptor.settings["threshold"],
                        descriptor.settings["patch_frame"],
                    )

                    part = partprobs.argmax()
                    if part > 0:
                        feats[i, j, part - 1] = 1

                # Now spread the parts
                feats = ag.features.bspread(feats, spread="box", radius=2)

                totfeats[:, :, f] += feats

        # }

        kernels = totfeats[:, :, 0].astype(np.float32) / (descriptor.num_parts * dups)

        # Subsample kernels
        sub_kernels = gv.sub.subsample(kernels, psize, skip_first_axis=False)

        np.save("tmp2.npy", sub_kernels)
        print "saved tmp2.npy"
        import sys

        sys.exit(0)

    # ag.info("Iteration {0}/{1}".format(loop+1, num_duplicates))
    # ag.info("Iteration")
    for i, j in itr.product(locations0, locations1):
        x = i * psize[0]
        y = i * psize[1]

        print "processing", i, j
        selection = [slice(x, x + X_pad_size[0]), slice(y, y + X_pad_size[1])]
        # X_pad = edges_pad[selection].copy()
        patch = img_pad[selection]
        alpha_patch = alpha_pad[selection]

        patch = np.expand_dims(patch, 0)
        alpha_patch = np.expand_dims(alpha_patch, 0)

        for f in xrange(descriptor.num_parts + 1):
            num = bkg_stack_num[f]

            for d in xrange(dups):
                bkg_i = prnds[4].randint(num)
                bkgs[f * dups + d] = bkg_stack[f, bkg_i]

        img_with_bkgs = patch * alpha_patch + bkgs * (1 - alpha_patch)

        if 0:
            edges_pads = ag.features.bedges(img_with_bkgs, **bsettings)
            X_pad_spreads = ag.features.bspread(edges_pads, spread=bsettings["spread"], radius=radius)

            padding = pad - 2
            X_spreads = X_pad_spreads[:, padding:-padding:, padding:-padding]

        # partprobs = ag.features.code_parts_many(X_spreads, descriptor._log_parts, descriptor._log_invparts,
        # descriptor.settings['threshold'], descriptor.settings['patch_frame'])

        # parts = partprobs.argmax(axis=-1)

        parts = np.asarray([descriptor.extract_features(im, settings=sett)[0, 0] for im in img_with_bkgs])

        for f in xrange(descriptor.num_parts + 1):
            hist = np.bincount(parts[f * dups : (f + 1) * dups].ravel(), minlength=descriptor.num_parts + 1)
            counts[mixcomp, i, j, f] += hist[1:]

        # import pdb; pdb.set_trace()

        # for f in xrange(descriptor.num_parts):
        #    for d in xrange(dups):
        #        # Code parts
        #        #parts = descriptor.extract_parts(X_spreads[f*dups+d].astype(np.uint8))
    #
    #                f_plus = parts[f*dups+d]
    #                if f_plus > 0:
    # tau = self.settings.get('tau')
    # if self.settings.get('tau'):
    # parts = partprobs.argmax(axis=-1)

    # Accumulate and return
    #                    counts[mixcomp,i,j,f,f_plus-1] += 1#parts[0,0]

    if 0:
        kernels = counts[:, :, :, 0].astype(np.float32) / (descriptor.num_parts * dups)

        import pdb

        pdb.set_trace()

        radii = (2, 2)

        aa_log = np.log(1 - kernels)
        aa_log = ag.util.zeropad(aa_log, (0, radii[0], radii[1], 0))

        integral_aa_log = aa_log.cumsum(1).cumsum(2)

        offsets = gv.sub.subsample_offset(kernels[0], psize)

        if 1:
            # Fix kernels
            istep = 2 * radii[0]
            jstep = 2 * radii[1]
            sh = kernels.shape[1:3]
            for mixcomp in xrange(1):
                # Note, we are going in strides of psize, given a certain offset, since
                # we will be subsampling anyway, so we don't need to do the rest.
                for i in xrange(offsets[0], sh[0], psize[0]):
                    for j in xrange(offsets[1], sh[1], psize[1]):
                        p = gv.img.integrate(integral_aa_log[mixcomp], i, j, i + istep, j + jstep)
                        kernels[mixcomp, i, j] = 1 - np.exp(p)

        # Subsample kernels
        sub_kernels = gv.sub.subsample(kernels, psize, skip_first_axis=True)

        np.save("tmp.npy", sub_kernels)
        print "saved tmp.npy"
        import sys

        sys.exit(0)

    if 0:
        for f in xrange(descriptor.num_parts):

            # Pick only one background for this part and file
            num = bkg_stack_num[f]

            # Assumes num > 0

            bkg_i = prnds[4].randint(num)

            bkgmap = bkg_stack[f, bkg_i]

            # Composite
            img_with_bkg = gv.img.composite(patch, bkgmap, alpha_patch)

            # Retrieve unspread edges (with a given background gray level)
            edges_pad = ag.features.bedges(img_with_bkg, **bsettings)

            # Pad the edges
            # edges_pad = ag.util.zeropad(edges, (pad, pad, 0))

            # Do spreading
            X_pad_spread = ag.features.bspread(edges_pad, spread=bsettings["spread"], radius=radius)

            # De-pad
            padding = pad - 2
            X_spread = X_pad_spread[padding:-padding, padding:-padding]

            # Code parts
            parts = descriptor.extract_parts(X_spread.astype(np.uint8))

            # Accumulate and return
            counts[mixcomp, i, j, f] += parts[0, 0]

    # Translate counts to spread counts (since we're assuming independence of samples within one CAD image)

    return counts
Example #13
0
def laplacian_reconstruct(pyr):
	# Takes tuple of Laplacian pyramid levels and reconstructs original images
	if len(pyr) == 1:
		return pyr[0]
	else:
		return pyr[0] + skt.pyramid_expand(laplacian_reconstruct(pyr[1:]), upscale=2, sigma=10, mode='constant', cval=0.0)
def _properties2d(image, dim):
    """
    Compute shape property of the input 2D image. Accounts for partial volume information.
    :param image: 2D input image in uint8 or float (weighted for partial volume) that has a single object.
    :param dim: [px, py]: Physical dimension of the image (in mm). X,Y respectively correspond to AP,RL.
    :return:
    """
    upscale = 5  # upscale factor for resampling the input image (for better precision)
    pad = 3  # padding used for cropping
    # Check if slice is empty
    if not image.any():
        logging.debug('The slice is empty.')
        return None
    # Normalize between 0 and 1 (also check if slice is empty)
    image_norm = (image - image.min()) / (image.max() - image.min())
    # Convert to float64
    image_norm = image_norm.astype(np.float64)
    # Binarize image using threshold at 0. Necessary input for measure.regionprops
    image_bin = np.array(image_norm > 0.5, dtype='uint8')
    # Get all closed binary regions from the image (normally there is only one)
    regions = measure.regionprops(image_bin, intensity_image=image_norm)
    # Check number of regions
    if len(regions) > 1:
        logging.debug('There is more than one object on this slice.')
        return None
    region = regions[0]
    # Get bounding box of the object
    minx, miny, maxx, maxy = region.bbox
    # Use those bounding box coordinates to crop the image (for faster processing)
    image_crop = image_norm[np.clip(minx - pad, 0, image_bin.shape[0]):np.
                            clip(maxx + pad, 0, image_bin.shape[0]),
                            np.clip(miny - pad, 0, image_bin.shape[1]):np.
                            clip(maxy + pad, 0, image_bin.shape[1])]
    # Oversample image to reach sufficient precision when computing shape metrics on the binary mask
    image_crop_r = transform.pyramid_expand(image_crop,
                                            upscale=upscale,
                                            sigma=None,
                                            order=1)
    # Binarize image using threshold at 0. Necessary input for measure.regionprops
    image_crop_r_bin = np.array(image_crop_r > 0.5, dtype='uint8')
    # Get all closed binary regions from the image (normally there is only one)
    regions = measure.regionprops(image_crop_r_bin,
                                  intensity_image=image_crop_r)
    region = regions[0]
    # Compute area with weighted segmentation and adjust area with physical pixel size
    area = np.sum(image_crop_r) * dim[0] * dim[1] / upscale**2
    # Compute ellipse orientation, modulo pi, in deg, and between [0, 90]
    orientation = fix_orientation(region.orientation)
    # Find RL and AP diameter based on major/minor axes and cord orientation=
    [diameter_AP, diameter_RL] = \
        _find_AP_and_RL_diameter(region.major_axis_length, region.minor_axis_length, orientation,
                                 [i / upscale for i in dim])
    # TODO: compute major_axis_length/minor_axis_length by summing weighted voxels along axis
    # Deal with https://github.com/neuropoly/spinalcordtoolbox/issues/2307
    if any(x in platform.platform() for x in ['Darwin-15', 'Darwin-16']):
        solidity = np.nan
    else:
        solidity = region.solidity
    # Fill up dictionary
    properties = {
        'area': area,
        'diameter_AP': diameter_AP,
        'diameter_RL': diameter_RL,
        'centroid': region.centroid,
        'eccentricity': region.eccentricity,
        'orientation': orientation,
        'solidity': solidity  # convexity measure
    }

    return properties