""" Uniformly samples 3-dimensional value over the bounding box of the specified objects """
import math
import random
from typing import List, Union, Optional, Tuple
import numpy as np
from mathutils import Vector
from blenderproc.python.types.MeshObjectUtility import MeshObject
[docs]
def upper_region(objects_to_sample_on: Union[MeshObject, List[MeshObject]],
face_sample_range: Optional[Union[Vector, np.ndarray, List[float]]] = None, min_height: float = 0.0,
max_height: float = 1.0, use_ray_trace_check: bool = False,
upper_dir: Optional[Union[Vector, np.ndarray, List[float]]] = None,
use_upper_dir: bool = True) -> np.ndarray:
"""
Uniformly samples 3-dimensional value over the bounding box of the specified objects (can be just a plane) in the
defined upper direction. If "use_upper_dir" is False, samples along the face normal closest to "upper_dir". The
sampling volume results in a parallelepiped. "min_height" and "max_height" define the sampling distance from
the face.
Example 1: Sample a location on the surface of the given objects with height above this
surface in range of [1.5, 1.8].
.. code-block:: python
UpperRegionSampler.sample(
objects_to_sample_on=objs,
min_height=1.5,
max_height=1.8
)
:param objects_to_sample_on: Objects, on which to sample on.
:param face_sample_range: Restricts the area on the face where objects are sampled. Specifically describes
relative lengths of both face vectors between which points are sampled.
Default: [0.0, 1.0]
:param min_height: Minimum distance to the bounding box that a point is sampled on.
:param max_height: Maximum distance to the bounding box that a point is sampled on.
:param use_ray_trace_check: Toggles using a ray casting towards the sampled object (if the object is directly
below the sampled position is the position accepted).
:param upper_dir: The 'up' direction of the sampling box. Default: [0.0, 0.0, 1.0].
:param use_upper_dir: Toggles using a ray casting towards the sampled object (if the object is directly
below the sampled position is the position accepted).
:return: Sampled value.
"""
if face_sample_range is None:
face_sample_range = [0.0, 1.0]
if upper_dir is None:
upper_dir = [0.0, 0.0, 1.0]
face_sample_range = np.array(face_sample_range)
upper_dir = np.array(upper_dir)
upper_dir /= np.linalg.norm(upper_dir)
if not isinstance(objects_to_sample_on, list):
objects_to_sample_on = [objects_to_sample_on]
if max_height < min_height:
raise RuntimeError(f"The minimum height ({min_height}) must be smaller than the maximum height ({max_height})!")
regions = []
def calc_vec_and_normals(face: List[np.ndarray]) -> Tuple[Tuple[np.ndarray, np.ndarray], np.ndarray]:
""" Calculates the two vectors, which lie in the plane of the face and the normal of the face.
:param face: Four corner coordinates of a face. Type: [4x[3xfloat]].
:return: (two vectors in the plane), and the normal.
"""
vec1 = face[1] - face[0]
vec2 = face[3] - face[0]
normal = np.cross(vec1, vec2)
normal /= np.linalg.norm(normal)
return (vec1, vec2), normal
# determine for each object in objects the region, where to sample on
for obj in objects_to_sample_on:
bb = obj.get_bound_box()
faces = []
faces.append([bb[0], bb[1], bb[2], bb[3]])
faces.append([bb[0], bb[4], bb[5], bb[1]])
faces.append([bb[1], bb[5], bb[6], bb[2]])
faces.append([bb[6], bb[7], bb[3], bb[2]])
faces.append([bb[3], bb[7], bb[4], bb[0]])
faces.append([bb[7], bb[6], bb[5], bb[4]])
# select the face, which has the smallest angle to the upper direction
min_diff_angle = 2 * math.pi
selected_face = None
for face in faces:
# calc the normal of all faces
_, normal = calc_vec_and_normals(face)
diff_angle = math.acos(normal.dot(upper_dir))
if diff_angle < min_diff_angle:
min_diff_angle = diff_angle
selected_face = face
# save the selected face values
if selected_face is not None:
vectors, normal = calc_vec_and_normals(selected_face)
base_point = selected_face[0]
regions.append(Region2D(vectors, normal, base_point))
else:
raise RuntimeError(f"Couldn't find a face, for this obj: {obj.get_name()}")
if regions and len(regions) == len(objects_to_sample_on):
selected_region_id = random.randint(0, len(regions) - 1)
selected_region, obj = regions[selected_region_id], objects_to_sample_on[selected_region_id]
if use_ray_trace_check:
inv_world_matrix = np.linalg.inv(obj.get_local2world_mat())
while True:
ret = selected_region.sample_point(face_sample_range)
dir_val = upper_dir if use_upper_dir else selected_region.normal()
ret += dir_val * random.uniform(min_height, max_height)
if use_ray_trace_check:
# transform the coords into the reference frame of the object
c_ret = inv_world_matrix @ np.concatenate((ret, [1]), 0)
c_dir = inv_world_matrix @ np.concatenate((dir_val * -1.0, [0]), 0)
# check if the object was hit
hit, _, _, _ = obj.ray_cast(c_ret[:3], c_dir[:3])
if hit: # if the object was hit return
break
else:
break
return np.array(ret)
raise RuntimeError("The amount of regions is either zero or does not match the amount of objects!")
[docs]
class Region2D:
""" Helper class for UpperRegionSampler: Defines a 2D region in 3D.
"""
def __init__(self, vectors: Tuple[np.ndarray, np.ndarray], normal: np.ndarray, base_point: np.ndarray):
self._vectors = vectors # the two vectors which lie in the selected face
self._normal = normal # the normal of the selected face
self._base_point = base_point # the base point of the selected face
[docs]
def sample_point(self, face_sample_range: np.ndarray) -> np.ndarray:
"""
Samples a point in the 2D Region
:param face_sample_range: relative lengths of both face vectors between which points are sampled
:return:
"""
ret = self._base_point.copy()
# walk over both vectors in the plane and determine a distance in both direction
for vec in self._vectors:
ret += vec * random.uniform(face_sample_range[0], face_sample_range[1])
return ret
[docs]
def normal(self):
"""
:return: the normal of the region
"""
return self._normal