Source code for wormpose.images.synthetic.synthetic_dataset

"""
Module responsible for drawing the synthetic worm image
"""

import random
from typing import Tuple

import cv2
import numpy as np

from wormpose import BaseFramePreprocessing
from wormpose.dataset.image_processing import frame_preprocessor
from wormpose.images.synthetic import (
    MAX_OFFSET_PERCENT,
    GAUSSIAN_BLUR_MIN_SIZE,
    GAUSSIAN_BLUR_MAX_SIZE_PERCENT,
    GAUSSIAN_BLUR_MAX_SIZE,
    THICKNESS_MULTIPLIER_OFFSET,
    THICKNESS_MULTIPLIER,
    WORM_LENGTH_MULTIPLIER_OFFSET,
    BLUR_PROBABILITY,
)
from wormpose.images.synthetic._coord_transform import make_calc_all_coords
from wormpose.images.synthetic._helpers import WormOutlineMask, PatchDrawing, TargetSkeletonCalculator
from wormpose.pose.centerline import get_joint_indexes


def _make_synth_worm_measurements(nb_skeleton_joints: int, measurements):
    """
    Transforms the original measurements to make them usable by the synthetic dataset generator
    The width of the worm is interpolated between the known joints (head, midbody and tail)
    """
    head_width = np.nanmean(measurements["head_width"])
    midbody_width = np.nanmean(measurements["midbody_width"])
    tail_width = np.nanmean(measurements["tail_width"])

    head_joint, mid_body_joint, tail_body_joint = get_joint_indexes(nb_skeleton_joints)
    worm_thickness = np.empty(nb_skeleton_joints, np.float32)
    worm_thickness[:head_joint] = head_width
    worm_thickness[head_joint:mid_body_joint] = np.linspace(head_width, midbody_width, mid_body_joint - head_joint)
    worm_thickness[mid_body_joint:tail_body_joint] = np.linspace(
        midbody_width, tail_width, tail_body_joint - mid_body_joint
    )
    worm_thickness[tail_body_joint:] = tail_width

    worm_length = np.nanmean(measurements["worm_length"])

    return worm_length, worm_thickness


[docs]class SyntheticDataset(object): """ Class responsible for generating synthetic images """ def __init__( self, frame_preprocessing: BaseFramePreprocessing, enable_random_augmentations: bool, output_image_shape, ): """ :param frame_preprocessing: the FramePreprocessing object containing the image preprocessing logic :param enable_random_augmentations: Adds augmentation to the syntthetic images if set to True: translation, scale, blur :param output_image_shape: Desired size of the synthetic images """ self.output_image_shape = (output_image_shape[0], output_image_shape[1]) self.enable_random_augmentations = enable_random_augmentations self.frame_preprocessing = frame_preprocessing self._max_offset = ( int(self.output_image_shape[0] * MAX_OFFSET_PERCENT), int(self.output_image_shape[1] * MAX_OFFSET_PERCENT), ) self._nb_skeleton_joints = None self._theta_dims = None self._calc_all_coords = None self._synth_worm_measurements_cache = {} self._worm_outline_mask = WormOutlineMask(self.output_image_shape) self._frames_infos_cache = {} self._patch_drawing = PatchDrawing(output_image_shape=self.output_image_shape) self._target_skeleton_calculator = None self._target_skeleton = None self._gaussian_blur_filter_sizes = None # blurring image when random augmentations apply : # blur kernel size is chosen between GAUSSIAN_BLUR_MIN_SIZE and GAUSSIAN_BLUR_MAX_SIZE # (or a GAUSSIAN_BLUR_MAX_SIZE_PERCENT of the image size if smaller than GAUSSIAN_BLUR_MAX_SIZE) if self.enable_random_augmentations: self._gaussian_blur_filter_sizes = np.arange( GAUSSIAN_BLUR_MIN_SIZE, min(GAUSSIAN_BLUR_MAX_SIZE + 1, int(GAUSSIAN_BLUR_MAX_SIZE_PERCENT * min(output_image_shape))), 2, ) # pre allocate numpy arrays for calculations self._out_image = np.empty(self.output_image_shape, dtype=np.float32) def _get_recentered_frame(self, template_frame, template_skeleton): frame_id = hash(template_frame.tostring()) if frame_id in self._frames_infos_cache: worm_roi, bg_mean_color = self._frames_infos_cache[frame_id] else: frame, bg_mean_color, worm_roi = frame_preprocessor.run(self.frame_preprocessing, template_frame) self._frames_infos_cache[frame_id] = (worm_roi, bg_mean_color) template_skeletons_recentered = template_skeleton - ( worm_roi[1].start, worm_roi[0].start, ) return template_frame[worm_roi], bg_mean_color, template_skeletons_recentered def _get_synth_measurements(self, worm_measurements): measurements_id = id(worm_measurements) if measurements_id not in self._synth_worm_measurements_cache: self._synth_worm_measurements_cache[measurements_id] = _make_synth_worm_measurements( self._nb_skeleton_joints, worm_measurements ) return self._synth_worm_measurements_cache[measurements_id]
[docs] def generate( self, theta: np.ndarray, template_frame: np.ndarray, template_skeleton: np.ndarray, template_measurements: np.ndarray, out_image: np.ndarray, ) -> Tuple[int, np.ndarray]: """ Generates a synthetic image :param theta: desired posture (angles) :param template_frame: reference image :param template_skeleton: skeleton of the reference image :param template_measurements: measurements of the reference image (worm width and length) :param out_image: canvas to draw the synthetic image :return: background color and the synthetic worm skeleton coordinates """ if out_image.dtype != np.uint8: raise NotImplementedError("Only uint8 images supported.") if self._nb_skeleton_joints is None: self._nb_skeleton_joints = len(template_skeleton) self._target_skeleton = np.empty_like(template_skeleton) if len(template_skeleton) != self._nb_skeleton_joints: raise NotImplementedError("Can't change the amount of skeleton joints.") if self._theta_dims is None: self._theta_dims = len(theta) if len(theta) != self._theta_dims: raise NotImplementedError("Can't change theta dimensions.") if self._target_skeleton_calculator is None: self._target_skeleton_calculator = TargetSkeletonCalculator( theta_dims=self._theta_dims, nb_skeletons_joints=self._nb_skeleton_joints, output_image_shape=self.output_image_shape, ) if self._calc_all_coords is None: self._calc_all_coords = make_calc_all_coords( nb_skeleton_joints=self._nb_skeleton_joints, enable_random_augmentations=self.enable_random_augmentations, ) # get image and skeleton recentered to region of interest recentered_frame, cur_bg_color, template_skel = self._get_recentered_frame(template_frame, template_skeleton) # get worm length and worm thickness from cache cur_worm_length, cur_worm_thickness = self._get_synth_measurements(template_measurements) # increase thickness to include more background pixels when making patches # (with some variation for data augmentation) thickness_multiplier_offset = THICKNESS_MULTIPLIER_OFFSET if self.enable_random_augmentations else 0 thickness_multiplier = np.random.uniform( THICKNESS_MULTIPLIER - thickness_multiplier_offset, THICKNESS_MULTIPLIER + thickness_multiplier_offset ) target_worm_thickness = cur_worm_thickness * thickness_multiplier target_worm_length = cur_worm_length # vary target worm length for data augmentation if self.enable_random_augmentations: target_worm_length = cur_worm_length * np.random.uniform( 1.0 - WORM_LENGTH_MULTIPLIER_OFFSET, 1.0 + WORM_LENGTH_MULTIPLIER_OFFSET ) # calculate the target skeleton coordinates, apply optional image augmentation self._target_skeleton_calculator.calculate( theta=theta, worm_length=target_worm_length, out=self._target_skeleton ) target_skel = self._augment_skeleton(self._target_skeleton) # reset image canvas where we will draw the synthetic worm self._out_image.fill(0.0) # draw all the body patches: affine transform from template_coords to target_coords all_template_coords, all_target_coords = self._calc_all_coords( template_skel, target_skel, target_worm_thickness ) for template_coords, target_coords in zip(all_template_coords, all_target_coords): self._patch_drawing.draw_patch( template_coords=target_coords, target_coords=template_coords, recentered_frame=recentered_frame, out_image=self._out_image, ) # apply the worm outline mask to hide some unwanted pixels from the patch drawing self._worm_outline_mask.apply( worm_thickness=target_worm_thickness / 2, output_image=self._out_image, target_skel=target_skel, ) # all zero pixels become background color #FIXME this doesn't work if the worm image has zero values self._out_image[self._out_image == 0] = cur_bg_color # median blur to smooth out the connections between patches while preserving edge features cv2.medianBlur(self._out_image, 3, dst=self._out_image) # optional image augmentation: gaussian blur self._augment_extra_blur(self._out_image) # convert final image from float to uint8 np.copyto(out_image, self._out_image, casting="unsafe") return cur_bg_color, target_skel.copy()
def _augment_skeleton(self, target_skel): # random translation offset tx, ty = ( ( np.random.uniform(-self._max_offset[0], self._max_offset[0]), np.random.uniform(-self._max_offset[1], self._max_offset[1]), ) if self.enable_random_augmentations else (0, 0) ) target_skel[:, 0] += tx target_skel[:, 1] += ty return target_skel def _augment_extra_blur(self, out_image): if self.enable_random_augmentations and np.random.uniform() > 1 - BLUR_PROBABILITY: ksize = random.choice(self._gaussian_blur_filter_sizes) cv2.GaussianBlur(out_image, (ksize, ksize), 0, dst=out_image)