diff --git a/core/opengate_core/opengate_lib/GateGenericSource.cpp b/core/opengate_core/opengate_lib/GateGenericSource.cpp index 51c9fc9b2..97e2e54a1 100644 --- a/core/opengate_core/opengate_lib/GateGenericSource.cpp +++ b/core/opengate_core/opengate_lib/GateGenericSource.cpp @@ -683,15 +683,7 @@ void GateGenericSource::InitializeVisualization(py::dict puser_info) { if (G4Threading::IsWorkerThread()) return; auto user_info = py::dict(puser_info["visualization"]); - py::object color = user_info["color"]; - if (py::isinstance(color)) { - std::string color_str = color.cast(); - G4Colour::GetColour(color_str, fVisColour); - } else { - std::vector rgba = color.cast>(); - fVisColour = G4Colour(rgba[0], rgba[1], rgba[2], rgba[3]); - } - + fVisColour = DictGetColour(user_info, "color"); fVisSize = DictGetDouble(user_info, "size"); fVisCount = DictGetInt(user_info, "count"); } diff --git a/core/opengate_core/opengate_lib/GateHelpersDict.cpp b/core/opengate_core/opengate_lib/GateHelpersDict.cpp index 561872edc..aca9591cd 100644 --- a/core/opengate_core/opengate_lib/GateHelpersDict.cpp +++ b/core/opengate_core/opengate_lib/GateHelpersDict.cpp @@ -180,6 +180,42 @@ DictGetVecG4RotationMatrix(py::dict &user_info, const std::string &key) { return l; } +namespace { +G4Colour GetColour(const py::handle &color) { + if (py::isinstance(color)) { + G4Colour colour; + const std::string color_str = color.cast(); + G4Colour::GetColour(color_str, colour); + return colour; + } + + const auto rgba = color.cast>(); + + if (rgba.size() == 3) + return {rgba[0], rgba[1], rgba[2], 1.0}; + return {rgba[0], rgba[1], rgba[2], rgba[3]}; +} +} // namespace + +G4Colour DictGetColour(py::dict &user_info, const std::string &key) { + return GetColour(user_info[key.c_str()]); +} + +std::vector DictGetVecColour(py::dict &user_info, + const std::string &key) { + std::vector l; + auto color_list = py::list(user_info[key.c_str()]); + for (const auto color : color_list) { + l.push_back(GetColour(color)); + } + return l; +} + +std::vector DictGetColourVec(py::dict &user_info, + const std::string &key) { + return DictGetVecColour(user_info, key); +} + bool IsIn(const std::string &s, std::vector &v) { for (const auto &x : v) if (x == s) @@ -318,4 +354,4 @@ std::string ParamAt(const std::map ¶m, Fatal("Cannot find this key in the param list"); } return param.at(key); -} \ No newline at end of file +} diff --git a/core/opengate_core/opengate_lib/GateHelpersDict.h b/core/opengate_core/opengate_lib/GateHelpersDict.h index d67177680..d99e0eba7 100644 --- a/core/opengate_core/opengate_lib/GateHelpersDict.h +++ b/core/opengate_core/opengate_lib/GateHelpersDict.h @@ -8,6 +8,7 @@ #ifndef OPENGATE_CORE_OPENGATEHELPERSDICT_H #define OPENGATE_CORE_OPENGATEHELPERSDICT_H +#include #include #include #include @@ -62,6 +63,11 @@ DictGetVecG4RotationMatrix(py::dict &user_info, const std::string &key); std::vector DictGetVecG4ThreeVector(py::dict &user_info, const std::string &key); +G4Colour DictGetColour(py::dict &user_info, const std::string &key); + +std::vector DictGetVecColour(py::dict &user_info, + const std::string &key); + bool IsIn(const std::string &s, std::vector &v); std::map DictToMap(const py::dict &user_info); diff --git a/docs/source/user_guide/user_guide_reference_volumes.rst b/docs/source/user_guide/user_guide_reference_volumes.rst index 331e2d560..bb8a294ce 100644 --- a/docs/source/user_guide/user_guide_reference_volumes.rst +++ b/docs/source/user_guide/user_guide_reference_volumes.rst @@ -33,7 +33,10 @@ Common parameters are: respect to the mother volume. We advocate the use of ``scipy.spatial.transform.Rotation`` to manage the rotation matrix. - ``color``: a list of 4 values (Red, Green, Blue, Opacity) between 0 - and 1, e.g. ``[1, 0, 0, 0.5]``. Only used when visualization is on. + and 1, e.g. ``[1, 0, 0, 0.5]`` or a string representing a Geant4 color name, + including ``"white"``, ``"grey"``, ``"gray"``, + ``"black"``, ``"brown"``, ``"red"``, ``"green"``, ``"blue"``, ``"cyan"``, + ``"magenta"``, or ``"yellow"``. Only used when visualization is on. - ``style``: forced visualization style for this volume. Can be ``'solid'``, or ``'wireframe'``. Otherwise or not set, the volume will be displayed according to global visualization style. Only available with qt. diff --git a/opengate/geometry/volumes.py b/opengate/geometry/volumes.py index 537f0a355..a271dddc3 100644 --- a/opengate/geometry/volumes.py +++ b/opengate/geometry/volumes.py @@ -12,7 +12,7 @@ from ..base import DynamicGateObject, process_cls from . import solids -from ..utility import ensure_filename_is_str +from ..utility import ensure_filename_is_str, validate_color from ..exception import fatal, warning from ..image import write_itk_image from ..image import update_image_py_to_cpp @@ -164,6 +164,7 @@ class VolumeBase(DynamicGateObject, NodeMixin): "doc": ( "4 component vector defining the volume's color in visual rendering. " "The first 3 entries are RBG, the 4th is visible/invisible (1 or 0). " + "Can also use Geant4 color name strings, e.g. 'red', 'blue', 'cyan' etc. " ) }, ), @@ -464,6 +465,7 @@ def construct_logical_volume(self): self.g4_vis_attributes.SetForceWireframe(True) elif self.style == "solid": self.g4_vis_attributes.SetForceSolid(True) + self.color = validate_color(self.color, f"Color error for volume {self.name}: ") self.g4_vis_attributes.SetColor(*self.color) self.g4_vis_attributes.SetVisibility(bool(self.color[3])) self.g4_logical_volume.SetVisAttributes(self.g4_vis_attributes) diff --git a/opengate/sources/generic.py b/opengate/sources/generic.py index b6ba43a68..c7be820c4 100644 --- a/opengate/sources/generic.py +++ b/opengate/sources/generic.py @@ -11,7 +11,7 @@ from ..base import UserInfoValidatorBase, process_cls from ..exception import fatal, warning from ..logger import logger -from ..utility import g4_units +from ..utility import g4_units, validate_color from .base import SourceBase from .utility import ( all_beta_plus_radionuclides, @@ -270,45 +270,6 @@ class VisualizationValidator(UserInfoValidatorBase): __schema__ = set(visualization_parameters().keys()) - def validate_color(self, color, prefix=""): - valid_color_str = [ - "white", - "grey", - "gray", - "black", - "brown", - "red", - "green", - "blue", - "cyan", - "magenta", - "yellow", - ] - - if isinstance(color, str) and not color in valid_color_str: - fatal( - f"{prefix}Invalid color name '{color}'. Valid color name options are: {valid_color_str}." - ) - if isinstance(color, list): - if len(color) > 4 or len(color) < 3: - fatal( - f"{prefix}Color list must have 3 (RGB) or 4 (RGBA) elements. Got {len(color)}." - ) - if len(color) == 3: - color.append(1.0) # Add alpha value of 1.0 if only RGB is provided - logger.debug( - f"{prefix}Alpha value of 1.0 is added to the color list since only RGB values are provided." - ) - for i, c in enumerate(color): - if not isinstance(c, (int, float, np.number)): - fatal( - f"{prefix}All elements of color list must be numbers. Element {i} is not." - ) - if c < 0 or c > 1: - fatal( - f"{prefix}All elements of color list must be in the range [0, 1]. Element {i} is {c}." - ) - def validate(self, parent_obj, attr_name: str, parent_context: str = None): context_name = super().validate(parent_obj, attr_name, parent_context) b = getattr(parent_obj, attr_name) @@ -326,7 +287,7 @@ def validate(self, parent_obj, attr_name: str, parent_context: str = None): f"For source {parent_obj.name}, visualization size must be in the range (0, 20). Got {b.size}. Using 3 instead." ) b.size = 3 - self.validate_color(b.color, f"For visualization of source {parent_obj.name}: ") + validate_color(b.color, f"For visualization of source {parent_obj.name}: ") return context_name diff --git a/opengate/tests/src/geometry/test009_voxels_visu.py b/opengate/tests/src/geometry/test009_voxels_visu.py index d2cf1365f..63b7b70df 100755 --- a/opengate/tests/src/geometry/test009_voxels_visu.py +++ b/opengate/tests/src/geometry/test009_voxels_visu.py @@ -49,7 +49,7 @@ patient.image = paths.data / "patient-4mm.mhd" patient.mother = "fake" patient.material = "G4_AIR" # material used by default - patient.color = [1, 0, 1, 1] + patient.color = "magenta" patient.voxel_materials = [ [-2000, -900, "G4_AIR"], [-900, -100, "Lung"], diff --git a/opengate/utility.py b/opengate/utility.py index afeb4dedc..18097f67d 100644 --- a/opengate/utility.py +++ b/opengate/utility.py @@ -608,3 +608,43 @@ def get_basename_and_extension(filename): extensions.append(ext) extensions.reverse() return os.path.basename(base), "".join(extensions) + + +def validate_color(color, error_prefix=""): + valid_color_dict = { + "white": [1, 1, 1, 1], + "grey": [0.5, 0.5, 0.5, 1], + "gray": [0.5, 0.5, 0.5, 1], + "black": [0, 0, 0, 1], + "brown": [0.45, 0.25, 0, 1], + "red": [1, 0, 0, 1], + "green": [0, 1, 0, 1], + "blue": [0, 0, 1, 1], + "cyan": [0, 1, 1, 1], + "magenta": [1, 0, 1, 1], + "yellow": [1, 1, 0, 1], + } + if isinstance(color, str) and not color in valid_color_dict.keys(): + fatal( + f"{error_prefix}Invalid color name '{color}'. Valid color name options are: {valid_color_dict.keys()}." + ) + if isinstance(color, str): + color = valid_color_dict[color] + return color + if isinstance(color, list): + if len(color) > 4 or len(color) < 3: + fatal( + f"{error_prefix}Color list must have 3 (RGB) or 4 (RGBA) elements. Got {len(color)}." + ) + for i, c in enumerate(color): + if not isinstance(c, (int, float, np.number)): + fatal( + f"{error_prefix}All elements of color list must be numbers. Element {i} is not." + ) + if c < 0 or c > 1: + fatal( + f"{error_prefix}All elements of color list must be in the range [0, 1]. Element {i} is {c}." + ) + if len(color) == 3: + color.append(1) + return color