Reputation: 4984
I'd like to calculate and draw the minimum bounding rectangle (MBR) of an object in the image of a camera (2D projection).
The following picture shows the MBR of the cube
object of the example provided below.
I took a screenshot of the camera image and drew the MBR (red rectangle) manually by eye.
How can I calculate the MBR (size and position in the camera image) programmatically and draw it in the canvas of the camera image (in real time)?
Edit: I added a MBR to my example based on this related solution, but I drew the MBR on a 2D overlay canvas. It works fine till you move the camera (using the mouse). Then the MBR is not correct (shifted and wrong size). Does the calculation not fit to my example or is there a bug in my implementation?
Note: I do not only want to draw the MBR but also find out the 2D coordinates and size on the camera image.
function computeScreenSpaceBoundingBox(mesh, camera) {
var vertices = mesh.geometry.vertices;
var vertex = new THREE.Vector3();
var min = new THREE.Vector3(1, 1, 1);
var max = new THREE.Vector3(-1, -1, -1);
for (var i = 0; i < vertices.length; i++) {
var vertexWorldCoord = vertex.copy(vertices[i]).applyMatrix4(mesh.matrixWorld);
var vertexScreenSpace = vertexWorldCoord.project(camera);
min.min(vertexScreenSpace);
max.max(vertexScreenSpace);
}
return new THREE.Box2(min, max);
}
function normalizedToPixels(coord, renderWidthPixels, renderHeightPixels) {
var halfScreen = new THREE.Vector2(renderWidthPixels/2, renderHeightPixels/2)
return coord.clone().multiply(halfScreen);
}
const scene = new THREE.Scene();
const light = new THREE.DirectionalLight( 0xffffff, 1 );
light.position.set( 1, 1, 0.5 ).normalize();
scene.add( light );
scene.add(new THREE.AmbientLight(0x505050));
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshLambertMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
const renderWidth = window.innerWidth;
const renderHeight = window.innerHeight;
const camera = new THREE.PerspectiveCamera( 75, renderWidth / renderHeight, 0.1, 1000 );
camera.position.z = 10;
const controls = new THREE.OrbitControls( camera );
controls.update();
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
const clock = new THREE.Clock();
const overlayCanvas = document.getElementById('overlay');
overlayCanvas.width = renderWidth;
overlayCanvas.height = renderHeight;
const overlayCtx = overlayCanvas.getContext('2d');
overlayCtx.lineWidth = 4;
overlayCtx.strokeStyle = 'red';
const animate = function () {
const time = clock.getElapsedTime() * 0.5;
requestAnimationFrame( animate );
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
cube.position.x = Math.sin(time) * 5
controls.update();
renderer.render( scene, camera );
const boundingBox2D = computeScreenSpaceBoundingBox(cube, camera);
// Convert normalized screen coordinates [-1, 1] to pixel coordinates:
const {x: w, y: h} = normalizedToPixels(boundingBox2D.getSize(), renderWidth, renderHeight);
const {x, y} =
normalizedToPixels(boundingBox2D.min, renderWidth, renderHeight)
.add(new THREE.Vector2(renderWidth / 2, renderHeight / 2));
overlayCtx.clearRect(0, 0, renderWidth, renderHeight);
overlayCtx.strokeRect(x, y, w, h);
};
animate();
body {
margin: 0px;
background-color: #000000;
overflow: hidden;
}
#overlay {
position: absolute;
}
<body>
<script src="https://unpkg.com/three@0.104.0/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.104.0/examples/js/Detector.js"></script>
<script src="https://unpkg.com/three@0.104.0/examples/js/controls/OrbitControls.js"></script>
<canvas id="overlay"></canvas>
</body>
Note: Please write a comment if my question is unclear so that I know what to describe in more detail.
Upvotes: 4
Views: 1972
Reputation: 4984
Thanks to WestLangley for pointing me to the related solution of Holger L and thanks to manthrax for fixing a bug in that solution that made me miss a bug of my own. In my example, I simply miss-transformed the y-coordinate from WebGL 2D screen coordinates (origin is in the center of the canvas and y-axis points up) to regular 2D canvas coordinates (origin is in the top left corner of the canvas and y-axis points down). I did not consider that the y-axis is reversed. Below is a fixed version of the example:
const computeScreenSpaceBoundingBox = (function () {
const vertex = new THREE.Vector3();
const min = new THREE.Vector3(1, 1, 1);
const max = new THREE.Vector3(-1, -1, -1);
return function computeScreenSpaceBoundingBox(box, mesh, camera) {
box.set(min, max);
const vertices = mesh.geometry.vertices;
const length = vertices.length;
for (let i = 0; i < length; i++) {
const vertexWorldCoord =
vertex.copy(vertices[i]).applyMatrix4(mesh.matrixWorld);
const vertexScreenSpace = vertexWorldCoord.project(camera);
box.min.min(vertexScreenSpace);
box.max.max(vertexScreenSpace);
}
}
})();
const renderWidth = window.innerWidth;
const renderHeight = window.innerHeight;
const renderWidthHalf = renderWidth / 2;
const renderHeightHalf = renderHeight / 2;
const scene = new THREE.Scene();
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 0.5).normalize();
scene.add(light);
scene.add(new THREE.AmbientLight(0x505050));
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial({color: 0x00ff00});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
const camera = new THREE.PerspectiveCamera(75, renderWidth / renderHeight, 0.1, 1000);
camera.position.z = 10;
const controls = new THREE.OrbitControls(camera);
controls.update();
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const clock = new THREE.Clock();
const overlayCanvas = document.getElementById('overlay');
overlayCanvas.width = renderWidth;
overlayCanvas.height = renderHeight;
const overlayCtx = overlayCanvas.getContext('2d');
overlayCtx.lineWidth = 4;
overlayCtx.strokeStyle = 'red';
const boundingBox2D = new THREE.Box2();
const animate = function () {
const time = clock.getElapsedTime() * 0.5;
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
cube.position.x = Math.sin(time) * 5;
controls.update();
renderer.render(scene, camera);
computeScreenSpaceBoundingBox(boundingBox2D, cube, camera);
// Convert normalized screen coordinates [-1, 1] to pixel coordinates:
const x = (boundingBox2D.min.x + 1) * renderWidthHalf;
const y = (1 - boundingBox2D.max.y) * renderHeightHalf;
const w = (boundingBox2D.max.x - boundingBox2D.min.x) * renderWidthHalf;
const h = (boundingBox2D.max.y - boundingBox2D.min.y) * renderHeightHalf;
overlayCtx.clearRect(0, 0, renderWidth, renderHeight);
overlayCtx.strokeRect(x, y, w, h);
};
animate();
body {
margin: 0px;
background-color: #000000;
overflow: hidden;
}
#overlay {
position: absolute;
}
<body>
<script src="https://unpkg.com/three@0.104.0/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.104.0/examples/js/Detector.js"></script>
<script src="https://unpkg.com/three@0.104.0/examples/js/controls/OrbitControls.js"></script>
<canvas id="overlay"></canvas>
</body>
Upvotes: 1