Source code for xiuminglib.blender.camera

from os import remove, rename
from os.path import dirname, basename
from time import time
import numpy as np

from .object import get_bmesh, raycast
from ..imprt import preset_import
from ..io import exr
from ..geometry.proj import from_homo

from ..log import get_logger
logger = get_logger()


[docs]def add_camera(xyz=(0, 0, 0), rot_vec_rad=(0, 0, 0), name=None, proj_model='PERSP', f=35, sensor_fit='HORIZONTAL', sensor_width=32, sensor_height=18, clip_start=0.1, clip_end=100): """Adds a camera to the current scene. Args: xyz (tuple, optional): Location. Defaults to ``(0, 0, 0)``. rot_vec_rad (tuple, optional): Rotations in radians around x, y and z. Defaults to ``(0, 0, 0)``. name (str, optional): Camera object name. proj_model (str, optional): Camera projection model. Must be ``'PERSP'``, ``'ORTHO'``, or ``'PANO'``. Defaults to ``'PERSP'``. f (float, optional): Focal length in mm. Defaults to 35. sensor_fit (str, optional): Sensor fit. Must be ``'HORIZONTAL'`` or ``'VERTICAL'``. See also :func:`get_camera_matrix`. Defaults to ``'HORIZONTAL'``. sensor_width (float, optional): Sensor width in mm. Defaults to 32. sensor_height (float, optional): Sensor height in mm. Defaults to 18. clip_start (float, optional): Near clipping distance. Defaults to 0.1. clip_end (float, optional): Far clipping distance. Defaults to 100. Returns: bpy_types.Object: Camera added. """ bpy = preset_import('bpy', assert_success=True) bpy.ops.object.camera_add() cam = bpy.context.active_object if name is not None: cam.name = name cam.location = xyz cam.rotation_euler = rot_vec_rad cam.data.type = proj_model cam.data.lens = f cam.data.sensor_fit = sensor_fit cam.data.sensor_width = sensor_width cam.data.sensor_height = sensor_height cam.data.clip_start = clip_start cam.data.clip_end = clip_end logger.info("Camera '%s' added", cam.name) return cam
[docs]def easyset(cam, xyz=None, rot_vec_rad=None, name=None, proj_model=None, f=None, sensor_fit=None, sensor_width=None, sensor_height=None): """Sets camera parameters more easily. See :func:`add_camera` for arguments. ``None`` will result in no change. """ if name is not None: cam.name = name if xyz is not None: cam.location = xyz if rot_vec_rad is not None: cam.rotation_euler = rot_vec_rad if proj_model is not None: cam.data.type = proj_model if f is not None: cam.data.lens = f if sensor_fit is not None: cam.data.sensor_fit = sensor_fit if sensor_width is not None: cam.data.sensor_width = sensor_width if sensor_height is not None: cam.data.sensor_height = sensor_height
[docs]def point_camera_to(cam, xyz_target, up=(0, 0, 1)): """Points camera to target. Args: cam (bpy_types.Object): Camera object. xyz_target (array_like): Target point in world coordinates. up (array_like, optional): World vector that, when projected, points up in the image plane. """ Vector = preset_import('Vector', assert_success=True) Quaternion = preset_import('Quaternion', assert_success=True) failed_ensuring_up_msg = \ "Camera '%s' pointed to %s, but with no guarantee on up vector" \ % (cam.name, tuple(xyz_target)) up = Vector(up) xyz_target = Vector(xyz_target) direction = xyz_target - cam.location # Rotate camera with quaternion so that `track` aligns with `direction`, and # world +z, when projected, aligns with camera +y (i.e., points up in image # plane) track = '-Z' rot_quat = direction.to_track_quat(track, 'Y') cam.rotation_euler = (0, 0, 0) cam.rotation_euler.rotate(rot_quat) # Further rotate camera so that world `up`, when projected, points up on # image plane. We know right now world +z, when projected, points up, so # we just need to rotate the camera around the lookat direction by an angle cam_mat, _, _ = get_camera_matrix(cam) up_proj = cam_mat * up.to_4d() orig_proj = cam_mat * Vector((0, 0, 0)).to_4d() try: up_proj = Vector((up_proj[0] / up_proj[2], up_proj[1] / up_proj[2])) - \ Vector((orig_proj[0] / orig_proj[2], orig_proj[1] / orig_proj[2])) except ZeroDivisionError: logger.error( ("w in homogeneous coordinates is 0; " "camera coincides with the point to project? " "So can't rotate camera to ensure up vector")) logger.info(failed_ensuring_up_msg) return cam if up_proj.length == 0: logger.error( ("Up vector projected to zero length; " "optical axis coincides with the up vector? " "So can't rotate camera to ensure up vector")) logger.info( "Camera '%s' pointed to %s, but with no guarantee on up vector", cam.name, tuple(xyz_target)) return cam # +-------> # | # | # v up_proj[1] = -up_proj[1] # ^ # | # | # +-------> a = Vector((0, 1)).angle_signed(up_proj) # clockwise is positive cam.rotation_euler.rotate(Quaternion(direction, a)) logger.info("Camera '%s' pointed to %s with world %s pointing up", cam.name, tuple(xyz_target), tuple(up)) return cam
[docs]def intrinsics_compatible_with_scene(cam, eps=1e-6): r"""Checks if camera intrinsic parameters are comptible with the current scene. Intrinsic parameters include sensor size and pixel aspect ratio, and scene parameters refer to render resolutions and their scale. The entire sensor is assumed active. Args: cam (bpy_types.Object): Camera object eps (float, optional): :math:`\epsilon` for numerical comparison. Considered equal if :math:`\frac{|a - b|}{b} < \epsilon`. Returns: bool: Check result. """ bpy = preset_import('bpy', assert_success=True) # Camera sensor_width_mm = cam.data.sensor_width sensor_height_mm = cam.data.sensor_height # Scene scene = bpy.context.scene w = scene.render.resolution_x h = scene.render.resolution_y scale = scene.render.resolution_percentage / 100. pixel_aspect_ratio = \ scene.render.pixel_aspect_x / scene.render.pixel_aspect_y # Do these parameters make sense together? mm_per_pix_horizontal = sensor_width_mm / (w * scale) mm_per_pix_vertical = sensor_height_mm / (h * scale) if abs(mm_per_pix_horizontal / mm_per_pix_vertical - pixel_aspect_ratio) \ / pixel_aspect_ratio < eps: logger.info("OK") return True logger.error(( "Render resolutions (w_pix = %d; h_pix = %d), active sensor size " "(w_mm = %f; h_mm = %f), and pixel aspect ratio (r = %f) don't make " "sense together. This could cause unexpected behaviors later. " "Consider running correct_sensor_height()" ), w, h, sensor_width_mm, sensor_height_mm, pixel_aspect_ratio) return False
[docs]def correct_sensor_height(cam): r"""To make render resolutions, sensor size, and pixel aspect ratio comptible. If render resolutions are :math:`(w_\text{pix}, h_\text{pix})`, sensor sizes are :math:`(w_\text{mm}, h_\text{mm})`, and pixel aspect ratio is :math:`r`, then :math:`h_\text{mm}\leftarrow\frac{h_\text{pix}}{w_\text{pix}r}w_\text{mm}`. Args: cam (bpy_types.Object): Camera. """ bpy = preset_import('bpy', assert_success=True) # Camera sensor_width_mm = cam.data.sensor_width # Scene scene = bpy.context.scene w = scene.render.resolution_x h = scene.render.resolution_y pixel_aspect_ratio = \ scene.render.pixel_aspect_x / scene.render.pixel_aspect_y # Change sensor height sensor_height_mm = sensor_width_mm * h / w / pixel_aspect_ratio cam.data.sensor_height = sensor_height_mm logger.info("Sensor height changed to %f", sensor_height_mm)
[docs]def get_camera_matrix(cam, keep_disparity=False): r"""Gets camera matrix, intrinsics, and extrinsics from a camera. You can ask for a 4-by-4 projection that projects :math:`(x, y, z, 1)` to :math:`(x, y, 1, d)`, where :math:`d` is the disparity, reciprocal of depth. ``cam_mat.dot(pts)`` gives you projections in the following convention: .. code-block:: none +------------> | proj[:, 0] | | v proj[:, 1] Args: cam (bpy_types.Object): Camera. keep_disparity (bool, optional): Whether or not the matrices keep disparity. Returns: tuple: - **cam_mat** (*mathutils.Matrix*) -- Camera matrix, product of intrinsics and extrinsics. 4-by-4 if ``keep_disparity``; else, 3-by-4. - **int_mat** (*mathutils.Matrix*) -- Camera intrinsics. 4-by-4 if ``keep_disparity``; else, 3-by-3. - **ext_mat** (*mathutils.Matrix*) -- Camera extrinsics. 4-by-4 if ``keep_disparity``; else, 3-by-4. """ bpy = preset_import('bpy', assert_success=True) Matrix = preset_import('Matrix', assert_success=True) bpy.context.view_layer.update() # Check if camera intrinsic parameters comptible with render settings if not intrinsics_compatible_with_scene(cam): raise ValueError( ("Render settings and camera intrinsic parameters mismatch. " "Such computed matrices will not make sense. Make them " "consistent first. See error message from " "intrinsics_compatible_with_scene() above for advice")) # Intrinsics f_mm = cam.data.lens sensor_width_mm = cam.data.sensor_width sensor_height_mm = cam.data.sensor_height scene = bpy.context.scene w = scene.render.resolution_x h = scene.render.resolution_y scale = scene.render.resolution_percentage / 100. pixel_aspect_ratio = \ scene.render.pixel_aspect_x / scene.render.pixel_aspect_y if cam.data.sensor_fit == 'VERTICAL': # h times pixel height must fit into sensor_height_mm # w / pixel_aspect_ratio times pixel width will then fit into # sensor_width_mm s_y = h * scale / sensor_height_mm s_x = w * scale / pixel_aspect_ratio / sensor_width_mm else: # 'HORIZONTAL' or 'AUTO' # w times pixel width must fit into sensor_width_mm # h * pixel_aspect_ratio times pixel height will then fit into # sensor_height_mm s_x = w * scale / sensor_width_mm s_y = h * scale * pixel_aspect_ratio / sensor_height_mm skew = 0 # only use rectangular pixels if keep_disparity: # 4-by-4 int_mat = Matrix(( (s_x * f_mm, skew, w * scale / 2, 0), (0, s_y * f_mm, h * scale / 2, 0), (0, 0, 1, 0), (0, 0, 0, 1))) else: # 3-by-3 int_mat = Matrix(( (s_x * f_mm, skew, w * scale / 2), (0, s_y * f_mm, h * scale / 2), (0, 0, 1))) # Extrinsics # Three coordinate systems involved: # 1. World coordinates "world" # 2. Blender camera coordinates "cam": # - x is horizontal # - y is up # - right-handed: negative z is look-at direction # 3. Desired computer vision camera coordinates "cv": # - x is horizontal # - y is down (to align to the actual pixel coordinates) # - right-handed: positive z is look-at direction rotmat_cam2cv = Matrix(( (1, 0, 0), (0, -1, 0), (0, 0, -1))) # matrix_world defines local-to-world transformation, i.e., # where is local (x, y, z) in world coordinate system? t, rot_euler = cam.matrix_world.decompose()[0:2] # World to Blender camera rotmat_world2cam = rot_euler.to_matrix().transposed() # same as inverse t_world2cam = rotmat_world2cam @ -t # World to computer vision camera rotmat_world2cv = rotmat_cam2cv @ rotmat_world2cam t_world2cv = rotmat_cam2cv @ t_world2cam if keep_disparity: # 4-by-4 ext_mat = Matrix(( rotmat_world2cv[0][:] + (t_world2cv[0],), rotmat_world2cv[1][:] + (t_world2cv[1],), rotmat_world2cv[2][:] + (t_world2cv[2],), (0, 0, 0, 1))) else: # 3-by-4 ext_mat = Matrix(( rotmat_world2cv[0][:] + (t_world2cv[0],), rotmat_world2cv[1][:] + (t_world2cv[1],), rotmat_world2cv[2][:] + (t_world2cv[2],))) # Camera matrix cam_mat = int_mat @ ext_mat logger.info("Done computing camera matrix for '%s'", cam.name) logger.warning("... using w = %d; h = %d", w * scale, h * scale) return cam_mat, int_mat, ext_mat
[docs]def get_camera_zbuffer(cam, save_to=None, hide=None): """Gets :math:`z`-buffer of the camera. Values are :math:`z` components in camera-centered coordinate system, where - :math:`x` is horizontal; - :math:`y` is down (to align with the actual pixel coordinates); - right-handed: positive :math:`z` is look-at direction and means "in front of camera." Origin is the camera center, not image plane (one focal length away from origin). Args: cam (bpy_types.Object): Camera. save_to (str, optional): Path to which the .exr :math:`z`-buffer will be saved. ``None`` means don't save. hide (str or list(str)): Names of objects to be hidden while rendering this camera's :math:`z`-buffer. Returns: numpy.ndarray: Camera :math:`z`-buffer. """ bpy = preset_import('bpy', assert_success=True) cv2 = preset_import('cv2', assert_success=True) # Validate and standardize error-prone inputs if hide is not None: if not isinstance(hide, list): # A single object hide = [hide] for element in hide: assert isinstance(element, str), \ ("`hide` should contain object names (i.e., strings), " "not objects themselves") if save_to is None: outpath = '/tmp/%s_zbuffer' % time() elif save_to.endswith('.exr'): outpath = save_to[:-4] # Duplicate scene to avoid touching the original scene bpy.ops.scene.new(type='LINK_OBJECTS') scene = bpy.context.scene scene.camera = cam scene.use_nodes = True node_tree = scene.node_tree nodes = node_tree.nodes # Remove all nodes for node in nodes: nodes.remove(node) # Set up nodes for z pass rlayers_node = nodes.new('CompositorNodeRLayers') output_node = nodes.new('CompositorNodeOutputFile') node_tree.links.new(rlayers_node.outputs[2], output_node.inputs[0]) output_node.format.file_format = 'OPEN_EXR' output_node.format.color_mode = 'RGB' output_node.format.color_depth = '32' # full float output_node.base_path = dirname(outpath) output_node.file_slots[0].path = basename(outpath) # Hide objects from z-buffer, if necessary if hide is not None: orig_hide_render = {} # for later restoration for obj in bpy.data.objects: if obj.type == 'MESH': orig_hide_render[obj.name] = obj.hide_render obj.hide_render = obj.name in hide # Render scene.cycles.samples = 1 scene.render.filepath = '/tmp/%s_rgb.png' % time() # to avoid overwritting bpy.ops.render.render(write_still=True) w = scene.render.resolution_x h = scene.render.resolution_y scale = scene.render.resolution_percentage / 100. # Delete this new scene bpy.ops.scene.delete() # Restore objects' original render hide states, if necessary if hide is not None: for obj in bpy.data.objects: if obj.type == 'MESH': obj.hide_render = orig_hide_render[obj.name] # Load z-buffer as array exr_path = outpath + '%04d' % scene.frame_current + '.exr' im = exr.read(exr_path) assert ( np.array_equal(im[:, :, 0], im[:, :, 1]) and np.array_equal( im[:, :, 0], im[:, :, 2])), ( "BGR channels of the z-buffer should be all the same, " "but they are not") zbuffer = im[:, :, 0] # Delete or move the .exr as user wants if save_to is None: # User doesn't want it -- delete remove(exr_path) else: # User wants it -- rename rename(exr_path, outpath + '.exr') logger.info("Got z-buffer of camera '%s'", cam.name) logger.warning("... using w = %d; h = %d", w * scale, h * scale) return zbuffer
[docs]def backproject_to_3d(xys, cam, obj_names=None, world_coords=False): """Backprojects 2D coordinates to 3D. Since a 2D point could have been projected from any point on a 3D line, this function will return the 3D point at which this line (ray) intersects with an object for the first time. Args: xys (array_like): XY coordinates of length 2 or shape N-by-2, in the following convention: .. code-block:: none (0, 0) +------------> (w, 0) | x | | | v y (0, h) cam (bpy_types.Object): Camera. obj_names (str or list(str), optional): Name(s) of object(s) of interest. ``None`` means considering all objects. world_coords (bool, optional): Whether to return world or the object's local coordinates. Returns: tuple: - **ray_tos** (*mathutils.Vector or list(mathutils.Vector)*) -- Location(s) at which each ray points in the world coordinates, regardless of ``world_coords``. This and the (shared) ray origin (``cam.location``) determine the rays. - **xyzs** (*mathutils.Vector or list(mathutils.Vector)*) -- Intersection coordinates specified in either the world or the object's local coordinates, depending on ``world_coords``. ``None`` means no intersection. - **intersect_objnames** (*str or list(str)*) -- Name(s) of object(s) responsible for intersections. ``None`` means no intersection. - **intersect_facei** (*int or list(int)*) -- Index/indices of the face(s), where the intersection happens. - **intersect_normals** (*mathutils.Vector or list(mathutils.Vector)*) -- Normal vector(s) at the intersection(s) specified in the same space as ``xyzs``. """ from tqdm import tqdm bpy = preset_import('bpy', assert_success=True) Vector = preset_import('Vector', assert_success=True) BVHTree = preset_import('BVHTree', assert_success=True) # Standardize inputs xys = np.array(xys).reshape(-1, 2) objs = bpy.data.objects if isinstance(obj_names, str): obj_names = [obj_names] elif obj_names is None: obj_names = [o.name for o in objs if o.type == 'MESH'] z_c = 1 # any depth in the camera space, so long as not infinity scene = bpy.context.scene w, h = scene.render.resolution_x, scene.render.resolution_y scale = scene.render.resolution_percentage / 100. # Get 4-by-4 invertible camera matrix cam_mat, _, _ = get_camera_matrix(cam, keep_disparity=True) cam_mat_inv = cam_mat.inverted() # pixel space to world # Precompute BVH trees and world-to-object transformations trees, world2objs = {}, {} for obj_name in obj_names: world2objs[obj_name] = objs[obj_name].matrix_world.inverted() obj = objs[obj_name] bm = get_bmesh(obj) trees[obj_name] = BVHTree.FromBMesh(bm) ray_tos = [None] * xys.shape[0] xyzs = [None] * xys.shape[0] intersect_objnames = [None] * xys.shape[0] intersect_facei = [None] * xys.shape[0] intersect_normals = [None] * xys.shape[0] ray_from_world = cam.location # TODO: vectorize for performance for i, xy in enumerate(tqdm(xys, desc="Computing ray internsections")): # Compute any point on the line passing camera center and # projecting to (x, y) xy1d = np.append(xy, [1, 1 / z_c]) # with disparity xyzw = cam_mat_inv @ Vector(xy1d) # world # Ray start and direction in world coordinates ray_to_world = from_homo(xyzw) ray_tos[i] = ray_to_world first_intersect = None first_intersect_objname = None first_intersect_facei = None first_intersect_normal = None dist_min = np.inf # Test intersections with each object of interest for obj_name, tree in trees.items(): obj2world = objs[obj_name].matrix_world world2obj = world2objs[obj_name] # Ray start and direction in local coordinates ray_from = world2obj @ ray_from_world ray_to = world2obj @ ray_to_world # Ray tracing loc, normal, facei, _ = raycast(tree, ray_from, ray_to) # Not using the returned ray distance as that's local dist = None if loc is None else ( obj2world @ loc - ray_from_world).length # See if this intersection is closer to camera center than # previous intersections with other objects if (dist is not None) and (dist < dist_min): first_intersect = obj2world @ loc if world_coords else loc first_intersect_objname = obj_name first_intersect_facei = facei # NOTE: local-to-world transformation of normals should not # apply translations, since normals are "anchor-less," unlike # locations first_intersect_normal = \ obj2world.to_3x3() @ normal if world_coords else normal first_intersect_normal.normalize() # Re-normalize in case transforming to world coordinates has # ruined the unit length dist_min = dist xyzs[i] = first_intersect intersect_objnames[i] = first_intersect_objname intersect_facei[i] = first_intersect_facei intersect_normals[i] = first_intersect_normal assert None not in ray_tos, ( "No matter whether a ray is a hit or not, we must have a " "\"look-at\" for it") logger.info("Backprojection done with camera '%s'", cam.name) logger.warning("... using w = %d; h = %d", w * scale, h * scale) ret = ( ray_tos, xyzs, intersect_objnames, intersect_facei, intersect_normals) if xys.shape[0] == 1: return tuple(x[0] for x in ret) return ret
[docs]def get_visible_vertices(cam, obj, ignore_occlusion=False, hide=None, method='raycast', perc_eps=1e-6): r"""Gets vertices that are visible (projected within frame *and* unoccluded) from camera. Args: cam (bpy_types.Object): Camera. obj (bpy_types.Object): Object of interest. ignore_occlusion (bool, optional): Whether to ignore all occlusion (including self-occlusion). Useful for finding out which vertices fall inside the camera view. hide (str or list(str), optional): Names of objects to be hidden while rendering this camera's :math:`z`-buffer. No effect if ``ignore_occlusion``. method (str, optional): Visibility test method: ``'raycast'`` or ``'zbuffer'``. Ray casting is more robust than comparing the vertex's depth against :math:`z`-buffer (inaccurate when the render resolution is low, or when object's own depth variation is small compared with its overall depth). The advantage of the :math:`z`-buffer, though, is its runtime independent of number of vertices. perc_eps (float, optional): Threshold for percentage difference between test value :math:`x` and true value :math:`y`. :math:`x` is considered equal to :math:`y` when :math:`\frac{|x - y|}{y}` is smaller. No effect if ``ignore_occlusion``. Returns: list: Indices of vertices that are visible. """ bpy = preset_import('bpy', assert_success=True) bmesh = preset_import('bmesh', assert_success=True) BVHTree = preset_import('BVHTree', assert_success=True) legal = ('zbuffer', 'raycast') assert method in legal, \ "Legal methods: %s, but found '%s'" % (legal, method) scene = bpy.context.scene w, h = scene.render.resolution_x, scene.render.resolution_y scale = scene.render.resolution_percentage / 100. # Get camera matrix cam_mat, _, ext_mat = get_camera_matrix(cam) # Get z-buffer if not ignore_occlusion: if method == 'zbuffer': zbuffer = get_camera_zbuffer(cam, hide=hide) else: zbuffer = None # Get mesh data from object bm = bmesh.new() bm.from_mesh(obj.data) if zbuffer is None: tree = BVHTree.FromBMesh(bm) world2obj = obj.matrix_world.inverted() ray_from = world2obj * cam.location # object's local coordinates def are_close(x, y): return (x - y) / y < perc_eps visible_vert_ind = [] # For each of its vertices # TODO: vectorize for speed for bv in bm.verts: # Check if its projection falls inside frame v_world = obj.matrix_world * bv.co # local to world xy = np.array(cam_mat * v_world) # project to 2D xy = xy[:-1] / xy[-1] if xy[0] >= 0 and xy[0] < w * scale and \ xy[1] >= 0 and xy[1] < h * scale: # Falls into the camera view if ignore_occlusion: # Considered visible already visible = True else: # Check occlusion if zbuffer is None: # ... by raycasting ray_to = bv.co # local coordinates ray_dist_no_occlu = (ray_to - ray_from).length _, _, _, ray_dist = raycast(tree, ray_from, ray_to) visible = ray_dist is not None and \ are_close(ray_dist_no_occlu, ray_dist) else: # ... by comparing against z-buffer v_cv = ext_mat * v_world # world to camera to CV z = v_cv[-1] z_min = zbuffer[int(xy[1]), int(xy[0])] visible = are_close(z, z_min) if visible: visible_vert_ind.append(bv.index) logger.info("Visibility test done with camera '%s'", cam.name) logger.warning("... using w = %d; h = %d", w * scale, h * scale) return visible_vert_ind
[docs]def get_2d_bounding_box(obj, cam): """Gets a 2D bounding box of the object in the camera frame. This is different from projecting the 3D bounding box to 2D. Args: obj (bpy_types.Object): Object of interest. cam (bpy_types.Object): Camera. Returns: numpy.ndarray: 2D coordinates of the bounding box corners. Of shape 4-by-2. Corners are ordered counterclockwise, following: .. code-block:: none (0, 0) +------------> (w, 0) | x | | | v y (0, h) """ bpy = preset_import('bpy', assert_success=True) scene = bpy.context.scene scale = scene.render.resolution_percentage / 100. w = scene.render.resolution_x * scale h = scene.render.resolution_y * scale # Get camera matrix cam_mat, _, _ = get_camera_matrix(cam) # Project all vertices to 2D pts = np.vstack([v.co.to_4d() for v in obj.data.vertices]).T # 4-by-N world_mat = np.array(obj.matrix_world) # 4-by-4 cam_mat = np.array(cam_mat) # 3-by-4 xyw = cam_mat.dot(world_mat.dot(pts)) # 3-by-N pts_2d = np.divide(xyw[:2, :], np.tile(xyw[2, :], (2, 1))) # 2-by-N # Compute bounding box x_min, y_min = np.min(pts_2d, axis=1) x_max, y_max = np.max(pts_2d, axis=1) corners = np.vstack(( np.array([x_min, y_min]), np.array([x_max, y_min]), np.array([x_max, y_max]), np.array([x_min, y_max]))) logger.info("Got 2D bounding box of '%s' in camera '%s'", obj.name, cam.name) logger.warning("... using w = %d; h = %d", w, h) if x_min < 0 or x_max >= w or y_min < 0 or y_max >= h: logger.warning("Part of the bounding box falls outside the frame") return corners