//  [python]
def sample_affine_transform(batchsize,
                            n_dims,
                            rotation_bounds=False,
                            scaling_bounds=False,
                            shearing_bounds=False,
                            translation_bounds=False,
                            enable_90_rotations=False):
    """build batchsize x 4 x 4 tensor representing an affine transformation in homogeneous coordinates.
    If return_inv is True, also returns the inverse of the created affine matrix."""

    if (rotation_bounds is not False) | (enable_90_rotations is not False):
        if n_dims == 2:
            if rotation_bounds is not False:
                rotation = draw_value_from_distribution(rotation_bounds,
                                                        size=1,
                                                        default_range=15.0,
                                                        return_as_tensor=True,
                                                        batchsize=batchsize)
            else:
                rotation = tf.zeros(tf.concat([batchsize, tf.ones(1, dtype='int32')], axis=0))
        else:  # n_dims = 3
            if rotation_bounds is not False:
                rotation = draw_value_from_distribution(rotation_bounds,
                                                        size=n_dims,
                                                        default_range=15.0,
                                                        return_as_tensor=True,
                                                        batchsize=batchsize)
            else:
                rotation = tf.zeros(tf.concat([batchsize, 3 * tf.ones(1, dtype='int32')], axis=0))
        if enable_90_rotations:
            rotation = tf.to_float(tf.random.uniform(tf.shape(rotation), maxval=4, dtype='int32') * 90) \
                       + rotation
        T_rot = create_rotation_transform(rotation, n_dims)
    else:
        T_rot = tf.tile(tf.expand_dims(tf.eye(n_dims), axis=0),
                        tf.concat([batchsize, tf.ones(2, dtype='int32')], axis=0))

    if shearing_bounds is not False:
        shearing = draw_value_from_distribution(shearing_bounds,
                                                size=n_dims ** 2 - n_dims,
                                                default_range=.01,
                                                return_as_tensor=True,
                                                batchsize=batchsize)
        T_shearing = create_shearing_transform(shearing, n_dims)
    else:
        T_shearing = tf.tile(tf.expand_dims(tf.eye(n_dims), axis=0),
                             tf.concat([batchsize, tf.ones(2, dtype='int32')], axis=0))

    if scaling_bounds is not False:
        scaling = draw_value_from_distribution(scaling_bounds,
                                               size=n_dims,
                                               centre=1,
                                               default_range=.15,
                                               return_as_tensor=True,
                                               batchsize=batchsize)
        T_scaling = tf.linalg.diag(scaling)
    else:
        T_scaling = tf.tile(tf.expand_dims(tf.eye(n_dims), axis=0),
                            tf.concat([batchsize, tf.ones(2, dtype='int32')], axis=0))

    T = tf.matmul(T_scaling, tf.matmul(T_shearing, T_rot))

    if translation_bounds is not False:
        translation = draw_value_from_distribution(translation_bounds,
                                                   size=n_dims,
                                                   default_range=5,
                                                   return_as_tensor=True,
                                                   batchsize=batchsize)
        T = tf.concat([T, tf.expand_dims(translation, axis=-1)], axis=-1)
    else:
        T = tf.concat([T, tf.zeros(tf.concat([tf.shape(T)[:2], tf.ones(1, dtype='int32')], 0))], axis=-1)

    # build rigid transform
    T_last_row = tf.expand_dims(tf.concat([tf.zeros((1, n_dims)), tf.ones((1, 1))], axis=1), 0)
    T_last_row = tf.tile(T_last_row, tf.concat([batchsize, tf.ones(2, dtype='int32')], axis=0))
    T = tf.concat([T, T_last_row], axis=1)

    return T