├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .readthedocs.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ ├── contents.png │ ├── file.png │ ├── mpl.css │ ├── navigation.png │ ├── region_ds9.jpg │ └── region_mpl.png ├── _templates │ ├── autosummary │ │ ├── base.rst │ │ ├── class.rst │ │ └── module.rst │ ├── indexsidebar.html │ └── layout.html ├── api.rst ├── changelog.rst ├── conf.py ├── examples ├── examples.rst ├── figures │ ├── demo_filter_mask.py │ ├── pspc_skyview.fits │ ├── region_drawing.py │ ├── region_drawing2.py │ ├── test.reg │ └── test02.reg ├── getting_started.rst ├── index.rst └── installation.rst ├── examples ├── demo_helper.py ├── demo_print_region.py ├── demo_region01.py ├── demo_region02.py ├── demo_region03.py ├── demo_region04.py ├── demo_region_filter01.py ├── sample_fits01.header ├── sample_fits02.header ├── test.header ├── test.reg ├── test01.reg ├── test01_ciao.reg ├── test01_ciao_physical.reg ├── test01_ds9_physical.reg ├── test01_fk5.reg ├── test01_fk5_degree.reg ├── test01_fk5_sexagecimal.reg ├── test01_gal.reg ├── test01_img.reg ├── test01_mixed.reg ├── test01_print.reg ├── test02.reg ├── test04_img.reg ├── test_annuli.reg ├── test_annuli_ciao.reg ├── test_annuli_wcs.reg ├── test_context.reg └── test_text.reg ├── pyproject.toml ├── pyregion ├── __init__.py ├── _region_filter.pyx ├── c_numpy.pxd ├── c_python.pxd ├── conftest.py ├── core.py ├── ds9_attr_parser.py ├── ds9_region_parser.py ├── geom.h ├── mpl_helper.py ├── parser_helper.py ├── physical_coordinate.py ├── region_numbers.py ├── region_to_filter.py ├── setup_package.py ├── tests │ ├── coveragerc │ ├── data │ │ ├── sample_fits01.header │ │ ├── sample_fits02.header │ │ ├── sample_fits03.header │ │ ├── sample_fits04.header │ │ ├── test.header │ │ ├── test01.reg │ │ ├── test01_ciao.reg │ │ ├── test01_ciao_physical.reg │ │ ├── test01_ds9_physical.reg │ │ ├── test01_fk4.reg │ │ ├── test01_fk5.reg │ │ ├── test01_fk5_degree.reg │ │ ├── test01_fk5_sexagecimal.reg │ │ ├── test01_gal.reg │ │ ├── test01_icrs.reg │ │ ├── test01_img.reg │ │ ├── test01_mixed.reg │ │ ├── test01_print.reg │ │ ├── test02.reg │ │ ├── test02_1_fk5.reg │ │ ├── test02_1_img.reg │ │ ├── test03_ciao_physical.reg │ │ ├── test03_fk5.reg │ │ ├── test03_gal.reg │ │ ├── test03_icrs.reg │ │ ├── test03_img.reg │ │ ├── test04_img.reg │ │ ├── test_annuli.reg │ │ ├── test_annuli_ciao.reg │ │ ├── test_annuli_wcs.reg │ │ ├── test_context.reg │ │ └── test_text.reg │ ├── test_cube.py │ ├── test_ds9_attr_parser.py │ ├── test_ds9_region_parser.py │ ├── test_get_mask.py │ ├── test_parser_helper.py │ ├── test_region.py │ ├── test_region_numbers.py │ └── test_wcs.py ├── wcs_converter.py └── wcs_helper.py └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | actions: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | schedule: 5 | # run every day at 4am UTC 6 | - cron: '0 4 * * *' 7 | push: 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | 17 | tests: 18 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@924441154cf3053034c6513d5e06c69d262fb9a6 # v1.13.0 19 | with: 20 | envs: | 21 | - macos: py310-test-oldestdeps 22 | - macos: py311-test 23 | - macos: py312-test 24 | - linux: py310-test-oldestdeps 25 | - linux: py311-test 26 | - linux: py312-test 27 | - linux: py312-test-devdeps 28 | - windows: py310-test-oldestdeps 29 | - windows: py311-test 30 | - windows: py312-test 31 | coverage: 'codecov' 32 | 33 | publish: 34 | needs: tests 35 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@8c0fde6f7e926df6ed7057255d29afa9c1ad5320 # v1.16.0 36 | with: 37 | test_extras: test 38 | test_command: pytest -p no:warnings --pyargs pyregion 39 | targets: | 40 | - cp*-manylinux_x86_64 41 | - cp*-macosx_x86_64 42 | - cp*-macosx_arm64 43 | - cp*-win_amd64 44 | 45 | secrets: 46 | pypi_token: ${{ secrets.pypi_token }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.py[cod] 3 | *.a 4 | *.o 5 | *.so 6 | __pycache__ 7 | 8 | # Ignore .c files by default to avoid including generated code. If you want to 9 | # add a non-generated .c extension, use `git add -f filename.c`. 10 | *.c 11 | 12 | # Other generated files 13 | */version.py 14 | */cython_version.py 15 | htmlcov 16 | .coverage 17 | MANIFEST 18 | .ipynb_checkpoints 19 | 20 | # Sphinx 21 | docs/api 22 | docs/_build 23 | 24 | # Eclipse editor project files 25 | .project 26 | .pydevproject 27 | .settings 28 | 29 | # Pycharm editor project files 30 | .idea 31 | 32 | # Packages/installer info 33 | *.egg 34 | *.egg-info 35 | dist 36 | build 37 | eggs 38 | parts 39 | bin 40 | var 41 | sdist 42 | develop-eggs 43 | .installed.cfg 44 | distribute-*.tar.gz 45 | 46 | # Other 47 | .cache 48 | .tox 49 | .*.sw[op] 50 | *~ 51 | 52 | # Mac OSX 53 | .DS_Store 54 | 55 | v 56 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.10" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # Optionally build your docs in additional formats such as PDF and ePub 19 | formats: html 20 | 21 | python: 22 | install: 23 | - method: pip 24 | path: . 25 | extra_requirements: 26 | - docs 27 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 2.3.0 (2024-10-04) 2 | ------------------ 3 | 4 | - Fixed build and installation and bump minimum required version of Python to 3.10. [#164, #168] 5 | 6 | 7 | 2.2.0 (2022-12-09) 8 | ------------------ 9 | 10 | - Fixed build, installation, and testing issues. [#161] 11 | 12 | 13 | 2.1.1 (2021-06-25) 14 | ------------------ 15 | 16 | - Support for angular units of 'd' and 'r' added. 17 | 18 | 19 | 2.0 (Oct 14, 2017) 20 | ------------------ 21 | 22 | This is a major new release of **pyregion**. There are some API changes 23 | (listed below), but overall our goal was to keep backwards-compatibility 24 | as much as possible while fixing code and installation issues 25 | and refactor the internals to use Astropy more. 26 | 27 | We note that we are developing a new **regions** package that is supposed 28 | to become a superset of the functionality that is now in **pyregion** and 29 | might be moved in the Astropy core package as **astropy.regions** in the future. 30 | The main difference is that it represents regions as classes and uses Astropy 31 | angle and coordinate objects, allowing for easier region-based analysis. 32 | It is not feature complete, especially the DS9 region file parser is not a 33 | complete replacement for **pyregion** yet. Still, you are encouraged to try 34 | it out ( http://astropy-regions.readthedocs.io/ ), give feedback or even contribute. 35 | 36 | For **pyregion**, the plan is to continue to do bugfixes and releases, 37 | but to keep API changes to a minimum to avoid breaking existing scripts or pipelines. 38 | If you have any questions or issues or requests, please open an issue in the **pyregion** 39 | issue tracker on Github. 40 | 41 | 42 | API Changes 43 | ^^^^^^^^^^^ 44 | 45 | - Removed ``rot_wrt_axis`` parameter from ``ShapeList`` and internal methods. 46 | 47 | - ``ShapeList.as_imagecoord`` no longer accepts a ``asropy.wcs.WCS`` object. The 48 | conversion from pixel to image coordinates depends on the center of the 49 | image defined in ``astropy.io.fits.Header`` in order to agree with DS9. 50 | 51 | - ``pyregion.ds9_region_parser`` 52 | 53 | - ``RegionParser.sky_to_image`` now calls its first parameter ``shape_list`` 54 | instead of ``l``. 55 | 56 | - ``pyregion.extern`` 57 | 58 | - ``kapteyn_celestial`` removed. 59 | 60 | - ``pyregion.wcs_converter`` 61 | 62 | - ``convert_to_imagecoord`` changed signature with the switch to Astropy 63 | and takes a ``Shape`` object. 64 | 65 | - ``convert_physical_to_imagecoord`` changed signature to accept a ``Shape`` 66 | object. 67 | 68 | - ``pyregion.wcs_helper`` 69 | 70 | - All public methods and constants removed. They are replaced by Astropy, 71 | or replaced by private methods. 72 | 73 | 74 | Other Changes and Additions 75 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 76 | 77 | - Astropy is used for all sky to image coordinate conversions. Science results may 78 | change, as `SIP `_ 79 | and `distortion paper `_ 80 | corrections are now used if present in the FITS file. 81 | 82 | - Headers with more then 2 axes are now supported; only the celestial axes are 83 | used. 84 | 85 | - Rotation angles are measured from the Y-axis instead of the X-axis, in order 86 | to agree with DS9 and potentially other astronomy software. This is a change 87 | from previous behavior, but only affects images with non-orthogonal axes. 88 | Previously, this behavior was controlled by the ``rot_wrt_axis`` parameter. 89 | 90 | - Astropy 1.0 is now required. 91 | 92 | - Shape conversion for multi-dimenstional HDU does not raise exceptions. 93 | 94 | - Parser supports hex color in attributes 95 | 96 | 1.2 (Aug 11, 2016) 97 | ------------------ 98 | 99 | - https://pypi.org/project/pyregion/1.2/ 100 | - The changelog for this release is incomplete. 101 | - We'll start collecting a complete changelog starting after this release. 102 | 103 | - This release brings major changes to the code, docs and test setup, 104 | the package was converted to an Astropy affiliated package. 105 | - There are only a few bugfixes and there should be no changes 106 | that break scripts or change results for pyregion users. 107 | 108 | 109 | 1.1.4 (Oct 26, 2014) 110 | -------------------- 111 | 112 | - https://pypi.org/project/pyregion/1.1.4/ 113 | - The changelog for this release is incomplete. 114 | - Change tag attribute from string to list of strings. [#26] 115 | 116 | 1.1 (March 15, 2013) 117 | -------------------- 118 | 119 | - https://pypi.org/project/pyregion/1.1/ 120 | - No changelog available 121 | 122 | 1.0 (Sep 14, 2010) 123 | ------------------ 124 | 125 | - https://pypi.org/project/pyregion/1.0/ 126 | - First stable release 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Jae-Joon Lee 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include CHANGES.rst 4 | include setup.cfg 5 | include setup.py 6 | include pyproject.toml 7 | 8 | recursive-include pyregion *.pyx *.c *.pxd 9 | recursive-include docs * 10 | 11 | prune build 12 | prune docs/_build 13 | prune docs/api 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyregion 2 | ======== 3 | 4 | pyregion is a python module to parse ds9 region files. 5 | It also supports ciao region files. 6 | 7 | * Code: https://github.com/astropy/pyregion 8 | * Docs: https://pyregion.readthedocs.io 9 | * PyPI: http://pypi.python.org/pypi/pyregion 10 | 11 | Installation: ``pip install pyregion`` 12 | 13 | Lead developer: Jae-Joon Lee ([@leejjoon](http://github.com/leejjoon)) 14 | 15 | FEATURES 16 | -------- 17 | 18 | * ds9 and ciao region files. 19 | * (physical, wcs) coordinate conversion to the image coordinate. 20 | * convert regions to matplotlib patches. 21 | * convert regions to spatial filter (i.e., generate mask images) 22 | 23 | LICENSE 24 | ------- 25 | 26 | All files are under MIT License. See LICENSE. 27 | 28 | Status 29 | ------ 30 | 31 | .. image:: https://travis-ci.org/astropy/pyregion.svg?branch=master 32 | :target: https://travis-ci.org/astropy/pyregion 33 | 34 | .. image:: https://coveralls.io/repos/astropy/pyregion/badge.svg?branch=master 35 | :target: https://coveralls.io/r/astropy/pyregion 36 | 37 | New regions package 38 | ------------------- 39 | 40 | See also the in-development ``regions`` package 41 | at https://github.com/astropy/regions 42 | a new astronomy package for regions based on Astropy. 43 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | #This is needed with git because git doesn't create a dir if it's empty 18 | $(shell [ -d "_static" ] || mkdir -p _static) 19 | 20 | help: 21 | @echo "Please use \`make ' where is one of" 22 | @echo " html to make standalone HTML files" 23 | @echo " dirhtml to make HTML files named index.html in directories" 24 | @echo " singlehtml to make a single large HTML file" 25 | @echo " pickle to make pickle files" 26 | @echo " json to make JSON files" 27 | @echo " htmlhelp to make HTML files and a HTML help project" 28 | @echo " qthelp to make HTML files and a qthelp project" 29 | @echo " devhelp to make HTML files and a Devhelp project" 30 | @echo " epub to make an epub" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " text to make text files" 34 | @echo " man to make manual pages" 35 | @echo " changes to make an overview of all changed/added/deprecated items" 36 | @echo " linkcheck to check all external links for integrity" 37 | 38 | clean: 39 | -rm -rf $(BUILDDIR) 40 | -rm -rf api 41 | -rm -rf generated 42 | 43 | html: 44 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 45 | @echo 46 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 47 | 48 | dirhtml: 49 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 50 | @echo 51 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 52 | 53 | singlehtml: 54 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 55 | @echo 56 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 57 | 58 | pickle: 59 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 60 | @echo 61 | @echo "Build finished; now you can process the pickle files." 62 | 63 | json: 64 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 65 | @echo 66 | @echo "Build finished; now you can process the JSON files." 67 | 68 | htmlhelp: 69 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 70 | @echo 71 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 72 | ".hhp project file in $(BUILDDIR)/htmlhelp." 73 | 74 | qthelp: 75 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 76 | @echo 77 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 78 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 79 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Astropy.qhcp" 80 | @echo "To view the help file:" 81 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Astropy.qhc" 82 | 83 | devhelp: 84 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 85 | @echo 86 | @echo "Build finished." 87 | @echo "To view the help file:" 88 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Astropy" 89 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Astropy" 90 | @echo "# devhelp" 91 | 92 | epub: 93 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 94 | @echo 95 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 96 | 97 | latex: 98 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 99 | @echo 100 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 101 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 102 | "(use \`make latexpdf' here to do that automatically)." 103 | 104 | latexpdf: 105 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 106 | @echo "Running LaTeX files through pdflatex..." 107 | make -C $(BUILDDIR)/latex all-pdf 108 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 109 | 110 | text: 111 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 112 | @echo 113 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 114 | 115 | man: 116 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 117 | @echo 118 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 119 | 120 | changes: 121 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 122 | @echo 123 | @echo "The overview file is in $(BUILDDIR)/changes." 124 | 125 | linkcheck: 126 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 127 | @echo 128 | @echo "Link check complete; look for any errors in the above output " \ 129 | "or in $(BUILDDIR)/linkcheck/output.txt." 130 | 131 | doctest: 132 | @echo "Run 'python setup.py test' in the root directory to run doctests " \ 133 | @echo "in the documentation." 134 | -------------------------------------------------------------------------------- /docs/_static/contents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy/pyregion/94ddf4a817e42439c45859e3e927d77d17aa3327/docs/_static/contents.png -------------------------------------------------------------------------------- /docs/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy/pyregion/94ddf4a817e42439c45859e3e927d77d17aa3327/docs/_static/file.png -------------------------------------------------------------------------------- /docs/_static/mpl.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Alternate Sphinx design 3 | * Originally created by Armin Ronacher for Werkzeug, adapted by Georg Brandl. 4 | */ 5 | 6 | body { 7 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; 8 | font-size: 14px; 9 | letter-spacing: -0.01em; 10 | line-height: 150%; 11 | text-align: center; 12 | /*background-color: #AFC1C4; */ 13 | background-color: #BFD1D4; 14 | color: black; 15 | padding: 0; 16 | border: 1px solid #aaa; 17 | 18 | margin: 0px 80px 0px 80px; 19 | min-width: 740px; 20 | } 21 | 22 | a { 23 | color: #CA7900; 24 | text-decoration: none; 25 | } 26 | 27 | a:hover { 28 | color: #2491CF; 29 | } 30 | 31 | pre { 32 | font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 33 | font-size: 0.95em; 34 | letter-spacing: 0.015em; 35 | padding: 0.5em; 36 | border: 1px solid #ccc; 37 | background-color: #f8f8f8; 38 | } 39 | 40 | td.linenos pre { 41 | padding: 0.5em 0; 42 | border: 0; 43 | background-color: transparent; 44 | color: #aaa; 45 | } 46 | 47 | table.highlighttable { 48 | margin-left: 0.5em; 49 | } 50 | 51 | table.highlighttable td { 52 | padding: 0 0.5em 0 0.5em; 53 | } 54 | 55 | cite, code, tt { 56 | font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 57 | font-size: 0.95em; 58 | letter-spacing: 0.01em; 59 | } 60 | 61 | hr { 62 | border: 1px solid #abc; 63 | margin: 2em; 64 | } 65 | 66 | tt { 67 | background-color: #f2f2f2; 68 | border-bottom: 1px solid #ddd; 69 | color: #333; 70 | } 71 | 72 | tt.descname { 73 | background-color: transparent; 74 | font-weight: bold; 75 | font-size: 1.2em; 76 | border: 0; 77 | } 78 | 79 | tt.descclassname { 80 | background-color: transparent; 81 | border: 0; 82 | } 83 | 84 | tt.xref { 85 | background-color: transparent; 86 | font-weight: bold; 87 | border: 0; 88 | } 89 | 90 | a tt { 91 | background-color: transparent; 92 | font-weight: bold; 93 | border: 0; 94 | color: #CA7900; 95 | } 96 | 97 | a tt:hover { 98 | color: #2491CF; 99 | } 100 | 101 | dl { 102 | margin-bottom: 15px; 103 | } 104 | 105 | dd p { 106 | margin-top: 0px; 107 | } 108 | 109 | dd ul, dd table { 110 | margin-bottom: 10px; 111 | } 112 | 113 | dd { 114 | margin-top: 3px; 115 | margin-bottom: 10px; 116 | margin-left: 30px; 117 | } 118 | 119 | .refcount { 120 | color: #060; 121 | } 122 | 123 | dt:target, 124 | .highlight { 125 | background-color: #fbe54e; 126 | } 127 | 128 | dl.class, dl.function { 129 | border-top: 2px solid #888; 130 | } 131 | 132 | dl.method, dl.attribute { 133 | border-top: 1px solid #aaa; 134 | } 135 | 136 | dl.glossary dt { 137 | font-weight: bold; 138 | font-size: 1.1em; 139 | } 140 | 141 | pre { 142 | line-height: 120%; 143 | } 144 | 145 | pre a { 146 | color: inherit; 147 | text-decoration: underline; 148 | } 149 | 150 | .first { 151 | margin-top: 0 !important; 152 | } 153 | 154 | div.document { 155 | background-color: white; 156 | text-align: left; 157 | background-image: url(contents.png); 158 | background-repeat: repeat-x; 159 | } 160 | 161 | /* 162 | div.documentwrapper { 163 | width: 100%; 164 | } 165 | */ 166 | 167 | div.clearer { 168 | clear: both; 169 | } 170 | 171 | div.related h3 { 172 | display: none; 173 | } 174 | 175 | div.related ul { 176 | background-image: url(navigation.png); 177 | height: 2em; 178 | list-style: none; 179 | border-top: 1px solid #ddd; 180 | border-bottom: 1px solid #ddd; 181 | margin: 0; 182 | padding-left: 10px; 183 | } 184 | 185 | div.related ul li { 186 | margin: 0; 187 | padding: 0; 188 | height: 2em; 189 | float: left; 190 | } 191 | 192 | div.related ul li.right { 193 | float: right; 194 | margin-right: 5px; 195 | } 196 | 197 | div.related ul li a { 198 | margin: 0; 199 | padding: 0 5px 0 5px; 200 | line-height: 1.75em; 201 | color: #EE9816; 202 | } 203 | 204 | div.related ul li a:hover { 205 | color: #3CA8E7; 206 | } 207 | 208 | div.body { 209 | margin: 0; 210 | padding: 0.5em 20px 20px 20px; 211 | } 212 | 213 | div.bodywrapper { 214 | margin: 0 240px 0 0; 215 | border-right: 1px solid #ccc; 216 | } 217 | 218 | div.body a { 219 | text-decoration: underline; 220 | } 221 | 222 | div.sphinxsidebar { 223 | margin: 0; 224 | padding: 0.5em 15px 15px 0; 225 | width: 210px; 226 | float: right; 227 | text-align: left; 228 | /* margin-left: -100%; */ 229 | } 230 | 231 | div.sphinxsidebar h4, div.sphinxsidebar h3 { 232 | margin: 1em 0 0.5em 0; 233 | font-size: 0.9em; 234 | padding: 0.1em 0 0.1em 0.5em; 235 | color: white; 236 | border: 1px solid #86989B; 237 | background-color: #AFC1C4; 238 | } 239 | 240 | div.sphinxsidebar ul { 241 | padding-left: 1.5em; 242 | margin-top: 7px; 243 | list-style: none; 244 | padding: 0; 245 | line-height: 130%; 246 | } 247 | 248 | div.sphinxsidebar ul ul { 249 | list-style: square; 250 | margin-left: 20px; 251 | } 252 | 253 | p { 254 | margin: 0.8em 0 0.5em 0; 255 | } 256 | 257 | p.rubric { 258 | font-weight: bold; 259 | } 260 | 261 | h1 { 262 | margin: 0; 263 | padding: 0.7em 0 0.3em 0; 264 | font-size: 1.5em; 265 | color: #11557C; 266 | } 267 | 268 | h2 { 269 | margin: 1.3em 0 0.2em 0; 270 | font-size: 1.35em; 271 | padding: 0; 272 | } 273 | 274 | h3 { 275 | margin: 1em 0 -0.3em 0; 276 | font-size: 1.2em; 277 | } 278 | 279 | h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { 280 | color: black!important; 281 | } 282 | 283 | h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { 284 | display: none; 285 | margin: 0 0 0 0.3em; 286 | padding: 0 0.2em 0 0.2em; 287 | color: #aaa!important; 288 | } 289 | 290 | h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, 291 | h5:hover a.anchor, h6:hover a.anchor { 292 | display: inline; 293 | } 294 | 295 | h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, 296 | h5 a.anchor:hover, h6 a.anchor:hover { 297 | color: #777; 298 | background-color: #eee; 299 | } 300 | 301 | table { 302 | border-collapse: collapse; 303 | margin: 0 -0.5em 0 -0.5em; 304 | } 305 | 306 | table td, table th { 307 | padding: 0.2em 0.5em 0.2em 0.5em; 308 | } 309 | 310 | div.footer { 311 | background-color: #E3EFF1; 312 | color: #86989B; 313 | padding: 3px 8px 3px 0; 314 | clear: both; 315 | font-size: 0.8em; 316 | text-align: right; 317 | } 318 | 319 | div.footer a { 320 | color: #86989B; 321 | text-decoration: underline; 322 | } 323 | 324 | div.pagination { 325 | margin-top: 2em; 326 | padding-top: 0.5em; 327 | border-top: 1px solid black; 328 | text-align: center; 329 | } 330 | 331 | div.sphinxsidebar ul.toc { 332 | margin: 1em 0 1em 0; 333 | padding: 0 0 0 0.5em; 334 | list-style: none; 335 | } 336 | 337 | div.sphinxsidebar ul.toc li { 338 | margin: 0.5em 0 0.5em 0; 339 | font-size: 0.9em; 340 | line-height: 130%; 341 | } 342 | 343 | div.sphinxsidebar ul.toc li p { 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | div.sphinxsidebar ul.toc ul { 349 | margin: 0.2em 0 0.2em 0; 350 | padding: 0 0 0 1.8em; 351 | } 352 | 353 | div.sphinxsidebar ul.toc ul li { 354 | padding: 0; 355 | } 356 | 357 | div.admonition, div.warning { 358 | font-size: 0.9em; 359 | margin: 1em 0 0 0; 360 | border: 1px solid #86989B; 361 | background-color: #f7f7f7; 362 | } 363 | 364 | div.admonition p, div.warning p { 365 | margin: 0.5em 1em 0.5em 1em; 366 | padding: 0; 367 | } 368 | 369 | div.admonition pre, div.warning pre { 370 | margin: 0.4em 1em 0.4em 1em; 371 | } 372 | 373 | div.admonition p.admonition-title, 374 | div.warning p.admonition-title { 375 | margin: 0; 376 | padding: 0.1em 0 0.1em 0.5em; 377 | color: white; 378 | border-bottom: 1px solid #86989B; 379 | font-weight: bold; 380 | background-color: #AFC1C4; 381 | } 382 | 383 | div.warning { 384 | border: 1px solid #940000; 385 | } 386 | 387 | div.warning p.admonition-title { 388 | background-color: #CF0000; 389 | border-bottom-color: #940000; 390 | } 391 | 392 | div.admonition ul, div.admonition ol, 393 | div.warning ul, div.warning ol { 394 | margin: 0.1em 0.5em 0.5em 3em; 395 | padding: 0; 396 | } 397 | 398 | div.versioninfo { 399 | margin: 1em 0 0 0; 400 | border: 1px solid #ccc; 401 | background-color: #DDEAF0; 402 | padding: 8px; 403 | line-height: 1.3em; 404 | font-size: 0.9em; 405 | } 406 | 407 | 408 | a.headerlink { 409 | color: #c60f0f!important; 410 | font-size: 1em; 411 | margin-left: 6px; 412 | padding: 0 4px 0 4px; 413 | text-decoration: none!important; 414 | visibility: hidden; 415 | } 416 | 417 | h1:hover > a.headerlink, 418 | h2:hover > a.headerlink, 419 | h3:hover > a.headerlink, 420 | h4:hover > a.headerlink, 421 | h5:hover > a.headerlink, 422 | h6:hover > a.headerlink, 423 | dt:hover > a.headerlink { 424 | visibility: visible; 425 | } 426 | 427 | a.headerlink:hover { 428 | background-color: #ccc; 429 | color: white!important; 430 | } 431 | 432 | table.indextable td { 433 | text-align: left; 434 | vertical-align: top; 435 | } 436 | 437 | table.indextable dl, table.indextable dd { 438 | margin-top: 0; 439 | margin-bottom: 0; 440 | } 441 | 442 | table.indextable tr.pcap { 443 | height: 10px; 444 | } 445 | 446 | table.indextable tr.cap { 447 | margin-top: 10px; 448 | background-color: #f2f2f2; 449 | } 450 | 451 | img.toggler { 452 | margin-right: 3px; 453 | margin-top: 3px; 454 | cursor: pointer; 455 | } 456 | 457 | img.inheritance { 458 | border: 0px 459 | } 460 | 461 | form.pfform { 462 | margin: 10px 0 20px 0; 463 | } 464 | 465 | table.contentstable { 466 | width: 90%; 467 | } 468 | 469 | table.contentstable p.biglink { 470 | line-height: 150%; 471 | } 472 | 473 | a.biglink { 474 | font-size: 1.3em; 475 | } 476 | 477 | span.linkdescr { 478 | font-style: italic; 479 | padding-top: 5px; 480 | font-size: 90%; 481 | } 482 | 483 | ul.search { 484 | margin: 10px 0 0 20px; 485 | padding: 0; 486 | } 487 | 488 | ul.search li { 489 | padding: 5px 0 5px 20px; 490 | background-image: url(file.png); 491 | background-repeat: no-repeat; 492 | background-position: 0 7px; 493 | } 494 | 495 | ul.search li a { 496 | font-weight: bold; 497 | } 498 | 499 | ul.search li div.context { 500 | color: #888; 501 | margin: 2px 0 0 30px; 502 | text-align: left; 503 | } 504 | 505 | ul.keywordmatches li.goodmatch a { 506 | font-weight: bold; 507 | } 508 | -------------------------------------------------------------------------------- /docs/_static/navigation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy/pyregion/94ddf4a817e42439c45859e3e927d77d17aa3327/docs/_static/navigation.png -------------------------------------------------------------------------------- /docs/_static/region_ds9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy/pyregion/94ddf4a817e42439c45859e3e927d77d17aa3327/docs/_static/region_ds9.jpg -------------------------------------------------------------------------------- /docs/_static/region_mpl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy/pyregion/94ddf4a817e42439c45859e3e927d77d17aa3327/docs/_static/region_mpl.png -------------------------------------------------------------------------------- /docs/_templates/autosummary/base.rst: -------------------------------------------------------------------------------- 1 | {% extends "autosummary_core/base.rst" %} 2 | {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #} -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {% extends "autosummary_core/class.rst" %} 2 | {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #} -------------------------------------------------------------------------------- /docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {% extends "autosummary_core/module.rst" %} 2 | {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #} -------------------------------------------------------------------------------- /docs/_templates/indexsidebar.html: -------------------------------------------------------------------------------- 1 |

Links

2 | 3 |

GitHub Project Page

4 | 5 |

Download

6 | 7 | 8 |

Issue Tracker 9 | 10 | 11 |

Other stuff

12 | 13 | 14 |

license : MIT

15 | 16 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | 4 | {% block rootrellink %} 5 |
  • pyregion documentation
  • 6 | {% endblock %} 7 | 8 | 9 | {% block relbar1 %} 10 | 11 | {{ super() }} 12 | {% endblock %} 13 | 14 | {# put the sidebar before the body #} 15 | 16 | {% block sidebar1 %}{{ sidebar() }}{% endblock %} 17 | {% block sidebar2 %}{% endblock %} 18 | 19 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | Reference/API 2 | ============= 3 | 4 | .. automodapi:: pyregion 5 | :no-inheritance-diagram: 6 | :no-heading: 7 | :skip: cycle, RegionParser, read_region, read_region_as_imagecoord 8 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | ********* 4 | Changelog 5 | ********* 6 | 7 | .. include:: ../CHANGES.rst 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Astropy Package Template documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Jan 11 11:09:48 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | 20 | # -- General configuration ------------------------------------------------ 21 | 22 | # If your documentation needs a minimal Sphinx version, state it here. 23 | # 24 | needs_sphinx = '1.2' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = [ 30 | 'sphinx.ext.intersphinx', 31 | 'matplotlib.sphinxext.plot_directive', 32 | 'sphinx.ext.autodoc', 33 | 'sphinx_automodapi.automodapi', 34 | 'sphinx_automodapi.automodsumm', 35 | 'sphinx_automodapi.autodoc_enhancements', 36 | 'sphinx_automodapi.smart_resolver', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | source_suffix = '.rst' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = 'Pyregion' 51 | copyright = '2022, Jae-Joon Lee' 52 | author = 'Jae-Joon Lee' 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | from pyregion import __version__ as version 60 | # The full version, including alpha/beta/rc tags. 61 | release = version 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = "en" 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This patterns also effect to html_static_path and html_extra_path 73 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 74 | 75 | # The name of the Pygments (syntax highlighting) style to use. 76 | pygments_style = 'sphinx' 77 | 78 | # If true, `todo` and `todoList` produce output, else they produce nothing. 79 | todo_include_todos = False 80 | 81 | rst_epilog = """ 82 | .. _Astropy: http://astropy.org 83 | """ 84 | # -- Options for HTML output ---------------------------------------------- 85 | 86 | # The theme to use for HTML and HTML Help pages. See the documentation for 87 | # a list of builtin themes. 88 | 89 | import sphinx_rtd_theme 90 | html_theme = 'sphinx_rtd_theme' 91 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | # 97 | # html_theme_options = {} 98 | 99 | # Add any paths that contain custom static files (such as style sheets) here, 100 | # relative to this directory. They are copied after the builtin static files, 101 | # so a file named "default.css" will overwrite the builtin "default.css". 102 | #html_static_path = ['_static'] 103 | 104 | html_sidebars = { 105 | '**': ['localtoc.html'], 106 | 'search': [], 107 | 'genindex': [], 108 | 'py-modindex': [], 109 | } 110 | 111 | # -- Options for HTMLHelp output ------------------------------------------ 112 | 113 | # Output file base name for HTML help builder. 114 | htmlhelp_basename = 'AstropyPackageTemplatedoc' 115 | 116 | 117 | # Example configuration for intersphinx: refer to the Python standard library. 118 | intersphinx_mapping = {'https://docs.python.org/': None, 119 | 'http://docs.astropy.org/en/stable/': None} 120 | intersphinx_mapping["sphinx_automodapi"] = ("https://sphinx-automodapi.readthedocs.io/en/stable/", None) # noqa: E501, F405 121 | 122 | # -- Turn on nitpicky mode for sphinx (to warn about references not found) ---- 123 | nitpicky = True 124 | -------------------------------------------------------------------------------- /docs/examples: -------------------------------------------------------------------------------- 1 | ../examples -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | 3 | ******** 4 | Examples 5 | ******** 6 | 7 | demo_region01.py 8 | ---------------- 9 | 10 | .. plot:: examples/demo_region01.py 11 | 12 | demo_region02.py 13 | ---------------- 14 | 15 | .. plot:: examples/demo_region02.py 16 | 17 | demo_region03.py 18 | ---------------- 19 | 20 | .. plot:: examples/demo_region03.py 21 | 22 | demo_region04.py 23 | ---------------- 24 | 25 | .. plot:: examples/demo_region04.py 26 | 27 | demo_region_filter01.py 28 | ----------------------- 29 | 30 | .. plot:: examples/demo_region_filter01.py 31 | -------------------------------------------------------------------------------- /docs/figures/demo_filter_mask.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import pyregion 3 | 4 | region = """ 5 | image 6 | circle(100, 100, 80) 7 | box(200, 150, 150, 120, 0) 8 | """ 9 | 10 | r = pyregion.parse(region) 11 | mask_1or2 = r.get_mask(shape=(300, 300)) 12 | 13 | myfilter = r.get_filter() 14 | mask_1and2 = (myfilter[0] & myfilter[1]).mask((300, 300)) 15 | 16 | plt.subplot(121).imshow(mask_1or2, origin="lower", interpolation="nearest") 17 | plt.subplot(122).imshow(mask_1and2, origin="lower", interpolation="nearest") 18 | plt.show() 19 | -------------------------------------------------------------------------------- /docs/figures/pspc_skyview.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astropy/pyregion/94ddf4a817e42439c45859e3e927d77d17aa3327/docs/figures/pspc_skyview.fits -------------------------------------------------------------------------------- /docs/figures/region_drawing.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib.cm as cm 3 | from astropy.io import fits 4 | import pyregion 5 | 6 | # read in the image 7 | xray_name = "pspc_skyview.fits" 8 | f_xray = fits.open(xray_name) 9 | 10 | try: 11 | from astropy.wcs import WCS 12 | from astropy.visualization.wcsaxes import WCSAxes 13 | 14 | wcs = WCS(f_xray[0].header) 15 | fig = plt.figure() 16 | ax = WCSAxes(fig, [0.1, 0.1, 0.8, 0.8], wcs=wcs) 17 | fig.add_axes(ax) 18 | except ImportError: 19 | ax = plt.subplot(111) 20 | 21 | ax.imshow(f_xray[0].data, 22 | cmap=cm.gray, vmin=0., vmax=0.00038, origin="lower") 23 | 24 | reg_name = "test.reg" 25 | r = pyregion.open(reg_name).as_imagecoord(f_xray[0].header) 26 | 27 | patch_list, text_list = r.get_mpl_patches_texts() 28 | for p in patch_list: 29 | ax.add_patch(p) 30 | for t in text_list: 31 | ax.add_artist(t) 32 | 33 | plt.show() 34 | -------------------------------------------------------------------------------- /docs/figures/region_drawing2.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib.cm as cm 3 | from astropy.io import fits 4 | import pyregion 5 | 6 | # read in the image 7 | xray_name = "pspc_skyview.fits" 8 | f_xray = fits.open(xray_name) 9 | 10 | try: 11 | from astropy.wcs import WCS 12 | from astropy.visualization.wcsaxes import WCSAxes 13 | 14 | wcs = WCS(f_xray[0].header) 15 | fig = plt.figure() 16 | ax = WCSAxes(fig, [0.1, 0.1, 0.8, 0.8], wcs=wcs) 17 | fig.add_axes(ax) 18 | except ImportError: 19 | ax = plt.subplot(111) 20 | 21 | ax.imshow(f_xray[0].data, cmap=cm.gray, vmin=0., vmax=0.00038, origin="lower") 22 | 23 | reg_name = "test.reg" 24 | r = pyregion.open(reg_name).as_imagecoord(header=f_xray[0].header) 25 | 26 | from pyregion.mpl_helper import properties_func_default 27 | 28 | 29 | # Use custom function for patch attribute 30 | def fixed_color(shape, saved_attrs): 31 | attr_list, attr_dict = saved_attrs 32 | attr_dict["color"] = "red" 33 | kwargs = properties_func_default(shape, (attr_list, attr_dict)) 34 | 35 | return kwargs 36 | 37 | 38 | # select region shape with tag=="Group 1" 39 | r1 = pyregion.ShapeList([rr for rr in r if rr.attr[1].get("tag") == "Group 1"]) 40 | patch_list1, artist_list1 = r1.get_mpl_patches_texts(fixed_color) 41 | 42 | r2 = pyregion.ShapeList([rr for rr in r if rr.attr[1].get("tag") != "Group 1"]) 43 | patch_list2, artist_list2 = r2.get_mpl_patches_texts() 44 | 45 | for p in patch_list1 + patch_list2: 46 | ax.add_patch(p) 47 | for t in artist_list1 + artist_list2: 48 | ax.add_artist(t) 49 | 50 | plt.show() 51 | -------------------------------------------------------------------------------- /docs/figures/test.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: pspc_skyview.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | circle(305.66137,46.273027,286.45302") 6 | ellipse(305.2084,46.309061,240",600",15.433424) # color=black width=4 7 | box(304.61491,46.299899,1032",552",28.9055) # color=white tag={Group 1} 8 | polygon(304.30761,46.142612,303.94162,46.140616,304.12265,46.314927,303.72947,46.178781,303.9453,45.887034,304.10869,45.854872,304.30963,45.889013) # color=red 9 | line(305.70423,45.962694,305.10953,45.946101) # line=0 0 dash=1 10 | # vector(305.47681,45.437697,1448.972",63.434949) vector=1 11 | # text(304.75479,45.939998) text={Text} 12 | annulus(304.01194,45.570957,216",506.2428",674.9904") # color=yellow 13 | ellipse(304.7357,45.626666,349.44527",196.03028",797.79697",447.54464",339.24891) # width=2 tag={Group 1} 14 | panda(305.48266,45.157674,0,151.26,2,398.8488",797.6976",1) # color=blue width=2 15 | epanda(304.78308,45.140013,0,88.057145,1,193.49419",257.83216",504.22594",671.88407",1,8.11303) # epanda=(0 88.057145 228.1969 327.92448)(193.49419" 257.83216" 504.22594" 671.88407" 672.30125" 895.84543")(8.11303) color=cyan 16 | epanda(304.78308,45.140013,0,88.057145,1,504.22594",671.88407",672.30125",895.84543",1,8.11303) # epanda=ignore 17 | epanda(304.78308,45.140013,88.057145,228.1969,1,193.49419",257.83216",504.22594",671.88407",1,8.11303) # epanda=ignore 18 | epanda(304.78308,45.140013,88.057145,228.1969,1,504.22594",671.88407",672.30125",895.84543",1,8.11303) # epanda=ignore 19 | epanda(304.78308,45.140013,228.1969,327.92448,1,193.49419",257.83216",504.22594",671.88407",1,8.11303) # epanda=ignore 20 | epanda(304.78308,45.140013,228.1969,327.92448,1,504.22594",671.88407",672.30125",895.84543",1,8.11303) # epanda=ignore 21 | point(304.26232,45.252305) # point=circle 22 | point(304.0256,45.251053) # point=box color=magenta 23 | point(303.79815,45.262722) # point=diamond 24 | point(304.25414,45.105615) # point=cross 25 | point(304.04637,45.104528) # point=x 26 | point(304.25577,44.918982) # point=arrow 27 | point(304.02028,44.931056) # point=boxcircle 28 | bpanda(305.53095,44.934745,0,290,1,605.36378",294.48644",1371.1215",666.9984",1,342.545) # color=blue width=2 29 | -------------------------------------------------------------------------------- /docs/figures/test02.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: pspc_skyview.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | physical 5 | ellipse(82,167,36,75,346.95915) 6 | epanda(140,166,87.19363,195.80251,1,32.5,39.5,65,79,1,307.90041) 7 | -polygon(78.265142,201.73486,132,209,125,178,163.73486,116.26514,78.265142,116.26514) 8 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. _gs: 2 | 3 | *************** 4 | Getting started 5 | *************** 6 | 7 | pyregion is a python module to parse ds9 region files. 8 | It also supports ciao region files. 9 | 10 | Please note that the main emphasis of the package is to read in the regions files 11 | generated by ds9 itself. It reads most of the region files created by 12 | ds9. However, it may fail to read some of the user-created (or created 13 | by other programs) region files, even if they can be successfully read 14 | by ds9. Ruler, Compass and Projection types are ignored. 15 | 16 | 17 | +----------------------------------------+----------------------------------------+ 18 | | ds9 | pyregion + matplotlib | 19 | +========================================+========================================+ 20 | | .. image:: _static/region_ds9.jpg | .. image:: _static/region_mpl.png | 21 | | :width: 300px | :width: 300px | 22 | | :target: static/region_ds9.jpg | :target: static/region_mpl.png | 23 | +----------------------------------------+----------------------------------------+ 24 | 25 | 26 | .. contents:: 27 | :depth: 1 28 | :local: 29 | 30 | 31 | Read Region Files 32 | ================= 33 | 34 | `pyregion.open` takes the region name as an argument and returns a 35 | `~pyregion.ShapeList` object, which is basically a list of `~pyregion.Shape` objects 36 | (`~pyregion.ShapeList` is a sub-class of the Python built-in `list` class). :: 37 | 38 | import pyregion 39 | region_name = "ds9.reg" 40 | r = pyregion.open(region_name) 41 | 42 | You may use `pyregion.parse` if you have a string that defines a region :: 43 | 44 | region = 'fk5;circle(290.96388,14.019167,843.31194")' 45 | r = pyregion.parse(region) 46 | 47 | The shape object is a python representation of each region definition. For example,:: 48 | 49 | import pyregion 50 | 51 | region_string = """ 52 | # Region file format: DS9 version 4.1 53 | # Filename: test01.fits 54 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 55 | fk5 56 | circle(11:24:24.230,-59:15:02.20,18.5108") # color=cyan background 57 | box(11:24:39.213,-59:16:53.91,42.804",23.616",19.0384) # width=4 58 | """ 59 | 60 | r = pyregion.parse(region_string) 61 | 62 | And you have:: 63 | 64 | >>> print r[0] 65 | Shape : circle ( HMS(11:24:24.230),DMS(-59:15:02.20),Ang(18.5108") ) 66 | >>> print r[1] 67 | Shape : box ( HMS(11:24:39.213),DMS(-59:16:53.91),Ang(42.804"),Ang(23.616"),Number(19.0384) ) 68 | 69 | The shape object has the following attributes, 70 | 71 | * ``name`` : name of the shape. e.g., circle, box, etc.. :: 72 | 73 | >>> print r[0].name 74 | circle 75 | 76 | * ``coord_format`` : coordinate format. e.g., "fk5", "image", "physical", etc... :: 77 | 78 | >>> print r[0].coord_format 79 | fk5 80 | 81 | * ``coord_list`` : list of coordinates in *coord_format*. The coordinate 82 | value for sky coordinates is degree. :: 83 | 84 | >>> print r[0].coord_list 85 | [171.10095833333332, -59.250611111111112, 0.0051418888888888886] 86 | 87 | * ``comment`` : comment string associated with the shape (can be None) :: 88 | 89 | >>> print r[0].comment 90 | color=cyan background 91 | 92 | * ``attr`` : attributes of the shape. This includes global attributes 93 | defined by the global command and local attributes defined in the 94 | comment. The first item is a list of key-only attributes without 95 | associated values (e.g., background..) and the second item is a 96 | dictionary of attributes of key-value pairs. :: 97 | 98 | >>> print r[0].attr[0] 99 | ['background'] 100 | >>> print r[0].attr[1] 101 | {'color': 'cyan', 102 | 'dash': '0 ', 103 | 'dashlist': '8 3 ', 104 | 'delete': '1 ', 105 | 'edit': '1 ', 106 | 'fixed': '0 ', 107 | 'font': '"helvetica 10 normal"', 108 | 'highlite': '1 ', 109 | 'include': '1 ', 110 | 'move': '1 ', 111 | 'select': '1 ', 112 | 'source': '1', 113 | 'width': '1 '} 114 | 115 | 116 | Some attributes like "tag" allow multiple items, but this is not 117 | currently supported (the last definition overrides any previous ones). 118 | 119 | 120 | The `pyregion.ShapeList` class have a few methods that could be 121 | useful. `ShapeList.as_imagecoord ` returns a new `~pyregion.ShapeList` instance 122 | with the coordinates converted to the image coordinate system. It 123 | requires an `astropy.io.fits.Header` instance. :: 124 | 125 | from astropy.io import fits 126 | f = fits.open("t1.fits") 127 | r2 = pyregion.parse(region_string).as_imagecoord(f[0].header) 128 | 129 | The return value is a new `~pyregion.ShapeList` instance, but the coordinate is 130 | converted to image coordinates. :: 131 | 132 | >>> print r2[0].coord_format 133 | image 134 | 135 | >>> print r2[0].coord_list 136 | [482.27721401429852, 472.76641383805912, 18.811792596807045] 137 | 138 | `ShapeList.as_imagecoord ` will use the 139 | subset of the header defining a celestial coordinate system, ignoring any 140 | velocity or channel components. 141 | 142 | Draw Regions with Matplotlib 143 | ============================ 144 | 145 | pyregion can help you draw ds9 regions with matplotlib. 146 | `ShapeList.get_mpl_patches_texts ` returns a list of 147 | ``matplotlib.artist.Artist`` objects :: 148 | 149 | r2 = pyregion.parse(region_string).as_imagecoord(f[0].header) 150 | patch_list, artist_list = r2.get_mpl_patches_texts() 151 | 152 | The first item is a list of `matplotlib.patches.Patch`, and the second one is 153 | other kinds of artists (usually Text). It is your responsibility to add 154 | these to the axes. :: 155 | 156 | # ax is a mpl Axes object 157 | for p in patch_list: 158 | ax.add_patch(p) 159 | for t in artist_list: 160 | ax.add_artist(t) 161 | 162 | .. plot:: figures/region_drawing.py 163 | 164 | The (optional) argument of the ``get_mpl_patches_texts`` method is a 165 | callable object that takes the shape object as an argument and returns 166 | a dictionary object that will be used as a keyword arguments (e.g., 167 | colors and line width) for creating the mpl artists. By default, it 168 | uses ``pyregion.mpl_helper.properties_func_default``, which tries to respect 169 | the ds9 attributes. However, the colors (and other attributes) of some 170 | complex shapes are not correctly handled as shown in above example, 171 | and you need to manually adjust the associated attributes of patches. 172 | 173 | 174 | .. plot:: figures/region_drawing2.py 175 | :include-source: 176 | 177 | 178 | 179 | Use Regions for Spatial Filtering 180 | ================================= 181 | 182 | ``pyregion`` includes some basic spatial filter support. 183 | 184 | The `ShapeList.get_filter ` method returns the filter from the parsed region. 185 | 186 | The filter is meant to be used in the image coordinate, thus you need to convert the region 187 | to the image coordinate before calling ``get_filter``. :: 188 | 189 | r2 = pyregion.parse(region_string).as_imagecoord(f[0].header) 190 | myfilter = r2.get_filter() 191 | myfilter.inside1(50, 30) 192 | 193 | The returned filter has a ``mask`` method that creates a 2d mask. 194 | You can create the mask directly from the ShapeList object. :: 195 | 196 | r2 = pyregion.parse(region_string) 197 | mymask = r2.get_mask(hdu=f[0]) 198 | 199 | It will creates an mask in the shape of the given hdu image (the mask 200 | will be created after transforming the region to the image coordinate if 201 | necessary). 202 | 203 | .. plot:: figures/demo_filter_mask.py 204 | :include-source: 205 | 206 | Note that this will fail if your template image is not a simple 2D image. 207 | To work around this you may use the ``shape`` optional argument of `ShapeList.get_mask `: :: 208 | 209 | mymask = r2.get_mask(hdu=f[0],shape=(1024,1024)) 210 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | pyregion 3 | ======== 4 | 5 | :Release: |version| 6 | :Date: |today| 7 | 8 | pyregion is a python module to parse ds9 region files. 9 | It also supports ciao region files. 10 | 11 | .. note:: 12 | 13 | See also the in-development ``regions`` package 14 | at https://github.com/astropy/regions 15 | a new astronomy package for regions based on Astropy. 16 | 17 | 18 | +----------------------------------------+----------------------------------------+ 19 | | ds9 | pyregion + matplotlib | 20 | +========================================+========================================+ 21 | | .. image:: _static/region_ds9.jpg | .. image:: _static/region_mpl.png | 22 | | :width: 300px | :width: 300px | 23 | | :target: static/region_ds9.jpg | :target: static/region_mpl.png | 24 | +----------------------------------------+----------------------------------------+ 25 | 26 | 27 | Documentation 28 | ============= 29 | 30 | .. toctree:: 31 | :maxdepth: 1 32 | 33 | installation 34 | getting_started 35 | examples 36 | api 37 | changelog 38 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | ************ 4 | Installation 5 | ************ 6 | 7 | Stable version 8 | ============== 9 | 10 | Installing the latest stable version is possible either using pip or conda. 11 | 12 | Using pip 13 | --------- 14 | 15 | To install pyregion with `pip `_ 16 | from `PyPI `_ 17 | simply run:: 18 | 19 | pip install pyregion 20 | 21 | Using conda 22 | ----------- 23 | 24 | To install regions with `Anaconda `_ 25 | from the `conda-forge channel on anaconda.org `__ 26 | simply run:: 27 | 28 | conda install -c conda-forge pyregion 29 | 30 | 31 | Testing installation 32 | -------------------- 33 | 34 | To check if your install is OK, install the test dependencies and run the tests: 35 | 36 | .. code-block:: bash 37 | 38 | pip install "pyregion[test]" 39 | pytest --pyargs pyregion 40 | 41 | Development version 42 | =================== 43 | 44 | Install the latest development version from https://github.com/astropy/pyregion : 45 | 46 | .. code-block:: bash 47 | 48 | git clone https://github.com/astropy/pyregion 49 | cd pyregion 50 | pip install -e .[test] 51 | pytest 52 | cd docs 53 | make html 54 | 55 | Dependencies 56 | ============ 57 | 58 | Python 3.7+ is supported. 59 | 60 | ``pyregion`` has the following required dependencies: 61 | 62 | * `Astropy `__ version 4.0 or later (which requires Numpy) 63 | * ``pyparsing`` version 2.0 or later for parsing the DS9 region files 64 | * `Homepage `__ 65 | * `PyPI page `__ 66 | 67 | ``pyregion`` has the following optional dependencies for plotting: 68 | 69 | * `matplotlib `__ 70 | 71 | To work with the development version, you'll need a C compiler, 72 | because the code to generate masks from regions is written in Cython. 73 | -------------------------------------------------------------------------------- /examples/demo_helper.py: -------------------------------------------------------------------------------- 1 | import math 2 | import matplotlib.pyplot as plt 3 | import pyregion 4 | 5 | 6 | def demo_header(): 7 | from astropy.io.fits import Header 8 | return Header.fromtextfile("sample_fits01.header") 9 | 10 | 11 | def show_region(fig, region_list): 12 | h = demo_header() 13 | 14 | n = len(region_list) 15 | nx = int(math.ceil(n ** .5)) 16 | ny = int(math.ceil(1. * n / nx)) 17 | 18 | nrows_ncols = (ny, nx) 19 | 20 | grid = [plt.subplot(ny, nx, i + 1) for i in range(n)] 21 | 22 | for ax, reg_name in zip(grid, region_list): 23 | ax.set_aspect(1) 24 | 25 | r = pyregion.open(reg_name).as_imagecoord(h) 26 | 27 | patch_list, text_list = r.get_mpl_patches_texts() 28 | for p in patch_list: 29 | ax.add_patch(p) 30 | for t in text_list: 31 | ax.add_artist(t) 32 | 33 | if plt.rcParams["text.usetex"]: 34 | reg_name = reg_name.replace("_", r"\_") 35 | ax.set_title(reg_name, size=10) 36 | for t in ax.get_xticklabels() + ax.get_yticklabels(): 37 | t.set_visible(False) 38 | 39 | return grid 40 | -------------------------------------------------------------------------------- /examples/demo_print_region.py: -------------------------------------------------------------------------------- 1 | """Example how to read and print regions. 2 | """ 3 | from astropy.io.fits import Header 4 | import pyregion 5 | 6 | 7 | def print_shape_list(shape_list): 8 | for idx, shape in enumerate(shape_list, start=1): 9 | print("[region %d]" % idx) 10 | print() 11 | print("%s; %s(%s)" % (shape.coord_format, 12 | shape.name, 13 | ", ".join([str(s) for s in shape.coord_list]))) 14 | 15 | print(shape.attr[0]) 16 | print(", ".join(["%s=%s" % (k, v.strip()) for k, v in list(shape.attr[1].items())])) 17 | print() 18 | 19 | 20 | if __name__ == "__main__": 21 | print("** coordinate in FK5 **") 22 | print() 23 | filename = "test01_print.reg" 24 | # filename = "test_text.reg" 25 | # filename = "test01.reg" 26 | shape_list = pyregion.open(filename) 27 | print_shape_list(shape_list) 28 | 29 | print() 30 | print() 31 | print("** coordinate in image **") 32 | print() 33 | header = Header.fromtextfile("test.header") 34 | shape_list2 = shape_list.as_imagecoord(header=header) 35 | print_shape_list(shape_list2) 36 | -------------------------------------------------------------------------------- /examples/demo_region01.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from demo_helper import show_region 3 | 4 | region_list = [ 5 | "test01_fk5_sexagecimal.reg", 6 | "test01_gal.reg", 7 | "test01_img.reg", 8 | "test01_ds9_physical.reg", 9 | "test01_fk5_degree.reg", 10 | "test01_mixed.reg", 11 | "test01_ciao.reg", 12 | "test01_ciao_physical.reg", 13 | ] 14 | 15 | fig = plt.figure(1, figsize=(6, 5)) 16 | fig.clf() 17 | 18 | ax_list = show_region(fig, region_list) 19 | for ax in ax_list: 20 | ax.set_xlim(596, 1075) 21 | ax.set_ylim(585, 1057) 22 | 23 | plt.draw() 24 | plt.show() 25 | -------------------------------------------------------------------------------- /examples/demo_region02.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from demo_helper import show_region 3 | 4 | region_list = [ 5 | "test_annuli.reg", 6 | "test_annuli_wcs.reg", 7 | "test_annuli_ciao.reg", 8 | ] 9 | 10 | fig = plt.figure(figsize=(6, 6)) 11 | 12 | ax_list = show_region(fig, region_list) 13 | for ax in ax_list: 14 | ax.set_xlim(596, 1075) 15 | ax.set_ylim(585, 1057) 16 | 17 | plt.draw() 18 | plt.show() 19 | -------------------------------------------------------------------------------- /examples/demo_region03.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from matplotlib.offsetbox import AnchoredText 3 | from astropy.io.fits import Header 4 | from astropy.wcs import WCS 5 | from astropy.visualization.wcsaxes import WCSAxes 6 | import pyregion 7 | 8 | region_list = [ 9 | "test_text.reg", 10 | "test_context.reg", 11 | ] 12 | 13 | # Create figure 14 | fig = plt.figure(figsize=(8, 4)) 15 | 16 | # Parse WCS information 17 | header = Header.fromtextfile('sample_fits01.header') 18 | wcs = WCS(header) 19 | 20 | # Create axes 21 | ax1 = WCSAxes(fig, [0.1, 0.1, 0.4, 0.8], wcs=wcs) 22 | fig.add_axes(ax1) 23 | ax2 = WCSAxes(fig, [0.5, 0.1, 0.4, 0.8], wcs=wcs) 24 | fig.add_axes(ax2) 25 | 26 | # Hide labels on y axis 27 | ax2.coords[1].set_ticklabel_position('') 28 | 29 | for ax, reg_name in zip([ax1, ax2], region_list): 30 | 31 | ax.set_xlim(300, 1300) 32 | ax.set_ylim(300, 1300) 33 | ax.set_aspect(1) 34 | 35 | r = pyregion.open(reg_name).as_imagecoord(header) 36 | 37 | patch_list, text_list = r.get_mpl_patches_texts() 38 | 39 | for p in patch_list: 40 | ax.add_patch(p) 41 | 42 | for t in text_list: 43 | ax.add_artist(t) 44 | 45 | atext = AnchoredText(reg_name, loc=2) 46 | 47 | ax.add_artist(atext) 48 | 49 | plt.draw() 50 | plt.show() 51 | -------------------------------------------------------------------------------- /examples/demo_region04.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import pyregion 3 | 4 | reg_name = "test04_img.reg" 5 | 6 | ax = plt.subplot(111) 7 | ax.set_xlim(600, 1100) 8 | ax.set_ylim(600, 1100) 9 | ax.set_aspect(1) 10 | 11 | r = pyregion.open(reg_name) 12 | 13 | patch_list, text_list = r.get_mpl_patches_texts() 14 | for p in patch_list: 15 | ax.add_patch(p) 16 | for t in text_list: 17 | ax.add_artist(t) 18 | 19 | plt.draw() 20 | plt.show() 21 | -------------------------------------------------------------------------------- /examples/demo_region_filter01.py: -------------------------------------------------------------------------------- 1 | from astropy.io.fits import Header 2 | import matplotlib.pyplot as plt 3 | import pyregion 4 | 5 | 6 | # read in the image 7 | def demo_header(): 8 | return Header.fromtextfile("sample_fits02.header") 9 | 10 | 11 | header = demo_header() # sample fits header 12 | shape = (header["NAXIS1"], header["NAXIS2"]) 13 | 14 | reg_name = "test.reg" 15 | r = pyregion.open(reg_name).as_imagecoord(header) 16 | m = r.get_mask(shape=shape) 17 | 18 | fig = plt.figure(1, figsize=(7, 5)) 19 | ax = plt.subplot(121) 20 | plt.imshow(m, origin="lower") 21 | 22 | patch_list, text_list = r.get_mpl_patches_texts() 23 | for p in patch_list: 24 | ax.add_patch(p) 25 | for t in text_list: 26 | ax.add_artist(t) 27 | 28 | # another region 29 | 30 | reg_name = "test02.reg" 31 | r = pyregion.open(reg_name).as_imagecoord(header) 32 | m = r.get_mask(shape=shape) 33 | 34 | ax = plt.subplot(122) 35 | plt.imshow(m, origin="lower") 36 | 37 | patch_list, text_list = r.get_mpl_patches_texts() 38 | for p in patch_list: 39 | ax.add_patch(p) 40 | for t in text_list: 41 | ax.add_artist(t) 42 | 43 | plt.show() 44 | -------------------------------------------------------------------------------- /examples/sample_fits01.header: -------------------------------------------------------------------------------- 1 | SIMPLE = T / file does conform to FITS standard 2 | BITPIX = 16 / number of bits per data pixel 3 | NAXIS = 2 / number of data axes 4 | NAXIS1 = 1629 / length of data axis 5 | NAXIS2 = 1653 / length of data axis 6 | EXTEND = T / FITS dataset may contain extensions 7 | EQUINOX = 2.0000000000000E+03 / default 8 | CTYPE1 = 'RA---TAN' 9 | CRVAL1 = 1.7114680010248E+02 10 | CRPIX1 = 7.9250000000000E+02 11 | CDELT1 = -1.3666666666667E-04 12 | CUNIT1 = 'deg ' 13 | CTYPE2 = 'DEC--TAN' 14 | CRVAL2 = -5.9266678641361E+01 15 | CRPIX2 = 8.2750000000000E+02 16 | CDELT2 = 1.3666666666667E-04 17 | CUNIT2 = 'deg ' 18 | CTYPE1P = 'X ' / sky coordinates 19 | CRVAL1P = 3.3045000000000E+03 20 | CRPIX1P = 5.0000000000000E-01 21 | CDELT1P = 1.0000000000000E+00 22 | WCSTY1P = 'PHYSICAL' 23 | LTV1 = -3.3040000000000E+03 24 | LTM1_1 = 1.0000000000000E+00 25 | CTYPE2P = 'Y ' / sky coordinates 26 | CRVAL2P = 3.2695000000000E+03 27 | CRPIX2P = 5.0000000000000E-01 28 | CDELT2P = 1.0000000000000E+00 29 | WCSTY2P = 'PHYSICAL' 30 | LTV2 = -3.2690000000000E+03 31 | LTM2_2 = 1.0000000000000E+00 32 | -------------------------------------------------------------------------------- /examples/sample_fits02.header: -------------------------------------------------------------------------------- 1 | SIMPLE = T / Written by SkyView Thu Mar 19 00:15:05 GMT 2009 2 | BITPIX = -64 / 4 byte floating point 3 | NAXIS = 2 / Two dimensional image 4 | NAXIS1 = 300 / Width of image 5 | NAXIS2 = 300 / Height of image 6 | CRVAL1 = 304.75 / Reference longitude 7 | CRVAL2 = 45.7 / Reference latitude 8 | RADESYS = 'FK5 ' / Coordinate system 9 | EQUINOX = 2000.0 / Epoch of the equinox 10 | CTYPE1 = 'RA---TAN' / Coordinates -- projection 11 | CTYPE2 = 'DEC--TAN' / Coordinates -- projection 12 | CRPIX1 = 150.5 / X reference pixel 13 | CRPIX2 = 150.5 / Y reference pixel 14 | CDELT1 = -0.006666666666667 / X scale 15 | CDELT2 = 0.006666666666666666 / Y scale 16 | COMMENT 17 | COMMENT SkyView Survey metadata 18 | COMMENT 19 | COMMENT Provenance: Observational data from NASA Goddard Space Flight C 20 | COMMENT enter, mosaicking of images done by SkyView. 21 | COMMENT Copyright: Public domain 22 | COMMENT Regime: X-ray 23 | COMMENT NSurvey: 1 24 | COMMENT Frequency: 0.3 EHz (.1-2.4 keV) 25 | COMMENT Coverage: Isolated pointings in the sky. Total coverage < 14 26 | COMMENT % 27 | COMMENT PixelScale: 15" 28 | COMMENT PixelUnits: cts/s/pixel 29 | COMMENT Resolution: 30" but variable across the field of view 30 | COMMENT Coordinates: Equatorial 31 | COMMENT Equinox: 2000 32 | COMMENT Projection: Gnomonic 33 | COMMENT Epoch: 1991-1994 34 | COMMENT Reference: ROSAT Mission Description and Data Products Guide, availa 35 | COMMENT ble thr ough the ROSAT Guest Observer Facility, NASA GSFC. 36 | COMMENT SkyView Rosat Survey Generation description. 37 | COMMENT 38 | COMMENT Survey specific cards 39 | COMMENT 40 | SURVEY = 'PSPC 2.0 Deg-Inten' 41 | -------------------------------------------------------------------------------- /examples/test.header: -------------------------------------------------------------------------------- 1 | SIMPLE = T / file does conform to FITS standard 2 | BITPIX = 16 / number of bits per data pixel 3 | NAXIS = 2 / number of data axes 4 | NAXIS1 = 1629 / length of data axis 5 | NAXIS2 = 1653 / length of data axis 6 | EXTEND = T / FITS dataset may contain extensions 7 | CTYPE1 = 'RA---TAN' 8 | CRVAL1 = 1.7114680010248E+02 9 | CRPIX1 = 7.9250000000000E+02 10 | CDELT1 = -1.3666666666667E-04 11 | CUNIT1 = 'deg ' 12 | CTYPE2 = 'DEC--TAN' 13 | CRVAL2 = -5.9266678641361E+01 14 | CRPIX2 = 8.2750000000000E+02 15 | CDELT2 = 1.3666666666667E-04 16 | CUNIT2 = 'deg ' -------------------------------------------------------------------------------- /examples/test.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: pspc_skyview.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | circle(305.66137,46.273027,286.45302") 6 | ellipse(305.2084,46.309061,240",600",15.433424) # color=black width=4 7 | box(304.61491,46.299899,1032",552",28.9055) # color=white tag={Group 1} 8 | polygon(304.30761,46.142612,303.94162,46.140616,304.12265,46.314927,303.72947,46.178781,303.9453,45.887034,304.10869,45.854872,304.30963,45.889013) # color=red 9 | line(305.70423,45.962694,305.10953,45.946101) # line=0 0 dash=1 10 | # vector(305.47681,45.437697,1448.972",63.434949) vector=1 11 | # text(304.75479,45.939998) text={Text} 12 | annulus(304.01194,45.570957,216",506.2428",674.9904") # color=yellow 13 | ellipse(304.7357,45.626666,349.44527",196.03028",797.79697",447.54464",339.24891) # width=2 tag={Group 1} 14 | panda(305.48266,45.157674,0,151.26,2,398.8488",797.6976",1) # color=blue width=2 15 | epanda(304.78308,45.140013,0,88.057145,1,193.49419",257.83216",504.22594",671.88407",1,8.11303) # epanda=(0 88.057145 228.1969 327.92448)(193.49419" 257.83216" 504.22594" 671.88407" 672.30125" 895.84543")(8.11303) color=cyan 16 | epanda(304.78308,45.140013,0,88.057145,1,504.22594",671.88407",672.30125",895.84543",1,8.11303) # epanda=ignore 17 | epanda(304.78308,45.140013,88.057145,228.1969,1,193.49419",257.83216",504.22594",671.88407",1,8.11303) # epanda=ignore 18 | epanda(304.78308,45.140013,88.057145,228.1969,1,504.22594",671.88407",672.30125",895.84543",1,8.11303) # epanda=ignore 19 | epanda(304.78308,45.140013,228.1969,327.92448,1,193.49419",257.83216",504.22594",671.88407",1,8.11303) # epanda=ignore 20 | epanda(304.78308,45.140013,228.1969,327.92448,1,504.22594",671.88407",672.30125",895.84543",1,8.11303) # epanda=ignore 21 | point(304.26232,45.252305) # point=circle 22 | point(304.0256,45.251053) # point=box color=magenta 23 | point(303.79815,45.262722) # point=diamond 24 | point(304.25414,45.105615) # point=cross 25 | point(304.04637,45.104528) # point=x 26 | point(304.25577,44.918982) # point=arrow 27 | point(304.02028,44.931056) # point=boxcircle 28 | bpanda(305.53095,44.934745,0,290,1,605.36378",294.48644",1371.1215",666.9984",1,342.545) # color=blue width=2 29 | -------------------------------------------------------------------------------- /examples/test01.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | ## -ellipse(171.15816,-59.263193,22.632",10.332",317.01716) # width=3 background 6 | ascircle(171.10096,-59.250612,18.510811") # color=cyan 7 | box(171.16339,-59.281643,42.804",23.616",19.038396) # width=4 8 | polygon(171.1239,-59.26881,171.09051,-59.262088,171.0985,-59.285735,171.1239,-59.27698) 9 | -------------------------------------------------------------------------------- /examples/test01_ciao.reg: -------------------------------------------------------------------------------- 1 | # Region file format: CIAO version 1.0 2 | -ellipse(11:24:37.960,-59:15:47.50,0.3772',0.1722',317.017) 3 | circle(11:24:24.230,-59:15:02.20,0.308514') 4 | rotbox(11:24:39.213,-59:16:53.91,0.7134',0.3936',19.0384) 5 | polygon(11:24:29.737,-59:16:07.72,11:24:21.723,-59:15:43.52,11:24:23.641,-59:17:08.64,11:24:29.736,-59:16:37.13) 6 | -------------------------------------------------------------------------------- /examples/test01_ciao_physical.reg: -------------------------------------------------------------------------------- 1 | # Region file format: CIAO version 1.0 2 | -ellipse(4053.9922,4121.9905,46,21,317.017) 3 | circle(4267.9987,4214.0083,37.623659) 4 | rotbox(4034.5013,3987.0067,87,48,19.0384) 5 | polygon(4182.1103,4080.8819,4307.0067,4129.9947,4276.9938,3957.01,4182.1053,4021.1054) 6 | -------------------------------------------------------------------------------- /examples/test01_ds9_physical.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: t1.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | physical 5 | -ellipse(4053.9922,4121.9905,46,21,317.017) 6 | circle(4267.9987,4214.0083,37.623659) 7 | box(4034.5013,3987.0067,87,48,19.0384) 8 | polygon(4182.1103,4080.8819,4307.0067,4129.9947,4276.9938,3957.01,4182.1053,4021.1054) 9 | -------------------------------------------------------------------------------- /examples/test01_fk5.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | -ellipse(171.15816,-59.263193,22.632",10.332",317.01716) # width=3 background 6 | circle(171.10096,-59.250612,18.510811") # color=cyan 7 | box(171.16339,-59.281643,42.804",23.616",19.038396) # width=4 8 | polygon(171.1239,-59.26881,171.09051,-59.262088,171.0985,-59.285735,171.1239,-59.27698) 9 | -------------------------------------------------------------------------------- /examples/test01_fk5_degree.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | -ellipse(171.15816,-59.263193,22.632",10.332",317.01716) # width=3 background 6 | circle(171.10096,-59.250612,18.510811") # color=cyan 7 | box(171.16339,-59.281643,42.804",23.616",19.038396) # width=4 8 | polygon(171.1239,-59.26881,171.09051,-59.262088,171.0985,-59.285735,171.1239,-59.27698) 9 | -------------------------------------------------------------------------------- /examples/test01_fk5_sexagecimal.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | -ellipse(11:24:37.960,-59:15:47.50,22.632",10.332",317.017) # width=3 background 6 | circle(11:24:24.230,-59:15:02.20,18.5108") # color=cyan 7 | box(11:24:39.213,-59:16:53.91,42.804",23.616",19.0384) # width=4 8 | polygon(11:24:29.737,-59:16:07.72,11:24:21.723,-59:15:43.52,11:24:23.641,-59:17:08.64,11:24:29.736,-59:16:37.13) 9 | -------------------------------------------------------------------------------- /examples/test01_gal.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | galactic 5 | -ellipse(+292:01:59.027,+01:45:33.389,22.632",10.332",297.784) # width=3 background 6 | circle(+292:00:04.651,+01:45:41.444,18.5108") # color=cyan 7 | box(+292:02:29.979,+01:44:33.837,42.804",23.616",359.806) # width=4 8 | polygon(+292:01:06.152,+01:44:53.528,+292:00:00.146,+01:44:56.102,+292:00:42.141,+01:43:40.599,+292:01:15.844,+01:44:25.760) 9 | -------------------------------------------------------------------------------- /examples/test01_img.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | image 5 | -ellipse(750,853,46,21,317.01716) # width=3 background 6 | circle(964,945,37.6236) # color=cyan 7 | box(730.5,718,87,48,19.038396) # width=4 8 | polygon(878.11234,811.88766,1003,861,973,688,878.11234,752.11234) 9 | -------------------------------------------------------------------------------- /examples/test01_mixed.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | -ellipse(171.15816,-59.263193,22.632",10.332",317.01716) # width=3 background 6 | galactic 7 | circle(+292:00:04.651,+01:45:41.444,18.5108") # color=cyan 8 | image 9 | box(730.5,718,87,48,19.038396) # width=4 10 | galactic 11 | polygon(+292:01:06.152,+01:44:53.528,+292:00:00.146,+01:44:56.102,+292:00:42.141,+01:43:40.599,+292:01:15.844,+01:44:25.760) 12 | -------------------------------------------------------------------------------- /examples/test01_print.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | circle(11:24:24.230,-59:15:02.20,18.5108") # color=cyan background 6 | box(11:24:39.213,-59:16:53.91,42.804",23.616",19.0384) # width=4 7 | -------------------------------------------------------------------------------- /examples/test02.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: pspc_skyview.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | physical 5 | ellipse(82,167,36,75,346.95915) 6 | epanda(140,166,87.19363,195.80251,1,32.5,39.5,65,79,1,307.90041) 7 | -polygon(78.265142,201.73486,132,209,125,178,163.73486,116.26514,78.265142,116.26514) 8 | -------------------------------------------------------------------------------- /examples/test04_img.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: E3000-7000.b1.img.fl.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | image 5 | -ellipse(750,853,46,21,317.017) # width=3 text={Ellipse} background 6 | circle(964,945,37.6236) # color=cyan text={Circle} 7 | box(730.5,718,87,48,19.0384) # width=4 text={Rectangle} 8 | polygon(878.11237,811.88766,1003,861,973.00003,688,878.11237,752.11234) # text={Polygon} 9 | point(800, 960) # point=box color=black text={Point} 10 | -------------------------------------------------------------------------------- /examples/test_annuli.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | image 5 | ellipse(936.01565,922.00132,30.407693,20.504055,58.654304,39.550882,107.20306,72.287547,30) 6 | annulus(712.00157,914.99658,57.134451,76.17922,95.223988,114.26876) 7 | panda(711,716,338.334,648.034,4,38.9388,77.8776,2) 8 | epanda(893,720,53.141616,122.03456,1,33.936596,47.21819,67.873192,94.43638,1,28.831146) || # epanda=(53.141616 122.03456 217.20636 259.80344 311.69855)(33.936596 47.21819 67.873192 94.43638)(28.831146) 9 | epanda(893,720,122.03456,217.20636,1,33.936596,47.21819,67.873192,94.43638,1,28.831146) || # epanda=ignore 10 | epanda(893,720,217.20636,259.80344,1,33.936596,47.21819,67.873192,94.43638,1,28.831146) || # epanda=ignore 11 | epanda(893,720,259.80344,311.69855,1,33.936596,47.21819,67.873192,94.43638,1,28.831146) # epanda=ignore 12 | bpanda(982.0005,775.9995,0,360,4,28.9995,30.9995,57.999,61.999,1,0) 13 | -------------------------------------------------------------------------------- /examples/test_annuli_ciao.reg: -------------------------------------------------------------------------------- 1 | # Region file format: CIAO version 1.0 2 | annulus(11:24:40.397,-59:15:16.99,0.468502',0.62467') 3 | annulus(11:24:40.397,-59:15:16.99,0.62467',0.780837') 4 | annulus(11:24:40.397,-59:15:16.99,0.780837',0.937004') 5 | pie(11:24:40.465,-59:16:54.89,0.319298',0.478947',338.334,415.759) 6 | pie(11:24:40.465,-59:16:54.89,0.319298',0.478947',55.759,133.184) 7 | pie(11:24:40.465,-59:16:54.89,0.319298',0.478947',133.184,210.609) 8 | pie(11:24:40.465,-59:16:54.89,0.319298',0.478947',210.609,288.034) 9 | pie(11:24:40.465,-59:16:54.89,0.478947',0.638596',338.334,415.759) 10 | pie(11:24:40.465,-59:16:54.89,0.478947',0.638596',55.759,133.184) 11 | pie(11:24:40.465,-59:16:54.89,0.478947',0.638596',133.184,210.609) 12 | pie(11:24:40.465,-59:16:54.89,0.478947',0.638596',210.609,288.034) 13 | -------------------------------------------------------------------------------- /examples/test_annuli_wcs.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | ellipse(171.10843,-59.253758,14.960585",10.087995",28.857918",19.459034",52.743907",35.565473",30) 6 | annulus(171.16832,-59.254719,28.11015",37.480176",46.850202",56.22023") 7 | panda(171.16861,-59.281915,338.334,648.034,4,19.15789",38.315779",2) 8 | epanda(171.11991,-59.281368,53.141616,122.03456,1,16.696805",23.231349",33.39361",46.462699",1,28.831146) || # epanda=(53.141616 122.03456 217.20636 259.80344 311.69855)(16.696805" 23.231349" 33.39361" 46.462699")(28.831146) 9 | epanda(171.11991,-59.281368,122.03456,217.20636,1,16.696805",23.231349",33.39361",46.462699",1,28.831146) || # epanda=ignore 10 | epanda(171.11991,-59.281368,217.20636,259.80344,1,16.696805",23.231349",33.39361",46.462699",1,28.831146) || # epanda=ignore 11 | epanda(171.11991,-59.281368,259.80344,311.69855,1,16.696805",23.231349",33.39361",46.462699",1,28.831146) # epanda=ignore 12 | bpanda(171.09611,-59.273707,0,360,4,14.267754",15.251754",28.535508",30.503508",1,0) 13 | -------------------------------------------------------------------------------- /examples/test_context.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | image 5 | # composite(818,804,22.791484) || composite=1 color=red 6 | point(818,804) || # point=boxcircle 7 | # text(917.93183,1012.7012) || textangle=22.791484 font="times 12 normal" text={I} 8 | # text(1038.5994,726.09152) || textangle=22.791484 font="times 12 normal" text={II} 9 | # text(719.41413,591.67326) || textangle=22.791484 font="times 12 normal" text={III} 10 | # text(601.61767,880.81539) || textangle=22.791484 font="times 12 normal" text={IV} 11 | box(605.81608,905.40937,312.19512,39.02439,22.791484) || 12 | box(620.9621,869.4441,312.19512,39.02439,22.791484) || 13 | box(919.09424,1016.1681,39.02439,312.19512,22.791484) || 14 | box(1014.738,738.86864,312.19512,39.02439,22.791484) || 15 | box(1029.8552,702.89125,312.19512,39.02439,22.791484) || 16 | box(716.57467,592.13153,39.02439,312.19512,22.791484) || 17 | box(666.40018,761.54827,312.19512,39.02439,22.791484) || 18 | box(651.25415,797.51354,312.19512,39.02439,22.791484) || 19 | box(636.10813,833.47882,312.19512,39.02439,22.791484) || 20 | box(590.67005,941.37466,312.19512,39.02439,22.791484) || 21 | box(575.524,977.33995,312.19512,39.02439,22.791484) || 22 | box(560.37801,1013.3052,312.19512,39.02439,22.791484) || 23 | box(1075.2743,594.98745,312.19512,39.02439,22.791484) || 24 | box(1060.1571,630.96483,312.19512,39.02439,22.791484) || 25 | box(1044.9724,666.91387,312.19512,39.02439,22.791484) || 26 | box(999.55334,774.81768,312.19512,39.02439,22.791484) || 27 | box(984.43613,810.79506,312.19512,39.02439,22.791484) || 28 | box(969.25147,846.74409,312.19512,39.02439,22.791484) || 29 | box(1026.9902,1061.6062,39.02439,312.19512,22.791484) || 30 | box(991.04728,1046.4696,39.02439,312.19512,22.791484) || 31 | box(955.03709,1031.3047,39.02439,312.19512,22.791484) || 32 | box(883.15144,1001.0315,39.02439,312.19512,22.791484) || 33 | box(847.14118,985.86658,39.02439,312.19512,22.791484) || 34 | box(811.19838,970.72999,39.02439,312.19512,22.791484) || 35 | box(775.25558,955.59341,39.02439,312.19512,22.791484) || 36 | box(860.43576,652.71563,39.02439,312.19512,22.791484) || 37 | box(824.47049,637.56961,39.02439,312.19512,22.791484) || 38 | box(788.50521,622.42358,39.02439,312.19512,22.791484) || 39 | box(752.53995,607.27755,39.02439,312.19512,22.791484) || 40 | box(680.60939,576.9855,39.02439,312.19512,22.791484) || 41 | box(644.64412,561.83947,39.02439,312.19512,22.791484) || 42 | box(608.67884,546.69344,39.02439,312.19512,22.791484) 43 | -------------------------------------------------------------------------------- /examples/test_text.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | line(171.2398,-59.290221,171.17045,-59.241461) # line=1 1 width=2 6 | # vector(171.15897,-59.260323,121.9732",291.286) vector=1 7 | # text(171.08249,-59.263998) font="helvetica 14 normal" text={Region} 8 | line(171.12155,-59.236131,171.06222,-59.243213) # line=0 0 font="helvetica 14 normal" dash=1 9 | # compass(171.09742,-59.292704,31.583948") compass=physical {N} {E} 1 1 font="helvetica 14 normal" dash=1 10 | # ruler(171.25782,-59.237453,171.2087,-59.261266) ruler=physical physical 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | 4 | requires = [ 5 | "cython>=3,<4", 6 | "extension-helpers>=1,<2", 7 | "numpy>=2,<3", 8 | "setuptools", 9 | "setuptools-scm>=6.2", 10 | ] 11 | 12 | [project] 13 | name = "pyregion" 14 | description = "python parser for ds9 region files" 15 | readme.content-type = "text/x-rst" 16 | readme.file = "README.rst" 17 | license.text = "MIT" 18 | authors = [ 19 | {name = "Jae-Joon Lee", email = "lee.j.joon@gmail.com"} 20 | ] 21 | requires-python = ">=3.10" 22 | classifiers = [ 23 | "Programming Language :: Python :: 3 :: Only", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Topic :: Scientific/Engineering :: Astronomy", 28 | ] 29 | dynamic = [ "version" ] 30 | 31 | dependencies = [ 32 | "astropy>=5", 33 | "numpy>=1.23", 34 | "pyparsing>=2" ] 35 | optional-dependencies.docs = [ 36 | "matplotlib", 37 | "sphinx", 38 | "sphinx-astropy", 39 | "sphinx-rtd-theme" ] 40 | optional-dependencies.test = [ 41 | "pytest", 42 | "pytest-astropy" 43 | ] 44 | urls.Documentation = "https://pyregion.readthedocs.io/" 45 | urls.Homepage = "https://github.com/astropy/pyregion" 46 | urls.Source = "https://github.com/astropy/pyregion" 47 | urls.Tracker = "https://github.com/astropy/pyregion/issues" 48 | 49 | [tool.setuptools] 50 | include-package-data = true 51 | 52 | [tool.setuptools.packages] 53 | find = { namespaces = false } 54 | 55 | [tool.setuptools.package-data] 56 | "pyregion.tests.data" = [ "*.header", "*.reg" ] 57 | 58 | [tool.distutils.upload_docs] 59 | upload-dir = "docs/_build/html" 60 | show-response = 1 61 | 62 | [tool.setuptools_scm] 63 | write_to = "pyregion/version.py" 64 | 65 | [tool.pytest.ini_options] 66 | minversion = "6.0" 67 | norecursedirs = [ "build", "docs/_build" ] 68 | testpaths = [ "pyregion", "docs" ] 69 | 70 | [tool.extension-helpers] 71 | use_extension_helpers = "true" 72 | -------------------------------------------------------------------------------- /pyregion/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyregion: a Python parser for ds9 region files 3 | 4 | * Code : https://github.com/astropy/pyregion 5 | * Docs : http://pyregion.readthedocs.io/ 6 | 7 | See also the in-development ``regions`` package 8 | at https://github.com/astropy/regions 9 | a new astronomy package for regions based on Astropy. 10 | """ 11 | 12 | from .core import * 13 | from .core import open 14 | from .parser_helper import Shape 15 | from .version import __version__ 16 | -------------------------------------------------------------------------------- /pyregion/c_numpy.pxd: -------------------------------------------------------------------------------- 1 | # :Author: Travis Oliphant 2 | 3 | cdef extern from "numpy/arrayobject.h": 4 | cdef enum NPY_TYPES: 5 | NPY_BOOL 6 | NPY_BYTE 7 | NPY_UBYTE 8 | NPY_SHORT 9 | NPY_USHORT 10 | NPY_INT 11 | NPY_UINT 12 | NPY_LONG 13 | NPY_ULONG 14 | NPY_LONGLONG 15 | NPY_ULONGLONG 16 | NPY_FLOAT 17 | NPY_DOUBLE 18 | NPY_LONGDOUBLE 19 | NPY_CFLOAT 20 | NPY_CDOUBLE 21 | NPY_CLONGDOUBLE 22 | NPY_OBJECT 23 | NPY_STRING 24 | NPY_UNICODE 25 | NPY_VOID 26 | NPY_NTYPES 27 | NPY_NOTYPE 28 | 29 | cdef enum requirements: 30 | NPY_CONTIGUOUS 31 | NPY_FORTRAN 32 | NPY_OWNDATA 33 | NPY_FORCECAST 34 | NPY_ENSURECOPY 35 | NPY_ENSUREARRAY 36 | NPY_ELEMENTSTRIDES 37 | NPY_ALIGNED 38 | NPY_NOTSWAPPED 39 | NPY_WRITEABLE 40 | NPY_UPDATEIFCOPY 41 | NPY_ARR_HAS_DESCR 42 | 43 | NPY_BEHAVED 44 | NPY_BEHAVED_NS 45 | NPY_CARRAY 46 | NPY_CARRAY_RO 47 | NPY_FARRAY 48 | NPY_FARRAY_RO 49 | NPY_DEFAULT 50 | 51 | NPY_IN_ARRAY 52 | NPY_OUT_ARRAY 53 | NPY_INOUT_ARRAY 54 | NPY_IN_FARRAY 55 | NPY_OUT_FARRAY 56 | NPY_INOUT_FARRAY 57 | 58 | NPY_UPDATE_ALL 59 | 60 | cdef enum defines: 61 | # Note: as of Pyrex 0.9.5, enums are type-checked more strictly, so this 62 | # can't be used as an integer. 63 | NPY_MAXDIMS 64 | 65 | ctypedef struct npy_cdouble: 66 | double real 67 | double imag 68 | 69 | ctypedef struct npy_cfloat: 70 | double real 71 | double imag 72 | 73 | ctypedef int npy_intp 74 | 75 | ctypedef short npy_bool 76 | 77 | ctypedef extern class numpy.dtype[object PyArray_Descr]: 78 | cdef int type_num, elsize, alignment 79 | cdef char type, kind, byteorder, hasobject 80 | cdef object fields, typeobj 81 | 82 | ctypedef extern class numpy.ndarray[object PyArrayObject]: 83 | cdef char *data 84 | cdef int nd 85 | cdef npy_intp *dimensions 86 | cdef npy_intp *strides 87 | cdef object base 88 | cdef dtype descr 89 | cdef int flags 90 | 91 | ctypedef extern class numpy.flatiter[object PyArrayIterObject]: 92 | cdef int nd_m1 93 | cdef npy_intp index, size 94 | cdef ndarray ao 95 | cdef char *dataptr 96 | 97 | ctypedef extern class numpy.broadcast[object PyArrayMultiIterObject]: 98 | cdef int numiter 99 | cdef npy_intp size, index 100 | cdef int nd 101 | # These next two should be arrays of [NPY_MAXITER], but that is 102 | # difficult to cleanly specify in Pyrex. Fortunately, it doesn't matter. 103 | cdef npy_intp *dimensions 104 | cdef void ** iters 105 | 106 | object PyArray_ZEROS(int ndims, npy_intp*dims, NPY_TYPES type_num, int fortran) 107 | object PyArray_EMPTY(int ndims, npy_intp*dims, NPY_TYPES type_num, int fortran) 108 | dtype PyArray_DescrFromTypeNum(NPY_TYPES type_num) 109 | object PyArray_SimpleNew(int ndims, npy_intp*dims, NPY_TYPES type_num) 110 | int PyArray_Check(object obj) 111 | object PyArray_ContiguousFromAny(object obj, NPY_TYPES type, 112 | int mindim, int maxdim) 113 | object PyArray_CopyFromObject(object obj, NPY_TYPES type, int mindim, int maxdim) 114 | npy_intp PyArray_SIZE(ndarray arr) 115 | npy_intp PyArray_NBYTES(ndarray arr) 116 | void *PyArray_DATA(ndarray arr) 117 | object PyArray_FromAny(object obj, dtype newtype, int mindim, int maxdim, 118 | int requirements, object context) 119 | object PyArray_FROMANY(object obj, NPY_TYPES type_num, int min, 120 | int max, int requirements) 121 | object PyArray_FROM_OTF(object obj, NPY_TYPES type_num, int requirements) 122 | object PyArray_NewFromDescr(object subtype, dtype newtype, int nd, 123 | npy_intp*dims, npy_intp*strides, void*data, 124 | int flags, object parent) 125 | 126 | object PyArray_IterNew(object obj) 127 | void PyArray_ITER_NEXT(flatiter it) 128 | 129 | int _import_array() except -1 130 | int __pyx_import_array "_import_array"() except -1 131 | 132 | 133 | void import_ufunc() 134 | 135 | cdef inline int import_array() except -1: 136 | try: 137 | __pyx_import_array() 138 | except Exception: 139 | raise ImportError("numpy._core.multiarray failed to import") 140 | -------------------------------------------------------------------------------- /pyregion/c_python.pxd: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python -*- Not really, but close enough 2 | 3 | # Expose as much of the Python C API as we need here 4 | 5 | cdef extern from "stdlib.h": 6 | ctypedef int size_t 7 | 8 | cdef extern from "Python.h": 9 | ctypedef int Py_intptr_t 10 | void* PyMem_Malloc(size_t) 11 | void* PyMem_Realloc(void *p, size_t n) 12 | void PyMem_Free(void *p) 13 | char* PyString_AsString(object string) 14 | object PyString_FromString(char *v) 15 | object PyString_InternFromString(char *v) 16 | int PyErr_CheckSignals() 17 | object PyFloat_FromDouble(double v) 18 | void Py_XINCREF(object o) 19 | void Py_XDECREF(object o) 20 | void Py_CLEAR(object o) # use instead of decref 21 | void* PyCObject_AsVoidPtr(object o) 22 | object PyObject_GetAttrString(object o, char *v) 23 | 24 | ctypedef struct PyListObject: 25 | void *ob_item 26 | 27 | void * PyList_GET_ITEM(PyListObject *o, int i) 28 | int PyList_GET_SIZE(PyListObject *o) 29 | 30 | int PySequence_Check(object o) 31 | object PySequence_GetItem(object o, int i) 32 | int PySequence_Length(object o) 33 | -------------------------------------------------------------------------------- /pyregion/conftest.py: -------------------------------------------------------------------------------- 1 | try: 2 | from pytest_astropy_header.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS 3 | except ImportError: # In case this plugin is not installed 4 | PYTEST_HEADER_MODULES = {} 5 | TESTED_VERSIONS = {} 6 | 7 | 8 | def pytest_configure(config): 9 | from pyregion import __version__ as version 10 | 11 | config.option.astropy_header = True 12 | 13 | PYTEST_HEADER_MODULES.pop('Scipy', None) 14 | PYTEST_HEADER_MODULES.pop('h5py', None) 15 | PYTEST_HEADER_MODULES.pop('Pandas', None) 16 | PYTEST_HEADER_MODULES['Astropy'] = 'astropy' 17 | PYTEST_HEADER_MODULES['pyparsing'] = 'pyparsing' 18 | TESTED_VERSIONS['pyregion'] = version 19 | -------------------------------------------------------------------------------- /pyregion/core.py: -------------------------------------------------------------------------------- 1 | from itertools import cycle 2 | 3 | from .ds9_region_parser import RegionParser 4 | from .wcs_converter import check_wcs as _check_wcs 5 | 6 | _builtin_open = open 7 | 8 | 9 | class ShapeList(list): 10 | """A list of `~pyregion.Shape` objects. 11 | 12 | Parameters 13 | ---------- 14 | shape_list : list 15 | List of `pyregion.Shape` objects 16 | comment_list : list, None 17 | List of comment strings for each argument 18 | """ 19 | 20 | def __init__(self, shape_list, comment_list=None): 21 | if comment_list is not None: 22 | if len(comment_list) != len(shape_list): 23 | err = "Ambiguous number of comments {} for number of shapes {}" 24 | raise ValueError(err.format(len(comment_list), 25 | len(shape_list))) 26 | self._comment_list = comment_list 27 | list.__init__(self, shape_list) 28 | 29 | def __getitem__(self, key): 30 | if isinstance(key, slice): 31 | return ShapeList(list.__getitem__(self, key)) 32 | else: 33 | return list.__getitem__(self, key) 34 | 35 | def __getslice__(self, i, j): 36 | return self[max(0, i):max(0, j):] 37 | 38 | def check_imagecoord(self): 39 | """Are all shapes in image coordinates? 40 | 41 | Returns ``True`` if yes, and ``False`` if not. 42 | """ 43 | if [s for s in self if s.coord_format != "image"]: 44 | return False 45 | else: 46 | return True 47 | 48 | def as_imagecoord(self, header): 49 | """New shape list in image coordinates. 50 | 51 | Parameters 52 | ---------- 53 | header : `~astropy.io.fits.Header` 54 | FITS header 55 | 56 | Returns 57 | ------- 58 | shape_list : `ShapeList` 59 | New shape list, with coordinates of the each shape 60 | converted to the image coordinate using the given header 61 | information. 62 | """ 63 | 64 | comment_list = self._comment_list 65 | if comment_list is None: 66 | comment_list = cycle([None]) 67 | 68 | r = RegionParser.sky_to_image(zip(self, comment_list), 69 | header) 70 | shape_list, comment_list = zip(*list(r)) 71 | return ShapeList(shape_list, comment_list=comment_list) 72 | 73 | def get_mpl_patches_texts(self, properties_func=None, 74 | text_offset=5.0, 75 | origin=1): 76 | """ 77 | Often, the regions files implicitly assume the lower-left 78 | corner of the image as a coordinate (1,1). However, the python 79 | convetion is that the array index starts from 0. By default 80 | (``origin=1``), coordinates of the returned mpl artists have 81 | coordinate shifted by (1, 1). If you do not want this shift, 82 | use ``origin=0``. 83 | """ 84 | from .mpl_helper import as_mpl_artists 85 | patches, txts = as_mpl_artists(self, properties_func, 86 | text_offset, 87 | origin=origin) 88 | 89 | return patches, txts 90 | 91 | def get_filter(self, header=None, origin=1): 92 | """Get filter. 93 | Often, the regions files implicitly assume the lower-left 94 | corner of the image as a coordinate (1,1). However, the python 95 | convetion is that the array index starts from 0. By default 96 | (``origin=1``), coordinates of the returned mpl artists have 97 | coordinate shifted by (1, 1). If you do not want this shift, 98 | use ``origin=0``. 99 | 100 | Parameters 101 | ---------- 102 | header : `astropy.io.fits.Header` 103 | FITS header 104 | origin : {0, 1} 105 | Pixel coordinate origin 106 | 107 | Returns 108 | ------- 109 | filter : TODO 110 | Filter object 111 | """ 112 | 113 | from .region_to_filter import as_region_filter 114 | 115 | if header is None: 116 | if not self.check_imagecoord(): 117 | raise RuntimeError("the region has non-image coordinate. header is required.") 118 | reg_in_imagecoord = self 119 | else: 120 | reg_in_imagecoord = self.as_imagecoord(header) 121 | 122 | region_filter = as_region_filter(reg_in_imagecoord, origin=origin) 123 | 124 | return region_filter 125 | 126 | def get_mask(self, hdu=None, header=None, shape=None): 127 | """Create a 2-d mask. 128 | 129 | Parameters 130 | ---------- 131 | hdu : `astropy.io.fits.ImageHDU` 132 | FITS image HDU 133 | header : `~astropy.io.fits.Header` 134 | FITS header 135 | shape : tuple 136 | Image shape 137 | 138 | Returns 139 | ------- 140 | mask : `numpy.array` 141 | Boolean mask 142 | 143 | Examples 144 | -------- 145 | get_mask(hdu=f[0]) 146 | get_mask(shape=(10,10)) 147 | get_mask(header=f[0].header, shape=(10,10)) 148 | """ 149 | 150 | if hdu and header is None: 151 | header = hdu.header 152 | if hdu and shape is None: 153 | shape = hdu.data.shape 154 | 155 | region_filter = self.get_filter(header=header) 156 | mask = region_filter.mask(shape) 157 | 158 | return mask 159 | 160 | def write(self, outfile): 161 | """Write this shape list to a region file. 162 | 163 | Parameters 164 | ---------- 165 | outfile : str 166 | File name 167 | """ 168 | if len(self) < 1: 169 | print("WARNING: The region list is empty. The region file " 170 | "'{:s}' will be empty.".format(outfile)) 171 | try: 172 | outf = _builtin_open(outfile, 'w') 173 | outf.close() 174 | return 175 | except IOError as e: 176 | cmsg = "Unable to create region file '{:s}'.".format(outfile) 177 | if e.args: 178 | e.args = (e.args[0] + '\n' + cmsg,) + e.args[1:] 179 | else: 180 | e.args = (cmsg,) 181 | raise e 182 | 183 | prev_cs = self[0].coord_format 184 | 185 | outf = None 186 | try: 187 | outf = _builtin_open(outfile, 'w') 188 | 189 | attr0 = self[0].attr[1] 190 | defaultline = " ".join(["{:s}={:s}".format(a, attr0[a]) 191 | for a in attr0 if a != 'text']) 192 | 193 | # first line is globals 194 | outf.write("global {0}\n".format(defaultline)) 195 | # second line must be a coordinate format 196 | outf.write("{0}\n".format(prev_cs)) 197 | 198 | for shape in self: 199 | shape_attr = '' if prev_cs == shape.coord_format \ 200 | else shape.coord_format + "; " 201 | shape_excl = '-' if shape.exclude else '' 202 | text_coordlist = ["{:f}".format(f) for f in shape.coord_list] 203 | shape_coords = "(" + ",".join(text_coordlist) + ")" 204 | shape_comment = " # " + shape.comment if shape.comment else '' 205 | 206 | shape_str = (shape_attr + shape_excl + shape.name + 207 | shape_coords + shape_comment) 208 | 209 | outf.write("{0}\n".format(shape_str)) 210 | 211 | except IOError as e: 212 | cmsg = "Unable to create region file \'{:s}\'.".format(outfile) 213 | if e.args: 214 | e.args = (e.args[0] + '\n' + cmsg,) + e.args[1:] 215 | else: 216 | e.args = (cmsg,) 217 | raise e 218 | finally: 219 | if outf: 220 | outf.close() 221 | 222 | 223 | def parse(region_string): 224 | """Parse DS9 region string into a ShapeList. 225 | 226 | Parameters 227 | ---------- 228 | region_string : str 229 | Region string 230 | 231 | Returns 232 | ------- 233 | shapes : `ShapeList` 234 | List of `~pyregion.Shape` 235 | """ 236 | rp = RegionParser() 237 | ss = rp.parse(region_string) 238 | sss1 = rp.convert_attr(ss) 239 | sss2 = _check_wcs(sss1) 240 | 241 | shape_list, comment_list = rp.filter_shape2(sss2) 242 | return ShapeList(shape_list, comment_list=comment_list) 243 | 244 | 245 | def open(fname): 246 | """Open, read and parse DS9 region file. 247 | 248 | Parameters 249 | ---------- 250 | fname : str 251 | Filename 252 | 253 | Returns 254 | ------- 255 | shapes : `ShapeList` 256 | List of `~pyregion.Shape` 257 | """ 258 | with _builtin_open(fname) as fh: 259 | region_string = fh.read() 260 | return parse(region_string) 261 | 262 | 263 | def read_region(s): 264 | """Read region. 265 | 266 | Parameters 267 | ---------- 268 | s : str 269 | Region string 270 | 271 | Returns 272 | ------- 273 | shapes : `ShapeList` 274 | List of `~pyregion.Shape` 275 | """ 276 | rp = RegionParser() 277 | ss = rp.parse(s) 278 | sss1 = rp.convert_attr(ss) 279 | sss2 = _check_wcs(sss1) 280 | 281 | shape_list = rp.filter_shape(sss2) 282 | return ShapeList(shape_list) 283 | 284 | 285 | def read_region_as_imagecoord(s, header): 286 | """Read region as image coordinates. 287 | 288 | Parameters 289 | ---------- 290 | s : str 291 | Region string 292 | header : `~astropy.io.fits.Header` 293 | FITS header 294 | 295 | Returns 296 | ------- 297 | shapes : `~pyregion.ShapeList` 298 | List of `~pyregion.Shape` 299 | """ 300 | rp = RegionParser() 301 | ss = rp.parse(s) 302 | sss1 = rp.convert_attr(ss) 303 | sss2 = _check_wcs(sss1) 304 | sss3 = rp.sky_to_image(sss2, header) 305 | 306 | shape_list = rp.filter_shape(sss3) 307 | return ShapeList(shape_list) 308 | 309 | 310 | def get_mask(region, hdu, origin=1): 311 | """Get mask. 312 | 313 | Parameters 314 | ---------- 315 | region : `~pyregion.ShapeList` 316 | List of `~pyregion.Shape` 317 | hdu : `~astropy.io.fits.ImageHDU` 318 | FITS image HDU 319 | origin : float 320 | TODO: document me 321 | 322 | Returns 323 | ------- 324 | mask : `~numpy.array` 325 | Boolean mask 326 | 327 | Examples 328 | -------- 329 | >>> from astropy.io import fits 330 | >>> from pyregion import read_region_as_imagecoord, get_mask 331 | >>> hdu = fits.open("test.fits")[0] 332 | >>> region = "test01.reg" 333 | >>> reg = read_region_as_imagecoord(open(region), f[0].header) 334 | >>> mask = get_mask(reg, hdu) 335 | """ 336 | from pyregion.region_to_filter import as_region_filter 337 | 338 | data = hdu.data 339 | region_filter = as_region_filter(region, origin=origin) 340 | mask = region_filter.mask(data) 341 | return mask 342 | -------------------------------------------------------------------------------- /pyregion/ds9_attr_parser.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from pyparsing import (Literal, CaselessKeyword, Word, Optional, Combine, 4 | ZeroOrMore, nums, alphas, And, Or, quotedString, 5 | QuotedString, White) 6 | 7 | from .region_numbers import CoordOdd, CoordEven, Distance, Angle 8 | 9 | from .parser_helper import wcs_shape, define_shape_helper, Shape 10 | 11 | 12 | def get_ds9_attr_parser(): 13 | lhs = Word(alphas) 14 | paren = QuotedString("(", endQuoteChar=")") 15 | rhs = Or([Word(alphas + nums), 16 | Combine(Literal("#") + Word(alphas + nums)), # color with '#' 17 | Combine(Word(alphas) + White() + Word(nums)), # for point 18 | quotedString, 19 | QuotedString("{", endQuoteChar="}"), 20 | paren + ZeroOrMore(paren), 21 | Word(nums + " "), 22 | Word(nums + ".") 23 | ]) 24 | expr = lhs + Optional(Literal("=").suppress() + rhs) 25 | expr.setParseAction(lambda s, l, tok: tuple(tok)) 26 | 27 | return ZeroOrMore(expr) 28 | 29 | 30 | ds9_shape_in_comment_defs = dict( 31 | text=wcs_shape(CoordOdd, CoordEven), 32 | vector=wcs_shape(CoordOdd, CoordEven, 33 | Distance, Angle), 34 | composite=wcs_shape(CoordOdd, CoordEven, Angle), 35 | ruler=wcs_shape(CoordOdd, CoordEven, CoordOdd, CoordEven), 36 | compass=wcs_shape(CoordOdd, CoordEven, Distance), 37 | projection=wcs_shape(CoordOdd, CoordEven, CoordOdd, CoordEven, Distance), 38 | segment=wcs_shape(CoordOdd, CoordEven, 39 | repeat=(0, 2)) 40 | ) 41 | 42 | 43 | class Ds9AttrParser(object): 44 | def set_continued(self, s, l, tok): 45 | self.continued = True 46 | 47 | def __init__(self): 48 | self.continued = False 49 | 50 | ds9_attr_parser = get_ds9_attr_parser() 51 | 52 | regionShape = define_shape_helper(ds9_shape_in_comment_defs) 53 | regionShape = regionShape.setParseAction(lambda s, l, tok: Shape(tok[0], tok[1:])) 54 | 55 | self.parser_default = ds9_attr_parser 56 | 57 | cont = CaselessKeyword("||").setParseAction(self.set_continued).suppress() 58 | line = Optional(And([regionShape, Optional(cont)])) + ds9_attr_parser 59 | 60 | self.parser_with_shape = line 61 | 62 | def parse_default(self, s): 63 | return self.parser_default.parseString(s) 64 | 65 | def parse_check_shape(self, s): 66 | l = self.parser_with_shape.parseString(s) 67 | if l and isinstance(l[0], Shape): 68 | if self.continued: 69 | l[0].continued = True 70 | return l[0], l[1:] 71 | else: 72 | return None, l 73 | 74 | 75 | def get_attr(attr_list, global_attrs): 76 | """ 77 | Parameters 78 | ---------- 79 | attr_list : list 80 | A list of (keyword, value) tuple pairs 81 | global_attrs : tuple(list, dict) 82 | Global attributes which update the local attributes 83 | """ 84 | local_attr = [], {} 85 | for kv in attr_list: 86 | keyword = kv[0] 87 | if len(kv) == 1: 88 | local_attr[0].append(keyword) 89 | continue 90 | elif len(kv) == 2: 91 | value = kv[1] 92 | elif len(kv) > 2: 93 | value = kv[1:] 94 | 95 | if keyword == 'tag': 96 | local_attr[1].setdefault(keyword, set()).add(value) 97 | else: 98 | local_attr[1][keyword] = value 99 | 100 | attr0 = copy.copy(global_attrs[0]) 101 | attr1 = copy.copy(global_attrs[1]) 102 | 103 | if local_attr[0]: 104 | attr0.extend(local_attr[0]) 105 | 106 | if local_attr[1]: 107 | attr1.update(local_attr[1]) 108 | 109 | return attr0, attr1 110 | -------------------------------------------------------------------------------- /pyregion/ds9_region_parser.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import warnings 3 | from pyparsing import (Literal, CaselessKeyword, CaselessLiteral, 4 | Optional, And, Or, Word, 5 | StringEnd, ParseException, Combine, 6 | alphas) 7 | from .region_numbers import CoordOdd, CoordEven, Distance, Angle, Integer 8 | from .parser_helper import (wcs_shape, define_shape_helper, Shape, Global, 9 | RegionPusher, define_expr, define_line, 10 | CoordCommand, 11 | comment_shell_like, define_simple_literals) 12 | from .ds9_attr_parser import Ds9AttrParser, get_attr 13 | from .wcs_converter import (convert_to_imagecoord, 14 | convert_physical_to_imagecoord) 15 | 16 | ds9_shape_defs = dict( 17 | circle=wcs_shape(CoordOdd, CoordEven, Distance), 18 | rotbox=wcs_shape(CoordOdd, CoordEven, Distance, Distance, Angle, 19 | repeat=(2, 4)), 20 | box=wcs_shape(CoordOdd, CoordEven, Distance, Distance, Angle, 21 | repeat=(2, 4)), 22 | polygon=wcs_shape(CoordOdd, CoordEven, repeat=(0, 2)), 23 | ellipse=wcs_shape(CoordOdd, CoordEven, Distance, Distance, Angle, repeat=(2, 4)), 24 | annulus=wcs_shape(CoordOdd, CoordEven, Distance, repeat=(2, 3)), 25 | panda=wcs_shape(CoordOdd, CoordEven, Angle, Angle, Integer, Distance, Distance, Integer), 26 | pie=wcs_shape(CoordOdd, CoordEven, 27 | Distance, Distance, 28 | Angle, Angle), 29 | epanda=wcs_shape(CoordOdd, CoordEven, 30 | Angle, Angle, Integer, 31 | Distance, Distance, Distance, 32 | Distance, Integer, Angle), 33 | bpanda=wcs_shape(CoordOdd, CoordEven, 34 | Angle, Angle, Integer, 35 | Distance, Distance, Distance, 36 | Distance, Integer, Angle), 37 | point=wcs_shape(CoordOdd, CoordEven), 38 | line=wcs_shape(CoordOdd, CoordEven, CoordOdd, CoordEven), 39 | vector=wcs_shape(CoordOdd, CoordEven, Distance, Angle), 40 | text=wcs_shape(CoordOdd, CoordEven), 41 | ) 42 | 43 | image_like_coordformats = ["image", "physical", "detector", "logical"] 44 | 45 | 46 | class RegionParser(RegionPusher): 47 | 48 | def __init__(self): 49 | 50 | RegionPusher.__init__(self) 51 | 52 | self.shape_definition = ds9_shape_defs 53 | regionShape = define_shape_helper(self.shape_definition) 54 | regionShape = regionShape.setParseAction(lambda s, l, tok: Shape(tok[0], tok[1:])) 55 | 56 | regionExpr = define_expr(regionShape, 57 | negate_func=lambda s, l, tok: tok[-1].set_exclude(), 58 | ) 59 | 60 | coord_command_keys = ['PHYSICAL', 'IMAGE', 'FK4', 'B1950', 'FK5', 61 | 'J2000', 'GALACTIC', 'ECLIPTIC', 'ICRS', 62 | 'LINEAR', 'AMPLIFIER', 'DETECTOR'] 63 | 64 | coordCommandLiterals = define_simple_literals(coord_command_keys) 65 | coordCommandWCS = Combine(CaselessLiteral("WCS") + Optional(Word(alphas))) 66 | 67 | coordCommand = (coordCommandLiterals | coordCommandWCS) 68 | coordCommand.setParseAction(lambda s, l, tok: CoordCommand(tok[-1])) 69 | 70 | regionGlobal = comment_shell_like(CaselessKeyword("global"), 71 | lambda s, l, tok: Global(tok[-1])) 72 | 73 | regionAtom = (regionExpr | coordCommand | regionGlobal) 74 | 75 | regionAtom = regionAtom.setParseAction(self.pushAtom) 76 | 77 | regionComment = comment_shell_like(Literal("#"), 78 | parseAction=self.pushComment) 79 | 80 | line_simple = define_line(atom=regionAtom, 81 | separator=Literal(";"), 82 | comment=regionComment 83 | ) 84 | 85 | line_w_composite = And([regionAtom, 86 | CaselessKeyword("||").setParseAction(self.set_continued) 87 | ]) \ 88 | + Optional(regionComment) 89 | 90 | line = Or([line_simple, line_w_composite]) 91 | 92 | self.parser = Optional(line) + StringEnd() 93 | 94 | def parseLine(self, l): 95 | self.parser.parseString(l) 96 | s, c, continued = self.stack, self.comment, self.continued 97 | self.flush() 98 | 99 | return s, c, continued 100 | 101 | def parse(self, s): 102 | 103 | for l in s.split("\n"): 104 | try: 105 | s, c, continued = self.parseLine(l) 106 | except ParseException: 107 | warnings.warn("Failed to parse : " + l) 108 | self.flush() 109 | continue 110 | 111 | if len(s) > 1: 112 | for s1 in s[:-1]: 113 | yield s1, None 114 | 115 | s[-1].comment = c 116 | s[-1].continued = continued 117 | yield s[-1], c 118 | elif len(s) == 1: 119 | s[-1].comment = c 120 | s[-1].continued = continued 121 | yield s[-1], c 122 | elif c: 123 | yield None, c 124 | 125 | self.flush() 126 | 127 | def convert_attr(self, l): 128 | global_attr = [], {} 129 | 130 | parser = Ds9AttrParser() 131 | 132 | for l1, c1 in l: 133 | if isinstance(l1, Global): 134 | for kv in parser.parse_default(l1.text): 135 | if len(kv) == 1: 136 | global_attr[0].append(kv[0]) 137 | elif len(kv) == 2: 138 | if kv[0] == 'tag': 139 | global_attr[1].setdefault(kv[0], set()).add(kv[1]) 140 | else: 141 | global_attr[1][kv[0]] = kv[1] 142 | 143 | elif isinstance(l1, Shape): 144 | if c1: 145 | attr_list = parser.parse_default(c1) 146 | attr0, attr1 = get_attr(attr_list, global_attr) 147 | else: 148 | attr0, attr1 = global_attr 149 | l1n = copy.copy(l1) 150 | l1n.attr = attr0, attr1 151 | yield l1n, c1 152 | 153 | elif not l1 and c1: 154 | shape, attr_list = parser.parse_check_shape(c1) 155 | if shape: 156 | shape.attr = get_attr(attr_list, global_attr) 157 | yield shape, c1 158 | else: 159 | yield l1, c1 160 | 161 | @staticmethod 162 | def sky_to_image(shape_list, header): 163 | """Converts a `ShapeList` into shapes with coordinates in image coordinates 164 | 165 | Parameters 166 | ---------- 167 | shape_list : `pyregion.ShapeList` 168 | The ShapeList to convert 169 | header : `~astropy.io.fits.Header` 170 | Specifies what WCS transformations to use. 171 | 172 | Yields 173 | ------- 174 | shape, comment : Shape, str 175 | Shape with image coordinates and the associated comment 176 | 177 | Note 178 | ---- 179 | The comments in the original `ShapeList` are unaltered 180 | 181 | """ 182 | 183 | for shape, comment in shape_list: 184 | if isinstance(shape, Shape) and \ 185 | (shape.coord_format not in image_like_coordformats): 186 | 187 | new_coords = convert_to_imagecoord(shape, header) 188 | 189 | l1n = copy.copy(shape) 190 | 191 | l1n.coord_list = new_coords 192 | l1n.coord_format = "image" 193 | yield l1n, comment 194 | 195 | elif isinstance(shape, Shape) and shape.coord_format == "physical": 196 | 197 | if header is None: 198 | raise RuntimeError("Physical coordinate is not known.") 199 | 200 | new_coordlist = convert_physical_to_imagecoord(shape, header) 201 | 202 | l1n = copy.copy(shape) 203 | 204 | l1n.coord_list = new_coordlist 205 | l1n.coord_format = "image" 206 | yield l1n, comment 207 | 208 | else: 209 | yield shape, comment 210 | 211 | def filter_shape(self, sss): 212 | return [s1[0] for s1 in sss if isinstance(s1[0], Shape)] 213 | 214 | @staticmethod 215 | def filter_shape2(sss): 216 | r = [s1 for s1 in sss if isinstance(s1[0], Shape)] 217 | return zip(*r) 218 | -------------------------------------------------------------------------------- /pyregion/geom.h: -------------------------------------------------------------------------------- 1 | 2 | #inlcude 3 | 4 | 5 | typedef struct { 6 | double sin_delta; 7 | double cos_delta; 8 | double cos_delta2; 9 | } Metric; 10 | 11 | 12 | void metric_init(Metric *m, double x0, double y0) { 13 | 14 | double theta = y0/180.*3.1415926; 15 | 16 | m->sin_delta = sin(theta); 17 | m->cos_delta = cos(theta); 18 | m->cos_delta2 = m->cos_delta * m->cos_delta; 19 | 20 | } 21 | 22 | 23 | double metric_distance2(Metric *m, double x1, double y1, double x2, double y2) { 24 | double dx = (x2-x1); 25 | double dy = (y2-y1); 26 | 27 | return (dx*dx*cos_delta2 + dy*dy) 28 | } 29 | 30 | 31 | void metric_rotate(Metric *m, double x1, double y1, double *x2, double *y2) { 32 | 33 | } 34 | 35 | 36 | 37 | typedef struct { 38 | double g_x; 39 | double g_y; 40 | } Metric; 41 | 42 | 43 | void metric_init(Metric *m, double x0, double y0) { 44 | 45 | double theta = y0/180.*3.1415926; 46 | 47 | m->g_x = cos(theta); 48 | m->g_y = 1.; 49 | 50 | } 51 | 52 | 53 | double metric_distance2(Metric *m, double x1, double y1, double x2, double y2) { 54 | double dx = (x2-x1); 55 | double dy = (y2-y1); 56 | 57 | return ((dx*g_x)^2 + (dy*g_y)^2); 58 | } 59 | 60 | 61 | 62 | 63 | 64 | 65 | #define DISTANCE( x0, y0, x1, y1, x2, y2, d ) ((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)) 66 | #define ROTATE( x0, y0, x1, y1, angle, x2, y2) 67 | -------------------------------------------------------------------------------- /pyregion/mpl_helper.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import numpy as np 3 | from math import cos, sin, pi, atan2 4 | import warnings 5 | import matplotlib.patches as mpatches 6 | from matplotlib.path import Path 7 | from matplotlib.lines import Line2D 8 | from matplotlib.transforms import Affine2D, Bbox, IdentityTransform 9 | from matplotlib.text import Annotation 10 | 11 | 12 | def rotated_polygon(xy, ox, oy, angle): 13 | # angle in degree 14 | theta = angle / 180. * pi 15 | 16 | st = sin(theta) 17 | ct = cos(theta) 18 | 19 | xy = np.asarray(xy, dtype="d") 20 | x, y = xy[:, 0], xy[:, 1] 21 | x1 = x - ox 22 | y1 = y - oy 23 | 24 | x2 = ct * x1 + -st * y1 25 | y2 = st * x1 + ct * y1 26 | 27 | xp = x2 + ox 28 | yp = y2 + oy 29 | 30 | return np.hstack((xp.reshape((-1, 1)), yp.reshape((-1, 1)))) 31 | 32 | # sss3 = [s1[0] for s1 in sss2 if isinstance(s1[0], parser_ds9.Shape)] 33 | 34 | 35 | _point_type_dict = dict(circle="o", 36 | box="s", 37 | diamond="D", 38 | x="x", 39 | cross="+", 40 | arrow="^", 41 | boxcircle="*") 42 | 43 | _ds9_to_mpl_colormap = dict(green="lime", 44 | ) 45 | 46 | 47 | def properties_func_default(shape, saved_attrs): 48 | attr_list = copy.copy(shape.attr[0]) 49 | attr_dict = copy.copy(shape.attr[1]) 50 | 51 | attr_list.extend(saved_attrs[0]) 52 | attr_dict.update(saved_attrs[1]) 53 | 54 | color = attr_dict.get("color", None) 55 | color = _ds9_to_mpl_colormap.get(color, color) 56 | 57 | if shape.name == "text": 58 | kwargs = dict(color=color, 59 | rotation=float(attr_dict.get("textangle", 0)), 60 | ) 61 | font = attr_dict.get("font") 62 | if font: 63 | a = font.split() 64 | if len(a) >= 3: 65 | fontsize = float(a[1]) 66 | kwargs["fontsize"] = fontsize 67 | elif shape.name == "point": 68 | point_attrs = attr_dict.get("point", "boxcircle").split() 69 | if len(point_attrs) == 1: 70 | point_type = point_attrs[0] 71 | point_size = 11 72 | elif len(point_attrs) > 1: 73 | point_type = point_attrs[0] 74 | point_size = int(point_attrs[1]) 75 | 76 | marker = _point_type_dict.get(point_type, "o") 77 | kwargs = dict(markeredgecolor=color, 78 | markerfacecolor="none", 79 | marker=marker, 80 | markeredgewidth=int(attr_dict.get("width", 1)), 81 | markersize=point_size 82 | ) 83 | elif shape.name in ["line", "vector"]: 84 | fontsize = 10 # default font size 85 | 86 | font = attr_dict.get("font") 87 | if font: 88 | a = font.split() 89 | if len(a) >= 3: 90 | fontsize = float(a[1]) 91 | 92 | kwargs = dict(color=color, 93 | linewidth=int(attr_dict.get("width", 1)), 94 | mutation_scale=fontsize, 95 | ) 96 | if int(attr_dict.get("dash", "0")): 97 | kwargs["linestyle"] = "dashed" 98 | 99 | else: 100 | # The default behavior of matplotlib edgecolor has changed, and it does 101 | # not draw edges by default. To remdy this, simply use black edgecolor 102 | # if None. 103 | # https://matplotlib.org/stable/users/dflt_style_changes.html#patch-edges-and-color 104 | 105 | if color is None: 106 | color = "k" 107 | kwargs = dict(edgecolor=color, 108 | linewidth=int(attr_dict.get("width", 1)), 109 | facecolor="none" 110 | ) 111 | 112 | if "background" in attr_list: 113 | kwargs["linestyle"] = "dashed" 114 | 115 | if int(attr_dict.get("dash", "0")): 116 | kwargs["linestyle"] = "dashed" 117 | if shape.exclude: 118 | kwargs["hatch"] = "/" 119 | 120 | return kwargs 121 | 122 | 123 | def _get_text(txt, x, y, dx, dy, ha="center", va="center", **kwargs): 124 | if "color" in kwargs: 125 | textcolor = kwargs["color"] 126 | del kwargs["color"] 127 | elif "markeredgecolor" in kwargs: 128 | textcolor = kwargs["markeredgecolor"] 129 | else: 130 | import matplotlib as mpl 131 | textcolor = mpl.rcParams['text.color'] 132 | ann = Annotation(txt, (x, y), xytext=(dx, dy), 133 | xycoords='data', 134 | textcoords="offset points", 135 | color=textcolor, 136 | ha=ha, va=va, 137 | **kwargs) 138 | ann.set_transform(IdentityTransform()) 139 | 140 | return ann 141 | 142 | 143 | def as_mpl_artists(shape_list, 144 | properties_func=None, 145 | text_offset=5.0, origin=1): 146 | """ 147 | Converts a region list to a list of patches and a list of artists. 148 | 149 | 150 | Optional Keywords: 151 | [ text_offset ] - If there is text associated with the regions, add 152 | some vertical offset (in pixels) to the text so that it doesn't overlap 153 | with the regions. 154 | 155 | Often, the regions files implicitly assume the lower-left corner 156 | of the image as a coordinate (1,1). However, the python convetion 157 | is that the array index starts from 0. By default (origin = 1), 158 | coordinates of the returned mpl artists have coordinate shifted by 159 | (1, 1). If you do not want this shift, set origin=0. 160 | """ 161 | 162 | patch_list = [] 163 | artist_list = [] 164 | 165 | if properties_func is None: 166 | properties_func = properties_func_default 167 | 168 | # properties for continued(? multiline?) regions 169 | saved_attrs = None 170 | 171 | for shape in shape_list: 172 | 173 | patches = [] 174 | 175 | if saved_attrs is None: 176 | _attrs = [], {} 177 | else: 178 | _attrs = copy.copy(saved_attrs[0]), copy.copy(saved_attrs[1]) 179 | 180 | kwargs = properties_func(shape, _attrs) 181 | 182 | if shape.name == "composite": 183 | saved_attrs = shape.attr 184 | continue 185 | 186 | if saved_attrs is None and shape.continued: 187 | saved_attrs = shape.attr 188 | # elif (shape.name in shape.attr[1]): 189 | # if (shape.attr[1][shape.name] != "ignore"): 190 | # saved_attrs = shape.attr 191 | 192 | if not shape.continued: 193 | saved_attrs = None 194 | 195 | # text associated with the shape 196 | txt = shape.attr[1].get("text") 197 | 198 | if shape.name == "polygon": 199 | xy = np.array(shape.coord_list) 200 | xy.shape = -1, 2 201 | 202 | # -1 for change origin to 0,0 203 | patches = [mpatches.Polygon(xy - origin, closed=True, **kwargs)] 204 | 205 | elif shape.name == "rotbox" or shape.name == "box": 206 | xc, yc, w, h, rot = shape.coord_list 207 | # -1 for change origin to 0,0 208 | xc, yc = xc - origin, yc - origin 209 | _box = np.array([[-w / 2., -h / 2.], 210 | [-w / 2., h / 2.], 211 | [w / 2., h / 2.], 212 | [w / 2., -h / 2.]]) 213 | box = _box + [xc, yc] 214 | rotbox = rotated_polygon(box, xc, yc, rot) 215 | patches = [mpatches.Polygon(rotbox, closed=True, **kwargs)] 216 | 217 | elif shape.name == "ellipse": 218 | xc, yc = shape.coord_list[:2] 219 | # -1 for change origin to 0,0 220 | xc, yc = xc - origin, yc - origin 221 | angle = shape.coord_list[-1] 222 | 223 | maj_list, min_list = shape.coord_list[2:-1:2], shape.coord_list[3:-1:2] 224 | 225 | patches = [mpatches.Ellipse((xc, yc), 2 * maj, 2 * min, 226 | angle=angle, **kwargs) 227 | for maj, min in zip(maj_list, min_list)] 228 | 229 | elif shape.name == "annulus": 230 | xc, yc = shape.coord_list[:2] 231 | # -1 for change origin to 0,0 232 | xc, yc = xc - origin, yc - origin 233 | r_list = shape.coord_list[2:] 234 | 235 | patches = [mpatches.Ellipse((xc, yc), 2 * r, 2 * r, **kwargs) for r in r_list] 236 | 237 | elif shape.name == "circle": 238 | xc, yc, major = shape.coord_list 239 | # -1 for change origin to 0,0 240 | xc, yc = xc - origin, yc - origin 241 | patches = [mpatches.Ellipse((xc, yc), 2 * major, 2 * major, angle=0, **kwargs)] 242 | 243 | elif shape.name == "panda": 244 | xc, yc, a1, a2, an, r1, r2, rn = shape.coord_list 245 | # -1 for change origin to 0,0 246 | xc, yc = xc - origin, yc - origin 247 | patches = [mpatches.Arc((xc, yc), rr * 2, rr * 2, angle=0, 248 | theta1=a1, theta2=a2, **kwargs) 249 | for rr in np.linspace(r1, r2, rn + 1)] 250 | 251 | for aa in np.linspace(a1, a2, an + 1): 252 | xx = np.array([r1, r2]) * np.cos(aa / 180. * np.pi) + xc 253 | yy = np.array([r1, r2]) * np.sin(aa / 180. * np.pi) + yc 254 | p = Path(np.transpose([xx, yy])) 255 | patches.append(mpatches.PathPatch(p, **kwargs)) 256 | 257 | elif shape.name == "pie": 258 | xc, yc, r1, r2, a1, a2 = shape.coord_list 259 | # -1 for change origin to 0,0 260 | xc, yc = xc - origin, yc - origin 261 | 262 | patches = [mpatches.Arc((xc, yc), rr * 2, rr * 2, angle=0, 263 | theta1=a1, theta2=a2, **kwargs) 264 | for rr in [r1, r2]] 265 | 266 | for aa in [a1, a2]: 267 | xx = np.array([r1, r2]) * np.cos(aa / 180. * np.pi) + xc 268 | yy = np.array([r1, r2]) * np.sin(aa / 180. * np.pi) + yc 269 | p = Path(np.transpose([xx, yy])) 270 | patches.append(mpatches.PathPatch(p, **kwargs)) 271 | 272 | elif shape.name == "epanda": 273 | xc, yc, a1, a2, an, r11, r12, r21, r22, rn, angle = shape.coord_list 274 | # -1 for change origin to 0,0 275 | xc, yc = xc - origin, yc - origin 276 | 277 | # mpl takes angle a1, a2 as angle as in circle before 278 | # transformation to ellipse. 279 | 280 | x1, y1 = cos(a1 / 180. * pi), sin(a1 / 180. * pi) * r11 / r12 281 | x2, y2 = cos(a2 / 180. * pi), sin(a2 / 180. * pi) * r11 / r12 282 | 283 | a1, a2 = atan2(y1, x1) / pi * 180., atan2(y2, x2) / pi * 180. 284 | 285 | patches = [mpatches.Arc((xc, yc), rr1 * 2, rr2 * 2, 286 | angle=angle, theta1=a1, theta2=a2, 287 | **kwargs) 288 | for rr1, rr2 in zip(np.linspace(r11, r21, rn + 1), 289 | np.linspace(r12, r22, rn + 1))] 290 | 291 | for aa in np.linspace(a1, a2, an + 1): 292 | xx = np.array([r11, r21]) * np.cos(aa / 180. * np.pi) 293 | yy = np.array([r11, r21]) * np.sin(aa / 180. * np.pi) 294 | p = Path(np.transpose([xx, yy])) 295 | tr = Affine2D().scale(1, r12 / r11).rotate_deg(angle).translate(xc, yc) 296 | p2 = tr.transform_path(p) 297 | patches.append(mpatches.PathPatch(p2, **kwargs)) 298 | 299 | elif shape.name == "text": 300 | xc, yc = shape.coord_list[:2] 301 | # -1 for change origin to 0,0 302 | xc, yc = xc - origin, yc - origin 303 | 304 | if txt: 305 | _t = _get_text(txt, xc, yc, 0, 0, **kwargs) 306 | artist_list.append(_t) 307 | 308 | elif shape.name == "point": 309 | xc, yc = shape.coord_list[:2] 310 | # -1 for change origin to 0,0 311 | xc, yc = xc - origin, yc - origin 312 | artist_list.append(Line2D([xc], [yc], 313 | **kwargs)) 314 | 315 | if txt: 316 | textshape = copy.copy(shape) 317 | textshape.name = "text" 318 | textkwargs = properties_func(textshape, _attrs) 319 | _t = _get_text(txt, xc, yc, 0, text_offset, 320 | va="bottom", 321 | **textkwargs) 322 | artist_list.append(_t) 323 | 324 | elif shape.name in ["line", "vector"]: 325 | if shape.name == "line": 326 | x1, y1, x2, y2 = shape.coord_list[:4] 327 | # -1 for change origin to 0,0 328 | x1, y1, x2, y2 = x1 - origin, y1 - origin, x2 - origin, y2 - origin 329 | 330 | a1, a2 = shape.attr[1].get("line", "0 0").strip().split()[:2] 331 | 332 | arrowstyle = "-" 333 | if int(a1): 334 | arrowstyle = "<" + arrowstyle 335 | if int(a2): 336 | arrowstyle = arrowstyle + ">" 337 | 338 | else: # shape.name == "vector" 339 | x1, y1, l, a = shape.coord_list[:4] 340 | # -1 for change origin to 0,0 341 | x1, y1 = x1 - origin, y1 - origin 342 | x2, y2 = x1 + l * np.cos(a / 180. * np.pi), y1 + l * np.sin(a / 180. * np.pi) 343 | v1 = int(shape.attr[1].get("vector", "0").strip()) 344 | 345 | if v1: 346 | arrowstyle = "->" 347 | else: 348 | arrowstyle = "-" 349 | 350 | patches = [mpatches.FancyArrowPatch(posA=(x1, y1), 351 | posB=(x2, y2), 352 | arrowstyle=arrowstyle, 353 | connectionstyle="arc3", 354 | patchA=None, patchB=None, 355 | shrinkA=0, shrinkB=0, 356 | **kwargs)] 357 | 358 | else: 359 | warnings.warn("'as_mpl_artists' does not know how to convert {0} " 360 | "to mpl artist".format(shape.name)) 361 | 362 | patch_list.extend(patches) 363 | 364 | if txt and patches: 365 | # the text associated with a shape uses different 366 | # matplotlib keywords than the shape itself for, e.g., 367 | # color 368 | textshape = copy.copy(shape) 369 | textshape.name = "text" 370 | textkwargs = properties_func(textshape, _attrs) 371 | 372 | # calculate the text position 373 | _bb = [p.get_window_extent() for p in patches] 374 | 375 | # this is to work around backward-incompatible change made 376 | # in matplotlib 1.2. This change is later reverted so only 377 | # some versions are affected. With affected version of 378 | # matplotlib, get_window_extent method calls get_transform 379 | # method which sets the _transformSet to True, which is 380 | # not desired. 381 | for p in patches: 382 | p._transformSet = False 383 | 384 | _bbox = Bbox.union(_bb) 385 | x0, y0, x1, y1 = _bbox.extents 386 | xc = .5 * (x0 + x1) 387 | 388 | _t = _get_text(txt, xc, y1, 0, text_offset, 389 | va="bottom", 390 | **textkwargs) 391 | artist_list.append(_t) 392 | 393 | return patch_list, artist_list 394 | -------------------------------------------------------------------------------- /pyregion/parser_helper.py: -------------------------------------------------------------------------------- 1 | from pyparsing import (Literal, CaselessKeyword, Optional, OneOrMore, 2 | ZeroOrMore, restOfLine, MatchFirst, And, Or) 3 | 4 | 5 | def as_comma_separated_list(al): 6 | l = [al[0]] 7 | comma = Literal(",").suppress() 8 | 9 | for a1 in al[1:]: 10 | l.append(comma) 11 | l.append(a1) 12 | 13 | return And(l) 14 | 15 | 16 | class wcs_shape(object): 17 | def __init__(self, *kl, **kw): 18 | self.args_list = kl 19 | self.args_repeat = kw.get("repeat", None) 20 | 21 | def get_pyparsing(self): 22 | return [a.parser for a in self.args_list] 23 | 24 | 25 | def define_shape(name, shape_args, args_repeat=None): 26 | lparen = Literal("(").suppress() 27 | rparen = Literal(")").suppress() 28 | comma = Literal(",").suppress() 29 | 30 | shape_name = CaselessKeyword(name) 31 | 32 | if args_repeat is None: 33 | shape_with_parens = And([shape_name, lparen, 34 | as_comma_separated_list(shape_args), 35 | rparen]) 36 | 37 | shape_with_spaces = shape_name + And(shape_args) 38 | 39 | else: 40 | n1, n2 = args_repeat 41 | sl = [] 42 | 43 | ss = shape_args[:n1] 44 | if ss: 45 | sl.append(as_comma_separated_list(ss)) 46 | 47 | ss = shape_args[n1:n2] 48 | if ss: 49 | ar = as_comma_separated_list(ss) 50 | if sl: 51 | sl.extend([comma + ar, ZeroOrMore(comma + ar)]) 52 | else: 53 | sl.extend([ar, ZeroOrMore(comma + ar)]) 54 | 55 | ss = shape_args[n2:] 56 | if ss: 57 | if sl: 58 | sl.extend([comma, as_comma_separated_list(ss)]) 59 | else: 60 | sl.extend([as_comma_separated_list(ss)]) 61 | 62 | sl = [shape_name, lparen] + sl + [rparen] 63 | 64 | shape_with_parens = And(sl) 65 | 66 | shape_with_spaces = shape_name + OneOrMore(And(shape_args)) 67 | 68 | return (shape_with_parens | shape_with_spaces) 69 | 70 | 71 | def define_shape_helper(shape_defs): 72 | l = [] 73 | 74 | for n, args in shape_defs.items(): 75 | s = define_shape(n, 76 | args.get_pyparsing(), 77 | args_repeat=args.args_repeat) 78 | l.append(s) 79 | 80 | return Or(l) 81 | 82 | 83 | def define_expr(regionShape, negate_func): 84 | minus = Literal("-").suppress() 85 | regionExclude = (minus + regionShape).setParseAction(negate_func) 86 | regionExpr = (regionShape | regionExclude) 87 | 88 | return regionExpr 89 | 90 | 91 | def define_line(atom, 92 | separator, 93 | comment): 94 | atomSeparator = separator.suppress() 95 | 96 | atomList = atom + ZeroOrMore(atomSeparator + atom) + \ 97 | Optional(atomSeparator) 98 | 99 | line = (atomList + Optional(comment)) | comment 100 | 101 | return line 102 | 103 | 104 | def comment_shell_like(comment_begin, parseAction=None): 105 | c = comment_begin + restOfLine 106 | if parseAction: 107 | c = c.setParseAction(parseAction) 108 | 109 | return c 110 | 111 | 112 | def define_simple_literals(literal_list, parseAction=None): 113 | l = MatchFirst([CaselessKeyword(k) for k in literal_list]) 114 | 115 | if parseAction: 116 | l = l.setParseAction(parseAction) 117 | 118 | return l 119 | 120 | 121 | class Shape(object): 122 | """Shape. 123 | 124 | Parameters 125 | ---------- 126 | shape_name : str 127 | Shape name 128 | shape_params : list 129 | List of parameters 130 | 131 | Examples 132 | -------- 133 | >>> import pyregion 134 | >>> region_string = 'fk5;circle(290.96388,14.019167,843.31194")' 135 | >>> shape_list = pyregion.parse(region_string) 136 | >>> shape = shape_list[0] 137 | >>> print(shape.__dict__) 138 | {'attr': ([], {}), 139 | 'comment': None, 140 | 'continued': None, 141 | 'coord_format': 'fk5', 142 | 'coord_list': [290.96388, 14.019167, 0.23425331666666666], 143 | 'exclude': False, 144 | 'name': 'circle', 145 | 'params': [Number(290.96388), Number(14.019167), Ang(843.31194")]} 146 | """ 147 | 148 | def __init__(self, shape_name, shape_params): 149 | self.name = shape_name 150 | self.params = shape_params 151 | 152 | self.comment = None 153 | self.exclude = False 154 | self.continued = False 155 | 156 | def __repr__(self): 157 | params_string = ",".join(map(repr, self.params)) 158 | if self.exclude: 159 | return "Shape : -%s ( %s )" % (self.name, params_string) 160 | else: 161 | return "Shape : %s ( %s )" % (self.name, params_string) 162 | 163 | def set_exclude(self): 164 | self.exclude = True 165 | 166 | 167 | class Property(object): 168 | def __init__(self, text): 169 | self.text = text 170 | 171 | def __repr__(self): 172 | return "Property : " + repr(self.text) 173 | 174 | 175 | class CoordCommand(Property): 176 | def __repr__(self): 177 | return "CoordCommand : " + repr(self.text) 178 | 179 | 180 | class Global(Property): 181 | def __repr__(self): 182 | return "Global : " + repr(self.text) 183 | 184 | 185 | class Comment(Property): 186 | def __repr__(self): 187 | return "Comment : " + repr(self.text) 188 | 189 | 190 | class RegionPusher(object): 191 | def __init__(self): 192 | self.flush() 193 | 194 | def flush(self): 195 | self.stack = [] 196 | self.comment = None 197 | self.continued = None 198 | 199 | def pushAtom(self, s, l, tok): 200 | self.stack.append(tok[-1]) 201 | 202 | def pushComment(self, s, l, tok): 203 | self.comment = tok[-1].strip() 204 | 205 | def set_continued(self, s, l, tok): 206 | self.continued = True 207 | -------------------------------------------------------------------------------- /pyregion/physical_coordinate.py: -------------------------------------------------------------------------------- 1 | class PhysicalCoordinate(object): 2 | def __init__(self, header): 3 | phys_coord = "" 4 | 5 | # check if physical coordinate is defined. FIXME! 6 | for C in ["P", "L"]: 7 | try: 8 | if (header["WCSTY1" + C].strip() == "PHYSICAL") \ 9 | and (header["WCSTY2" + C].strip() == "PHYSICAL"): 10 | phys_coord = C 11 | except KeyError: 12 | pass 13 | 14 | try: 15 | if (header["CTYPE1" + C].strip() == "X") \ 16 | and (header["CTYPE2" + C].strip() == "Y"): 17 | phys_coord = C 18 | except KeyError: 19 | pass 20 | 21 | if phys_coord: 22 | C = phys_coord 23 | cv1, cr1, cd1 = header["CRVAL1" + C], header["CRPIX1" + C], header[" CDELT1" + C] 24 | cv2, cr2, cd2 = header["CRVAL2" + C], header["CRPIX2" + C], header[" CDELT2" + C] 25 | 26 | self._physical_coord_not_defined = False 27 | 28 | self.cv1_cr1_cd1 = cv1, cr1, cd1 29 | self.cv2_cr2_cd2 = cv2, cr2, cd2 30 | self.cdelt = (cd1 * cd2) ** .5 31 | 32 | else: 33 | self._physical_coord_not_defined = True 34 | self.cv1_cr1_cd1 = 0, 0, 1 35 | self.cv2_cr2_cd2 = 0, 0, 1 36 | self.cdelt = 1 37 | 38 | def to_physical(self, imx, imy): 39 | 40 | if self._physical_coord_not_defined: 41 | return imx, imy 42 | 43 | cv1, cr1, cd1 = self.cv1_cr1_cd1 44 | cv2, cr2, cd2 = self.cv2_cr2_cd2 45 | 46 | phyx = cv1 + (imx - cr1) * cd1 47 | phyy = cv2 + (imy - cr2) * cd2 48 | 49 | return phyx, phyy 50 | 51 | def to_image(self, phyx, phyy): 52 | 53 | if self._physical_coord_not_defined: 54 | return phyx, phyy 55 | 56 | cv1, cr1, cd1 = self.cv1_cr1_cd1 57 | cv2, cr2, cd2 = self.cv2_cr2_cd2 58 | 59 | imx = cr1 + (phyx - cv1) / cd1 60 | imy = cr2 + (phyy - cv2) / cd2 61 | 62 | return imx, imy 63 | 64 | def to_physical_distance(self, im_distance): 65 | 66 | if self._physical_coord_not_defined: 67 | return im_distance 68 | 69 | return im_distance * self.cdelt 70 | 71 | def to_image_distance(self, im_physical): 72 | 73 | if self._physical_coord_not_defined: 74 | return im_physical 75 | 76 | return im_physical / self.cdelt 77 | -------------------------------------------------------------------------------- /pyregion/region_numbers.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | from pyparsing import Literal, Optional, Combine, Or, Word, nums 3 | 4 | 5 | def _unsigned_simple_number(): 6 | # fnumber : 102.43, 12304.3e10, 7 | # .32??? 8 | point = Literal(".") 9 | e = Literal("e") | Literal("E") # CaselessLiteral( "E" ) 10 | fnumber = Combine(Word(nums) + 11 | Optional(point + Optional(Word(nums))) + 12 | Optional(e + Word("+-" + nums, nums))) 13 | 14 | return fnumber # .setParseAction(lambda s,l,t: (float(t[0]), t[0])) 15 | 16 | 17 | usn = _unsigned_simple_number() 18 | 19 | 20 | def get_default(v): 21 | def _f(s, l, tok): 22 | if tok: 23 | return tok 24 | return v 25 | 26 | return _f 27 | 28 | 29 | optional_sign = Optional(Literal("+") | Literal("-")).setParseAction(get_default("")) 30 | 31 | 32 | class SimpleNumber(object): 33 | def __repr__(self): 34 | return "Number(%s)" % (self.text,) 35 | 36 | def __str__(self): 37 | return self.__repr__() 38 | 39 | def __init__(self, text): 40 | self.text = text 41 | self.v = float(text) 42 | 43 | 44 | def _simple_number(): 45 | s = Combine(optional_sign + usn) 46 | s.setParseAction(lambda s, l, tok: SimpleNumber(tok[0])) 47 | 48 | return s 49 | 50 | 51 | simple_number = _simple_number() 52 | 53 | 54 | class SimpleInteger(object): 55 | def __repr__(self): 56 | return "Number(%s)" % (self.text,) 57 | 58 | def __str__(self): 59 | return self.__repr__() 60 | 61 | def __init__(self, text): 62 | self.text = text 63 | self.v = int(text) 64 | 65 | 66 | def _unsigned_integer(): 67 | s = Combine(Optional("+") + Word(nums)) 68 | s.setParseAction(lambda s, l, tok: SimpleInteger(tok[0])) 69 | 70 | return s 71 | 72 | 73 | simple_integer = _unsigned_integer() 74 | 75 | 76 | class Sixty(object): 77 | def __init__(self, sn, d, m, s): 78 | self.v = sn * (d + (m + s / 60.) / 60.) 79 | self.degree = self.v 80 | 81 | 82 | class HMS(object): 83 | def __repr__(self): 84 | return "HMS(%s)" % (self.text,) 85 | 86 | def __init__(self, kl): 87 | self.text = "".join(kl) 88 | 89 | if kl[0] == "-": 90 | sn = -1 91 | else: 92 | sn = +1 93 | 94 | kkl = kl[1::2] 95 | if len(kkl) == 3: 96 | d, m, s = float(kl[1]), float(kl[3]), float(kl[5]) 97 | elif len(kkl) == 2: 98 | d, m, s = float(kl[1]), float(kl[3]), 0. 99 | else: 100 | d, m, s = float(kl[1]), 0., 0. 101 | 102 | self.v = sn * (d + (m + s / 60.) / 60.) 103 | self.degree = self.v * 15 104 | 105 | 106 | class DMS(object): 107 | def __repr__(self): 108 | return "DMS(%s)" % (self.text,) 109 | 110 | def __init__(self, kl): 111 | self.text = "".join(kl) 112 | 113 | if kl[0] == "-": 114 | sn = -1 115 | else: 116 | sn = +1 117 | 118 | kkl = kl[1::2] 119 | if len(kkl) == 3: 120 | d, m, s = float(kl[1]), float(kl[3]), float(kl[5]) 121 | elif len(kkl) == 2: 122 | d, m, s = float(kl[1]), float(kl[3]), 0. 123 | else: 124 | d, m, s = float(kl[1]), 0., 0. 125 | 126 | self.v = sn * (d + (m + s / 60.) / 60.) 127 | self.degree = self.v 128 | 129 | 130 | class AngularDistance(object): 131 | def __repr__(self): 132 | return "Ang(%s)" % (self.text,) 133 | 134 | def __init__(self, kl): 135 | self.text = "".join(kl) 136 | 137 | d, m, s = 0, 0, 0 138 | if kl[1] == "d": # format of "3.5d" 139 | d = float(kl[0]) 140 | elif kl[1] == "r": # format of "3.5r" 141 | d = float(kl[0]) / pi * 180. 142 | else: # 3'5" or 3' 143 | if kl[1] == "'": 144 | m = float(kl[0]) 145 | if len(kl) == 4: 146 | s = float(kl[2]) 147 | else: # should be a format of 5" 148 | s = float(kl[0]) 149 | 150 | self.v = d + (m + s / 60.) / 60. 151 | self.degree = self.v 152 | 153 | 154 | def _sexadecimal(): 155 | colon = Literal(":") 156 | 157 | s = optional_sign + usn + colon + usn + \ 158 | Optional(colon + usn) 159 | 160 | return s 161 | 162 | 163 | sexadecimal60 = _sexadecimal().setParseAction(lambda s, l, tok: DMS(tok)) 164 | sexadecimal24 = _sexadecimal().setParseAction(lambda s, l, tok: HMS(tok)) 165 | 166 | 167 | def _hms_number(): 168 | _h = (usn + Literal("h")).leaveWhitespace() 169 | _m = (usn + Literal("m")).leaveWhitespace() 170 | _s = (usn + Literal("s")).leaveWhitespace() 171 | 172 | hms = optional_sign + _h + Optional(_m + Optional(_s)) 173 | 174 | hms = hms.setParseAction(lambda s, l, tok: HMS(tok)) 175 | 176 | return hms 177 | 178 | 179 | def _dms_number(): 180 | _d = (usn + Literal("d")).leaveWhitespace() 181 | _m = (usn + Literal("m")).leaveWhitespace() 182 | _s = (usn + Literal("s")).leaveWhitespace() 183 | 184 | dms = optional_sign + _d + Optional(_m + Optional(_s)) 185 | 186 | dms = dms.setParseAction(lambda s, l, tok: DMS(tok)) 187 | 188 | return dms 189 | 190 | 191 | hms_number = _hms_number() 192 | dms_number = _dms_number() 193 | 194 | 195 | def _angular_distance(): 196 | _m = (usn + Literal("\'").leaveWhitespace()) 197 | _s = (usn + Literal("\"").leaveWhitespace()) 198 | _dr = (usn + Or([Literal("d"), 199 | Literal("r")]).leaveWhitespace()) 200 | 201 | ms = Or([_m + Optional(_s), _s, _dr]) 202 | 203 | ms = ms.setParseAction(lambda s, l, tok: AngularDistance(tok)) 204 | 205 | return ms 206 | 207 | 208 | angular_distance = _angular_distance() 209 | 210 | 211 | class Arg(object): 212 | def __init__(self, type, parser): 213 | self.type = type 214 | self.parser = parser 215 | 216 | 217 | class CoordOdd: 218 | parser = (hms_number | sexadecimal24 | simple_number) 219 | type = HMS 220 | 221 | 222 | class CoordEven: 223 | parser = (dms_number | sexadecimal60 | simple_number) 224 | type = DMS 225 | 226 | 227 | class Distance: 228 | parser = (angular_distance | simple_number) 229 | type = AngularDistance 230 | 231 | 232 | class Angle: 233 | parser = (simple_number) 234 | type = simple_number 235 | 236 | 237 | class Integer: 238 | parser = simple_integer 239 | type = simple_number 240 | -------------------------------------------------------------------------------- /pyregion/region_to_filter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pyregion._region_filter as region_filter 3 | import warnings 4 | 5 | 6 | def as_region_filter(shape_list, origin=1): 7 | """ 8 | Often, the regions files implicitly assume the lower-left corner 9 | of the image as a coordinate (1,1). However, the python convetion 10 | is that the array index starts from 0. By default (origin = 1), 11 | coordinates of the returned mpl artists have coordinate shifted by 12 | (1, 1). If you do not want this shift, use origin=0. 13 | """ 14 | 15 | filter_list = [] 16 | for shape in shape_list: 17 | 18 | if shape.name == "composite": 19 | continue 20 | 21 | if shape.name == "polygon": 22 | xy = np.array(shape.coord_list) - origin 23 | f = region_filter.Polygon(xy[::2], xy[1::2]) 24 | 25 | elif shape.name == "rotbox" or shape.name == "box": 26 | xc, yc, w, h, rot = shape.coord_list 27 | # -1 for change origin to 0,0 28 | xc, yc = xc - origin, yc - origin 29 | 30 | f = region_filter.Rotated(region_filter.Box(xc, yc, w, h), 31 | rot, xc, yc) 32 | 33 | elif shape.name == "ellipse": 34 | xc, yc = shape.coord_list[:2] 35 | # -1 for change origin to 0,0 36 | xc, yc = xc - origin, yc - origin 37 | angle = shape.coord_list[-1] 38 | 39 | maj_list, min_list = shape.coord_list[2:-1:2], shape.coord_list[3:-1:2] 40 | 41 | if len(maj_list) > 1: 42 | w1, h1 = max(maj_list), max(min_list) 43 | w2, h2 = min(maj_list), min(min_list) 44 | 45 | f1 = region_filter.Ellipse(xc, yc, w1, h1) \ 46 | & ~region_filter.Ellipse(xc, yc, w2, h2) 47 | f = region_filter.Rotated(f1, angle, xc, yc) 48 | else: 49 | w, h = maj_list[0], min_list[0] 50 | f = region_filter.Rotated(region_filter.Ellipse(xc, yc, w, h), 51 | angle, xc, yc) 52 | 53 | elif shape.name == "annulus": 54 | xc, yc = shape.coord_list[:2] 55 | # -1 for change origin to 0,0 56 | xc, yc = xc - origin, yc - origin 57 | r_list = shape.coord_list[2:] 58 | 59 | r1 = max(r_list) 60 | r2 = min(r_list) 61 | 62 | f = region_filter.Circle(xc, yc, r1) & ~region_filter.Circle(xc, yc, r2) 63 | 64 | elif shape.name == "circle": 65 | xc, yc, r = shape.coord_list 66 | # -1 for change origin to 0,0 67 | xc, yc = xc - origin, yc - origin 68 | 69 | f = region_filter.Circle(xc, yc, r) 70 | 71 | elif shape.name == "panda": 72 | xc, yc, a1, a2, an, r1, r2, rn = shape.coord_list 73 | # -1 for change origin to 0,0 74 | xc, yc = xc - origin, yc - origin 75 | 76 | f1 = region_filter.Circle(xc, yc, r2) & ~region_filter.Circle(xc, yc, r1) 77 | f = f1 & region_filter.AngleRange(xc, yc, a1, a2) 78 | 79 | elif shape.name == "pie": 80 | xc, yc, r1, r2, a1, a2 = shape.coord_list 81 | # -1 for change origin to 0,0 82 | xc, yc = xc - origin, yc - origin 83 | 84 | f1 = region_filter.Circle(xc, yc, r2) & ~region_filter.Circle(xc, yc, r1) 85 | f = f1 & region_filter.AngleRange(xc, yc, a1, a2) 86 | 87 | elif shape.name == "epanda": 88 | xc, yc, a1, a2, an, r11, r12, r21, r22, rn, angle = shape.coord_list 89 | # -1 for change origin to 0,0 90 | xc, yc = xc - origin, yc - origin 91 | 92 | f1 = region_filter.Ellipse(xc, yc, r21, r22) & ~region_filter.Ellipse(xc, yc, r11, r12) 93 | f2 = f1 & region_filter.AngleRange(xc, yc, a1, a2) 94 | f = region_filter.Rotated(f2, angle, xc, yc) 95 | # f = f2 & region_filter.AngleRange(xc, yc, a1, a2) 96 | 97 | elif shape.name == "bpanda": 98 | xc, yc, a1, a2, an, r11, r12, r21, r22, rn, angle = shape.coord_list 99 | # -1 for change origin to 0,0 100 | xc, yc = xc - origin, yc - origin 101 | 102 | f1 = region_filter.Box(xc, yc, r21, r22) & ~region_filter.Box(xc, yc, r11, r12) 103 | f2 = f1 & region_filter.AngleRange(xc, yc, a1, a2) 104 | f = region_filter.Rotated(f2, angle, xc, yc) 105 | # f = f2 & region_filter.AngleRange(xc, yc, a1, a2) 106 | 107 | else: 108 | warnings.warn("'as_region_filter' does not know how to convert {0}" 109 | " to a region filter.".format(shape.name)) 110 | continue 111 | 112 | if shape.exclude: 113 | filter_list = [region_filter.RegionOrList(*filter_list) & ~f] 114 | else: 115 | filter_list.append(f) 116 | 117 | return region_filter.RegionOrList(*filter_list) 118 | -------------------------------------------------------------------------------- /pyregion/setup_package.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from numpy import get_include as get_numpy_include 4 | from setuptools import Extension 5 | from Cython.Build import cythonize 6 | 7 | ROOT = Path(__file__).parent.resolve().relative_to(Path.cwd()) 8 | 9 | 10 | def get_extensions(): 11 | _region_filter = Extension( 12 | name="pyregion._region_filter", 13 | # define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")], 14 | sources=[str(ROOT / "_region_filter.pyx")], 15 | include_dirs=[get_numpy_include()], 16 | ) 17 | 18 | return cythonize([_region_filter], language_level=3, include_path=["pyregion"]) 19 | -------------------------------------------------------------------------------- /pyregion/tests/coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = {packagename} 3 | omit = 4 | {packagename}/_astropy_init* 5 | {packagename}/conftest* 6 | {packagename}/cython_version* 7 | {packagename}/setup_package* 8 | {packagename}/*/setup_package* 9 | {packagename}/*/*/setup_package* 10 | {packagename}/tests/* 11 | {packagename}/*/tests/* 12 | {packagename}/*/*/tests/* 13 | {packagename}/version* 14 | {packagename}/extern/* 15 | 16 | [report] 17 | exclude_lines = 18 | # Have to re-enable the standard pragma 19 | pragma: no cover 20 | 21 | # Don't complain about packages we have installed 22 | except ImportError 23 | 24 | # Don't complain if tests don't hit assertions 25 | raise AssertionError 26 | raise NotImplementedError 27 | 28 | # Don't complain about script hooks 29 | def main\(.*\): 30 | 31 | # Ignore branches that don't pertain to this version of Python 32 | pragma: py{ignore_python_version} -------------------------------------------------------------------------------- /pyregion/tests/data/sample_fits01.header: -------------------------------------------------------------------------------- 1 | SIMPLE = T / file does conform to FITS standard 2 | BITPIX = 16 / number of bits per data pixel 3 | NAXIS = 2 / number of data axes 4 | NAXIS1 = 1629 / length of data axis 5 | NAXIS2 = 1653 / length of data axis 6 | EXTEND = T / FITS dataset may contain extensions 7 | EQUINOX = 2.0000000000000E+03 / default 8 | CTYPE1 = 'RA---TAN' 9 | CRVAL1 = 1.7114680010248E+02 10 | CRPIX1 = 7.9250000000000E+02 11 | CDELT1 = -1.3666666666667E-04 12 | CUNIT1 = 'deg ' 13 | CTYPE2 = 'DEC--TAN' 14 | CRVAL2 = -5.9266678641361E+01 15 | CRPIX2 = 8.2750000000000E+02 16 | CDELT2 = 1.3666666666667E-04 17 | CUNIT2 = 'deg ' 18 | CTYPE1P = 'X ' / sky coordinates 19 | CRVAL1P = 3.3045000000000E+03 20 | CRPIX1P = 5.0000000000000E-01 21 | CDELT1P = 1.0000000000000E+00 22 | WCSTY1P = 'PHYSICAL' 23 | LTV1 = -3.3040000000000E+03 24 | LTM1_1 = 1.0000000000000E+00 25 | CTYPE2P = 'Y ' / sky coordinates 26 | CRVAL2P = 3.2695000000000E+03 27 | CRPIX2P = 5.0000000000000E-01 28 | CDELT2P = 1.0000000000000E+00 29 | WCSTY2P = 'PHYSICAL' 30 | LTV2 = -3.2690000000000E+03 31 | LTM2_2 = 1.0000000000000E+00 32 | -------------------------------------------------------------------------------- /pyregion/tests/data/sample_fits02.header: -------------------------------------------------------------------------------- 1 | SIMPLE = T / Written by SkyView Thu Mar 19 00:15:05 GMT 2009 2 | BITPIX = -64 / 4 byte floating point 3 | NAXIS = 2 / Two dimensional image 4 | NAXIS1 = 300 / Width of image 5 | NAXIS2 = 300 / Height of image 6 | CRVAL1 = 304.75 / Reference longitude 7 | CRVAL2 = 45.7 / Reference latitude 8 | RADESYS = 'FK5 ' / Coordinate system 9 | EQUINOX = 2000.0 / Epoch of the equinox 10 | CTYPE1 = 'RA---TAN' / Coordinates -- projection 11 | CTYPE2 = 'DEC--TAN' / Coordinates -- projection 12 | CRPIX1 = 150.5 / X reference pixel 13 | CRPIX2 = 150.5 / Y reference pixel 14 | CDELT1 = -0.006666666666667 / X scale 15 | CDELT2 = 0.006666666666666666 / Y scale 16 | COMMENT 17 | COMMENT SkyView Survey metadata 18 | COMMENT 19 | COMMENT Provenance: Observational data from NASA Goddard Space Flight C 20 | COMMENT enter, mosaicking of images done by SkyView. 21 | COMMENT Copyright: Public domain 22 | COMMENT Regime: X-ray 23 | COMMENT NSurvey: 1 24 | COMMENT Frequency: 0.3 EHz (.1-2.4 keV) 25 | COMMENT Coverage: Isolated pointings in the sky. Total coverage < 14 26 | COMMENT % 27 | COMMENT PixelScale: 15" 28 | COMMENT PixelUnits: cts/s/pixel 29 | COMMENT Resolution: 30" but variable across the field of view 30 | COMMENT Coordinates: Equatorial 31 | COMMENT Equinox: 2000 32 | COMMENT Projection: Gnomonic 33 | COMMENT Epoch: 1991-1994 34 | COMMENT Reference: ROSAT Mission Description and Data Products Guide, availa 35 | COMMENT ble thr ough the ROSAT Guest Observer Facility, NASA GSFC. 36 | COMMENT SkyView Rosat Survey Generation description. 37 | COMMENT 38 | COMMENT Survey specific cards 39 | COMMENT 40 | SURVEY = 'PSPC 2.0 Deg-Inten' 41 | -------------------------------------------------------------------------------- /pyregion/tests/data/sample_fits03.header: -------------------------------------------------------------------------------- 1 | SIMPLE = T / conforms to FITS standard 2 | BITPIX = 64 / array data type 3 | NAXIS = 2 / number of array dimensions 4 | NAXIS1 = 4096 5 | NAXIS2 = 2048 6 | WCSAXES = 2 / Number of coordinate axes 7 | CRPIX1 = -234.75 / Pixel coordinate of reference point 8 | CRPIX2 = 8.3393 / Pixel coordinate of reference point 9 | CDELT1 = -0.066667 / [deg] Coordinate increment at reference point 10 | CDELT2 = 0.026667 / [deg] Coordinate increment at reference point 11 | CUNIT1 = 'deg' / Units of coordinate increment and value 12 | CUNIT2 = 'deg' / Units of coordinate increment and value 13 | CTYPE1 = 'RA---TAN' / Right ascension, gnomonic projection 14 | CTYPE2 = 'DEC--TAN' / Declination, gnomonic projection 15 | CRVAL1 = 1.0 / [deg] Coordinate value at reference point 16 | CRVAL2 = 2.0 / [deg] Coordinate value at reference point 17 | LONPOLE = 180.0 / [deg] Native longitude of celestial pole 18 | LATPOLE = 2.0 / [deg] Native latitude of celestial pole 19 | RADESYS = 'ICRS' / Equatorial coordinate system -------------------------------------------------------------------------------- /pyregion/tests/data/sample_fits04.header: -------------------------------------------------------------------------------- 1 | SIMPLE = T / 2 | BITPIX = -32 / 3 | NAXIS = 3 / 4 | NAXIS1 = 256 / 5 | NAXIS2 = 256 / 6 | NAXIS3 = 63 / 7 | EQUINOX = 2.000000000E+03 /Epoch of RA DEC 8 | CTYPE1 = 'RA---SIN' / 9 | CRVAL1 = 1.81046666667E+02 / 10 | CDELT1 = -1.388888923E-03 / 11 | CRPIX1 = 1.280000000E+02 / 12 | CROTA1 = 0.000000000E+00 / 13 | CTYPE2 = 'DEC--SIN' / 14 | CRVAL2 = 1.84433333333E+01 / 15 | CDELT2 = 1.388888923E-03 / 16 | CRPIX2 = 1.290000000E+02 / 17 | CROTA2 = 0.000000000E+00 / 18 | CTYPE3 = 'FREQ ' / 19 | CRVAL3 = 1.41604138193E+09 / 20 | CDELT3 = 4.882812500E+04 / 21 | CRPIX3 = 3.200000000E+01 / 22 | CROTA3 = 0.000000000E+00 / 23 | -------------------------------------------------------------------------------- /pyregion/tests/data/test.header: -------------------------------------------------------------------------------- 1 | SIMPLE = T / file does conform to FITS standard 2 | BITPIX = 16 / number of bits per data pixel 3 | NAXIS = 2 / number of data axes 4 | NAXIS1 = 1629 / length of data axis 5 | NAXIS2 = 1653 / length of data axis 6 | EXTEND = T / FITS dataset may contain extensions 7 | CTYPE1 = 'RA---TAN' 8 | CRVAL1 = 1.7114680010248E+02 9 | CRPIX1 = 7.9250000000000E+02 10 | CDELT1 = -1.3666666666667E-04 11 | CUNIT1 = 'deg ' 12 | CTYPE2 = 'DEC--TAN' 13 | CRVAL2 = -5.9266678641361E+01 14 | CRPIX2 = 8.2750000000000E+02 15 | CDELT2 = 1.3666666666667E-04 16 | CUNIT2 = 'deg ' -------------------------------------------------------------------------------- /pyregion/tests/data/test01.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | ## -ellipse(171.15816,-59.263193,22.632",10.332",317.01716) # width=3 background 6 | circle(171.10096,-59.250612,18.510811") # color=cyan 7 | box(171.16339,-59.281643,42.804",23.616",19.038396) # width=4 8 | polygon(171.1239,-59.26881,171.09051,-59.262088,171.0985,-59.285735,171.1239,-59.27698) 9 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_ciao.reg: -------------------------------------------------------------------------------- 1 | # Region file format: CIAO version 1.0 2 | -ellipse(11:24:37.960,-59:15:47.50,0.3772',0.1722',317.017) 3 | circle(11:24:24.230,-59:15:02.20,0.308514') 4 | rotbox(11:24:39.213,-59:16:53.91,0.7134',0.3936',19.0384) 5 | polygon(11:24:29.737,-59:16:07.72,11:24:21.723,-59:15:43.52,11:24:23.641,-59:17:08.64,11:24:29.736,-59:16:37.13) 6 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_ciao_physical.reg: -------------------------------------------------------------------------------- 1 | # Region file format: CIAO version 1.0 2 | -ellipse(4053.9922,4121.9905,46,21,317.017) 3 | circle(4267.9987,4214.0083,37.623659) 4 | rotbox(4034.5013,3987.0067,87,48,19.0384) 5 | polygon(4182.1103,4080.8819,4307.0067,4129.9947,4276.9938,3957.01,4182.1053,4021.1054) 6 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_ds9_physical.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: t1.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | physical 5 | -ellipse(4053.9922,4121.9905,46,21,317.017) 6 | circle(4267.9987,4214.0083,37.623659) 7 | box(4034.5013,3987.0067,87,48,19.0384) 8 | polygon(4182.1103,4080.8819,4307.0067,4129.9947,4276.9938,3957.01,4182.1053,4021.1054) 9 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_fk4.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 3 | fk4 4 | -ellipse(170.59136,-58.988299,22.632",10.332",316.93614) # width=3 font="helvetica 10 normal" background 5 | circle(170.53458,-58.975762,18.510811") # color=cyan font="helvetica 10 normal" 6 | box(170.5966,-59.006744,42.804",23.616",18.957374) # width=4 font="helvetica 10 normal" 7 | polygon(170.55739,-58.993942,170.52425,-58.987246,170.53224,-59.010886,170.55741,-59.002112) # font="helvetica 10 normal" 8 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_fk5.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | -ellipse(171.15816,-59.263193,22.632",10.332",317.01716) # width=3 background 6 | circle(171.10096,-59.250612,18.510811") # color=cyan 7 | box(171.16339,-59.281643,42.804",23.616",19.038396) # width=4 8 | polygon(171.1239,-59.26881,171.09051,-59.262088,171.0985,-59.285735,171.1239,-59.27698) 9 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_fk5_degree.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | -ellipse(171.15816,-59.263193,22.632",10.332",317.01716) # width=3 background 6 | circle(171.10096,-59.250612,18.510811") # color=cyan 7 | box(171.16339,-59.281643,42.804",23.616",19.038396) # width=4 8 | polygon(171.1239,-59.26881,171.09051,-59.262088,171.0985,-59.285735,171.1239,-59.27698) 9 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_fk5_sexagecimal.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | -ellipse(11:24:37.960,-59:15:47.50,22.632",10.332",317.017) # width=3 background 6 | circle(11:24:24.230,-59:15:02.20,18.5108") # color=cyan 7 | box(11:24:39.213,-59:16:53.91,42.804",23.616",19.0384) # width=4 8 | polygon(11:24:29.737,-59:16:07.72,11:24:21.723,-59:15:43.52,11:24:23.641,-59:17:08.64,11:24:29.736,-59:16:37.13) 9 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_gal.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | galactic 5 | -ellipse(+292:01:59.027,+01:45:33.389,22.632",10.332",297.784) # width=3 background 6 | circle(+292:00:04.651,+01:45:41.444,18.5108") # color=cyan 7 | box(+292:02:29.979,+01:44:33.837,42.804",23.616",359.806) # width=4 8 | polygon(+292:01:06.152,+01:44:53.528,+292:00:00.146,+01:44:56.102,+292:00:42.141,+01:43:40.599,+292:01:15.844,+01:44:25.760) 9 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_icrs.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 3 | icrs 4 | -ellipse(11:24:37.956,-59:15:47.48,22.632",10.332",317.022) # width=3 font="helvetica 10 normal" background 5 | circle(11:24:24.226,-59:15:02.19,18.5108") # color=cyan font="helvetica 10 normal" 6 | box(11:24:39.210,-59:16:53.90,42.804",23.616",19.0435) # width=4 font="helvetica 10 normal" 7 | polygon(11:24:29.733,-59:16:07.71,11:24:21.720,-59:15:43.51,11:24:23.637,-59:17:08.63,11:24:29.732,-59:16:37.11) # font="helvetica 10 normal" 8 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_img.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | image 5 | -ellipse(750,853,46,21,317.01716) # width=3 background 6 | circle(964,945,37.6236) # color=cyan 7 | box(730.5,718,87,48,19.038396) # width=4 8 | polygon(878.11234,811.88766,1003,861,973,688,878.11234,752.11234) 9 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_mixed.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | -ellipse(171.15816,-59.263193,22.632",10.332",317.01716) # width=3 background 6 | galactic 7 | circle(+292:00:04.651,+01:45:41.444,18.5108") # color=cyan 8 | image 9 | box(730.5,718,87,48,19.038396) # width=4 10 | galactic 11 | polygon(+292:01:06.152,+01:44:53.528,+292:00:00.146,+01:44:56.102,+292:00:42.141,+01:43:40.599,+292:01:15.844,+01:44:25.760) 12 | -------------------------------------------------------------------------------- /pyregion/tests/data/test01_print.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | circle(11:24:24.230,-59:15:02.20,18.5108") # color=cyan background 6 | box(11:24:39.213,-59:16:53.91,42.804",23.616",19.0384) # width=4 7 | -------------------------------------------------------------------------------- /pyregion/tests/data/test02.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: pspc_skyview.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | physical 5 | ellipse(82,167,36,75,346.95915) 6 | epanda(140,166,87.19363,195.80251,1,32.5,39.5,65,79,1,307.90041) 7 | -polygon(78.265142,201.73486,132,209,125,178,163.73486,116.26514,78.265142,116.26514) 8 | -------------------------------------------------------------------------------- /pyregion/tests/data/test02_1_fk5.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: pspc_skyview.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | circle(305.66137,46.273027,286.45302") 6 | ellipse(305.2084,46.309061,240",600",15.433424) # color=black width=4 7 | box(304.61491,46.299899,1032",552",28.9055) # color=white tag={Group 1} 8 | polygon(304.30761,46.142612,303.94162,46.140616,304.12265,46.314927,303.72947,46.178781,303.9453,45.887034,304.10869,45.854872,304.30963,45.889013) # color=red 9 | line(305.70423,45.962694,305.10953,45.946101) # line=0 0 dash=1 10 | # vector(305.47681,45.437697,1448.972",63.434949) vector=1 11 | # text(304.75479,45.939998) text={Text} 12 | annulus(304.01194,45.570957,216",506.2428",674.9904") # color=yellow 13 | ellipse(304.7357,45.626666,349.44527",196.03028",797.79697",447.54464",339.24891) # width=2 tag={Group 1} 14 | panda(305.48266,45.157674,0,151.26,2,398.8488",797.6976",1) # color=blue width=2 15 | epanda(304.78308,45.140013,0,88.057145,1,193.49419",257.83216",504.22594",671.88407",1,8.11303) # epanda=(0 88.057145 228.1969 327.92448)(193.49419" 257.83216" 504.22594" 671.88407" 672.30125" 895.84543")(8.11303) color=cyan 16 | epanda(304.78308,45.140013,0,88.057145,1,504.22594",671.88407",672.30125",895.84543",1,8.11303) # epanda=ignore 17 | epanda(304.78308,45.140013,88.057145,228.1969,1,193.49419",257.83216",504.22594",671.88407",1,8.11303) # epanda=ignore 18 | epanda(304.78308,45.140013,88.057145,228.1969,1,504.22594",671.88407",672.30125",895.84543",1,8.11303) # epanda=ignore 19 | epanda(304.78308,45.140013,228.1969,327.92448,1,193.49419",257.83216",504.22594",671.88407",1,8.11303) # epanda=ignore 20 | epanda(304.78308,45.140013,228.1969,327.92448,1,504.22594",671.88407",672.30125",895.84543",1,8.11303) # epanda=ignore 21 | point(304.26232,45.252305) # point=circle 22 | point(304.0256,45.251053) # point=box color=magenta 23 | point(303.79815,45.262722) # point=diamond 24 | point(304.25414,45.105615) # point=cross 25 | point(304.04637,45.104528) # point=x 26 | point(304.25577,44.918982) # point=arrow 27 | point(304.02028,44.931056) # point=boxcircle 28 | bpanda(305.53095,44.934745,0,290,1,605.36378",294.48644",1371.1215",666.9984",1,342.545) # color=blue width=2 29 | -------------------------------------------------------------------------------- /pyregion/tests/data/test02_1_img.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 3 | image 4 | circle(55.999545,237.00008,11.935542) # font="helvetica 10 normal" 5 | ellipse(102.99988,241.99999,10,25,15.430008) # color=black width=4 font="helvetica 10 normal" 6 | box(164.50049,240.50007,43,23,28.902084) # color=white font="helvetica 10 normal" tag={Group 1} 7 | polygon(196.47911,217.02112,234.52161,217.0211,215.50036,242.99994,256.50007,222.99997,234.52148,178.97883,217.49965,173.99993,196.47842,178.97892) # color=red font="helvetica 10 normal" 8 | line(51.000257,190.50004,113.00049,187.49993) # line=0 0 font="helvetica 10 normal" dash=1 9 | # vector(73.999544,111.49999,60.373833,63.431533) vector=1 font="helvetica 10 normal" 10 | # text(150.00034,186.49993) font="helvetica 10 normal" text={Text} 11 | annulus(228.00031,131.49998,9,21.09345,28.1246) # color=yellow font="helvetica 10 normal" 12 | ellipse(152.00007,139.50003,14.56022,8.1679283,33.24154,18.647693,339.24549) # width=2 font="helvetica 10 normal" tag={Group 1} 13 | panda(72.999339,69.500035,359.99658,511.25658,2,16.6187,33.2374,1) # color=blue width=2 font="helvetica 10 normal" 14 | epanda(146.99975,66.499992,359.99658,448.05373,1,8.0622579,10.743007,21.009414,27.99517,1,8.1096144) # epanda=(359.99658 88.053729 228.19348 327.92106)(8.0622579 10.743007 21.009414 27.99517 28.012552 37.326893)(8.1096144) color=cyan font="helvetica 10 normal" 15 | epanda(146.99975,66.499992,359.99658,448.05373,1,21.009414,27.99517,28.012552,37.326893,1,8.1096144) # epanda=ignore 16 | epanda(146.99975,66.499992,88.053729,228.19348,1,8.0622579,10.743007,21.009414,27.99517,1,8.1096144) # epanda=ignore 17 | epanda(146.99975,66.499992,88.053729,228.19348,1,21.009414,27.99517,28.012552,37.326893,1,8.1096144) # epanda=ignore 18 | epanda(146.99975,66.499992,228.19348,327.92106,1,8.0622579,10.743007,21.009414,27.99517,1,8.1096144) # epanda=ignore 19 | epanda(146.99975,66.499992,228.19348,327.92106,1,21.009414,27.99517,28.012552,37.326893,1,8.1096144) # epanda=ignore 20 | point(201.99986,83.500049) # point=circle font="helvetica 10 normal" 21 | point(227.00014,83.50004) # point=box color=magenta font="helvetica 10 normal" 22 | point(251.00007,85.50005) # point=diamond font="helvetica 10 normal" 23 | point(203,61.499996) # point=cross font="helvetica 10 normal" 24 | point(224.99987,61.500064) # point=x font="helvetica 10 normal" 25 | point(203.00027,33.499944) # point=arrow font="helvetica 10 normal" 26 | point(227.99956,35.50006) # point=boxcircle font="helvetica 10 normal" 27 | bpanda(67.56482,36.104174,359.99658,649.99658,1,25.223491,12.270268,57.130062,27.7916,1,342.54158) # color=blue width=2 font="helvetica 10 normal" 28 | -------------------------------------------------------------------------------- /pyregion/tests/data/test03_ciao_physical.reg: -------------------------------------------------------------------------------- 1 | # Region file format: CIAO version 1.0 2 | ellipse(785.88577,1797.7287,549.82619,89.161004,0) 3 | rotbox(1959.8391,683.21624,638.98721,267.48301,42.160426) 4 | polygon(2736.2827,1458.4216,2779.6249,1829.9257,2983.9522,1458.4216,3400.5806,1340.7403,2983.9522,1210.7521,2815.3277,1176.5112,2869.0754,1352.6843,2736.2827,1210.7521,2528.6732,1364.6282,2728.7342,1358.6562,2597.3509,1612.4649) 5 | -------------------------------------------------------------------------------- /pyregion/tests/data/test03_fk5.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 3 | fk5 4 | ellipse(20:41:00.281,+29:29:15.89,131959",8559.56",357.84) 5 | box(19:28:38.535,+7:15:35.29,153358",25678.7",40) # color=white 6 | polygon(19:07:03.207,+11:10:17.68,19:05:49.490,+13:37:37.96,19:02:25.027,+10:23:12.68,18:56:03.686,+8:34:30.53,19:02:38.855,+8:43:34.52,19:05:46.348,+8:55:32.97,19:04:35.006,+10:00:26.45,19:07:18.023,+9:23:23.72,19:11:37.745,+11:12:20.02,19:07:18.345,+10:28:49.11,19:09:49.324,+12:48:22.55) # width=3 7 | -------------------------------------------------------------------------------- /pyregion/tests/data/test03_gal.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 3 | galactic 4 | ellipse(+71:42:00.444,-7:34:59.297,131959",8559.56",47.7848) 5 | box(+43:39:56.983,-4:52:47.527,153358",25678.7",89.9452) # color=white 6 | polygon(+44:38:34.311,+1:39:22.099,+46:41:23.616,+3:02:52.493,+43:25:23.329,+2:18:32.674,+41:05:39.208,+2:52:46.601,+41:58:16.635,+1:29:58.465,+42:30:09.271,+0:54:17.680,+43:19:45.731,+1:39:41.400,+43:05:17.265,+0:46:59.718,+45:11:25.241,+0:40:33.344,+44:03:25.785,+1:17:00.534,+46:24:23.695,+1:48:24.681) # width=3 7 | -------------------------------------------------------------------------------- /pyregion/tests/data/test03_icrs.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 3 | icrs 4 | ellipse(20:41:00.279,+29:29:15.87,131959",8559.56",357.84) 5 | box(19:28:38.533,+7:15:35.27,153358",25678.7",40) # color=white 6 | polygon(19:07:03.205,+11:10:17.66,19:05:49.489,+13:37:37.94,19:02:25.026,+10:23:12.66,18:56:03.685,+8:34:30.51,19:02:38.854,+8:43:34.50,19:05:46.346,+8:55:32.95,19:04:35.004,+10:00:26.43,19:07:18.022,+9:23:23.70,19:11:37.743,+11:12:20.00,19:07:18.343,+10:28:49.09,19:09:49.323,+12:48:22.53) # width=3 7 | -------------------------------------------------------------------------------- /pyregion/tests/data/test03_img.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 3 | image 4 | ellipse(785.88577,1797.7287,549.82619,89.161004,0) 5 | box(1959.8391,683.21624,638.98721,267.48301,42.160426) # color=white 6 | polygon(2736.2827,1458.4216,2779.6249,1829.9257,2983.9522,1458.4216,3400.5806,1340.7403,2983.9522,1210.7521,2815.3277,1176.5112,2869.0754,1352.6843,2736.2827,1210.7521,2528.6732,1364.6282,2728.7342,1358.6562,2597.3509,1612.4649) # width=3 7 | -------------------------------------------------------------------------------- /pyregion/tests/data/test04_img.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: E3000-7000.b1.img.fl.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | image 5 | -ellipse(750,853,46,21,317.017) # width=3 text={Ellipse} background 6 | circle(964,945,37.6236) # color=cyan text={Circle} 7 | box(730.5,718,87,48,19.0384) # width=4 text={Rectangle} 8 | polygon(878.11237,811.88766,1003,861,973.00003,688,878.11237,752.11234) # text={Polygon} 9 | point(800, 960) # point=box color=black text={Point} 10 | -------------------------------------------------------------------------------- /pyregion/tests/data/test_annuli.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | image 5 | ellipse(936.01565,922.00132,30.407693,20.504055,58.654304,39.550882,107.20306,72.287547,30) 6 | annulus(712.00157,914.99658,57.134451,76.17922,95.223988,114.26876) 7 | panda(711,716,338.334,648.034,4,38.9388,77.8776,2) 8 | epanda(893,720,53.141616,122.03456,1,33.936596,47.21819,67.873192,94.43638,1,28.831146) || # epanda=(53.141616 122.03456 217.20636 259.80344 311.69855)(33.936596 47.21819 67.873192 94.43638)(28.831146) 9 | epanda(893,720,122.03456,217.20636,1,33.936596,47.21819,67.873192,94.43638,1,28.831146) || # epanda=ignore 10 | epanda(893,720,217.20636,259.80344,1,33.936596,47.21819,67.873192,94.43638,1,28.831146) || # epanda=ignore 11 | epanda(893,720,259.80344,311.69855,1,33.936596,47.21819,67.873192,94.43638,1,28.831146) # epanda=ignore 12 | bpanda(982.0005,775.9995,0,360,4,28.9995,30.9995,57.999,61.999,1,0) 13 | -------------------------------------------------------------------------------- /pyregion/tests/data/test_annuli_ciao.reg: -------------------------------------------------------------------------------- 1 | # Region file format: CIAO version 1.0 2 | annulus(11:24:40.397,-59:15:16.99,0.468502',0.62467') 3 | annulus(11:24:40.397,-59:15:16.99,0.62467',0.780837') 4 | annulus(11:24:40.397,-59:15:16.99,0.780837',0.937004') 5 | pie(11:24:40.465,-59:16:54.89,0.319298',0.478947',338.334,415.759) 6 | pie(11:24:40.465,-59:16:54.89,0.319298',0.478947',55.759,133.184) 7 | pie(11:24:40.465,-59:16:54.89,0.319298',0.478947',133.184,210.609) 8 | pie(11:24:40.465,-59:16:54.89,0.319298',0.478947',210.609,288.034) 9 | pie(11:24:40.465,-59:16:54.89,0.478947',0.638596',338.334,415.759) 10 | pie(11:24:40.465,-59:16:54.89,0.478947',0.638596',55.759,133.184) 11 | pie(11:24:40.465,-59:16:54.89,0.478947',0.638596',133.184,210.609) 12 | pie(11:24:40.465,-59:16:54.89,0.478947',0.638596',210.609,288.034) 13 | -------------------------------------------------------------------------------- /pyregion/tests/data/test_annuli_wcs.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | ellipse(171.10843,-59.253758,14.960585",10.087995",28.857918",19.459034",52.743907",35.565473",30) 6 | annulus(171.16832,-59.254719,28.11015",37.480176",46.850202",56.22023") 7 | panda(171.16861,-59.281915,338.334,648.034,4,19.15789",38.315779",2) 8 | epanda(171.11991,-59.281368,53.141616,122.03456,1,16.696805",23.231349",33.39361",46.462699",1,28.831146) || # epanda=(53.141616 122.03456 217.20636 259.80344 311.69855)(16.696805" 23.231349" 33.39361" 46.462699")(28.831146) 9 | epanda(171.11991,-59.281368,122.03456,217.20636,1,16.696805",23.231349",33.39361",46.462699",1,28.831146) || # epanda=ignore 10 | epanda(171.11991,-59.281368,217.20636,259.80344,1,16.696805",23.231349",33.39361",46.462699",1,28.831146) || # epanda=ignore 11 | epanda(171.11991,-59.281368,259.80344,311.69855,1,16.696805",23.231349",33.39361",46.462699",1,28.831146) # epanda=ignore 12 | bpanda(171.09611,-59.273707,0,360,4,14.267754",15.251754",28.535508",30.503508",1,0) 13 | -------------------------------------------------------------------------------- /pyregion/tests/data/test_context.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | image 5 | # composite(818,804,22.791484) || composite=1 color=red 6 | point(818,804) || # point=boxcircle 7 | # text(917.93183,1012.7012) || textangle=22.791484 font="times 12 normal" text={I} 8 | # text(1038.5994,726.09152) || textangle=22.791484 font="times 12 normal" text={II} 9 | # text(719.41413,591.67326) || textangle=22.791484 font="times 12 normal" text={III} 10 | # text(601.61767,880.81539) || textangle=22.791484 font="times 12 normal" text={IV} 11 | box(605.81608,905.40937,312.19512,39.02439,22.791484) || 12 | box(620.9621,869.4441,312.19512,39.02439,22.791484) || 13 | box(919.09424,1016.1681,39.02439,312.19512,22.791484) || 14 | box(1014.738,738.86864,312.19512,39.02439,22.791484) || 15 | box(1029.8552,702.89125,312.19512,39.02439,22.791484) || 16 | box(716.57467,592.13153,39.02439,312.19512,22.791484) || 17 | box(666.40018,761.54827,312.19512,39.02439,22.791484) || 18 | box(651.25415,797.51354,312.19512,39.02439,22.791484) || 19 | box(636.10813,833.47882,312.19512,39.02439,22.791484) || 20 | box(590.67005,941.37466,312.19512,39.02439,22.791484) || 21 | box(575.524,977.33995,312.19512,39.02439,22.791484) || 22 | box(560.37801,1013.3052,312.19512,39.02439,22.791484) || 23 | box(1075.2743,594.98745,312.19512,39.02439,22.791484) || 24 | box(1060.1571,630.96483,312.19512,39.02439,22.791484) || 25 | box(1044.9724,666.91387,312.19512,39.02439,22.791484) || 26 | box(999.55334,774.81768,312.19512,39.02439,22.791484) || 27 | box(984.43613,810.79506,312.19512,39.02439,22.791484) || 28 | box(969.25147,846.74409,312.19512,39.02439,22.791484) || 29 | box(1026.9902,1061.6062,39.02439,312.19512,22.791484) || 30 | box(991.04728,1046.4696,39.02439,312.19512,22.791484) || 31 | box(955.03709,1031.3047,39.02439,312.19512,22.791484) || 32 | box(883.15144,1001.0315,39.02439,312.19512,22.791484) || 33 | box(847.14118,985.86658,39.02439,312.19512,22.791484) || 34 | box(811.19838,970.72999,39.02439,312.19512,22.791484) || 35 | box(775.25558,955.59341,39.02439,312.19512,22.791484) || 36 | box(860.43576,652.71563,39.02439,312.19512,22.791484) || 37 | box(824.47049,637.56961,39.02439,312.19512,22.791484) || 38 | box(788.50521,622.42358,39.02439,312.19512,22.791484) || 39 | box(752.53995,607.27755,39.02439,312.19512,22.791484) || 40 | box(680.60939,576.9855,39.02439,312.19512,22.791484) || 41 | box(644.64412,561.83947,39.02439,312.19512,22.791484) || 42 | box(608.67884,546.69344,39.02439,312.19512,22.791484) 43 | -------------------------------------------------------------------------------- /pyregion/tests/data/test_text.reg: -------------------------------------------------------------------------------- 1 | # Region file format: DS9 version 4.1 2 | # Filename: test01.fits 3 | global color=green dashlist=8 3 width=1 font="helvetica 10 normal" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 4 | fk5 5 | line(171.2398,-59.290221,171.17045,-59.241461) # line=1 1 width=2 6 | # vector(171.15897,-59.260323,121.9732",291.286) vector=1 7 | # text(171.08249,-59.263998) font="helvetica 14 normal" text={Region} 8 | line(171.12155,-59.236131,171.06222,-59.243213) # line=0 0 font="helvetica 14 normal" dash=1 9 | # compass(171.09742,-59.292704,31.583948") compass=physical {N} {E} 1 1 font="helvetica 14 normal" dash=1 10 | # ruler(171.25782,-59.237453,171.2087,-59.261266) ruler=physical physical 11 | -------------------------------------------------------------------------------- /pyregion/tests/test_cube.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join 3 | 4 | from astropy.io.fits import Header 5 | from numpy.testing import assert_allclose 6 | 7 | from pyregion import parse 8 | 9 | 10 | rootdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') 11 | 12 | 13 | def demo_header(): 14 | return Header.fromtextfile(join(rootdir, "sample_fits04.header")) 15 | 16 | 17 | def test_cube(): 18 | header = demo_header() 19 | 20 | region_string = 'circle(12:04:15.065,+18:26:51.00,173.029")' 21 | r = parse(region_string).as_imagecoord(header) 22 | 23 | assert_allclose(r[0].coord_list, [117, 132, 34.6], atol=0.01) 24 | -------------------------------------------------------------------------------- /pyregion/tests/test_ds9_attr_parser.py: -------------------------------------------------------------------------------- 1 | from pyregion.ds9_attr_parser import get_ds9_attr_parser, get_attr, Ds9AttrParser 2 | 3 | 4 | def test_attr(): 5 | p = get_ds9_attr_parser() 6 | assert p.parseString("color=green")[0] == ("color", "green") 7 | assert p.parseString("font=\"123 123\"")[0] == ("font", '"123 123"') 8 | assert p.parseString("color")[0] == ("color",) 9 | assert p.parseString("tag={group 1}")[0] == ("tag", "group 1") 10 | assert p.parseString('color=#6a8')[0] == ("color", "#6a8") 11 | 12 | 13 | def test_get_attr(): 14 | attr_list = [('tag', 'group1'), ('tag', 'group2'), 15 | ('tag', 'group3'), ('color', 'green')] 16 | global_attrs = [], {} 17 | 18 | attr = get_attr(attr_list, global_attrs) 19 | assert attr[0] == [] 20 | assert attr[1] == {'color': 'green', 21 | 'tag': set(['group1', 'group3', 'group2'])} 22 | 23 | 24 | def test_shape_in_comment(): 25 | parser = Ds9AttrParser() 26 | 27 | r = parser.parse_check_shape("segment(0, 2)") 28 | assert r[0].name == "segment" 29 | assert r[1] == [] 30 | 31 | r = parser.parse_check_shape("projection(0, 2, 3, 2, 4)") 32 | assert r[0].name == "projection" 33 | assert r[1] == [] 34 | -------------------------------------------------------------------------------- /pyregion/tests/test_ds9_region_parser.py: -------------------------------------------------------------------------------- 1 | from pyregion.ds9_region_parser import RegionParser, Global 2 | from pyregion.parser_helper import CoordCommand, Shape 3 | from pyregion.region_numbers import SimpleNumber, AngularDistance 4 | 5 | 6 | def test_regionLine(): 7 | test_string_1 = [ 8 | "circle(109,253,28.950304) # comment 1", 9 | "polygon(257,290,300.78944,271.78944,300.78944,178.21056,258,216,207.21056,178.21056)", 10 | "polygon(273.98971,175.01029,274.01029,175.01029,274.01029,174.98971,273.98971,174.98971)", 11 | "-rotbox(162,96.5,134,41,43.801537)", 12 | "ellipse(172,328,23,41,27.300813)", 13 | ] 14 | 15 | test_names = [ 16 | "circle", 17 | "polygon", 18 | "polygon", 19 | "rotbox", 20 | "ellipse", 21 | ] 22 | 23 | rp = RegionParser() 24 | 25 | for s, n in zip(test_string_1, test_names): 26 | s = rp.parseLine(s)[0] 27 | assert len(s) == 1 28 | assert s[0].name == n 29 | 30 | 31 | def test_comment(): 32 | s = "circle(3323, 423, 423) # comment" 33 | 34 | rp = RegionParser() 35 | c = rp.parseLine(s)[1] 36 | 37 | assert c == "comment" 38 | 39 | s = " # comment2" 40 | c = rp.parseLine(s)[1] 41 | 42 | assert c == "comment2" 43 | 44 | 45 | def test_global(): 46 | s = 'global color=green font="helvetica 10 normal" select=1 highlite=1 edit=1 move=1 delete=1 include=1 fixed=0 source' 47 | 48 | rp = RegionParser() 49 | ss = rp.parseLine(s)[0] 50 | 51 | assert isinstance(ss[0], Global) 52 | 53 | 54 | def test_space_delimited_region(): 55 | """ 56 | Regression test for https://github.com/astropy/pyregion/issues/73 57 | """ 58 | s = 'J2000; circle 188.5557102 12.0314056 1" # color=red' 59 | 60 | rp = RegionParser() 61 | ss = rp.parseLine(s)[0] 62 | 63 | assert isinstance(ss[0], CoordCommand) 64 | assert ss[0].text == "J2000" 65 | 66 | assert isinstance(ss[1], Shape) 67 | param_types = list(map(type, ss[1].params)) 68 | assert param_types == [SimpleNumber, SimpleNumber, AngularDistance] 69 | -------------------------------------------------------------------------------- /pyregion/tests/test_get_mask.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | from os.path import join 4 | from astropy.io.fits import Header 5 | 6 | from pyregion import open as pyregion_open 7 | 8 | rootdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') 9 | 10 | 11 | def demo_header(): 12 | return Header.fromtextfile(join(rootdir, "sample_fits01.header")) 13 | 14 | 15 | def test_region(): 16 | ref_name = "test01_img.reg" 17 | header = demo_header() 18 | 19 | ref_region = pyregion_open(join(rootdir, ref_name)).as_imagecoord(header) 20 | mask = ref_region.get_mask(shape=(100, 100)) 21 | 22 | assert isinstance(mask, np.ndarray) and mask.shape == (100, 100) 23 | 24 | # TODO: assert the content of the mask, too 25 | -------------------------------------------------------------------------------- /pyregion/tests/test_parser_helper.py: -------------------------------------------------------------------------------- 1 | from pyregion.parser_helper import define_shape 2 | from pyregion.region_numbers import CoordOdd, CoordEven, Distance 3 | 4 | 5 | def test_define_shape(): 6 | args = [s.parser for s in [CoordOdd, CoordEven, Distance]] 7 | circle_parser = define_shape("circle", args, args_repeat=None) 8 | 9 | p = circle_parser.parseString("circle(1:2:3, 2:4:5, 3.)") 10 | assert p[0] == "circle" 11 | assert isinstance(p[1], CoordOdd.type) 12 | assert isinstance(p[2], CoordEven.type) 13 | 14 | args = [s.parser for s in [CoordEven]] 15 | circle_parser = define_shape("circle", args, args_repeat=None) 16 | 17 | p = circle_parser.parseString("circle(1:2:3)") 18 | p = circle_parser.parseString("circle(1d2m3s)") 19 | assert p[0] == "circle" 20 | assert isinstance(p[1], CoordEven.type) 21 | 22 | args = [s.parser for s in [CoordOdd, CoordEven, Distance, Distance]] 23 | ell_parser = define_shape("ell", args, args_repeat=(2, 4)) 24 | 25 | p = ell_parser.parseString("ell(1:2:2, 2:2:2, 3, 4, 5, 6)") 26 | assert p[0] == "ell" 27 | assert isinstance(p[1], CoordOdd.type) 28 | assert isinstance(p[2], CoordEven.type) 29 | 30 | args = [s.parser for s in [CoordOdd, CoordEven]] 31 | polygon_parser = define_shape("polygon", args, args_repeat=(0, 2)) 32 | p = polygon_parser.parseString("polygon(3:2:4.22, 3:3:4., 3:2:3, 3.2)") 33 | assert p[0] == "polygon" 34 | assert isinstance(p[-2], CoordOdd.type) 35 | assert isinstance(p[2], CoordEven.type) 36 | -------------------------------------------------------------------------------- /pyregion/tests/test_region.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | import numpy as np 4 | from os.path import join 5 | from astropy.io.fits import Header 6 | from numpy.testing import assert_allclose 7 | 8 | from pyregion import open as pyregion_open 9 | 10 | 11 | rootdir = join(os.path.dirname(os.path.abspath(__file__)), 'data') 12 | 13 | 14 | @pytest.fixture(scope="module") 15 | def header(): 16 | return Header.fromtextfile(join(rootdir, "sample_fits01.header")) 17 | 18 | 19 | @pytest.mark.parametrize(("ref_name", "reg_name", "header_name"), [ 20 | ("test01_img.reg", "test01_fk5_sexagecimal.reg", "sample_fits01.header"), 21 | ("test01_img.reg", "test01_gal.reg", "sample_fits01.header"), 22 | ("test01_img.reg", "test01_ds9_physical.reg", "sample_fits01.header"), 23 | ("test01_img.reg", "test01_fk5_degree.reg", "sample_fits01.header"), 24 | ("test01_img.reg", "test01_mixed.reg", "sample_fits01.header"), 25 | ("test01_img.reg", "test01_ciao.reg", "sample_fits01.header"), 26 | ("test01_img.reg", "test01_ciao_physical.reg", "sample_fits01.header"), 27 | ("test01_img.reg", "test01_fk5.reg", "sample_fits01.header"), 28 | ("test01_img.reg", "test01_fk4.reg", "sample_fits01.header"), 29 | ("test01_img.reg", "test01_icrs.reg", "sample_fits01.header"), 30 | ("test02_1_img.reg", "test02_1_fk5.reg", "sample_fits02.header"), 31 | ("test_annuli.reg", "test_annuli_wcs.reg", "sample_fits01.header"), 32 | ("test03_img.reg", "test03_fk5.reg", "sample_fits03.header"), 33 | ("test03_img.reg", "test03_icrs.reg", "sample_fits03.header"), 34 | ("test03_img.reg", "test03_ciao_physical.reg", "sample_fits03.header"), 35 | ("test03_img.reg", "test03_gal.reg", "sample_fits03.header"), 36 | ]) 37 | def test_region(ref_name, reg_name, header_name): 38 | header = Header.fromtextfile(join(rootdir, header_name)) 39 | ref_region = pyregion_open(join(rootdir, ref_name)).as_imagecoord(header) 40 | 41 | r = pyregion_open(join(rootdir, reg_name)).as_imagecoord(header) 42 | 43 | assert len(r) == len(ref_region) 44 | 45 | for ref_reg, reg in zip(ref_region, r): 46 | if reg.name == "rotbox": 47 | reg.name = "box" 48 | 49 | assert ref_reg.name == reg.name 50 | 51 | # Normalize everything like angles 52 | ref_list = np.asarray(ref_reg.coord_list) 53 | reg_list = np.asarray(reg.coord_list) 54 | assert_allclose((ref_list + 180) % 360 - 180, 55 | (reg_list + 180) % 360 - 180, 56 | atol=0.03) 57 | 58 | assert ref_reg.exclude == reg.exclude 59 | 60 | 61 | @pytest.mark.parametrize("reg_name", [ 62 | "test_annuli_ciao.reg", # subset of test03_img.reg 63 | "test_context.reg", 64 | "test02.reg", 65 | "test04_img.reg", 66 | "test_text.reg", 67 | "test01.reg", 68 | ]) 69 | def test_open_regions(reg_name, header): 70 | # TODO: Better test. Like figure out how these files relate to each other 71 | pyregion_open(join(rootdir, reg_name)).as_imagecoord(header) 72 | -------------------------------------------------------------------------------- /pyregion/tests/test_region_numbers.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | from pyregion.region_numbers import (usn, simple_integer, sexadecimal60, Sixty, 3 | hms_number, dms_number, angular_distance, 4 | CoordOdd, HMS) 5 | 6 | 7 | def test_usn(): 8 | for f in ["32.4", "0.23", "0.3e-7", "1.234e+7"]: 9 | assert usn.parseString(f)[0] == f 10 | 11 | 12 | def test_integer(): 13 | for f in ["32", "+3"]: 14 | assert simple_integer.parseString(f)[0].text == f 15 | 16 | 17 | def test_sexadecimal(): 18 | s = sexadecimal60.parseString 19 | 20 | assert s("32:24:32.2")[0].v == Sixty(1, 32, 24, 32.2).v 21 | assert s("-32:24:32.2")[0].v == Sixty(-1, 32, 24, 32.2).v 22 | assert s("+32:24:32.2")[0].v == Sixty(1, 32, 24, 32.2).v 23 | 24 | 25 | def test_hms(): 26 | s = hms_number.parseString 27 | 28 | assert s("32h24m32.2s")[0].v == Sixty(1, 32, 24, 32.2).v 29 | assert s("0h24m32.2s")[0].v == Sixty(1, 0, 24, 32.2).v 30 | assert s("32h")[0].v == Sixty(1, 32, 0, 0).v 31 | 32 | 33 | def test_dms(): 34 | s = dms_number.parseString 35 | 36 | assert s("32d24m32.2s")[0].v == Sixty(1, 32, 24, 32.2).v 37 | assert s("-32d24m32.2s")[0].v == Sixty(-1, 32, 24, 32.2).v 38 | assert s("32d")[0].v == Sixty(1, 32, 0, 0).v 39 | 40 | 41 | def test_ang_distance(): 42 | s = angular_distance.parseString 43 | 44 | assert s("32.3'")[0].v == Sixty(1, 0, 32.3, 0.).v 45 | assert s("32\'24\"")[0].v == Sixty(1, 0, 32, 24).v 46 | assert s("0.3d")[0].v == Sixty(1, 0.3, 0, 0).v 47 | assert s("1r")[0].v == Sixty(1, 1./pi*180., 0, 0).v 48 | 49 | 50 | def test_coord_odd(): 51 | s = CoordOdd.parser.parseString 52 | 53 | assert s("32h24m32.2s")[0].v == Sixty(1, 32, 24, 32.2).v 54 | assert s("32:24:32.2s")[0].v == Sixty(1, 32, 24, 32.2).v 55 | assert s("32.24")[0].v == 32.24 56 | 57 | s1 = s("32:24:32.2s")[0] 58 | assert isinstance(s1, HMS) 59 | -------------------------------------------------------------------------------- /pyregion/tests/test_wcs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os.path 3 | from numpy.testing import assert_allclose 4 | from astropy.io.fits import Header 5 | from pyregion.ds9_region_parser import ds9_shape_defs 6 | from pyregion.region_numbers import CoordOdd, CoordEven 7 | from pyregion import wcs_converter 8 | from pyregion.wcs_helper import _calculate_rotation_angle 9 | 10 | 11 | rootdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') 12 | 13 | 14 | def test__generate_arg_types_min_list(): 15 | for name, shape in ds9_shape_defs.items(): 16 | args_list = shape.args_list 17 | min_list = wcs_converter._generate_arg_types(len(args_list), name) 18 | for expected_arg, tested_arg in zip(args_list, min_list): 19 | assert expected_arg == tested_arg 20 | 21 | 22 | @pytest.mark.parametrize(("name", "length", "result"), [ 23 | ("polygon", 6, 3 * [CoordOdd, CoordEven]), 24 | ]) 25 | def test__generate_arg_types_with_repeats(name, length, result): 26 | test_list = wcs_converter._generate_arg_types(length, name) 27 | 28 | for expected_arg, tested_arg in zip(result, test_list): 29 | assert expected_arg == tested_arg 30 | 31 | 32 | @pytest.mark.parametrize(("region_frame", "header_name", "rot_angle"), [ 33 | ('fk5', 'sample_fits01.header', 0.00505712), 34 | ('galactic', 'sample_fits01.header', -19.2328), 35 | ('fk4', 'sample_fits01.header', -0.0810223), 36 | ('fk5', 'sample_fits03.header', -2.16043), 37 | ]) 38 | def test_calculate_rotation_angle(region_frame, header_name, rot_angle): 39 | header = Header.fromtextfile(os.path.join(rootdir, header_name)) 40 | assert_allclose( 41 | _calculate_rotation_angle(region_frame, header), rot_angle, 42 | atol=0.001 43 | ) 44 | -------------------------------------------------------------------------------- /pyregion/wcs_converter.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from astropy.coordinates import SkyCoord 4 | from astropy.wcs import WCS 5 | from astropy.wcs.utils import proj_plane_pixel_area, proj_plane_pixel_scales 6 | import numpy as np 7 | from .wcs_helper import _estimate_angle 8 | from .region_numbers import CoordOdd, Distance, Angle 9 | from .parser_helper import Shape, CoordCommand 10 | from .region_numbers import SimpleNumber, SimpleInteger 11 | 12 | 13 | def _generate_arg_types(coordlist_length, shape_name): 14 | """Find coordinate types based on shape name and coordlist length 15 | 16 | This function returns a list of coordinate types based on which 17 | coordinates can be repeated for a given type of shap 18 | 19 | Parameters 20 | ---------- 21 | coordlist_length : int 22 | The number of coordinates or arguments used to define the shape. 23 | 24 | shape_name : str 25 | One of the names in `pyregion.ds9_shape_defs`. 26 | 27 | Returns 28 | ------- 29 | arg_types : list 30 | A list of objects from `pyregion.region_numbers` with a length equal to 31 | coordlist_length. 32 | 33 | """ 34 | from .ds9_region_parser import ds9_shape_defs 35 | from .ds9_attr_parser import ds9_shape_in_comment_defs 36 | 37 | if shape_name in ds9_shape_defs: 38 | shape_def = ds9_shape_defs[shape_name] 39 | else: 40 | shape_def = ds9_shape_in_comment_defs[shape_name] 41 | 42 | initial_arg_types = shape_def.args_list 43 | arg_repeats = shape_def.args_repeat 44 | 45 | if arg_repeats is None: 46 | return initial_arg_types 47 | 48 | # repeat args between n1 and n2 49 | n1, n2 = arg_repeats 50 | arg_types = list(initial_arg_types[:n1]) 51 | num_of_repeats = coordlist_length - (len(initial_arg_types) - n2) 52 | arg_types.extend((num_of_repeats - n1) // 53 | (n2 - n1) * initial_arg_types[n1:n2]) 54 | arg_types.extend(initial_arg_types[n2:]) 55 | return arg_types 56 | 57 | 58 | def convert_to_imagecoord(shape, header): 59 | """Convert the coordlist of `shape` to image coordinates 60 | 61 | Parameters 62 | ---------- 63 | shape : `pyregion.parser_helper.Shape` 64 | The `Shape` to convert coordinates 65 | 66 | header : `~astropy.io.fits.Header` 67 | Specifies what WCS transformations to use. 68 | 69 | Returns 70 | ------- 71 | new_coordlist : list 72 | A list of image coordinates defining the shape. 73 | 74 | """ 75 | arg_types = _generate_arg_types(len(shape.coord_list), shape.name) 76 | 77 | new_coordlist = [] 78 | is_even_distance = True 79 | coord_list_iter = iter(zip(shape.coord_list, arg_types)) 80 | 81 | new_wcs = WCS(header) 82 | pixel_scales = proj_plane_pixel_scales(new_wcs) 83 | 84 | for coordinate, coordinate_type in coord_list_iter: 85 | if coordinate_type == CoordOdd: 86 | even_coordinate = next(coord_list_iter)[0] 87 | 88 | old_coordinate = SkyCoord(coordinate, even_coordinate, 89 | frame=shape.coord_format, unit='degree', 90 | obstime='J2000') 91 | new_coordlist.extend( 92 | x.item() 93 | for x in old_coordinate.to_pixel(new_wcs, origin=1) 94 | ) 95 | 96 | elif coordinate_type == Distance: 97 | if arg_types[-1] == Angle: 98 | degree_per_pixel = pixel_scales[0 if is_even_distance else 1] 99 | 100 | is_even_distance = not is_even_distance 101 | else: 102 | degree_per_pixel = np.sqrt(proj_plane_pixel_area(new_wcs)) 103 | 104 | new_coordlist.append(coordinate / degree_per_pixel) 105 | 106 | elif coordinate_type == Angle: 107 | new_angle = _estimate_angle(coordinate, 108 | shape.coord_format, 109 | header) 110 | new_coordlist.append(new_angle) 111 | 112 | else: 113 | new_coordlist.append(coordinate) 114 | 115 | return new_coordlist 116 | 117 | 118 | def convert_physical_to_imagecoord(shape, header): 119 | arg_types = _generate_arg_types(len(shape.coord_list), shape.name) 120 | 121 | new_coordlist = [] 122 | coord_list_iter = iter(zip(shape.coord_list, arg_types)) 123 | 124 | from .physical_coordinate import PhysicalCoordinate 125 | pc = PhysicalCoordinate(header) 126 | 127 | for coordinate, coordinate_type in coord_list_iter: 128 | if coordinate_type == CoordOdd: 129 | even_coordinate = next(coord_list_iter)[0] 130 | 131 | xy0 = pc.to_image(coordinate, even_coordinate) 132 | new_coordlist.extend(xy0) 133 | elif coordinate_type == Distance: 134 | new_coordlist.append(pc.to_image_distance(coordinate)) 135 | else: 136 | new_coordlist.append(coordinate) 137 | 138 | return new_coordlist 139 | 140 | 141 | def check_wcs_and_convert(args, all_dms=False): 142 | is_wcs = False 143 | 144 | value_list = [] 145 | for a in args: 146 | if isinstance(a, SimpleNumber) or isinstance(a, SimpleInteger) \ 147 | or all_dms: 148 | value_list.append(a.v) 149 | else: 150 | value_list.append(a.degree) 151 | is_wcs = True 152 | 153 | return is_wcs, value_list 154 | 155 | 156 | def check_wcs(l): 157 | default_coord = "physical" 158 | 159 | for l1, c1 in l: 160 | if isinstance(l1, CoordCommand): 161 | default_coord = l1.text.lower() 162 | continue 163 | if isinstance(l1, Shape): 164 | if default_coord == "galactic": 165 | is_wcs, coord_list = check_wcs_and_convert(l1.params, 166 | all_dms=True) 167 | else: 168 | is_wcs, coord_list = check_wcs_and_convert(l1.params) 169 | 170 | if is_wcs and (default_coord == "physical"): # ciao format 171 | coord_format = "fk5" 172 | else: 173 | coord_format = default_coord 174 | 175 | l1n = copy.copy(l1) 176 | 177 | l1n.coord_list = coord_list 178 | l1n.coord_format = coord_format 179 | 180 | yield l1n, c1 181 | else: 182 | yield l1, c1 183 | -------------------------------------------------------------------------------- /pyregion/wcs_helper.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from astropy.coordinates import SkyCoord 3 | from astropy.wcs import WCS 4 | from astropy.wcs.utils import proj_plane_pixel_scales 5 | 6 | 7 | def _estimate_angle(angle, reg_coordinate_frame, header): 8 | """Transform an angle into a different frame 9 | 10 | Parameters 11 | ---------- 12 | angle : float, int 13 | The number of degrees, measured from the Y axis in origin's frame 14 | 15 | reg_coordinate_frame : str 16 | Coordinate frame in which ``angle`` is defined 17 | 18 | header : `~astropy.io.fits.Header` instance 19 | Header describing the image 20 | 21 | Returns 22 | ------- 23 | angle : float 24 | The angle, measured from the Y axis in the WCS defined by ``header'` 25 | """ 26 | y_axis_rot = _calculate_rotation_angle(reg_coordinate_frame, header) 27 | return angle - y_axis_rot 28 | 29 | 30 | def _calculate_rotation_angle(reg_coordinate_frame, header): 31 | """Calculates the rotation angle from the region to the header's frame 32 | 33 | This attempts to be compatible with the implementation used by SAOImage 34 | DS9. In particular, this measures the rotation of the north axis as 35 | measured at the center of the image, and therefore requires a 36 | `~astropy.io.fits.Header` object with defined 'NAXIS1' and 'NAXIS2' 37 | keywords. 38 | 39 | Parameters 40 | ---------- 41 | reg_coordinate_frame : str 42 | Coordinate frame used by the region file 43 | 44 | header : `~astropy.io.fits.Header` instance 45 | Header describing the image 46 | 47 | Returns 48 | ------- 49 | y_axis_rot : float 50 | Degrees by which the north axis in the region's frame is rotated when 51 | transformed to pixel coordinates 52 | """ 53 | new_wcs = WCS(header) 54 | region_frame = SkyCoord( 55 | '0d 0d', 56 | frame=reg_coordinate_frame, 57 | obstime='J2000') 58 | region_frame = SkyCoord( 59 | '0d 0d', 60 | frame=reg_coordinate_frame, 61 | obstime='J2000', 62 | equinox=region_frame.equinox) 63 | 64 | origin = SkyCoord.from_pixel( 65 | header['NAXIS1'] / 2, 66 | header['NAXIS2'] / 2, 67 | wcs=new_wcs, 68 | origin=1).transform_to(region_frame) 69 | 70 | offset = proj_plane_pixel_scales(new_wcs)[1] 71 | 72 | origin_x, origin_y = origin.to_pixel(new_wcs, origin=1) 73 | origin_lon = origin.data.lon.degree 74 | origin_lat = origin.data.lat.degree 75 | 76 | offset_point = SkyCoord( 77 | origin_lon, origin_lat + offset, unit='degree', 78 | frame=origin.frame.name, obstime='J2000') 79 | offset_x, offset_y = offset_point.to_pixel(new_wcs, origin=1) 80 | 81 | north_rot = np.arctan2( 82 | offset_y - origin_y, 83 | offset_x - origin_x) / np.pi * 180. 84 | 85 | cdelt = new_wcs.wcs.get_cdelt() 86 | if (cdelt > 0).all() or (cdelt < 0).all(): 87 | return north_rot - 90 88 | else: 89 | return -(north_rot - 90) 90 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{310,311,312}-{test}{,-oldestdeps} 4 | build_docs 5 | isolated_build = True 6 | 7 | [testenv] 8 | setenv = 9 | HOME = {envtmpdir} 10 | MPLBACKEND = Agg 11 | devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/liberfa/simple https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple 12 | changedir = 13 | .tmp/{envname} 14 | deps = 15 | 16 | oldestdeps: pyparsing==2.0.* 17 | oldestdeps: numpy==1.23.* 18 | oldestdeps: astropy==5.0.* 19 | 20 | devdeps: numpy>=0.0.dev0 21 | devdeps: pyerfa>=0.0.dev0 22 | devdeps: astropy>=0.0.dev0 23 | 24 | extras = 25 | test 26 | pip_pre = 27 | devdeps: true 28 | commands = 29 | {list_dependencies_command} 30 | !oldestdeps: pytest --pyargs pyregion {posargs} 31 | 32 | [testenv:build_docs] 33 | changedir = docs 34 | description = invoke sphinx-build to build the HTML docs 35 | extras = docs 36 | commands = 37 | {list_dependencies_command} 38 | sphinx-build -W -b html . _build/html 39 | --------------------------------------------------------------------------------