diff --git a/.gitignore b/.gitignore index a13599d..8c5034d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /build/ /dist/ /.idea/ +/.vscode/ +/pIceImarisConnector.egg-info/ diff --git a/.pydevproject b/.pydevproject index 2b5013b..99c7e71 100644 --- a/.pydevproject +++ b/.pydevproject @@ -4,5 +4,5 @@ /pIceImarisConnector/pIceImarisConnector python 2.7 -Python 2.7 +Default diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..67cef22 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,9 @@ +version: 2 +sphinx: + configuration: docs/conf.py +formats: + - htmlzip +python: + version: 3 + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 1d399fb..a2e986d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ -recursive-include doc/html * -prune doc/html/.doctrees/ -exclude doc/html/.buildinfo +recursive-include doc/_build/html * +exclude doc/_build/html/.buildinfo +include *.ims *.py include LICENSE.txt include README.md diff --git a/build_documentation.bat b/build_documentation.bat new file mode 100644 index 0000000..7890313 --- /dev/null +++ b/build_documentation.bat @@ -0,0 +1,3 @@ +@ECHO OFF +cd docs +make html diff --git a/build_pip_package.bat b/build_pip_package.bat new file mode 100644 index 0000000..fe2dabe --- /dev/null +++ b/build_pip_package.bat @@ -0,0 +1,6 @@ +@ECHO OFF +if exist build rd /s /q build +rd /s /q dist +python setup.py bdist_wheel +rd /s /q build +dir dist diff --git a/doc/conf.py b/doc/conf.py deleted file mode 100644 index b2caaee..0000000 --- a/doc/conf.py +++ /dev/null @@ -1,303 +0,0 @@ -# -*- coding: utf-8 -*- -# -# IceImarisConnector for python documentation build configuration file, created by -# sphinx-quickstart on Sat Aug 10 18:18:11 2013. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os -sys.path.append('../pIceImarisConnector'); - -if sys.version_info [0] < 3: - - class Mock(object): - - __all__ = [] - - def __init__(self, *args, **kwargs): - pass - - def __call__(self, *args, **kwargs): - return Mock() - - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType - else: - return Mock() - - MOCK_MODULES = ['numpy'] - for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() - -else: - - # RTD - - from unittest.mock import MagicMock - - class Mock(MagicMock): - @classmethod - def __getattr__(cls, name): - return Mock() - - MOCK_MODULES = ['numpy'] - sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) - -# Try importing the sphinx readthedocs.org theme -# Install it with: -# (sudo) pip install sphinx_rtd_theme - -found_html_theme = 'default' -found_html_theme_path = [] -if os.environ.get('USE_LOCAL_SPHINX_RTD_THEME') is not None: - try: - import sphinx_rtd_theme - found_html_theme = 'sphinx_rtd_theme' - found_html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - print("Using local sphinx RTD theme.") - except: - print("Could not import sphinx RTD theme. Using default.") - pass -else: - print("Using default theme.") - - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.autodoc'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'IceImarisConnector for python' -copyright = u'2013-2015, Aaron Christian Ponti' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.3' -# The full version, including alpha/beta/rc tags. -release = '0.3.1' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = found_html_theme - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = found_html_theme_path - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'IceImarisConnectorforpythondoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'IceImarisConnectorforpython.tex', u'IceImarisConnector for python Documentation', - u'Aaron Christian Ponti', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'iceimarisconnectorforpython', u'IceImarisConnector for python Documentation', - [u'Aaron Christian Ponti'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'IceImarisConnectorforpython', u'IceImarisConnector for python Documentation', - u'Aaron Christian Ponti', 'IceImarisConnectorforpython', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' diff --git a/doc/index.rst b/doc/index.rst deleted file mode 100644 index 9cdc3ab..0000000 --- a/doc/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. IceImarisConnector for python documentation master file, created by - sphinx-quickstart on Sat Aug 10 18:18:11 2013. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -pIceImarisConnector -=================== - -* :ref:`genindex` -* :ref:`search` - -.. automodule:: pIceImarisConnector - :members: - diff --git a/doc/.gitignore b/docs/.gitignore similarity index 100% rename from doc/.gitignore rename to docs/.gitignore diff --git a/doc/Makefile b/docs/Makefile similarity index 100% rename from doc/Makefile rename to docs/Makefile diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..fc4316a --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,64 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +import datetime +import sphinx_rtd_theme +import sys, os +sys.path.insert(0, os.path.abspath('../pIceImarisConnector')) +from pIceImarisConnector import pIceImarisConnector + +# -- Project information ----------------------------------------------------- +project = 'pIceImarisConnector' +year = str(datetime.datetime.now().year) +copyright = f"2013 - {year}, Aaron Ponti" +author = 'Aaron Ponti' + +# The full version, including alpha/beta/rc tags +release = str(pIceImarisConnector.__version__) + +master_doc = 'index' + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_rtd_theme' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +#html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d5261c0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,15 @@ +.. pIceImarisConnector documentation master file + +pIceImarisConnector +=================== + +* :ref:`genindex` +* :ref:`search` + +.. toctree:: + :maxdepth: 2 + + usage + +.. automodule:: pIceImarisConnector + :members: diff --git a/doc/make.bat b/docs/make.bat similarity index 96% rename from doc/make.bat rename to docs/make.bat index c4b773c..c9bb990 100644 --- a/doc/make.bat +++ b/docs/make.bat @@ -1,242 +1,242 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\IceImarisConnectorforpython.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\IceImarisConnectorforpython.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\IceImarisConnectorforpython.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\IceImarisConnectorforpython.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..2512274 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +numpy +sphinx_rtd_theme diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..afc5a26 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,72 @@ +.. toctree:: + :maxdepth: 1 + +.. _usage: + +Usage +===== + +In an Imaris XT file: + +.. code-block:: python + + # + # + # + # Python3XT::HelloWorldXT(%i) + # + # + # + + from pIceImarisConnector import pIceImarisConnector + import tkinter + + def HelloWorldXT(aImarisId): + + # Instantiate an IceImarisConnector object + conn = pIceImarisConnector(aImarisId) + + # Display version info in a dialog + top = tkinter.Tk() + top.title("Hello World!") + l = tkinter.Label(top, text=f"... from pIceImarisConnector {conn.__version__} " + f"and {conn.mImarisApplication.GetVersion()}") + l.pack() + top.mainloop() + +.. note:: + + In the call `def HelloWorldXT(aImarisId):`, the argument `aImarisId` can be both an Imaris Application ID as passed by Imaris when running the function from the Imaris Image Processing menu, or an IceImarisConnection object. + +From a python console: + +.. code-block:: python + + # If Imaris is already running + + In [1]: from pIceImarisConnector import pIceImarisConnector + + In [2]: conn = pIceImarisConnector(0) # 0 is the ID of the running Imaris + + In [3]: conn + Out[3]: pIceImarisConnector: connected to Imaris. + + In[4]: print(conn.mImarisApplication.GetVersion()) + Imaris x64 9.5.1 [Nov 27 2019] + +.. code-block:: python + + # If Imaris is not running yet + + In [1]: from pIceImarisConnector import pIceImarisConnector + + In [2]: conn = pIceImarisConnector() + + In [3]: conn.startImaris() + + # Remember to activate the ImarisXT license! + + In[4]: print(conn.mImarisApplication.GetVersion()) + Imaris x64 9.5.1 [Nov 27 2019] + + In[5]: conn.closeImaris(True) diff --git a/pIceImarisConnector/.gitignore b/pIceImarisConnector/.gitignore new file mode 100644 index 0000000..a348e50 --- /dev/null +++ b/pIceImarisConnector/.gitignore @@ -0,0 +1 @@ +/__pycache__/ diff --git a/pIceImarisConnector/__init__.py b/pIceImarisConnector/__init__.py index df0b6ad..7c1e974 100644 --- a/pIceImarisConnector/__init__.py +++ b/pIceImarisConnector/__init__.py @@ -1 +1 @@ -from pIceImarisConnector import pIceImarisConnector +from .pIceImarisConnector import pIceImarisConnector diff --git a/pIceImarisConnector/pIceImarisConnector.py b/pIceImarisConnector/pIceImarisConnector.py index e997e9d..1b5c64c 100644 --- a/pIceImarisConnector/pIceImarisConnector.py +++ b/pIceImarisConnector/pIceImarisConnector.py @@ -4,7 +4,8 @@ import glob import re import random -import imp +import imp # Deprecated; used only as fallback until we are sure that importlib works fine. +import importlib import subprocess import time import math @@ -12,40 +13,40 @@ class pIceImarisConnector(object): - """pIceImarisConnector is a simple Python class that eases communication -between Bitplane Imaris and Python using the Imaris XT interface. + """pIceImarisConnector is a simple Python class that eases communication between Bitplane Imaris and Python + using the Imaris XT interface. -*Copyright Aaron Ponti, 2013.* + *Copyright Aaron Ponti, 2013 - 2020.* -:param imarisApplication: (optional) if omitted, a pIceImarisConnector object is created that is not connected to any Imaris instance. + :param imarisApplication: (optional) if omitted, a pIceImarisConnector object is created that is not + connected to any Imaris instance. -Imaris can then be started (and connected) using the ``startImaris()`` method: + Imaris can then be started (and connected) using the ``startImaris()`` method: ->>> conn.startImaris() + >>> conn.startImaris() -Alternatively, imarisApplication can be: + Alternatively, imarisApplication can be: -* an Imaris Application ID as provided by Imaris, -* a pIceImarisConnector reference, -* an Imaris Application ICE object. + * an Imaris Application ID as provided by Imaris, + * a pIceImarisConnector reference, + * an Imaris Application ICE object. -In all these cases, the instantiated pIceImarisConnector object is connected to and ready to interface with Imaris. + In all these cases, the instantiated pIceImarisConnector object is connected to and ready to interface with Imaris. -**REMARK** + **REMARK** -The Imaris Application ICE object is stored in the property mImarisApplication. -The mImarisApplication property gives access to the entire Imaris ICE API. + The Imaris Application ICE object is stored in the property mImarisApplication. + The mImarisApplication property gives access to the entire Imaris ICE API. -Example: + Example: ->>> conn.mImarisApplication.GetSurpassSelection() - -returns the currently selected object in the Imaris surpass scene. + >>> conn.mImarisApplication.GetSurpassSelection() + returns the currently selected object in the Imaris surpass scene. """ # pIceImarisConnector version - _mVersion = "0.3.1" + __version__ = "0.4.0" # Imaris-related paths _mImarisPath = "" @@ -53,6 +54,9 @@ class pIceImarisConnector(object): _mImarisServerIceExePath = "" _mImarisLibPath = "" + # Imaris version in integer form + _mImarisIntegerVersion = 1 + # ImarisLib object _mImarisLib = None @@ -65,49 +69,51 @@ class pIceImarisConnector(object): # Use control _mUserControl = False + # Possible type filters + _mPossibleTypeFilters = ["Cells", "ClippingPlane", "DataSet", "Filaments", "Frame", "LightSource", + "MeasurementPoints", "Spots", "Surfaces", "SurpassCamera", "Volume", + "ReferenceFrames"] + @property def version(self): - return self._mVersion + """Return the version number.""" + return self.__version__ @property def mImarisApplication(self): + """Return the ICE ImarisApplication object""" return self._mImarisApplication def __new__(cls, *args, **kwargs): """Create or re-use a pIceImarisConnector object. -If an argument of type pIceImarisConnector is passed to __new__(), -this is returned; otherwise, a new pIceImarisConnector object is -instantiated and returned. - + If an argument of type pIceImarisConnector is passed to __new__(), + this is returned; otherwise, a new pIceImarisConnector object is + instantiated and returned. """ - if args and args[0] is not None and \ - type(args[0]).__name__ == "pIceImarisConnector": + if args and args[0] is not None and type(args[0]).__name__ == "pIceImarisConnector": # Reusing passed object return args[0] else: # Creating new object - return object.__new__(cls, *args, **kwargs) + return object.__new__(cls) def __init__(self, imarisApplication=None): """"Initializes the created pIceImarisConnector object. -imarisApplication : (optional) if omitted (or set to None), a - pIceImarisConnector object is created that - is not connected to any Imaris instance. - - Imaris can then be started (and connected) - using the startImaris() method, i.e. + imarisApplication : (optional) if omitted (or set to None), a pIceImarisConnector object is created that + is not connected to any Imaris instance. - conn.startImaris() + Imaris can then be started (and connected) using the startImaris() method, i.e. - Alternatively, imarisApplication can be: + conn.startImaris() - - an Imaris Application ID as provided by Imaris - - a pIceImarisConnector reference - - an Imaris Application ICE object. + Alternatively, imarisApplication can be: + - an Imaris Application ID as provided by Imaris + - a pIceImarisConnector reference + - an Imaris Application ICE object. """ # If imarisApplication is a pIceImarisConnector reference, @@ -115,8 +121,7 @@ def __init__(self, imarisApplication=None): # object without changes. The __new__() method took care of # returnig a reference to the passed object instead of creating # a new one. - if imarisApplication is not None and \ - type(imarisApplication).__name__ == "pIceImarisConnector": + if imarisApplication is not None and type(imarisApplication).__name__ == "pIceImarisConnector": return # Store the required paths @@ -126,6 +131,12 @@ def __init__(self, imarisApplication=None): # that the required dynamic libraries are imported correctly. os.chdir(self._mImarisPath) + # Temporarily add Imaris path to system path (if needed) + systemPath = os.environ["PATH"] + if not self._mImarisPath in systemPath: + systemPath = self._mImarisPath + os.pathsep + systemPath + os.environ["PATH"] = systemPath + # Add the python lib folder to the python path sys.path.append(self._mImarisLibPath) @@ -135,8 +146,9 @@ def __init__(self, imarisApplication=None): # Instantiate and store the ImarisLib object self._mImarisLib = ImarisLib.ImarisLib() - # Assign a random id - self._mImarisObjectID = random.randint(0, 100000) + # Assign a random id. We reserve the first 1000 to manually + # started Imaris instances. + self._mImarisObjectID = 1000 + random.randint(0, 100000) # Now we check the (optional) input parameter imarisApplication. # We have three remaining cases (the first one we took care of @@ -185,7 +197,11 @@ def __init__(self, imarisApplication=None): if self._mImarisApplication is None: raise Exception('Could not connect to Imaris!') + # We also update the ID + self._mImarisObjectID = imarisApplication + # Case 2: we get an ImarisApplication object + # We leave the ID to the randomly generated one. elif type(imarisApplication).__name__ == "IApplicationPrx": self._mImarisApplication = imarisApplication @@ -195,13 +211,12 @@ def __init__(self, imarisApplication=None): def __del__(self): """pIceImarisConnector destructor. -If UserControl is True, Imaris terminates when the IceImarisConnector -object is deleted. If is it set to False, Imaris stays open after the -IceImarisConnector object is deleted. - + If UserControl is True, Imaris terminates when the IceImarisConnector + object is deleted. If is it set to False, Imaris stays open after the + IceImarisConnector object is deleted. """ - if self._mUserControl == True: + if self._mUserControl: if self._mImarisApplication is not None: self.closeImaris() @@ -209,35 +224,38 @@ def __str__(self): """Converts the pIceImarisConnector object to a string.""" if self._mImarisApplication is None: - return "pIceImarisConnector: not connected to an Imaris " \ - "instance yet." + return "pIceImarisConnector: not connected to an Imaris instance yet." else: return "pIceImarisConnector: connected to Imaris." + def __repr__(self): + """Converts the pIceImarisConnector object to a string.""" + + return self.__str__() + def autocast(self, dataItem): """Casts IDataItems to their derived types. -:param dataItem: object to be cast. -:type dataItem: Imaris::IDataItem - -:return: object cast to the appropriate *Imaris::IDataItem* subclass. -:rtype: One of: - -* Imaris::IClippingPlane -* Imaris::IDataContainer -* Imaris::IFilaments -* Imaris::IFrame -* Imaris::IDataSet -* Imaris::IICells -* Imaris::ILightSource -* Imaris::IMeasurementPoints -* Imaris::ISpots -* Imaris::ISurfaces -* Imaris::IVolume -* Imaris::ISurpassCamera -* Imaris::IImageProcessing -* Imaris::IFactory - + :param dataItem: object to be cast. + :type dataItem: Imaris::IDataItem + + :return: object cast to the appropriate *Imaris::IDataItem* subclass. + :rtype: One of: + + * Imaris::IClippingPlane + * Imaris::IDataContainer + * Imaris::IFilaments + * Imaris::IFrame + * Imaris::IDataSet + * Imaris::IICells + * Imaris::ILightSource + * Imaris::IMeasurementPoints + * Imaris::ISpots + * Imaris::ISurfaces + * Imaris::IVolume + * Imaris::ISurpassCamera + * Imaris::IImageProcessing + * Imaris::IFactory """ # Get the factory @@ -275,18 +293,97 @@ def autocast(self, dataItem): elif factory.IsImageProcessing(dataItem): return factory.ToImageProcessing(dataItem) else: - raise ValueError('Invalid IDataItem object!') + try: + # The Reference Frame object does not have an Is...() method + return factory.ToReferenceFrames(dataItem) + except Exception as e: + print(e) + return None + + @staticmethod + def calcRotationBetweenVectors3D(start, dest): + """This method calculates the rotation needed to bring a 3D vector on top of another. + + :param start: starting 3D vector. + :type start: list or numpy array + :param dest: target 3D vector. + :type dest: list or numpy array + + :return: quaternion + :rtype: numpy array + """ + # Make sure that start and dest are lists or Numpy arrays + if isinstance(start, list): + start = np.array(start, dtype=np.float32) + elif isinstance(start, np.ndarray): + pass + else: + raise TypeError('Expected list or Numpy array.') - def closeImaris(self, quiet=False): - """Closes the Imaris instance associated to the pIceImarisConnector -object and resets the mImarisApplication property. + if isinstance(dest, list): + dest = np.array(dest, dtype=np.float32) + elif isinstance(dest, np.ndarray): + pass + else: + raise TypeError('Expected list or Numpy array.') + + # Normalize + start = pIceImarisConnector.normalize(start) + dest = pIceImarisConnector.normalize(dest) + + # Calculate the angle + cos_theta = np.dot(start, dest) + + # Make sure to handle extreme cases + if cos_theta < (-1 + 0.001): + # Special case when vectors in opposite directions: there is no "ideal" rotation axis + # So guess one; any will do as long as it 's perpendicular to start + rotation_axis = np.cross([0, 0, 1], start) + + if np.linalg.norm(rotation_axis, 2) < 0.1: + # The vectors were parallel; try again + rotation_axis = np.cross([1, 0, 0], start) + + rotation_axis = pIceImarisConnector.normalize(rotation_axis) + + q = pIceImarisConnector.mapAxisAngleToQuaternion(rotation_axis, math.pi) + + return q + + # The angle should not give problems + rotation_axis = np.cross(start, dest) + + # Build the quaternion + s = np.sqrt((1 + cos_theta) * 2) + invs = 1 / s + q = np.array([rotation_axis[0] * invs, rotation_axis[1] * invs, rotation_axis[2] * invs, s * 0.5], + dtype=np.float32) + + return q + + def cloneDataSet(self, iDataSet=None): + """ + This method returns a clone of the dataset. + + :param iDataSet: (optional) iDataSet: if not None, clone the passed IDataSet object instead of + current one; if omitted, current dataset (i.e. self.mImarisApplication.GetDataSet()) + will be used. + :return: cloned DataSet + """ + if iDataSet is None: + return self.mImarisApplication.GetDataSet().Clone() + else: + return iDataSet.Clone() -:param quiet: (optional, default False) If True, Imaris won't pop-up a save dialog and close silently. -:type quiet: Boolean + def closeImaris(self, quiet=False): + """Closes the Imaris instance associated to the pIceImarisConnector object and resets the + mImarisApplication property. -:return: True if Imaris could be closed successfully, False otherwise. -:rtype: Boolean + :param quiet: (optional, default False) If True, Imaris won't pop-up a save dialog and close silently. + :type quiet: Boolean + :return: True if Imaris could be closed successfully, False otherwise. + :rtype: Boolean """ # Check if the connection is still alive @@ -297,6 +394,9 @@ def closeImaris(self, quiet=False): try: if quiet: + iDataSet = self._mImarisApplication.GetDataSet() + if iDataSet is not None: + iDataSet.SetModified(False) self._mImarisApplication.SetVisible(False) self._mImarisApplication.Quit() @@ -308,26 +408,89 @@ def closeImaris(self, quiet=False): print("Error: " + str(sys.exc_info()[1])) return False - def createAndSetSpots(self, coords, timeIndices, radii, name, - color, container=None): - """Creates Spots and adds them to the Surpass Scene. + def copyChannels(self, channelIndices): + """Copies one or more channels. + + :param channelIndices: channel indices to be copied. + :type channelIndices: list (or scalar) + """ + + # Check if the connection is still alive + if not self.isAlive(): + return + + # Is there a dataset loaded? + iDataSet = self._mImarisApplication.GetDataSet() + if iDataSet is None or iDataSet.GetSizeX() == 0: + return None + + # Get the dataset sizes + sz = self.getSizes() + + # Some aliases + nChannels = sz[3] + nTimepoints = sz[4] + + # Make sure to have a valid array + npChannelIndices = np.array(channelIndices) + if npChannelIndices.ndim == 0: + npChannelIndices = np.array([channelIndices]) -:param coords: (nx3) [x, y, z]\ :sub:`n` coordinate matrix in dataset units. -:type coords: list -:param timeIndices: spots time indices. -:type timeIndices: list -:param radii: spots radii. -:type radii: list -:param name: name of the Spots object. -:type name: string -:param color: (1x4), (0..1) vector of [R, G, B, A] values. Example: [0.5, 1.0, 1.0, 1.0]. -:type color: list, tuple or float32 Numpy Array -:param container: (optional) if not set, the Spots object is added at the root of the Surpass Scene. Please note that it is the user's responsibility to attach the container to the surpass scene! -:type container: an Imaris::IDataContainer object + # Check the passed indices are within bounds + if np.any(np.logical_or(npChannelIndices < 0, npChannelIndices > (nChannels - 1))): + ValueError("channelIndices is out of bounds.") + # Collect the channel names + channelNames = [] + for i in range(nChannels): + channelNames.append(iDataSet.GetChannelName(i)) -:return: the generated Spots object. -:rtype: Imaris::ISpots + # Copy the channels + for c in range(npChannelIndices.size): + + # Add a channel + nChannels = nChannels + 1 + iDataSet.SetSizeC(nChannels) + + # New channel index + newChannelIndex = nChannels - 1 + + # Set the new channel name + newChannelName = 'Copy of ' + channelNames[npChannelIndices[c]] + iDataSet.SetChannelName(newChannelIndex, newChannelName) + + # Set the new channel color + iDataSet.SetChannelColorRGBA(newChannelIndex, \ + iDataSet.GetChannelColorRGBA(npChannelIndices[c])) + + for t in range(nTimepoints): + # Get the stack + stack = self.getDataVolume(npChannelIndices[c], t) + + # Set the stack + self.setDataVolume(stack, newChannelIndex, t) + + def createAndSetSpots(self, coords, timeIndices, radii, name, color, container=None): + """Creates Spots and adds them to the Surpass Scene. + + :param coords: (nx3) [x, y, z]\ :sub:`n` coordinate matrix in dataset units. + :type coords: list + :param timeIndices: spots time indices. + :type timeIndices: list + :param radii: spots radii. + :type radii: list + :param name: name of the Spots object. + :type name: string + :param color: (1x4), (0..1) vector of [R, G, B, A] values. Example: [0.5, 1.0, 1.0, 1.0]. + :type color: list, tuple or float32 Numpy Array + :param container: (optional) if not set, the Spots object is added at the root of the Surpass Scene. + Please note that it is the user's responsibility to attach the container to the surpass + scene! + :type container: an Imaris::IDataContainer object + + + :return: the generated Spots object. + :rtype: Imaris::ISpots """ @@ -389,43 +552,43 @@ def createAndSetSpots(self, coords, timeIndices, radii, name, # Return it return newSpots - def createDataset(self, datatype, sizeX, sizeY, sizeZ, sizeC, sizeT, \ + def createDataSet(self, datatype, sizeX, sizeY, sizeZ, sizeC, sizeT, \ voxelSizeX=1, voxelSizeY=1, voxelSizeZ=1, deltaTime=1): """Creates an Imaris dataset and replaces current one. -:param datatype: datype for the dataset to be created -:type datatype: one of 'uint8', 'uint16', 'single', Imaris.tType.eTypeUInt8, - Imaris.tType.eTypeUInt16, Imaris.tType.eTypeFloat - -:param sizeX: dataset width. -:type sizeX: int -:param sizeY: dataset height. -:type sizeY: int -:param sizeZ: number of planes. -:type sizeZ: int -:param sizeC: number of channels. -:type sizeC: int -:param sizeT: number of timepoints. -:type sizeT: int -:param voxelSizeX: (optional, default = 1) voxel size in X direction. -:type voxelSizeX: float -:param voxelSizeY: (optional, default = 1) voxel size in Y direction. -:type voxelSizeY: float -:param voxelSizeZ: (optional, default = 1) voxel size in Z direction. -:type voxelSizeZ: float -:param deltaTime: (optional, default = 1) time difference between consecutive time points. -:type deltaTime: float - -:return: created DataSet -:rtype: Imaris::IDataSet - -**EXAMPLE** - ->>> conn.createDataset('uint8', 100, 200, 50, 3, 10, 0.20, 0.25, 0.5, 0.1) - -**REMARKS** - -The function takes care of adding the created dataset to Imaris. + :param datatype: datype for the dataset to be created + :type datatype: one of 'uint8', 'uint16', 'single', Imaris.tType.eTypeUInt8, + Imaris.tType.eTypeUInt16, Imaris.tType.eTypeFloat + + :param sizeX: dataset width. + :type sizeX: int + :param sizeY: dataset height. + :type sizeY: int + :param sizeZ: number of planes. + :type sizeZ: int + :param sizeC: number of channels. + :type sizeC: int + :param sizeT: number of timepoints. + :type sizeT: int + :param voxelSizeX: (optional, default = 1) voxel size in X direction. + :type voxelSizeX: float + :param voxelSizeY: (optional, default = 1) voxel size in Y direction. + :type voxelSizeY: float + :param voxelSizeZ: (optional, default = 1) voxel size in Z direction. + :type voxelSizeZ: float + :param deltaTime: (optional, default = 1) time difference between consecutive time points. + :type deltaTime: float + + :return: created DataSet + :rtype: Imaris::IDataSet + + **EXAMPLE** + + >>> conn.createDataSet('uint8', 100, 200, 50, 3, 10, 0.20, 0.25, 0.5, 0.1) + + **REMARKS** + + The function takes care of adding the created dataset to Imaris. """ # Is Imaris running? @@ -443,20 +606,20 @@ def createDataset(self, datatype, sizeX, sizeY, sizeZ, sizeC, sizeT, \ # Data type if datatype == np.uint8 or \ - str(datatype) == 'uint8' or \ - str(datatype) == 'eTypeUInt8': + str(datatype) == 'uint8' or \ + str(datatype) == 'eTypeUInt8': imarisDataType = ImarisTType.eTypeUInt8 elif datatype == np.uint16 or \ - str(datatype) == 'uint16' or \ - str(datatype) == 'eTypeUInt16': + str(datatype) == 'uint16' or \ + str(datatype) == 'eTypeUInt16': imarisDataType = ImarisTType.eTypeUInt16 elif datatype == np.float32 or \ - str(datatype) == 'float' or \ - str(datatype) == 'eTypeFloat': + str(datatype) == 'float' or \ + str(datatype) == 'eTypeFloat': imarisDataType = ImarisTType.eTypeFloat @@ -490,29 +653,127 @@ def display(self): print(self.__str__()) - def getAllSurpassChildren(self, recursive, typeFilter=None): - """Returns all children of the surpass scene recursively. Folders (i.e. IDataContainer objects) may be scanned (recursively) but are not returned. Optionally, the returned objects may be filtered by type. - -:param recursive: If True, folders will be scanned recursively; if False, only objects at root level will be inspected. -:type recursive: Boolean -:param typeFilter: (optional) Filters the children by type. Only the surpass children of the specified type are returned. One of: -:type typeFilter: string - -* 'Cells' -* 'ClippingPlane' -* 'Dataset' -* 'Filaments' -* 'Frame' -* 'LightSource' -* 'MeasurementPoints' -* 'Spots' -* 'Surfaces' -* 'SurpassCamera' -* 'Volume' - -:return: child objects. -:rtype: list + def getChannelNames(self): + """Returns the channel names. + + :return: channel names + :rtype: list + """ + + # Initialize output + channelNames = []; + + # Is Imaris running? + if not self.isAlive(): + return [] + # Is there a DataSet? + iDataSet = self._mImarisApplication.GetDataSet() + if iDataSet is None: + return [] + + # Number of channels + nChannels = iDataSet.GetSizeC() + + # Fill the list of channel names + for c in range(nChannels): + channelNames.append(iDataSet.GetChannelName(c)) + + # Return the list of channel names + return channelNames + + def getDataSlice(self, plane, channel, timepoint, iDataSet=None): + """Returns a data slice from Imaris. + + :param plane: plane index. + :type plane: int + :param channel: channel index. + :type channel: int + :param timepoint: timepoint index. + :type timepoint: int + :param iDataSet: (optional) get the data slice from the passed IDataSet object instead of current one; + if omitted, current dataset (i.e. ``conn.mImarisApplication.GetDataSet()``) will be used. + :type iDataSet: Imaris::IDataSet + + :return: data slice (2D Numpy array). + :rtype: Numpy array with dtype being one of ``np.uint8``, ``np.uint16``, ``np.float32``. + """ + + if not self.isAlive(): + return None + + if iDataSet is None: + iDataSet = self._mImarisApplication.GetDataSet() + else: + # Is the passed argument a valid iDataSet? + if not self._mImarisApplication.GetFactory().IsDataSet(iDataSet): + raise Exception("Invalid IDataSet object.") + + # Get sizes + (sizeX, sizeY, sizeZ, sizeC, sizeT) = self.getSizes() + + if iDataSet is None or sizeX == 0: + return None + + # Check that the requested plane, channel and timepoint exist + if plane < 0 or plane > sizeZ - 1: + raise Exception("The requested plane index is out of bounds!") + if timepoint < 0 or timepoint > sizeT - 1: + raise Exception("The requested time index is out of bounds!") + if channel < 0 or channel > sizeC - 1: + raise Exception("The requested channel index is out of bounds!") + if timepoint < 0 or timepoint > sizeT - 1: + raise Exception("The requested time index is out of bounds!") + + # Get the dataset class + imarisDataType = str(iDataSet.GetType()) + if imarisDataType == "eTypeUInt8": + # Ice returns uint8 as a string: we must cast. This behavior might + # be changed in the future. + arr = np.array(iDataSet.GetDataSliceBytes(plane, channel, timepoint)) + arr = np.frombuffer(arr.data, dtype=np.uint8) + arr = np.reshape(arr, (sizeX, sizeY)) + elif imarisDataType == "eTypeUInt16": + arr = np.array(iDataSet.GetDataSliceShorts(plane, channel, timepoint), + dtype=np.uint16) + elif imarisDataType == "eTypeFloat": + arr = np.array(iDataSet.GetDataSliceFloats(plane, channel, timepoint), + dtype=np.float32) + else: + raise Exception("Bad value for iDataSet::getType().") + + # Transpose + arr = np.transpose(arr) + + # Return + return arr + + def getAllSurpassChildren(self, recursive, typeFilter=None): + """Returns all children of the surpass scene recursively. Folders (i.e. IDataContainer objects) may be + scanned (recursively) but are not returned. Optionally, the returned objects may be filtered by type. + + :param recursive: If True, folders will be scanned recursively; if False, only objects at root level + will be inspected. + :type recursive: Boolean + :param typeFilter: (optional) Filters the children by type. Only the surpass children of the specified type + are returned. + :type typeFilter: string + + typeFilter is one of: + * 'Cells' + * 'ClippingPlane' + * 'DataSet' + * 'Filaments' + * 'Frame' + * 'LightSource' + * 'MeasurementPoints' + * 'Spots' + * 'Surfaces' + * 'SurpassCamera' + * 'Volume' + + :return: child objects. + :rtype: list """ # Check that recursive is boolen @@ -521,12 +782,7 @@ def getAllSurpassChildren(self, recursive, typeFilter=None): # Possible filter values if typeFilter is not None: - possibleTypeFilters = ["Cells", "ClippingPlane", "Dataset", \ - "Frame", "LightSource", \ - "MeasurementPoints", "Spots", \ - "Surfaces", "SurpassCamera", "Volume"] - - if not typeFilter in possibleTypeFilters: + if typeFilter not in self._mPossibleTypeFilters: raise ValueError("Invalid value for ''typeFilter''.") # Check that there is a Surpass Scene and there are children @@ -554,46 +810,47 @@ def getAllSurpassChildren(self, recursive, typeFilter=None): return children def getDataSubVolume(self, x0, y0, z0, channel, - timepoint, dX, dY, dZ, iDataSet=None): + timepoint, dX, dY, dZ, iDataSet=None): """Returns a data subvolume from Imaris. -:param x0: x coordinate of the top-left vertex of the subvolume to be returned. -:type x0: int -:param y0: y coordinate of the top-left vertex of the subvolume to be returned. -:type y0: int -:param z0: z coordinate of the top-left vertex of the subvolume to be returned. -:type z0: int -:param channel: channel index. -:type channel: int -:param timepoint: timepoint index. -:type timepoint: int -:param dX: extension in x direction of the subvolume to be returned. -:type dX: int -:param dY: extension in y direction of the subvolume to be returned. -:type dY: int -:param dZ: extension in z direction of the subvolume to be returned. -:type dZ: int -:param iDataSet: (optional) get the data volume from the passed IDataSet object instead of current one; if omitted, current dataset (i.e. ``conn.mImarisApplication.GetDataSet()``) will be used. This is useful for instance when masking channels. -:type iDataSet: Imaris::IDataSet - -:return: data subvolume. -:rtype: Numpy array with dtype being one of ``numpy.uint8``, ``numpy.uint16``, ``numpy.float32``. - -**EXAMPLE** - -The following holds: - ->>> stack = conn.getDataVolume(0, 0) ->>> subVolume = conn.getDataSubVolume(x0, y0, z0, 0, 0, dX, dY, dZ) ->>> subStack = stack[z0 : z0 + dZ, y0 : y0 + dY, x0 : x0 + dX] - -subVolume is identical to subStack - -**REMARKS** - -* Implementation detail: this function gets the subvolume as a 1D array and reshapes it in place. -* Coordinates and extensions are in voxels (integers) and not in units! - + :param x0: x coordinate of the top-left vertex of the subvolume to be returned. + :type x0: int + :param y0: y coordinate of the top-left vertex of the subvolume to be returned. + :type y0: int + :param z0: z coordinate of the top-left vertex of the subvolume to be returned. + :type z0: int + :param channel: channel index. + :type channel: int + :param timepoint: timepoint index. + :type timepoint: int + :param dX: extension in x direction of the subvolume to be returned. + :type dX: int + :param dY: extension in y direction of the subvolume to be returned. + :type dY: int + :param dZ: extension in z direction of the subvolume to be returned. + :type dZ: int + :param iDataSet: (optional) get the data volume from the passed IDataSet object instead of current one; + if omitted, current dataset (i.e. ``conn.mImarisApplication.GetDataSet()``) will be used. + This is useful for instance when masking channels. + :type iDataSet: Imaris::IDataSet + + :return: data subvolume. + :rtype: Numpy array with dtype being one of ``numpy.uint8``, ``numpy.uint16``, ``numpy.float32``. + + **EXAMPLE** + + The following holds: + + >>> stack = conn.getDataVolume(0, 0) + >>> subVolume = conn.getDataSubVolume(x0, y0, z0, 0, 0, dX, dY, dZ) + >>> subStack = stack[z0 : z0 + dZ, y0 : y0 + dY, x0 : x0 + dX] + + subVolume is identical to subStack + + **REMARKS** + + * Implementation detail: this function gets the subvolume as a 1D array and reshapes it in place. + * Coordinates and extensions are in voxels (integers) and not in units! """ if not self.isAlive(): @@ -669,20 +926,21 @@ def getDataSubVolume(self, x0, y0, z0, channel, def getDataVolume(self, channel, timepoint, iDataSet=None): """Returns the data volume from Imaris. -:param channel: channel index. -:type channel: int -:param timepoint: timepoint index. -:type timepoint: int -:param iDataSet: (optional) get the data volume from the passed IDataSet object instead of current one; if omitted, current dataset (i.e. ``conn.mImarisApplication.GetDataSet()``) will be used. This is useful for instance when masking channels. -:type iDataSet: Imaris::IDataSet + :param channel: channel index. + :type channel: int + :param timepoint: timepoint index. + :type timepoint: int + :param iDataSet: (optional) get the data volume from the passed IDataSet object instead of current one; + if omitted, current dataset (i.e. ``conn.mImarisApplication.GetDataSet()``) will be used. + This is useful for instance when masking channels. + :type iDataSet: Imaris::IDataSet -:return: data volume (3D Numpy array). -:rtype: Numpy array with dtype being one of ``np.uint8``, ``np.uint16``, ``np.float32``. + :return: data volume (3D Numpy array). + :rtype: Numpy array with dtype being one of ``np.uint8``, ``np.uint16``, ``np.float32``. -**REMARKS** - -Implementation detail: this function gets the volume as a 1D array and reshapes it in place. + **REMARKS** + Implementation detail: this function gets the volume as a 1D array and reshapes it in place. """ if not self.isAlive(): @@ -713,10 +971,10 @@ def getDataVolume(self, channel, timepoint, iDataSet=None): arr = np.frombuffer(arr.data, dtype=np.uint8) elif imarisDataType == "eTypeUInt16": arr = np.array(iDataSet.GetDataVolumeAs1DArrayShorts(channel, timepoint), - dtype=np.uint16) + dtype=np.uint16) elif imarisDataType == "eTypeFloat": arr = np.array(iDataSet.GetDataVolumeAs1DArrayFloats(channel, timepoint), - dtype=np.float32) + dtype=np.float32) else: raise Exception("Bad value for iDataSet::getType().") @@ -730,20 +988,23 @@ def getDataVolume(self, channel, timepoint, iDataSet=None): def getExtends(self): """Returns the dataset extends. -:return: DataSet extends. -:rtype: tuple - -The extends tuple is: ``(minX, minY, minZ, maxX, maxY, maxZ)``, where: + :return: DataSet extends. + :rtype: tuple -* minX : min extend along X dimension, -* minY : min extend along Y dimension, -* minZ : min extend along Z dimension, -* maxX : max extend along X dimension, -* maxY : max extend along Y dimension, -* maxZ : max extend along Z dimension. + The extends tuple is: ``(minX, minY, minZ, maxX, maxY, maxZ)``, where: + * minX : min extend along X dimension, + * minY : min extend along Y dimension, + * minZ : min extend along Z dimension, + * maxX : max extend along X dimension, + * maxY : max extend along Y dimension, + * maxZ : max extend along Z dimension. """ + # Do we have a dataset? + if self._mImarisApplication.GetDataSet() is None: + return None + # Wrap the extends into a tuple return (self._mImarisApplication.GetDataSet().GetExtendMinX(), self._mImarisApplication.GetDataSet().GetExtendMaxY(), @@ -755,11 +1016,10 @@ def getExtends(self): def getImarisVersionAsInteger(self): """Returns the Imaris version as an integer. -The conversion is performed as follows: ``v = 100000 * Major + 10000 * Minor + 100 * Patch``. - -:return: Imaris version as integer. -:rtype: int + The conversion is performed as follows: ``v = 100000 * Major + 10000 * Minor + 100 * Patch``. + :return: Imaris version as integer. + :rtype: int """ # Is Imaris running? @@ -802,9 +1062,8 @@ def getImarisVersionAsInteger(self): def getNumpyDatatype(self): """Returns the datatype of the dataset as a python Numpy type (or None). -:return: datatype of the dataset as a Numpy type. -:rtype: one of ``np.uint8``, ``np.uint16``, ``np.float32``, or ``None`` if the type is unknown in Imaris. - + :return: datatype of the dataset as a Numpy type. + :rtype: one of ``np.uint8``, ``np.uint16``, ``np.float32``, or ``None`` if the type is unknown in Imaris. """ if not self.isAlive(): @@ -813,6 +1072,10 @@ def getNumpyDatatype(self): # Alias iDataSet = self.mImarisApplication.GetDataSet() + # Do we have a dataset? + if iDataSet is None: + return None + # Get the dataset class imarisDataType = str(iDataSet.GetType()) if imarisDataType == "eTypeUInt8": @@ -829,17 +1092,16 @@ def getNumpyDatatype(self): def getSizes(self): """Returns the dataset sizes. -:return: DataSet sizes. -:rtype: tuple + :return: DataSet sizes. + :rtype: tuple -The sizes tuple is: ``(sizeX, sizeY, sizeZ, sizeC, sizeT)``, where: - -* sizeX : dataset size X, -* sizeY : dataset size Y, -* sizeZ : number of planes, -* sizeC : number of channels, -* sizeT : number of time points. + The sizes tuple is: ``(sizeX, sizeY, sizeZ, sizeC, sizeT)``, where: + * sizeX : dataset size X, + * sizeY : dataset size Y, + * sizeZ : number of planes, + * sizeC : number of channels, + * sizeT : number of time points. """ # Wrap the sizes into a tuple @@ -850,15 +1112,16 @@ def getSizes(self): self._mImarisApplication.GetDataSet().GetSizeT()) def getSurpassCameraRotationMatrix(self): - """Calculates the rotation matrix that corresponds to current view in the Surpass Scene (from the Camera Quaternion) for the axes with "Origin Bottom Left". - -:return: tuple with (4 x 4) rotation matrix (R) and a Boolean (isI) that indicates whether or not the rotation matrix is the Identity matrix (i.e. the camera is perpendicular to the dataset). -:rtype: tuple (R, isI) + """Calculates the rotation matrix that corresponds to current view in the Surpass Scene (from the Camera + Quaternion) for the axes with "Origin Bottom Left". -**REMARKS** + :return: tuple with (4 x 4) rotation matrix (R) and a Boolean (isI) that indicates whether or not the + rotation matrix is the Identity matrix (i.e. the camera is perpendicular to the dataset). + :rtype: tuple (R, isI) -**TODO**: Verify the correctness for the other axes orientations. + **REMARKS** + **TODO**: Verify the correctness for the other axes orientations. """ # Get the camera @@ -876,13 +1139,13 @@ def getSurpassCameraRotationMatrix(self): W = q[3] # Make sure the quaternion is a unit quaternion - n2 = X**2 + Y**2 + Z**2 + W**2 + n2 = X ** 2 + Y ** 2 + Z ** 2 + W ** 2 if abs(n2 - 1) > 1e-4: n = math.sqrt(n2) - X = X / n - Y = Y / n - Z = Z / n - W = W / n + X /= n + Y /= n + Z /= n + W /= n # Calculate the rotation matrix R from the quaternion R = np.zeros((4, 4), dtype=np.float32) @@ -918,44 +1181,46 @@ def getSurpassCameraRotationMatrix(self): isI = np.all(abs(R - T) < 1e-4) # Return R and isI - return (R, isI) + return R, isI def getSurpassSelection(self, typeFilter=None): - """Returns the auto-cast current surpass selection. If the 'typeFilter' parameter is specified, the object class is checked against it and None is returned instead of the object if the type does not match. - -:param typeFilter: (optional, default False) Specifies the expected object class. If the selected object is not of the specified type, the function will return None instead. Type is one of: -:type typeFilter: Boolean - -* 'Cells' -* 'ClippingPlane' -* 'Dataset' -* 'Filaments' -* 'Frame' -* 'LightSource' -* 'MeasurementPoints' -* 'Spots' -* 'Surfaces' -* 'SurpassCamera' -* 'Volume' - -:return: autocast, currently selected surpass object; if nothing is selected, or if the object class does not match the passed type, selection will be None instead. -:rtype: One of: - -* Imaris::IClippingPlane -* Imaris::IDataContainer -* Imaris::IFilaments -* Imaris::IFrame -* Imaris::IDataSet -* Imaris::IICells -* Imaris::ILightSource -* Imaris::IMeasurementPoints -* Imaris::ISpots -* Imaris::ISurfaces -* Imaris::IVolume -* Imaris::ISurpassCamera -* Imaris::IImageProcessing -* Imaris::IFactory - + """Returns the auto-cast current surpass selection. If the 'typeFilter' parameter is specified, the object + class is checked against it and None is returned instead of the object if the type does not match. + + :param typeFilter: (optional, default None) Specifies the expected object class. If the selected object is + not of the specified type, the function will return None instead. Type is one of: + :type typeFilter: String + + * 'Cells' + * 'ClippingPlane' + * 'DataSet' + * 'Filaments' + * 'Frame' + * 'LightSource' + * 'MeasurementPoints' + * 'Spots' + * 'Surfaces' + * 'SurpassCamera' + * 'Volume' + + :return: autocast, currently selected surpass object; if nothing is selected, or if the object class does not + match the passed type, selection will be None instead. + :rtype: One of: + + * Imaris::IClippingPlane + * Imaris::IDataContainer + * Imaris::IFilaments + * Imaris::IFrame + * Imaris::IDataSet + * Imaris::IICells + * Imaris::ILightSource + * Imaris::IMeasurementPoints + * Imaris::ISpots + * Imaris::ISurfaces + * Imaris::IVolume + * Imaris::ISurpassCamera + * Imaris::IImageProcessing + * Imaris::IFactory """ # Is Imaris running? @@ -978,41 +1243,130 @@ def getSurpassSelection(self, typeFilter=None): # Return the object return selection + def getTracks(self, iObject=None): + """This method returns the tracks associated to an ISpots or an ISurfaces object. If no object is passed + as argument, the function will try with the currently selected object in the Surpass Scene. If this is not + an ISpots nor an ISurfaces object, an empty result set will be returned. + + :param iSpots (optional, default None) (optional) either an ISpots or an ISurfaces object. If not passed, + the function will try with the currently selected object in the Surpass Scene. + :type Imaris::IDataItem + + :return: tuple containing an array of tracks and and array of starting time indices for each track. + :rtype: tuple + + The tracks array will be empty if no tracks exist for the object or if the argument is not an ISpots or an + ISurfaces object. Each track is in the form [x y z]n, were n is the length of the track. + """ + + # Initialize tracks and timeIndices + tracks = [] + startTimes = [] + + # Do we have an open connection? + if not self.isAlive(): + return tracks, startTimes + + # Check the input parameter + if iObject is None: + + # Try to get the currently selected object in the Surpass scene + iObject = self.mImarisApplication.GetSurpassSelection() + if iObject is None: + raise Exception("If no object is passed to the function, then either an ISpots or an ISurfaces " + + "object must be selected in ") + + # Check the type + factory = self.mImarisApplication.GetFactory() + + if factory.IsSpots(iObject) or factory.IsSurfaces(iObject): + iObject = self.autocast(iObject) + else: + raise Exception("Expected ISpots or ISurfaces object.") + + # Now extract the tracks + + # Get the IDs of the tracks + ids = np.array(iObject.GetTrackIds()) + uids = np.unique(iObject.GetTrackIds()) + nTracks = uids.size + + # Get all spot positions and the track edges + + # Get the positions + if factory.IsSpots(iObject): + # This is an ISPots object. We can get all positions in one shot. + positions = np.array(iObject.GetPositionsXYZ()) + else: + # This is an ISurfaces object. We query each contained surface for its + # center of mass. + nSurfaces = iObject.GetNumberOfSurfaces() + positions = np.zeros((nSurfaces, 3)) + for i in range(nSurfaces): + positions[i, :] = iObject.GetCenterOfMass(i)[0] + + # Get the time indices + if factory.IsSpots(iObject): + # This is an ISPots object. We can get all time indices in one shot. + timeIndices = iObject.GetIndicesT() + else: + # This is an ISurfaces object. We query each contained surface for its + # center of mass. + timeIndices = np.zeros(nSurfaces) + for i in range(nSurfaces): + timeIndices[i] = iObject.GetTimeIndex(i) + + # Get the track edges + trackEdges = np.array(iObject.GetTrackEdges()) + + # Now extract one track after the other and store them into a cell array + tracks = [] + startTimes = [] + + for i in range(nTracks): + # Get the positions and edges for current track (id) + edges = trackEdges[ids == uids[i], :] + edges = np.unique(edges) + + # Extract and store the track and the initial time index + tracks.append(positions[edges, :]) + startTimes.append(timeIndices[edges[0]]) + + return tracks, startTimes + def getVoxelSizes(self): """Returns the X, Y, and Z voxel sizes of the dataset. -:return: dataset voxel sizes. -:rtype: tuple + :return: dataset voxel sizes. + :rtype: tuple -The voxelsize tuple is: ``(voxelSizeX, voxelSizeY, voxelSizeZ)``, where: - -* voxelSizeX: voxel Size in X direction, -* voxelSizeY: voxel Size in Y direction, -* voxelSizeZ: voxel Size in Z direction. + The voxelsize tuple is: ``(voxelSizeX, voxelSizeY, voxelSizeZ)``, where: + * voxelSizeX: voxel Size in X direction, + * voxelSizeY: voxel Size in Y direction, + * voxelSizeZ: voxel Size in Z direction. """ # Voxel size X - vX = (self._mImarisApplication.GetDataSet().GetExtendMaxX() - \ + vX = (self._mImarisApplication.GetDataSet().GetExtendMaxX() - self._mImarisApplication.GetDataSet().GetExtendMinX()) / \ - self._mImarisApplication.GetDataSet().GetSizeX() + self._mImarisApplication.GetDataSet().GetSizeX() # Voxel size Y - vY = (self._mImarisApplication.GetDataSet().GetExtendMaxY() - \ + vY = (self._mImarisApplication.GetDataSet().GetExtendMaxY() - self._mImarisApplication.GetDataSet().GetExtendMinY()) / \ - self._mImarisApplication.GetDataSet().GetSizeY() + self._mImarisApplication.GetDataSet().GetSizeY() # Voxel size Z - vZ = (self._mImarisApplication.GetDataSet().GetExtendMaxZ() - \ + vZ = (self._mImarisApplication.GetDataSet().GetExtendMaxZ() - self._mImarisApplication.GetDataSet().GetExtendMinZ()) / \ - self._mImarisApplication.GetDataSet().GetSizeZ() + self._mImarisApplication.GetDataSet().GetSizeZ() # Wrap the voxel sizes into a tuple - return (vX, vY, vZ) + return vX, vY, vZ def info(self): """Prints to console the full paths to the Imaris and ImarisServerIce executables and the ImarisLib module. - """ # Display info to console @@ -1025,9 +1379,8 @@ def info(self): def isAlive(self): """Checks whether the (stored) connection to Imaris is still alive. -:return: True if the connection is still alive, False otherwise. -:rtype: Boolean - + :return: True if the connection is still alive, False otherwise. + :rtype: Boolean """ # Do we have an ImarisApplication object? @@ -1042,15 +1395,159 @@ def isAlive(self): self._mImarisApplication = None return False + @staticmethod + def mapAxisAngleToQuaternion(r_axis, r_angle): + """This method converts axis–angle representation to quaternion. + + :param: r_axis axis of rotation, e.g. [0, 1, 0] + :type: r_axis list or numpy array + :param: r_angle angle in radians + :type: r_angle float + + :return: q: quaternion + :rtype: numpy array + """ + # Normalize + r_axis = pIceImarisConnector.normalize(r_axis) + + # Flatten + r_axis = r_axis.flatten() + + # Map to quaternion + s = np.sin(r_angle / 2.0) + x = r_axis[0] * s + y = r_axis[1] * s + z = r_axis[2] * s + w = np.cos(r_angle / 2.0) + q = np.array([x, y, z, w]) + + return q + + @staticmethod + def mapAxisAngleToRotationMatrix(r_axis, r_angle): + """This method calculates the 3D rotation matrix for an angle and an axis of rotation. + + :param r_axis: axis of rotation, e.g.[0, 1, 0] + :type r_axis: list or numpy array + :param r_angle: angle in radians + :type r_angle: float + :return: (R: rotation in matrix form; x_axis: x axis of the rotation coordinate system; + y_axis: y axis of the rotation coordinate system; z_axis: z axis of the rotation coordinate system) + :rtype: tuple + """ + + if isinstance(r_axis, list): + r_axis = np.array(r_axis, dtype=np.float32) + elif isinstance(r_axis, np.ndarray): + r_axis = r_axis.astype(np.float32) + else: + raise TypeError('Expected list or numpy array') + + # Make sure the vector is normalized + r_axis = pIceImarisConnector.normalize(r_axis) + + # Pre-compute some values + ux = r_axis[0] + ux2 = ux * ux + uy = r_axis[1] + uy2 = uy * uy + uz = r_axis[2] + uz2 = uz * uz + ca = np.cos(r_angle) + uca = 1 - ca + sa = np.sin(r_angle) + ux_uy_uca = ux * uy * uca + ux_uz_uca = ux * uz * uca + uy_uz_uca = uy * uz * uca + + # Rotation matrix by r_angle around the normal vector r_axis + R = np.zeros((4, 4), dtype=np.float32) + R[0, 0] = ca + ux2 * uca + R[0, 1] = ux_uy_uca - uz * sa + R[0, 2] = ux_uz_uca + uy * sa + + R[1, 0] = ux_uy_uca + uz * sa + R[1, 1] = ca + uy2 * uca + R[1, 2] = uy_uz_uca - ux * sa + + R[2, 0] = ux_uz_uca - uy * sa + R[2, 1] = uy_uz_uca + ux * sa + R[2, 2] = ca + uz2 * uca + + R[3, 3] = 1.0 + + x_axis = R[0:3, 0].T + y_axis = R[0:3, 1].T + z_axis = R[0:3, 2].T + + return R, x_axis, y_axis, z_axis + + @staticmethod + def mapQuaternionToRotationMatrix(q): + """This method calculates the 3D rotation matrix for an angle and an axis of rotation. + + :param q: quaternion + :type q: list or numpy array + :return: (R: rotation in matrix form; x_axis: x axis of the rotation coordinate system; + y_axis: y axis of the rotation coordinate system; z_axis: z axis of the rotation coordinate system) + :rtype: tuple + """ + + if isinstance(q, list): + q = np.array(q, dtype=np.float32) + elif isinstance(q, np.ndarray): + q = q.astype(np.float32) + else: + raise TypeError('Expected list or numpy array') + + # Make sure the quaternion is normalized + q = pIceImarisConnector.normalize(q) + + # Pre-compute some values + x2 = q[0] + q[0] + y2 = q[1] + q[1] + z2 = q[2] + q[2] + xx = q[0] * x2 + xy = q[0] * y2 + xz = q[0] * z2 + yy = q[1] * y2 + yz = q[1] * z2 + zz = q[2] * z2 + wx = q[3] * x2 + wy = q[3] * y2 + wz = q[3] * z2 + + # Rotation matrix by quaternion + R = np.zeros((4, 4), dtype=np.float32) + + R[0, 0] = 1.0 - (yy + zz) + R[0, 1] = xy - wz + R[0, 2] = xz + wy + + R[1, 0] = xy + wz + R[1, 1] = 1.0 - (xx + zz) + R[1, 2] = yz - wx + + R[2, 0] = xz - wy + R[2, 1] = yz + wx + R[2, 2] = 1.0 - (xx + yy) + + R[3, 3] = 1 + + x_axis = R[0:3, 0].T + y_axis = R[0:3, 1].T + z_axis = R[0:3, 2].T + + return R, x_axis, y_axis, z_axis + def mapPositionsUnitsToVoxels(self, uPos): """Maps voxel coordinates in dataset units to voxel indices. -:param uPos: (N x 3) matrix containing the X, Y, Z coordinates in dataset units. -:type uPos: list or float32 Numpy array. - -:return: (N x 3) matrix containing the X, Y, Z voxel indices. -:rtype: list + :param uPos: (N x 3) matrix containing the X, Y, Z coordinates in dataset units. + :type uPos: list or float32 Numpy array. + :return: (N x 3) matrix containing the X, Y, Z voxel indices. + :rtype: list """ # Do we have a connection? @@ -1062,7 +1559,7 @@ def mapPositionsUnitsToVoxels(self, uPos): # Check the input parameter uPos if not isinstance(uPos, list) and \ - not isinstance(uPos, np.ndarray): + not isinstance(uPos, np.ndarray): raise TypeError(errMsg) # If list, convert to Numpy array @@ -1095,12 +1592,11 @@ def mapPositionsUnitsToVoxels(self, uPos): def mapPositionsVoxelsToUnits(self, vPos): """Maps voxel indices in dataset units to unit coordinates. -:param vPos: (N x 3) matrix containing the X, Y, Z unit coordinates mapped onto a voxel grid. -:type vPos: list or float32 Numpy array. - -:return: (N x 3) matrix containing the X, Y, Z coordinates in dataset units. -:rtype: list + :param vPos: (N x 3) matrix containing the X, Y, Z unit coordinates mapped onto a voxel grid. + :type vPos: list or float32 Numpy array. + :return: (N x 3) matrix containing the X, Y, Z coordinates in dataset units. + :rtype: list """ # Is Imaris running? @@ -1112,7 +1608,7 @@ def mapPositionsVoxelsToUnits(self, vPos): # Check the input parameter vPos if not isinstance(vPos, list) and \ - not isinstance(vPos, np.ndarray): + not isinstance(vPos, np.ndarray): raise TypeError(errMsg) # If list, convert to Numpy array @@ -1145,28 +1641,33 @@ def mapPositionsVoxelsToUnits(self, vPos): def mapRgbaScalarToVector(self, rgbaScalar): """Maps an int32 RGBA scalar to an 1-by-4, (0..1) float vector. -:param rgbaScalar: scalar coding for RGBA (as returned from Imaris via the ``GetColorRGBA()`` method of IDataItem objects). -:type rgbaScalar: int32 + :param rgbaScalar: scalar coding for RGBA (as returned from Imaris via the ``GetColorRGBA()`` method + of IDataItem objects). + :type rgbaScalar: int32 -:return: 1-by-4 array with [R, G, B, A] indicating (R)ed, (G)reen, (B)lue, and (A)lpha (=transparency; 0 is opaque) in the 0..1 range. -:rtype: float Numpy array + :return: 1-by-4 array with [R, G, B, A] indicating (R)ed, (G)reen, (B)lue, and (A)lpha (=transparency; 0 + is opaque) in the 0..1 range. + :rtype: float Numpy array -**IMPORTANT REMARKS** + **IMPORTANT REMARKS** -Imaris stores the color components of objects internally as an **uint32** scalar. When this scalar is returned by ImarisXT via the ``GetColorRGBA()`` method, it reaches python as **signed int32** instead. + Imaris stores the color components of objects internally as an **uint32** scalar. When this scalar is + returned by ImarisXT via the ``GetColorRGBA()`` method, it reaches python as **signed int32** instead. -By the way the R, G, B and A components are packed into the scalar, a forced typecast from uint32 to int32 corrupts the value of the transparency component (the actual colors are not affected). In case the transparency is zero, the uint32 and int32 rendition of the number is the same, and there is no problem; but if it is not, the returned value WILL BE NEGATIVE and will need to be casted before it can be processed. + By the way the R, G, B and A components are packed into the scalar, a forced typecast from uint32 to + int32 corrupts the value of the transparency component (the actual colors are not affected). In case the + transparency is zero, the uint32 and int32 rendition of the number is the same, and there is no problem; + but if it is not, the returned value WILL BE NEGATIVE and will need to be casted before it can be processed. -The mapRgbaScalarToVector() method will transparently work around this problem for you. + The mapRgbaScalarToVector() method will transparently work around this problem for you. -**EXAMPLE** + **EXAMPLE** -In this example, current color of a Spots object is obtained from Imaris and pushed back. - ->>> spots = conn.getSurpassSelection('Spots') ->>> current = conn.mapRgbaScalarToVector(spots.GetColorRGBA()) ->>> spots.SetColorRGBA(conn.mapRgbaVectorToScalar(current)) + In this example, current color of a Spots object is obtained from Imaris and pushed back. + >>> spots = conn.getSurpassSelection('Spots') + >>> current = conn.mapRgbaScalarToVector(spots.GetColorRGBA()) + >>> spots.SetColorRGBA(conn.mapRgbaVectorToScalar(current)) """ # rgbaScalar is a signed integer 32 bit, but we support # also the value already wrapped as an uint32 into a numpy @@ -1174,7 +1675,7 @@ def mapRgbaScalarToVector(self, rgbaScalar): if isinstance(rgbaScalar, int): rgbaScalar = np.array(rgbaScalar, dtype=np.uint32) elif isinstance(rgbaScalar, np.ndarray) and \ - rgbaScalar.dtype == np.uint32: + rgbaScalar.dtype == np.uint32: pass else: raise TypeError('Expected integer of Numpy scalar (uint32).') @@ -1188,24 +1689,26 @@ def mapRgbaScalarToVector(self, rgbaScalar): def mapRgbaVectorToScalar(self, rgbaVector): """Maps an 1-by-4, (0..1) RGBA vector to an int32 scalar. -:param rgbaVector: 1-by-4 array with [R, G, B, A] indicating (R)ed, (G)reen, (B)lue, and (A)lpha (=transparency; 0 is opaque). All values are between 0 and 1. -:type rgbaVector: float32 Numpy array - -:return: scalar coding for RGBA (to be used with the ``SetColorRGBA()`` method of IDataItem objects). -:rtype: int32 + :param rgbaVector: 1-by-4 array with [R, G, B, A] indicating (R)ed, (G)reen, (B)lue, and (A)lpha + (=transparency; 0 is opaque). All values are between 0 and 1. + :type rgbaVector: float32 Numpy array -**IMPORTANT REMARKS** + :return: scalar coding for RGBA (to be used with the ``SetColorRGBA()`` method of IDataItem objects). + :rtype: int32 -The way one calculates the RGBA value from an [R, G, B, A] vector (with the values of R, G, B, and A all between 0 and 1) is simply: + **IMPORTANT REMARKS** -``uint32([R G B A] * [1 256 256^2 256^3])`` + The way one calculates the RGBA value from an [R, G, B, A] vector (with the values of R, G, B, and A + all between 0 and 1) is simply: -(where * is the matrix product). This gives a number between 0 and 2^32 - 1. + ``uint32([R G B A] * [1 256 256^2 256^3])`` -To pass this number to Imaris through ImarisXT via the ``SetColorRGBA()`` method, we need to type cast it to **signed int32**. If we do not do this, Imaris will misinterpret the value for the transparency. + (where * is the matrix product). This gives a number between 0 and 2^32 - 1. -The mapRgbaVectorToScalar() method will transparently work around this problem for you. + To pass this number to Imaris through ImarisXT via the ``SetColorRGBA()`` method, we need to type cast + it to **signed int32**. If we do not do this, Imaris will misinterpret the value for the transparency. + The mapRgbaVectorToScalar() method will transparently work around this problem for you. """ # Make sure that rgbaScalar is a list or Numpy array with @@ -1219,7 +1722,7 @@ def mapRgbaVectorToScalar(self, rgbaVector): # Check rgbaVector if rgbaVector.ndim != 1 or rgbaVector.shape[0] != 4 or \ - np.any(np.logical_or(rgbaVector < 0, rgbaVector > 1)): + np.any(np.logical_or(rgbaVector < 0, rgbaVector > 1)): raise ValueError("rgbaVector must be a vector with 4 elements in " + "the 0 .. 1 range.") @@ -1230,20 +1733,121 @@ def mapRgbaVectorToScalar(self, rgbaVector): rgba = np.frombuffer(rgbaVector.data, dtype=np.int32) return int(rgba) + @staticmethod + def multiplyQuaternions(q1, q2): + """This method multiplies two quaternions.. + + :param q1: quaternion + :type q1: list or numpy array + :param q2: quaternion + :type q2: list or numpy array + :return: quaternion + :rtype: numpy array + """ + + if isinstance(q1, list): + q1 = np.array(q1, dtype=np.float32) + elif isinstance(q1, np.ndarray): + q1 = q1.astype(np.float32) + else: + raise TypeError('Expected list or numpy array') + + if isinstance(q2, list): + q2 = np.array(q2, dtype=np.float32) + elif isinstance(q2, np.ndarray): + q2 = q2.astype(np.float32) + else: + raise TypeError('Expected list or numpy array') + + # Normalize + q1 = pIceImarisConnector.normalize(q1) + q2 = pIceImarisConnector.normalize(q2) + + # Multiply the quaternions + q = [q1[0] * q2[3] + q1[1] * q2[2] - q1[2] * q2[1] + q1[3] * q2[0], + - q1[0] * q2[2] + q1[1] * q2[3] + q1[2] * q2[0] + q1[3] * q2[1], + q1[0] * q2[1] - q1[1] * q2[0] + q1[2] * q2[3] + q1[3] * q2[2], + - q1[0] * q2[0] - q1[1] * q2[1] - q1[2] * q2[2] + q1[3] * q2[3] + ] + + return np.array(q) + + @staticmethod + def normalize(v, epsilon=1e-8): + """ + This method normalizes a vector (to length 1). + + However, if the length of the vector is approximately zero, + a zero-vector of the length of the original vector will be returned. + + :param v: vector to normalize + :type v: list or numpy array + :param epsilon: (optional, default = 1e-8) Min length to consider the vector of zero length. + :type epsilon: float + + :return: normalized vector (to length 1); or zero-vector if original length was approximately zero. + :rtype: numpy array + """ + + if isinstance(v, list): + v = np.array(v, dtype=np.float32) + elif isinstance(v, np.ndarray): + v = v.astype(np.float32) + else: + raise Exception("v must be either a list or a numpy array.") + + # Normalize + n_v = np.linalg.norm(v, 2) + if n_v < epsilon: + v = np.zeros(v.shape, dtype=np.float32) + else: + v = v / n_v + + return v + + @staticmethod + def quaternionConjugate(q): + """This method returns the conjugate of a quaternion. + + :param q: quaternion [a b c d], or list of quaternions [a_i b_i c_i d_i], i = 0..n. + If q is an Nx4 matrix, it will be assumed that each row represent a quaternion + (e.g. all quaternions for a time series). If the quaternions are not normalized, + they will be before the conjugate is calculated. + :type q: list or numpy array + + :return: conjugate of the quaternion (list) + :rtype: numpy array + """ + + if isinstance(q, list) or isinstance(q, np.ndarray): + q = np.array(q, dtype=np.float32, ndmin=2) + else: + raise Exception("q must be a list or a numpy array") + + # Prepare the output + qc = np.zeros(q.shape) + + # Calculate the conjugate(normalize if needed) + for i in range(q.shape[0]): + tmp = pIceImarisConnector.normalize(q[i, :]) + qc[i, :] = [tmp[0], -tmp[1], -tmp[2], -tmp[3]] + + return qc + def setDataVolume(self, stack, channel, timepoint): """Sets the data volume to Imaris. -:param stack: 3D array. -:type stack: np.uint8, np.uint16 or np.float32 -:param channel: channel index. -:type channel: int -:param timepoint: timepoint index. -:type timepoint: int + :param stack: 3D array. + :type stack: np.uint8, np.uint16 or np.float32 + :param channel: channel index. + :type channel: int + :param timepoint: timepoint index. + :type timepoint: int -**REMARKS** - -If a dataset exists, the X, Y, and Z dimensions must match the ones of the stack being copied in. If no dataset exists, one will be created to fit it with default other values. + **REMARKS** + If a dataset exists, the X, Y, and Z dimensions must match the ones of the stack being copied in. If no + dataset exists, one will be created to fit it with default other values. """ if not self.isAlive(): @@ -1262,7 +1866,7 @@ def setDataVolume(self, stack, channel, timepoint): sz = stack.shape if len(sz) == 2: sz = (sz[0], sz[1], 1) - iDataSet = self.createDataset(stack.dtype, sz[0], sz[1], sz[2], 1, 1) + iDataSet = self.createDataSet(stack.dtype, sz[0], sz[1], sz[2], 1, 1) # Check that the requested channel and timepoint exist if channel > iDataSet.GetSizeC() - 1: @@ -1287,15 +1891,52 @@ def setDataVolume(self, stack, channel, timepoint): else: raise Exception("Bad value for iDataSet::getType().") + def setVoxelSizes(self, voxelSizes): + """Sets the X, Y, and Z voxel sizes of the dataset. + + It does not move the min extends. + + :param voxelSizes: voxel sizes [vX, vY, xZ] + :type voxelSizes: tuple, list or numpy array + """ + if not self.isAlive(): + return + + # Test the type and shape of voxel size + if type(voxelSizes) is not list and \ + type(voxelSizes) is not tuple and \ + type(voxelSizes) is not np.ndarray: + raise Exception("Bad value for voxelSizes.") + + if len(voxelSizes) != 3: + raise Exception("voxelSizes must be in the form [vX, vY, vZ].") + + # Get the dataset + iDataSet = self._mImarisApplication.GetDataSet() + + if iDataSet is None: + return + + # Voxel size X + iDataSet.SetExtendMaxX(voxelSizes[0] * iDataSet.GetSizeX() + iDataSet.GetExtendMinX()) + + # Voxel size Y + iDataSet.SetExtendMaxY(voxelSizes[1] * iDataSet.GetSizeY() + iDataSet.GetExtendMinY()) + + # Voxel size Z + iDataSet.SetExtendMaxZ(voxelSizes[2] * iDataSet.GetSizeZ() + iDataSet.GetExtendMinZ()) + def startImaris(self, userControl=False): """Starts an Imaris instance and stores the ImarisApplication ICE object. -:param userControl: (optional, default False) The optional parameter userControl sets the fate of Imaris when the client is closed: if userControl is True, Imaris terminates when the pIceImarisConnector object is deleted. If is it set to False, Imaris stays open after the pIceImarisConnector object is deleted. -:type userControl: Boolean - -:return: True if starting Imaris was successful, False otherwise. -:rtype: Boolean + :param userControl: (optional, default False) The optional parameter userControl sets the fate of Imaris + when the client is closed: if userControl is True, Imaris terminates when the + pIceImarisConnector object is deleted. If is it set to False, Imaris stays open after + the pIceImarisConnector object is deleted. + :type userControl: Boolean + :return: True if starting Imaris was successful, False otherwise. + :rtype: Boolean """ # Check the platform @@ -1306,7 +1947,7 @@ def startImaris(self, userControl=False): self._mUserControl = userControl # If an Imaris instance is open, we close it -- no questions asked - if self.isAlive() == True: + if self.isAlive(): self.closeImaris(True) # Now we open a new one @@ -1327,13 +1968,16 @@ def startImaris(self, userControl=False): print(v) return False except: - print "Unexpected error:", sys.exc_info()[0] + print("Unexpected error:", sys.exc_info()[0]) return False # Try getting the application over a certain time period in case it - # takes to long for Imaris to be registered. + # takes to long for Imaris to be registered. Since Imaris 8, a + # license selection dialog will open that can make the time it takes + # for Imaris to be ready to connect quite long. So, we give enough + # time to the user to pick the licenses... nAttempts = 0 - while nAttempts < 200: + while nAttempts < 500: try: # A too quick call to mImarisLib.GetApplication() could # potentially throw an exception and leave the _mImarisLib @@ -1370,6 +2014,30 @@ def startImaris(self, userControl=False): except: print("Error: " + str(sys.exc_info()[0])) + @staticmethod + def getTestFolder(): + """Retrieve the absolute path to the test folder. + + This folder contains two test datasets and some XT functions. + + :return: full path to the test folder. + :rtype: string + """ + import pIceImarisConnector.test as t + return os.path.abspath(os.path.dirname(t.__file__)) + + def loadPyramidalCellTestDataset(self): + """Loads the PyramidalCell.ims test dataset.""" + filename = str(os.path.join(pIceImarisConnector.getTestFolder(), 'PyramidalCell.ims')) + if self.isAlive(): + self.mImarisApplication.FileOpen(filename, '') + + def loadSwimmingAlgaeTestDataset(self): + """Loads the SwimmingAlgae.ims test dataset.""" + filename = str(os.path.join(pIceImarisConnector.getTestFolder(), 'SwimmingAlgae.ims')) + if self.isAlive(): + self.mImarisApplication.FileOpen(filename, '') + # -------------------------------------------------------------------------- # # PRIVATE METHODS FOR INTERNAL USE ONLY. @@ -1378,9 +2046,7 @@ def startImaris(self, userControl=False): # # -------------------------------------------------------------------------- def _findImaris(self): - """Gets or discovers the path to the Imaris executable. For internal use only! - - """ + """Gets or discovers the path to the Imaris executable. For internal use only!""" # Try getting the environment variable IMARISPATH imarisPath = os.getenv('IMARISPATH') @@ -1394,8 +2060,7 @@ def _findImaris(self): elif self._ismac(): tmp = "/Applications" else: - raise OSError("pIceImarisConnector only works " + \ - "on Windows and Mac OS X.") + raise OSError("pIceImarisConnector only works on Windows and Mac OS X.") # Check that the folder exist if os.path.isdir(tmp): @@ -1403,10 +2068,8 @@ def _findImaris(self): # Pick the directory name with highest version number newestVersionDir = self._findNewestVersionDir(tmp) if newestVersionDir is None: - raise OSError("No Imaris installation found " + \ - "in " + tmp + ". Please define " + \ - "an environment variable " + \ - "'IMARISPATH'.") + raise OSError("No Imaris installation found in " + tmp + ". Please define " + + "an environment variable 'IMARISPATH'.") else: imarisPath = newestVersionDir @@ -1414,8 +2077,7 @@ def _findImaris(self): # Check that IMARISPATH points to a valid directory if not os.path.isdir(imarisPath): - raise OSError("The content of the IMARISPATH " + \ - "environment variable does not " + \ + raise OSError("The content of the IMARISPATH environment variable does not " + "point to a valid directory.") # Now store imarisPath and proceed with setting all required @@ -1426,20 +2088,30 @@ def _findImaris(self): # the ImarisLib library if self._ispc(): exePath = os.path.join(imarisPath, 'Imaris.exe') - serverExePath = os.path.join(imarisPath, - 'ImarisServerIce.exe') - libPath = os.path.join(imarisPath, 'XT', 'python') + serverExePath = os.path.join(imarisPath, 'ImarisServerIce.exe') + if self._mImarisIntegerVersion >= 9050000: + # Imaris 9.5 supports also python 3 + if sys.version_info[0] == 2: + libPath = os.path.join(imarisPath, 'XT', 'python2') + else: + libPath = os.path.join(imarisPath, 'XT', 'python3') + else: + libPath = os.path.join(imarisPath, 'XT', 'python') elif self._ismac(): exePath = os.path.join(imarisPath, 'Contents', 'MacOS', 'Imaris') serverExePath = os.path.join(imarisPath, - 'Contents', 'MacOS', - 'ImarisServerIce') - libPath = os.path.join(imarisPath, 'Contents', 'SharedSupport', - 'XT', 'python') + 'Contents', 'MacOS', 'ImarisServerIce') + if self._mImarisIntegerVersion >= 9050000: + # Imaris 9.5 supports also python 3 + if sys.version_info[0] == 2: + libPath = os.path.join(imarisPath, 'Contents', 'SharedSupport', 'XT', 'python2') + else: + libPath = os.path.join(imarisPath, 'Contents', 'SharedSupport', 'XT', 'python3') + else: + libPath = os.path.join(imarisPath, 'Contents', 'SharedSupport', 'XT', 'python') else: - raise OSError("pIceImarisConnector only works " + \ - "on Windows and Mac OS X.") + raise OSError("pIceImarisConnector only works on Windows and Mac OS X.") # Check whether the executable Imaris file exists if not os.path.isfile(exePath): @@ -1454,11 +2126,12 @@ def _findImaris(self): self._mImarisLibPath = libPath def _findNewestVersionDir(self, directory): - """Scans for candidate Imaris directories and returns the one with highest version number. For internal use only! - -:param directory: directory to be scanned. Most likely C:\\Program Files\\Bitplane in Windows and /Applications on Mac OS X. -:type directory: string + """Scans for candidate Imaris directories and returns the one with highest version number. For internal + use only! + :param directory: directory to be scanned. Most likely C:\\Program Files\\Bitplane in Windows and + /Applications on Mac OS X. + :type directory: string """ # If found, this will be the (relative) ImarisPath @@ -1482,8 +2155,8 @@ def _findNewestVersionDir(self, directory): # Make sure to ignore the Scene Viewer, the File Converter # and the 32bit version on 64 bit machines if "ImarisSceneViewer" in d or \ - "FileConverter" in d or \ - "32bit" in d: + "FileConverter" in d or \ + "32bit" in d: continue # Get version information @@ -1517,21 +2190,26 @@ def _findNewestVersionDir(self, directory): newestVersionDir = d newestVersion = version + # Store the version + self._mImarisIntegerVersion = newestVersion + + # Return it return newestVersionDir def _getChildrenAtLevel(self, container, recursive, children): """Scans the children of a given container recursively. For internal use only! -:param container: data container to be scanned for children. -:type container: Imaris::IDataContainer -:param recursive: True if the container must be scanned recursively, False otherwise. -:type recursive: Boolean -:param children : list of children. Since this is a recursive function, the list of children is passed as input so that the children found in current iteration can be appended to the list and returned for the next iteration. -:type children: list - -:return: children found (so far). -:rtype: list - + :param container: data container to be scanned for children. + :type container: Imaris::IDataContainer + :param recursive: True if the container must be scanned recursively, False otherwise. + :type recursive: Boolean + :param children : list of children. Since this is a recursive function, the list of children is passed + as input so that the children found in current iteration can be appended to the list + and returned for the next iteration. + :type children: list + + :return: children found (so far). + :rtype: list """ for i in range(container.GetNumberOfChildren()): @@ -1541,7 +2219,7 @@ def _getChildrenAtLevel(self, container, recursive, children): # Is this a folder? If it is, call this function recursively if self.mImarisApplication.GetFactory().IsDataContainer(child): - if recursive == True: + if recursive: children = self._getChildrenAtLevel(self.autocast(child), recursive, children) @@ -1550,35 +2228,36 @@ def _getChildrenAtLevel(self, container, recursive, children): return children - def _getFilteredChildrenAtLevel(self, container, recursive, \ - typeFilter, children): + def _getFilteredChildrenAtLevel(self, container, recursive, typeFilter, children): """Scans the children of a certain type in a given container recursively. For internal use only! -:param container: data container to be scanned for children. -:type container: Imaris::IDataContainer -:param recursive: True if the container must be scanned recursively, False otherwise. -:type recursive: Boolean -:param typeFilter: Filters the children by type. Only the surpass children of the specified type are returned. One of: -:type typeFilter: string - -* 'Cells' -* 'ClippingPlane' -* 'Dataset' -* 'Filaments' -* 'Frame' -* 'LightSource' -* 'MeasurementPoints' -* 'Spots' -* 'Surfaces' -* 'SurpassCamera' -* 'Volume' - -:param children : list of children. Since this is a recursive function, the list of children is passed as input so that the children found in current iteration can be appended to the list and returned for the next iteration. -:type children: list - -:return: children found (so far). -:rtype: list - + :param container: data container to be scanned for children. + :type container: Imaris::IDataContainer + :param recursive: True if the container must be scanned recursively, False otherwise. + :type recursive: Boolean + :param typeFilter: Filters the children by type. Only the surpass children of the specified type are returned. + :type typeFilter: string + + typeFilter is one of: + * 'Cells' + * 'ClippingPlane' + * 'DataSet' + * 'Filaments' + * 'Frame' + * 'LightSource' + * 'MeasurementPoints' + * 'Spots' + * 'Surfaces' + * 'SurpassCamera' + * 'Volume' + + :param children : list of children. Since this is a recursive function, the list of children is passed as + input so that the children found in current iteration can be appended to the list and + returned for the next iteration. + :type children: list + + :return: children found (so far). + :rtype: list """ for i in range(container.GetNumberOfChildren()): @@ -1587,7 +2266,7 @@ def _getFilteredChildrenAtLevel(self, container, recursive, \ # Is this a folder? If it is, call this function recursively if self.mImarisApplication.GetFactory().IsDataContainer(child): - if recursive == True: + if recursive: children = self._getFilteredChildrenAtLevel( self.autocast(child), recursive, typeFilter, children) else: @@ -1598,33 +2277,34 @@ def _getFilteredChildrenAtLevel(self, container, recursive, \ return children def _importImarisLib(self): - """Imports the ImarisLib module. For internal use only! - - """ + """Imports the ImarisLib module. For internal use only!""" # Dynamically find and import the ImarisLib module - fileobj, pathname, description = imp.find_module('ImarisLib') - ImarisLib = imp.load_module('ImarisLib', fileobj, pathname, description) - fileobj.close() + try: + ImarisLib = importlib.import_module("ImarisLib") + except: + # The imp module is deprecated. + fileobj, pathname, description = imp.find_module('ImarisLib') + ImarisLib = imp.load_module('ImarisLib', fileobj, pathname, description) + fileobj.close() return ImarisLib def _isImarisServerIceRunning(self): """ Checks whether an instance of ImarisServerIce is already running and can be reused. For internal use only! -:return: True is an instance of ImarisServerIce is running and can be reused, False otherwise. -:rtype: Boolean - + :return: True is an instance of ImarisServerIce is running and can be reused, False otherwise. + :rtype: Boolean """ # The check will be different on Windows and on Mac OS X if self._ispc(): cmd = "tasklist /NH /FI \"IMAGENAME eq ImarisServerIce.exe\"" - result = subprocess.check_output(cmd) + result = str(subprocess.check_output(cmd)) if "ImarisServerIce.exe" in result: return True elif self._ismac(): - result = subprocess.check_output(["ps", "aux"]) + result = str(subprocess.check_output(["ps", "aux"])) if self._mImarisServerIceExePath in result: return True else: @@ -1635,9 +2315,8 @@ def _isImarisServerIceRunning(self): def _ismac(self): """Returns true if pIceImarisConnector is being run on Mac OS X. For internal use only! -:return: True if pIceImarisConnector is being run on Mac OS X, False otherwise. -:rtype: Boolean - + :return: True if pIceImarisConnector is being run on Mac OS X, False otherwise. + :rtype: Boolean """ return platform.system() == "Darwin" @@ -1645,34 +2324,29 @@ def _ismac(self): def _isOfType(self, obj, typeValue): """Checks that a passed object is of a given type. For internal use only! -:param obj: object for which the type is to be checked. -:type obj: one of the Imaris objects -:param typeValue: Required type for the object. One of: -:type typeValue: string - -* 'Cells' -* 'ClippingPlane' -* 'Dataset' -* 'Filaments' -* 'Frame' -* 'LightSource' -* 'MeasurementPoints' -* 'Spots' -* 'Surfaces' -* 'SurpassCamera' -* 'Volume' - -:return: True if the checked object is of the passed type, False otherwise. -:rtype: Boolean - + :param obj: object for which the type is to be checked. + :type obj: one of the Imaris objects + :param typeValue: Required type for the object. + :type typeValue: string + + typeValue if one of: + * 'Cells' + * 'ClippingPlane' + * 'DataSet' + * 'Filaments' + * 'Frame' + * 'LightSource' + * 'MeasurementPoints' + * 'Spots' + * 'Surfaces' + * 'SurpassCamera' + * 'Volume' + + :return: True if the checked object is of the passed type, False otherwise. + :rtype: Boolean """ - # Possible type values - possibleTypeValues = ["Cells", "ClippingPlane", "Dataset", "Frame", \ - "LightSource", "MeasurementPoints", "Spots", \ - "Surfaces", "SurpassCamera", "Volume"] - - if not typeValue in possibleTypeValues: + if typeValue not in self._mPossibleTypeFilters: raise ValueError("Invalid value for typeValue.") # Get the factory @@ -1683,8 +2357,8 @@ def _isOfType(self, obj, typeValue): return factory.IsCells(obj) elif typeValue == 'ClippingPlane': return factory.IsClippingPlane(obj) - elif typeValue == 'Dataset': - return factory.IsDataset(obj) + elif typeValue == 'DataSet': + return factory.IsDataSet(obj) elif typeValue == 'Filaments': return factory.IsFilaments(obj) elif typeValue == 'Frame': @@ -1701,15 +2375,17 @@ def _isOfType(self, obj, typeValue): return factory.IsSurpassCamera(obj) elif typeValue == 'Volume': return factory.IsVolume(obj) + elif typeValue == 'ReferenceFrames': + # The factory does not have a Is...() method for reference frames + return factory.ToReferenceFrames(obj) is not None else: raise ValueError('Bad value for ''typeValue''.') def _ispc(self): """Returns true if pIceImarisConnector is being run on Windows. For internal use only! -:return: True if pIceImarisConnector is being run on Windows, False otherwise. -:rtype: Boolean - + :return: True if pIceImarisConnector is being run on Windows, False otherwise. + :rtype: Boolean """ return platform.system() == "Windows" @@ -1717,18 +2393,17 @@ def _ispc(self): def _isSupportedPlatform(self): """Returns True if running on a supported platform. For internal use only! -:return: True if pIceImarisConnector is being run on Windows or Mac OS X, False otherwise. -:rtype: Boolean - + :return: True if pIceImarisConnector is being run on Windows or Mac OS X, False otherwise. + :rtype: Boolean """ return (self._ispc() or self._ismac()) def _startImarisServerIce(self): - """Starts an instance of ImarisServerIce and waits until it is ready to accept connections. For internal use only! - -:return: True if ImarisServerIce could be started successfully, False otherwise. -:rtype: Boolean + """Starts an instance of ImarisServerIce and waits until it is ready to accept connections. For internal + use only! + :return: True if ImarisServerIce could be started successfully, False otherwise. + :rtype: Boolean """ # Imaris only runs on Windows and Mac OS X @@ -1751,7 +2426,7 @@ def _startImarisServerIce(self): print(v) return False except: - print "Unexpected error:", sys.exc_info()[0] + print("Unexpected error:", sys.exc_info()[0]) return False if not process: @@ -1767,3 +2442,5 @@ def _startImarisServerIce(self): t = time.time() return False + + diff --git a/pIceImarisConnector/test/.gitignore b/pIceImarisConnector/test/.gitignore new file mode 100644 index 0000000..a348e50 --- /dev/null +++ b/pIceImarisConnector/test/.gitignore @@ -0,0 +1 @@ +/__pycache__/ diff --git a/pIceImarisConnector/test/HelloWorldXT.py b/pIceImarisConnector/test/HelloWorldXT.py new file mode 100644 index 0000000..fa4f560 --- /dev/null +++ b/pIceImarisConnector/test/HelloWorldXT.py @@ -0,0 +1,23 @@ +# +# +# +# Python3XT::HelloWorldXT(%i) +# +# +# + +from pIceImarisConnector import pIceImarisConnector +import tkinter + +def HelloWorldXT(aImarisId): + + # Instantiate an IceImarisConnector object + conn = pIceImarisConnector(aImarisId) + + # Display version info in a dialog + top = tkinter.Tk() + top.title("Hello World!") + l = tkinter.Label(top, text=f"... from pIceImarisConnector {conn.__version__} " + f"and {conn.mImarisApplication.GetVersion()}") + l.pack() + top.mainloop() diff --git a/pIceImarisConnector/test/PyramidalCell.ims b/pIceImarisConnector/test/PyramidalCell.ims new file mode 100644 index 0000000..f742b1e Binary files /dev/null and b/pIceImarisConnector/test/PyramidalCell.ims differ diff --git a/pIceImarisConnector/test/SwimmingAlgae.ims b/pIceImarisConnector/test/SwimmingAlgae.ims new file mode 100644 index 0000000..36b8ca0 Binary files /dev/null and b/pIceImarisConnector/test/SwimmingAlgae.ims differ diff --git a/test/test_pIceImarisConnector.py b/pIceImarisConnector/test/TestPIcePyramidalCellXT.py similarity index 73% rename from test/test_pIceImarisConnector.py rename to pIceImarisConnector/test/TestPIcePyramidalCellXT.py index 68607d2..550f40c 100644 --- a/test/test_pIceImarisConnector.py +++ b/pIceImarisConnector/test/TestPIcePyramidalCellXT.py @@ -1,78 +1,30 @@ -''' -Name pIceImarisApplication Test Unit -Purpose Test pIceImarisApplication - -Author Aaron Ponti - -Created 21-03-2013 -Copyright (c) Aaron Ponti 2013 -Licence GPL v2 -''' +# +# +# +# Python3XT::TestPIcePyramidalCellXT(%i) +# +# +# import os import numpy as np from pIceImarisConnector import pIceImarisConnector -if __name__ == '__main__': - - # ImarisConnector version - # ========================================================================= - conn = pIceImarisConnector() - print('Testing IceImarisConnector version ' + conn.version) - del(conn) - - # Instantiate pIceImarisConnector object without parameters - # ========================================================================= - print('Instantiate pIceImarisConnector conn1 object without parameters...') - conn1 = pIceImarisConnector() - conn1.display() - conn1.info() - - # Instantiate pIceImarisConnector object with existing instance as parameter - # ========================================================================= - print('Instantiate pIceImarisConnector object conn2 with existing instance conn1 as parameter...') - conn2 = pIceImarisConnector(conn1) - - # Check that conn1 and conn2 are the same object - # ========================================================================= - print("Check that conn1 and conn2 are the same object...") - assert(conn1 is conn2) - - # Delete the objects - # ========================================================================= - del(conn1) - del(conn2) - - # Create an ImarisConnector object - # ========================================================================= - print('Create a pIceImarisConnector object...') - conn = pIceImarisConnector() - - # Start Imaris - # ========================================================================= - print('Start Imaris...') - assert(conn.startImaris() == True) +def TestPIcePyramidalCellXT(aImarisId): - # Test that the connection is valid - # ========================================================================= - print('Get version...') - assert(conn.getImarisVersionAsInteger() > 0) - print('Test if connection is alive...') - assert(conn.isAlive() == True) + # Instantiate the pIceImarisConnector object + conn = pIceImarisConnector(aImarisId) - # Open a file + # Open the PyramidalCell file # =======================================================s================== print('Load file...') - currFilePath = os.path.realpath(__file__) - currPath = os.path.dirname(currFilePath) - filename = os.path.join(currPath, 'PyramidalCell.ims') - conn.mImarisApplication.FileOpen(filename, '') - + conn.loadPyramidalCellTestDataset() + # Check that there is something loaded # ========================================================================= print('Test that the file was loaded...') - assert(conn.mImarisApplication.GetDataSet().GetSizeX > 0) + assert(conn.mImarisApplication.GetDataSet().GetSizeX() > 0) # Check the extends # ========================================================================= @@ -129,7 +81,7 @@ child = conn.getAllSurpassChildren(False, 'Spots') assert(len(child) == 1) spot = child[0] - assert(callable(getattr(spot, 'GetPositionsXYZ')) == True) + assert(callable(getattr(spot, 'GetPositionsXYZ')) is True) # Get the coordinates pos = spot.GetPositionsXYZ() @@ -137,10 +89,10 @@ # These are the expected spot coordinates print('Check spot coordinates and conversions units<->pixels...') POS = [ - [18.5396, 1.4178, 8.7341], - [39.6139, 14.8819, 9.0352], - [35.1155, 9.4574, 9.0352], - [12.3907, 21.6221, 11.7459]] + [18.5396, 1.4178, 8.7341], + [39.6139, 14.8819, 9.0352], + [35.1155, 9.4574, 9.0352], + [12.3907, 21.6221, 11.7459]] assert(np.all(abs(np.array(pos) - np.array(POS)) < 1e-4)) @@ -214,6 +166,11 @@ print('Get the data volume...') stack = conn.getDataVolume(0, 0) + # Get and check a data slice + print('Get and check a data slice...') + slice = conn.getDataSlice(34, 0, 0) + assert(np.all(stack[34, :, :] == slice)) + print('Check the data volume type...') assert(stack.dtype == conn.getNumpyDatatype()) @@ -231,6 +188,11 @@ print('Get the data volume by explicitly passing an iDataSet object...') stack = conn.getDataVolume(0, 0, conn.mImarisApplication.GetDataSet()) + # Get a slice + print('Get and check a data slice by explicitly passing an iDataSet object...') + slice = conn.getDataSlice(34, 0, 0, conn.mImarisApplication.GetDataSet()) + assert(np.all(stack[34, :, :] == slice)) + print('Check the data volume type...') assert(stack.dtype == conn.getNumpyDatatype()) @@ -306,32 +268,27 @@ # Compare (rounding errors allowed) assert(all([abs(x - y) < 1e-2 for x, y in zip(c, current)])) - # Close Imaris - # ========================================================================= - print('Close Imaris...') - assert(conn.closeImaris(1) == 1) - - # Create an ImarisConnector object with starting index 0 - # ========================================================================= - print('Create an IceImarisConnector object...') - conn = pIceImarisConnector() - - # Start Imaris - # ========================================================================= - print('Start Imaris...') - assert(conn.startImaris() == 1) - - # Send a data volume that will force creation of a compatible dataset - # ========================================================================= - print('Send volume (force dataset creation)...') - stack = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], \ - [10, 11, 12]], [[13, 14, 15], [16, 17, 18]]], np.uint16) - conn.setDataVolume(stack, 0, 0) + # Copy channel a couple of times + # ========================================================================== + print('Test copying channels...') + conn.mImarisApplication.GetDataSet().SetChannelName(0, 'One') + conn.copyChannels(0) + conn.copyChannels([0, 1]) + conn.copyChannels([0, 2]) + conn.copyChannels(3) + channelNames = conn.getChannelNames() + assert(channelNames[0] == 'One') + assert(channelNames[1] == 'Copy of One') + assert(channelNames[2] == 'Copy of One') + assert(channelNames[3] == 'Copy of Copy of One') + assert(channelNames[4] == 'Copy of One') + assert(channelNames[5] == 'Copy of Copy of One') + assert(channelNames[6] == 'Copy of Copy of Copy of One') # Create a dataset # ========================================================================= print('Create a dataset (replace existing one)...') - conn.createDataset('uint8', 100, 200, 50, 3, 10, 0.20, 0.25, 0.5, 0.1) + conn.createDataSet('uint16', 100, 200, 50, 3, 10, 0.20, 0.25, 0.5, 0.1) # Check sizes # ========================================================================= @@ -351,6 +308,18 @@ assert(voxelSizes[1] == 0.25) assert(voxelSizes[2] == 0.5) + # Send a data volume + # ========================================================================= + print('Send volume (force dataset creation)...') + stack = np.zeros((100, 200, 50), dtype = np.uint16) + conn.setDataVolume(stack, 0, 0) + + # Test retrieving volume and slice for a non 8-bit dataset + print('Test retrieving volume and slice for a non 8-bit dataset...') + volume16 = conn.getDataVolume(0, 0) + slice16 = conn.getDataSlice(1, 0, 0) + assert(np.all(volume16[1, :, :] == slice16)) + # Check the time delta # ========================================================================= print('Check time interval...') @@ -359,29 +328,14 @@ # Check transferring volume data # ========================================================================= print('Check two-way data volume transfer...') - data = np.empty((2, 255, 255), dtype=np.uint8) + data = np.zeros((2, 255, 255), dtype=np.uint8) x = np.linspace(1, 255, 255) y = np.linspace(1, 255, 255) xv, yv = np.meshgrid(x, y) data[0, :, :] = x data[1, :, :] = y data = data[:, :, 1:255] # Make it not square in xy - conn.createDataset('uint8', 254, 255, 2, 1, 1) + conn.createDataSet('uint8', 254, 255, 2, 1, 1) conn.setDataVolume(data, 0, 0) dataOut = conn.getDataVolume(0, 0) assert(np.array_equal(data, dataOut)) - - # Close Imaris - # ========================================================================= - print('Close Imaris...') - assert(conn.closeImaris(True) == 1) - - # Make sure Imaris is closed - # ========================================================================= - print('Make sure Imaris is closed...') - assert(conn.isAlive() == False) - - # All done - # ========================================================================= - print('') - print('All test succesfully run.') diff --git a/pIceImarisConnector/test/TestPIceSwimmingAlgaeXT.py b/pIceImarisConnector/test/TestPIceSwimmingAlgaeXT.py new file mode 100644 index 0000000..c38c728 --- /dev/null +++ b/pIceImarisConnector/test/TestPIceSwimmingAlgaeXT.py @@ -0,0 +1,77 @@ +# +# +# +# Python3XT::TestPIceSwimmingAlgaeXT(%i) +# +# +# + +import os +import numpy as np + +from pIceImarisConnector import pIceImarisConnector + +def TestPIceSwimmingAlgaeXT(aImarisId): + + # Instantiate the pIceImarisConnector object + conn = pIceImarisConnector(aImarisId) + + # Open the SwimmingAlgae file + # =======================================================s================== + print('Load file...') + conn.loadSwimmingAlgaeTestDataset() + + # Check that there is something loaded + # ========================================================================= + print('Test that the file was loaded...') + assert(conn.mImarisApplication.GetDataSet().GetSizeX() > 0) + + # Get the Spots object + print('Test retrieving spot object...') + iSpots = conn.getAllSurpassChildren(False, 'Spots') + assert (len(iSpots) == 1) + + # Get the tracks + print('Test retrieving tracks from spot object...') + [tracks, startTimes] = conn.getTracks(iSpots[0]) + + # Compare + TRACKS_0 = np.array([ + [257.0569, 158.0890, 0.5000], + [258.2019, 160.3281, 0.5000], + [258.6424, 161.7611, 0.5000], + [257.0615, 162.8971, 0.5000], + [254.7822, 163.0764, 0.5000], + [252.9628, 162.2183, 0.5000], + [251.9430, 160.6685, 0.5000], + [252.0315, 159.2506, 0.5000], + [252.5433, 157.9091, 0.5000], + [254.0479, 156.8815, 0.5000], + [255.7876, 156.3626, 0.5000], + [257.4710, 156.3670, 0.5000]]) + + TRACKS_1 = np.array([ + [245.0000, 125.4513, 0.5000], + [248.0088, 127.2925, 0.5000], + [251.1482, 128.9230, 0.5000], + [254.2048, 130.3164, 0.5000], + [257.1553, 132.4333, 0.5000], + [259.4069, 134.9209, 0.5000], + [261.9462, 137.7944, 0.5000], + [264.0524, 140.6828, 0.5000]]) + + TRACKS_2 = np.array([ + [284.0000, 128.0667, 0.5000], + [281.3237, 130.0378, 0.5000], + [278.5699, 131.9248, 0.5000], + [275.9659, 133.9807, 0.5000]]) + + # Check spot coordinates + print('Test tracks coordinates...') + assert (np.all(abs(tracks[0] - TRACKS_0) < 1e-4)) + assert (np.all(abs(tracks[1] - TRACKS_1) < 1e-4)) + assert (np.all(abs(tracks[2] - TRACKS_2) < 1e-4)) + + # Check start time points + print('Test timepoints...') + assert (np.all(startTimes == np.array([0, 4, 8]))) diff --git a/pIceImarisConnector/test/TestStaticMethods.py b/pIceImarisConnector/test/TestStaticMethods.py new file mode 100644 index 0000000..0e4bba0 --- /dev/null +++ b/pIceImarisConnector/test/TestStaticMethods.py @@ -0,0 +1,160 @@ +# This file tests static functionality of pIceImarisConnector and does not require to +# be connected to Imaris to run. + +from pIceImarisConnector import pIceImarisConnector as pIce +import numpy as np +import math + +# testCalcRotationBetweenVectors3D +def testCalcRotationBetweenVectors3D(start, dest, expected_q, epsilon=1e-4): + q = pIce.calcRotationBetweenVectors3D(start, dest) + return np.all(np.abs(q - expected_q) <= epsilon) + +# testMapAxisAngleToQuaternion +def testMapAxisAngleToQuaternion(axis, angle, expected_q, epsilon=1e-4): + q = pIce.mapAxisAngleToQuaternion(axis, angle) + return np.all(np.abs(q - expected_q) <= epsilon) + +# testMapAxisAngleToRotationMatrix +def testMapAxisAngleToRotationMatrix(axis, angle, expected_R, expected_x, expected_y, expected_z, epsilon=1e-4): + R, x, y, z = pIce.mapAxisAngleToRotationMatrix(axis, angle) + return np.all(np.abs(R - expected_R) <= epsilon) and np.all(np.abs(x - expected_x) <= epsilon) and \ + np.all(np.abs(y - expected_y) <= epsilon) and np.all(np.abs(z - expected_z) <= epsilon) + +# testMapQuaternionToRotationMatrix +def testMapQuaternionToRotationMatrix(q, expected_R, expected_x, expected_y, expected_z, epsilon=1e-4): + R, x, y, z = pIce.mapQuaternionToRotationMatrix(q) + return np.all(np.abs(R - expected_R) <= epsilon) and np.all(np.abs(x - expected_x) <= epsilon) and \ + np.all(np.abs(y - expected_y) <= epsilon) and np.all(np.abs(z - expected_z) <= epsilon) + +# testMultiplyQuaternions +def testMultiplyQuaternions(q1, q2, expected_q, epsilon=1e-4): + q = pIce.multiplyQuaternions(q1, q2) + return np.all(np.abs(q - expected_q) <= epsilon) + +# testMapAxisAngleToQuaternion +def testNormalize(v, expected_n, epsilon=1e-4): + n = pIce.normalize(v) + return np.all(np.abs(n - expected_n) <= epsilon) + +# testQuaternionConjugate +def testQuaternionConjugate(q, expected_qc, epsilon=1e-4): + qc = pIce.quaternionConjugate(q) + return np.all(np.abs(qc - expected_qc) <= epsilon) + +# ====================================================================================================================== + +# +# calcRotationBetweenVectors3D +# +assert (testCalcRotationBetweenVectors3D([1, 0, 1], [0, 1, 0], [-0.5000, 0.0000, 0.5000, 0.7071])) +assert (testCalcRotationBetweenVectors3D([0, 0, 0], [0, 0, 0], [0.0000, 0.0000, 0.0000, 0.7071])) +assert (testCalcRotationBetweenVectors3D([1.8339, -2.2588, 0.8622], [0.3188, -1.3077, -0.4336], + [0.2634, 0.1338, -0.2098, 0.9321])) + +# +# mapAxisAngleToQuaternion +# +assert (testMapAxisAngleToQuaternion([0.0, 1.0, 0.0], 0.0, [0.0, 0.0, 0.0, 1.0])) +assert (testMapAxisAngleToQuaternion([0.0, 0.0, 0.0], 0.0, [0.0, 0.0, 0.0, 1.0])) +assert (testMapAxisAngleToQuaternion([0.0, 1.0, 0.0], math.pi/4.0, [0.0, 0.3827, 0.0, 0.9239])) +assert (testMapAxisAngleToQuaternion([0.0, -1.0, 0.0], math.pi/4.0, [0.0, -0.3827, 0.0, 0.9239])) +assert (testMapAxisAngleToQuaternion([1.0, 1.0, 1.0], math.pi/4.0, [0.2209, 0.2209, 0.2209, 0.9239])) +assert (testMapAxisAngleToQuaternion([0.0, 1.0, 0.0], math.pi, [0.0, 1.0, 0.0, 0.0])) +assert (testMapAxisAngleToQuaternion([-1.0689, -0.8095, -2.9443], 1.4384, [-0.2177, -0.1648, -0.5995, 0.7523])) + +# +# mapAxisAngleToRotationMatrix +# +expected_R = [[1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0]] +expected_x = [1.0, 0.0, 0.0] +expected_y = [0.0, 1.0, 0.0] +expected_z = [0.0, 0.0, 1.0] +assert (testMapAxisAngleToRotationMatrix([0.0, 1.0, 0.0], 0.0, expected_R, + expected_x, expected_y, expected_z)) + +expected_R = [[0.7071, 0.0, 0.7071, 0.0], + [0.0, 1.0, 0.0, 0.0], + [-0.7071, 0.0, 0.7071, 0.0], + [0.0, 0.0, 0.0, 1.0]] +expected_x = [0.7071, 0.0, -0.7071] +expected_y = [0.0, 1.0, 0.0] +expected_z = [0.7071, 0.0, 0.7071] +assert (testMapAxisAngleToRotationMatrix([0.0, 1.0, 0.0], math.pi / 4.0, expected_R, + expected_x, expected_y, expected_z)) + +expected_R = [[0.6552, -0.7467, -0.1147, 0], + [0.5398, 0.5690, -0.6204, 0.0], + [0.5285, 0.3445, 0.7759, 0.0], + [0.0, 0.0, 0.0, 1.0]] +expected_x = [0.6552, 0.5398, 0.5285] +expected_y = [-0.7467, 0.5690, 0.3445] +expected_z = [-0.1147, -0.6204, 0.7759] +assert (testMapAxisAngleToRotationMatrix([0.3, -0.2, 0.4], math.pi / 3.0, expected_R, + expected_x, expected_y, expected_z)) + +# +# mapQuaternionToRotationMatrix +# +expected_R = [[1.0, 0.0, 0.0, 0.0], + [0.0, -1.0, 0.0, 0.0], + [0.0, 0.0, -1.0, 0.0], + [0.0, 0.0, 0.0, 1.0]] +expected_x = [1.0, 0.0, 0.0] +expected_y = [0.0, -1.0, 0.0] +expected_z = [0.0, 0.0, -1.0] +assert (testMapQuaternionToRotationMatrix([1.0, 0.0, 0.0, 0.0], expected_R, expected_x, expected_y, expected_z)) + +expected_R = [[1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0]] +expected_x = [1.0, 0.0, 0.0] +expected_y = [0.0, 1.0, 0.0] +expected_z = [0.0, 0.0, 1.0] +assert (testMapQuaternionToRotationMatrix([0.0, 0.0, 0.0, 0.0], expected_R, expected_x, expected_y, expected_z)) + +expected_R = [[0.1667, 0.9513, -0.2592, 0.0], + [0.4018, 0.1746, 0.8989, 0.0], + [0.9004, -0.2540, -0.3532, 0.0], + [0.0, 0.0, 0.0, 1.0]] +expected_x = [0.1667, 0.4018, 0.9004] +expected_y = [0.9513, 0.1746, -0.2540] +expected_z = [-0.2592, 0.8989, -0.3532] +assert (testMapQuaternionToRotationMatrix([1.4090, 1.4172, 0.6715, -1.2075], + expected_R, expected_x, expected_y, expected_z)) + +# +# quaternionConjugate +# +assert(testQuaternionConjugate([0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])) +assert(testQuaternionConjugate([1.0, 1.0, 1.0, 1.0], [0.5000, -0.5000, -0.5000, -0.5000])) +assert(testQuaternionConjugate([-1.0, -1.0, -1.0, -1.0], [-0.5000, 0.5000, 0.5000, 0.5000])) +assert(testQuaternionConjugate([10.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0])) +assert(testQuaternionConjugate([-0.0631, 0.7147, -0.2050, -0.1241], [-0.0834, -0.9448, 0.2710, 0.1641])) + +# +# multiplyQuaternions +# +assert (testMultiplyQuaternions([0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])) +assert (testMultiplyQuaternions([1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])) +assert (testMultiplyQuaternions([1.0, 1.0, 1.0, 1.0], [-1.0, -1.0, -1.0, -1.0], [-0.5000, -0.5000, -0.5000, 0.5000])) +assert (testMultiplyQuaternions([1.6302, 0.4889, 1.0347, 0.7269], [-0.3034, 0.2939, -0.7873, 0.8884], + [0.2017, 0.6054, 0.3647, 0.6780])) + +# +# normalize +# +assert (testNormalize([0.0, 0.0, 0.0], [0.0, 0.0, 0.0])) +assert (testNormalize(np.array([0.0, 0.0, 0.0]), [0.0, 0.0, 0.0])) +assert (testNormalize([1.0, 0.0, 0.0], [1.0, 0.0, 0.0])) +assert (testNormalize([[1.0], [0.0], [0.0]], [[1.0], [0.0], [0.0]])) # Column vector + +# Test calling a static method from the object +conn = pIce() +n = conn.normalize([0.0, 3.0, 4.0]) +expected_n = [0.0, 0.6, 0.8] +assert(np.all(np.abs(n - expected_n) <= 1e-4)) diff --git a/pIceImarisConnector/test/__init__.py b/pIceImarisConnector/test/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pIceImarisConnector/test/__init__.py @@ -0,0 +1 @@ + diff --git a/setup.py b/setup.py index 6d2cf9d..d72ea3e 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,20 @@ -from distutils.core import setup +from setuptools import setup +from pIceImarisConnector import pIceImarisConnector setup(name='pIceImarisConnector', - version='0.3.1', + version=pIceImarisConnector.__version__, author='Aaron Ponti', author_email='aaron.ponti@bsse.ethz.ch', - url='http://www.scs2.net/next/index.php?id=110', - download_url='http://www.scs2.net/next/index.php?id=110', - description='IceImarisConnector for python (pIceImarisConnector) is a simple commodity class that eases communication between Bitplane Imaris and python using the Imaris XT interface.', - long_description='', + url='https://github.com/aarpon/pIceImarisConnector', + download_url='https://github.com/aarpon/pIceImarisConnector/releases', + project_urls={ + "Bug Tracker": "https://github.com/aarpon/pIceImarisConnector/issues", + "Documentation": "https://piceimarisconnector.readthedocs.io/", + "Source Code": "https://github.com/aarpon/pIceImarisConnector", + }, + description='Easier communication between Bitplane Imaris and python over ImarisXT.', + long_description='IceImarisConnector for python (pIceImarisConnector) is a simple commodity class that eases communication between Bitplane Imaris and python using the Imaris XT interface.', + include_package_data=True, packages=['pIceImarisConnector'], package_dir={'pIceImarisConnector': 'pIceImarisConnector'}, provides=['pIceImarisConnector'], @@ -19,8 +26,9 @@ 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 'Topic :: Scientific/Engineering' ], requires=['numpy'] - ) +) diff --git a/test/HelloWorldXT.py b/test/HelloWorldXT.py deleted file mode 100644 index a83fc32..0000000 --- a/test/HelloWorldXT.py +++ /dev/null @@ -1,28 +0,0 @@ -# Hello World! XT example -# -# -# -# -# -# PythonXT::HelloWorldXT(#i) -# -# -# -# - -from pIceImarisConnector import pIceImarisConnector -import Tkinter - -def HelloWorldXT(aImarisId): - - # Instantiate an IceImarisConnector object - conn = pIceImarisConnector(aImarisId) - - # Display version info in a dialog - top = Tkinter.Tk() - top.title("Hello World!") - l = Tkinter.Label(top, - text = '... from pIceImarisConnector ' + conn.version + - ' and ' + conn.mImarisApplication.GetVersion()) - l.pack() - top.mainloop() diff --git a/test/PyramidalCell.ims b/test/PyramidalCell.ims deleted file mode 100644 index 88ad8ff..0000000 Binary files a/test/PyramidalCell.ims and /dev/null differ diff --git a/upload_pip_package_to_test_pypi.bat b/upload_pip_package_to_test_pypi.bat new file mode 100644 index 0000000..e37c14e --- /dev/null +++ b/upload_pip_package_to_test_pypi.bat @@ -0,0 +1,2 @@ +@ECHO OFF +python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*.whl \ No newline at end of file