Determining the smallest bounding box of an object within a camera image using Three.js

Looking to analyze and outline the minimum bounding rectangle (MBR) of an object captured by a camera in a 2D projection. In the image below, you can see the MBR of a cube object. I manually sketched the MBR (red rectangle) based on visual estimation. Is there a way to programmatically calculate the MBR (dimension and position) and display it in real-time on the camera image canvas?

https://i.sstatic.net/0rJn8.jpg

Edit: I managed to add an MBR to my example using a similar solution. However, the MBR is accurately drawn on a 2D overlay canvas, but when I move the camera (via mouse), the MBR shifts and the size becomes incorrect. Is there a flaw in the calculation or is it a bug in my implementation?

Note: My goal is not just to visualize the MBR, but also to determine 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/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="2b5f43594e4e6b1b051a1b1f051b">[email protected]</a>/build/three.min.js"></script>
    <script src="https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="cfbba7bdaaaa8fffe1fefffbe1ff">[email protected]</a>/examples/js/Detector.js"></script>
    <script src="https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f2869a809797b2c2dcc3c2c6dcc2">[email protected]</a>/examples/js/controls/OrbitControls.js"></script>
    <canvas id="overlay"></canvas>
</body>

Note: Please provide feedback if my question requires further clarification for a more detailed explanation.

Answer №1

Gratitude to WestLangley for guiding me to the related solution provided by Holger L, and special thanks to manthrax for identifying and fixing a bug in the solution that helped me uncover a bug in my own code. In my scenario, the mistake was in incorrectly transforming the y-coordinate from WebGL 2D screen coordinates (with the origin at the center of the canvas and y-axis pointing up) to standard 2D canvas coordinates (with the origin at the top left corner of the canvas and y-axis pointing down). I failed to account for the y-axis reversal. Below, you can find the corrected 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/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="41352933242401716f7071756f71">[email protected]</a>/build/three.min.js"></script>
    <script src="https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9aeef2e8ffffdaaab4abaaaeb4aa">[email protected]</a>/examples/js/Detector.js"></script>
    <script src="https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="35415d47505075051b0405011b05">[email protected]</a>/examples/js/controls/OrbitControls.js"></script>
    <canvas id="overlay"></canvas>
</body>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the method for reaching a service in a different feature module?

Currently, I am utilizing Angular 2/4 and have organized my code into feature modules. For instance, I have a Building Module and a Client Module. https://i.stack.imgur.com/LvmkU.png The same structure applies to my Client Feature Module as well. Now, i ...

The efficiency of Testing Library findBy* queries is optimized when utilized alongside async/await functionality

After reviewing the documentation, it was noted that queries made using findBy return a Promise. Interestingly, utilizing these queries with Promise.prototype.catch() seems ineffective in comparison to pairing them with async/await + try...catch. An insta ...

Utilize React's Context Provider to centrally manage all state while incorporating async calls

I am currently exploring more refined methods to establish a provider/consumer setup in which an asynchronous call is initiated from the provider, but the consumer does not need to handle state management. Within my context provider, I am fetching data to ...

What is the best way to incorporate parallax scrolling in the center of a webpage?

I am trying to implement a parallax scrolling effect in the middle of my page, but it seems to be causing issues once I reach that section while scrolling. I attempted to use intersection observer, but unfortunately, it did not resolve the problem. const ...

Leveraging AJAX to send a form filled with dynamic content to multiple destinations

I have a hidden form that needs to be submitted to two different locations. The form is automatically filled using the .val method in jQuery. How can I write the ajax script to submit this form to two different locations? Is it possible to do this without ...

implement a jQuery loop to dynamically apply css styles

Attempting to utilize a jQuery loop to set a variable that will vary in each iteration through the loop. The plan is for this variable to be assigned to a css property. However, the issue arises where every css property containing the variable ends up with ...

The response from the Ajax call to the WCF is coming back as null

I am currently facing an issue where my ajax call to a function exposed by a WCF service is always returning 'undefined' in the success method, despite the fact that the function on the WCF side is returning the correct answer. I have debugged an ...

Emphasize the selected page number

My React application contains page numbers, but currently when a page number is clicked, it does not get highlighted or displayed in a different color. The className "text-success" can be added to make the text green. How can I dynamically add this class t ...

How to retrieve data from a nested object within a JSON array using JavaScript

When I use Logger.log(response.data.phone), the following data is displayed in my log: [{label=work, primary=true, value=5558675309}, {label=work, value=6108287680, primary=false}, {value=6105516373, label=work, primary=false}] My goal is to have the two ...

Is it always the case that modifying the props of a child component will trigger a re-render of the parent component, even

I am currently exploring ways to prevent a modal element from re-rendering when it is supposed to be hidden. The tutorial I am following suggests converting the component to a class-based one and using shouldComponentUpdate(). However, I wanted to test if ...

Troubleshooting jQuery's issue with dynamically adding input fields

I came across a tutorial (which I tweaked slightly) on this website: code In JSFiddle, everything works perfectly fine with the code. However, when I implemented it on my actual page, it's not functioning as expected. I've been trying to trouble ...

Error in Express application due to uncaught SyntaxError

I am attempting to live stream data using websockets to Chartjs, however I keep encountering the following error. main.js:1 Uncaught SyntaxError: Unexpected token <https://i.sstatic.net/9OCI1.png https://i.sstatic.net/bmGeW.gif What could be causi ...

Creating distinct short identifiers across various servers

Utilizing the shortid package for creating unique room IDs has proven effective when used on a single server. However, concerns arise regarding the uniqueness of IDs generated when utilized across multiple servers. Is there a method to ensure unique ID g ...

Utilize Google Drive and scripts to incorporate map images into a React application

I'm currently working on setting up an album feature on my react website for a friend, and I would like the images in the album to be linked to a Google Drive so that he can easily upload new images whenever he wants. After successfully inserting the ...

Why is the size of my array shrinking with every iteration of the for-loop in JavaScript?

I am struggling to change the classname of three elements that share the same classname. Unfortunately, as I loop through my array, it seems to decrease in size with each iteration, preventing me from successfully changing all three elements. Any advice or ...

An issue occurred when attempting to integrate Angular

Currently, I am in the process of learning AngularJS. To practice, I attempted a simple code snippet in an HTML file: <!DOCTYPE html> <html lang="en" ng-app=""> <head> <meta charset="utf-8"> &l ...

Obtain JSON information using an underscore template

As someone fairly new to using Backbone and Underscore, and web development in general, I am seeking guidance on how to retrieve individual model data on-click from a template format in order to populate a pop-up modal. Any advice or direction would be gre ...

The pre tag does not have any effect when added after the onload event

I have been experimenting with a jQuery plugin for drawing arrows, detailed in this article. When using the plugin, this code is transformed: <pre class="arrows-and-boxes"> (Src) > (Target) </pre> into this format: Src --> Target The ...

Toggle Div Visibility with Span Value Change

In my quiz, there is a span that displays the sum of checked checkboxes. Depending on the value in the span, I want to show or hide two different divs. Currently, my code hides the divs but does not show them. Please help! <div class="results center"&g ...

Transfer the values of specific keys in an array to a separate array based on the values of another key within the same object that satisfy a particular condition

I am seeking a refined solution to create a function that retrieves the names of legal drivers from a given array of people: function getNamesOfLegalDrivers(people) { } const driverArray = [ { name: 'John', age: 14 }, { name: 'Joey&apos ...