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)
