Source code for openalea.phenomenal.multi_view_reconstruction.multi_view_reconstruction

# -*- python -*-
#
#       Copyright INRIA - CIRAD - INRA
#
#       Distributed under the Cecill-C License.
#       See accompanying file LICENSE.txt or copy at
#           http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html
#
# ==============================================================================
from __future__ import division, print_function

import math
import collections
import cv2
import scipy.spatial
import numpy
import sklearn.neighbors

import openalea.phenomenal.multi_view_reconstruction._c_mvr as c_mvr

from ..object import VoxelGrid

# ==============================================================================
# Class

Voxels = collections.namedtuple("Voxels", ["position", "size"])
VoxelsStage = collections.namedtuple("VoxelsStage", ["consistent", "inconsistent"])


# ==============================================================================


[docs] def get_voxels_corners(voxels_position, voxels_size): """According to the voxels position and their size, return a numpy array containing for each input voxels the position of the 8 corners. Parameters ---------- voxels_position : numpy.ndarray Center position of the voxels voxels_size : float Diameter size of the voxels Returns ------- a : numpy.array """ r = voxels_size / 2.0 x_minus = voxels_position[:, 0] - r x_plus = voxels_position[:, 0] + r y_minus = voxels_position[:, 1] - r y_plus = voxels_position[:, 1] + r z_minus = voxels_position[:, 2] - r z_plus = voxels_position[:, 2] + r a1 = numpy.column_stack((x_minus, y_minus, z_minus)) a2 = numpy.column_stack((x_plus, y_minus, z_minus)) a3 = numpy.column_stack((x_minus, y_plus, z_minus)) a4 = numpy.column_stack((x_minus, y_minus, z_plus)) a5 = numpy.column_stack((x_plus, y_plus, z_minus)) a6 = numpy.column_stack((x_plus, y_minus, z_plus)) a7 = numpy.column_stack((x_minus, y_plus, z_plus)) a8 = numpy.column_stack((x_plus, y_plus, z_plus)) a = numpy.concatenate((a1, a2, a3, a4, a5, a6, a7, a8), axis=1) a = numpy.reshape(a, (a.shape[0] * 8, 3)) return a
[docs] def get_bounding_box_voxel_projected(voxels_position, voxels_size, projection): """Compute the bounding box value according the radius, angle and calibration parameters of point_3d projection Parameters ---------- voxels_position : numpy.ndarray Center position of voxel voxels_size : float Size of side geometry of voxel projection : function ((x, y, z)) -> (x, y) Function of projection who take 1 argument (tuple of position (x, y, z)) and return this position 2D (x, y) Returns ------- bbox : numpy.ndarray [[x_min, x_max, y_min, y_max], ...] Containing min and max value of point_3d projection in x and y axes. """ voxels_corners = get_voxels_corners(voxels_position, voxels_size) pt = projection(voxels_corners) pt = numpy.reshape(pt, (pt.shape[0] // 8, 8, 2)) bbox = numpy.column_stack((pt.min(axis=1), pt.max(axis=1))) return bbox
# ==============================================================================
[docs] def split_voxels_in_eight(voxels): """Split each voxel in 8 en return the numpy.array position _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ / /| / / /| / / | /--------/---------/ | /_ _ _ _ _ _ _ _ _ / | /_ _ _ _ / _ _ _ _ /| | | | | | | | |/| | | | =======> | | | / | | | | |_ _ _ _ | _ _ _ _ |/| | | | / | | | |/ | | / | | | / | _ _ _ _ _ _ _ _ _|/ |_ _ _ _ | _ _ _ _ |/ Parameters ---------- voxels : Voxels Where position is a numpy.array([[x, y, z], ...] containing the center position of each voxel and size the diameter size value (float) of each voxel. Returns ------- voxels : Voxels """ if len(voxels.position) == 0: return Voxels(voxels.position, voxels.size / 2.0) r = voxels.size / 4.0 x_minus = voxels.position[:, 0] - r x_plus = voxels.position[:, 0] + r y_minus = voxels.position[:, 1] - r y_plus = voxels.position[:, 1] + r z_minus = voxels.position[:, 2] - r z_plus = voxels.position[:, 2] + r a1 = numpy.column_stack((x_minus, y_minus, z_minus)) a2 = numpy.column_stack((x_plus, y_minus, z_minus)) a3 = numpy.column_stack((x_minus, y_plus, z_minus)) a4 = numpy.column_stack((x_minus, y_minus, z_plus)) a5 = numpy.column_stack((x_plus, y_plus, z_minus)) a6 = numpy.column_stack((x_plus, y_minus, z_plus)) a7 = numpy.column_stack((x_minus, y_plus, z_plus)) a8 = numpy.column_stack((x_plus, y_plus, z_plus)) return Voxels( numpy.concatenate((a1, a2, a3, a4, a5, a6, a7, a8), axis=0), voxels.size / 2.0 )
# ==============================================================================
[docs] def voxels_is_visible_in_image( voxels_position, voxels_size, image, projection, inclusive, image_int=None ): """ Return a numpy array containing True if the voxel are projected is photo-consistent on image else False **Algorithm** 1. Project each voxel center position on the image, if the position projected (x, y) is positive on image return True | 2. If the bounding box of the voxel projected have positive value on the image the voxel are True | 3. Check if one pixel containing in the bounding box projected on image have positive value, if yes return True else return False Parameters ---------- voxels_position : numpy.array([[x, y, z], ...] Center position of the voxels voxels_size : float diameter size of the voxels image: numpy.array Binary image where the voxels are projected. projection : function (numpy.array([[x, y, z], ...]) -> numpy.array([[x, y], ...]) Function of projection who take 1 argument (numpy.array([[x, y, z], ...] of voxels positions) and return the projected 2D position numpy.array([[x, y], ...]) inclusive: Describe if the voxels projection are out of the image, they are considered like still visible image_int: Integral image of the binary image (optimization) Returns ------- out : numpy.array([True, False, ...]) Numpy array containing True if the voxel are projected is photo-consistent on image else False """ height, length = image.shape ori_result = numpy.zeros( ( len( voxels_position, ) ), dtype=int, ) r = projection(voxels_position) cond = (r[:, 0] >= 0) & (r[:, 1] >= 0) & (r[:, 0] < length) & (r[:, 1] < height) rr = r[cond].astype(int) (ori_result[cond])[image[rr[:, 1], rr[:, 0]] > 0] = 1 not_cond = numpy.logical_not(ori_result > 0) result = ori_result[not_cond] voxels_position = voxels_position[not_cond] # ========================================================================== min_xy_max_xy = get_bounding_box_voxel_projected( voxels_position, voxels_size, projection ) vv = ( (min_xy_max_xy[:, 2] < 0) | (min_xy_max_xy[:, 0] >= length) | (min_xy_max_xy[:, 3] < 0) | (min_xy_max_xy[:, 1] >= height) ) not_vv = numpy.logical_not(vv) result[vv] = 1 if inclusive else 0 min_xy_max_xy = min_xy_max_xy[not_vv] bb = result[not_vv] min_xy_max_xy = numpy.floor(min_xy_max_xy).astype(int) # X limit (min_xy_max_xy[:, 0])[min_xy_max_xy[:, 0] >= length] = length - 1 (min_xy_max_xy[:, 2])[min_xy_max_xy[:, 2] >= length] = length - 1 # Y limit (min_xy_max_xy[:, 1])[min_xy_max_xy[:, 1] >= height] = height - 1 (min_xy_max_xy[:, 3])[min_xy_max_xy[:, 3] >= height] = height - 1 # Under zero limit min_xy_max_xy[:, 0:2] -= 1 # For integral image optimization min_xy_max_xy[min_xy_max_xy < 0] = 0 # ========================================================================== for i, (x_min, y_min, x_max, y_max) in enumerate(min_xy_max_xy): if ( image_int[y_max, x_max] + image_int[y_min, x_min] - image_int[y_min, x_max] - image_int[y_max, x_min] ) > 0: bb[i] = 1 # for i, (x_min, y_min, x_max, y_max) in enumerate(min_xy_max_xy): # if numpy.count_nonzero(image[y_min:y_max + 1, x_min:x_max + 1]) > 0: # bb[i] = 1 result[not_vv] = bb ori_result[not_cond] = result return ori_result
# ==============================================================================
[docs] def kept_visible_voxel( voxels_position, voxels_size, image_views, error_tolerance=0, int_images=None ): """ Kept in a new collections.deque the voxel who is visible on each image of images_projections according the error_tolerance Parameters ---------- voxels_position : numpy.array([[x, y, z], ...] Center position of the voxels voxels_size : float Diameter size of the voxels image_views : [(image, projection), ...] List of tuple (image, projection) where image is a binary image (numpy.ndarray) and function projection (function (x, y, z) -> (x, y)) who take (x, y, z) position on return (x, y) position according space representation of this image error_tolerance : int, optional Number of image will be ignored if the projected voxel is not visible. int_images: Integral image of the binary image (optimization) Returns ------- out : VoxelsStage """ photo_consistent = numpy.zeros((len(voxels_position),), dtype=int) no_kept = None for i, image_view in enumerate(image_views): photo_consistent += voxels_is_visible_in_image( voxels_position, voxels_size, image_view.image, image_view.projection, image_view.inclusive, image_int=int_images[i], ) cond = photo_consistent >= i + 1 - error_tolerance if no_kept is None: no_kept = voxels_position[numpy.logical_not(cond)] else: no_kept = numpy.insert( no_kept, 0, voxels_position[numpy.logical_not(cond)], axis=0 ) voxels_position = voxels_position[cond] photo_consistent = photo_consistent[cond] consistent = Voxels(voxels_position, voxels_size) inconsistent = Voxels(no_kept, voxels_size) return VoxelsStage(consistent, inconsistent)
# ==============================================================================
[docs] def have_image_ref(image_views): """ Returns whether an array of ImageView has a reference image or not. Parameters ---------- image_views: array[ImageView] The array of ImageView to test Returns ------- True if the array has a reference image else False. """ for iv in image_views: if iv.image_ref is not None: return True return False
[docs] def create_groups(image_views, inconsistent): groups = collections.defaultdict(list) kept_groups = collections.defaultdict(list) group_id = 0 for iv in image_views: if iv.image_ref is not None: height, length = iv.image.shape min_xy_max_xy = get_bounding_box_voxel_projected( inconsistent.position, inconsistent.size, iv.projection ) # add each voxel to a visual cones for i, (x_min, y_min, x_max, y_max) in enumerate(min_xy_max_xy): if x_max < 0 or x_min >= length or y_max < 0 or y_min >= height: continue x_min = int(min(max(math.floor(x_min), 0), length - 1)) y_min = int(min(max(math.floor(y_min), 0), height - 1)) x_max = int(min(max(math.floor(x_max), 0), length - 1)) y_max = int(min(max(math.floor(y_max), 0), height - 1)) img = iv.image_ref[y_min : y_max + 1, x_min : x_max + 1] yy, xx = numpy.where(img > 0) yy += y_min xx += x_min for y, x in zip(yy, xx): groups[(group_id, y, x)].append(i) for y, x in zip(iv.yy, iv.xx): if len(groups[(group_id, y, x)]) > 0: kept_groups[(group_id, y, x)] = groups[(group_id, y, x)] group_id += 1 return kept_groups
[docs] def check_groups(neigh, inconsistent, groups, nb_distance): if len(groups.values()) == 0: return None positions = [] for index in groups.values(): index = numpy.array(index) distance, _ = neigh.kneighbors(inconsistent.position[index]) distance = distance.min(axis=1) xx = distance.argsort()[:nb_distance] positions.append(inconsistent.position[index[xx]]) position = numpy.unique(numpy.concatenate(positions, axis=0), axis=0) return Voxels(position, inconsistent.size)
[docs] def reconstruction_inconsistent(image_views, stages, attractor=None): for iv in image_views: if iv.image_ref is not None: im = project_voxel_centers_on_image( stages[-1].consistent.position, stages[-1].consistent.size, iv.image.shape, iv.projection, ) iv.il = iv.image_ref - im iv.yy, iv.xx = numpy.where(iv.il > 0) consistent_neighbors = sklearn.neighbors.NearestNeighbors( n_neighbors=1, metric="euclidean" ) if numpy.size(stages[-1].consistent.position) == 0: consistent_neighbors.fit(numpy.array([[0, 0, 0]])) else: if attractor is not None: consistent_neighbors.fit(attractor) else: consistent_neighbors.fit(stages[-1].consistent.position) consistents = [None] * len(stages) for i, stage in enumerate(stages): if stage.inconsistent is None: continue inconsistent = stage.inconsistent if consistents[i - 1] is not None: voxels = split_voxels_in_eight(consistents[i - 1]) position = numpy.concatenate( (inconsistent.position, voxels.position), axis=0 ) position = numpy.unique(position, axis=0) inconsistent = Voxels(position, inconsistent.size) groups = create_groups(image_views, inconsistent) nb_distance = max(20 - int((20 / len(stages)) * i), 2) consistents[i] = check_groups( consistent_neighbors, inconsistent, groups, nb_distance ) consistent_stages = [None] * len(stages) for i, (stage, consistent) in enumerate(zip(stages, consistents)): consistent_stages[i] = stage.consistent if consistent is not None: voxels_position = numpy.concatenate( (consistent_stages[i].position, consistent.position), axis=0 ) voxels_position = numpy.unique(voxels_position, axis=0) consistent_stages[i] = Voxels(voxels_position, consistent.size) return consistent_stages
# ==============================================================================
[docs] def get_integrale_image(img): a = numpy.zeros_like(img, dtype=int) a[img > 0] = 1 for y, x in numpy.ndindex(a.shape): if x - 1 >= 0: a[y, x] += a[y, x - 1] if y - 1 >= 0: a[y, x] += a[y - 1, x] if x - 1 >= 0 and y - 1 >= 0: a[y, x] -= a[y - 1, x - 1] return a
# ==============================================================================
[docs] def reconstruction_3d( image_views, voxels_size=4, error_tolerance=0, voxel_center_origin=(0.0, 0.0, 0.0), start_voxel_size=4096, voxels_position=None, attractor=None, ): """ Construct a list of voxel represented object with positive value on binary image in images of images_projections. Parameters ---------- image_views : [(image, projection), ...] List of tuple (image, projection) where image is a binary image (numpy.ndarray) and function projection (function (x, y, z) -> (x, y)) who take (x, y, z) position on return (x, y) position according space representation of this image voxels_size : float, optional Diameter size of the voxels error_tolerance : int, optional voxel_center_origin : (x, y, z), optional Center position of the first original voxel, who will be split. start_voxel_size: int, optional Minimum size that the origin voxel size must include at beginning voxels_position : numpy.ndarray, optional List of first original voxel who will be split. If None, a list is created with the voxel_center_origin value. Returns ------- out : VoxelGrid """ if len(image_views) == 0: raise ValueError("Len images view have not length") if voxels_position is None: voxels_position = numpy.array([voxel_center_origin]) list_voxels_size = [ voxels_size * 2**i for i in range(20, -1, -1) if voxels_size * 2 ** (i - 1) < start_voxel_size ] # Pre-processing (optimization): Compute integral image for speed # computation int_images = [] for i, image_view in enumerate(image_views): a = numpy.zeros_like(image_view.image, dtype=numpy.uint32) c_mvr.integral_image(image_view.image, a) int_images.append(a) stage = VoxelsStage(Voxels(voxels_position, list_voxels_size[0]), None) stages = [stage] while stage.consistent.size != voxels_size: if len(stage.consistent.position) == 0: break voxels = split_voxels_in_eight(stage.consistent) # print(voxels.size) if voxels.size < 512: stage = kept_visible_voxel( voxels.position, voxels.size, image_views, error_tolerance=error_tolerance, int_images=int_images, ) else: stage = VoxelsStage(voxels, None) stages.append(stage) consistent_stages = [stage.consistent for stage in stages] if have_image_ref(image_views): consistent_stages = reconstruction_inconsistent( image_views, stages, attractor=attractor ) return VoxelGrid(consistent_stages[-1].position, consistent_stages[-1].size)
# ==============================================================================
[docs] def project_voxel_centers_on_image( voxels_position, voxels_size, shape_image, projection, value=255, dtype=numpy.uint8 ): """ Create a image with same shape that shape_image and project each voxel on image and write positive value (255) on it. Parameters ---------- voxels_position : numpy.ndarray Voxels center position [[x, y, z], ...] voxels_size : float Diameter size of the voxels shape_image: 2-tuple Size height and width of the image target projected projection : function ((x, y, z)) -> (x, y) Function of projection who take 1 argument (tuple of position (x, y, z)) and return this position 2D (x, y) value : int value between 0 and 255 of positive pixel. By default, 255. dtype : type numpy type of the returned image. By default, numpy.uint8. Returns ------- out : numpy.ndarray Binary image """ height, length = shape_image img = numpy.zeros((height, length), dtype=dtype) min_xy_max_xy = get_bounding_box_voxel_projected( voxels_position, voxels_size, projection ) vv = ( (min_xy_max_xy[:, 2] < 0) | (min_xy_max_xy[:, 0] >= length) | (min_xy_max_xy[:, 3] < 0) | (min_xy_max_xy[:, 1] >= height) ) not_vv = numpy.logical_not(vv) min_xy_max_xy = min_xy_max_xy[not_vv] min_xy_max_xy = numpy.floor(min_xy_max_xy) min_xy_max_xy[min_xy_max_xy < 0] = 0 (min_xy_max_xy[:, 0])[min_xy_max_xy[:, 0] >= length] = length - 1 (min_xy_max_xy[:, 1])[min_xy_max_xy[:, 1] >= height] = height - 1 (min_xy_max_xy[:, 2])[min_xy_max_xy[:, 2] >= length] = length - 1 (min_xy_max_xy[:, 3])[min_xy_max_xy[:, 3] >= height] = height - 1 min_xy_max_xy = min_xy_max_xy.astype(int) for x_min, y_min, x_max, y_max in min_xy_max_xy: img[y_min : y_max + 1, x_min : x_max + 1] = value return img
[docs] def project_voxels_position_on_image( voxels_position, voxels_size, shape_image, projection ): """ Create an image with same shape that shape_image and project each voxel on image and write positive value (255) on it. Parameters ---------- voxels_position : [(x, y, z)] cList (collections.deque) of center position of voxel voxels_size : float Size of side geometry of voxel shape_image: Tuple size height and length of the image target projected projection : function ((x, y, z)) -> (x, y) Function of projection who take 1 argument (tuple of position (x, y, z)) and return this position 2D (x, y) Returns ------- out : numpy.ndarray Binary image """ voxels_position = numpy.array(voxels_position) height, length = shape_image img = numpy.zeros((height, length), dtype=numpy.uint8) voxels_corners = get_voxels_corners(voxels_position, voxels_size) pt = projection(voxels_corners) pt = numpy.reshape(pt, (pt.shape[0] // 8, 8, 2)) pt[pt < 0] = 0 (pt[:, :, 0])[pt[:, :, 0] >= length] = length - 1 (pt[:, :, 1])[pt[:, :, 1] >= height] = height - 1 pt = numpy.floor(pt).astype(int) for points in pt: hull = scipy.spatial.ConvexHull(points) cv2.fillConvexPoly(img, points[hull.vertices], 255) return img
# ==============================================================================
[docs] def image_error(img_ref, img_src, precision=2): """ Return false position and true negative result from the comparison on two binaries images """ img_ref = img_ref.astype(numpy.int32) nb_ref = max(numpy.count_nonzero(img_ref), 1) nb_ref2 = max(numpy.count_nonzero(img_ref == 0), 1) img_src = img_src.astype(numpy.int32) img = numpy.subtract(img_ref, img_src) true_negative = numpy.bitwise_and(img_ref == 0, img_src == 0) true_negative = round( numpy.count_nonzero(true_negative) * 100.0 / nb_ref2, precision ) # true_negative = numpy.bitwise_and(img_ref == 0, img_src == 0) false_positive = round( numpy.count_nonzero(img[img < 0]) * 100.0 / nb_ref2, precision ) false_negative = round( numpy.count_nonzero(img[img > 0]) * 100.0 / nb_ref, precision ) print(true_negative, false_positive, false_negative) return false_positive, false_negative
[docs] def reconstruction_error(voxels_grid, image_views): """ Compute the reconstruction error (false positive and true negative) of the 3d reconstruction from the image view. Parameters ---------- voxels_grid: VoxelGrid The voxel grid image_views : numpy.ndarray[ImageView] An array of all image views Returns ------- out : (float,float) A tuple with the mean false positive and the mean false negative """ sum_false_positive = 0 sum_false_negative = 0 for image_view in image_views: img_src = project_voxel_centers_on_image( voxels_grid.voxels_position, voxels_grid.voxels_size, image_view.image.shape, image_view.projection, ) false_positive, false_negative = image_error(image_view.image, img_src) sum_false_positive += false_positive sum_false_negative += false_negative mean_false_positive = sum_false_positive / len(image_views) mean_false_negative = sum_false_negative / len(image_views) return mean_false_positive, mean_false_negative