Help with fitting an object into camera

I’m making a randomised hand pose generator to train a keypoint ai on. Part of my script randomised the pose of the armature and another part randomises the rotation of the object.

I’m having difficulty with a bit that fits the hand into the camera frame. Fitting the bounding box makes it far too small at angles. Gemini has given me an iterative solver which mostly works but sometimes fails terribly. I’d rather write it myself but i’m stuck.

I figure i should make a 2d bounding box my going through the armature points to find max and min but then i need to resize and move the hand accounting for camera perspective…

import bpy
import bpy_extras
import random
import math
import json
import os

class HandFactory:
    def __init__(self, armature_name="Armature", camera_name="Camera"):
        self.armature = bpy.data.objects.get(armature_name)
        self.camera = bpy.data.objects.get(camera_name)
        self.scene = bpy.context.scene
        
        # Configuration
        self.res_x = 224
        self.res_y = 224
        self.output_dir = os.path.join(os.path.dirname(bpy.data.filepath), "output")
        
        if not os.path.exists(self.output_dir):
            os.makedirs(self.output_dir)

    #  POSE RANDOMIZATION 
    def randomize_pose(self):
        """Internal logic for finger flexion/abduction."""
        if not self.armature: return
        bones = self.armature.pose.bones

        def set_rot(name, x=0, y=0, z=0):
            if name in bones:
                bone = bones[name]
                bone.rotation_mode = 'XYZ'
                bone.rotation_euler = (math.radians(x), math.radians(y), math.radians(z))

        def get_flex(min_a, max_a):
            # Squaring random.random() favors 0.0 (straight fingers)
            return min_a + (max_a - min_a) * (random.random()**2)

        # Thumb
        set_rot("Thumb.0", get_flex(0, 35), 0, random.uniform(-20, 20))
        set_rot("Thumb.1", get_flex(0, 30))
        set_rot("Thumb.2", get_flex(0, 40))

        # Fingers
        fingers = ["Index", "Middle", "Ring", "Little"]
        last_spread = 15
        for f in fingers:
            # Spread (Abduction) - Prevents finger crossing
            spread = random.uniform(last_spread - 8, last_spread - 15)
            last_spread = spread
            
            # Flexion (Knuckle, PIP, DIP)
            kf = get_flex(10, -90)
            pf = get_flex(0, -100)
            set_rot(f"{f}.1", kf, 0, spread)
            set_rot(f"{f}.2", pf)
            set_rot(f"{f}.3", pf * 0.66)

    #  ORIENTATION RANDOMISATION
    def randomize_orientation(self):
        """Rotates the armature object globally."""
        self.armature.rotation_mode = 'XYZ'
        self.armature.rotation_euler = (
            math.radians(random.uniform(-90, 90)),
            math.radians(random.uniform(-90, 90)),
            math.radians(random.uniform(0, 360))
        )
        # Random jitter to prevent over-centering
        #self.armature.location = (random.uniform(-0.05, 0.05), 0, random.uniform(-0.05, 0.05))


   # KEEP HAND IN FRAME
    def fit_and_center_hand(self, target_coverage=0.70):
        """
        Iteratively scales and translates the armature to perfectly center 
        the 2D bounding box and hit the target coverage.
        Optimized to eliminate duplicate joint evaluations.
        """
        # 1. Reset location and scale before starting the fit
        self.armature.location = (0, 0, 0)
        self.armature.scale = (1.0, 1.0, 1.0)
        
        # 2. Extract camera's local axes
        cam_right = self.camera.matrix_world.col[0].xyz
        cam_up = self.camera.matrix_world.col[1].xyz
        
        # 3. Build a unique list of local joint points
        # Every bone's tail is included
        joint_points = [bone.tail for bone in self.armature.pose.bones]
        
        # Add the head of any bone that has no parent (the true roots, like the Wrist/Thumb base)
        for bone in self.armature.pose.bones:
            if not bone.parent:
                joint_points.append(bone.head)

        # 4. Proportional Control Loop
        for _ in range(100):
            bpy.context.view_layer.update() 
            
            min_x, max_x = float('inf'), float('-inf')
            min_y, max_y = float('inf'), float('-inf')
            
            # Evaluate each unique joint exactly once per iteration
            for pt in joint_points:
                p_world = self.armature.matrix_world @ pt
                p_2d = bpy_extras.object_utils.world_to_camera_view(self.scene, self.camera, p_world)
                
                min_x = min(min_x, p_2d.x)
                max_x = max(max_x, p_2d.x)
                min_y = min(min_y, p_2d.y)
                max_y = max(max_y, p_2d.y)
                
            width = max_x - min_x
            height = max_y - min_y
            max_dim = max(width, height)
            
            if max_dim < 0.001: 
                break 
                
            # --- APPLY SCALE ---
            scale_factor = target_coverage / max_dim
            self.armature.scale *= scale_factor
            
            # --- APPLY TRANSLATION ---
            center_x = (min_x + max_x) / 2.0
            center_y = (min_y + max_y) / 2.0
            
            err_x = 0.5 - center_x
            err_y = 0.5 - center_y
            
            dist_to_cam = (self.armature.location - self.camera.location).length
            
            shift_x = cam_right * (err_x * dist_to_cam * 0.5)
            shift_y = cam_up * (err_y * dist_to_cam * 0.5)
            
            self.armature.location += (shift_x + shift_y)
        

# TESTING
if __name__ == "__main__":
    factory = HandFactory()
    
    factory.armature.scale = (1.0, 1.0, 1.0)

    # TEST A: Just test the pose logic (run once, no render)
    #factory.randomize_pose()
    
    # TEST B: Test orientation
    factory.randomize_orientation()
    
    #factory.fit_hand_to_frame(target_coverage=0.85) # 85% of screen
    factory.fit_and_center_hand(target_coverage=0.90)