From 99408de04a60a21ad25d79e4a729e25fd959eae8 Mon Sep 17 00:00:00 2001 From: Yogesh Chawla Date: Wed, 22 Jul 2026 15:01:50 -0700 Subject: [PATCH] Add apple tree collision-aware build script and camera capture rig apple_tree.py builds apple trees with fruit-aware soft collision avoidance enabled and prints each tree's bounding-box extent. apple_tree_cameras.py reuses build_apple_tree() to render a same-distance, fixed-elevation-arc camera rig (above/level/below) per tree, intended as input views for Gaussian splatting. Co-Authored-By: Claude Sonnet 5 --- apple_tree.py | 146 ++++++++++++++++++++++++++++++++++++++++++ apple_tree_cameras.py | 76 ++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 apple_tree.py create mode 100644 apple_tree_cameras.py diff --git a/apple_tree.py b/apple_tree.py new file mode 100644 index 0000000..96343e9 --- /dev/null +++ b/apple_tree.py @@ -0,0 +1,146 @@ +import sys + +from pyhelios import Context, PlantArchitecture, Visualizer +from pyhelios.types import RGBcolor, vec3 + + +def build_apple_tree(plantarch, position=None, age_days=365.0, build_parameters=None): + """Load the apple library model and build one tree instance. + + Args: + plantarch: PlantArchitecture instance bound to a Context. + position: Base of the trunk (default: origin). + age_days: Plant age in days. Older = larger / more developed. + build_parameters: Optional training overrides, e.g. + {'trunk_height': 0.8, 'num_scaffolds': 4, 'scaffold_angle': 40} + + Returns: + plant_id for the created tree. + """ + if position is None: + position = vec3(0, 0, 0) + + if build_parameters and build_parameters.get("trunk_height", 0.8) > 0.8: + raise ValueError( + "The built-in apple model requires trunk_height <= 0.80 m " + "(20 nodes at 0.04 m per internode)." + ) + + plantarch.loadPlantModelFromLibrary("apple") + plant_id = plantarch.buildPlantInstanceFromLibrary( + base_position=position, + age=age_days, + build_parameters=build_parameters, + ) + return plant_id + + +def visualize_trees(context, plantarch, plant_ids): + """Open one interactive window showing all built trees together.""" + all_uuids = [] + for plant_id in plant_ids: + uuids = plantarch.getAllPlantUUIDs(plant_id) + if not uuids: + raise RuntimeError(f"Plant {plant_id} has no geometry to visualize") + all_uuids.extend(uuids) + + if not all_uuids: + raise RuntimeError("No tree geometry to visualize") + + # Fit the camera to the combined scene bounding box + x_bounds, y_bounds, z_bounds = context.getDomainBoundingBox(all_uuids) + center = vec3( + 0.5 * (x_bounds.x + x_bounds.y), + 0.5 * (y_bounds.x + y_bounds.y), + 0.5 * (z_bounds.x + z_bounds.y), + ) + extent = max( + x_bounds.y - x_bounds.x, + y_bounds.y - y_bounds.x, + z_bounds.y - z_bounds.x, + 1.0, + ) + + with Visualizer(width=1000, height=800) as visualizer: + visualizer.buildContextGeometry(context, uuids=all_uuids) + visualizer.setCameraPosition( + position=vec3( + center.x + 1.8 * extent, + center.y + 1.8 * extent, + center.z + 0.6 * extent, + ), + lookAt=center, + ) + visualizer.setBackgroundColor(RGBcolor(0.70, 0.85, 1.0)) + visualizer.setLightingModel("phong_shadowed") + visualizer.plotInteractive() + + +if __name__ == "__main__": + # Age is the main "size" control for library plants (days). + # Try ~90 for a young tree, ~365 for a year-old tree. + AGE_DAYS = [720.0] * 10 + + POSITIONS = [ + vec3(0, 0, 0), + vec3(1.5, 0, 0), + vec3(3, 0, 0), + vec3(9, 0, 0), + vec3(12, 0, 0), + vec3(15, 0, 0), + vec3(18, 0, 0), + vec3(21, 0, 0), + vec3(24, 0, 0), + vec3(27, 0, 0), + ] + + # Optional apple training-system overrides (omit to use defaults): + BUILD_PARAMS_LIST = [ + {"trunk_height": 0.72, "num_scaffolds": 4, "scaffold_angle": 33}, + {"trunk_height": 0.78, "num_scaffolds": 6, "scaffold_angle": 47}, + {"trunk_height": 0.79, "num_scaffolds": 5, "scaffold_angle": 39}, + {"trunk_height": 0.74, "num_scaffolds": 4, "scaffold_angle": 44}, + {"trunk_height": 0.70, "num_scaffolds": 6, "scaffold_angle": 31}, + {"trunk_height": 0.80, "num_scaffolds": 5, "scaffold_angle": 50}, + {"trunk_height": 0.76, "num_scaffolds": 4, "scaffold_angle": 36}, + {"trunk_height": 0.77, "num_scaffolds": 5, "scaffold_angle": 42}, + {"trunk_height": 0.71, "num_scaffolds": 6, "scaffold_angle": 35}, + {"trunk_height": 0.74, "num_scaffolds": 5, "scaffold_angle": 48}, + ] + + with Context() as context: + with PlantArchitecture(context) as plantarch: + plantarch.loadPlantModelFromLibrary("apple") + + # Include fruit in collision detection and steer growth away from + # overlaps (fruit is excluded by default) so apples on neighboring + # trees/branches don't grow into each other. + plantarch.setCollisionRelevantOrgans( + include_internodes=True, + include_leaves=True, + include_fruit=True, + ) + plantarch.enableSoftCollisionAvoidance(enable_fruit_collision=True) + + plant_ids = [] + + for i in range(3): + plant_id_i = build_apple_tree( + plantarch, + position=POSITIONS[i], + age_days=AGE_DAYS[i], + build_parameters=BUILD_PARAMS_LIST[i], + ) + plant_ids.append(plant_id_i) + + object_ids = plantarch.getAllPlantObjectIDs(plant_id_i) + uuids = plantarch.getAllPlantUUIDs(plant_id_i) + x_bounds, y_bounds, z_bounds = context.getDomainBoundingBox(uuids) + print(f"Apple tree plant_id={plant_id_i}") + print(f" objects: {len(object_ids)}") + print(f" primitives: {len(uuids)}") + print(f" age: {AGE_DAYS[i]} days") + print(f" extent: x={x_bounds.y - x_bounds.x:.3f}m, y={y_bounds.y - y_bounds.x:.3f}m, z={z_bounds.y - z_bounds.x:.3f}m") + + if "--no-visualization" not in sys.argv: + visualize_trees(context, plantarch, plant_ids) diff --git a/apple_tree_cameras.py b/apple_tree_cameras.py new file mode 100644 index 0000000..c07410c --- /dev/null +++ b/apple_tree_cameras.py @@ -0,0 +1,76 @@ +import os +import sys + +from apple_tree import build_apple_tree +from pyhelios import Context, PlantArchitecture, Visualizer +from pyhelios.types import RGBcolor, vec3 + +# Gaussian-splat capture rig: all 3 cameras sit at the same horizontal +# distance in front of the tree (same x, same y offset) and differ only in +# elevation (z) -- one above the tree looking down, one level with the +# tree's center, one low near the ground looking up. The same rig is +# reused for every tree, just re-centered on that tree's position/height. +CAMERA_DISTANCE = 2.5 # front offset from the tree, in meters, same for all rigs + +CAMERA_RIGS = [ + {"name": "above", "height_frac": 1.15}, + {"name": "level", "height_frac": 0.5}, + {"name": "below", "height_frac": 0.05}, +] + +OUTPUT_DIR = "renders" + + +def camera_position_for_tree(context, plantarch, plant_id, base_position, rig): + """Compute a camera position/lookAt in front of one tree for a given rig.""" + uuids = plantarch.getAllPlantUUIDs(plant_id) + x_bounds, y_bounds, z_bounds = context.getDomainBoundingBox(uuids) + tree_height = z_bounds.y - z_bounds.x + + look_at = vec3(base_position.x, base_position.y, z_bounds.x + 0.5 * tree_height) + position = vec3( + base_position.x, + base_position.y - CAMERA_DISTANCE, + z_bounds.x + rig["height_frac"] * tree_height, + ) + return position, look_at + + +if __name__ == "__main__": + AGE_DAYS = 365.0 + POSITIONS = [vec3(0, 0, 0), vec3(3, 0, 0), vec3(6, 0, 0)] + + os.makedirs(OUTPUT_DIR, exist_ok=True) + + with Context() as context: + with PlantArchitecture(context) as plantarch: + plant_ids = [] + for i, position in enumerate(POSITIONS): + plant_id = build_apple_tree(plantarch, position=position, age_days=AGE_DAYS) + plant_ids.append(plant_id) + print(f"Built apple tree plant_id={plant_id} at {position}") + + all_uuids = [] + for plant_id in plant_ids: + all_uuids.extend(plantarch.getAllPlantUUIDs(plant_id)) + + with Visualizer(width=1000, height=800, headless=True) as visualizer: + visualizer.buildContextGeometry(context, uuids=all_uuids) + visualizer.setBackgroundColor(RGBcolor(0.70, 0.85, 1.0)) + visualizer.setLightingModel("phong_shadowed") + + for plant_id, position in zip(plant_ids, POSITIONS): + for rig in CAMERA_RIGS: + camera_pos, look_at = camera_position_for_tree( + context, plantarch, plant_id, position, rig + ) + visualizer.setCameraPosition(position=camera_pos, lookAt=look_at) + visualizer.plotUpdate() + + filename = os.path.join( + OUTPUT_DIR, f"tree{plant_id}_{rig['name']}.png" + ) + visualizer.printWindow(filename) + print(f" Saved {filename}") + + print(f"\nDone. Images written to {OUTPUT_DIR}/")