diff --git a/docs/experimental/float_to_rgb.py b/docs/experimental/float_to_rgb.py new file mode 100644 index 000000000..90a322869 --- /dev/null +++ b/docs/experimental/float_to_rgb.py @@ -0,0 +1,71 @@ +import numpy as np +i = 255 + 255*256 + 255*256**2 + 255*256**3 +i = 0.000000001 +j = 0.000000001 +f = int(i*256*256*256*256 - 1) +g = int(j*256*256*256*256 - 1) +print("f:", f) + +def converter(n): + f = int(n*256*256*256*256 - 1) + c = np.zeros(4, dtype = float) + c[0] = f % 256 + c[1] = float((f % 256**2 - c[0]) // 256) + c[2] = float((f % 256**3 - c[1] - c[0]) // 256**2) + c[3] = float((f % 256**4 - c[2] - c[1] - c[0]) // 256**3) + + return c/255 + +def de_converter(h): + return (255*(h[0] + h[1]*256 + h[2]*256**2 + h[3]*256**3) + 1.0)/(256*256*256*256) + +c = converter(i) +d = converter(j) +# print(f, g) +# print(i) +# print(np.array(c)) +de = de_converter(c + d) +# print(int(de*256*256*256*256 - 1)) + + + +def gaussian_kernel(n, sigma = 1.0): + x0 = np.arange(0.0, 1.0, 1/n) + y0 = np.arange(0.0, 1.0, 1/n) + x, y = np.meshgrid(x0, y0) + center = np.array([x0[n // 2], y0[n // 2]]) + mesh = np.stack((x, y), 2) + center = np.repeat(center, x.shape[0]*y.shape[0]).reshape(x.shape[0], y.shape[0], 2) + kernel = np.exp((-1.0*np.linalg.norm(center - mesh, axis = 2)**2)/(2*sigma**2)) + string = f"const float gauss_kernel[{x.shape[0]*y.shape[0]}] = " + kernel = kernel/np.sum(kernel) + flat = str(kernel.flatten()).split(" ") + copy_flat = flat.copy() + taken = 0 + for i in range(len(flat)): + if flat[i] == ' ' or flat[i] == '': + copy_flat.pop(i - taken) + taken += 1 + if "[" in copy_flat[0]: + copy_flat[0] = copy_flat[0][1:] + else: + copy_flat.pop(0) + + if "]" in copy_flat[-1]: + copy_flat[-1] = copy_flat[-1][:-1] + else: + copy_flat.pop(-1) + + if '' == copy_flat[0]: + copy_flat.pop(0) + + if '' == copy_flat[-1]: + copy_flat.pop(-1) + + # copy_flat.pop(-1) + print(copy_flat) + + string += "{" + ", ".join(copy_flat) + "};" + return string + +print(gaussian_kernel(13, 3.0)) \ No newline at end of file diff --git a/docs/experimental/viz_kde_class.py b/docs/experimental/viz_kde_class.py new file mode 100644 index 000000000..bcff6f376 --- /dev/null +++ b/docs/experimental/viz_kde_class.py @@ -0,0 +1,68 @@ +import numpy as np + +from fury.actors.effect_manager import EffectManager +from fury.window import Scene, ShowManager, record + +def normalize(array : np.array, min : float = 0.0, max : float = 1.0, axis : int = 0): + """Convert an array to a given desired range. + + Parameters + ---------- + array : np.ndarray + Array to be normalized. + min : float + Bottom value of the interval of normalization. If no value is given, it is passed as 0.0. + max : float + Upper value of the interval of normalization. If no value is given, it is passed as 1.0. + + Returns + ------- + array : np.array + Array converted to the given desired range. + """ + if np.max(array) != np.min(array): + return ((array - np.min(array))/(np.max(array) - np.min(array)))*(max - min) + min + else: + raise ValueError( + "Can't normalize an array which maximum and minimum value are the same.") + + +width, height = (1200, 1000) + +scene = Scene() +scene.set_camera(position=(-24, 20, -40), + focal_point=(0.0, + 0.0, + 0.0), + view_up=(0.0, 0.0, 0.0)) + +manager = ShowManager( + scene, + "demo", + (width, + height)) + +manager.initialize() + + +n_points = 1000 +points = np.random.rand(n_points, 3) +points = normalize(points, -5, 5) +sigmas = normalize(np.random.rand(n_points, 1), 0.1, 0.3) +offset = np.array([0.0, 0.0, 0.0]) +points = points + np.tile(offset, points.shape[0]).reshape(points.shape) + +effects = EffectManager(manager) + +kde_actor = effects.kde(points, sigmas, kernel = "exponential", colormap = "inferno") + + +manager.scene.add(kde_actor) +# effects.remove_effect(kde_actor) + +interactive = True + +if interactive: + manager.start() + +record(scene, out_path = "kde_points.png", size = (800, 800)) diff --git a/fury/actors/effect_manager.py b/fury/actors/effect_manager.py new file mode 100644 index 000000000..bccf1e1af --- /dev/null +++ b/fury/actors/effect_manager.py @@ -0,0 +1,432 @@ +import os +import numpy as np +from fury.actor import Actor, billboard +from fury.colormap import create_colormap +from fury.io import load_image +from fury.lib import Texture, WindowToImageFilter +from fury.shaders import (attribute_to_actor, + compose_shader, + import_fury_shader, + shader_apply_effects, + shader_custom_uniforms) +from fury.ui import LineSlider2D, TextBlock2D, Panel2D +from fury.utils import rgb_to_vtk +from fury.window import (gl_disable_depth, + gl_set_additive_blending, + RenderWindow, + Scene, + ShowManager) + + +WRAP_MODE_DIC = {"clamptoedge" : Texture.ClampToEdge, + "repeat" : Texture.Repeat, + "mirroredrepeat" : Texture.MirroredRepeat, + "clamptoborder" : Texture.ClampToBorder} + +BLENDING_MODE_DIC = {"none" : 0, "replace" : 1, + "modulate" : 2, "add" : 3, + "addsigned" : 4, "interpolate" : 5, + "subtract" : 6} + +def window_to_texture( + window : RenderWindow, + texture_name : str, + target_actor : Actor, + blending_mode : str = "None", + wrap_mode : str = "ClampToBorder", + border_color : tuple = ( + 0.0, + 0.0, + 0.0, + 1.0), + interpolate : bool = True, + d_type : str = "rgb"): + """Capture a rendered window and pass it as a texture to the given actor. + + Parameters + ---------- + window : window.RenderWindow + Window to be captured. + texture_name : str + Name of the texture to be passed to the actor. + target_actor : Actor + Target actor to receive the texture. + blending_mode : str, optional + Texture blending mode. The options are: + 1. None + 2. Replace + 3. Modulate + 4. Add + 5. AddSigned + 6. Interpolate + 7. Subtract + wrap_mode : str, optional + Texture wrapping mode. The options are: + 1. ClampToEdge + 2. Repeat + 3. MirroredRepeat + 4. ClampToBorder + border_color : tuple (4, ), optional + Texture RGBA border color. + interpolate : bool, optional + Texture interpolation. + d_type : str, optional + Texture pixel type, "rgb" or "rgba". Default is "rgb" + """ + + windowToImageFilter = WindowToImageFilter() + windowToImageFilter.SetInput(window) + type_dic = {"rgb" : windowToImageFilter.SetInputBufferTypeToRGB, + "rgba" : windowToImageFilter.SetInputBufferTypeToRGBA, + "zbuffer" : windowToImageFilter.SetInputBufferTypeToZBuffer} + type_dic[d_type.lower()]() + windowToImageFilter.Update() + + texture = Texture() + texture.SetMipmap(True) + texture.SetInputConnection(windowToImageFilter.GetOutputPort()) + texture.SetBorderColor(*border_color) + texture.SetWrap(WRAP_MODE_DIC[wrap_mode.lower()]) + texture.SetInterpolate(interpolate) + texture.MipmapOn() + texture.SetBlendingMode(BLENDING_MODE_DIC[blending_mode.lower()]) + + target_actor.GetProperty().SetTexture(texture_name, texture) + +def texture_to_actor( + path_to_texture : str, + texture_name : str, + target_actor : Actor, + blending_mode : str = "None", + wrap_mode : str = "ClampToBorder", + border_color : tuple = ( + 0.0, + 0.0, + 0.0, + 1.0), + interpolate : bool = True): + """Pass an imported texture to an actor. + + Parameters + ---------- + path_to_texture : str + Texture image path. + texture_name : str + Name of the texture to be passed to the actor. + target_actor : Actor + Target actor to receive the texture. + blending_mode : str + Texture blending mode. The options are: + 1. None + 2. Replace + 3. Modulate + 4. Add + 5. AddSigned + 6. Interpolate + 7. Subtract + wrap_mode : str + Texture wrapping mode. The options are: + 1. ClampToEdge + 2. Repeat + 3. MirroredRepeat + 4. ClampToBorder + border_color : tuple (4, ) + Texture RGBA border color. + interpolate : bool + Texture interpolation.""" + + texture = Texture() + + textureArray = load_image(path_to_texture) + textureData = rgb_to_vtk(textureArray) + + texture.SetInputDataObject(textureData) + texture.SetBorderColor(*border_color) + texture.SetWrap(WRAP_MODE_DIC[wrap_mode.lower()]) + texture.SetInterpolate(interpolate) + texture.MipmapOn() + texture.SetBlendingMode(BLENDING_MODE_DIC[blending_mode.lower()]) + + target_actor.GetProperty().SetTexture(texture_name, texture) + +def colormap_to_texture( + colormap : np.array, + texture_name : str, + target_actor : Actor, + interpolate : bool = True): + """Convert a colormap to a texture and pass it to an actor. + + Parameters + ---------- + colormap : np.array (N, 4) or (1, N, 4) + RGBA color map array. The array can be two dimensional, although a three dimensional one is preferred. + texture_name : str + Name of the color map texture to be passed to the actor. + target_actor : Actor + Target actor to receive the color map texture. + interpolate : bool + Color map texture interpolation.""" + + if len(colormap.shape) == 2: + colormap = np.array([colormap]) + + texture = Texture() + + cmap = (255*colormap).astype(np.uint8) + cmap = rgb_to_vtk(cmap) + + texture.SetInputDataObject(cmap) + texture.SetWrap(Texture.ClampToEdge) + texture.SetInterpolate(interpolate) + texture.MipmapOn() + texture.SetBlendingMode(0) + + target_actor.GetProperty().SetTexture(texture_name, texture) + +class EffectManager(): + """Class that manages the application of post-processing effects on actors. + + Parameters + ---------- + manager : ShowManager + Target manager that will render post processed actors.""" + def __init__(self, manager : ShowManager): + self.scene = Scene() + cam_params = manager.scene.get_camera() + self.scene.set_camera(*cam_params) + self.on_manager = manager + self.off_manager = ShowManager(self.scene, + size=manager.size) + self.off_manager.window.SetOffScreenRendering(True) + self.off_manager.initialize() + self._n_active_effects = 0 + self._active_effects = {} + self._active_ui = {} + self._intensity = 1.0 + + def kde(self, + points : np.ndarray, + sigmas, + kernel : str = "gaussian", + opacity : float = 1.0, + colormap : str = "viridis", + custom_colormap : np.array = None): + """Actor that displays the Kernel Density Estimation of a given set of points. + + Parameters + ---------- + points : np.ndarray (N, 3) + Array of points to be displayed. + sigmas : np.ndarray (1, ) or (N, 1) + Array of sigmas to be used in the KDE calculations. Must be one or one for each point. + kernel : str, optional + Kernel to be used for the distribution calculation. The available options are: + * "cosine" + * "epanechnikov" + * "exponential" + * "gaussian" + * "linear" + * "tophat" + + opacity : float, optional + Opacity of the actor. + colormap : str, optional. + Colormap matplotlib name for the KDE rendering. Default is "viridis". + custom_colormap : np.ndarray (N, 4), optional + Custom colormap for the KDE rendering. Default is none which means no + custom colormap is desired. If passed, will overwrite matplotlib colormap + chosen in the previous parameter. + + Returns + ------- + textured_billboard : actor.Actor + KDE rendering actor.""" + if not isinstance(sigmas, np.ndarray): + sigmas = np.array(sigmas) + if sigmas.shape[0] != 1 and sigmas.shape[0] != points.shape[0]: + raise IndexError("sigmas size must be one or points size.") + if np.min(sigmas) <= 0: + raise ValueError("sigmas can't have zero or negative values.") + + varying_dec = """ + varying float out_sigma; + varying float out_scale; + """ + + kde_dec = import_fury_shader(os.path.join("utils", f"{kernel.lower()}_distribution.glsl")) + + kde_impl = """ + float current_kde = kde(normalizedVertexMCVSOutput*out_scale, out_sigma); + color = vec3(current_kde); + fragOutput0 = vec4(color, 1.0); + """ + + kde_vs_dec = """ + in float in_sigma; + varying float out_sigma; + + in float in_scale; + varying float out_scale; + """ + + + kde_vs_impl = """ + out_sigma = in_sigma; + out_scale = in_scale; + """ + + tex_dec = import_fury_shader(os.path.join("effects", "color_mapping.glsl")) + + tex_impl = """ + // Turning screen coordinates to texture coordinates + vec2 res_factor = vec2(res.y/res.x, 1.0); + vec2 renorm_tex = res_factor*normalizedVertexMCVSOutput.xy*0.5 + 0.5; + float intensity = texture(screenTexture, renorm_tex).r; + + if(intensity<=0.0){ + discard; + }else{ + vec4 final_color = color_mapping(intensity, colormapTexture); + fragOutput0 = vec4(final_color.rgb, u_opacity*final_color.a); + } + """ + + fs_dec = compose_shader([varying_dec, kde_dec]) + + """Scales parameter will be defined by the empirical rule: + 1*sima radius = 68.27% of data inside the curve + 2*sigma radius = 95.45% of data inside the curve + 3*sigma radius = 99.73% of data inside the curve""" + scales = 2*3.0*np.copy(sigmas) + + center_of_mass = np.average(points, axis = 0) + bill = billboard( + points, + (0.0, + 0.0, + 1.0), + scales=scales, + fs_dec=fs_dec, + fs_impl=kde_impl, + vs_dec=kde_vs_dec, + vs_impl=kde_vs_impl) + + # Blending and uniforms setup + window = self.off_manager.window + + shader_apply_effects(window, bill, gl_disable_depth) + shader_apply_effects(window, bill, gl_set_additive_blending) + attribute_to_actor(bill, self._intensity*np.repeat(sigmas, 4), "in_sigma") + attribute_to_actor(bill, np.repeat(scales, 4), "in_scale") + + if self._n_active_effects > 0: + self.off_manager.scene.GetActors().GetLastActor().SetVisibility(False) + self.off_manager.scene.add(bill) + + bill_bounds = bill.GetBounds() + max_sigma = 2*4.0*np.max(sigmas) + + actor_scales = np.array([[bill_bounds[1] - bill_bounds[0] + center_of_mass[0] + max_sigma, + bill_bounds[3] - bill_bounds[2] + center_of_mass[1] + max_sigma, + 0.0]]) + + scale = np.array([[actor_scales.max(), + actor_scales.max(), + 0.0]]) + + res = self.off_manager.size + + # Render to second billboard for color map post-processing. + textured_billboard = billboard(np.array([center_of_mass]), scales=scale, fs_dec=tex_dec, fs_impl=tex_impl) + shader_custom_uniforms(textured_billboard, "fragment").SetUniform2f("res", res) + shader_custom_uniforms(textured_billboard, "fragment").SetUniformf("u_opacity", opacity) + + # Disables the texture warnings + textured_billboard.GetProperty().GlobalWarningDisplayOff() + + if custom_colormap == None: + cmap = create_colormap(np.arange(0.0, 1.0, 1/256), colormap) + else: + cmap = custom_colormap + + colormap_to_texture(cmap, "colormapTexture", textured_billboard) + + def kde_callback(obj = None, event = None): + cam_params = self.on_manager.scene.get_camera() + self.off_manager.scene.set_camera(*cam_params) + self.off_manager.scene.Modified() + shader_apply_effects(window, bill, gl_disable_depth) + shader_apply_effects(window, bill, gl_set_additive_blending) + attribute_to_actor(bill, self._intensity*np.repeat(sigmas, 4), "in_sigma") + bill.Modified() + self.off_manager.render() + + window_to_texture( + self.off_manager.window, + "screenTexture", + textured_billboard, + blending_mode="Interpolate", + d_type = "rgba") + + # Initialization + kde_callback() + + minv = 1 + initv = 1000 + maxv = 2000 + offset = 25 + text_template = lambda slider: f'{(slider.value/initv):.2f} ({slider.ratio:.0%})' + line_slider = LineSlider2D(center = (res[0] - offset, 0 + (res[1]/res[0])*offset), + initial_value = initv, + min_value = minv, max_value = maxv, + text_alignment='bottom', + orientation = 'horizontal', + text_template = text_template) + + text_block = TextBlock2D("Intensity") + panel_size = (line_slider.size[0] + text_block.size[0], 2*line_slider.size[1] + text_block.size[1]) + panel = Panel2D(size = (line_slider.size[0] + text_block.size[0], 2*line_slider.size[1] + text_block.size[1]), + position = (res[0] - panel_size[0] - offset, 0 + panel_size[1] + offset), + color = (1, 1, 1), opacity = 0.1, align = 'right') + panel.add_element(line_slider, (0.38, 0.5)) + panel.add_element(text_block, (0.1, 0.5)) + + def intensity_change(slider): + self._intensity = slider.value/initv + kde_callback() + + line_slider.on_moving_slider = intensity_change + + self.on_manager.scene.add(panel) + + callback_id = self.on_manager.add_iren_callback(kde_callback, "RenderEvent") + + self._active_effects[textured_billboard] = (callback_id, bill) + self._active_ui[textured_billboard] = panel.actors + self._n_active_effects += 1 + + return textured_billboard + + def remove_effect(self, effect_actor): + """Remove an existing effect from the effects manager. + Beware that the effect and the actor will be removed from the rendering pipeline + and shall not work after this action. + + Parameters + ---------- + effect_actor : actor.Actor + Actor of effect to be removed. + """ + if self._n_active_effects > 0: + self.on_manager.iren.RemoveObserver(self._active_effects[effect_actor][0]) + self.off_manager.scene.RemoveActor(self._active_effects[effect_actor][1]) + self.on_manager.scene.RemoveActor(effect_actor) + ui_actors = self._active_ui[effect_actor] + for i in range(len(ui_actors)): + self.on_manager.scene.RemoveActor(ui_actors[i]) + self._active_effects.pop(effect_actor) + self._n_active_effects -= 1 + else: + raise IndexError("Manager has no active effects.") + + \ No newline at end of file diff --git a/fury/shaders/__init__.py b/fury/shaders/__init__.py index 8a8f11764..37cb94110 100644 --- a/fury/shaders/__init__.py +++ b/fury/shaders/__init__.py @@ -8,6 +8,7 @@ replace_shader_in_actor, shader_apply_effects, shader_to_actor, + shader_custom_uniforms ) __all__ = [ diff --git a/fury/shaders/base.py b/fury/shaders/base.py index 6caec9f5c..6d1c825b1 100644 --- a/fury/shaders/base.py +++ b/fury/shaders/base.py @@ -412,3 +412,24 @@ def attribute_to_actor(actor, arr, attr_name, deep=True): mapper.MapDataArrayToVertexAttribute( attr_name, attr_name, DataObject.FIELD_ASSOCIATION_POINTS, -1 ) + +def shader_custom_uniforms(actor, shader_type): + """Eases the passing of uniform values to the shaders by returning ``actor.GetShaderProperty().GetVertexCustomUniforms()``, + that give access to the ``SetUniform`` methods. + Parameters + ---------- + actor : actor.Actor + Actor which the uniform values will be passed to. + shader_type : str + Shader type of the uniform values to be passed. It can be: + * "vertex" + * "fragment" + * "geometry" + """ + SHADER_FUNCTIONS = {"vertex" : actor.GetShaderProperty().GetVertexCustomUniforms(), + "fragment" : actor.GetShaderProperty().GetFragmentCustomUniforms(), + "geometry" : actor.GetShaderProperty().GetGeometryCustomUniforms()} + + + + return SHADER_FUNCTIONS[shader_type] diff --git a/fury/shaders/effects/color_mapping.glsl b/fury/shaders/effects/color_mapping.glsl new file mode 100644 index 000000000..be9334ec6 --- /dev/null +++ b/fury/shaders/effects/color_mapping.glsl @@ -0,0 +1,3 @@ +vec4 color_mapping(float intensity, sampler2D colormapTexture){ + return texture(colormapTexture, vec2(intensity,0)); +} \ No newline at end of file diff --git a/fury/shaders/utils/cosine_distribution.glsl b/fury/shaders/utils/cosine_distribution.glsl new file mode 100644 index 000000000..85c6b4558 --- /dev/null +++ b/fury/shaders/utils/cosine_distribution.glsl @@ -0,0 +1,12 @@ +// This assumes the center of the normal distribution is the center of the screen +#define PI 3.1415926 +float kde(vec3 point, float sigma){ + float norm = (PI/(4.0*sigma)); + return norm*cos(PI*length(point)/(2*sigma))*int(length(point) < sigma); +} + + +// This requires a center to be passed +// float kde(vec3 point, vec3 center, float sigma){ +// return cos(PI*length(center - point)/(2*sigma))*int(length(center - point) < sigma); +// } \ No newline at end of file diff --git a/fury/shaders/utils/epanechnikov_distribution.glsl b/fury/shaders/utils/epanechnikov_distribution.glsl new file mode 100644 index 000000000..17feb52ef --- /dev/null +++ b/fury/shaders/utils/epanechnikov_distribution.glsl @@ -0,0 +1,11 @@ +// This assumes the center of the normal distribution is the center of the screen +float kde(vec3 point, float sigma){ + float norm = (3.0/(4.0*sigma)); + return norm*(1.0 - (length(point)*length(point))/(sigma*sigma)); +} + + +// This requires a center to be passed +// float kde(vec3 point, vec3 center, float sigma){ +// return 1.0 - (length(center - point)*length(center - point))/(sigma*sigma); +// } \ No newline at end of file diff --git a/fury/shaders/utils/exponential_distribution.glsl b/fury/shaders/utils/exponential_distribution.glsl new file mode 100644 index 000000000..417305f69 --- /dev/null +++ b/fury/shaders/utils/exponential_distribution.glsl @@ -0,0 +1,11 @@ +// This assumes the center of the normal distribution is the center of the screen +#define E 2.7182818 +float kde(vec3 point, float sigma){ + return exp(-1.0*length(point)/sigma); +} + + +// This requires a center to be passed +// float kde(vec3 point, vec3 center, float sigma){ +// return exp(-1.0*length(center - point)/sigma); +// } \ No newline at end of file diff --git a/fury/shaders/utils/gaussian_distribution.glsl b/fury/shaders/utils/gaussian_distribution.glsl new file mode 100644 index 000000000..7d1087986 --- /dev/null +++ b/fury/shaders/utils/gaussian_distribution.glsl @@ -0,0 +1,12 @@ +// This assumes the center of the normal distribution is the center of the screen +#define PI 3.1415926 +float kde(vec3 point, float sigma){ + float norm = (1/(sigma*sqrt(2.0*PI))); + return norm*exp(-1.0*pow(length(point), 2.0)/(2.0*sigma*sigma) ); +} + + +// This requires a center to be passed +// float kde(vec3 point, vec3 center, float sigma){ +// return (1/(sigma*sqrt(2.0*PI)))*exp(-1.0*pow(length(center - point), 2.0)/(2.0*sigma*sigma) ); +// } \ No newline at end of file diff --git a/fury/shaders/utils/linear_distribution.glsl b/fury/shaders/utils/linear_distribution.glsl new file mode 100644 index 000000000..4d7bfa874 --- /dev/null +++ b/fury/shaders/utils/linear_distribution.glsl @@ -0,0 +1,11 @@ +// This assumes the center of the normal distribution is the center of the screen +float kde(vec3 point, float sigma){ + float norm = (1.0/sigma); + return norm*(1.0 - length(point)/sigma)*int(length(point) < sigma); +} + + +// This requires a center to be passed +// float kde(vec3 point, vec3 center, float sigma){ +// return (1.0 - length(center - point)/sigma)*int(length(center - point) < sigma); +// } \ No newline at end of file diff --git a/fury/shaders/utils/tophat_distribution.glsl b/fury/shaders/utils/tophat_distribution.glsl new file mode 100644 index 000000000..972c1f3db --- /dev/null +++ b/fury/shaders/utils/tophat_distribution.glsl @@ -0,0 +1,11 @@ +// This assumes the center of the normal distribution is the center of the screen +float kde(vec3 point, float sigma){ + float norm = (1.0/sigma*2.0); + return norm*int(length(point) < sigma); +} + + +// This requires a center to be passed +// float kde(vec3 point, vec3 center, float sigma){ +// return 1.0*int(length(center - point) < sigma); +// } \ No newline at end of file diff --git a/fury/window.py b/fury/window.py index 649a3bf07..b583bb690 100644 --- a/fury/window.py +++ b/fury/window.py @@ -740,8 +740,9 @@ def play_events_from_file(self, filename): def add_window_callback(self, win_callback, event=Command.ModifiedEvent): """Add window callbacks.""" - self.window.AddObserver(event, win_callback) + window_id = self.window.AddObserver(event, win_callback) self.window.Render() + return window_id def add_timer_callback(self, repeat, duration, timer_callback): if not self.iren.GetInitialized(): @@ -758,7 +759,8 @@ def add_timer_callback(self, repeat, duration, timer_callback): def add_iren_callback(self, iren_callback, event='MouseMoveEvent'): if not self.iren.GetInitialized(): self.initialize() - self.iren.AddObserver(event, iren_callback) + iren_id = self.iren.AddObserver(event, iren_callback) + return iren_id def destroy_timer(self, timer_id): self.iren.DestroyTimer(timer_id)