diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..a57e0b729 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# Set update schedule for GitHub Actions + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" + groups: + actions: + patterns: + - "*" diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 5768d7c63..6f32efeaf 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -14,6 +14,6 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Codespell - uses: codespell-project/actions-codespell@v1 + uses: codespell-project/actions-codespell@v2 diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index b016e18c3..ae8a4c1fe 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -24,11 +24,11 @@ jobs: matrix: python-version: [3.9] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies @@ -62,5 +62,5 @@ jobs: ssh-key: ${{ secrets.ACTIONS_DEPLOY_KEY }} repository-name: fury-gl/fury-website folder: ./docs/build/html-web-only - target-folder: v0.9.x + target-folder: v0.10.x clean: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0648dfa9f..670b22db6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,10 +54,10 @@ jobs: PRE_WHEELS: "https://pypi.anaconda.org/scipy-wheels-nightly/simple" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/cache@v2 + - uses: actions/cache@v4 if: startsWith(runner.os, 'Linux') with: path: | @@ -66,14 +66,14 @@ jobs: key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - - uses: actions/cache@v2 + - uses: actions/cache@v4 if: startsWith(runner.os, 'macOS') with: path: ~/Library/Caches/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - - uses: actions/cache@v2 + - uses: actions/cache@v4 if: startsWith(runner.os, 'Windows') with: path: ~/.pip @@ -81,7 +81,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Set up Virtualenv @@ -90,7 +90,7 @@ jobs: python -m pip install --upgrade pip virtualenv virtualenv $VENV_ARGS venv - name: Setup Miniconda - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 if: ${{ matrix.install-type == 'conda' }} with: auto-update-conda: true @@ -113,7 +113,7 @@ jobs: ci/run_tests.sh fi - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 if: ${{ matrix.coverage }} with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.mailmap b/.mailmap index c4f518731..e41e11bab 100644 --- a/.mailmap +++ b/.mailmap @@ -61,3 +61,5 @@ Sreekar Chigurupati sreekar chigurupati lej0hn Francois Rheault frheault Dwij Raj Hari Dwij Raj Hari <75260253+dwijrajhari@users.noreply.github.com> +Tania Castillo tvcastillod +Tania Castillo Tania Castillo <31288525+tvcastillod@users.noreply.github.com> diff --git a/LICENSE b/LICENSE index 6fda7b6f7..d67e68ac3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023, FURY - Free Unified Rendering in Python. All rights reserved. +Copyright (c) 2024, FURY - Free Unified Rendering in Python. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/docs/examples/_valid_examples.toml b/docs/examples/_valid_examples.toml index 0efd032ba..e5ecfb142 100644 --- a/docs/examples/_valid_examples.toml +++ b/docs/examples/_valid_examples.toml @@ -88,7 +88,7 @@ readme = "file: _animation.rst" position = 10 enable = true files = [ - "viz_interpolators.py", + # "viz_interpolators.py", "viz_bezier_interpolator.py", "viz_introduction.py", "viz_camera.py", diff --git a/docs/examples/viz_card_sprite_sheet.py b/docs/examples/viz_card_sprite_sheet.py index b05620421..a0840c811 100644 --- a/docs/examples/viz_card_sprite_sheet.py +++ b/docs/examples/viz_card_sprite_sheet.py @@ -10,10 +10,11 @@ First, some imports. """ import os +from tempfile import TemporaryDirectory as InTemporaryDirectory + from fury import ui, window from fury.data import fetch_viz_icons from fury.io import load_image, load_sprite_sheet, save_image -from tempfile import TemporaryDirectory as InTemporaryDirectory ############################################################################## # First we need to fetch some icons that are included in FURY. @@ -23,7 +24,9 @@ fetch_viz_icons() -sprite_sheet = load_sprite_sheet('https://i.imgur.com/0yKFTBQ.png', 5, 5) +sprite_sheet = load_sprite_sheet('https://raw.githubusercontent.com/fury-gl/' + 'fury-data/master/unittests/fury_sprite.png', + 5, 5) CURRENT_SPRITE_IDX = 0 vtk_sprites = [] diff --git a/docs/examples/viz_chain.py b/docs/examples/viz_chain.py index 90f5a8902..f9898c240 100644 --- a/docs/examples/viz_chain.py +++ b/docs/examples/viz_chain.py @@ -92,15 +92,15 @@ basePosition, baseOrientation, linkMasses=link_Masses, - linkCollisionShapeIndices=linkCollisionShapeIndices, - linkVisualShapeIndices=linkVisualShapeIndices, + linkCollisionShapeIndices=linkCollisionShapeIndices.astype(int), + linkVisualShapeIndices=linkVisualShapeIndices.astype(int), linkPositions=linkPositions, linkOrientations=linkOrientations, linkInertialFramePositions=linkInertialFramePositions, linkInertialFrameOrientations=linkInertialFrameOrns, - linkParentIndices=indices, - linkJointTypes=jointTypes, - linkJointAxis=axis, + linkParentIndices=indices.astype(int), + linkJointTypes=jointTypes.astype(int), + linkJointAxis=axis.astype(int), ) ############################################################################### diff --git a/docs/examples/viz_interaction.py b/docs/examples/viz_interaction.py index 775fccb8b..4775626cd 100644 --- a/docs/examples/viz_interaction.py +++ b/docs/examples/viz_interaction.py @@ -18,7 +18,6 @@ Notes ----- - If you don't have ffmpeg installed, you need to install it to use WebRTC Linux @@ -29,6 +28,7 @@ OS X `brew install ffmpeg opus libvpx pkg-config` + """ import multiprocessing diff --git a/docs/examples/viz_skinning.py b/docs/examples/viz_skinning.py index c7fc035f0..5a83ab54f 100644 --- a/docs/examples/viz_skinning.py +++ b/docs/examples/viz_skinning.py @@ -32,7 +32,7 @@ # After we get the timeline object, We want to initialise the skinning process. # You can set `bones=true` to visualize each bone transformation. Additionally, -# you can set `lenght` of bones in the `initialise_skin` method. +# you can set `length` of bones in the `initialise_skin` method. # Note: Make sure to call this method before you initialize ShowManager, else # bones won't be added to the scene. diff --git a/docs/examples/viz_widget.py b/docs/examples/viz_widget.py index f5ad3e39e..5a500803a 100644 --- a/docs/examples/viz_widget.py +++ b/docs/examples/viz_widget.py @@ -31,8 +31,9 @@ `brew install ffmpeg opus libvpx pkg-config` Notes ------- +----- For this example your python version should be 3.8 or greater + """ import asyncio diff --git a/docs/examples/viz_wrecking_ball.py b/docs/examples/viz_wrecking_ball.py index 31ec68c6c..1c3eb02d9 100644 --- a/docs/examples/viz_wrecking_ball.py +++ b/docs/examples/viz_wrecking_ball.py @@ -177,15 +177,15 @@ basePosition, baseOrientation, linkMasses=link_Masses, - linkCollisionShapeIndices=linkCollisionShapeIndices, - linkVisualShapeIndices=linkVisualShapeIndices, - linkPositions=linkPositions, - linkOrientations=linkOrientations, - linkInertialFramePositions=linkInertialFramePositions, - linkInertialFrameOrientations=linkInertialFrameOrns, - linkParentIndices=indices, - linkJointTypes=jointTypes, - linkJointAxis=axis, + linkCollisionShapeIndices=linkCollisionShapeIndices.astype(int), + linkVisualShapeIndices=linkVisualShapeIndices.astype(int), + linkPositions=linkPositions.astype(int), + linkOrientations=linkOrientations.astype(int), + linkInertialFramePositions=linkInertialFramePositions.astype(int), + linkInertialFrameOrientations=linkInertialFrameOrns.astype(int), + linkParentIndices=indices.astype(int), + linkJointTypes=jointTypes.astype(int), + linkJointAxis=axis.astype(int), ) ############################################################################### diff --git a/docs/experimental/viz_canvas.py b/docs/experimental/viz_canvas.py index adaf34164..b3da4f5b1 100644 --- a/docs/experimental/viz_canvas.py +++ b/docs/experimental/viz_canvas.py @@ -194,7 +194,7 @@ def test_sh(): vec3 lin = 2.5*occ*vec3(1.0,1.00,1.00)*(0.6+0.4*nor.y); lin += 1.0*sss*vec3(1.0,0.95,0.70)*occ; - // surface-light interacion + // surface-light interaction col = mate.xyz * lin; } diff --git a/docs/experimental/viz_molecular_demo.py b/docs/experimental/viz_molecular_demo.py index 72ce5cf10..a8155d41c 100644 --- a/docs/experimental/viz_molecular_demo.py +++ b/docs/experimental/viz_molecular_demo.py @@ -25,6 +25,7 @@ Importing necessary modules + """ import os diff --git a/docs/experimental/viz_shader_canvas.py b/docs/experimental/viz_shader_canvas.py index 51bc0a7cb..35d5ccb1d 100644 --- a/docs/experimental/viz_shader_canvas.py +++ b/docs/experimental/viz_shader_canvas.py @@ -160,6 +160,7 @@ def rectangle2(centers, colors, use_vertices=False, size=(2, 2)): centers : ndarray, shape (N, 3) colors : ndarray (N,3) or (N, 4) or tuple (3,) or tuple (4,) RGB or RGBA (for opacity) R, G, B and A should be at the range [0, 1] + """ if np.array(colors).ndim == 1: colors = np.tile(colors, (len(centers), 1)) diff --git a/docs/experimental/viz_shader_frag_fract.py b/docs/experimental/viz_shader_frag_fract.py index 6abb857b1..337073a40 100644 --- a/docs/experimental/viz_shader_frag_fract.py +++ b/docs/experimental/viz_shader_frag_fract.py @@ -1,5 +1,4 @@ -""" -This simple example demonstrates how to use shaders to modify the fragments in +"""This simple example demonstrates how to use shaders to modify the fragments in your scene. We will use the AddShaderReplacement() function to modify the fragment shader with VTK's shader template system. diff --git a/docs/source/_static/versions_switcher.json b/docs/source/_static/versions_switcher.json index 1ba7ed4e6..bf8a8f2c8 100644 --- a/docs/source/_static/versions_switcher.json +++ b/docs/source/_static/versions_switcher.json @@ -5,8 +5,13 @@ "url": "https://fury.gl/dev/index.html" }, { - "name": "v0.9.x (stable)", + "name": "v0.10.x (stable)", "version": "stable", + "url": "https://fury.gl/v0.10.x/index.html" + }, + { + "name": "v0.9.x", + "version": "v0.9.x", "url": "https://fury.gl/v0.9.x/index.html" }, { diff --git a/docs/source/conf.py b/docs/source/conf.py index 8680badcf..af87cc95a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -341,9 +341,6 @@ 'scipy': ('https://docs.scipy.org/doc/scipy/', None), 'pandas': ('https://pandas.pydata.org/docs/', None), 'matplotlib': ('https://matplotlib.org/stable/', None), - 'dipy': ( - 'https://dipy.org/documentation/latest', - 'https://dipy.org/documentation/latest/objects.inv/', - ), + 'dipy': ('https://docs.dipy.org/stable', None), 'scikit-learn': ('https://scikit-learn.org/stable/', None), } diff --git a/docs/source/ext/apigen.py b/docs/source/ext/apigen.py index 03348a660..118d04daa 100644 --- a/docs/source/ext/apigen.py +++ b/docs/source/ext/apigen.py @@ -1,5 +1,4 @@ -""" -Attempt to generate templates for module reference with Sphinx. +"""Attempt to generate templates for module reference with Sphinx. To include extension modules, first identify them as valid in the ``_uri2path`` method, then handle them in the ``_parse_module_with_import`` @@ -15,6 +14,7 @@ This is a modified version of a script originally shipped with the PyMVPA project, then adapted for use first in NIPY and then in skimage. PyMVPA is an MIT-licensed project. + """ # Stdlib imports @@ -29,7 +29,8 @@ class ApiDocWriter: """Class for automatic detection and parsing of API docs - to Sphinx-parsable reST format.""" + to Sphinx-parsable reST format. + """ # only separating first two levels rst_section_levels = ['*', '=', '-', '~', '^'] diff --git a/docs/source/ext/build_modref_templates.py b/docs/source/ext/build_modref_templates.py index 4d5455551..ef96c6a00 100755 --- a/docs/source/ext/build_modref_templates.py +++ b/docs/source/ext/build_modref_templates.py @@ -54,7 +54,6 @@ def generate_api_reference_rst( def setup(app): """Setup sphinx extension for API reference generation.""" - app.connect('builder-inited', generate_api_reference_rst) # app.connect('build-finished', summarize_failing_examples) diff --git a/docs/source/ext/github.py b/docs/source/ext/github.py index a6df31f3c..99bea76e3 100644 --- a/docs/source/ext/github.py +++ b/docs/source/ext/github.py @@ -30,7 +30,6 @@ def make_link_node(rawtext, app, type, slug, options): :param slug: ID of the thing to link to :param options: Options dictionary passed to role func. """ - try: base = app.config.github_project_url if not base: @@ -68,7 +67,6 @@ def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): :param options: Directive options for customization. :param content: The directive content for customization. """ - try: issue_num = int(text) if issue_num <= 0: diff --git a/docs/source/posts/2021/2021-06-28-gsoc-devmessias-4.rst b/docs/source/posts/2021/2021-06-28-gsoc-devmessias-4.rst index cfceb132f..94fcaa35f 100644 --- a/docs/source/posts/2021/2021-06-28-gsoc-devmessias-4.rst +++ b/docs/source/posts/2021/2021-06-28-gsoc-devmessias-4.rst @@ -156,7 +156,7 @@ must have a way to lock the write/read if the memory resource is busy. Meanwhile the `multiprocessing.Arrays `__ already has a context which allows lock (.get_lock()) SharedMemory -dosen’t[2]. The use of abstract class allowed me to deal with those +doesn’t[2]. The use of abstract class allowed me to deal with those peculiarities. `commit 358402e `__ diff --git a/docs/source/posts/2021/2021-07-05-gsoc-devmessias-5.rst b/docs/source/posts/2021/2021-07-05-gsoc-devmessias-5.rst index 416ac6c1f..d1c4b81b6 100644 --- a/docs/source/posts/2021/2021-07-05-gsoc-devmessias-5.rst +++ b/docs/source/posts/2021/2021-07-05-gsoc-devmessias-5.rst @@ -35,7 +35,7 @@ tasks related with this PR: `#424`_ now is possible to control all the visual characteristics at runtime. - 2D Layout: Meanwhile 3d network representations are very usefully - for exploring a dataset is hard to convice a group of network + for exploring a dataset is hard to convince a group of network scientists to use a visualization system which doesn't allow 2d representations. Because of that I started to coding the 2d behavior in the network visualization system. diff --git a/docs/source/posts/2021/2021-08-23-final-work-antriksh.rst b/docs/source/posts/2021/2021-08-23-final-work-antriksh.rst index 4dd4702a2..8bf4f39bf 100644 --- a/docs/source/posts/2021/2021-08-23-final-work-antriksh.rst +++ b/docs/source/posts/2021/2021-08-23-final-work-antriksh.rst @@ -83,7 +83,7 @@ Objectives Completed - **Add Accordion2D UI element to the UI sub-module** - Added Accordion2D to the UI sub-module. This Ui element allows users to visulize data in a tree with depth of one. Each node has a title and a content panel. The children for each node can be N if and only if the children are not nodes themselves. The child UIs can be placed inside the content panel by passing some coordinates, which can be absolute or normalized w.r.t the node content panel size. Tests and two demos were added for this UI element. Below is a screenshot for reference + Added Accordion2D to the UI sub-module. This Ui element allows users to visualize data in a tree with depth of one. Each node has a title and a content panel. The children for each node can be N if and only if the children are not nodes themselves. The child UIs can be placed inside the content panel by passing some coordinates, which can be absolute or normalized w.r.t the node content panel size. Tests and two demos were added for this UI element. Below is a screenshot for reference .. image:: https://camo.githubusercontent.com/9395d0ea572d7f253a051823f02496450c9f79d19ff0baf32841ec648b6f2860/68747470733a2f2f692e696d6775722e636f6d2f7854754f645a742e706e67 :width: 200 diff --git a/docs/source/posts/2023/2023-01-24-final-report-praneeth.rst b/docs/source/posts/2023/2023-01-24-final-report-praneeth.rst index c6428ecfe..fdd6230bd 100644 --- a/docs/source/posts/2023/2023-01-24-final-report-praneeth.rst +++ b/docs/source/posts/2023/2023-01-24-final-report-praneeth.rst @@ -133,7 +133,7 @@ Other Objectives - **Grouping Shapes** - Many times we need to perform some actions on a group of shapes so here we are with the grouping feature using which you can group shapes together, reposition them, rotate them and delete them together. To activate grouping of shapes you have to be on selection mode then by holding **Ctrl** key select the required shapes and they will get highlighted. To remove shape from the group just hold the **Ctrl** and click the shape again it will get deselected. Then once eveything is grouped you can use the normal transformation as normal i.e. for translation just drag the shapes around and for rotation the rotation slider appears at usual lower left corner which can be used. + Many times we need to perform some actions on a group of shapes so here we are with the grouping feature using which you can group shapes together, reposition them, rotate them and delete them together. To activate grouping of shapes you have to be on selection mode then by holding **Ctrl** key select the required shapes and they will get highlighted. To remove shape from the group just hold the **Ctrl** and click the shape again it will get deselected. Then once everything is grouped you can use the normal transformation as normal i.e. for translation just drag the shapes around and for rotation the rotation slider appears at usual lower left corner which can be used. *Pull Requests:* diff --git a/docs/source/posts/2023/2023-08-21-joaodellagli-final-report.rst b/docs/source/posts/2023/2023-08-21-joaodellagli-final-report.rst new file mode 100644 index 000000000..33650149a --- /dev/null +++ b/docs/source/posts/2023/2023-08-21-joaodellagli-final-report.rst @@ -0,0 +1,360 @@ +.. image:: https://developers.google.com/open-source/gsoc/resources/downloads/GSoC-logo-horizontal.svg + :height: 40 + :target: https://summerofcode.withgoogle.com/programs/2023/projects/ED0203De + +.. image:: https://www.python.org/static/img/python-logo@2x.png + :height: 40 + :target: https://summerofcode.withgoogle.com/programs/2023/organizations/python-software-foundation + +.. image:: https://python-gsoc.org/logos/fury_logo.png + :width: 40 + :target: https://fury.gl/latest/index.html + + + +Google Summer of Code Final Work Product +======================================== + +.. post:: August 21 2023 + :author: João Victor Dell Agli Floriano + :tags: google + :category: gsoc + +- **Name:** João Victor Dell Agli Floriano +- **Organisation:** Python Software Foundation +- **Sub-Organisation:** FURY +- **Project:** `FURY - Project 2. Fast 3D kernel-based density rendering using billboards. `_ + + +Abstract +-------- +This project had the goal to implement 3D Kernel Density Estimation rendering to FURY. Kernel Density Estimation, or KDE, is a +statistical method that uses kernel smoothing for modeling and estimating the density distribution of a set of points defined +inside a given region. For its graphical implementation, it was used post-processing techniques such as offscreen rendering to +framebuffers and colormap post-processing as tools to achieve the desired results. This was completed with a functional basic KDE +rendering result, that relies on a solid and easy-to-use API, as well as some additional features. + +Proposed Objectives +------------------- + +- **First Phase** : Implement framebuffer usage in FURY + * Investigate the usage of float framebuffers inside FURY's environment. + * Implement a float framebuffer API. + +- **Second Phase** : Shader-framebuffer integration + * Implement a shader that uses a colormap to render framebuffers. + * Escalate this rendering for composing multiple framebuffers. + +- **Third Phase** : KDE Calculations + * Investigate KDE calculation for point-cloud datasets. + * Implement KDE calculation inside the framebuffer rendering shaders. + * Test KDE for multiple datasets. + +Objectives Completed +-------------------- + +- **Implement framebuffer usage in FURY** + The first phase, addressed from *May/29* to *July/07*, started with the investigation of + `VTK's Framebuffer Object `_, a vital part of this project, to understand + how to use it properly. + + Framebuffer Objects, abbreviated as FBOs, are the key to post-processing effects in OpenGL, as they are used to render things offscreen and save the resulting image to a texture + that will be later used to apply the desired post-processing effects within the object's `fragment shader `_ + rendered to screen, in this case, a `billboard `_. In the case of the + `Kernel Density Estimation `_ post-processing effect, we need a special kind of FBO, one that stores textures' + values as floats, different from the standard 8-bit unsigned int storage. This is necessary because the KDE rendering involves rendering every KDE point calculation + to separate billboards, rendered to the same scene, which will have their intensities, divided by the number of points rendered, blended with + `OpenGL Additive Blending `_, and if a relative big number of points are rendered at the + same time, 32-bit float precision is needed to guarantee that small-intensity values will not be capped to zero, and disappear. + + After a month going through VTK's FBO documentation and weeks spent trying different approaches to this method, it would not work + properly, as some details seemed to be missing from the documentation, and asking the community haven't solved the problem as well. + Reporting that to my mentors, which unsuccessfully tried themselves to make it work, they decided it was better if another path was taken, using + `VTK's WindowToImageFilter `_ method as a workaround, described + in this `blogpost `_. This method helped the development of + three new functions to FURY, *window_to_texture()*, *texture_to_actor()* and *colormap_to_texture()*, that allow the passing of + different kinds of textures to FURY's actor's shaders, the first one to capture a window and pass it as a texture to an actor, + the second one to pass an external texture to an actor, and the third one to specifically pass a colormap as a texture to an + actor. It is important to say that *WindowToImageFilter()* is not the ideal way to make it work, as this method does not seem to + support float textures. However, a workaround to that is currently being worked on, as I will describe later on. + + *Pull Requests:* + + - **KDE Rendering Experimental Program (Needs major revision):** `https://github.com/fury-gl/fury/pull/804 `_ + + The result of this whole FBO and WindowToImageFilter experimentation is well documented in PR + `#804 `_ that implements an experimental version of a KDE rendering program. + The future of this PR, as discussed with my mentors, is to be better documented to be used as an example for developers on + how to develop features in FURY with the tools used, and it shall be done soon. + +- **Shader-framebuffer integration** + The second phase, which initially was thought of as "Implement a shader that uses a colormap to render framebuffers" and "Escalate this + rendering for composing multiple framebuffers" was actually a pretty simple phase that could be addressed in one week, *July/10* + to *July/17*, done at the same time as the third phase goal, documented in this + `blogpost `_. As FURY already had a tool for generating and + using colormaps, they were simply connected to the shader part of the program as textures, with the functions explained above. + Below, is the result of the *matplotlib viridis* colormap passed to a simple gaussian KDE render: + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/final_2d_plot.png + :align: center + :alt: Final 2D plot + + That is also included in PR `#804 `_. Having the 2D plot ready, some time was taken to + figure out how to enable a 3D render, that includes rotation and other movement around the set rendered, which was solved by + learning about the callback properties that exist inside *VTK*. Callbacks are ways to enable code execution inside the VTK rendering + loop, enclosed inside *vtkRenderWindowInteractor.start()*. If it is desired to add a piece of code that, for example, passes a time + variable to the fragment shader over time, a callback function can be declared: + + .. code-block:: python + + from fury import window + t = 0 + showm = window.ShowManager(...) + + def callback_function: + t += 0.01 + pass_shader_uniforms_to_fs(t, "t") + + showm.add_iren_callback(callback_function, "RenderEvent") + + The piece of code above created a function that updates the time variable *t* in every *"RenderEvent"*, and passes it to the + fragment shader. With that property, the camera and some other parameters could be updated, which enabled 3D visualization, that + then, outputted the following result, using *matplotlib inferno* colormap: + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/3d_kde_gif.gif + :align: center + :alt: 3D Render gif + +- **KDE Calculations** (ongoing) + As said before, the second and third phases were done simultaneously, so after having a way to capture the window and use it as a + texture ready, the colormap ready, and an initial KDE render ready, all it was needed to do was to improve the KDE calculations. + As this `Wikipedia page `_ explains, a KDE calculation is to estimate an + abstract density around a set of points defined inside a given region with a kernel, that is a function that models the density + around a point based on its associated distribution :math:`\sigma`. + + A well-known kernel is, for example, the **Gaussian Kernel**, that says that the density around a point :math:`p` with distribution + :math:`\sigma` is defined as: + + .. math:: + + GK_{\textbf{p}, \sigma} (\textbf{x}) = e^{-\frac{1}{2}\frac{||\textbf{x} - \textbf{p}||^2}{\sigma^2}} + + Using that kernel, we can calculate the KDE of a set of points :math:`P` with associated distributions :math:`S` calculating their individual + Gaussian distributions, summing them up and dividing them by the total number of points :math:`n`: + + .. math:: + + KDE(A, S)=\frac{1}{n}\sum_{i = 0}^{n}GK(x, p_{i}, \sigma_{i}) + + So I dove into implementing all of that into the offscreen rendering part, and that is when the lack of a float framebuffer would + charge its cost. As it can be seen above, just calculating each point's density isn't the whole part, as I also need to divide + everyone by the total number of points :math:`n`, and then sum them all. The problem is that, if the number of points its big enough, + the individual densities will be really low, and that would not be a problem for a 32-bit precision float framebuffer, but that is + *definitely* a problem for a 8-bit integer framebuffer, as small enough values will simply underflow and disappear. That issue is + currently under investigation, and some solutions have already being presented, as I will show in the **Objectives in Progress** + section. + + Apart from that, after having the experimental program ready, I focused on modularizing it into a functional and simple API + (without the :math:`n` division for now), and I could get a good set of results from that. The API I first developed implemented the + *EffectManager* class, responsible for managing all of the behind-the-scenes steps necessary for the kde render to work, + encapsulated inside the *ÈffectManager.kde()* method. It had the following look: + + .. code-block:: python + from fury.effect_manager import EffectManager + from fury import window + + showm = window.ShowManager(...) + + # KDE rendering setup + em = EffectManager(showm) + kde_actor = em.kde(...) + # End of KDE rendering setup + + showmn.scene.add(kde_actor) + + showm.start() + + Those straightforward instructions, that hid several lines of code and setup, could manage to output the following result: + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/fianl_3d_plot.png + :align: center + :alt: API 3D KDE plot + + And this was not the only feature I had implemented for this API, as the use of *WindowToImageFilter* method opened doors for a + whole new world for FURY: The world of post-processing effects. With this features setup, I managed to implement a *gaussian blur* + effect, a *grayscale* effect and a *Laplacian* effect for calculating "borders": + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/gaussian_blur.png + :align: center + :alt: Gaussian Blur effect + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/grayscale.png + :align: center + :alt: Grayscale effect + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/laplacian1.gif + :align: center + :alt: Laplacian effect + + As this wasn't the initial goal of the project and I still had several issues to deal with, I have decided to leave these features as a + future addition. + + Talking with my mentors, we realized that the first KDE API, even though simple, could lead to bad usage from users, as the + *em.kde()* method, that outputted a *FURY actor*, had dependencies different from any other object of its kind, making it a new + class of actors, which could lead to confusion and bad handling. After some pair programming sessions, they instructed me to take + a similar, but different road from what I was doing, turning the kde actor into a new class, the *KDE* class. This class would + have almost the same set of instructions present in the prior method, but it would break them in a way it would only be completely + set up after being passed to the *EffectManager* via its add function. Below, how the refactoring handles it: + + .. code-block:: python + + from fury.effects import EffectManager, KDE + from fury import window + + showm = window.ShowManager(...) + + # KDE rendering setup + em = EffectManager(showm) + kde_effect = KDE(...) + em.add(kde_effect) + # End of KDE rendering setup + + showm.start() + + Which outputted the same results as shown above. It may have cost some simplicity as we are now one line farther from having it + working, but it is more explicit in telling the user this is not just a normal actor. + + Another detail I worked on was the kernel variety. The Gaussian Kernel isn't the only one available to model density distributions, + there are several others that can do that job, as it can be seen in this `scikit-learn piece of documentation `_ + and this `Wikipedia page on kernels `_. Based on the scikit-learn KDE + implementation, I worked on implementing the following kernels inside our API, that can be chosen as a parameter when calling the + *KDE* class: + + * Cosine + * Epanechnikov + * Exponential + * Gaussian + * Linear + * Tophat + + Below, the comparison between them using the same set of points and bandwidths: + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/kernels.png + :align: center + :alt: Comparison between the six implemented kernels + + + *Pull Requests*: + + - **First Stage of the KDE Rendering API (will be merged soon)**: `https://github.com/fury-gl/fury/pull/826 `_ + + All of this work culminated in PR `#826 `_, that proposes to add the first stage of + this API (there are some details yet to be completed, like the :math:`n` division) to FURY. This PR added the described API, and also + proposed some minor changes to some already existing FURY functions related to callbacks, changes necessary for this and other + future applications that would use it to work. It also added the six kernels described, and a simple documented example on how + to use this feature. + +Other Objectives +---------------- + +- **Stretch Goals** : SDE Implementation, Network/Graph visualization using SDE/KDE, Tutorials + * Investigate SDE calculation for surface datasets. + * Implement SDE calculation inside the framebuffer rendering shaders. + * Test SDE for multiple datasets. + * Develop comprehensive tutorials that explain SDE concepts and FURY API usage. + * Create practical, scenario-based tutorials using real datasets and/or simulations. + +Objectives in Progress +---------------------- + +- **KDE Calculations** (ongoing) + The KDE rendering, even though almost complete, have the $n$ division, an important step, missing, as this normalization allows colormaps + to cover the whole range o values rendered. The lack of a float FBO made a big difference in the project, as the search for a functional implementation of it not only delayed the project, but it is vital for + the correct calculations to work. + + For the last part, a workaround thought was to try an approach I later figured out is an old one, as it can be check in + `GPU Gems 12.3.3 section `_: + If I need 32-bit float precision and I got 4 8-bit integer precision available, why not trying to pack this float into this RGBA + texture? I have first tried to do one myself, but it didn't work for some reason, so I tried `Aras Pranckevičius `_ + implementation, that does the following: + + .. code-block:: GLSL + + vec4 float_to_rgba(float value) { + vec4 bitEnc = vec4(1.,256.,65536.0,16777216.0); + vec4 enc = bitEnc * value; + enc = fract(enc); + enc -= enc.yzww * vec2(1./255., 0.).xxxy; + return enc; + } + + That initially worked, but for some reason I am still trying to understand, it is resulting in a really noisy texture: + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/noisy%20kde.png + :align: center + :alt: Noisy KDE render + + One way to try to mitigate that while is to pass this by a gaussian blur filter, to try to smooth out the result: + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/blurred_kde.png + :align: center + :alt: Blurred result + + But it is not an ideal solution as well, as it may lead to distortions in the actual density values, depending on the application of + the KDE. Now, my goal is to first find the root of the noise problem, and then, if that does not work, try to make the gaussian filter + work. + + Another detail that would be a good addition to the API is UI controls. Filipi, one of my mentors, told me it would be a good feature + if the user could control the intensities of the bandwidths for a better structural visualization of the render, and knowing FURY already + have a good set of `UI elements `_, I just needed to integrate + that into my program via callbacks. I tried implementing an intensity slider. However, for some reason, it is making the program crash + randomly, for reasons I still don't know, so that is another issue under investigation. Below, we show a first version of that feature, + which was working before the crashes: + + .. image:: https://raw.githubusercontent.com/JoaoDell/gsoc_assets/main/images/slider.gif + :align: center + :alt: Slider for bandwidths + + *Pull Requests* + + - **UI intensity slider for the KDE rendering API (draft)**: `https://github.com/fury-gl/fury/pull/849 `_ + - **Post-processing effects for FURY Effects API (draft)**: `https://github.com/fury-gl/fury/pull/850 `_ + + +GSoC Weekly Blogs +----------------- + +- My blog posts can be found at `FURY website `_ and `Python GSoC blog `_. + +Timeline +-------- + ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Date | Description | Blog Post Link | ++=====================+====================================================+===========================================================================================================================================================================================================+ +| Week 0 (29-05-2023) | The Beginning of Everything | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 1 (05-06-2022) | The FBO Saga | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 2 (12-06-2022) | The Importance of (good) Documentation | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 3 (19-06-2022) | Watch Your Expectations | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 4 (26-06-2022) | Nothing is Ever Lost | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 5 (03-07-2022) | All Roads Lead to Rome | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 6 (10-07-2022) | Things are Starting to Build Up | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 7 (17-07-2022) | Experimentation Done | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 8 (24-07-2022) | The Birth of a Versatile API | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 9 (31-07-2022) | It is Polishing Time! | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 10 (07-08-2022)| Ready for Review! | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 11 (14-08-2022)| A Refactor is Sometimes Needed | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 12 (21-08-2022)| Now That is (almost) a Wrap! | `FURY `__ - `Python `__ | ++---------------------+----------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/docs/source/posts/2023/2023-08-24-final-report-tvcastillod.rst b/docs/source/posts/2023/2023-08-24-final-report-tvcastillod.rst new file mode 100644 index 000000000..a1d730e1b --- /dev/null +++ b/docs/source/posts/2023/2023-08-24-final-report-tvcastillod.rst @@ -0,0 +1,174 @@ +.. image:: https://developers.google.com/open-source/gsoc/resources/downloads/GSoC-logo-horizontal.svg + :height: 50 + :align: center + :target: https://summerofcode.withgoogle.com/programs/2023/projects/ymwnLwtT + +.. image:: https://www.python.org/static/community_logos/python-logo.png + :width: 40% + :target: https://summerofcode.withgoogle.com/programs/2023/organizations/python-software-foundation + +.. image:: https://python-gsoc.org/logos/FURY.png + :width: 25% + :target: https://fury.gl/latest/index.html + +Google Summer of Code Final Work Product +======================================== + +.. post:: August 24 2023 + :author: Tania Castillo + :tags: google + :category: gsoc + +- **Name:** Tania Castillo +- **Organisation:** Python Software Foundation +- **Sub-Organisation:** FURY +- **Project:** `SDF-based uncertainty representation for dMRI glyphs `_ + + +Abstract +-------------------- +Diffusion Magnetic Resonance Imaging (dMRI) is a non-invasive imaging technique used by neuroscientists to measure the diffusion of water molecules in biological tissue. The directional information is reconstructed using either a Diffusion Tensor Imaging (DTI) or High Angular Resolution Diffusion Imaging (HARDI) based model, which is graphically represented as tensors and Orientation Distribution Functions (ODF). Traditional rendering engines discretize Tensor and ODF surfaces using triangles or quadrilateral polygons, making their visual quality depending on the number of polygons used to build the 3D mesh, which might compromise real-time display performance. This project proposes a methodological approach to further improve the visualization of DTI tensors and HARDI ODFs glyphs by using well-established techniques in the field of computer graphics, such as geometry amplification, billboarding, signed distance functions (SDFs), and ray marching. + + +Proposed Objectives +------------------- + +- Implement a parallelized version of computer-generated billboards using geometry shaders for amplification. +- Model the mathematical functions that express the geometry of ellipsoid glyphs and implement them using Ray Marching techniques. +- Model the mathematical functions that express the geometry of ODF glyphs and implement them using Ray Marching techniques. +- Use SDF properties and techniques to represent the uncertainty of dMRI reconstruction models. + + +Objectives Completed +-------------------- + +Ellipsoid actor implemented with SDF +************************************ + +A first approach for tensor glyph generation has been made, using ray marching and SDF applied to a box. The current implementation (``tensor_slicer``) requires a sphere with a specific number of vertices to be deformed. Based on this model, a sphere with more vertices is needed to get a higher resolution. Because the ray marching technique does not use polygonal meshes, it is possible to define perfectly smooth surfaces and still obtain a fast rendering. + +Details of the implementation: + +- *Vertex shader pre-calculations*: Some minor calculations are done in the vertex shader. One, corresponding to the eigenvalues constraining and min-max normalization, are to avoid incorrect visualizations when the difference between the eigenvalues is too large. And the other is related to the tensor matrix calculation given by the diffusion tensor definition :math:`T = R^{−1}\Lambda R`, where :math:`R` is a rotation matrix that transforms the standard basis onto the eigenvector basis, and :math:`\Lambda` is the diagonal matrix of eigenvalues [4]_. +- *Ellipsoid SDF definition*: The definition of the SDF is done in the fragment shader inside the ``map`` function, which is used later for the ray marching algorithm and the normals calculation. We define the SDF more simply by transforming a sphere into an ellipsoid, considering that the SDF of a sphere is easily computed and the definition of a tensor gives us a linear transformation of a given geometry. Also, as scaling is not a rigid body transformation, we multiply the final result by a factor to compensate for the difference, which gave us the SDF of the ellipsoid defined as ``sdSphere(tensorMatrix * (position - centerMCVSOutput), scaleVSOutput*0.48) * scFactor``. +- *Ray marching algorithm and lighting*: For the ray marching algorithm, a small value of 20 was taken as the maximum distance since we apply the technique to each individual object and not all at the same time. Additionally, we set the convergence precision to 0.001. We use the central differences method to compute the normals necessary for the scene’s illumination, besides the Blinn-Phong lighting technique, which is high-quality and computationally cheap. +- *Visualization example*: Below is a detailed visualization of the ellipsoids created from this new implementation. + +.. image:: https://user-images.githubusercontent.com/31288525/244503195-a626718f-4a13-4275-a2b7-6773823e553c.png + :width: 376 + :align: center + +This implementation does show a better quality in the displayed glyphs, and supports the display of a large amount of data, as seen in the image below. For this reason, a tutorial was made to justify in more detail the value of this new implementation. Below are some images generated for the tutorial. + +.. image:: https://user-images.githubusercontent.com/31288525/260906510-d422e7b4-3ba3-4de6-bfd0-09c04bec8876.png + :width: 600 + :align: center + +*Pull Requests:* + +- **Ellipsoid actor implemented with SDF (Merged)** https://github.com/fury-gl/fury/pull/791 +- **Tutorial on using ellipsoid actor to visualize tensor ellipsoids for DTI (Merged)** https://github.com/fury-gl/fury/pull/818 + +**Future work:** In line with one of the initial objectives, it is expected to implement billboards later on to improve the performance, i.e., higher frame rate and less memory usage for the tensor ellipsoid creation. In addition to looking for ways to optimize the naive ray marching algorithm and the definition of SDFs. + +Objectives in Progress +---------------------- + +DTI uncertainty visualization +***************************** + +The DTI visualization pipeline is fairly complex, as a level of uncertainty arises, which, if visualized, helps to assess the model's accuracy. This measure is not currently implemented, and even though there are several methods to calculate and visualize the uncertainty in the DTI model, because of its simplicity and visual representation, we considered Matrix Perturbation Analysis (MPA) proposed by Basser [1]_. This measurement is visualized as double cones representing the variance of the main direction of diffusion, for which the ray marching technique was also used to create these objects. + +Details of the implementation: + +- *Source of uncertainty*: The method of MPA arises from the susceptibility of DTI to dMRI noise present in diffusion-weighted images (DWIs), and also because the model is inherently statistical, making the tensor estimation and other derived quantities to be random variables [1]_. For this reason, this method focus on the premise that image noise produces a random perturbation in the diffusion tensor estimation, and therefore in the calculation of eigenvalues and eigenvectors, particularly in the first eigenvector associated with the main diffusion direction. +- *Mathematical equation*: The description of the perturbation of the principal eigenvector is given by math formula where :math:`\Delta D` corresponds to the estimated perturbation matrix of :math:`D` given by the diagonal elements of the covariance matrix :math:`\Sigma_{\alpha} \approx (B^T\Sigma^{−1}_{e}B)^{−1}`, where :math:`\Sigma_{e}` is the covariance matrix of the error e, defined as a diagonal matrix made with the diagonal elements of :math:`(\Sigma^{−1}_{e}) = ⟨S(b)⟩^2 / \sigma^{2}_{\eta}`. Then, to get the angle :math:`\theta` between the perturbed principal eigenvector of :math:`D`, :math:`\varepsilon_1 + \Delta\varepsilon_1`, and the estimated eigenvector :math:`\varepsilon_1`, it can be approximated by :math:`\theta = \tan^{−1}( \| \Delta\varepsilon_1 \|)` [2]_. Taking into account the above, we define the function ``main_dir_uncertainty(evals, evecs, signal, sigma, b_matrix)`` that calculates the uncertainty of the eigenvector associated to the main direction of diffusion. +- *Double cone SDF definition*: The final SDF is composed by the union of 2 separately cones using the definition taken from this list of `distance functions `_, in this way we have the SDF for the double cone defined as ``opUnion(sdCone(p,a,h), sdCone(-p,a,h)) * scaleVSOutput`` +- *Visualization example*: Below is a demo of how this new feature is intended to be used, an image of diffusion tensor ellipsoids and their associated uncertainty cones. + +.. image:: https://user-images.githubusercontent.com/31288525/254747296-09a8674e-bfc0-4b3f-820f-8a1b1ad8c5c9.png + :width: 610 + :align: center + +The implementation is almost complete, but as it is a new addition that includes mathematical calculations and for which there is no direct reference for comparison, it requires a more detail review before it can be incorporated. + +*Pull Request:* + +- **DTI uncertainty visualization (Under Review)** https://github.com/fury-gl/fury/pull/810 + +**Future work:** A tutorial will be made explaining in more detail how to calculate the parameters needed for the uncertainty cones using **dipy** functions, specifically: `estimate_sigma `_ for the noise variance calculation, `design_matrix `_ to get the b-matrix, and `tensor_prediction `_ for the signal estimation. Additionally, when the ODF implementation is complete, uncertainty for this other reconstruction model is expected to be added, using semitransparent glyphs representing the mean directional information proposed by Tournier [3]_. + +ODF actor implemented with SDF +****************************** + +HARDI-based techniques require more images than DTI, however, they model the diffusion directions as probability distribution functions (PDFs), and the fitted values are returned as orientation distribution functions (ODFs). ODFs are more diffusion sensitive than the diffusion tensor and, therefore, can determine the structure of multi-directional voxels very common in the white matter regions of the brain [3]_. The current actor to display this kind of glyphs is the ``odf_slicer`` which, given an array of spherical harmonics (SH) coefficients renders a grid of ODFs, which are created from a sphere with a specific number of vertices that fit the data. + +For the application of this model using the same SDF ray marching techniques, we need the data of the SH coefficients, which are used to calculate the orientation distribution function (ODF) described `here `_. Different SH bases can be used, but for this first approach we focus on ``descoteaux07`` (as labeled in dipy). After performing the necessary calculations, we obtain an approximate result of the current implementation of FURY, as seen below. + +.. image:: https://user-images.githubusercontent.com/31288525/260909561-fd90033c-018a-465b-bd16-3586bb31ca36.png + :width: 580 + :align: center + +With a first implementation we start to solve some issues related to direction, color, and data handling, to obtain exactly the same results as the current implementation. + +Details on the issues: + +- *The direction and the scaling*: When the shape of the ODF is more sphere-like, the size of the glyph is smaller, so for the moment it needs to be adjusted manually, but the idea is to find a relationship between the coefficients and the final object size so it can be automatically scaled. Additionally, as seen in the image, the direction does not match. To fix this, an adjustment in the calculation of the spherical coordinates can be made, or pass the direction information directly. +- *Pass the coefficients data efficiently*: I'm currently creating one actor per glyph since I'm using a *uniform* array to pass the coefficients, but the idea is to pass all the data simultaneously. The first idea is to encode the coefficients data through a texture and retrieve them in the fragment shader. +- *The colormapping and the lighting*: As these objects present curvatures with quite a bit of detail in some cases, this requires more specific lighting work, in addition to having now not only one color but a color map. This can also be done with texture, but it is necessary to see in more detail how to adjust the texture to the glyph's shape. + +More details on current progress can be seen in blogpost of `week 11 `_ and `week 12 `_. + +*Working branch:* + +- **ODF implementation (Under Development)** + https://github.com/tvcastillod/fury/tree/SH-for-ODF-impl + + +GSoC Weekly Blogs +----------------- + +- My blog posts can be found on the `FURY website `__ and the `Python GSoC blog `__. + + +Timeline +-------- + ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Date | Description | Blog Post Link | ++=====================+========================================================================+==========================================================================================================================================================================+ +| Week 0(02-06-2022) | Community Bounding Period | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 1(05-06-2022) | Ellipsoid actor implemented with SDF | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 2(12-06-2022) | Making adjustments to the Ellipsoid Actor | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 3(19-06-2022) | Working on uncertainty and details of the first PR | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 4(27-06-2022) | First draft of the DTI uncertainty visualization | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 5(03-07-2022) | Preparing the data for the Ellipsoid tutorial | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 6(10-07-2022) | First draft of the Ellipsoid tutorial | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 7(17-07-2022) | Adjustments on the Uncertainty Cones visualization | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 8(25-07-2022) | Working on Ellipsoid Tutorial and exploring SH | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 9(31-07-2022) | Tutorial done and polishing DTI uncertainty | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 10(08-08-2022) | Start of SH implementation experiments | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 11(16-08-2022) | Adjusting ODF implementation and looking for solutions on issues found | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Week 12(24-08-2022) | Experimenting with ODFs implementation | `FURY `__ - `Python `__ | ++---------------------+------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + +References +---------- + +.. [1] Basser, P. J. (1997). Quantifying errors in fiber direction and diffusion tensor field maps resulting from MR noise. In 5th Scientific Meeting of the ISMRM (Vol. 1740). +.. [2] Chang, L. C., Koay, C. G., Pierpaoli, C., & Basser, P. J. (2007). Variance of estimated DTI‐derived parameters via first‐order perturbation methods. Magnetic Resonance in Medicine: An Official Journal of the International Society for Magnetic Resonance in Medicine, 57(1), 141-149. +.. [3] J-Donald Tournier, Fernando Calamante, David G Gadian, and Alan Connelly. Direct estimation of the fiber orientation density function from diffusion-weighted mri data using spherical deconvolution. Neuroimage, 23(3):1176–1185, 2004. +.. [4] Gordon Kindlmann. Superquadric tensor glyphs. In Proceedings of the Sixth Joint Eurographics-IEEE TCVG conference on Visualization, pages 147–154, 2004. diff --git a/docs/source/posts/2023/2023-08-25-final-report-praneeth.rst b/docs/source/posts/2023/2023-08-25-final-report-praneeth.rst new file mode 100644 index 000000000..0209ba61b --- /dev/null +++ b/docs/source/posts/2023/2023-08-25-final-report-praneeth.rst @@ -0,0 +1,216 @@ +.. image:: https://developers.google.com/open-source/gsoc/resources/downloads/GSoC-logo-horizontal.svg + :height: 50 + :align: center + :target: https://summerofcode.withgoogle.com/programs/2023/projects/BqfBWfwS + +.. image:: https://www.python.org/static/community_logos/python-logo.png + :width: 40% + :target: https://summerofcode.withgoogle.com/programs/2023/organizations/python-software-foundation + +.. image:: https://python-gsoc.org/logos/FURY.png + :width: 25% + :target: https://fury.gl/latest/index.html + +Google Summer of Code Final Work Product +======================================== + +.. post:: August 25 2023 + :author: Praneeth Shetty + :tags: google + :category: gsoc + +- **Name:** Praneeth Shetty +- **Organisation:** Python Software Foundation +- **Sub-Organisation:** FURY +- **Project:** `FURY - Update user interface widget + Explore new UI Framework `_ + + +Proposed Objectives +------------------- + +- SpinBoxUI +- Scrollbar as Independent Element +- FileDialog +- TreeUI +- AccordionUI +- ColorPickerUI + +- Stretch Goals: + - Exploring new UI Framework + - Implementing Borders for UI elements + +Objectives Completed +-------------------- + + +- **SpinBoxUI:** + The ``SpinBoxUI`` element is essential for user interfaces as it allows users to pick a numeric value from a set range. While we had an active pull request (PR) to add this element, updates in the main code caused conflicts and required further changes for added features. At one point, we noticed that text alignment wasn't centered properly within the box due to a flaw. To fix this, we began a PR to adjust the alignment, but it turned into a larger refactoring of the ``TextBlock2D``, a core component connected to various parts. This was a complex task that needed careful handling. After sorting out the ``TextBlock2D``, we returned to the ``SpinBoxUI`` and made a few tweaks. Once we were confident with the changes, the PR was successfully merged after thorough review and testing. + + **Pull Requests:** + - **SpinBoxUI (Merged)** - https://github.com/fury-gl/fury/pull/499 + + .. image:: https://user-images.githubusercontent.com/64432063/263165327-c0b19cdc-9ebd-433a-8ff1-99e706a76508.gif + :height: 500 + :align: center + :alt: SpinBoxUI + + + +- **`TextBlock2D` Refactoring:** + This was a significant aspect of the GSoC period and occupied a substantial portion of the timeline. The process began when we observed misaligned text in the ``SpinBoxUI``, as previously discussed. The root cause of the alignment issue was the mispositioning of the text actor concerning the background actor. The text actor's independent repositioning based on justification conflicted with the static position of the background actor, leading to the alignment problem. + + To address this, the initial focus was on resolving the justification issue. However, as the work progressed, we recognized that solely adjusting justification would not suffice. The alignment was inherently linked to the UI's size, which was currently retrieved only when a valid scene was present. This approach lacked scalability and efficiency, as it constrained size retrieval to scene availability. + + To overcome these challenges, we devised a solution involving the creation of a bounding box around the ``TextBlock2D``. This bounding box would encapsulate the size information, enabling proper text alignment. This endeavor spanned several weeks of development, culminating in a finalized solution that underwent rigorous testing before being merged. + + As a result of this refactoring effort, the ``TextBlock2D`` now offers three distinct modes: + + 1. **Fully Static Background:** This mode requires background setup during initialization. + 2. **Dynamic Background:** The background dynamically scales based on the text content. + 3. **Auto Font Scale Mode:** The font within the background box automatically scales to fill the available space. + + An issue has been identified with ``TextBlock2D`` where its text actor aligns with the top boundary of the background actor, especially noticeable with letters like "g," "y," and "j". These letters extend beyond the baseline of standard alphabets, causing the text box to shift upwards. + + However, resolving this matter is complex. Adjusting the text's position might lead to it touching the bottom boundary, especially in font scale mode, resulting in unexpected positioning and transformations. To address this, the plan is to defer discussions about this matter until after GSoC, allowing for thorough consideration and solutions. + + For more detailed insights into the individual steps and nuances of this process, you can refer to the comprehensive weekly blog post provided below. It delves into the entire journey of this ``TextBlock2D`` refactoring effort. + + **Pull Requests:** + - **Fixing Justification Issue - 1st Draft (Closed)** - https://github.com/fury-gl/fury/pull/790 + - **Adding BoundingBox and fixing Justificaiton (Merged)** - https://github.com/fury-gl/fury/pull/803 + - **Adding getters and setter for properties (Merged)** - https://github.com/fury-gl/fury/pull/830 + - **Text Offset PR (Closed)** - https://github.com/fury-gl/fury/pull/837 + + + .. image:: https://user-images.githubusercontent.com/64432063/258603191-d540105a-0612-450e-8ae3-ca8aa87916e6.gif + :height: 500 + :align: center + :alt: TextBlock2D Feature Demo + + .. image:: https://github-production-user-asset-6210df.s3.amazonaws.com/64432063/254652569-94212105-7259-48da-8fdc-41ee987bda84.png + :height: 500 + :align: center + :alt: TextBlock2D All Justification + +- **ScrollbarUI as Independent Element:** + We initially planned to make the scrollbar independent based on PR `#16 `_. The main goal was to avoid redundancy by not rewriting the scrollbar code for each element that requires it, such as the ``FileMenu2D``. However, upon further analysis, we realized that elements like the ``FileMenu2D`` and others utilize the ``Listbox2D``, which already includes an integrated scrollbar. We also examined other UI libraries and found that they also have independent scrollbars but lack a proper use case. Typically, display containers like ``Listbox2D`` are directly used instead of utilizing an independent scrollbar. + + Based on these findings, we have decided to close all related issues and pull requests for now. If the need arises in the future, we can revisit this topic. + + **Topic:** - https://github.com/fury-gl/fury/discussions/816 + + +Other Objectives +---------------- + +- **Reviewing & Merging:** + In this phase, my focus was not on specific coding but rather on facilitating the completion of ongoing PRs. Here are two instances where I played a role: + + 1. **CardUI PR:** + I assisted with the ``CardUI`` PR by aiding in the rebase process and reviewing the changes. The CardUI is a simple UI element consisting of an image and a description, designed to function like a flash card. I worked closely with my mentor to ensure a smooth rebase and review process. + + 2. **ComboBox Issue:** + There was an issue with the ``ComboBox2D`` functionality, where adding it to a ``TabUI`` caused all elements to open simultaneously, which shouldn't be the case. I tested various PRs addressing this problem and identified a suitable solution. I then helped the lead in reviewing the PR that fixed the issue, which was successfully merged. + + **Pull Requests:** + - **CardUI (Merged)** - https://github.com/fury-gl/fury/pull/398 + - **ComboBox Flaw (Merged)** - https://github.com/fury-gl/fury/pull/768 + + + .. image:: https://user-images.githubusercontent.com/54466356/112532305-b090ef80-8dce-11eb-90a0-8d06eed55993.png + :height: 500 + :align: center + :alt: CardUI + + +- **Updating Broken Website Links:** + I addressed an issue with malfunctioning links in the Scientific Section of the website. The problem emerged from alterations introduced in PR `#769 `_. These changes consolidated demos and examples into a unified "auto_examples" folder, and a toml file was utilized to retrieve this data and construct examples. However, this led to challenges with the paths employed in website generation. My responsibility was to rectify these links, ensuring they accurately direct users to the intended content. + + **Pull Requests:** + - **Updating Broken Links (Merged)** - https://github.com/fury-gl/fury/pull/820 + + +Objectives in Progress +---------------------- + +- **FileDialogUI:** + An existing ``FileDialog`` PR by Soham (`#294 `_) was worked upon. The primary task was to rebase the PR to match the current UI structure, resolving compatibility concerns with the older base. In PR `#832 `_, we detailed issues encompassing resizing ``FileDialog`` and components, addressing text overflow, fixing ``ZeroDivisionError``, and correcting ``ListBox2D`` item positioning. The PR is complete with comprehensive testing and documentation. Presently, it's undergoing review, and upon approval, it will be prepared for integration. + + **Pull Requests:** + - **Soham's FileDialog (Closed)** - https://github.com/fury-gl/fury/pull/294 + - **FileDialogUI (Under Review)** - https://github.com/fury-gl/fury/pull/832 + + + .. image:: https://user-images.githubusercontent.com/64432063/263189092-6b0891d5-f0ef-4185-8b17-c7104f1a7d60.gif + :height: 500 + :align: center + :alt: FileDialogUI + + +- **TreeUI:** + Continuing Antriksh's initial PR for ``TreeUI`` posed some challenges. Antriksh had set the foundation, and I picked up from there. The main issue was with the visibility of TreeUI due to updates in the ``set_visibility`` method of ``Panel2D``. These updates affected how ``TreeUI`` was displayed, and after investigating the actors involved, it was clear that the visibility features had changed. This took some time to figure out, and I had a helpful pair programming session with my mentor, Serge, to narrow down the problem. Now, I've updated the code to address this issue. However, I'm still a bit cautious about potential future problems. The PR is now ready for review. + + **Pull Requests:** + - **TreeUI (In Progress)** - https://github.com/fury-gl/fury/pull/821 + + + .. image:: https://user-images.githubusercontent.com/64432063/263237308-70e77ba0-1ce8-449e-a79c-d5e0fbb58b45.gif + :height: 500 + :align: center + :alt: TreeUI + +GSoC Weekly Blogs +----------------- + +- My blog posts can be found at `FURY website `__ + and `Python GSoC blog `__. + +Timeline +-------- + +.. list-table:: + :widths: 40 40 20 + :header-rows: 1 + + * - Date + - Description + - Blog Post Link + * - Week 0 (27-05-2023) + - Community Bounding Period + - `FURY `_ - `Python `_ + * - Week 1 (03-06-2023) + - Working with SpinBox and TextBox Enhancements + - `FURY `_ - `Python `_ + * - Week 2 (10-06-2023) + - Tackling Text Justification and Icon Flaw Issues + - `FURY `_ - `Python `_ + * - Week 3 (17-06-2023) + - Resolving Combobox Icon Flaw and TextBox Justification + - `FURY `_ - `Python `_ + * - Week 4 (24-06-2023) + - Exam Preparations and Reviewing + - `FURY `_ - `Python `_ + * - Week 5 (01-07-2023) + - Trying out PRs and Planning Ahead + - `FURY `_ - `Python `_ + * - Week 6 (08-07-2023) + - BoundingBox for TextBlock2D! + - `FURY `_ - `Python `_ + * - Week 7 (15-07-2023) + - Sowing the seeds for TreeUI + - `FURY `_ - `Python `_ + * - Week 8 (22-07-2023) + - Another week with TextBlockUI + - `FURY `_ - `Python `_ + * - Week 9 (29-07-2023) + - TextBlock2D is Finally Merged! + - `FURY `_ - `Python `_ + * - Week 10 (05-08-2023) + - Its time for a Spin-Box! + - `FURY `_ - `Python `_ + * - Week 11 (12-08-2023) + - Bye Bye SpinBox + - `FURY `_ - `Python `_ + * - Week 12 (19-08-2023) + - FileDialog Quest Begins! + - `FURY `_ - `Python `_ diff --git a/docs/source/release-history.rst b/docs/source/release-history.rst index d46eb07ad..78a1e25b9 100644 --- a/docs/source/release-history.rst +++ b/docs/source/release-history.rst @@ -7,6 +7,7 @@ For a full list of the features implemented in the most recent release cycle, ch .. toctree:: :maxdepth: 1 + release_notes/releasev0.10.0 release_notes/releasev0.9.0 release_notes/releasev0.8.0 release_notes/releasev0.7.1 diff --git a/docs/source/release_notes/releasev0.10.0.rst b/docs/source/release_notes/releasev0.10.0.rst new file mode 100644 index 000000000..308082bfe --- /dev/null +++ b/docs/source/release_notes/releasev0.10.0.rst @@ -0,0 +1,177 @@ +.. _releasev0.10.0: + +=================================== + Release notes v0.10.0 (2024/02/28) +=================================== + +Quick Overview +-------------- + +* Uncertainty Visualization added. +* New actors added. +* Many UI components updated. +* Multiple tutorials added and updated. +* Documentation updated. +* Website updated. + + +Details +------- + +GitHub stats for 2023/04/15 - 2024/02/27 (tag: v0.9.0) + +These lists are automatically generated, and may be incomplete or contain duplicates. + +The following 11 authors contributed 382 commits. + +* Antriksh Misri +* Dwij Raj Hari +* Eleftherios Garyfallidis +* Joao Victor Dell Agli +* Maharshi Gor +* Praneeth Shetty +* Robin Roy +* Serge Koudoro +* Tania Castillo +* dependabot[bot] +* maharshigor + + +We closed a total of 129 issues, 54 pull requests and 75 regular issues; +this is the full list (generated with the script +:file:`tools/github_stats.py`): + +Pull Requests (54): + +* :ghpull:`810`: DTI uncertainty visualization +* :ghpull:`861`: Added/Modified docstrings for 3 actor.py functions +* :ghpull:`863`: UI Bug fixes for Horizon +* :ghpull:`866`: build(deps): bump the actions group with 6 updates +* :ghpull:`865`: Fix ci +* :ghpull:`845`: GSoC: Final Report +* :ghpull:`847`: GSoC: Adding Final Report 23 +* :ghpull:`848`: Added Final Report +* :ghpull:`852`: add Code of conduct +* :ghpull:`846`: Added blogpost week 12 +* :ghpull:`844`: GSoC: Week 12 Blogpost +* :ghpull:`843`: GSoC: Adding Week 12 Blogpost +* :ghpull:`842`: GSoC: Week 11 Blogpost +* :ghpull:`839`: GSoC: Adding Week 10 Blogpost +* :ghpull:`840`: Added blogposts week 8, 9, 10, 11 +* :ghpull:`841`: GSoC: Adding Week 11 Blogpost +* :ghpull:`831`: GSoC: Week 9 Blogpost +* :ghpull:`833`: GSoC: Adding Week 9 Blogpost +* :ghpull:`836`: GSoC: Week 10 Blogpost +* :ghpull:`499`: Adding `SpinBoxUI` to the `UI` module +* :ghpull:`818`: Tutorial on using ellipsoid actor to visualize tensor ellipsoids for DTI +* :ghpull:`834`: citation section added +* :ghpull:`830`: UI: Adding getters and setters for the `TextBlock2D` properties +* :ghpull:`829`: GSoC: Adding Week 8 Blogpost +* :ghpull:`828`: GSoC: Week 8 Blogpost +* :ghpull:`803`: UI: Adding Bounding Box & Fixing Alignment issue in TextBlock2D +* :ghpull:`814`: physics-simulation done +* :ghpull:`827`: Added blogpost week 4, 5, 6, 7 +* :ghpull:`822`: GSoC: Week 7 Blogpost +* :ghpull:`823`: GSoC: Adding Week 6 - 7 Blogpost +* :ghpull:`791`: Ellipsoid actor implemented with SDF +* :ghpull:`817`: GSoC: Adding Week 5 Blogpost +* :ghpull:`820`: Updating broken links in the Scientific Domain Section +* :ghpull:`819`: GSoC: Week 6 Blogpost +* :ghpull:`815`: Week 5 blogpost +* :ghpull:`812`: Feature/compatible software +* :ghpull:`811`: GSoC: Adding Week 4 Blogpost +* :ghpull:`809`: Week 4 Blogpost +* :ghpull:`807`: Added blogpost week 3 +* :ghpull:`806`: Week 3 Blogpost +* :ghpull:`805`: GSoC: Adding Week3 Blogpost +* :ghpull:`398`: feat: added a Card2D widget to UI +* :ghpull:`800`: Week2 Blogpost +* :ghpull:`802`: Added blogpost week 2 +* :ghpull:`801`: [fix] update deprecated Test +* :ghpull:`799`: Adding Week2 BlogPost +* :ghpull:`798`: Added blogpost week 1 +* :ghpull:`768`: Overload set_visibility for Panel2D and Combobox2D +* :ghpull:`797`: Week 1 blogpost +* :ghpull:`796`: Adding Week1 Blogpost +* :ghpull:`792`: Adding week 0 blogpost +* :ghpull:`789`: Added blogpost week 0 +* :ghpull:`788`: Adding Week0 Blogpost +* :ghpull:`629`: Release preparation 0.9.0 + +Issues (75): + +* :ghissue:`810`: DTI uncertainty visualization +* :ghissue:`861`: Added/Modified docstrings for 3 actor.py functions +* :ghissue:`863`: UI Bug fixes for Horizon +* :ghissue:`866`: build(deps): bump the actions group with 6 updates +* :ghissue:`864`: Missing files for sprite test +* :ghissue:`865`: Fix ci +* :ghissue:`845`: GSoC: Final Report +* :ghissue:`847`: GSoC: Adding Final Report 23 +* :ghissue:`848`: Added Final Report +* :ghissue:`425`: WIP: Cube Axes Actor. +* :ghissue:`852`: add Code of conduct +* :ghissue:`846`: Added blogpost week 12 +* :ghissue:`844`: GSoC: Week 12 Blogpost +* :ghissue:`843`: GSoC: Adding Week 12 Blogpost +* :ghissue:`842`: GSoC: Week 11 Blogpost +* :ghissue:`397`: Card2D UI widget +* :ghissue:`839`: GSoC: Adding Week 10 Blogpost +* :ghissue:`840`: Added blogposts week 8, 9, 10, 11 +* :ghissue:`841`: GSoC: Adding Week 11 Blogpost +* :ghissue:`837`: UI: Adding Text Offset to contain text into the Background +* :ghissue:`831`: GSoC: Week 9 Blogpost +* :ghissue:`833`: GSoC: Adding Week 9 Blogpost +* :ghissue:`836`: GSoC: Week 10 Blogpost +* :ghissue:`499`: Adding `SpinBoxUI` to the `UI` module +* :ghissue:`818`: Tutorial on using ellipsoid actor to visualize tensor ellipsoids for DTI +* :ghissue:`834`: citation section added +* :ghissue:`830`: UI: Adding getters and setters for the `TextBlock2D` properties +* :ghissue:`294`: File Dialog UI component +* :ghissue:`829`: GSoC: Adding Week 8 Blogpost +* :ghissue:`828`: GSoC: Week 8 Blogpost +* :ghissue:`803`: UI: Adding Bounding Box & Fixing Alignment issue in TextBlock2D +* :ghissue:`814`: physics-simulation done +* :ghissue:`827`: Added blogpost week 4, 5, 6, 7 +* :ghissue:`822`: GSoC: Week 7 Blogpost +* :ghissue:`823`: GSoC: Adding Week 6 - 7 Blogpost +* :ghissue:`825`: [WIP] KDE Rendering API +* :ghissue:`824`: [WIP] KDE Rendering API +* :ghissue:`791`: Ellipsoid actor implemented with SDF +* :ghissue:`817`: GSoC: Adding Week 5 Blogpost +* :ghissue:`820`: Updating broken links in the Scientific Domain Section +* :ghissue:`819`: GSoC: Week 6 Blogpost +* :ghissue:`815`: Week 5 blogpost +* :ghissue:`460`: [WIP] Adding `Tree2D` to the UI sub-module +* :ghissue:`592`: Creating ScrollBar as a separate UI element +* :ghissue:`285`: Separation of Scrollbars as a standalone API. +* :ghissue:`222`: Attempt to refactor scrolling in FileMenu2D +* :ghissue:`812`: Feature/compatible software +* :ghissue:`811`: GSoC: Adding Week 4 Blogpost +* :ghissue:`809`: Week 4 Blogpost +* :ghissue:`808`: sponsors added +* :ghissue:`807`: Added blogpost week 3 +* :ghissue:`806`: Week 3 Blogpost +* :ghissue:`805`: GSoC: Adding Week3 Blogpost +* :ghissue:`402`: ImageContainer2D renders RGB .png images in black and white +* :ghissue:`398`: feat: added a Card2D widget to UI +* :ghissue:`800`: Week2 Blogpost +* :ghissue:`802`: Added blogpost week 2 +* :ghissue:`801`: [fix] update deprecated Test +* :ghissue:`799`: Adding Week2 BlogPost +* :ghissue:`794`: FURY dependencies aren't accurate in the README +* :ghissue:`790`: Fixing `TextBlock2D` justification issue +* :ghissue:`798`: Added blogpost week 1 +* :ghissue:`576`: Resolving icon flaw in comboBox2D +* :ghissue:`731`: Clicking the tab of a ComboBox2D opens dropdown without changing icon +* :ghissue:`562`: drop_down_menu icon flaw in ComboBox2D +* :ghissue:`768`: Overload set_visibility for Panel2D and Combobox2D +* :ghissue:`797`: Week 1 blogpost +* :ghissue:`796`: Adding Week1 Blogpost +* :ghissue:`792`: Adding week 0 blogpost +* :ghissue:`789`: Added blogpost week 0 +* :ghissue:`787`: Segmentation Fault while plotting (diffusion tractography) images on a non-interactive remote cluster +* :ghissue:`788`: Adding Week0 Blogpost +* :ghissue:`448`: Added the watcher class to UI +* :ghissue:`774`: WIP: Double arrow actor and a few utility functions +* :ghissue:`629`: Release preparation 0.9.0 diff --git a/fury/__init__.py b/fury/__init__.py index 64153fa99..4ad440f81 100644 --- a/fury/__init__.py +++ b/fury/__init__.py @@ -8,11 +8,12 @@ def get_info(verbose=False): """Return dict describing the context of this package. Parameters - ------------ + ---------- pkg_path : str path containing __init__.py for package + Returns - ---------- + ------- context : dict with named parameters of interest diff --git a/fury/actor.py b/fury/actor.py index b8a4b440b..77c370863 100644 --- a/fury/actor.py +++ b/fury/actor.py @@ -10,7 +10,7 @@ from fury import layout from fury.actors.odf_slicer import OdfSlicerActor from fury.actors.peak import PeakActor -from fury.actors.tensor import tensor_ellipsoid +from fury.actors.tensor import double_cone, main_dir_uncertainty, tensor_ellipsoid from fury.colormap import colormap_lookup_table from fury.deprecator import deprecate_with_version, deprecated_params from fury.io import load_image @@ -2712,9 +2712,10 @@ def billboard( bb_type='spherical' ): """Create a billboard actor. -- + - Billboards are 2D elements placed in a 3D world. They offer possibility to draw different shapes/elements at the fragment shader level. + Parameters ---------- centers : ndarray, shape (N, 3) @@ -2744,6 +2745,7 @@ def billboard( Returns ------- billboard_actor: Actor + """ verts, faces = fp.prim_square() res = fp.repeat_primitive( @@ -3103,12 +3105,11 @@ class Container: """ def __init__(self, layout=layout.Layout()): - """ - - Parameters + """Parameters ---------- layout : ``fury.layout.Layout`` object Items of this container will be arranged according to `layout`. + """ self.layout = layout self._items = [] @@ -3136,6 +3137,7 @@ def add(self, *items, **kwargs): If True the items are added as-is, otherwise a shallow copy is made first. If you intend to reuse the items elsewhere you should set `borrow=False`. Default: True. + """ self._need_update = True @@ -3443,6 +3445,20 @@ def texture_update(texture_actor, arr): def _textured_sphere_source(theta=60, phi=60): + """Use vtkTexturedSphereSource to set the theta and phi. + + Parameters + ---------- + theta : int, optional + Set the number of points in the longitude direction. + phi : int, optional + Set the number of points in the latitude direction. + + Returns + ------- + tss : TexturedSphereSource + + """ tss = TexturedSphereSource() tss.SetThetaResolution(theta) tss.SetPhiResolution(phi) @@ -3451,7 +3467,24 @@ def _textured_sphere_source(theta=60, phi=60): def texture_on_sphere(rgb, theta=60, phi=60, interpolate=True): + """Map an RGB or RGBA texture on a sphere. + + Parameters + ---------- + rgb : ndarray + Input 2D RGB or RGBA array. Dtype should be uint8. + theta : int, optional + Set the number of points in the longitude direction. + phi : int, optional + Set the number of points in the latitude direction. + interpolate : bool, optional + Interpolate between grid centers. + + Returns + ------- + earthActor : Actor + """ tss = _textured_sphere_source(theta=theta, phi=phi) earthMapper = PolyDataMapper() earthMapper.SetInputConnection(tss.GetOutputPort()) @@ -3476,8 +3509,8 @@ def texture_2d(rgb, interp=False): ---------- rgb : ndarray Input 2D RGB or RGBA array. Dtype should be uint8. - interp : bool - Interpolate between grid centers. Default True. + interp : bool, optional + Interpolate between grid centers. Returns ------- @@ -3626,6 +3659,7 @@ def markers( edge_opacity=0.8, ): """Create a marker actor with different shapes. + Parameters ---------- centers : ndarray, shape (N, 3) @@ -3806,8 +3840,7 @@ def ellipsoid( scales=1.0, opacity=1.0 ): - """ - VTK actor for visualizing ellipsoids. + """VTK actor for visualizing ellipsoids. Parameters ---------- @@ -3829,7 +3862,6 @@ def ellipsoid( tensor_ellipsoid: Actor """ - if not isinstance(centers, np.ndarray): centers = np.array(centers) if centers.ndim == 1: @@ -3868,3 +3900,57 @@ def ellipsoid( return tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity) + +def uncertainty_cone( + evals, + evecs, + signal, + sigma, + b_matrix, + scales=.6, + opacity=1.0 +): + """VTK actor for visualizing the cone of uncertainty representing the + variance of the main direction of diffusion. + + Parameters + ---------- + evals : ndarray (3, ) or (N, 3) + Eigenvalues. + evecs : ndarray (3, 3) or (N, 3, 3) + Eigenvectors. + signal : 3D or 4D ndarray + Predicted signal. + sigma : ndarray + Standard deviation of the noise. + b_matrix : array (N, 7) + Design matrix for DTI. + scales : float or ndarray (N, ), optional + Cones of uncertainty size. + opacity : float, optional + Takes values from 0 (fully transparent) to 1 (opaque), default(1.0). + + Returns + ------- + double_cone: Actor + + """ + valid_mask = np.abs(evecs).max(axis=(-2, -1)) > 0 + indices = np.nonzero(valid_mask) + + evecs = evecs[indices] + evals = evals[indices] + signal = signal[indices] + + centers = np.asarray(indices).T + colors = np.array([107, 107, 107]) + + x, y, z = evecs.shape + if not isinstance(scales, np.ndarray): + scales = np.array(scales) + if scales.size == 1: + scales = np.repeat(scales, x) + + angles = main_dir_uncertainty(evals, evecs, signal, sigma, b_matrix) + + return double_cone(centers, evecs, angles, colors, scales, opacity) diff --git a/fury/actors/odf_slicer.py b/fury/actors/odf_slicer.py index 253ea9cf1..cf4315275 100644 --- a/fury/actors/odf_slicer.py +++ b/fury/actors/odf_slicer.py @@ -12,8 +12,7 @@ class OdfSlicerActor(Actor): - """ - VTK actor for visualizing slices of ODF field. + """VTK actor for visualizing slices of ODF field. Parameters ---------- @@ -49,6 +48,7 @@ class OdfSlicerActor(Actor): Optional SH to SF matrix for projecting `odfs` given in SH coefficients on the `sphere`. If None, then the input is assumed to be expressed in SF coefficients. + """ def __init__( @@ -108,14 +108,12 @@ def __init__( self.set_opacity(opacity) def set_opacity(self, opacity): - """ - Set opacity value of ODFs to display. + """Set opacity value of ODFs to display. """ self.GetProperty().SetOpacity(opacity) def display_extent(self, x1, x2, y1, y2, z1, z2): - """ - Set visible volume from x1 (inclusive) to x2 (inclusive), + """Set visible volume from x1 (inclusive) to x2 (inclusive), y1 (inclusive) to y2 (inclusive), z1 (inclusive) to z2 (inclusive). """ @@ -126,8 +124,7 @@ def display_extent(self, x1, x2, y1, y2, z1, z2): self._update_mapper() def slice_along_axis(self, slice_index, axis='zaxis'): - """ - Slice ODF field at given `slice_index` along axis + """Slice ODF field at given `slice_index` along axis in ['xaxis', 'yaxis', zaxis']. """ if axis == 'xaxis': @@ -161,8 +158,7 @@ def slice_along_axis(self, slice_index, axis='zaxis'): raise ValueError('Invalid axis name {0}.'.format(axis)) def display(self, x=None, y=None, z=None): - """ - Display a slice along x, y, or z axis. + """Display a slice along x, y, or z axis. """ if x is None and y is None and z is None: self.slice_along_axis(self.grid_shape[2] // 2) @@ -174,8 +170,7 @@ def display(self, x=None, y=None, z=None): self.slice_along_axis(z, 'zaxis') def update_sphere(self, vertices, faces, B): - """ - Dynamically change the sphere used for SH to SF projection. + """Dynamically change the sphere used for SH to SF projection. """ if self.B is None: raise ValueError("Can't update sphere when using " 'SF coefficients.') @@ -189,8 +184,7 @@ def update_sphere(self, vertices, faces, B): self._update_mapper() def _update_mapper(self): - """ - Map vtkPolyData to the actor. + """Map vtkPolyData to the actor. """ polydata = PolyData() @@ -215,24 +209,21 @@ def _update_mapper(self): self.mapper.SetInputData(polydata) def _get_odf_offsets(self, mask): - """ - Get the position of non-zero voxels inside `mask`. + """Get the position of non-zero voxels inside `mask`. """ if self.affine is not None: return self.w_pos[mask[self.indices]] return np.asarray(self.indices).T[mask[self.indices]] def _get_sphere_directions(self): - """ - Get the sphere directions onto which is projected the signal. + """Get the sphere directions onto which is projected the signal. """ if self.affine is not None: return self.w_verts return self.vertices def _get_sf(self, mask): - """ - Get SF coefficients inside `mask`. + """Get SF coefficients inside `mask`. """ # when odfs are expressed in SH coefficients if self.B is not None: @@ -246,8 +237,7 @@ def _get_sf(self, mask): return self.odfs[mask[self.indices]] def _get_all_vertices(self, offsets, sph_dirs, sf): - """ - Get array of all the vertices of the ODFs to display. + """Get array of all the vertices of the ODFs to display. """ if self.radial_scale: # apply SF amplitudes to all sphere @@ -261,16 +251,14 @@ def _get_all_vertices(self, offsets, sph_dirs, sf): ) def _get_all_faces(self, nb_odfs, nb_dirs): - """ - Get array of all the faces of the ODFs to display. + """Get array of all the faces of the ODFs to display. """ return np.tile(self.faces, (nb_odfs, 1)) + np.repeat( np.arange(nb_odfs) * nb_dirs, len(self.faces) ).reshape(-1, 1) def _generate_color_for_vertices(self, sf): - """ - Get array of all vertices colors. + """Get array of all vertices colors. """ if self.global_cm: if self.colormap is None: diff --git a/fury/actors/peak.py b/fury/actors/peak.py index 338a9c7b0..3798b400a 100644 --- a/fury/actors/peak.py +++ b/fury/actors/peak.py @@ -277,7 +277,6 @@ def min_centers(self): def _orientation_colors(points, cmap='rgb_standard'): """ - Parameters ---------- points : (N, 3) array or ndarray @@ -308,8 +307,7 @@ def _orientation_colors(points, cmap='rgb_standard'): def _peaks_colors_from_points(points, colors=None, points_per_line=2): - """ - Returns a VTK scalar array containing colors information for each one of + """Return a VTK scalar array containing colors information for each one of the peaks according to the policy defined by the parameter colors. Parameters @@ -379,9 +377,7 @@ def _peaks_colors_from_points(points, colors=None, points_per_line=2): def _points_to_vtk_cells(points, points_per_line=2): - """ - - Returns the VTK cell array for the peaks given the set of points + """Return the VTK cell array for the peaks given the set of points coordinates. Parameters diff --git a/fury/actors/tensor.py b/fury/actors/tensor.py index a3b473e57..c4478554b 100644 --- a/fury/actors/tensor.py +++ b/fury/actors/tensor.py @@ -3,12 +3,16 @@ import numpy as np from fury import actor -from fury.shaders import (attribute_to_actor, compose_shader, - import_fury_shader, shader_to_actor) +from fury.shaders import ( + attribute_to_actor, + compose_shader, + import_fury_shader, + shader_to_actor, +) + def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity): - """ - Visualize one or many Tensor Ellipsoids with different features. + """Visualize one or many Tensor Ellipsoids with different features. Parameters ---------- @@ -30,7 +34,6 @@ def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity): box_actor: Actor """ - box_actor = actor.box(centers, colors=colors, scales=scales) box_actor.GetMapper().SetVBOShiftScaleMethod(False) box_actor.GetProperty().SetOpacity(opacity) @@ -86,10 +89,10 @@ def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity): # Normalization n_evals = "evalsVSOutput = evals/(max(evals.x, max(evals.y, evals.z)));" - + # Values constraint to avoid incorrect visualizations evals = "evalsVSOutput = clamp(evalsVSOutput,0.05,1);" - + # Scaling matrix sc_matrix = \ """ @@ -100,7 +103,7 @@ def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity): # Rotation matrix rot_matrix = "mat3 R = mat3(evec1, evec2, evec3);" - + # Tensor matrix t_matrix = "tensorMatrix = inverse(R) * S * R;" @@ -136,7 +139,7 @@ def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity): */ float scFactor = min(evalsVSOutput.x, min(evalsVSOutput.y, evalsVSOutput.z)); - + /* The approximation of distance is calculated by stretching the space such that the ellipsoid becomes a sphere (multiplying by @@ -156,31 +159,210 @@ def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity): cast_ray = import_fury_shader(os.path.join( 'ray_marching', 'cast_ray.frag')) + # Importing the function that generates the ray components + ray_generation = import_fury_shader(os.path.join( + 'ray_marching', 'gen_ray.frag')) + # Importing Blinn-Phong model for lighting blinn_phong_model = import_fury_shader(os.path.join( 'lighting', 'blinn_phong_model.frag')) # Full fragment shader declaration fs_dec = compose_shader([fs_vars_dec, sd_sphere, sdf_map, - central_diffs_normal, cast_ray, + central_diffs_normal, cast_ray, ray_generation, blinn_phong_model]) shader_to_actor(box_actor, 'fragment', decl_code=fs_dec) - # Vertex in Model Coordinates. - point = "vec3 point = vertexMCVSOutput.xyz;" + ray_components = \ + """ + vec3 ro; vec3 rd; float t; + gen_ray(ro, rd, t); + """ - # Camera position in world space - ray_origin = "vec3 ro = (-MCVCMatrix[3] * MCVCMatrix).xyz;" + # Fragment shader output definition + # If surface is detected, color is assigned, otherwise, nothing is painted + frag_output_def = \ + """ + if(t < 20) + { + vec3 pos = ro + t * rd; + vec3 normal = centralDiffsNormals(pos, .0001); + // Light Direction + vec3 ld = normalize(ro - pos); + // Light Attenuation + float la = dot(ld, normal); + vec3 color = blinnPhongIllumModel(la, lightColor0, + diffuseColor, specularPower, specularColor, ambientColor); + fragOutput0 = vec4(color, opacity); + } + else + { + discard; + } + """ - ray_direction = "vec3 rd = normalize(point - ro);" + # Full fragment shader implementation + sdf_frag_impl = compose_shader([ray_components, frag_output_def]) - light_direction = "vec3 ld = normalize(ro - point);" + shader_to_actor(box_actor, 'fragment', impl_code=sdf_frag_impl, + block='light') + + return box_actor - ray_origin_update = "ro += point - ro;" - # Total distance traversed along the ray - distance = "float t = castRay(ro, rd);" +def double_cone(centers, axes, angles, colors, scales, opacity): + """Visualize one or many Double Cones with different features. + + Parameters + ---------- + centers : ndarray(N, 3) + Cone positions. + axes : ndarray (3, 3) or (N, 3, 3) + Axes of the cone. + angles : float or ndarray (N, ) + Angles of the cone. + colors : ndarray (N, 3) or tuple (3,) + R, G and B should be in the range [0, 1]. + scales : float or ndarray (N, ) + Cone size. + opacity : float + Takes values from 0 (fully transparent) to 1 (opaque). + + Returns + ------- + box_actor: Actor + + """ + box_actor = actor.box(centers, colors=colors, scales=scales) + box_actor.GetMapper().SetVBOShiftScaleMethod(False) + box_actor.GetProperty().SetOpacity(opacity) + + # Number of vertices that make up the box + n_verts = 8 + + big_centers = np.repeat(centers, n_verts, axis=0) + attribute_to_actor(box_actor, big_centers, 'center') + + big_scales = np.repeat(scales, n_verts, axis=0) + attribute_to_actor(box_actor, big_scales, 'scale') + + evec1 = np.array([item[0] for item in axes]) + evec2 = np.array([item[1] for item in axes]) + evec3 = np.array([item[2] for item in axes]) + + big_vectors_1 = np.repeat(evec1, n_verts, axis=0) + attribute_to_actor(box_actor, big_vectors_1, 'evec1') + big_vectors_2 = np.repeat(evec2, n_verts, axis=0) + attribute_to_actor(box_actor, big_vectors_2, 'evec2') + big_vectors_3 = np.repeat(evec3, n_verts, axis=0) + attribute_to_actor(box_actor, big_vectors_3, 'evec3') + + big_angles = np.repeat(np.array(angles, dtype=float), n_verts, axis=0) + attribute_to_actor(box_actor, big_angles, 'angle') + + # Start of shader implementation + + vs_dec = \ + """ + in vec3 center; + in float scale; + in vec3 evec1; + in vec3 evec2; + in vec3 evec3; + in float angle; + + out vec4 vertexMCVSOutput; + out vec3 centerMCVSOutput; + out float scaleVSOutput; + out mat3 rotationMatrix; + out float angleVSOutput; + """ + + # Variables assignment + v_assign = \ + """ + vertexMCVSOutput = vertexMC; + centerMCVSOutput = center; + scaleVSOutput = scale; + angleVSOutput = angle; + """ + + # Rotation matrix + rot_matrix = \ + """ + mat3 R = mat3(normalize(evec1), normalize(evec2), normalize(evec3)); + float a = radians(90); + mat3 rot = mat3(cos(a),-sin(a), 0, + sin(a), cos(a), 0, + 0 , 0, 1); + rotationMatrix = transpose(R) * rot; + """ + + vs_impl = compose_shader([v_assign, rot_matrix]) + + shader_to_actor(box_actor, 'vertex', decl_code=vs_dec, + impl_code=vs_impl) + + fs_vars_dec = \ + """ + in vec4 vertexMCVSOutput; + in vec3 centerMCVSOutput; + in float scaleVSOutput; + in mat3 rotationMatrix; + in float angleVSOutput; + + uniform mat4 MCVCMatrix; + """ + + # Importing the cone SDF + sd_cone = import_fury_shader(os.path.join('sdf', 'sd_cone.frag')) + + # Importing the union operation SDF + sd_union = import_fury_shader(os.path.join('sdf', 'sd_union.frag')) + + # SDF definition + sdf_map = \ + """ + float map(in vec3 position) + { + vec3 p = (position - centerMCVSOutput)/scaleVSOutput + *rotationMatrix; + float angle = clamp(angleVSOutput, 0, 6.283); + vec2 a = vec2(sin(angle), cos(angle)); + float h = .5 * a.y; + return opUnion(sdCone(p,a,h), sdCone(-p,a,h)) * scaleVSOutput; + } + """ + + # Importing central differences function for computing surface normals + central_diffs_normal = import_fury_shader(os.path.join( + 'sdf', 'central_diffs.frag')) + + # Importing raymarching function + cast_ray = import_fury_shader(os.path.join( + 'ray_marching', 'cast_ray.frag')) + + # Importing the function that generates the ray components + ray_generation = import_fury_shader(os.path.join( + 'ray_marching', 'gen_ray.frag')) + + # Importing Blinn-Phong model for lighting + blinn_phong_model = import_fury_shader(os.path.join( + 'lighting', 'blinn_phong_model.frag')) + + # Full fragment shader declaration + fs_dec = compose_shader([fs_vars_dec, sd_cone, sd_union, sdf_map, + central_diffs_normal, cast_ray, ray_generation, + blinn_phong_model]) + + shader_to_actor(box_actor, 'fragment', decl_code=fs_dec) + + ray_components = \ + """ + vec3 ro; vec3 rd; float t; + gen_ray(ro, rd, t); + """ # Fragment shader output definition # If surface is detected, color is assigned, otherwise, nothing is painted @@ -190,9 +372,11 @@ def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity): { vec3 pos = ro + t * rd; vec3 normal = centralDiffsNormals(pos, .0001); + // Light Direction + vec3 ld = normalize(ro - pos); // Light Attenuation float la = dot(ld, normal); - vec3 color = blinnPhongIllumModel(la, lightColor0, + vec3 color = blinnPhongIllumModel(la, lightColor0, diffuseColor, specularPower, specularColor, ambientColor); fragOutput0 = vec4(color, opacity); } @@ -203,11 +387,106 @@ def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity): """ # Full fragment shader implementation - sdf_frag_impl = compose_shader([point, ray_origin, ray_direction, - light_direction, ray_origin_update, - distance, frag_output_def]) + sdf_frag_impl = compose_shader([ray_components, frag_output_def]) shader_to_actor(box_actor, 'fragment', impl_code=sdf_frag_impl, block='light') return box_actor + + +def main_dir_uncertainty(evals, evecs, signal, sigma, b_matrix): + """Calculate the angle of the cone of uncertainty that represents the + perturbation of the main eigenvector of the diffusion tensor matrix. + + Parameters + ---------- + evals : ndarray (3, ) or (N, 3) + Eigenvalues. + evecs : ndarray (3, 3) or (N, 3, 3) + Eigenvectors. + signal : 3D or 4D ndarray + Predicted signal. + sigma : ndarray + Standard deviation of the noise. + b_matrix : array (N, 7) + Design matrix for DTI. + + Returns + ------- + angles: array + + Notes + ----- + The uncertainty calculation is based on first-order matrix perturbation + analysis described in [1]_. The idea is to estimate the variance of the + main eigenvector which corresponds to the main direction of diffusion, + directly from estimated D and its estimated covariance matrix + :math:`\Delta D` (see [2]_, equation 4). The angle :math:`\\Theta` + between the perturbed principal eigenvector of D, + :math:`\\epsilon_1+\\Delta\\epsilon_1`, and the estimated eigenvector + :math:`\\epsilon_1`, measures the angular deviation of the main fiber + direction and can be approximated by: + + .. math:: + \\Theta=tan^{-1}(\\|\\Delta\\epsilon_1\\|) + + Giving way to a graphical construct for displaying both the main + eigenvector of D and its associated uncertainty, with the so-called + "uncertainty cone". + + References + ---------- + .. [1] Basser, P. J. (1997). Quantifying errors in fiber direction and + diffusion tensor field maps resulting from MR noise. In 5th Scientific + Meeting of the ISMRM (Vol. 1740). + + .. [2] Chang, L. C., Koay, C. G., Pierpaoli, C., & Basser, P. J. (2007). + Variance of estimated DTI-derived parameters via first-order perturbation + methods. Magnetic Resonance in Medicine: An Official Journal of the + International Society for Magnetic Resonance in Medicine, 57(1), 141-149. + + """ + angles = np.ones(evecs.shape[0]) + for i in range(evecs.shape[0]): + sigma_e = np.diag(signal[i] / sigma ** 2) + k = np.dot(np.transpose(b_matrix), sigma_e) + sigma_ = np.dot(k, b_matrix) + + dd = np.diag(sigma_) + delta_DD = np.array([[dd[0], dd[3], dd[4]], + [dd[3], dd[1], dd[5]], + [dd[4], dd[5], dd[2]]]) + + # perturbation matrix of tensor D + try: + delta_D = np.linalg.inv(delta_DD) + except np.linalg.LinAlgError: + delta_D = delta_DD + + D_ = evecs + eigen_vals = evals[i] + + e1, e2, e3 = np.array(D_[i, :, 0]), np.array(D_[i, :, 1]), \ + np.array(D_[i, :, 2]) + lambda1, lambda2, lambda3 = eigen_vals[0], eigen_vals[1], eigen_vals[2] + + if lambda1 > lambda2 and lambda1 > lambda3: + # The perturbation of the eigenvector associated with the largest + # eigenvalue is given by + a = np.dot(np.outer(np.dot(e1, delta_D), np.transpose(e2)) / + (lambda1 - lambda2), e2) + b = np.dot(np.outer(np.dot(e1, delta_D), np.transpose(e3)) / + (lambda1 - lambda3), e3) + delta_e1 = a + b + + # The angle \theta between the perturbed principal eigenvector of D + theta = np.arctan(np.linalg.norm(delta_e1)) + angles[i] = theta + else: + # If the condition is not satisfied it means that there is not a + # predominant diffusion direction, hence the uncertainty will be + # higher and a default value close to pi/2 is assigned + theta = 1.39626 + + return angles diff --git a/fury/animation/animation.py b/fury/animation/animation.py index ac232e47e..afc2775f2 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -39,6 +39,7 @@ class Animation: motion_path_res : int, default: None the number of line segments used to visualizer the animation's motion path (visualizing position). + """ def __init__(self, actors=None, length=None, loop=True, motion_path_res=None): @@ -73,6 +74,7 @@ def update_duration(self): ------- float The duration of the animation. + """ if self._length is not None: self._duration = self._length @@ -92,6 +94,7 @@ def duration(self): ------- float The duration of the animation. + """ return self._duration @@ -103,6 +106,7 @@ def current_timestamp(self): ------- float The current time of the animation. + """ if self._timeline: return self._timeline.current_timestamp @@ -156,6 +160,7 @@ def _get_data(self): ------- dict: The animation data containing keyframes and interpolators. + """ return self._data @@ -171,6 +176,7 @@ def _get_attribute_data(self, attrib): ------- dict: The animation data for a specific attribute. + """ data = self._get_data() @@ -194,8 +200,8 @@ def get_keyframes(self, attrib=None): attrib: str, optional, default: None The name of the attribute. If None, all keyframes for all set attributes will be returned. - """ + """ data = self._get_data() if attrib is None: attribs = data.keys() @@ -230,8 +236,8 @@ def set_keyframe( The in tangent at that position for the cubic spline curve. out_tangent: ndarray, shape (1, M), optional The out tangent at that position for the cubic spline curve. - """ + """ attrib_data = self._get_attribute_data(attrib) keyframes = attrib_data.get('keyframes') @@ -279,6 +285,7 @@ def set_keyframes(self, attrib, keyframes): >>> 2: {'value': [3, 4, 5], 'in_cp': [1, 2, 3]}} >>> pos_keyframes = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} >>> Animation.set_keyframes('position', pos_keyframes) + """ for t, keyframe in keyframes.items(): if isinstance(keyframe, dict): @@ -295,6 +302,7 @@ def is_inside_scene_at(self, timestamp): bool True if the Animation is set to be inside the scene at the given timestamp. + Notes ----- If the parent Animation is set to be out of the scene at that time, all @@ -319,6 +327,7 @@ def add_to_scene_at(self, timestamp): ---------- timestamp: float Timestamp of the event. + """ if not self.is_interpolatable('in_scene'): self.set_keyframe('in_scene', timestamp, True) @@ -333,6 +342,7 @@ def remove_from_scene_at(self, timestamp): ---------- timestamp: float Timestamp of the event. + """ if not self.is_interpolatable('in_scene'): self.set_keyframe('in_scene', timestamp, False) @@ -385,8 +395,8 @@ def set_interpolator(self, attrib, interpolator, is_evaluator=False, **kwargs): >>> pos_fun = lambda t: np.array([np.sin(t), np.cos(t), 0]) >>> Animation.set_interpolator('position', pos_fun) - """ + """ attrib_data = self._get_attribute_data(attrib) keyframes = attrib_data.get('keyframes', {}) interp_data = attrib_data.get('interpolator', {}) @@ -449,6 +459,7 @@ def set_position_interpolator(self, interpolator, is_evaluator=False, **kwargs): Examples -------- >>> Animation.set_position_interpolator(spline_interpolator, degree=5) + """ self.set_interpolator( 'position', interpolator, is_evaluator=is_evaluator, **kwargs @@ -469,6 +480,7 @@ def set_scale_interpolator(self, interpolator, is_evaluator=False): Examples -------- >>> Animation.set_scale_interpolator(step_interpolator) + """ self.set_interpolator('scale', interpolator, is_evaluator=is_evaluator) @@ -487,6 +499,7 @@ def set_rotation_interpolator(self, interpolator, is_evaluator=False): Examples -------- >>> Animation.set_rotation_interpolator(slerp) + """ self.set_interpolator('rotation', interpolator, is_evaluator=is_evaluator) @@ -505,6 +518,7 @@ def set_color_interpolator(self, interpolator, is_evaluator=False): Examples -------- >>> Animation.set_color_interpolator(lab_color_interpolator) + """ self.set_interpolator('color', interpolator, is_evaluator=is_evaluator) @@ -519,9 +533,11 @@ def set_opacity_interpolator(self, interpolator, is_evaluator=False): is_evaluator: bool, optional Specifies whether the `interpolator` is time-only based evaluation function that does not depend on keyframes. + Examples -------- >>> Animation.set_opacity_interpolator(step_interpolator) + """ self.set_interpolator('opacity', interpolator, is_evaluator=is_evaluator) @@ -534,6 +550,7 @@ def get_value(self, attrib, timestamp): The attribute name. timestamp: float The timestamp to interpolate at. + """ value = ( self._data.get(attrib, {}).get('interpolator', {}).get('func')(timestamp) @@ -547,6 +564,7 @@ def get_current_value(self, attrib): ---------- attrib: str The attribute name. + """ return ( self._data.get(attrib) @@ -581,6 +599,7 @@ def set_position(self, timestamp, position, **kwargs): ----- `in_cp` and `out_cp` only needed when using the cubic bezier interpolation method. + """ self.set_keyframe('position', timestamp, position, **kwargs) @@ -598,6 +617,7 @@ def set_position_keyframes(self, keyframes): -------- >>> pos_keyframes = {1, np.array([0, 0, 0]), 3, np.array([50, 6, 6])} >>> Animation.set_position_keyframes(pos_keyframes) + """ self.set_keyframes('position', keyframes) @@ -616,6 +636,7 @@ def set_rotation(self, timestamp, rotation, **kwargs): ----- Euler rotations are executed by rotating first around Z then around X, and finally around Y. + """ no_components = len(np.array(rotation).flatten()) if no_components == 4: @@ -643,6 +664,7 @@ def set_rotation_as_vector(self, timestamp, vector, **kwargs): Timestamp of the keyframe vector: ndarray, shape(1, 3) Directional vector that describes the rotation. + """ quat = transform.Rotation.from_rotvec(vector).as_quat() self.set_keyframe('rotation', timestamp, quat, **kwargs) @@ -656,6 +678,7 @@ def set_scale(self, timestamp, scalar, **kwargs): Timestamp of the keyframe scalar: ndarray, shape(1, 3) Scale keyframe value associated with the timestamp. + """ self.set_keyframe('scale', timestamp, scalar, **kwargs) @@ -673,6 +696,7 @@ def set_scale_keyframes(self, keyframes): -------- >>> scale_keyframes = {1, np.array([1, 1, 1]), 3, np.array([2, 2, 3])} >>> Animation.set_scale_keyframes(scale_keyframes) + """ self.set_keyframes('scale', keyframes) @@ -685,6 +709,7 @@ def set_color(self, timestamp, color, **kwargs): Timestamp of the keyframe color: ndarray, shape(1, 3) Color keyframe value associated with the timestamp. + """ self.set_keyframe('color', timestamp, color, **kwargs) @@ -702,6 +727,7 @@ def set_color_keyframes(self, keyframes): -------- >>> color_keyframes = {1, np.array([1, 0, 1]), 3, np.array([0, 0, 1])} >>> Animation.set_color_keyframes(color_keyframes) + """ self.set_keyframes('color', keyframes) @@ -714,6 +740,7 @@ def set_opacity(self, timestamp, opacity, **kwargs): Timestamp of the keyframe opacity: ndarray, shape(1, 3) Opacity keyframe value associated with the timestamp. + """ self.set_keyframe('opacity', timestamp, opacity, **kwargs) @@ -735,6 +762,7 @@ def set_opacity_keyframes(self, keyframes): -------- >>> opacity = {1, np.array([1, 1, 1]), 3, np.array([2, 2, 3])} >>> Animation.set_scale_keyframes(opacity) + """ self.set_keyframes('opacity', keyframes) @@ -750,6 +778,7 @@ def get_position(self, t): ------- ndarray(1, 3): The interpolated position. + """ return self.get_value('position', t) @@ -767,6 +796,7 @@ def get_rotation(self, t, as_quat=False): ------- ndarray(1, 3): The interpolated rotation as Euler degrees by default. + """ rot = self.get_value('rotation', t) if len(rot) == 4: @@ -793,6 +823,7 @@ def get_scale(self, t): ------- ndarray(1, 3): The interpolated scale. + """ return self.get_value('scale', t) @@ -808,6 +839,7 @@ def get_color(self, t): ------- ndarray(1, 3): The interpolated color. + """ return self.get_value('color', t) @@ -823,6 +855,7 @@ def get_opacity(self, t): ------- ndarray(1, 1): The interpolated opacity. + """ return self.get_value('opacity', t) @@ -835,6 +868,7 @@ def add(self, item): ---------- item: Animation, vtkActor, list[Animation], or list[vtkActor] Actor/s to be animated by the Animation. + """ if isinstance(item, list): for a in item: @@ -854,6 +888,7 @@ def add_child_animation(self, animation): ---------- animation: Animation or list[Animation] Animation/s to be added. + """ if isinstance(animation, list): for a in animation: @@ -875,6 +910,7 @@ def add_actor(self, actor, static=False): Indicated whether the actor should be animated and controlled by the animation or just a static actor that gets added to the scene along with the Animation. + """ if isinstance(actor, list): for a in actor: @@ -906,7 +942,6 @@ def timeline(self, timeline): Parameters ---------- - timeline: Timeline The Timeline handling the current animation, None, if there is no associated Timeline. @@ -937,6 +972,7 @@ def parent_animation(self, parent_animation): ---------- parent_animation: Animation The parent Animation instance. + """ self._parent_animation = parent_animation @@ -948,6 +984,7 @@ def actors(self): ------- list: List of actors controlled by the Animation. + """ return self._actors @@ -959,6 +996,7 @@ def child_animations(self) -> 'list[Animation]': ------- list: List of child Animations of this Animation. + """ return self._animations @@ -971,6 +1009,7 @@ def add_static_actor(self, actor): ---------- actor: vtkActor or list(vtkActor) Static actor/s. + """ self.add_actor(actor, static=True) @@ -982,6 +1021,7 @@ def static_actors(self): ------- list: List of static actors. + """ return self._static_actors @@ -996,6 +1036,7 @@ def remove_actor(self, actor): ---------- actor: vtkActor Actor to be removed from the Animation. + """ self._actors.remove(actor) @@ -1011,6 +1052,7 @@ def loop(self): ------- bool Whether the animation in loop mode (True) or play one mode (False). + """ return self._loop @@ -1023,6 +1065,7 @@ def loop(self, loop): loop: bool The loop condition to be set. (True) to loop the animation, and (False) to play only once. + """ self._loop = loop @@ -1043,6 +1086,7 @@ def add_update_callback(self, callback, prop=None): ----- If no attribute name was provided, current time of the animation will be provided instead of current value for the callback. + """ if prop is None: self._general_callbacks.append(callback) @@ -1061,6 +1105,7 @@ def update_animation(self, time=None): time: float or int, optional, default: None The time to update animation at. If None, the animation will play without adding it to a Timeline. + """ has_handler = True if time is None: @@ -1169,6 +1214,7 @@ class CameraAnimation(Animation): motion_path_res : int, default: None the number of line segments used to visualizer the animation's motion path (visualizing position). + """ def __init__(self, camera=None, length=None, loop=True, motion_path_res=None): @@ -1195,7 +1241,6 @@ def camera(self, camera: Camera): Parameters ---------- - camera: Camera The camera to be animated @@ -1211,6 +1256,7 @@ def set_focal(self, timestamp, position, **kwargs): The time to interpolate opacity at. position: ndarray, shape(1, 3) The camera position + """ self.set_keyframe('focal', timestamp, position, **kwargs) @@ -1223,6 +1269,7 @@ def set_view_up(self, timestamp, direction, **kwargs): The time to interpolate at. direction: ndarray, shape(1, 3) The camera view-up direction + """ self.set_keyframe('view_up', timestamp, direction, **kwargs) @@ -1241,6 +1288,7 @@ def set_focal_keyframes(self, keyframes): -------- >>> focal_pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} >>> CameraAnimation.set_focal_keyframes(focal_pos) + """ self.set_keyframes('focal', keyframes) @@ -1259,6 +1307,7 @@ def set_view_up_keyframes(self, keyframes): -------- >>> view_ups = {0, np.array([1, 0, 0]), 3, np.array([0, 1, 0])} >>> CameraAnimation.set_view_up_keyframes(view_ups) + """ self.set_keyframes('view_up', keyframes) @@ -1279,6 +1328,7 @@ def get_focal(self, t): ----- The returned focal position does not necessarily reflect the current camera's focal position, but the expected one. + """ return self.get_value('focal', t) @@ -1299,6 +1349,7 @@ def get_view_up(self, t): ----- The returned focal position does not necessarily reflect the actual camera view up directional vector, but the expected one. + """ return self.get_value('view_up', t) @@ -1313,6 +1364,7 @@ def set_focal_interpolator(self, interpolator, is_evaluator=False): is_evaluator: bool, optional Specifies whether the `interpolator` is time-only based evaluation function that does not depend on keyframes. + """ self.set_interpolator('focal', interpolator, is_evaluator=is_evaluator) @@ -1327,6 +1379,7 @@ def set_view_up_interpolator(self, interpolator, is_evaluator=False): is_evaluator: bool, optional Specifies whether the `interpolator` is time-only based evaluation function that does not depend on keyframes. + """ self.set_interpolator('view_up', interpolator, is_evaluator=is_evaluator) @@ -1338,6 +1391,7 @@ def update_animation(self, time=None): time: float or int, optional, default: None The time to update the camera animation at. If None, the animation will play. + """ if self._camera is None: if self._scene: diff --git a/fury/animation/helpers.py b/fury/animation/helpers.py index 51f82bc55..1b553d36c 100644 --- a/fury/animation/helpers.py +++ b/fury/animation/helpers.py @@ -18,8 +18,8 @@ def get_previous_timestamp(timestamps, current_time, include_last=False): ------- float or int The previous timestamp - """ + """ for timestamp in timestamps[::-1] if include_last else timestamps[-2::-1]: if timestamp <= current_time: return timestamp @@ -43,6 +43,7 @@ def get_next_timestamp(timestamps, current_time, include_first=False): ------- float or int The next timestamp + """ for timestamp in timestamps[:] if include_first else timestamps[1:]: if timestamp > current_time: @@ -62,6 +63,7 @@ def get_timestamps_from_keyframes(keyframes): ------- ndarray Array of sorted timestamps extracted from the keyframes. + """ return np.sort(np.array(list(keyframes)), axis=None) @@ -78,6 +80,7 @@ def get_values_from_keyframes(keyframes): ------- ndarray Array of sorted values extracted from the keyframes. + """ return np.asarray( [keyframes.get(t, {}).get('value', None) for t in sorted(keyframes.keys())] @@ -100,6 +103,7 @@ def get_time_tau(t, t0, t1): ------- float The time tau + """ return 0 if t <= t0 else 1 if t >= t1 else (t - t0) / (t1 - t0) @@ -124,6 +128,7 @@ def lerp(v0, v1, t0, t1, t): ------- ndarray or float The interpolated value + """ if t0 == t1: return v0 @@ -144,5 +149,6 @@ def euclidean_distances(points): ------- list A List of euclidean distance between each consecutive points or values. + """ return [np.linalg.norm(x - y) for x, y in zip(points, points[1:])] diff --git a/fury/animation/interpolator.py b/fury/animation/interpolator.py index bd081b0e4..97260acea 100644 --- a/fury/animation/interpolator.py +++ b/fury/animation/interpolator.py @@ -102,8 +102,8 @@ def step_interpolator(keyframes): function The interpolation function that take time and return interpolated value at that time. - """ + """ timestamps = get_timestamps_from_keyframes(keyframes) def interpolate(t): @@ -129,6 +129,7 @@ def linear_interpolator(keyframes): function The interpolation function that take time and return interpolated value at that time. + """ timestamps = get_timestamps_from_keyframes(keyframes) is_single = len(keyframes) == 1 @@ -167,8 +168,8 @@ def cubic_bezier_interpolator(keyframes): ----- If no control points are set in the keyframes, The cubic Bézier interpolator will almost behave as a linear interpolator. - """ + """ timestamps = get_timestamps_from_keyframes(keyframes) for ts in timestamps: @@ -293,6 +294,7 @@ def hsv_color_interpolator(keyframes): See Also -------- color_interpolator + """ return color_interpolator(keyframes, rgb2hsv, hsv2rgb) @@ -303,6 +305,7 @@ def lab_color_interpolator(keyframes): See Also -------- color_interpolator + """ return color_interpolator(keyframes, rgb2lab, lab2rgb) @@ -313,6 +316,7 @@ def xyz_color_interpolator(keyframes): See Also -------- color_interpolator + """ return color_interpolator(keyframes, rgb2xyz, xyz2rgb) @@ -335,7 +339,6 @@ def tan_cubic_spline_interpolator(keyframes): value at that time. """ - timestamps = get_timestamps_from_keyframes(keyframes) for time in keyframes: data = keyframes.get(time) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index 1bfaa49bb..0a11d9698 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -1,11 +1,13 @@ import os +from time import perf_counter + import numpy as np -from fury.lib import WindowToImageFilter, RenderWindow, numpy_support +from PIL import Image + from fury import window from fury.animation.animation import Animation +from fury.lib import RenderWindow, WindowToImageFilter, numpy_support from fury.ui.elements import PlaybackPanel -from PIL import Image -from time import perf_counter class Timeline: @@ -28,6 +30,7 @@ class Timeline: its length from the animations that it controls automatically. loop : bool, optional Whether loop playing the timeline or play once. + """ def __init__(self, animations=None, playback_panel=False, loop=True, length=None): @@ -69,6 +72,7 @@ def update_duration(self): ------- float The duration of the Timeline. + """ if self._length is not None: self._duration = self._length @@ -88,6 +92,7 @@ def duration(self): ------- float The duration of the Timeline. + """ return self._duration @@ -186,6 +191,7 @@ def playing(self): ------- bool True if the Timeline is playing. + """ return self._playing @@ -211,7 +217,6 @@ def paused(self): True if the Timeline is paused. """ - return not self.playing and self._current_timestamp is not None @property @@ -222,6 +227,7 @@ def speed(self): ------- float The speed of the timeline's playback. + """ return self._speed @@ -251,6 +257,7 @@ def loop(self): bool Whether the playback is in loop mode (True) or play one mode (False). + """ return self._loop @@ -263,6 +270,7 @@ def loop(self, loop): loop: bool The loop condition to be set. (True) to loop the playback, and (False) to play only once. + """ self._loop = loop @@ -273,6 +281,7 @@ def has_playback_panel(self): Returns ------- bool: 'True' if the `Timeline` has a playback panel. otherwise, 'False' + """ return self.playback_panel is not None @@ -282,7 +291,7 @@ def record(self, fname=None, fps=30, speed=1.0, size=(900, 768), """Record the animation Parameters - ----------- + ---------- fname : str, optional The file name. Save a GIF file if name ends with '.gif', or mp4 video if name ends with'.mp4'. @@ -313,8 +322,8 @@ def record(self, fname=None, fps=30, speed=1.0, size=(900, 768), Notes ----- It's recommended to use 50 or 30 FPS while recording to a GIF file. - """ + """ ext = os.path.splitext(fname)[-1] mp4 = ext == '.mp4' @@ -407,6 +416,7 @@ def add_animation(self, animation): ---------- animation: Animation or list[Animation] or tuple[Animation] Animation/s to be added. + """ if isinstance(animation, (list, tuple)): [self.add_animation(anim) for anim in animation] @@ -425,6 +435,7 @@ def animations(self) -> 'list[Animation]': ------- list: List of Animations controlled by the timeline. + """ return self._animations diff --git a/fury/colormap.py b/fury/colormap.py index 6855d1d10..33ab4fc5c 100644 --- a/fury/colormap.py +++ b/fury/colormap.py @@ -60,7 +60,7 @@ def ss(na, nd): def boys2rgb(v): - """boys 2 rgb cool colormap + """Boys 2 rgb cool colormap Maps a given field of undirected lines (line field) to rgb colors using Boy's Surface immersion of the real projective @@ -79,24 +79,23 @@ def boys2rgb(v): the FURY Team. Thank you Cagatay for putting this online. Parameters - ------------ + ---------- v : array, shape (N, 3) of unit vectors (e.g., principal eigenvectors of tensor data) representing one of the two directions of the undirected lines in a line field. Returns - --------- + ------- c : array, shape (N, 3) matrix of rgb colors corresponding to the vectors given in V. Examples - ---------- - + -------- >>> from fury import colormap >>> v = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) >>> c = colormap.boys2rgb(v) - """ + """ if v.ndim == 1: x = v[0] y = v[1] @@ -638,6 +637,7 @@ def hex_to_rgb(color): def rgb2hsv(rgb): """RGB to HSV color space conversion. + Parameters ---------- rgb : (..., 3, ...) array_like @@ -655,6 +655,7 @@ def rgb2hsv(rgb): it can be found at: https://github.com/scikit-image/scikit-image/blob/main/skimage/color/colorconv.py This implementation might have been modified. + """ input_is_one_pixel = rgb.ndim == 1 if input_is_one_pixel: @@ -725,7 +726,6 @@ def hsv2rgb(hsv): This implementation might have been modified. """ - hi = np.floor(hsv[..., 0] * 6) f = hsv[..., 0] * 6 - hi p = hsv[..., 2] * (1 - hsv[..., 1]) @@ -875,6 +875,7 @@ def get_xyz_coords(illuminant, observer): observer : {"2", "10", "R"}, optional One of: 2-degree observer, 10-degree observer, or 'R' observer as in R function grDevices::convertColor. + Returns ------- out : array @@ -927,7 +928,6 @@ def xyz2lab(xyz, illuminant='D65', observer='2'): This implementation might have been modified. """ - xyz_ref_white = get_xyz_coords(illuminant, observer) # scale by CIE XYZ tristimulus values of the reference white point @@ -960,6 +960,7 @@ def lab2xyz(lab, illuminant='D65', observer='2'): The name of the illuminant (the function is NOT case-sensitive). observer : {"2", "10", "R"}, optional The aperture angle of the observer. + Returns ------- out : (..., 3, ...) ndarray diff --git a/fury/data/fetcher.py b/fury/data/fetcher.py index 330ae82e1..1b784a42d 100644 --- a/fury/data/fetcher.py +++ b/fury/data/fetcher.py @@ -183,6 +183,7 @@ def fetch_data(files, folder, data_size=None): Raises if the sha checksum of the file does not match the expected value. The downloaded file is not deleted when this error is raised. + """ if not os.path.exists(folder): print("Creating new folder %s" % (folder)) @@ -282,7 +283,7 @@ def fetcher(): async def _request(session, url): - """An asynchronous function to get the request data as json. + """Get the request data as json. Parameters ---------- @@ -295,6 +296,7 @@ async def _request(session, url): ------- response : dictionary The response of url request. + """ async with session.get(url) as response: if not response.status == 200: @@ -304,7 +306,7 @@ async def _request(session, url): async def _download(session, url, filename, size=None): - """An asynchronous function to download file from url. + """Download file from url. Parameters ---------- @@ -316,6 +318,7 @@ async def _download(session, url, filename, size=None): Name of the downloaded file (e.g. BoxTextured.gltf) size : int, optional Length of the content in bytes + """ if not os.path.exists(filename): print(f'Downloading: {filename}') @@ -332,7 +335,7 @@ async def _download(session, url, filename, size=None): async def _fetch_gltf(name, mode): - """An asynchronous function to fetch glTF samples. + """Fetch glTF samples. Parameters ---------- @@ -349,8 +352,8 @@ async def _fetch_gltf(name, mode): list of fetched all file names. folder : str Path to the fetched files. - """ + """ if name is None: name = ['BoxTextured', 'Duck', 'CesiumMilkTruck', 'CesiumMan'] @@ -411,6 +414,7 @@ def fetch_gltf(name=None, mode='glTF'): ------- filenames : tuple tuple of feteched filenames (list) and folder (str) path. + """ if platform.system().lower() == "windows": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -716,6 +720,7 @@ def read_viz_gltf(fname, mode='glTF'): ------- path : str Complete path of models. + """ folder = pjoin(fury_home, 'glTF') model = pjoin(folder, fname) @@ -731,13 +736,14 @@ def read_viz_gltf(fname, mode='glTF'): def list_gltf_sample_models(): - """Returns all model name from the glTF-samples repository + """Return all model name from the glTF-samples repository. Returns ------- model_names : list Lists the name of glTF sample from https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0 + """ DATA_DIR = pjoin(dirname(__file__), 'files') with open(pjoin(DATA_DIR, 'KhronosGltfSamples.json'), 'r') as f: diff --git a/fury/data/files/test_ui_combobox_2d.json b/fury/data/files/test_ui_combobox_2d.json index ec38facb7..31a74e353 100644 --- a/fury/data/files/test_ui_combobox_2d.json +++ b/fury/data/files/test_ui_combobox_2d.json @@ -1 +1 @@ -{"CharEvent": 0, "MouseMoveEvent": 695, "KeyPressEvent": 0, "KeyReleaseEvent": 0, "LeftButtonPressEvent": 13, "LeftButtonReleaseEvent": 13, "RightButtonPressEvent": 0, "RightButtonReleaseEvent": 0, "MiddleButtonPressEvent": 0, "MiddleButtonReleaseEvent": 0} \ No newline at end of file +{"CharEvent": 0, "MouseMoveEvent": 296, "KeyPressEvent": 0, "KeyReleaseEvent": 0, "LeftButtonPressEvent": 16, "LeftButtonReleaseEvent": 16, "RightButtonPressEvent": 0, "RightButtonReleaseEvent": 0, "MiddleButtonPressEvent": 0, "MiddleButtonReleaseEvent": 0} \ No newline at end of file diff --git a/fury/data/files/test_ui_combobox_2d.log.gz b/fury/data/files/test_ui_combobox_2d.log.gz index 95a0841d3..5c068917e 100644 Binary files a/fury/data/files/test_ui_combobox_2d.log.gz and b/fury/data/files/test_ui_combobox_2d.log.gz differ diff --git a/fury/deprecator.py b/fury/deprecator.py index d848680c4..0f3d81a1a 100644 --- a/fury/deprecator.py +++ b/fury/deprecator.py @@ -5,6 +5,7 @@ this file is copied (with minor modifications) from the Nibabel. https://github.com/nipy/nibabel. See COPYING file distributed along with the Nibabel package for the copyright and license terms. + """ import functools diff --git a/fury/gltf.py b/fury/gltf.py index 91df8508f..4d06bea3e 100644 --- a/fury/gltf.py +++ b/fury/gltf.py @@ -498,6 +498,7 @@ def transverse_channels(self, animation: gltflib.Animation, count: int): pygltflib animation object. count : int Animation count. + """ name = animation.name if name is None: @@ -533,6 +534,7 @@ def get_sampler_data(self, sampler: gltflib.Sampler, node_id: int, transform_typ sampler_data : dict dictionary of data containing timestamps, node transformations and interpolation type. + """ time_array = self.get_acc_data(sampler.input) transform_array = self.get_acc_data(sampler.output) @@ -562,6 +564,7 @@ def get_matrix_from_sampler( Containing previous animations with node as keys. sampler : gltflib.Sampler Sampler object for an animation channel. + """ time_array = self.get_acc_data(sampler.input) tran_array = self.get_acc_data(sampler.output) @@ -608,6 +611,7 @@ def get_skin_data(self, skin_id): List of bones in the skin. inv_bind_matrix : ndarray Numpy array containing inverse bind pose for each bone. + """ skin = self.gltf.skins[skin_id] inv_bind_matrix = self.get_acc_data(skin.inverseBindMatrices) @@ -630,6 +634,7 @@ def generate_tmatrix(self, transf, prop): ------- matrix : ndarray (4, 4) ransformation matrix of shape (4, 4) with respective transforms. + """ if prop == 'translation': matrix = transform.translate(transf) @@ -665,6 +670,7 @@ def transverse_animations( parent_bone_transform : ndarray (4, 4) Transformation matrix of the parent bone. (default=np.identity(4)) + """ deform = animation.get_value('transform', timestamp) new_deform = np.dot(parent_bone_deform, deform) @@ -698,6 +704,7 @@ def update_skin(self, animation): ---------- animation : Animation Animation object. + """ animation.update_animation() timestamp = animation.current_timestamp @@ -735,6 +742,7 @@ def initialize_skin(self, animation, bones=False, length=0.2): length : float Length of the bones. (default=0.2) + """ self.show_bones = bones if bones: @@ -756,6 +764,7 @@ def apply_skin_matrix(self, vertices, joint_matrices, actor_index=0): ------- vertices : ndarray Modified vertices. + """ clone = np.copy(vertices) weights = self.weights_0[actor_index] @@ -791,6 +800,7 @@ def transverse_bones(self, bone_id, channel_name, parent_animation: Animation): parent_animation : Animation The animation of the parent bone. Should be `root_animation` by default. + """ node = self.gltf.nodes[bone_id] animation = Animation() @@ -819,6 +829,7 @@ def skin_animation(self): ------- root_animations : Dict An animation containing all the child animations for bones. + """ root_animations = {} self._vertices = [utils.vertices_from_actor(act) for act in self.actors()] @@ -842,6 +853,7 @@ def get_joint_actors(self, length=0.5, with_transforms=False): with_transforms : bool (default = False) Applies respective transformations to bone. Bones will be at origin if set to `False`. + """ origin = np.zeros((3, 3)) parent_transforms = self.bone_tranforms @@ -865,6 +877,7 @@ def update_morph(self, animation): ---------- animation : Animation Animation object. + """ animation.update_animation() timestamp = animation.current_timestamp @@ -887,6 +900,7 @@ def apply_morph_vertices(self, vertices, weights, cnt): vertex. cnt : int Count of the actor. + """ clone = np.copy(vertices) target_vertices = np.copy(self.morph_vertices[cnt]) @@ -905,6 +919,7 @@ def morph_animation(self): root_animations : Dict A dictionary containing animations as values and animation name as keys. + """ animations = {} self._vertices = [utils.vertices_from_actor(act) for act in self.actors()] @@ -935,6 +950,7 @@ def get_animations(self): ------- animations: List List of animations containing actors. + """ actors = self.actors() interpolators = { @@ -1012,6 +1028,7 @@ def main_animation(self): main_animation : Animation A parent animation containing all child animations for simple animation. + """ main_animation = Animation() animations = self.get_animations() @@ -1029,6 +1046,7 @@ def export_scene(scene, filename='default.gltf'): FURY scene object. filename: str, optional Name of the model to be saved + """ gltf_obj = gltflib.GLTF2() name, extension = os.path.splitext(filename) @@ -1089,8 +1107,8 @@ def _connect_primitives(gltf, actor, buff_file, byteoffset, count, name): Offset size of a primitive count: int BufferView count after adding the primitive. - """ + """ polydata = actor.GetMapper().GetInput() colors = utils.colors_from_actor(actor) if colors is not None: @@ -1244,6 +1262,7 @@ def write_scene(gltf, nodes): Pygltflib GLTF2 object nodes: list List of node indices. + """ scene = gltflib.Scene() scene.nodes = nodes @@ -1261,6 +1280,7 @@ def write_node(gltf, mesh_id=None, camera_id=None): Mesh index camera_id: int, optional Camera index. + """ node = gltflib.Node() if mesh_id is not None: @@ -1279,6 +1299,7 @@ def write_mesh(gltf, primitives): Pygltflib GLTF2 object. primitives: list List of Primitive object. + """ mesh = gltflib.Mesh() for prim in primitives: @@ -1296,6 +1317,7 @@ def write_camera(gltf, camera): Pygltflib GLTF2 object. camera: vtkCamera scene camera. + """ orthographic = camera.GetParallelProjection() cam = gltflib.Camera() @@ -1340,6 +1362,7 @@ def get_prim(vertex, index, color, tcoord, normal, material, mode=4): ------- prim: Primitive pygltflib primitive object. + """ prim = gltflib.Primitive() attr = gltflib.Attributes() @@ -1366,6 +1389,7 @@ def write_material(gltf, basecolortexture: int, uri: str): BaseColorTexture index. uri: str BaseColorTexture uri. + """ material = gltflib.Material() texture = gltflib.Texture() @@ -1407,6 +1431,7 @@ def write_accessor( Maximum elements of an array min: ndarray, optional Minimum elements of an array + """ accessor = gltflib.Accessor() accessor.bufferView = bufferview @@ -1436,6 +1461,7 @@ def write_bufferview(gltf, buffer, byte_offset, byte_length, byte_stride=None): the buffer byte_stride: int, optional Byte stride of the bufferview. + """ buffer_view = gltflib.BufferView() buffer_view.buffer = buffer @@ -1456,6 +1482,7 @@ def write_buffer(gltf, byte_length, uri): Length of the buffer uri: str Path to the external `.bin` file. + """ buffer = gltflib.Buffer() buffer.uri = uri diff --git a/fury/interactor.py b/fury/interactor.py index 2960b0ca6..4cd3692ec 100644 --- a/fury/interactor.py +++ b/fury/interactor.py @@ -392,6 +392,7 @@ def add_callback(self, prop, event_type, callback, priority=0, args=[]): event_type : event code callback : function priority : int + """ def _callback(_obj, event_name): diff --git a/fury/io.py b/fury/io.py index 8d0dac3e6..5b64b8228 100644 --- a/fury/io.py +++ b/fury/io.py @@ -199,6 +199,7 @@ def load_text(file): ------- text: str Text contained in the file. + """ if not os.path.isfile(file): raise IOError('File {} does not exist.'.format(file)) @@ -325,7 +326,6 @@ def load_polydata(file_name): output : vtkPolyData """ - # Check if file actually exists if not os.path.isfile(file_name): raise FileNotFoundError(file_name) diff --git a/fury/layout.py b/fury/layout.py index 187ae7854..560415547 100644 --- a/fury/layout.py +++ b/fury/layout.py @@ -41,8 +41,7 @@ def __init__( dim=None, position_offset=(0, 0, 0), ): - """ - + """Initialize the grid layout. Parameters ---------- cell_padding : 2-tuple of float or float (optional) @@ -63,6 +62,7 @@ def __init__( `aspect_ratio` will be ignored. position_offset: tuple (optional) Offset the grid by some factor + """ self.cell_shape = cell_shape self.aspect_ratio = aspect_ratio @@ -88,7 +88,6 @@ def get_cells_shape(self, actors): The 2D shape (on the xy-plane) of every actors. """ - if self.cell_shape == 'rect': bounding_box_sizes = np.asarray(list(map(self.compute_sizes, actors))) cell_shape = np.max(bounding_box_sizes, axis=0)[:2] @@ -119,14 +118,17 @@ def get_cells_shape(self, actors): def compute_positions(self, actors): """Compute the 3D coordinates of some actors. The coordinates will lie on the xy-plane and form a 2D grid. + Parameters ---------- actors : list of `vtkProp3D` objects Actors to be layout in a grid manner. + Returns ------- list of 3-tuple The computed 3D coordinates of every actors. + """ shapes = self.get_cells_shape(actors) @@ -139,15 +141,17 @@ def compute_positions(self, actors): def compute_sizes(self, actor): """Compute the bounding box size of the actor/UI element + Parameters - --------- + ---------- actor: `vtkProp3D` or `UI` element Actor/UI element whose size is to be calculated + Returns ------- bounding box sizes: tuple - """ + """ if is_ui(actor): width, height = actor.size return (width, height, 0) @@ -159,7 +163,8 @@ class HorizontalLayout(GridLayout): """Provide functionalities for laying out actors in a horizontal layout.""" def __init__(self, cell_padding=0, cell_shape='rect'): - """ + """Initialize the Horizontal layout. + Parameters ---------- cell_padding : 2-tuple of float or float (optional) @@ -173,6 +178,7 @@ def __init__(self, cell_padding=0, cell_shape='rect'): 'square' ensures the cells are as wide as high. 'diagonal' ensures the content of the cells can be rotated without colliding with content of the neighboring cells. + """ super(HorizontalLayout, self).__init__( cell_padding=cell_padding, cell_shape=cell_shape @@ -181,14 +187,17 @@ def __init__(self, cell_padding=0, cell_shape='rect'): def compute_positions(self, actors): """Compute the 3D coordinates of some actors. The coordinates will lie on the xy-plane and form a horizontal stack. + Parameters ---------- actors : list of `vtkProp3D` objects Actors to be layout in a horizontal fashion. + Returns ------- list of 3-tuple The computed 3D coordinates of every actors. + """ positions = [ np.asarray([0, 0, 0]), @@ -209,7 +218,8 @@ class VerticalLayout(GridLayout): """Provide functionalities for laying out actors in a vertical stack.""" def __init__(self, cell_padding=0, cell_shape='rect'): - """ + """Initialize the Vertical layout. + Parameters ---------- cell_padding : 2-tuple of float or float (optional) @@ -223,6 +233,7 @@ def __init__(self, cell_padding=0, cell_shape='rect'): 'square' ensures the cells are as wide as high. 'diagonal' ensures the content of the cells can be rotated without colliding with content of the neighboring cells. + """ super(VerticalLayout, self).__init__( cell_padding=cell_padding, cell_shape=cell_shape @@ -230,14 +241,17 @@ def __init__(self, cell_padding=0, cell_shape='rect'): def compute_positions(self, actors): """Compute the 3D coordinates of some actors. + Parameters ---------- actors : list of `vtkProp3D` objects Actors to be layout in a vertical stack. + Returns ------- list of 3-tuple The computed 3D coordinates of every actors. + """ positions = [ np.asarray([0, 0, 0]), @@ -258,7 +272,8 @@ class XLayout(HorizontalLayout): """Provide functionalities for laying out actors along x-axis.""" def __init__(self, direction='x+', cell_padding=0, cell_shape='rect'): - """ + """Initialize the X layout. + Parameters ---------- direction: str, optional @@ -276,6 +291,7 @@ def __init__(self, direction='x+', cell_padding=0, cell_shape='rect'): 'square' ensures the cells are as wide as high. 'diagonal' ensures the content of the cells can be rotated without colliding with content of the neighboring cells. + """ self.direction = direction.lower() @@ -319,6 +335,7 @@ def compute_positions(self, actors): ------- list of 3-tuple The computed 3D coordinates of every actors. + """ if self.direction == 'x-': actors = actors[::-1] @@ -337,7 +354,8 @@ class YLayout(VerticalLayout): """Provide functionalities for laying out actors along y-axis.""" def __init__(self, direction='y+', cell_padding=0, cell_shape='rect'): - """ + """Initialize the Y layout. + Parameters ---------- direction: str, optional @@ -355,6 +373,7 @@ def __init__(self, direction='y+', cell_padding=0, cell_shape='rect'): 'square' ensures the cells are as wide as high. 'diagonal' ensures the content of the cells can be rotated without colliding with content of the neighboring cells. + """ self.direction = direction.lower() @@ -398,6 +417,7 @@ def compute_positions(self, actors): ------- list of 3-tuple The computed 3D coordinates of every actors. + """ if self.direction == 'y-': actors = actors[::-1] @@ -416,7 +436,8 @@ class ZLayout(GridLayout): """Provide functionalities for laying out actors along z-axis.""" def __init__(self, direction='z+', cell_padding=0, cell_shape='rect'): - """ + """Initialize the Z layout. + Parameters ---------- direction: str, optional @@ -434,6 +455,7 @@ def __init__(self, direction='z+', cell_padding=0, cell_shape='rect'): 'square' ensures the cells are as wide as high. 'diagonal' ensures the content of the cells can be rotated without colliding with content of the neighboring cells. + """ self.direction = direction.lower() @@ -486,6 +508,7 @@ def compute_positions(self, actors): ------- list of 3-tuple The computed 3D coordinates of every actors. + """ if self.direction == 'z-': actors = actors[::-1] diff --git a/fury/material.py b/fury/material.py index 34ea358f1..ff317a380 100644 --- a/fury/material.py +++ b/fury/material.py @@ -1,10 +1,13 @@ import os import warnings - -from fury.shaders import (add_shader_callback, compose_shader, - import_fury_shader, shader_to_actor) from fury.lib import VTK_OBJECT, calldata_type +from fury.shaders import ( + add_shader_callback, + compose_shader, + import_fury_shader, + shader_to_actor, +) class __PBRParams: @@ -41,7 +44,9 @@ class __PBRParams: coat_ior : float Index of refraction of the coat material. Default is 1.5. Values must be between 1.0 and 2.3. + """ + def __init__(self, actor_properties, metallic, roughness, anisotropy, anisotropy_rotation, coat_strength, coat_roughness, base_ior, coat_ior): @@ -215,7 +220,6 @@ def manifest_principled(actor, subsurface=0, metallic=0, specular=0, Dictionary containing the Principled Shading parameters. """ - try: prop = actor.GetProperty() @@ -263,8 +267,7 @@ def uniforms_callback(_caller, _event, calldata=None): pi = '#define PI 3.14159265359' # Adding uniforms - uniforms = \ - """ + uniforms = """ uniform float subsurface; uniform float metallic; uniform float specularTint; @@ -274,7 +277,7 @@ def uniforms_callback(_caller, _event, calldata=None): uniform float sheenTint; uniform float clearcoat; uniform float clearcoatGloss; - + uniform vec3 anisotropicDirection; """ @@ -388,8 +391,7 @@ def uniforms_callback(_caller, _event, calldata=None): # calculate one single dot product dot_n_v = 'float dotNV = clamp(dot(normal, view), 1e-5, 1);' - dot_n_v_validation = \ - """ + dot_n_v_validation = """ if(dotNV < 0) fragOutput0 = vec4(vec3(0), opacity); """ @@ -400,8 +402,7 @@ def uniforms_callback(_caller, _event, calldata=None): bitangent = 'vec3 bitangent = vec3(.0);' # The shader function updateTanBitan aligns tangents and bitangents # according to a direction of anisotropy - update_aniso_vecs = \ - """ + update_aniso_vecs = """ updateTanBitan(normal, anisotropicDirection, tangent, bitangent); """ @@ -420,37 +421,32 @@ def uniforms_callback(_caller, _event, calldata=None): fsw = 'float fsw = schlickWeight(dotNV);' # Calculating the diffuse coefficient - diff_coeff = \ - """ + diff_coeff = """ float diffCoeff = evaluateDiffuse(roughness, fsw, fsw, dotNV); """ # Calculating the subsurface coefficient - subsurf_coeff = \ - """ - float subsurfCoeff = evaluateSubsurface(roughness, fsw, fsw, dotNV, + subsurf_coeff = """ + float subsurfCoeff = evaluateSubsurface(roughness, fsw, fsw, dotNV, dotNV, dotNV); """ # Calculating the sheen irradiance - sheen_rad = \ - """ + sheen_rad = """ vec3 sheenRad = evaluateSheen(sheen, sheenTint, tint, fsw); """ # Calculating the specular irradiance - spec_rad = \ - """ - vec3 specRad = evaluateSpecularAnisotropic(specularIntensity, - specularTint, metallic, anisotropic, roughness, tint, linColor, - fsw, dotNV, dotTV, dotBV, dotNV, dotTV, dotBV, dotNV, dotTV, + spec_rad = """ + vec3 specRad = evaluateSpecularAnisotropic(specularIntensity, + specularTint, metallic, anisotropic, roughness, tint, linColor, + fsw, dotNV, dotTV, dotBV, dotNV, dotTV, dotBV, dotNV, dotTV, dotBV); """ # Calculating the clear coat coefficient - clear_coat_coef = \ - """ - float coatCoeff = evaluateClearcoat(clearcoat, clearcoatGloss, fsw, + clear_coat_coef = """ + float coatCoeff = evaluateClearcoat(clearcoat, clearcoatGloss, fsw, dotNV, dotNV, dotNV); """ @@ -560,4 +556,3 @@ def manifest_standard(actor, ambient_level=0, ambient_color=(1, 1, 1), warnings.warn('Actor does not have the attribute property. This ' 'material will not be applied.') return - diff --git a/fury/molecular.py b/fury/molecular.py index 53648051e..eb56c8574 100644 --- a/fury/molecular.py +++ b/fury/molecular.py @@ -92,6 +92,7 @@ def __init__( atoms present in the molecule. Array containing a bool value to indicate if an atom is a heteroatom. + """ if atomic_numbers is None and coords is None: self.Initialize() @@ -147,6 +148,7 @@ def add_atom(molecule, atomic_num, x_coord, y_coord, z_coord): y-coordinate of the atom. z_coord : float z-coordinate of the atom. + """ molecule.AppendAtom(atomic_num, x_coord, y_coord, z_coord) @@ -171,6 +173,7 @@ def add_bond(molecule, atom1_index, atom2_index, bond_order=1): Ensure that the total number of bonds between two atoms doesn't exceed 3. Calling ``add_bond`` to add bonds between atoms that already have a triple bond between them leads to erratic behavior and must be avoided. + """ molecule.AppendBond(atom1_index, atom2_index, bond_order) @@ -186,6 +189,7 @@ def get_atomic_number(molecule, atom_index): The molecule to which the atom belongs. atom_index : int Index of the atom whose atomic number is to be obtained. + """ return molecule.GetAtomAtomicNumber(atom_index) @@ -204,6 +208,7 @@ def set_atomic_number(molecule, atom_index, atomic_num): Index of the atom to whom the atomic number is to be assigned. atom_num : int Atomic number to be assigned to the atom. + """ molecule.SetAtomAtomicNumber(atom_index, atomic_num) @@ -219,6 +224,7 @@ def get_atomic_position(molecule, atom_index): The molecule to which the atom belongs. atom_index : int Index of the atom whose atomic coordinates are to be obtained. + """ return molecule.GetAtomPosition(atom_index) @@ -241,6 +247,7 @@ def set_atomic_position(molecule, atom_index, x_coord, y_coord, z_coord): y-coordinate of the atom. z_coord : float z-coordinate of the atom. + """ molecule.SetAtomPosition(atom_index, x_coord, y_coord, z_coord) @@ -257,6 +264,7 @@ def get_bond_order(molecule, bond_index): The molecule to which the bond belongs. bond_index : int Index of the bond whose order is to be obtained. + """ return molecule.GetBondOrder(bond_index) @@ -275,6 +283,7 @@ def set_bond_order(molecule, bond_index, bond_order): Index of the bond whose order is to be assigned. bond_order : int Bond order (whether it's a single/double/triple bond). + """ return molecule.SetBondOrder(bond_index, bond_order) @@ -287,6 +296,7 @@ def get_all_atomic_numbers(molecule): ---------- molecule : Molecule The molecule whose atomic number array is to be obtained. + """ return nps.vtk_to_numpy(molecule.GetAtomicNumberArray()) @@ -299,6 +309,7 @@ def get_all_bond_orders(molecule): ---------- molecule : Molecule The molecule whose bond types array is to be obtained. + """ return nps.vtk_to_numpy(molecule.GetBondOrdersArray()) @@ -311,13 +322,13 @@ def get_all_atomic_positions(molecule): ---------- molecule : Molecule The molecule whose atomic position array is to be obtained. + """ return nps.vtk_to_numpy(molecule.GetAtomicPositionArray().GetData()) def deep_copy_molecule(molecule1, molecule2): - """ - Deep copies the atomic information (atoms and bonds) from molecule2 into + """Deep copies the atomic information (atoms and bonds) from molecule2 into molecule1. Parameters @@ -326,13 +337,13 @@ def deep_copy_molecule(molecule1, molecule2): The molecule to which the atomic information is copied. molecule2 : Molecule The molecule from which the atomic information is copied. + """ molecule1.DeepCopyStructure(molecule2) def compute_bonding(molecule): - """ - Uses `vtkSimpleBondPerceiver` to generate bonding information for a + """Uses `vtkSimpleBondPerceiver` to generate bonding information for a molecule. `vtkSimpleBondPerceiver` performs a simple check of all interatomic distances and adds a single bond between atoms that are reasonably @@ -349,6 +360,7 @@ def compute_bonding(molecule): This algorithm does not consider valences, hybridization, aromaticity, or anything other than atomic separations. It will not produce anything other than single bonds. + """ bonder = SimpleBondPerceiver() bonder.SetInputData(molecule) @@ -376,6 +388,7 @@ def atomic_symbol(self, atomic_number): ---------- atomic_number : int Atomic number of the element whose symbol is to be obtained. + """ return self.GetSymbol(atomic_number) @@ -386,6 +399,7 @@ def element_name(self, atomic_number): ---------- atomic_number : int Atomic number of the element whose name is to be obtained. + """ return self.GetElementName(atomic_number) @@ -397,6 +411,7 @@ def atomic_number(self, element_name): ---------- element_name : string Name of the element whose atomic number is to be obtained. + """ return self.GetAtomicNumber(element_name) @@ -416,6 +431,7 @@ def atomic_radius(self, atomic_number, radius_type='VDW'): * 'Covalent' : for covalent radius of the atom Default: 'VDW' + """ radius_type = radius_type.lower() if radius_type == 'vdw': @@ -437,6 +453,7 @@ def atom_color(self, atomic_number): ---------- atomicNumber : int Atomic number of the element whose RGB tuple is to be obtained. + """ rgb = np.array(self.GetDefaultRGBTuple(atomic_number)) return rgb @@ -472,6 +489,7 @@ def sphere_cpk(molecule, colormode='discrete'): Peptides, and Proteins `Review of Scientific Instruments 1953, 24 (8), 621-627. `_ + """ colormode = colormode.lower() msp_mapper = OpenGLMoleculeMapper() @@ -543,6 +561,7 @@ def ball_stick( Turner, M. Ball and stick models for organic chemistry `J. Chem. Educ. 1971, 48, 6, 407. `_ + """ if molecule.total_num_bonds == 0: raise ValueError( @@ -602,6 +621,7 @@ def stick(molecule, colormode='discrete', bond_thickness=0.1): molecule_actor : vtkActor Actor created to render the stick representation of the molecule to be visualized. + """ if molecule.total_num_bonds == 0: raise ValueError( @@ -648,6 +668,7 @@ def ribbon(molecule): Richardson, J.S. The anatomy and taxonomy of protein structure `Advances in Protein Chemistry, 1981, 34, 167-339. `_ + """ coords = get_all_atomic_positions(molecule) all_atomic_numbers = get_all_atomic_numbers(molecule) diff --git a/fury/pick.py b/fury/pick.py index a31139978..35d94ec06 100644 --- a/fury/pick.py +++ b/fury/pick.py @@ -20,7 +20,7 @@ def __init__(self, vertices=True, faces=True, actors=True, world_coords=True): """Initialize Picking Manager. Parameters - ----------- + ---------- vertices : bool If True allows to pick vertex indices. faces : bool @@ -124,7 +124,7 @@ def __init__(self, select='faces'): """Initialize Selection Manager. Parameters - ----------- + ---------- select : 'faces' Options are 'faces', 'vertices' or 'actors'. Default 'faces'. @@ -142,7 +142,7 @@ def update_selection_type(self, select): """Update selection type. Parameters - ----------- + ---------- select : 'faces' Options are 'faces', 'vertices' or 'actors'. Default 'faces'. diff --git a/fury/pkg_info.py b/fury/pkg_info.py index ab39f4b87..82e320b78 100644 --- a/fury/pkg_info.py +++ b/fury/pkg_info.py @@ -38,6 +38,7 @@ def pkg_commit_hash(pkg_path: str | None = None) -> tuple[str, str]: Where we got the hash from - description hash_str : str short form of hash + """ if not COMMIT_HASH.startswith('$Format'): # it has been substituted return 'archive substitution', COMMIT_HASH diff --git a/fury/primitive.py b/fury/primitive.py index b896d1485..bd8fa7669 100644 --- a/fury/primitive.py +++ b/fury/primitive.py @@ -24,8 +24,7 @@ def faces_from_sphere_vertices(vertices): - """ - Triangulate a set of vertices on the sphere. + """Triangulate a set of vertices on the sphere. Parameters ---------- @@ -311,6 +310,7 @@ def prim_sphere(name='symmetric362', gen_faces=False, phi=None, theta=None): Set the number of points in the latitude direction theta : int, optional Set the number of points in the longitude direction + Returns ------- vertices: ndarray @@ -933,8 +933,8 @@ def prim_cylinder(radius=0.5, height=1, sectors=36, capped=True): vertices coords that compose our cylinder triangles: ndarray triangles that compose our cylinder - """ + """ if not isinstance(sectors, int): raise TypeError('Only integers are allowed for sectors parameter') if not sectors > 7: @@ -1066,7 +1066,6 @@ def prim_arrow( Triangles of the Arrow """ - shaft_height = height - tip_length all_faces = [] @@ -1151,7 +1150,6 @@ def prim_cone(radius=0.5, height=1, sectors=10): triangles that compose our cone """ - if sectors < 3: raise ValueError('Sectors parameter should be greater than 2') diff --git a/fury/shaders/base.py b/fury/shaders/base.py index 6caec9f5c..cb9b8b82b 100644 --- a/fury/shaders/base.py +++ b/fury/shaders/base.py @@ -126,6 +126,7 @@ def load_shader(shader_file): ------- code : str GLSL shader code. + """ file_ext = os.path.splitext(os.path.basename(shader_file))[1] if file_ext not in SHADERS_EXTS: @@ -291,7 +292,7 @@ def add_shader_callback(actor, callback, priority=0.0): See more at: https://vtk.org/doc/nightly/html/classvtkObject.html Examples - --------- + -------- .. code-block:: python add_shader_callback(actor, func_call1) diff --git a/fury/shaders/ray_marching/gen_ray.frag b/fury/shaders/ray_marching/gen_ray.frag new file mode 100644 index 000000000..1ddd92801 --- /dev/null +++ b/fury/shaders/ray_marching/gen_ray.frag @@ -0,0 +1,17 @@ +void gen_ray(out vec3 ro, out vec3 rd, out float t) +{ + // Vertex in Model Coordinates + vec3 point = vertexMCVSOutput.xyz; + + // Ray Origin + // Camera position in world space + ro = (-MCVCMatrix[3] * MCVCMatrix).xyz; + + // Ray Direction + rd = normalize(point - ro); + + ro += point - ro; + + // Total distance traversed along the ray + t = castRay(ro, rd); +} diff --git a/fury/shaders/sdf/sd_cone.frag b/fury/shaders/sdf/sd_cone.frag new file mode 100644 index 000000000..678e5107d --- /dev/null +++ b/fury/shaders/sdf/sd_cone.frag @@ -0,0 +1,15 @@ +float sdCone( vec3 p, vec2 c, float h ) +{ + // c is the sin/cos of the angle, h is height + // Alternatively pass q instead of (c,h), + // which is the point at the base in 2D + vec2 q = h*vec2(c.x/c.y,-1.0); + + vec2 w = vec2( length(p.xz), p.y ); + vec2 a = w - q*clamp( dot(w,q)/dot(q,q), 0.0, 1.0 ); + vec2 b = w - q*vec2( clamp( w.x/q.x, 0.0, 1.0 ), 1.0 ); + float k = sign( q.y ); + float d = min(dot( a, a ),dot(b, b)); + float s = max( k*(w.x*q.y-w.y*q.x),k*(w.y-q.y) ); + return sqrt(d)*sign(s); +} diff --git a/fury/shaders/sdf/sd_union.frag b/fury/shaders/sdf/sd_union.frag new file mode 100644 index 000000000..6bcfe8903 --- /dev/null +++ b/fury/shaders/sdf/sd_union.frag @@ -0,0 +1,4 @@ +float opUnion( float d1, float d2 ) +{ + return min(d1,d2); +} diff --git a/fury/stream/client.py b/fury/stream/client.py index aac565e35..2d73e7a4e 100644 --- a/fury/stream/client.py +++ b/fury/stream/client.py @@ -55,8 +55,7 @@ def __init__( whithout_iren_start=False, num_buffers=2, ): - """ - A StreamClient extracts a framebuffer from the OpenGL context + """A StreamClient extracts a framebuffer from the OpenGL context and writes into a shared memory resource. Parameters @@ -77,7 +76,6 @@ def __init__( technique. """ - self._whithout_iren_start = whithout_iren_start self.showm = showm self.window2image_filter = vtk.vtkWindowToImageFilter() @@ -275,7 +273,7 @@ class FuryStreamInteraction: def __init__( self, showm, max_queue_size=50, use_raw_array=True, whithout_iren_start=False ): - """ + """Initialize the StreamInteraction obj. Parameters ---------- @@ -291,7 +289,6 @@ def __init__( instance. """ - self.showm = showm self.iren = self.showm.iren if use_raw_array: @@ -321,7 +318,6 @@ def start(self, ms=3, use_asyncio=False): separate thread. """ - use_asyncio = platform.system() == 'Windows' or use_asyncio if ms <= 0: raise ValueError('ms must be greater than zero') diff --git a/fury/stream/server/async_app.py b/fury/stream/server/async_app.py index 2b07222d7..90dae87fa 100644 --- a/fury/stream/server/async_app.py +++ b/fury/stream/server/async_app.py @@ -47,8 +47,8 @@ async def mjpeg_handler(request): """This async function it's responsible to create the MJPEG streaming. - Notes: - ------ + Notes + ----- endpoint : /video/mjpeg """ @@ -147,8 +147,7 @@ def set_mouse(data, circular_queue): def set_mouse_click(data, circular_queue): - """ - 3 | LeftButtonPressEvent + """3 | LeftButtonPressEvent 4 | LeftButtonReleaseEvent 5 | MiddleButtonPressEvent 6 | MiddleButtonReleaseEvent diff --git a/fury/stream/server/main.py b/fury/stream/server/main.py index f9e1352eb..40e485fc8 100644 --- a/fury/stream/server/main.py +++ b/fury/stream/server/main.py @@ -48,7 +48,7 @@ def __init__( self, image_buffer_manager, ): - """ + """Initialize the RTCServer Parameters ---------- @@ -163,7 +163,6 @@ def web_server_raw_array( is used just to be able to test the server. """ - image_buffer_manager = RawArrayImageBufferManager( image_buffers=image_buffers, info_buffer=info_buffer ) @@ -258,7 +257,6 @@ def web_server( is used just to be able to test the server. """ - if avoid_unlink_shared_mem and PY_VERSION_8: remove_shm_from_resource_tracker() diff --git a/fury/stream/tools.py b/fury/stream/tools.py index 0362f0823..fe6d48c10 100644 --- a/fury/stream/tools.py +++ b/fury/stream/tools.py @@ -66,7 +66,7 @@ class GenericMultiDimensionalBuffer(ABC): """This implements a abstract (generic) multidimensional buffer.""" def __init__(self, max_size=None, dimension=8): - """ + """Initialize the multidimensional buffer. Parameters ---------- @@ -133,9 +133,7 @@ class RawArrayMultiDimensionalBuffer(GenericMultiDimensionalBuffer): """This implements a multidimensional buffer with RawArray.""" def __init__(self, max_size, dimension=4, buffer=None): - """ - - Stream system uses that to implement the CircularQueue + """Stream system uses that to implement the CircularQueue with shared memory resources. Parameters @@ -180,12 +178,11 @@ def cleanup(self): class SharedMemMultiDimensionalBuffer(GenericMultiDimensionalBuffer): """This implements a generic multidimensional buffer - with SharedMemory.""" + with SharedMemory. + """ def __init__(self, max_size, dimension=4, buffer_name=None): - """ - - Stream system uses that to implement the + """Stream system uses that to implement the CircularQueue with shared memory resources. Parameters @@ -274,7 +271,8 @@ def cleanup(self): class GenericCircularQueue(ABC): """This implements a generic circular queue which works with - shared memory resources.""" + shared memory resources. + """ def __init__( self, @@ -284,7 +282,7 @@ def __init__( buffer=None, buffer_name=None, ): - """ + """Initialize the circular queue. Parameters ---------- @@ -391,12 +389,11 @@ def cleanup(self): class ArrayCircularQueue(GenericCircularQueue): """This implements a MultiDimensional Queue which works with - Arrays and RawArrays.""" + Arrays and RawArrays. + """ def __init__(self, max_size=10, dimension=6, head_tail_buffer=None, buffer=None): - """ - - Stream system uses that to implement user interactions + """Stream system uses that to implement user interactions Parameters ---------- @@ -416,7 +413,6 @@ def __init__(self, max_size=10, dimension=6, head_tail_buffer=None, buffer=None) RawArray to store the data """ - super().__init__(max_size, dimension, use_shared_mem=False, buffer=buffer) if head_tail_buffer is None: @@ -460,14 +456,13 @@ def cleanup(self): class SharedMemCircularQueue(GenericCircularQueue): """This implements a MultiDimensional Queue which works with - SharedMemory.""" + SharedMemory. + """ def __init__( self, max_size=10, dimension=6, head_tail_buffer_name=None, buffer_name=None ): - """ - - Stream system uses that to implement user interactions + """Stream system uses that to implement user interactions Parameters ---------- @@ -569,10 +564,11 @@ def cleanup(self): class GenericImageBufferManager(ABC): """This implements a abstract (generic) ImageBufferManager with - the n-buffer technique.""" + the n-buffer technique. + """ def __init__(self, max_window_size=None, num_buffers=2, use_shared_mem=False): - """ + """Initialize the ImageBufferManager. Parameters ---------- @@ -657,8 +653,10 @@ def get_current_frame(self): def get_jpeg(self): """Returns a jpeg image from the buffer. - Returns: + Returns + ------- bytes: jpeg image. + """ width, height, image = self.get_current_frame() @@ -702,7 +700,7 @@ def __init__( image_buffers=None, info_buffer=None, ): - """ + """Initialize the ImageBufferManager. Parameters ---------- @@ -717,6 +715,7 @@ def __init__( frame to be streamed and the respective sizes image_buffers : list of buffers, optional A list of buffers with each one containing a frame. + """ super().__init__(max_window_size, num_buffers, use_shared_mem=False) if image_buffers is None or info_buffer is None: @@ -768,7 +767,8 @@ def cleanup(self): class SharedMemImageBufferManager(GenericImageBufferManager): """This implements an ImageBufferManager using the - SharedMemory approach.""" + SharedMemory approach. + """ def __init__( self, @@ -777,11 +777,7 @@ def __init__( image_buffer_names=None, info_buffer_name=None, ): - """ - - Note - ----- - Python >=3.8 is a requirement to use this object. + """Initialize the ImageBufferManager. Parameters ---------- @@ -797,6 +793,10 @@ def __init__( image_buffer_names : list of str, optional a list of buffer names. Each buffer contains a frame + Notes + ----- + Python >=3.8 is a requirement to use this object. + """ super().__init__(max_window_size, num_buffers, use_shared_mem=True) if image_buffer_names is None or info_buffer_name is None: @@ -978,9 +978,7 @@ class IntervalTimer: """A object that creates a timer that calls a function periodically.""" def __init__(self, seconds, callback, *args, **kwargs): - """ - - Parameters + """Parameters ---------- seconds : float A positive float number. Represents the total amount of diff --git a/fury/stream/widget.py b/fury/stream/widget.py index 33e631489..0996752f2 100644 --- a/fury/stream/widget.py +++ b/fury/stream/widget.py @@ -57,7 +57,8 @@ def __init__( ms_jpeg=33, queue_size=20, ): - """ + """Initialize the widget. + Parameters ---------- showm : ShowmManager diff --git a/fury/testing.py b/fury/testing.py index 50d8001ce..1c4fd939a 100644 --- a/fury/testing.py +++ b/fury/testing.py @@ -188,10 +188,11 @@ def setup_test(): If imported into a file, nosetest will run this before any doctests. References - ----------- + ---------- https://github.com/numpy/numpy/commit/710e0327687b9f7653e5ac02d222ba62c657a718 https://github.com/numpy/numpy/commit/734b907fc2f7af6e40ec989ca49ee6d87e21c495 https://github.com/nipy/nibabel/pull/556 + """ if LooseVersion(np.__version__) >= LooseVersion('1.14'): np.set_printoptions(legacy='1.13') diff --git a/fury/tests/test_actors.py b/fury/tests/test_actors.py index 8d4722bdb..c09862b98 100644 --- a/fury/tests/test_actors.py +++ b/fury/tests/test_actors.py @@ -1773,6 +1773,68 @@ def test_ellipsoid_actor(interactive=False): npt.assert_equal(report.actors, 1) +def test_uncertainty_cone_actor(interactive=False): + scene = window.Scene() + + evals = np.array([1.4, 0.5, 0.35]) + evecs = np.eye(3) + + mevals = np.zeros((10, 10, 1, 3)) + mevecs = np.zeros((10, 10, 1, 3, 3)) + + mevals[..., :] = evals + mevecs[..., :, :] = evecs + + signal = np.ones((10, 10, 1, 10)) + sigma = np.array([14.791911, 14.999622, 14.880976, 14.933881, 14.392784, + 14.132468, 14.334953, 14.409375, 14.514647, 14.409275]) + + b_matrix = np.array([[-1.8, -1.9, -4.8, -4.4, -2.3, -1.2, -1.0], + [-5.4, -1.8, -1.6, -1.7, -6.1, -1.3, -1.0], + [-6.2, -5.1, -1.0, -1.9, -9.3, -2.2, -1.0], + [-2.8, -1.9, -4.8, -1.4, -2.1, -3.6, -1.0], + [-5.6, -1.3, -7.8, -2.4, -5.2, -4.2, -1.0], + [-1.8, -2.5, -1.8, -1.2, -2.3, -4.8, -1.0], + [-2.3, -1.9, -6.8, -4.4, -6.4, -1.9, -1.0], + [-1.8, -2.6, -4.8, -6.5, -7.7, -3.1, -1.0], + [-6.2, -1.9, -5.6, -4.6, -1.5, -2.0, -1.0], + [-2.4, -1.9, -4.5, -3.6, -2.5, -1.2, -1.0]]) + + uncert_cones = actor.uncertainty_cone(evecs=mevecs, evals=mevals, + signal=signal, sigma=sigma, + b_matrix=b_matrix) + scene.add(uncert_cones) + + if interactive: + window.show(scene) + + report = window.analyze_scene(scene) + npt.assert_equal(report.actors, 1) + scene.clear() + + evals = np.array([1.4, 0.5, 0.35]) + evecs = np.eye(3) + + mevals = np.zeros((4, 4, 4, 3)) + mevecs = np.zeros((4, 4, 4, 3, 3)) + + mevals[..., :] = evals + mevecs[..., :, :] = evecs + + signal = np.ones((4, 4, 4, 10)) + uncert_cones = actor.uncertainty_cone(evecs=mevecs, evals=mevals, + signal=signal, sigma=sigma, + b_matrix=b_matrix) + scene.add(uncert_cones) + + if interactive: + window.show(scene) + + report = window.analyze_scene(scene) + npt.assert_equal(report.actors, 1) + scene.clear() + + def test_actors_primitives_count(): centers = np.array([[1, 1, 1], [2, 2, 2]]) directions = np.array([[1, 0, 0], [1, 0, 0]]) diff --git a/fury/tests/test_io.py b/fury/tests/test_io.py index dff705055..45dd8c018 100644 --- a/fury/tests/test_io.py +++ b/fury/tests/test_io.py @@ -242,7 +242,7 @@ def test_load_cubemap_texture(): def test_load_sprite_sheet(): sprite_URL = ( 'https://raw.githubusercontent.com/' - 'antrikshmisri/DATA/master/fury/0yKFTBQ.png' + 'fury-gl/fury-data/master/unittests/fury_sprite.png' ) with InTemporaryDirectory() as tdir: diff --git a/fury/tests/test_layout.py b/fury/tests/test_layout.py index 136c47a1a..983d40209 100644 --- a/fury/tests/test_layout.py +++ b/fury/tests/test_layout.py @@ -32,6 +32,7 @@ def get_default_cubes( RGB or RGBA (for opacity) scales: list of 2 floats Cube Sizes + """ cube_first_center, cube_second_center = centers cube_first_direction, cube_second_direction = directions @@ -58,6 +59,7 @@ def get_default_panels(sizes=[(100, 100), (200, 200)], colors=np.random.rand(2, Sizes of the two panels colors: ndarray ndarray (2,3) or (2, 4) RGB or RGBA (for opacity) + """ panel_first_size, panel_second_size = sizes panel_first_color, panel_second_color = colors diff --git a/fury/tests/test_stream.py b/fury/tests/test_stream.py index cabf200a6..0833c2d0f 100644 --- a/fury/tests/test_stream.py +++ b/fury/tests/test_stream.py @@ -29,9 +29,10 @@ @pytest.fixture def loop(): - """ - Refs - ---- + """Use this fixture to get the event loop. + + References + ---------- https://promity.com/2020/06/03/testing-asynchronous-code-in-python/ """ loop = asyncio.new_event_loop() @@ -665,7 +666,7 @@ def test_comm(use_raw_array=True): def test_queue_and_webserver(): - """check if the correct + """Check if the correct envent ids and the data are stored in the correct positions """ diff --git a/fury/transform.py b/fury/transform.py index ff5db9479..d12e49dcc 100644 --- a/fury/transform.py +++ b/fury/transform.py @@ -44,12 +44,12 @@ def euler_matrix(ai, aj, ak, axes='sxyz'): http://www.lfd.uci.edu/~gohlke/code/transformations.py.html Parameters - ------------ + ---------- ai, aj, ak : Euler's roll, pitch and yaw angles axes : One of 24 axis sequences as string or encoded tuple Returns - --------- + ------- matrix : ndarray (4, 4) Code modified from the work of Christoph Gohlke link provided here @@ -138,7 +138,7 @@ def sphere2cart(r, theta, phi): as 'longitude' Parameters - ------------ + ---------- r : array_like radius theta : array_like @@ -147,16 +147,16 @@ def sphere2cart(r, theta, phi): azimuth angle Returns - --------- + ------- x : array - x coordinate(s) in Cartesion space + x coordinate(s) in Cartesian space y : array y coordinate(s) in Cartesian space z : array z coordinate Notes - -------- + ----- See these pages: * http://en.wikipedia.org/wiki/Spherical_coordinate_system @@ -197,7 +197,7 @@ def cart2sphere(x, y, z): $0\le\theta\mathrm{(theta)}\le\pi$ and $-\pi\le\phi\mathrm{(phi)}\le\pi$ Parameters - ------------ + ---------- x : array_like x coordinate in Cartesian space y : array_like @@ -206,7 +206,7 @@ def cart2sphere(x, y, z): z coordinate Returns - --------- + ------- r : array radius theta : array @@ -345,6 +345,7 @@ def apply_transformation(vertices, transformation): ------- vertices : ndarray (n, 3) transformed vertices of the mesh + """ shape = vertices.shape temp = np.full((shape[0], 1), 1) @@ -374,6 +375,7 @@ def transform_from_matrix(matrix): rotation component from the transformation matrix scale : ndarray (3, ) scale component from the transformation matrix. + """ translate = matrix[:, -1:].reshape((-1,))[:-1] diff --git a/fury/ui/containers.py b/fury/ui/containers.py index 1f5bc2021..f88a00634 100644 --- a/fury/ui/containers.py +++ b/fury/ui/containers.py @@ -29,6 +29,7 @@ class Panel2D(UI): ---------- alignment : [left, right] Alignment of the panel with respect to the overall screen. + """ def __init__( @@ -62,6 +63,7 @@ def __init__( width of the border has_border: bool, optional If the panel should have borders. + """ self.has_border = has_border self._border_color = border_color @@ -132,6 +134,7 @@ def _add_to_scene(self, scene): Parameters ---------- scene : scene + """ for element in self._elements: element.add_to_scene(scene) @@ -146,6 +149,7 @@ def resize(self, size): ---------- size : (float, float) Panel size (width, height) in pixels. + """ self.background.resize(size) @@ -245,6 +249,7 @@ def remove_element(self, element): ---------- element : UI The UI item to be removed. + """ idx = self._elements.index(element) del self._elements[idx] @@ -263,6 +268,7 @@ def update_element(self, element, coords, anchor='position'): between [0,1]. If int, pixels coordinates are assumed and it must fit within the panel's size. + """ self.remove_element(element) self.add_element(element, coords, anchor) @@ -286,6 +292,7 @@ def re_align(self, window_size_change): ---------- window_size_change : (int, int) New window size (width, height) in pixels. + """ if self.alignment == 'left': pass @@ -320,6 +327,7 @@ def border_color(self, side_color): ---------- side_color: Iterable Iterable to pack side, color values + """ side, color = side_color @@ -350,6 +358,7 @@ def border_width(self, side_width): ---------- side_width: Iterable Iterable to pack side, width values + """ side, border_width = side_width @@ -370,6 +379,7 @@ class TabPanel2D(UI): Hold all the content UI components. text_block: :class: 'TextBlock2D' Renders the title of the tab. + """ def __init__( @@ -395,6 +405,7 @@ def __init__( Background color of tab panel. content_panel : Panel2D Panel consisting of the content UI elements. + """ self.content_panel = content_panel self.panel_size = size @@ -472,6 +483,7 @@ def color(self, color): Parameters ---------- color : list of 3 floats. + """ self.panel.color = color @@ -488,6 +500,7 @@ def title(self, text): ---------- text : str New title for tab panel. + """ self.text_block.message = text @@ -504,6 +517,7 @@ def title_bold(self, bold): ---------- bold : bool Bold property for a text title in a tab panel. + """ self.text_block.bold = bold @@ -520,6 +534,7 @@ def title_color(self, color): ---------- color : tuple New title color for tab panel. + """ self.text_block.color = color @@ -536,6 +551,7 @@ def title_font_size(self, font_size): ---------- font_size : int New title font size for tab panel. + """ self.text_block.font_size = font_size @@ -552,6 +568,7 @@ def title_italic(self, italic): ---------- italic : bool Italic property for a text title in a tab panel. + """ self.text_block.italic = italic @@ -570,6 +587,7 @@ def add_element(self, element, coords, anchor='position'): between [0,1]. If int, pixels coordinates are assumed and it must fit within the panel's size. + """ element.set_visibility(False) self.content_panel.add_element(element, coords, anchor) @@ -581,6 +599,7 @@ def remove_element(self, element): ---------- element : UI The UI item to be removed. + """ self.content_panel.remove_element(element) @@ -597,6 +616,7 @@ def update_element(self, element, coords, anchor='position'): between [0,1]. If int, pixels coordinates are assumed and it must fit within the panel's size. + """ self.content_panel.update_element(element, coords, anchor='position') @@ -608,6 +628,7 @@ class TabUI(UI): ---------- tabs: :class: List of 'TabPanel2D' Stores all the instances of 'TabPanel2D' that renders the contents. + """ def __init__( @@ -640,6 +661,7 @@ def __init__( startup_tab_id : int, optional Tab to be activated and uncollapsed on startup. by default None is activated/ all collapsed. + """ self.tabs = [] self.nb_tabs = nb_tabs @@ -854,6 +876,7 @@ def __init__(self, img_path, position=(0, 0), size=(100, 100)): Absolute coordinates (x, y) of the lower-left corner of the image. size : (int, int), optional Width and height in pixels of the image. + """ super(ImageContainer2D, self).__init__(position) self.img = load_image(img_path, as_vtktype=True) @@ -874,6 +897,7 @@ def _setup(self): Returns ------- :class:`vtkTexturedActor2D` + """ self.texture_polydata = PolyData() self.texture_points = Points() @@ -927,6 +951,7 @@ def _add_to_scene(self, scene): Parameters ---------- scene : scene + """ scene.add(self.actor) @@ -937,6 +962,7 @@ def resize(self, size): ---------- size : (float, float) image size (width, height) in pixels. + """ # Update actor. self.texture_points.SetPoint(0, 0, 0, 0.0) @@ -952,6 +978,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ self.actor.SetPosition(*coords) @@ -962,6 +989,7 @@ def scale(self, factor): ---------- factor : (float, float) Scaling factor (width, height) in pixels. + """ self.resize(self.size * factor) @@ -1168,6 +1196,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ # coords = (0, 0, 0) pass diff --git a/fury/ui/core.py b/fury/ui/core.py index f184e7d16..c6dbe99e0 100644 --- a/fury/ui/core.py +++ b/fury/ui/core.py @@ -3,7 +3,6 @@ __all__ = ['Rectangle2D', 'Disk2D', 'TextBlock2D', 'Button2D'] import abc -from warnings import warn import numpy as np @@ -368,6 +367,7 @@ def __init__(self, size=(0, 0), position=(0, 0), color=(1, 1, 1), opacity=1.0): Must take values in [0, 1]. opacity : float Must take values in [0, 1]. + """ super(Rectangle2D, self).__init__(position) self.color = color @@ -424,6 +424,7 @@ def _add_to_scene(self, scene): Parameters ---------- scene : scene + """ scene.add(self.actor) @@ -594,6 +595,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ # Disk actor are positioned with respect to their center. self.actor.SetPosition(*coords + self.outer_radius) @@ -689,6 +691,7 @@ class TextBlock2D(UI): Automatically scale font according to the text bounding box. dynamic_bbox : bool Automatically resize the bounding box according to the content. + """ def __init__( @@ -740,6 +743,7 @@ def __init__( Automatically scale font according to the text bounding box. dynamic_bbox : bool, optional Automatically resize the bounding box according to the content. + """ self.boundingbox = [0, 0, 0, 0] super(TextBlock2D, self).__init__(position=position) @@ -776,6 +780,7 @@ def resize(self, size): ---------- size : (int, int) Text bounding box size(width, height) in pixels. + """ self.update_bounding_box(size) @@ -789,6 +794,7 @@ def _add_to_scene(self, scene): Parameters ---------- scene : scene + """ scene.add(self.background, self.actor) @@ -800,6 +806,7 @@ def message(self): ------- str The current text message. + """ return self.actor.GetInput() @@ -811,6 +818,7 @@ def message(self, text): ---------- text : str The message to be set. + """ self.actor.SetInput(text) if self.dynamic_bbox: @@ -821,9 +829,10 @@ def font_size(self): """Get text font size. Returns - ---------- + ------- int Text font size. + """ return self.actor.GetTextProperty().GetFontSize() @@ -835,6 +844,7 @@ def font_size(self, size): ---------- size : int Text font size. + """ if not self.auto_font_scale: self.actor.SetTextScaleModeToNone() @@ -848,9 +858,10 @@ def font_family(self): """Get font family. Returns - ---------- + ------- str Text font family. + """ return self.actor.GetTextProperty().GetFontFamilyAsString() @@ -864,6 +875,7 @@ def font_family(self, family='Arial'): ---------- family : str The font family. + """ if family == 'Arial': self.actor.GetTextProperty().SetFontFamilyToArial() @@ -880,6 +892,7 @@ def justification(self): ------- str Text justification. + """ return self._justification @@ -965,6 +978,7 @@ def italic(self, flag): ---------- flag : bool Italicises text if True. + """ self.actor.GetTextProperty().SetItalic(flag) @@ -976,6 +990,7 @@ def shadow(self): ------- bool Text is shadowed if True. + """ return self.actor.GetTextProperty().GetShadow() @@ -987,6 +1002,7 @@ def shadow(self, flag): ---------- flag : bool Shadows text if True. + """ self.actor.GetTextProperty().SetShadow(flag) @@ -998,6 +1014,7 @@ def color(self): ------- (float, float, float) Returns text color in RGB. + """ return self.actor.GetTextProperty().GetColor() @@ -1022,6 +1039,7 @@ def background_color(self): (float, float, float) or None If None, there no background color. Otherwise, background color in RGB. + """ if not self.have_bg: return None @@ -1057,6 +1075,7 @@ def auto_font_scale(self): ------- bool Text is auto_font_scaled if True. + """ return self._auto_font_scale @@ -1068,6 +1087,7 @@ def auto_font_scale(self, flag): ---------- flag : bool Automatically scales the text font if True. + """ self._auto_font_scale = flag if flag: @@ -1085,6 +1105,7 @@ def dynamic_bbox(self): ------- bool Bounding box is dynamic if True. + """ return self._dynamic_bbox @@ -1096,6 +1117,7 @@ def dynamic_bbox(self, flag): ---------- flag : bool The text bounding box is dynamic if True. + """ self._dynamic_bbox = flag if flag: @@ -1138,7 +1160,7 @@ def update_alignment(self): self.actor.SetPosition(updated_text_position) def cal_size_from_message(self): - "Calculate size of background according to the message it contains." + """Calculate size of background according to the message it contains.""" lines = self.message.split("\n") max_length = max(len(line) for line in lines) return [max_length*self.font_size, len(lines)*self.font_size] @@ -1162,7 +1184,8 @@ def update_bounding_box(self, size=None): if self.auto_font_scale: self.actor.SetPosition2( - self.boundingbox[2]-self.boundingbox[0], self.boundingbox[3]-self.boundingbox[1]) + self.boundingbox[2]-self.boundingbox[0], + self.boundingbox[3]-self.boundingbox[1]) else: self.update_alignment() @@ -1336,6 +1359,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ self.actor.SetPosition(*coords) diff --git a/fury/ui/elements.py b/fury/ui/elements.py index 1bcefd54d..41b49de55 100644 --- a/fury/ui/elements.py +++ b/fury/ui/elements.py @@ -24,16 +24,22 @@ from collections import OrderedDict from numbers import Number from string import printable -from PIL import UnidentifiedImageError, Image from urllib.request import urlopen import numpy as np +from PIL import Image, UnidentifiedImageError from fury.data import read_viz_icons from fury.lib import Command -from fury.ui.containers import Panel2D, ImageContainer2D +from fury.ui.containers import ImageContainer2D, Panel2D from fury.ui.core import UI, Button2D, Disk2D, Rectangle2D, TextBlock2D -from fury.ui.helpers import TWO_PI, cal_bounding_box_2d, clip_overflow, rotate_2d, wrap_overflow +from fury.ui.helpers import ( + TWO_PI, + cal_bounding_box_2d, + clip_overflow, + rotate_2d, + wrap_overflow, +) from fury.utils import set_polydata_vertices, update_actor, vertices_from_actor @@ -64,6 +70,7 @@ class TextBox2D(UI): Position of the caret in the text. init : bool Flag which says whether the textbox has just been initialized. + """ def __init__( @@ -151,6 +158,7 @@ def _add_to_scene(self, scene): Parameters ---------- scene : scene + """ self.text.add_to_scene(scene) @@ -215,6 +223,7 @@ def handle_character(self, key, key_char): Parameters ---------- character : str + """ if key.lower() == 'return': self.render_text(False) @@ -360,6 +369,7 @@ def left_button_press(self, i_ren, _obj, _textbox_object): obj: :class:`vtkActor` The picked actor _textbox_object: :class:`TextBox2D` + """ i_ren.add_active_prop(self.text.actor) self.edit_mode() @@ -409,6 +419,7 @@ class LineSlider2D(UI): Color of the handle when in unpressed state. active_color : (float, float, float) Color of the handle when it is pressed. + """ def __init__( @@ -580,6 +591,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ # Offset the slider line by the handle's radius. track_position = coords + self.handle.size / 2.0 @@ -630,8 +642,8 @@ def set_position(self, position): ---------- position : (float, float) The absolute position of the disk (x, y). - """ + """ # Move slider disk. if self.orientation == 'horizontal': x_position = position[0] @@ -784,6 +796,7 @@ class LineDoubleSlider2D(UI): Color of the handles when in unpressed state. active_color : (float, float, float) Color of the handles when they are pressed. + """ def __init__( @@ -948,11 +961,11 @@ def _get_size(self): width = None height = None if self.orientation == 'horizontal': - width = self.track.width + 2 * self.handles[0].size[0] + width = self.track.width + self.handles[0].size[0] height = max(self.track.height, self.handles[0].size[1]) else: width = max(self.track.width, self.handles[0].size[0]) - height = self.track.height + 2 * self.handles[0].size[1] + height = self.track.height + self.handles[0].size[1] return np.array([width, height]) @@ -966,13 +979,14 @@ def _set_position(self, coords): """ # Offset the slider line by the handle's radius. - track_position = coords + self.handles[0].size / 2.0 + track_position = coords if self.orientation == 'horizontal': # Offset the slider line height by half the slider line width. track_position[1] -= self.track.size[1] / 2.0 else: # Offset the slider line width by half the slider line height. track_position[0] -= self.track.size[0] / 2.0 + self.track.position = track_position self.handles[0].position = self.handles[0].position.astype(float) @@ -985,11 +999,11 @@ def _set_position(self, coords): # Position the text below the handles. self.text[0].position = ( self.handles[0].center[0], - self.handles[0].position[1] - 20, + self.handles[0].position[1] - 10, ) self.text[1].position = ( self.handles[1].center[0], - self.handles[1].position[1] - 20, + self.handles[1].position[1] - 10, ) else: # Position the text to the left of the handles. @@ -1035,6 +1049,7 @@ def ratio_to_coord(self, ratio): Parameters ---------- ratio : float + """ if self.orientation == 'horizontal': return self.left_x_position + ratio * self.track.width @@ -1046,6 +1061,7 @@ def coord_to_ratio(self, coord): Parameters ---------- coord : float + """ if self.orientation == 'horizontal': return (coord - self.left_x_position) / float(self.track.width) @@ -1174,6 +1190,7 @@ def right_disk_value(self, right_disk_value): ---------- right_disk_value : float New value for the right disk. + """ self.right_disk_ratio = self.value_to_ratio(right_disk_value) self.on_value_changed(self) @@ -1210,6 +1227,7 @@ def top_disk_ratio(self, top_disk_ratio): ---------- top_disk_ratio : float New ratio for the top disk. + """ position_x = self.ratio_to_coord(top_disk_ratio) position_y = self.ratio_to_coord(top_disk_ratio) @@ -1228,6 +1246,7 @@ def left_disk_ratio(self, left_disk_ratio): ---------- left_disk_ratio : float New ratio for the left disk. + """ position_x = self.ratio_to_coord(left_disk_ratio) position_y = self.ratio_to_coord(left_disk_ratio) @@ -1246,6 +1265,7 @@ def right_disk_ratio(self, right_disk_ratio): ---------- right_disk_ratio : float New ratio for the right disk. + """ position_x = self.ratio_to_coord(right_disk_ratio) position_y = self.ratio_to_coord(right_disk_ratio) @@ -1258,6 +1278,7 @@ def format_text(self, disk_number): ---------- disk_number : int Index of the disk. + """ if callable(self.text_template): return self.text_template(self) @@ -1364,6 +1385,7 @@ class RingSlider2D(UI): Color of the handle when in unpressed state. active_color : (float, float, float) Color of the handle when it is pressed. + """ def __init__( @@ -1406,6 +1428,7 @@ def __init__( replacement fields: `{value:}`, `{ratio:}`, `{angle:}`. If callable, this instance of `:class:RingSlider2D` will be passed as argument to the text template function. + """ self.default_color = (1, 1, 1) self.active_color = (0, 0, 1) @@ -1469,6 +1492,7 @@ def _add_to_scene(self, scene): Parameters ---------- scene : scene + """ self.track.add_to_scene(scene) self.handle.add_to_scene(scene) @@ -1484,6 +1508,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ self.track.position = coords + self.handle.size / 2.0 self.handle.position += coords - self.position @@ -1537,7 +1562,6 @@ def format_text(self): def update(self): """Update the slider.""" - # Compute the ratio determined by the position of the slider disk. self._ratio = self.angle / TWO_PI @@ -1624,7 +1648,6 @@ def handle_release_callback(self, i_ren, _obj, _slider): class RangeSlider(UI): - """A set of a LineSlider2D and a LineDoubleSlider2D. The double slider is used to set the min and max value for the LineSlider2D @@ -1639,6 +1662,7 @@ class RangeSlider(UI): The line slider which sets the min and max values value_slider : :class:`LineSlider2D` The line slider which sets the value + """ def __init__( @@ -1763,6 +1787,7 @@ def _add_to_scene(self, scene): Parameters ---------- scene : scene + """ self.range_slider.add_to_scene(scene) self.value_slider.add_to_scene(scene) @@ -1800,7 +1825,6 @@ def range_slider_handle_move_callback(self, i_ren, obj, _slider): class Option(UI): - """A set of a Button2D and a TextBlock2D to act as a single option for checkboxes and radio buttons. Clicking the button toggles its checked/unchecked status. @@ -1911,7 +1935,6 @@ def deselect(self): class Checkbox(UI): - """A 2D set of :class:'Option' objects. Multiple options can be selected. @@ -1923,6 +1946,7 @@ class Checkbox(UI): Dictionary of all the options in the checkbox set. padding : float Distance between two adjacent options + """ def __init__( @@ -1951,8 +1975,8 @@ def __init__( position : (float, float), optional Absolute coordinates (x, y) of the lower-left corner of the button of the first option. - """ + """ self.labels = list(reversed(list(labels))) self._padding = padding self._font_size = font_size @@ -2014,6 +2038,7 @@ def _handle_option_change(self, option): Parameters ---------- option : :class:`Option` + """ if option.checked: self.checked_labels.append(option.label) @@ -2095,6 +2120,7 @@ def __init__( position : (float, float), optional Absolute coordinates (x, y) of the lower-left corner of the button of the first option. + """ if len(checked_labels) > 1: err_msg = 'Only one option can be pre-selected for radio buttons.' @@ -2129,6 +2155,7 @@ class ComboBox2D(UI): Button to show or hide menu. drop_down_menu: :class: 'ListBox2D' Container for item list. + """ def __init__( @@ -2185,6 +2212,7 @@ def __init__( The font size of selected text in pixels. line_spacing: float Distance between drop down menu's items in pixels. + """ self.items = items.copy() self.font_size = font_size @@ -2205,9 +2233,9 @@ def __init__( self.menu_opacity = menu_opacity # Define subcomponent sizes. - self.text_block_size = (int(0.8 * size[0]), int(0.3 * size[1])) - self.drop_menu_size = (size[0], int(0.7 * size[1])) - self.drop_button_size = (int(0.2 * size[0]), int(0.3 * size[1])) + self.text_block_size = (int(0.9 * size[0]), int(0.1 * size[1])) + self.drop_menu_size = (int(0.9 * size[0]), int(0.7 * size[1])) + self.drop_button_size = (int(0.1 * size[0]), int(0.1 * size[1])) self._icon_files = [ ('left', read_viz_icons(fname='circle-left.png')), @@ -2319,12 +2347,13 @@ def resize(self, size): ---------- size : (int, int) ComboBox size(width, height) in pixels. + """ self.panel.resize(size) - self.text_block_size = (int(0.8 * size[0]), int(0.3 * size[1])) - self.drop_menu_size = (size[0], int(0.7 * size[1])) - self.drop_button_size = (int(0.2 * size[0]), int(0.3 * size[1])) + self.text_block_size = (int(0.9 * size[0]), int(0.1 * size[1])) + self.drop_menu_size = (int(0.9 * size[0]), int(0.7 * size[1])) + self.drop_button_size = (int(0.1 * size[0]), int(0.1 * size[1])) self.panel.update_element(self.selection_box, (0.001, 0.7)) self.panel.update_element(self.drop_down_button, (0.8, 0.7)) @@ -2344,6 +2373,8 @@ def _set_position(self, coords): """ self.panel.position = coords + self.panel.position = (self.panel.position[0], + self.panel.position[1] - self.drop_menu_size[1]) def _add_to_scene(self, scene): """Add all subcomponents or VTK props that compose this UI component. @@ -2405,7 +2436,6 @@ def select_option_callback(self, i_ren, _obj, listboxitem): listboxitem: :class:`ListBoxItem2D` """ - # Set the Text of TextBlock2D to the text of listboxitem self._selection = listboxitem.element self._selection_ID = self.items.index(self._selection) @@ -2461,6 +2491,7 @@ class ListBox2D(UI): ---------- on_change: function Callback function for when the selected items have changed. + """ def __init__( @@ -2505,6 +2536,7 @@ def __init__( scroll_bar_active_color : tuple of 3 floats scroll_bar_inactive_color : tuple of 3 floats background_opacity : float + """ self.view_offset = 0 self.slots = [] @@ -2896,6 +2928,7 @@ def __init__( unselected_color : tuple of 3 floats selected_color : tuple of 3 floats background_opacity : float + """ super(ListBoxItem2D, self).__init__() self._element = None @@ -2939,6 +2972,7 @@ def _add_to_scene(self, scene): Parameters ---------- scene : scene + """ self.background.add_to_scene(scene) self.textblock.add_to_scene(scene) @@ -2953,6 +2987,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ self.textblock.position = coords # Center background underneath the text. @@ -3048,6 +3083,7 @@ def __init__( The font size in pixels. line_spacing: float Distance between listbox's items in pixels. + """ self.font_size = font_size self.multiselection = multiselection @@ -3278,6 +3314,7 @@ def __init__(self, shape_type, drawpanel=None, position=(0, 0)): Reference to the main canvas on which it is drawn. position : (float, float), optional (x, y) in pixels. + """ self.shape = None self.shape_type = shape_type.lower() @@ -3330,6 +3367,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ if self.shape_type == 'circle': self.shape.center = coords @@ -3343,6 +3381,7 @@ def update_shape_position(self, center_position): ---------- center_position: (float, float) Absolute pixel coordinates (x, y). + """ new_center = self.clamp_position(center=center_position) self.drawpanel.canvas.update_element(self, new_center, 'center') @@ -3391,6 +3430,7 @@ def rotate(self, angle): ---------- angle: float Value by which the vertices are rotated in radian. + """ if self.shape_type == 'circle': return @@ -3425,6 +3465,7 @@ def clamp_position(self, center=None): ------- new_center: ndarray(int) New center for the shape. + """ center = self.center if center is None else center new_center = np.clip( @@ -3505,6 +3546,7 @@ def __init__(self, size=(400, 400), position=(0, 0), is_draggable=False): (x, y) in pixels. is_draggable : bool, optional Whether the background canvas will be draggble or not. + """ self.panel_size = size super(DrawPanel, self).__init__(position) @@ -3610,6 +3652,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ self.canvas.position = coords + [0, self.mode_panel.size[1]] slider_position = self.canvas.position + \ @@ -3644,6 +3687,7 @@ def cal_min_boundary_distance(self, position): ------- float Minimum distance from the boundary. + """ distance_list = [] # calculate distance from element to left and lower boundary @@ -3662,6 +3706,7 @@ def draw_shape(self, shape_type, current_position): Type of shape - line, quad, circle. current_position: (float,float) Lower left corner position for the shape. + """ shape = DrawShape( shape_type=shape_type, drawpanel=self, position=current_position @@ -3680,6 +3725,7 @@ def resize_shape(self, current_position): ---------- current_position: (float,float) Lower left corner position for the shape. + """ self.current_shape = self.shape_list[-1] size = current_position - self.current_shape.position @@ -3706,6 +3752,7 @@ def update_button_icons(self, current_mode): ---------- current_mode: string Current mode of the UI. + """ for btn in self.mode_panel._elements[1:]: if btn.icon_names[0] == current_mode: @@ -3725,6 +3772,7 @@ def clamp_mouse_position(self, mouse_position): ------- list(float) New clipped position. + """ return np.clip( mouse_position, @@ -3938,6 +3986,7 @@ def final_time(self): ------- float Final time for the progress slider. + """ return self._progress_bar.max_value @@ -3949,6 +3998,7 @@ def final_time(self, t): ---------- t: float Final time for the progress slider. + """ self._progress_bar.max_value = t @@ -3960,6 +4010,7 @@ def current_time(self): ------- float Progress slider current value. + """ return self._progress_bar.value @@ -3968,9 +4019,10 @@ def current_time(self, t): """Set progress slider value. Parameters - ------- + ---------- t: float Current time to be set. + """ self._progress_bar.value = t self.current_time_str = t @@ -4000,6 +4052,7 @@ def current_time_str(self, t): ----- This should only be used when the `current_value` is not being set since setting`current_value` automatically sets this property as well. + """ t = np.clip(t, 0, self.final_time) if self.final_time < 3600: @@ -4031,6 +4084,7 @@ def speed(self, speed): ---------- speed: float Speed value to be set in the speed_text counter. + """ if speed <= 0: speed = 0.01 @@ -4080,6 +4134,7 @@ def width(self): ------- float The width of the PlaybackPanel. + """ return self._width @@ -4092,6 +4147,7 @@ def width(self, width): width: float The width of the whole panel. If set to None, The width will be the same as the window's width. + """ self._width = width if width is not None else 900 self._auto_width = width is None @@ -4121,6 +4177,7 @@ class Card2D(UI): Displays the title on card. body_box: :class: 'TextBLock2D' Displays the body text. + """ def __init__(self, image_path, body_text="", draggable=True, @@ -4129,9 +4186,7 @@ def __init__(self, image_path, body_text="", draggable=True, bg_opacity=1, title_color=(0., 0., 0.), body_color=(0., 0., 0.), border_color=(1., 1., 1.), border_width=0, maintain_aspect=False): - """ - - Parameters + """Parameters ---------- image_path: str Path of the image, supports png and jpg/jpeg images @@ -4164,8 +4219,8 @@ def __init__(self, image_path, body_text="", draggable=True, Width of the border maintain_aspect: bool, optional If the image should be scaled to maintain aspect ratio - """ + """ self.image_path = image_path self._basename = os.path.basename(self.image_path) self._extension = self._basename.split('.')[-1] @@ -4209,7 +4264,7 @@ def __init__(self, image_path, body_text="", draggable=True, self.resize(size) def _setup(self): - """ Setup this UI component + """Setup this UI component Create the image. Create the title and body. Create a Panel2D widget to hold image, title, body. @@ -4247,17 +4302,17 @@ def _setup(self): lambda i_ren, _obj, _comp: i_ren.force_render def _get_actors(self): - """ Get the actors composing this UI component. + """Get the actors composing this UI component. """ - return self.panel.actors def _add_to_scene(self, _scene): - """ Add all subcomponents or VTK props that compose this UI component. + """Add all subcomponents or VTK props that compose this UI component. Parameters ---------- scene : scene + """ self.panel.add_to_scene(_scene) if self.size[0] <= 200: @@ -4277,6 +4332,7 @@ def resize(self, size): ---------- size : (int, int) Card2D size(width, height) in pixels. + """ _width, _height = size self.panel.resize(size) @@ -4309,39 +4365,37 @@ def resize(self, size): self.title_box.resize(_title_box_size) def _set_position(self, _coords): - """ Position the lower-left corner of this UI component. + """Position the lower-left corner of this UI component. Parameters ---------- coords: (float, float) Absolute pixel coordinates (x, y). - """ + """ self.panel.position = _coords @property def color(self): - """ Returns the background color of card. + """Returns the background color of card. """ - return self.panel.color @color.setter def color(self, color): - """ Sets background color of card. + """Sets background color of card. Parameters ---------- color : list of 3 floats. - """ + """ self.panel.color = color @property def body(self): - """ Returns the body text of the card. + """Returns the body text of the card. """ - return self.body_box.message @body.setter @@ -4350,9 +4404,8 @@ def body(self, text): @property def title(self): - """ Returns the title text of the card + """Returns the title text of the card """ - return self.title_box.message @title.setter @@ -4404,6 +4457,7 @@ def __init__(self, position=(350, 400), size=(300, 100), padding=10, Max number of characters in a line. max_line: int, optional Max number of lines in the textbox. + """ self.panel_size = size self.padding = padding @@ -4455,6 +4509,7 @@ def resize(self, size): ---------- size : (float, float) SpinBox size(width, height) in pixels. + """ self.panel_size = size self.textbox_size = (int(0.7 * size[0]), int(0.8 * size[1])) @@ -4500,6 +4555,7 @@ def _set_position(self, coords): ---------- coords: (float, float) Absolute pixel coordinates (x, y). + """ self.panel.center = coords @@ -4540,6 +4596,7 @@ def validate_value(self, value): ------- int If valid return converted integer else the previous value. + """ if value.isnumeric(): return int(value) diff --git a/fury/ui/helpers.py b/fury/ui/helpers.py index db4e541b5..3a33df2e2 100644 --- a/fury/ui/helpers.py +++ b/fury/ui/helpers.py @@ -7,6 +7,7 @@ def clip_overflow(textblock, width, side='right'): """Clips overflowing text of TextBlock2D with respect to width. + Parameters ---------- textblock : TextBlock2D @@ -16,10 +17,12 @@ def clip_overflow(textblock, width, side='right'): side : str, optional Clips the overflowing text according to side. It takes values "left" or "right". + Returns ------- clipped text : str Clipped version of the text. + """ original_str = textblock.message prev_bg = textblock.have_bg @@ -35,6 +38,7 @@ def clip_overflow(textblock, width, side='right'): def wrap_overflow(textblock, wrap_width, side='right'): """Wraps overflowing text of TextBlock2D with respect to width. + Parameters ---------- textblock : TextBlock2D @@ -44,10 +48,12 @@ def wrap_overflow(textblock, wrap_width, side='right'): side : str, optional Clips the overflowing text according to side. It takes values "left" or "right". + Returns ------- wrapped text : str Wrapped version of the text. + """ original_str = textblock.message str_copy = textblock.message @@ -76,6 +82,7 @@ def wrap_overflow(textblock, wrap_width, side='right'): def check_overflow(textblock, width, overflow_postfix='', side='right'): """Checks if the text is overflowing. + Parameters ---------- textblock : TextBlock2D @@ -84,10 +91,12 @@ def check_overflow(textblock, width, overflow_postfix='', side='right'): Required width of the text. overflow_postfix: str, optional Postfix to be added to the text if it is overflowing. + Returns ------- mid_ptr: int Overflow index of the text. + """ side = side.lower() if side not in ['left', 'right']: @@ -113,7 +122,8 @@ def check_overflow(textblock, width, overflow_postfix='', side='right'): elif textblock.cal_size_from_message()[0] > width: end_ptr = mid_ptr - if mid_ptr == (start_ptr + end_ptr) // 2 or textblock.cal_size_from_message()[0] == width: + if (mid_ptr == (start_ptr + end_ptr) // 2 or + textblock.cal_size_from_message()[0] == width): if side == 'left': textblock.message = textblock.message[::-1] return mid_ptr @@ -126,8 +136,8 @@ def cal_bounding_box_2d(vertices): ---------- vertices : ndarray vertices of the actors. - """ + """ if vertices.ndim != 2 or vertices.shape[1] not in [2, 3]: raise IOError('vertices should be a 2D array with shape (n,2) or (n,3).') @@ -163,6 +173,7 @@ def rotate_2d(vertices, angle): vertices of the actors. angle: float Value by which the vertices are rotated in radian. + """ if vertices.ndim != 2 or vertices.shape[1] != 3: raise IOError('vertices should be a 2D array with shape (n,3).') diff --git a/fury/ui/tests/test_elements.py b/fury/ui/tests/test_elements.py index de82aa168..fb6d3ebef 100644 --- a/fury/ui/tests/test_elements.py +++ b/fury/ui/tests/test_elements.py @@ -1074,8 +1074,8 @@ def test_ui_combobox_2d(interactive=False): npt.assert_raises(TypeError, combobox.append_item, invalid_item) npt.assert_equal(values, combobox.items) - npt.assert_equal((60, 60), combobox.drop_button_size) - npt.assert_equal([300, 140], combobox.drop_menu_size) + npt.assert_equal((30, 20), combobox.drop_button_size) + npt.assert_equal([270, 140], combobox.drop_menu_size) npt.assert_equal([300, 200], combobox.size) ui.ComboBox2D(items=values, draggable=False) @@ -1094,9 +1094,9 @@ def test_ui_combobox_2d(interactive=False): npt.assert_equal(1, combobox.selected_text_index) combobox.resize((450, 300)) - npt.assert_equal((360, 90), combobox.text_block_size) - npt.assert_equal((90, 90), combobox.drop_button_size) - npt.assert_equal((450, 210), combobox.drop_menu_size) + npt.assert_equal((405, 30), combobox.text_block_size) + npt.assert_equal((45, 30), combobox.drop_button_size) + npt.assert_equal((405, 210), combobox.drop_menu_size) def test_ui_combobox_2d_dropdown_visibility(interactive=False): diff --git a/fury/ui/tests/test_elements_callback.py b/fury/ui/tests/test_elements_callback.py index dea15b13c..669c76a88 100644 --- a/fury/ui/tests/test_elements_callback.py +++ b/fury/ui/tests/test_elements_callback.py @@ -18,7 +18,6 @@ def test_frame_rate_and_anti_aliasing(): """Testing frame rate with/out anti-aliasing""" - length_ = 200 multi_samples = 32 max_peels = 8 diff --git a/fury/ui/tests/test_helpers.py b/fury/ui/tests/test_helpers.py index 3584ee48e..fbf3bde13 100644 --- a/fury/ui/tests/test_helpers.py +++ b/fury/ui/tests/test_helpers.py @@ -22,7 +22,7 @@ def test_clip_overflow(): clip_overflow(text, text.size[0]) npt.assert_equal('Hello', text.message) - text.message = 'Hello wassup' + text.message = "Hello what's up?" clip_overflow(text, text.size[0]) npt.assert_equal('He...', text.message) @@ -63,9 +63,9 @@ def test_wrap_overflow(): wrap_overflow(text, text.size[0]) npt.assert_equal('Hello', text.message) - text.message = 'Hello wassup' + text.message = "Hello what's up?" wrap_overflow(text, text.size[0]) - npt.assert_equal('Hello\n wass\nup', text.message) + npt.assert_equal("Hello\n what\n's up\n?", text.message) text.message = 'A very very long message to clip text overflow' wrap_overflow(text, text.size[0]) diff --git a/fury/utils.py b/fury/utils.py index 5928d383b..92abd4e4a 100644 --- a/fury/utils.py +++ b/fury/utils.py @@ -56,7 +56,7 @@ def set_input(vtk_object, inp): vtk_object Notes - ------- + ----- This can be used in the following way:: from fury.utils import set_input poly_mapper = set_input(PolyDataMapper(), poly_data) @@ -569,6 +569,7 @@ def get_polydata_primitives_count(polydata): Returns ------- primitives count : int + """ return get_polydata_field(polydata, 'prim_count')[0] @@ -596,6 +597,7 @@ def primitives_count_from_actor(actor): Returns ------- primitives count : int + """ polydata = actor.GetMapper().GetInput() return get_polydata_primitives_count(polydata) @@ -694,6 +696,7 @@ def set_polydata_tcoords(polydata, tcoords): polydata : vtkPolyData tcoords : texture coordinates, represented as 2D ndarrays (Nx2) (one per vertex range (0, 1)) + """ vtk_tcoords = numpy_support.numpy_to_vtk(tcoords, deep=True, array_type=VTK_FLOAT) polydata.GetPointData().SetTCoords(vtk_tcoords) @@ -920,6 +923,7 @@ def apply_affine_to_actor(act, affine): Returns ------- transformed_act: Actor + """ act.SetUserMatrix(numpy_to_vtk_matrix(affine)) return act @@ -1169,7 +1173,7 @@ def normalize_v3(arr): """Normalize a numpy array of 3 component vectors shape=(N, 3). Parameters - ----------- + ---------- array : ndarray Shape (N, 3) @@ -1499,6 +1503,7 @@ def represent_actor_as_wireframe(actor): Returns ------- actor : actor + """ return actor.GetProperty().SetRepresentationToWireframe() @@ -1511,6 +1516,7 @@ def update_surface_actor_colors(actor, colors): actor : surface actor colors : ndarray of shape (N, 3) having colors. The colors should be in the range [0, 1]. + """ actor.GetMapper().GetInput().GetPointData().SetScalars( numpy_to_vtk_colors(255 * colors) @@ -1518,8 +1524,7 @@ def update_surface_actor_colors(actor, colors): def color_check(pts_len, colors=None): - """ - Returns a VTK scalar array containing colors information for each one of + """Returns a VTK scalar array containing colors information for each one of the points according to the policy defined by the parameter colors. Parameters @@ -1569,6 +1574,7 @@ def is_ui(actor): ---------- actor: :class: `UI` or `vtkProp3D` actor that is to be checked + """ return all([hasattr(actor, attr) for attr in ['add_to_scene', '_setup']]) @@ -1583,6 +1589,7 @@ def set_actor_origin(actor, center=None): center: ndarray, optional, default: None The new center position. If `None`, the origin will be set to the mean of the actor's vertices. + """ vertices = vertices_from_actor(actor) if center is None: diff --git a/fury/window.py b/fury/window.py index 649a3bf07..f86b22393 100644 --- a/fury/window.py +++ b/fury/window.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import gzip import time -import fury.animation as anim from tempfile import TemporaryDirectory as InTemporaryDirectory from threading import Lock from warnings import warn @@ -9,6 +8,7 @@ import numpy as np from scipy import ndimage +import fury.animation as anim from fury import __version__ as fury_version from fury.interactor import CustomInteractorStyle from fury.io import load_image, save_image @@ -276,7 +276,6 @@ def camera_direction(self): @property def last_render_time(self): """Returns the last render time in seconds.""" - return self.GetLastRenderTimeInSeconds() def fxaa_on(self): @@ -435,6 +434,7 @@ def timelines(self): ------- list[Timeline]: List of Timelines. + """ return self._timelines @@ -446,6 +446,7 @@ def animations(self): ------- list[Animation]: List of Animations. + """ return self._animations @@ -460,6 +461,7 @@ def add_animation(self, animation): ---------- animation : Animation or Timeline The Animation or Timeline to be added to the ShowManager. + """ animation.add_to_scene(self.scene) if isinstance(animation, anim.Animation): @@ -491,8 +493,8 @@ def remove_animation(self, animation): ---------- animation : Animation or Timeline The Timeline to be removed. - """ + """ if animation in self.timelines or animation in self.animations: animation.remove_from_scene(self.scene) if isinstance(animation, anim.Animation): @@ -590,7 +592,9 @@ def lock_current(self): Returns ------- successful : bool - Returns if the lock was acquired.""" + Returns if the lock was acquired. + + """ if self.is_done(): return False if not hasattr(self, 'window'): @@ -770,7 +774,6 @@ def destroy_timers(self): def exit(self): """Close window and terminate interactor.""" - # if is_osx and self.timers: # OSX seems to not destroy correctly timers # segfault 11 appears sometimes if we do not do it manually. @@ -839,7 +842,7 @@ def show( """Show window with current scene. Parameters - ------------ + ---------- scene : Scene() or vtkRenderer() The scene that holds all the actors. title : string @@ -880,7 +883,7 @@ def show( Occlusion ration for depth peeling (Default 0 - exact image). Examples - ---------- + -------- >>> import numpy as np >>> from fury import window, actor >>> r = window.Scene() @@ -892,8 +895,8 @@ def show( >>> r.add(l) >>> #window.show(r) - See also - --------- + See Also + -------- fury.window.record fury.window.snapshot @@ -936,7 +939,7 @@ def record( azimuth angle az_angle in every frame. Parameters - ----------- + ---------- scene : Scene() or vtkRenderer() object Scene instance cam_pos : None or sequence (3,), optional @@ -984,7 +987,7 @@ def record( print information about the camera. Default is False. Examples - --------- + -------- >>> from fury import window, actor >>> scene = window.Scene() >>> a = actor.axes() @@ -1124,11 +1127,10 @@ def snapshot( dpi=(72, 72), render_window=None, ): - """Save a snapshot of the scene in a file or in memory. Parameters - ----------- + ---------- scene : Scene() or vtkRenderer Scene instance fname : str or None