diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml index 14e225d..257c9a7 100644 --- a/.github/workflows/readme.yml +++ b/.github/workflows/readme.yml @@ -5,26 +5,56 @@ on: paths: - README.Rmd +# Needed for the commit step to push the re-rendered README back +permissions: + contents: write + jobs: render: name: Render README - runs-on: macOS-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true - uses: r-lib/actions/setup-pandoc@v2 - - name: Install Core Dependencies - run: Rscript -e 'install.packages(c("rmarkdown", "dplyr", "remotes"))' - - name: Install Python + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + # README.Rmd imports pyllusion itself, so install the package (which brings in numpy, pandas, + # Pillow and scipy) rather than a hand-listed subset of its dependencies. + - name: Install Python dependencies run: | - Rscript -e "remotes::install_github('rstudio/reticulate')" - Rscript -e "reticulate::install_miniconda()" - Rscript -e "reticulate::py_install(c('Pillow', 'scipy'), pip = TRUE)" + python -m pip install --upgrade pip + python -m pip install . + + # reticulate comes from CRAN. It used to be installed from GitHub via remotes, which failed + # because an unauthenticated install_github() falls back to the bundled PAT and gets rate + # limited; there is no need for the development version here anyway. + - name: Install R dependencies + run: Rscript -e 'install.packages(c("rmarkdown", "reticulate"))' + - name: Render README + env: + # Point reticulate at the interpreter set up above, instead of the local path in README.Rmd + RETICULATE_PYTHON: ${{ env.pythonLocation }}/bin/python run: Rscript -e 'rmarkdown::render("README.Rmd")' + - name: Commit results run: | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" - git commit README.md -m 'Re-build README.Rmd' || echo "No changes to commit" - git push origin || echo "No changes to commit" + git add README.md docs/img + # Only commit and push when the render actually changed something. Guarding these with + # `|| echo` instead would hide auth, branch-protection and non-fast-forward failures behind + # a green run (a push with nothing to send already exits 0, so it never needed a guard). + if git diff --staged --quiet; then + echo "Render produced no changes" + else + git commit -m 'Re-build README.Rmd' + git push origin HEAD:"${GITHUB_REF_NAME}" + fi diff --git a/NEWS.rst b/NEWS.rst index 8273918..8c7b541 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,27 @@ News ===== +1.5 +--------- + +**Fixes** + +- ``Delboeuf`` and ``Ebbinghaus``: fixed ``Size_Inner_Difference`` in ``get_parameters()``. An operator + precedence slip (``np.pi * a**2 / np.pi * b**2``, which evaluates as ``(pi * a**2 / pi) * b**2``) made + it return the product of the two squared radii instead of a difference of areas - it reported + 0.000244140625 for two circles of *identical* size. It is now the signed difference between the areas + of the two inner circles, positive when the left circle is larger, as its docstring always described. + Both illusions were affected, since ``Ebbinghaus`` reuses the same helper. **Any analysis that used + this value will need re-running.** +- Corrected the name of that entry in the ``Delboeuf`` and ``Ebbinghaus`` docstrings, where it was + listed as ``Sine_Inner_Difference``. + +**Misc** + +- Repaired the "Render README" workflow, which could not run: it installed reticulate from GitHub + (rate limited without a token), never installed Pyllusion itself although ``README.Rmd`` imports it, + and relied on a hard-coded local path to a Python distribution. + 1.4 --------- diff --git a/README.Rmd b/README.Rmd index d2438c8..7bb64bb 100644 --- a/README.Rmd +++ b/README.Rmd @@ -24,11 +24,14 @@ knitr::opts_chunk$set( -# Setup python - you need to change the path to your python distribution +# Setup python - locally, point `local_python` at your own python distribution. On CI the +# interpreter comes from RETICULATE_PYTHON instead, so the local path is skipped there (it also +# gets skipped if it does not exist, so a wrong path does not break anyone else's render). library(reticulate) -# reticulate::use_python("D:/Downloads/WPy64-3902/python-3.9.0.amd64/") -reticulate::use_python("C:/Users/Zen Juen/Downloads/WPy64-3850/python-3.8.5.amd64") -# reticulate::use_python("C:/Users/Pham Thanh Tam/Desktop/WPy-3710b/python-3.7.1.amd64") +local_python <- "C:/Users/Zen Juen/Downloads/WPy64-3850/python-3.8.5.amd64" +if (!nzchar(Sys.getenv("CI")) && dir.exists(local_python)) { + reticulate::use_python(local_python) +} # matplotlib <- import("matplotlib") @@ -77,10 +80,7 @@ If you have used **Pyllusion** in your experiments, and have made your scripts a You can cite the package as follows: -``` -Makowski, D., Pham, T., Lau, Z. J. (2020). Pyllusion: A Parametric Framework to Generate Visual Illusions using Python. GitHub. -Retrieved from https://github.com/RealityBending/Pyllusion -``` +- Makowski, D., Lau, Z. J., Pham, T., Paul Boyce, W., & Annabel Chen, S. H. (2021). [A Parametric Framework to Generate Visual Illusions Using Python](https://journals.sagepub.com/doi/abs/10.1177/03010066211057347). Perception, 50(11), 950-965. ## Features diff --git a/README.md b/README.md index 1046416..28e35aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ ![](https://github.com/RealityBending/Pyllusion/blob/master/docs/img/logo.png) ![](https://img.shields.io/pypi/pyversions/pyllusion.svg?logo=python&logoColor=FFE873) +![](https://img.shields.io/pypi/v/pyllusion.svg?logo=pypi&logoColor=FFE873) [![Tests](https://github.com/RealityBending/Pyllusion/actions/workflows/tests.yml/badge.svg)](https://github.com/RealityBending/Pyllusion/actions/workflows/tests.yml) [![](https://img.shields.io/badge/paper-Pyllusion-9C27B0)](https://journals.sagepub.com/doi/10.1177/03010066211057347) @@ -19,9 +20,9 @@ The parametric approach implemented in this software proposes to the *difference* and *illusion strength*. These two parameters can be modulated to independently impact either the objective difference of the core components of the illusion (e.g., the difference between the two -segments in the [Müller-Lyer](#müller-lyer-illusion) illusion) -or the intensity of the illusion effect (e.g., the angle of the -“distractors” arrows). +segments in the [Müller-Lyer](#müller-lyer-illusion) illusion) or the +intensity of the illusion effect (e.g., the angle of the “distractors” +arrows). Describing illusions using a set of parameters aims at fostering **reproducible science**, allowing neuroscientists to easily report, @@ -53,7 +54,11 @@ issue](https://github.com/RealityBending/Pyllusion/issues) with the You can cite the package as follows: -- Makowski, D., Lau, Z. J., Pham, T., Paul Boyce, W., & Annabel Chen, S. H. (2021). [A Parametric Framework to Generate Visual Illusions Using Python](https://journals.sagepub.com/doi/abs/10.1177/03010066211057347). Perception, 50(11), 950-965. +- Makowski, D., Lau, Z. J., Pham, T., Paul Boyce, W., & Annabel + Chen, S. H. (2021). [A Parametric Framework to Generate Visual + Illusions Using + Python](https://journals.sagepub.com/doi/abs/10.1177/03010066211057347). + Perception, 50(11), 950-965. ## Features @@ -262,8 +267,8 @@ white.to_image() The **Kanizsa Square** is an [illusory contour](https://en.wikipedia.org/wiki/Illusory_contours) illusion. See -[Keane et -al., 2019](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6667222/). +[Keane et al., +2019](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6667222/). Some studies have tested the effect of the Kanizsa Square in individuals with schizophrenia, but the finding of greater resistance to the @@ -271,7 +276,7 @@ illusion is not robust (King et al., 2017). ![](https://www.researchgate.net/profile/Ken_Manktelow/publication/6821211/figure/fig2/AS:277716826902534@1443224259391/Kanizsa-square-An-illusory-figure-induced-by-edges-showing-an-illusory-contour-and-an.png) - - **TO DO** *(consider helping\!)* +- **TO DO** *(consider helping!)* ### Autostereograms @@ -305,7 +310,7 @@ it’s a mind-blowing experience 🤯 The function is highly customisable, and we can use a black and white image as a **depth mask** (in this case, the [picture of a -skull](https://raw.githubusercontent.com/RealityBending/Pyllusion/master/docs/img/depthmask.png) +skull](https://github.com/DominiqueMakowski/Pyllusion/docs/img/depthmask.png) that you will see as emerging from the background), and customise the pattern used by providing another function (here, the `image_circles()` function to which we can provide additional arguments like `blackwhite`, @@ -357,9 +362,9 @@ by blending images of faces with noise-like images. Blending of images can be achieved: as followed ``` python -pareidolia = pyllusion.Pareidolia(pattern="docs/img/snake.png", - n=[20, 300, 4000], - sd=[4, 2, 1], +pareidolia = pyllusion.Pareidolia(pattern="docs/img/snake.png", + n=[20, 300, 4000], + sd=[4, 2, 1], weight=[3, 2, 1], alpha=80, blur=0.5) @@ -374,18 +379,18 @@ In visual perception, the **[kinetic depth effect](https://en.wikipedia.org/wiki/Kinetic_depth_effect)** refers to the phenomenon whereby the three-dimensional structural form of an object can be perceived when the object is moving (Wallach & O’Connell, -1953; [Mamassian & -Wallace, 2010](https://jov.arvojournals.org/article.aspx?articleid=2191769)). -One of its derivative is the **Transparency-From-Motion** illusion, +1953; [Mamassian & Wallace, +2010](https://jov.arvojournals.org/article.aspx?articleid=2191769)). One +of its derivative is the **Transparency-From-Motion** illusion, consisting in the superposition of two dot clouds moving in different directions that results in the perception of two transparent layers (See -; -[Schütz, 2014](https://jov.arvojournals.org/article.aspx?articleid=2193903); -[Wexler et -al., 2015](https://www.pnas.org/content/pnas/112/48/14990.full.pdf); -[Schütz & -Mamassian, 2016](https://jov.arvojournals.org/article.aspx?articleid=2548036) -and for a demo). +; [Schütz, +2014](https://jov.arvojournals.org/article.aspx?articleid=2193903); +[Wexler et al., +2015](https://www.pnas.org/content/pnas/112/48/14990.full.pdf); [Schütz +& Mamassian, +2016](https://jov.arvojournals.org/article.aspx?articleid=2548036) and + for a demo). ``` python parameters = pyllusion.motiontransparency_parameters(angle=45) @@ -402,10 +407,10 @@ pyllusion.images_to_gif(images, path="Transparency_From_Motion.gif", fps=paramet ### Pinna illusion -See also **[Zeljic et -al., 2021](https://www.sciencedirect.com/science/article/pii/S0278584620303717)**. +See also **[Zeljic et al., +2021](https://www.sciencedirect.com/science/article/pii/S0278584620303717)**. - - **TO DO** *(consider helping\!)* +- **TO DO** *(consider helping!)* ![](http://psychedelic-information-theory.com/upload/img/pinna-illusion-image.jpg) @@ -416,7 +421,7 @@ See also [David Novick’s](https://twitter.com/NovickProf) tweets and [**here**](https://twitter.com/NovickProf/status/1358787676326031360). - - **TO DO** *(consider helping\!)* +- **TO DO** *(consider helping!)* ![](https://pbs.twimg.com/media/Erdge3AXIAcisZj.png) @@ -431,7 +436,7 @@ and ## PsychoPy Integration Pyllusion can be easily integrated into -[PsychoPy](https://www.psychopy.org/) for running experiments as well\! +[PsychoPy](https://www.psychopy.org/) for running experiments as well! # Load packages import pyllusion diff --git a/docs/img/README_contrast1.png b/docs/img/README_contrast1.png index 42630bb..4957ea0 100644 Binary files a/docs/img/README_contrast1.png and b/docs/img/README_contrast1.png differ diff --git a/docs/img/README_delboeuf1.png b/docs/img/README_delboeuf1.png index 53199d2..90df749 100644 Binary files a/docs/img/README_delboeuf1.png and b/docs/img/README_delboeuf1.png differ diff --git a/docs/img/README_ebbinghaus1.png b/docs/img/README_ebbinghaus1.png index aee0489..89ade8c 100644 Binary files a/docs/img/README_ebbinghaus1.png and b/docs/img/README_ebbinghaus1.png differ diff --git a/docs/img/README_mullerlyer1.png b/docs/img/README_mullerlyer1.png index b4e74b7..a803211 100644 Binary files a/docs/img/README_mullerlyer1.png and b/docs/img/README_mullerlyer1.png differ diff --git a/docs/img/README_pareidolia.png b/docs/img/README_pareidolia.png index e1d7e50..f3e4079 100644 Binary files a/docs/img/README_pareidolia.png and b/docs/img/README_pareidolia.png differ diff --git a/docs/img/README_poggendorff1.png b/docs/img/README_poggendorff1.png index 68b20c6..0fbd61e 100644 Binary files a/docs/img/README_poggendorff1.png and b/docs/img/README_poggendorff1.png differ diff --git a/docs/img/README_white1.png b/docs/img/README_white1.png index 10234b1..3bc15a7 100644 Binary files a/docs/img/README_white1.png and b/docs/img/README_white1.png differ diff --git a/docs/img/README_zollner1.png b/docs/img/README_zollner1.png index 10d5d20..942c1b6 100644 Binary files a/docs/img/README_zollner1.png and b/docs/img/README_zollner1.png differ diff --git a/pyllusion/Delboeuf/Delboeuf.py b/pyllusion/Delboeuf/Delboeuf.py index 6a5a524..c739a43 100644 --- a/pyllusion/Delboeuf/Delboeuf.py +++ b/pyllusion/Delboeuf/Delboeuf.py @@ -88,7 +88,7 @@ def get_parameters(self): - **Illusion_Type** : `Congruent` if the illusion enhances the perception of the objective difference in the illusion, and `Incongruent` if it reduces the perceived difference. - **Size_Inner_Left** : Size of the inner left circle. - **Size_Inner_Right** : Size of the inner right circle. - - **Sine_Inner_Difference** : Difference in areas of the left and right inner circles. + - **Size_Inner_Difference** : Difference in areas of the left and right inner circles (signed, positive when the left circle is larger). - **Size_Outer_Left** : Size of the outer left rim. - **Size_Outer_Right** : Size of the outer right rim. - **Distance** : Distance between the circles, by modifying `distance` of delboeuf_parameters(). diff --git a/pyllusion/Delboeuf/delboeuf_parameters.py b/pyllusion/Delboeuf/delboeuf_parameters.py index 61834e9..648ecdb 100644 --- a/pyllusion/Delboeuf/delboeuf_parameters.py +++ b/pyllusion/Delboeuf/delboeuf_parameters.py @@ -109,10 +109,17 @@ def _delboeuf_parameters_sizeinner(difference=0, size_min=0.25): inner_size_right = size_bigger inner_size_left = size_min + # Signed difference between the areas of the two inner circles, positive when the left one is + # larger. Note the parentheses: written as `np.pi * a**2 / np.pi * b**2` this evaluates left to + # right as `(pi * a**2 / pi) * b**2`, i.e. the product of the squared radii, which is what this + # returned before. + area_left = np.pi * (inner_size_left / 2) ** 2 + area_right = np.pi * (inner_size_right / 2) ** 2 + parameters = { "Difference": difference, "Size_Inner_Left": inner_size_left, "Size_Inner_Right": inner_size_right, - "Size_Inner_Difference": np.pi * (size_bigger / 2) ** 2 / np.pi * (size_min / 2) ** 2, + "Size_Inner_Difference": area_left - area_right, } return parameters diff --git a/pyllusion/Ebbinghaus/Ebbinghaus.py b/pyllusion/Ebbinghaus/Ebbinghaus.py index cea6fef..627f14c 100644 --- a/pyllusion/Ebbinghaus/Ebbinghaus.py +++ b/pyllusion/Ebbinghaus/Ebbinghaus.py @@ -84,7 +84,7 @@ def get_parameters(self): - **Illusion_Type** : `Congruent` if the illusion enhances the perception of the objective difference in the illusion, and `Incongruent` if it reduces the perceived difference. - **Size_Inner_Left** : Size of the inner left circle. - **Size_Inner_Right** : Size of the inner right circle. - - **Sine_Inner_Difference** : Difference in areas of the left and right inner circles. + - **Size_Inner_Difference** : Difference in areas of the left and right inner circles (signed, positive when the left circle is larger). - **Size_Outer_Left** : Size of the surrounding circles around the left inner circle. - **Size_Outer_Right** : Size of the surrounding circles around the right inner circle. - **Distance**: Distance between the circles, by modifying `distance` of ebbinghaus_parameters(). diff --git a/pyllusion/__init__.py b/pyllusion/__init__.py index 5ae42bd..3ade878 100644 --- a/pyllusion/__init__.py +++ b/pyllusion/__init__.py @@ -1,7 +1,7 @@ """ The Pyllusion module. """ -__version__ = "1.4" +__version__ = "1.5" from .Autostereogram import * diff --git a/tests/__init__.py b/tests/__init__.py index d069c82..7887059 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,6 @@ import numpy as np +import pytest + import pyllusion @@ -498,3 +500,26 @@ def test_analyze_luminance(): out = pyllusion.analyze_luminance(image, average=False) assert out["Luminance"].shape == (50, 50) + + +def test_size_inner_difference(): + # `Size_Inner_Difference` is the signed difference between the areas of the two inner circles. + # It used to be mis-parenthesised (`pi * a**2 / pi * b**2`, which is the product of the squared + # radii) and returned 0.000244140625 for two circles of identical size. + for illusion in [pyllusion.Delboeuf, pyllusion.Ebbinghaus]: + # Identical circles -> no difference + parameters = illusion(difference=0).get_parameters() + assert parameters["Size_Inner_Difference"] == 0 + + # Positive difference -> left circle larger -> positive area difference + parameters = illusion(difference=1).get_parameters() + area_left = np.pi * (parameters["Size_Inner_Left"] / 2) ** 2 + area_right = np.pi * (parameters["Size_Inner_Right"] / 2) ** 2 + assert parameters["Size_Inner_Difference"] == pytest.approx(area_left - area_right) + assert parameters["Size_Inner_Difference"] > 0 + # difference=1 doubles the area, as the size is scaled by sqrt(1 + abs(difference)) + assert area_left == pytest.approx(2 * area_right) + + # Sign follows the side the larger circle is on + parameters = illusion(difference=-1).get_parameters() + assert parameters["Size_Inner_Difference"] < 0