termfits 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-13
4
+
5
+ First release.
6
+
7
+ - Truecolour half-block rendering: a 100 x 100 character view shows 100 x 200 pixels.
8
+ - Zoom (steps of sqrt 2) and pan; block-mean or block-max binning when zoomed out.
9
+ - Radio defaults: display limits from a robust (MAD) noise estimate, -1 to +10 sigma;
10
+ pixel value in BUNIT and in sigma; restoring beam shown in the status bar and drawn.
11
+ - Local noise re-estimate from the region in view.
12
+ - Ten colour maps, including a diverging map centred on the median.
13
+ - Data cubes (step through planes) and multi-extension files.
14
+ - RA/Dec readout via astropy WCS; built-in reader for plain images if astropy is missing.
termfits-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ian Heywood
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include LICENSE README.md CHANGELOG.md
2
+ recursive-include tests *.py
3
+ prune docs
4
+ prune .github
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: termfits
3
+ Version: 0.1.0
4
+ Summary: A terminal FITS image viewer with zoom and pan, tuned for radio images
5
+ Author-email: Ian Heywood <email.ian.heywood@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ianheywood/termfits
8
+ Project-URL: Issues, https://github.com/ianheywood/termfits/issues
9
+ Keywords: fits,astronomy,radio astronomy,terminal,viewer,tui
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
18
+ Classifier: Topic :: Scientific/Engineering :: Visualization
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy>=1.21
23
+ Requires-Dist: astropy>=5.0
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=7; extra == "test"
26
+ Dynamic: license-file
27
+
28
+ # termfits
29
+
30
+ A FITS image viewer that runs in your terminal, built for quick looks at radio images
31
+ over SSH: no X forwarding, no VNC, no copying files back to your laptop.
32
+
33
+ ![termfits showing a radio field: default limits vs a tightened upper limit](https://raw.githubusercontent.com/ianheywood/termfits/main/docs/images/radio_sigma_limits.png)
34
+
35
+ It draws the image with Unicode half-block characters in 24-bit colour. Each character
36
+ cell holds two pixels, so a 100 x 100 character window shows a 100 x 200 grid of
37
+ roughly square pixels. You can zoom, pan, step through cube planes and switch extensions,
38
+ and the status bar reports the pixel value, its significance in sigma, the position
39
+ in RA/Dec, the noise and the beam.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install termfits
45
+ ```
46
+
47
+ This installs a `termfits` command. termfits needs Python 3.9+, numpy and astropy, and
48
+ a terminal with truecolour support: most current terminals have it, including iTerm2,
49
+ GNOME Terminal, Konsole, kitty, WezTerm, Alacritty, Windows Terminal (via WSL) and tmux
50
+ with `set -g default-terminal "tmux-256color"` and `set -as terminal-features ",*:RGB"`.
51
+ It runs on Linux and macOS, or under WSL on Windows.
52
+
53
+ ## Use
54
+
55
+ ```bash
56
+ termfits image.fits
57
+ termfits cube.fits --plane 120 --cmap inferno
58
+ termfits image.fits --sigma-hi 4 # tighter limits for faint emission
59
+ termfits image.fits --width 100 --height 50 # square-ish view for a square image
60
+ ```
61
+
62
+ Make your terminal at least 100 columns by 104 rows for the default view (100 x 100
63
+ plus a 4-line status bar), or set `--width` / `--height` to fit. Terminal characters are
64
+ about twice as tall as they are wide, so `--height` set to half of `--width` gives a
65
+ square viewing area.
66
+
67
+ Press `?` in the viewer for the key list.
68
+
69
+ | Keys | Action |
70
+ |---|---|
71
+ | arrows or `h j k l` | pan by 10% of the view |
72
+ | Shift+arrows or `H J K L` | pan by half the view |
73
+ | `+` / `-` | zoom in / out |
74
+ | `f` / `1` / `r` | fit image / 1:1 / reset |
75
+ | `<` `>` | lower / raise the upper display limit (sigma) |
76
+ | `(` `)` | lower / raise the lower display limit (sigma) |
77
+ | `c` | cycle clip mode: sigma, 99.5, 99, 98, 95 %, min/max |
78
+ | `n` / `N` | measure the noise in the current view / back to the whole image |
79
+ | `b` | zoomed-out binning: block mean / block max |
80
+ | `m` / `M` | next / previous colour map |
81
+ | `s` | cycle stretch: linear, sqrt, log, asinh |
82
+ | `e` / `x` | toggle beam ellipse / crosshair |
83
+ | `[` `]` | previous / next plane (channel, Stokes) |
84
+ | `Tab` | next image extension |
85
+ | `q` or `Esc` | quit |
86
+
87
+ `termfits image.fits --snapshot` prints one frame to stdout and exits, which is handy
88
+ for logs or notebooks.
89
+
90
+ ## Radio defaults
91
+
92
+ - **Display limits from the noise.** The rms is estimated as 1.4826 x the median
93
+ absolute deviation, which bright sources barely affect. The default range is -1 to
94
+ +10 sigma about the median. Press `n` to re-measure the noise from the region in view
95
+ when it varies across the field (e.g. towards the primary-beam edge).
96
+ - **Values in sigma.** The status bar shows the pixel under the crosshair in BUNIT
97
+ (scaled to Jy, mJy or µJy so the rms reads sensibly) and in sigma.
98
+ - **Beam.** BMAJ/BMIN/BPA from the header are shown in arcsec and pixels, and the beam
99
+ is drawn in the lower-left corner once it is large enough to see.
100
+ - **Blanked pixels** (NaNs, e.g. outside a primary-beam cut) are drawn in a muted purple
101
+ so they are not mistaken for zero.
102
+
103
+ ## Zoomed out: what you can and can't trust
104
+
105
+ When the image is larger than the view, blocks of pixels are combined into one screen
106
+ pixel; none are skipped.
107
+
108
+ ![Block mean vs block max for the whole field](https://raw.githubusercontent.com/ianheywood/termfits/main/docs/images/mean_vs_max.png)
109
+
110
+ - **Block mean** (default) lowers the noise, so faint *extended* emission stands out,
111
+ but faint *compact* sources are diluted and can vanish.
112
+ - **Block max** (`b`) keeps every compact source visible, but it also lifts the
113
+ background: the maximum of many noise pixels sits around +3 sigma, so noise peaks
114
+ look like sources.
115
+
116
+ Use either for finding candidates, then press `1` to inspect them at full resolution.
117
+ For decisions about marginal detections, use a full-resolution viewer such as CARTA
118
+ or DS9.
119
+
120
+ ## Colour maps
121
+
122
+ ![The ten colour maps on the same field](https://raw.githubusercontent.com/ianheywood/termfits/main/docs/images/colourmaps.png)
123
+
124
+ `inverted` (default) and `gray`; `heat` (DS9-style); `inferno`, `magma`, `viridis` and
125
+ `cubehelix`, which are perceptually uniform and the best colour choices for faint
126
+ features; `cool`; `rainbow` (turbo-like, good for bright high-dynamic-range regions,
127
+ but it creates false contours in noise); and `diverging`, which is centred on the
128
+ median so negative sidelobes show blue and positive emission red.
129
+
130
+ Truecolour gives the full range; `--256` is a fallback for terminals without it, and
131
+ colour maps get coarse there (6 levels per channel).
132
+
133
+ ## Data support
134
+
135
+ - 2-D images, and cubes of any dimension: extra axes (FREQ, STOKES, ...) are
136
+ flattened into planes stepped with `[` and `]`.
137
+ - Multi-extension files: every image HDU is reachable with `Tab`.
138
+ - RA/Dec readout from the celestial WCS via astropy.
139
+ - Large images: one plane is loaded into memory as float32 (about 400 MB for
140
+ 10k x 10k). The first zoom-out builds the binned copies, which takes a few seconds;
141
+ after that it is quick.
142
+ - If astropy is missing, a built-in reader handles plain uncompressed images, without
143
+ WCS.
144
+
145
+ Not yet supported: per-channel beams stored in a CASA `BEAMS` table (the header beam is
146
+ used), and tile-compressed images without astropy.
147
+
148
+ ## Development
149
+
150
+ ```bash
151
+ git clone https://github.com/ianheywood/termfits
152
+ cd termfits
153
+ pip install -e ".[test]"
154
+ pytest
155
+ ```
156
+
157
+ The screenshots in `docs/images` were rendered from termfits' own output, using
158
+ synthetic data.
159
+
160
+ ## Licence
161
+
162
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,135 @@
1
+ # termfits
2
+
3
+ A FITS image viewer that runs in your terminal, built for quick looks at radio images
4
+ over SSH: no X forwarding, no VNC, no copying files back to your laptop.
5
+
6
+ ![termfits showing a radio field: default limits vs a tightened upper limit](https://raw.githubusercontent.com/ianheywood/termfits/main/docs/images/radio_sigma_limits.png)
7
+
8
+ It draws the image with Unicode half-block characters in 24-bit colour. Each character
9
+ cell holds two pixels, so a 100 x 100 character window shows a 100 x 200 grid of
10
+ roughly square pixels. You can zoom, pan, step through cube planes and switch extensions,
11
+ and the status bar reports the pixel value, its significance in sigma, the position
12
+ in RA/Dec, the noise and the beam.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install termfits
18
+ ```
19
+
20
+ This installs a `termfits` command. termfits needs Python 3.9+, numpy and astropy, and
21
+ a terminal with truecolour support: most current terminals have it, including iTerm2,
22
+ GNOME Terminal, Konsole, kitty, WezTerm, Alacritty, Windows Terminal (via WSL) and tmux
23
+ with `set -g default-terminal "tmux-256color"` and `set -as terminal-features ",*:RGB"`.
24
+ It runs on Linux and macOS, or under WSL on Windows.
25
+
26
+ ## Use
27
+
28
+ ```bash
29
+ termfits image.fits
30
+ termfits cube.fits --plane 120 --cmap inferno
31
+ termfits image.fits --sigma-hi 4 # tighter limits for faint emission
32
+ termfits image.fits --width 100 --height 50 # square-ish view for a square image
33
+ ```
34
+
35
+ Make your terminal at least 100 columns by 104 rows for the default view (100 x 100
36
+ plus a 4-line status bar), or set `--width` / `--height` to fit. Terminal characters are
37
+ about twice as tall as they are wide, so `--height` set to half of `--width` gives a
38
+ square viewing area.
39
+
40
+ Press `?` in the viewer for the key list.
41
+
42
+ | Keys | Action |
43
+ |---|---|
44
+ | arrows or `h j k l` | pan by 10% of the view |
45
+ | Shift+arrows or `H J K L` | pan by half the view |
46
+ | `+` / `-` | zoom in / out |
47
+ | `f` / `1` / `r` | fit image / 1:1 / reset |
48
+ | `<` `>` | lower / raise the upper display limit (sigma) |
49
+ | `(` `)` | lower / raise the lower display limit (sigma) |
50
+ | `c` | cycle clip mode: sigma, 99.5, 99, 98, 95 %, min/max |
51
+ | `n` / `N` | measure the noise in the current view / back to the whole image |
52
+ | `b` | zoomed-out binning: block mean / block max |
53
+ | `m` / `M` | next / previous colour map |
54
+ | `s` | cycle stretch: linear, sqrt, log, asinh |
55
+ | `e` / `x` | toggle beam ellipse / crosshair |
56
+ | `[` `]` | previous / next plane (channel, Stokes) |
57
+ | `Tab` | next image extension |
58
+ | `q` or `Esc` | quit |
59
+
60
+ `termfits image.fits --snapshot` prints one frame to stdout and exits, which is handy
61
+ for logs or notebooks.
62
+
63
+ ## Radio defaults
64
+
65
+ - **Display limits from the noise.** The rms is estimated as 1.4826 x the median
66
+ absolute deviation, which bright sources barely affect. The default range is -1 to
67
+ +10 sigma about the median. Press `n` to re-measure the noise from the region in view
68
+ when it varies across the field (e.g. towards the primary-beam edge).
69
+ - **Values in sigma.** The status bar shows the pixel under the crosshair in BUNIT
70
+ (scaled to Jy, mJy or µJy so the rms reads sensibly) and in sigma.
71
+ - **Beam.** BMAJ/BMIN/BPA from the header are shown in arcsec and pixels, and the beam
72
+ is drawn in the lower-left corner once it is large enough to see.
73
+ - **Blanked pixels** (NaNs, e.g. outside a primary-beam cut) are drawn in a muted purple
74
+ so they are not mistaken for zero.
75
+
76
+ ## Zoomed out: what you can and can't trust
77
+
78
+ When the image is larger than the view, blocks of pixels are combined into one screen
79
+ pixel; none are skipped.
80
+
81
+ ![Block mean vs block max for the whole field](https://raw.githubusercontent.com/ianheywood/termfits/main/docs/images/mean_vs_max.png)
82
+
83
+ - **Block mean** (default) lowers the noise, so faint *extended* emission stands out,
84
+ but faint *compact* sources are diluted and can vanish.
85
+ - **Block max** (`b`) keeps every compact source visible, but it also lifts the
86
+ background: the maximum of many noise pixels sits around +3 sigma, so noise peaks
87
+ look like sources.
88
+
89
+ Use either for finding candidates, then press `1` to inspect them at full resolution.
90
+ For decisions about marginal detections, use a full-resolution viewer such as CARTA
91
+ or DS9.
92
+
93
+ ## Colour maps
94
+
95
+ ![The ten colour maps on the same field](https://raw.githubusercontent.com/ianheywood/termfits/main/docs/images/colourmaps.png)
96
+
97
+ `inverted` (default) and `gray`; `heat` (DS9-style); `inferno`, `magma`, `viridis` and
98
+ `cubehelix`, which are perceptually uniform and the best colour choices for faint
99
+ features; `cool`; `rainbow` (turbo-like, good for bright high-dynamic-range regions,
100
+ but it creates false contours in noise); and `diverging`, which is centred on the
101
+ median so negative sidelobes show blue and positive emission red.
102
+
103
+ Truecolour gives the full range; `--256` is a fallback for terminals without it, and
104
+ colour maps get coarse there (6 levels per channel).
105
+
106
+ ## Data support
107
+
108
+ - 2-D images, and cubes of any dimension: extra axes (FREQ, STOKES, ...) are
109
+ flattened into planes stepped with `[` and `]`.
110
+ - Multi-extension files: every image HDU is reachable with `Tab`.
111
+ - RA/Dec readout from the celestial WCS via astropy.
112
+ - Large images: one plane is loaded into memory as float32 (about 400 MB for
113
+ 10k x 10k). The first zoom-out builds the binned copies, which takes a few seconds;
114
+ after that it is quick.
115
+ - If astropy is missing, a built-in reader handles plain uncompressed images, without
116
+ WCS.
117
+
118
+ Not yet supported: per-channel beams stored in a CASA `BEAMS` table (the header beam is
119
+ used), and tile-compressed images without astropy.
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ git clone https://github.com/ianheywood/termfits
125
+ cd termfits
126
+ pip install -e ".[test]"
127
+ pytest
128
+ ```
129
+
130
+ The screenshots in `docs/images` were rendered from termfits' own output, using
131
+ synthetic data.
132
+
133
+ ## Licence
134
+
135
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "termfits"
7
+ dynamic = ["version"]
8
+ description = "A terminal FITS image viewer with zoom and pan, tuned for radio images"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Ian Heywood", email = "email.ian.heywood@gmail.com" }]
12
+ requires-python = ">=3.9"
13
+ dependencies = [
14
+ "numpy>=1.21",
15
+ "astropy>=5.0",
16
+ ]
17
+ keywords = ["fits", "astronomy", "radio astronomy", "terminal", "viewer", "tui"]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Environment :: Console",
21
+ "Intended Audience :: Science/Research",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: POSIX",
24
+ "Operating System :: MacOS",
25
+ "Programming Language :: Python :: 3",
26
+ "Topic :: Scientific/Engineering :: Astronomy",
27
+ "Topic :: Scientific/Engineering :: Visualization",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ test = ["pytest>=7"]
32
+
33
+ [project.scripts]
34
+ termfits = "termfits.viewer:main"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/ianheywood/termfits"
38
+ Issues = "https://github.com/ianheywood/termfits/issues"
39
+
40
+ [tool.setuptools.dynamic]
41
+ version = { attr = "termfits.__version__" }
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """termfits - a terminal FITS image viewer with zoom and pan, tuned for radio images."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .viewer import Viewer, main, robust_stats, CMAPS, STRETCHES # noqa: E402,F401
6
+
7
+ __all__ = ["Viewer", "main", "robust_stats", "CMAPS", "STRETCHES", "__version__"]
@@ -0,0 +1,4 @@
1
+ from .viewer import main
2
+
3
+ if __name__ == "__main__":
4
+ main()