Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow use of external bpy package from python environment #1158

Merged
merged 20 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7c23de4
chore(WriterUtility): use np.bytes_ instead of deprecated np.string_
Griperis Oct 16, 2024
4b3df7d
fix(MeshObjectUtility): Use path from bpy.utils.resource_path to infe…
Griperis Oct 29, 2024
7725323
feat(external_bpy): Support for using external bpy module
Griperis Nov 7, 2024
97addad
chore(init): Mention external bpy in wrong command RuntimeError
Griperis Nov 21, 2024
9722389
fix(init): Don't throw error if module is called in Python3.11 from l…
Griperis Dec 3, 2024
419ae8c
chore(external_bpy): Improved CLI output, raise exceptions where it m…
Griperis Dec 3, 2024
59f92e3
feat(external_bpy): Unified way of using temporary directory
Griperis Dec 3, 2024
933fee8
feat(external_bpy): Print traceback when user's script fails to import
Griperis Dec 3, 2024
fd9aadb
doc(external_bpy): Document usage of Blenderproc with bpy package
Griperis Dec 4, 2024
6ecb8f9
feat(external_bpy): Allow only quickstart when using 'external_bpy'
Griperis Dec 10, 2024
e030d23
chore(external_bpy): Unify the debug cli message error
Griperis Dec 10, 2024
cd33f33
feat(external_bpy): Setup temporary dir in init when in external mode
Griperis Dec 10, 2024
ba8cf99
refactor(external_bpy): Separate temp directory initializations
Griperis Dec 12, 2024
993b5de
refactor(external_bpy): exit early in run commands
Griperis Dec 13, 2024
01cd63b
feat(external_bpy): Added custom directory arg to init
Griperis Dec 16, 2024
6e06996
fix(tests): Fixes tests in external mode
cornerfarmer Dec 16, 2024
6dab608
chore(docu): Some fixes and error message improvement
cornerfarmer Dec 16, 2024
d64184a
fix(stereo): Fixes float64/32 conversion error
cornerfarmer Dec 16, 2024
6f1d64f
fix(urdf): Replaces urdfpy with urchin to support newer python versions
cornerfarmer Dec 17, 2024
e112c0d
Merge remote-tracking branch 'dlr/main' into blenderproc-without-run
cornerfarmer Dec 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions blenderproc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
import os
import sys
from .version import __version__
from .python.utility.SetupUtility import SetupUtility, is_using_external_bpy_module

# If "USE_EXTERNAL_BPY_MODULE" is set, we expect the bpy module is provided from the outside
if is_using_external_bpy_module():
try:
import bpy
if bpy.app.version[0] != 4 and bpy.app.version[1] != 2:
raise RuntimeError("\n###############\nUSE_EXTERNAL_BPY_MODULE is set, but bpy module is not from Blender 4.2.\n\tpip install bpy==4.2.0\n###############\n")

print(f"BlenderProc is using external 'bpy' ({bpy.app.version_string}) module found in the environment.")
# If we successfully imported bpy of correct version, we can signal that we are in the internal blender python environment
os.environ.setdefault("INSIDE_OF_THE_INTERNAL_BLENDER_PYTHON_ENVIRONMENT", "1")
except ImportError:
raise RuntimeError("\n###############\nUSE_EXTERNAL_BPY_MODULE is set, but bpy module could not be imported. Make sure bpy module is present in your python environment.\n\tpip install bpy==4.2.0\n###############\n")


# check the python version, only python 3.X is allowed:
if sys.version_info.major < 3:
Expand All @@ -19,8 +34,10 @@
# Also clean the python path as this might disturb the pip installs
if "PYTHONPATH" in os.environ:
del os.environ["PYTHONPATH"]
from .python.utility.SetupUtility import SetupUtility
SetupUtility.setup([])

if not is_using_external_bpy_module():
SetupUtility.setup([])

from .api import loader
from .api import utility
from .api import sampler
Expand All @@ -46,7 +63,7 @@
stack_summary = traceback.extract_stack()
file_names_of_stack = [os.path.basename(file_summary.filename) for file_summary in stack_summary]
# check if blenderproc is called via python3 -m blenderproc ...
is_module_call = file_names_of_stack[0] == "runpy.py"
is_module_call = file_names_of_stack[0] == "runpy.py" or file_names_of_stack[0] == "blenderproc-script.py"
if sys.platform == "win32":
is_bproc_shell_called = file_names_of_stack[2] in ["metadata.py", "__main__.py"]
is_command_line_script_called = file_names_of_stack[0] == "command_line.py"
Expand All @@ -57,10 +74,11 @@
# check if the name of this file is either blenderproc or if the
# "OUTSIDE_OF_THE_INTERNAL_BLENDER_PYTHON_ENVIRONMENT_BUT_IN_RUN_SCRIPT" is set, which is set in the cli.py
is_correct_startup_command = is_bproc_shell_called or is_module_call

if "OUTSIDE_OF_THE_INTERNAL_BLENDER_PYTHON_ENVIRONMENT_BUT_IN_RUN_SCRIPT" not in os.environ \
and not is_correct_startup_command:
and not is_correct_startup_command and not is_using_external_bpy_module():
# pylint: disable=consider-using-f-string
raise RuntimeError("\n###############\nThis script can only be run by \"blenderproc run\", instead of calling:"
"\n\tpython {}\ncall:\n\tblenderproc run {}\n###############".format(sys.argv[0],
sys.argv[0]))
"\n\tpython {}\ncall:\n\tblenderproc run {}\n\nor consider using 'USE_EXTERNAL_BPY_MODULE=1'"
"\n###############".format(sys.argv[0], sys.argv[0]))
# pylint: enable=consider-using-f-string
62 changes: 42 additions & 20 deletions blenderproc/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

import argparse
import os
import shutil
import signal
import sys
import shutil
import subprocess

repo_root_directory = os.path.join(os.path.dirname(os.path.dirname(__file__)))
sys.path.append(repo_root_directory)

# pylint: disable=wrong-import-position
from blenderproc.python.utility.SetupUtility import SetupUtility
from blenderproc.python.utility.SetupUtility import SetupUtility, is_using_external_bpy_module
from blenderproc.python.utility.InstallUtility import InstallUtility
# pylint: enable=wrong-import-position

Expand Down Expand Up @@ -129,13 +129,23 @@ def cli():
# pylint: enable=import-outside-toplevel
print(__version__)
elif args.mode in ["run", "debug", "quickstart"]:

# Install blender, if not already done
determine_result = InstallUtility.determine_blender_install_path(args)
custom_blender_path, blender_install_path = determine_result
blender_run_path, major_version = InstallUtility.make_sure_blender_is_installed(custom_blender_path,
blender_install_path,
args.reinstall_blender)
# BlenderProc has two modes based on the environment variable USE_EXTERNAL_BPY_MODULE. If the
# variable is set, we expect the bpy module and all relevant dependencies to be provided from the outside.
# If not set, the script will install blender, setup the environment and run the script inside Blender.

# Any run commands are not supported in this mode and have to be executed directly via python.
if is_using_external_bpy_module():
if args.mode == "run":
print("USE_EXTERNAL_BPY_MODULE is set, run the script directly through python:\n\n"
f"python {args.file}")
elif args.mode == "debug":
print("USE_EXTERNAL_BPY_MODULE is set, debug mode is not supported.")
elif args.mode == "quickstart":
path_src_run = os.path.join(repo_root_directory, "blenderproc", "scripts", "quickstart.py")
print(f"USE_EXTERNAL_BPY_MODULE is set, quickstart is not supported, instead run:\n\n"
f"python {os.path.join(path_src_run)}")

sys.exit(1)

# Setup script path that should be executed
if args.mode == "quickstart":
Expand All @@ -154,6 +164,11 @@ def cli():
# this is done to enable the import of blenderproc inside the blender internal python environment
used_environment["INSIDE_OF_THE_INTERNAL_BLENDER_PYTHON_ENVIRONMENT"] = "1"

# Install blender, if not already done
custom_blender_path, blender_install_path = InstallUtility.determine_blender_install_path(args)
blender_run_path, major_version = InstallUtility.make_sure_blender_is_installed(custom_blender_path,
blender_install_path,
args.reinstall_blender)
# If pip update is forced, remove pip package cache
if args.force_pip_update:
SetupUtility.clean_installed_packages_cache(os.path.dirname(blender_run_path), major_version)
Expand All @@ -162,19 +177,20 @@ def cli():
if args.mode == "debug":
# pylint: disable=consider-using-with
p = subprocess.Popen([blender_run_path, "--python-use-system-env", "--python-exit-code", "0", "--python",
os.path.join(repo_root_directory, "blenderproc/debug_startup.py"), "--",
path_src_run, temp_dir] + unknown_args,
env=used_environment)
os.path.join(repo_root_directory, "blenderproc/debug_startup.py"), "--",
path_src_run, temp_dir] + unknown_args,
env=used_environment)
# pylint: enable=consider-using-with
else:
# pylint: disable=consider-using-with
p = subprocess.Popen([blender_run_path, "--background", "--python-use-system-env", "--python-exit-code",
"2", "--python", path_src_run, "--", args.file, temp_dir] + unknown_args,
env=used_environment)
"2", "--python", path_src_run, "--", args.file, temp_dir] + unknown_args,
env=used_environment)
# pylint: enable=consider-using-with

def clean_temp_dir():
# If temp dir should not be kept and temp dir still exists => remove it
# If temp dir should not be kept and temp dir still exists => remove it,
# in external bpy mode this is handled in the `Initializer`.
if not args.keep_temp_dir and os.path.exists(temp_dir):
print("Cleaning temporary directory")
shutil.rmtree(temp_dir)
Expand Down Expand Up @@ -232,11 +248,17 @@ def handle_sigterm(_signum, _frame):
# Call the script
current_cli()
elif args.mode == "pip":
# Install blender, if not already done
custom_blender_path, blender_install_path = InstallUtility.determine_blender_install_path(args)
blender_bin, major_version = InstallUtility.make_sure_blender_is_installed(custom_blender_path,
blender_install_path)
blender_path = os.path.dirname(blender_bin)
if is_using_external_bpy_module():
# In external mode we can't determine the blender_install_path correctly. Populate with
# stub values, so the SetupUtility can print the user suggestion in one place.
blender_path = None
major_version = None
else:
# Install blender, if not already done
custom_blender_path, blender_install_path = InstallUtility.determine_blender_install_path(args)
blender_bin, major_version = InstallUtility.make_sure_blender_is_installed(custom_blender_path,
blender_install_path)
blender_path = os.path.dirname(blender_bin)

if args.pip_mode == "install":
SetupUtility.setup_pip(user_required_packages=args.pip_packages, blender_path=blender_path,
Expand Down
38 changes: 19 additions & 19 deletions blenderproc/python/loader/URDFLoader.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ def load_urdf(urdf_file: str, weight_distribution: str = 'rigid',
visualization in blender.
:return: URDF object instance.
"""
# install urdfpy
SetupUtility.setup_pip(user_required_packages=["git+https://github.com/wboerdijk/urdfpy.git"])
# This import is done inside to avoid having the requirement that BlenderProc depends on urdfpy
# install urchin
SetupUtility.setup_pip(user_required_packages=["urchin"])
# This import is done inside to avoid having the requirement that BlenderProc depends on urchin
# pylint: disable=import-outside-toplevel
from urdfpy import URDF
from urchin import URDF
# pylint: enable=import-outside-toplevel

if fk_offset is None:
Expand Down Expand Up @@ -94,22 +94,22 @@ def load_urdf(urdf_file: str, weight_distribution: str = 'rigid',
return urdf_object


def get_joints_which_have_link_as_parent(link_name: str, joint_trees: List["urdfpy.Joint"]) -> List["urdfpy.Joint"]:
def get_joints_which_have_link_as_parent(link_name: str, joint_trees: List["urchin.Joint"]) -> List["urchin.Joint"]:
""" Returns a list of joints which have a specific link as parent.

:param link_name: Name of the link.
:param joint_trees: List of urdfpy.Joint objects.
:return: List of urdfpy.Joint objects.
:param joint_trees: List of urchin.Joint objects.
:return: List of urchin.Joint objects.
"""
return [joint_tree for i, joint_tree in enumerate(joint_trees) if joint_tree.parent == link_name]


def get_joints_which_have_link_as_child(link_name: str, joint_trees: List["urdfpy.Joint"]) -> Optional["urdfpy.Joint"]:
def get_joints_which_have_link_as_child(link_name: str, joint_trees: List["urchin.Joint"]) -> Optional["urchin.Joint"]:
""" Returns the joint which is the parent of a specific link.

:param link_name: Name of the link.
:param joint_trees: List of urdfpy.Joint objects.
:return: List of urdfpy.Joint objects, or None if no joint is defined as parent for the respective link.
:param joint_trees: List of urchin.Joint objects.
:return: List of urchin.Joint objects, or None if no joint is defined as parent for the respective link.
"""
valid_joint_trees = [joint_tree for i, joint_tree in enumerate(joint_trees) if joint_tree.child == link_name]
if not valid_joint_trees: # happens for the very first link
Expand All @@ -121,7 +121,7 @@ def get_joints_which_have_link_as_child(link_name: str, joint_trees: List["urdfp
f"{link_name}")


def create_bone(armature: bpy.types.Armature, joint_tree: "urdfpy.Joint", all_joint_trees: List["urdfpy.Joint"],
def create_bone(armature: bpy.types.Armature, joint_tree: "urchin.Joint", all_joint_trees: List["urchin.Joint"],
parent_bone_name: Optional[str] = None, create_recursive: bool = True,
parent_origin: Optional[Matrix] = None,
fk_offset: Union[List[float], Vector, np.array] = None,
Expand Down Expand Up @@ -237,9 +237,9 @@ def create_bone(armature: bpy.types.Armature, joint_tree: "urdfpy.Joint", all_jo
parent_origin=origin, fk_offset=fk_offset, ik_offset=ik_offset)


def load_links(link_trees: List["urdfpy.Link"], joint_trees: List["urdfpy.Joint"], armature: bpy.types.Armature,
def load_links(link_trees: List["urchin.Link"], joint_trees: List["urchin.Joint"], armature: bpy.types.Armature,
urdf_path: str) -> List[Link]:
""" Loads links and their visual, collision and inertial objects from a list of urdfpy.Link objects.
""" Loads links and their visual, collision and inertial objects from a list of urchin.Link objects.

:param link_trees: List of urdf definitions for all links.
:param joint_trees: List of urdf definitions for all joints.
Expand Down Expand Up @@ -288,9 +288,9 @@ def load_links(link_trees: List["urdfpy.Link"], joint_trees: List["urdfpy.Joint"
return links


def propagate_pose(links: List[Link], joint_tree: "urdfpy.Joint", joint_trees: List["urdfpy.Joint"],
def propagate_pose(links: List[Link], joint_tree: "urchin.Joint", joint_trees: List["urchin.Joint"],
armature: bpy.types.Armature, recursive: bool = True):
""" Loads links and their visual, collision and inertial objects from a list of urdfpy.Link objects.
""" Loads links and their visual, collision and inertial objects from a list of urchin.Link objects.

:param links: List of links.
:param joint_tree: The urdf definition for the joint.
Expand Down Expand Up @@ -320,7 +320,7 @@ def propagate_pose(links: List[Link], joint_tree: "urdfpy.Joint", joint_trees: L
propagate_pose(links, child_joint_tree, joint_trees, armature, recursive=True)


def load_geometry(geometry_tree: "urdfpy.Geometry", urdf_path: Optional[str] = None) -> MeshObject:
def load_geometry(geometry_tree: "urchin.Geometry", urdf_path: Optional[str] = None) -> MeshObject:
""" Loads a geometric element from an urdf tree.

:param geometry_tree: The urdf representation of the geometric element.
Expand Down Expand Up @@ -355,7 +355,7 @@ def load_geometry(geometry_tree: "urdfpy.Geometry", urdf_path: Optional[str] = N
return obj


def load_visual_collision_obj(viscol_tree: Union["urdfpy.Visual", "urdfpy.Collision"], name: str,
def load_visual_collision_obj(viscol_tree: Union["urchin.Visual", "urchin.Collision"], name: str,
urdf_path: Optional[str] = None) -> MeshObject:
""" Loads a visual / collision element from an urdf tree.

Expand Down Expand Up @@ -423,7 +423,7 @@ def load_visual_collision_obj(viscol_tree: Union["urdfpy.Visual", "urdfpy.Collis
return obj


def load_inertial(inertial_tree: "urdfpy.Inertial", name: str) -> Inertial:
def load_inertial(inertial_tree: "urchin.Inertial", name: str) -> Inertial:
""" Loads an inertial element from an urdf tree.

:param inertial_tree: The urdf representation of the inertial element.
Expand All @@ -444,7 +444,7 @@ def load_inertial(inertial_tree: "urdfpy.Inertial", name: str) -> Inertial:
return inertial


def get_size_from_geometry(geometry: "urdfpy.Geometry") -> Optional[float]:
def get_size_from_geometry(geometry: "urchin.Geometry") -> Optional[float]:
""" Helper to derive the link size from the largest geometric element.

:param geometry: The urdf representation of the geometric element.
Expand Down
2 changes: 1 addition & 1 deletion blenderproc/python/postprocessing/StereoGlobalMatching.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def stereo_global_matching(left_color_image: np.ndarray, right_color_image: np.n
disparity = np.float32(np.copy(disparity_to_be_written)) / 16.0

# Triangulation
depth = (1.0 / disparity) * baseline * focal_length
depth = ((1.0 / disparity) * baseline * focal_length).astype(np.float32)

# Clip from depth map to 25 meters
depth[depth > depth_max] = depth_max
Expand Down
3 changes: 1 addition & 2 deletions blenderproc/python/types/MeshObjectUtility.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,7 @@ def add_auto_smooth_modifier(self, angle: float = 30.0) -> bpy.types.Modifier:
# Known issue: https://projects.blender.org/blender/blender/issues/117399

# The datafiles are expected to be in the same folder relative to blender's python binary.
python_bin = SetupUtility.determine_python_paths(None, None)[0]
path = Path(python_bin).parent.parent.parent / "datafiles" / "assets" / "geometry_nodes" / "smooth_by_angle.blend"
path = Path(bpy.utils.resource_path('LOCAL')) / "datafiles" / "assets" / "geometry_nodes" / "smooth_by_angle.blend"
if not path.exists():
raise RuntimeError(f"Could not find the path to the 'ESSENTIALS' asset folder expected at {path}")

Expand Down
2 changes: 1 addition & 1 deletion blenderproc/python/types/URDFUtility.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class URDFObject(Entity):
This class represents an URDF object, which is comprised of an armature and one or multiple links. Among others, it
serves as an interface for manipulation of the URDF model.
"""
def __init__(self, armature: bpy.types.Armature, links: List[Link], xml_tree: Optional["urdfpy.URDF"] = None):
def __init__(self, armature: bpy.types.Armature, links: List[Link], xml_tree: Optional["urchin.URDF"] = None):
super().__init__(bpy_object=armature)

object.__setattr__(self, "links", links)
Expand Down
Loading