lstcalc 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.
Files changed (35) hide show
  1. lstcalc-0.1.0/CHANGELOG.md +23 -0
  2. lstcalc-0.1.0/CITATION.cff +20 -0
  3. lstcalc-0.1.0/LICENSE +21 -0
  4. lstcalc-0.1.0/MANIFEST.in +9 -0
  5. lstcalc-0.1.0/PKG-INFO +229 -0
  6. lstcalc-0.1.0/README.md +191 -0
  7. lstcalc-0.1.0/examples/quickstart.py +74 -0
  8. lstcalc-0.1.0/pyproject.toml +118 -0
  9. lstcalc-0.1.0/setup.cfg +4 -0
  10. lstcalc-0.1.0/src/lstcalc/__init__.py +117 -0
  11. lstcalc-0.1.0/src/lstcalc/__main__.py +7 -0
  12. lstcalc-0.1.0/src/lstcalc/_version.py +1 -0
  13. lstcalc-0.1.0/src/lstcalc/cli.py +212 -0
  14. lstcalc-0.1.0/src/lstcalc/emissivity.py +138 -0
  15. lstcalc-0.1.0/src/lstcalc/errors.py +17 -0
  16. lstcalc-0.1.0/src/lstcalc/io.py +90 -0
  17. lstcalc-0.1.0/src/lstcalc/lst.py +158 -0
  18. lstcalc-0.1.0/src/lstcalc/mtl.py +228 -0
  19. lstcalc-0.1.0/src/lstcalc/pipeline.py +159 -0
  20. lstcalc-0.1.0/src/lstcalc/py.typed +0 -0
  21. lstcalc-0.1.0/src/lstcalc/radiometry.py +199 -0
  22. lstcalc-0.1.0/src/lstcalc/uhi.py +212 -0
  23. lstcalc-0.1.0/src/lstcalc.egg-info/PKG-INFO +229 -0
  24. lstcalc-0.1.0/src/lstcalc.egg-info/SOURCES.txt +33 -0
  25. lstcalc-0.1.0/src/lstcalc.egg-info/dependency_links.txt +1 -0
  26. lstcalc-0.1.0/src/lstcalc.egg-info/entry_points.txt +2 -0
  27. lstcalc-0.1.0/src/lstcalc.egg-info/requires.txt +10 -0
  28. lstcalc-0.1.0/src/lstcalc.egg-info/top_level.txt +1 -0
  29. lstcalc-0.1.0/tests/conftest.py +79 -0
  30. lstcalc-0.1.0/tests/test_cli.py +170 -0
  31. lstcalc-0.1.0/tests/test_emissivity_lst.py +127 -0
  32. lstcalc-0.1.0/tests/test_mtl.py +90 -0
  33. lstcalc-0.1.0/tests/test_pipeline.py +93 -0
  34. lstcalc-0.1.0/tests/test_radiometry.py +105 -0
  35. lstcalc-0.1.0/tests/test_uhi.py +93 -0
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-09
8
+ ### Added
9
+ - `parse_mtl` / `read_mtl`: Landsat Collection 2 (and Collection 1) `MTL.txt` and `MTL.json`
10
+ parsing with group-aware accessors for radiance, TOA reflectance, surface reflectance,
11
+ thermal constants and Level-2 surface temperature coefficients.
12
+ - Radiometry: DN to radiance, TOA reflectance (sun-angle corrected), brightness temperature,
13
+ Level-2 `ST_B10` and `SR_Bn` scaling, Sentinel-2 reflectance, `block_mean` aggregation.
14
+ - NDVI, fractional vegetation cover and NDVI-threshold emissivity (Sobrino et al. 2004)
15
+ with overridable parameters.
16
+ - LST: generalised single-channel algorithm with Landsat 8 TIRS-1 coefficients
17
+ (Jimenez-Munoz et al. 2014), or with user atmospheric parameters; Planck
18
+ emissivity-corrected brightness temperature.
19
+ - `lst_from_level1` / `lst_from_level2` end-to-end array workflows.
20
+ - Urban heat island metrics: SUHI intensity, UTFVI with six-class evaluation, hot spots,
21
+ zonal statistics by label raster.
22
+ - `lstcalc` CLI (`mtl`, `scene`, `suhi`, `utfvi`, `zonal`); raster commands use the optional
23
+ `rasterio` extra.
@@ -0,0 +1,20 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ authors:
4
+ - family-names: "Eminoğlu"
5
+ given-names: "Yusuf"
6
+ email: "geospacephilo@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "lstcalc"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/lstcalc_sdk"
13
+ url: "https://pypi.org/project/lstcalc/"
14
+ abstract: "Land surface temperature (LST) and urban heat island metrics from Landsat 8/9 Collection 2 Level-1 and Level-2 data and Sentinel-2 NDVI on numpy arrays: MTL parsing, brightness temperature, NDVI-threshold emissivity, single-channel and Planck LST, SUHI intensity, UTFVI, hot spots and zonal statistics."
15
+ keywords:
16
+ - land surface temperature
17
+ - urban heat island
18
+ - landsat
19
+ - remote sensing
20
+ - gis
lstcalc-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yusuf Eminoğlu
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,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/lstcalc/py.typed
7
+ recursive-include src/lstcalc *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
lstcalc-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: lstcalc
3
+ Version: 0.1.0
4
+ Summary: Land surface temperature (LST) and urban heat island metrics from Landsat 8/9 Collection 2 and Sentinel-2 NDVI on numpy arrays: MTL parsing, brightness temperature, NDVI-threshold emissivity, single-channel and Planck LST, Level-2 ST scaling, SUHI, UTFVI, hot spots and zonal statistics.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/lstcalc_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/lstcalc_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/lstcalc_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/lstcalc_sdk/blob/main/CHANGELOG.md
11
+ Keywords: land-surface-temperature,lst,urban-heat-island,suhi,utfvi,landsat,landsat-8,landsat-9,sentinel-2,ndvi,emissivity,remote-sensing,thermal-infrared,gis
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Environment :: Console
21
+ Classifier: Topic :: Scientific/Engineering :: GIS
22
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
23
+ Classifier: Intended Audience :: Science/Research
24
+ Classifier: Intended Audience :: Developers
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.21
30
+ Provides-Extra: raster
31
+ Requires-Dist: rasterio>=1.2; extra == "raster"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
34
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
36
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # lstcalc
40
+
41
+ [![PyPI version](https://img.shields.io/pypi/v/lstcalc.svg)](https://pypi.org/project/lstcalc/)
42
+ [![Python versions](https://img.shields.io/pypi/pyversions/lstcalc.svg)](https://pypi.org/project/lstcalc/)
43
+ [![CI](https://github.com/YusufEminoglu/lstcalc_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/lstcalc_sdk/actions/workflows/ci.yml)
44
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/lstcalc_sdk/blob/main/LICENSE)
45
+
46
+ **Land surface temperature and urban heat island metrics from Landsat 8/9, on numpy arrays.**
47
+
48
+ `lstcalc` takes you from Landsat 8/9 Collection 2 digital numbers to land surface temperature
49
+ (LST) and on to the usual urban heat island indicators, with every formula traceable to its
50
+ source:
51
+
52
+ - **MTL parsing**: `MTL.txt` and `MTL.json`, group-aware (Level-1 and Level-2 coefficients that
53
+ share a key name are kept apart).
54
+ - **Radiometry**: DN to TOA radiance, TOA reflectance (sun-angle corrected), brightness
55
+ temperature; Level-2 `ST_B10` and `SR_Bn` scaling; Sentinel-2 reflectance.
56
+ - **Vegetation and emissivity**: NDVI, fractional vegetation cover and the NDVI threshold
57
+ emissivity method (Sobrino et al. 2004).
58
+ - **LST**: generalised single-channel algorithm with the Landsat 8 TIRS band 10 coefficients
59
+ (Jimenez-Munoz et al. 2014), and the emissivity-corrected Planck method.
60
+ - **Urban heat island**: SUHI intensity from urban/rural masks, UTFVI and its six-class
61
+ evaluation, hot spots, zonal statistics by a label raster.
62
+ - **CLI**: `lstcalc scene <folder>` processes a whole scene folder when `rasterio` is installed.
63
+
64
+ The core depends only on numpy. Raster I/O is an optional extra.
65
+
66
+ ```bash
67
+ pip install lstcalc # numpy only, works on arrays
68
+ pip install "lstcalc[raster]" # + rasterio for GeoTIFF I/O and the scene CLI
69
+ ```
70
+
71
+ Requires Python 3.9 or newer.
72
+
73
+ ## Quick start
74
+
75
+ ```python
76
+ import numpy as np
77
+ from lstcalc import read_mtl, lst_from_level1, suhi_intensity, utfvi, classify_utfvi
78
+
79
+ mtl = read_mtl("LC08_L1TP_180032_20230715_20230725_02_T1_MTL.txt")
80
+ # b10, b4, b5: numpy arrays of the TIRS band 10 and OLI band 4/5 DNs (same 30 m grid)
81
+ res = lst_from_level1(b10, mtl, red_dn=b4, nir_dn=b5, water_vapour=1.4) # w in g/cm2
82
+
83
+ print(np.nanmean(res.lst_celsius)) # LST in degC
84
+ res.brightness_temperature, res.emissivity # intermediate products are kept
85
+
86
+ suhi = suhi_intensity(res.lst, urban_mask, rural_mask)
87
+ print(suhi.intensity, suhi.urban_mean, suhi.rural_mean)
88
+
89
+ classes = classify_utfvi(utfvi(res.lst)) # 0 = excellent ... 5 = worst
90
+ ```
91
+
92
+ Level-2 surface temperature needs no algorithm, just scaling:
93
+
94
+ ```python
95
+ from lstcalc import lst_from_level2
96
+ st_kelvin = lst_from_level2(st_b10_dn, mtl) # 0.00341802 * DN + 149.0, fill (0) -> NaN
97
+ ```
98
+
99
+ Sentinel-2 NDVI (10 m) can replace the Landsat NDVI once it is on the Landsat grid:
100
+
101
+ ```python
102
+ from lstcalc import sentinel2_reflectance, ndvi, block_mean
103
+ s2_ndvi = ndvi(sentinel2_reflectance(b08), sentinel2_reflectance(b04))
104
+ s2_ndvi_30m = block_mean(s2_ndvi, 3) # only if the grids are co-registered
105
+ res = lst_from_level1(b10, mtl, ndvi=s2_ndvi_30m, method="planck")
106
+ ```
107
+
108
+ A self-contained example on synthetic data is in [`examples/quickstart.py`](examples/quickstart.py).
109
+
110
+ ## Command line
111
+
112
+ ```bash
113
+ lstcalc mtl LC08_..._MTL.txt # coefficients as JSON (no rasterio)
114
+ lstcalc scene ./LC08_scene --out lst.tif --water-vapour 1.4 --celsius
115
+ lstcalc scene ./LC08_L2SP_scene --out lst.tif # auto: Level-2 ST_B10 scaling
116
+ lstcalc suhi lst.tif --urban urban.tif --rural rural.tif
117
+ lstcalc utfvi lst.tif --out utfvi_class.tif --hotspots-out hot.tif
118
+ lstcalc zonal lst.tif --labels districts.tif --csv districts.csv
119
+ ```
120
+
121
+ `scene --method auto` uses Level-2 `ST_B10` if the folder has it, otherwise the
122
+ single-channel method when `--water-vapour` is given, otherwise the Planck method.
123
+
124
+ ## API overview
125
+
126
+ | Area | Functions |
127
+ | --- | --- |
128
+ | Metadata | `parse_mtl`, `read_mtl`, `MTL.radiance_coeffs`, `.reflectance_coeffs`, `.surface_reflectance_coeffs`, `.thermal_constants`, `.surface_temperature_coeffs`, `.sun_elevation`, `.summary` |
129
+ | Radiometry | `dn_to_radiance`, `dn_to_toa_reflectance`, `brightness_temperature`, `l2_surface_temperature`, `l2_surface_reflectance`, `sentinel2_reflectance`, `kelvin_to_celsius`, `block_mean` |
130
+ | Emissivity | `ndvi`, `fractional_vegetation_cover`, `emissivity_ndvi_threshold`, `EmissivityParams` |
131
+ | LST | `single_channel_lst`, `atmospheric_functions`, `atmospheric_functions_from_parameters`, `planck_lst`, `lst_from_level1`, `lst_from_level2` |
132
+ | Heat island | `suhi_intensity`, `utfvi`, `classify_utfvi`, `hotspots`, `zonal_statistics` |
133
+ | Raster I/O (extra) | `lstcalc.io.read_band`, `write_band`, `find_mtl`, `find_band_file` |
134
+
135
+ All array functions return `float64`, propagate `NaN` and treat DN `0` as fill by default.
136
+
137
+ ## Methods and sources
138
+
139
+ **Radiometric conversion** (USGS, *Using the USGS Landsat Level-1 Data Product*):
140
+ `L = M_L * Q_cal + A_L`, `rho = (M_rho * Q_cal + A_rho) / sin(theta_SE)`,
141
+ `BT = K2 / ln(K1 / L + 1)`, with all coefficients read from the MTL file.
142
+
143
+ **Level-2 scaling** (USGS FAQ *How do I use a scale factor with Landsat Level-2 science
144
+ products?*, checked 2026-11-09): surface temperature `0.00341802 * DN + 149.0` K, surface
145
+ reflectance `0.0000275 * DN - 0.2`.
146
+
147
+ **Emissivity, NDVI threshold method** (Sobrino, Jimenez-Munoz & Paolini 2004, *Remote Sensing
148
+ of Environment* 90, 434-440, checked against the paper):
149
+
150
+ | NDVI | Emissivity |
151
+ | --- | --- |
152
+ | `< 0.2` | bare soil, `e_s` (default 0.97) or `a + b * rho_red` if you supply `(a, b)` |
153
+ | `0.2 - 0.5` | `e_v Pv + e_s (1 - Pv) + (1 - e_s)(1 - Pv) F e_v`, `F = 0.55` |
154
+ | `> 0.5` | `e_v` (0.99) |
155
+
156
+ with `Pv = ((NDVI - 0.2) / (0.5 - 0.2))^2`. The defaults give the paper's
157
+ `e = 0.004 Pv + 0.986` (eq. 13). Every value is a field of `EmissivityParams`.
158
+
159
+ **Single-channel LST** (Jimenez-Munoz & Sobrino 2003, *JGR* 108(D22); Landsat 8 coefficients
160
+ from Jimenez-Munoz, Sobrino, Skokovic, Mattar & Cristobal 2014, *IEEE GRSL* 11(10), 1840-1843,
161
+ checked against the paper):
162
+
163
+ ```
164
+ Ts = gamma * [ (psi1 * L + psi2) / e + psi3 ] + delta
165
+ gamma = T^2 / (b_gamma * L), delta = T - T^2 / b_gamma, b_gamma = 1324 K (TIRS-1)
166
+ psi = C [w^2, w, 1]^T, C = [[ 0.04019, 0.02916, 1.01523],
167
+ [-0.38333, -1.50294, 0.20324],
168
+ [ 0.00918, 1.36072, -0.27514]]
169
+ ```
170
+
171
+ `w` is total column water vapour in g cm-2. If you have radiative-transfer outputs (for
172
+ example from the NASA Atmospheric Correction Parameter Calculator) use
173
+ `atmospheric_functions_from_parameters(tau, Lu, Ld)` instead: `psi1 = 1/tau`,
174
+ `psi2 = -Ld - Lu/tau`, `psi3 = Ld`.
175
+
176
+ **Planck method** (Artis & Carnahan 1982, *Remote Sensing of Environment* 12, 313-329):
177
+ `LST = BT / (1 + (lambda BT / rho) ln e)`, `rho = h c / k_B = 14387.77 um K`,
178
+ `lambda = 10.904 um` (TIRS-1 effective wavelength, Jimenez-Munoz et al. 2014).
179
+
180
+ **SUHI intensity**: mean LST over the urban mask minus mean LST over the rural mask
181
+ (e.g. Peng et al. 2012, *Environ. Sci. Technol.* 46, 696-703). The masks are yours to define.
182
+
183
+ **UTFVI** (Liu & Zhang 2011, *Remote Sensing* 3, 1535-1552): `(Ts - T_mean) / Ts`, classified
184
+ with the breaks 0, 0.005, 0.010, 0.015, 0.020 into none/excellent ... strongest/worst.
185
+
186
+ **Hot spots**: `LST > mean + 2 std` (Guha et al. 2018, *European Journal of Remote Sensing*
187
+ 51, 667-678); `k` is a parameter.
188
+
189
+ ## Limitations
190
+
191
+ - The single-channel water-vapour coefficients are for Landsat 8 TIRS band 10. For band 11, or
192
+ for Landsat 9, pass your own `coeffs`/`b_gamma` or atmospheric parameters. The authors note
193
+ that the water-vapour fit degrades for `w > 3 g cm-2`.
194
+ - The default emissivities are the Sobrino et al. (2004) TM6 values, commonly reused for
195
+ TIRS band 10; supply band-specific values through `EmissivityParams` if you have them.
196
+ Water, snow and built-up materials are not modelled separately unless you set
197
+ `water_emissivity`.
198
+ - NDVI from TOA reflectance (as in `lst_from_level1`) is not atmospherically corrected.
199
+ - `block_mean` does not reproject; Sentinel-2 and Landsat grids must already be aligned.
200
+ - No cloud masking: apply the QA_PIXEL band yourself before computing statistics.
201
+ - The UTFVI class breaks, the Sentinel-2 offset (-1000 from processing baseline 04.00) and
202
+ quantification value (10000), and the fallback `THERMAL_CONSTANTS` table were not checked
203
+ against a primary source for this release; all are parameters you can override.
204
+
205
+ ## Development
206
+
207
+ ```bash
208
+ pip install -e ".[dev,raster]"
209
+ python -m pytest
210
+ python -m ruff check . && python -m mypy src
211
+ ```
212
+
213
+ ## Academic citation
214
+
215
+ ```bibtex
216
+ @software{eminoglu_lstcalc,
217
+ author = {Eminoğlu, Yusuf},
218
+ title = {lstcalc},
219
+ year = {2026},
220
+ version = {0.1.0},
221
+ url = {https://github.com/YusufEminoglu/lstcalc_sdk}
222
+ }
223
+ ```
224
+
225
+ See also [`CITATION.cff`](CITATION.cff).
226
+
227
+ ## License
228
+
229
+ MIT License. Copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
@@ -0,0 +1,191 @@
1
+ # lstcalc
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/lstcalc.svg)](https://pypi.org/project/lstcalc/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/lstcalc.svg)](https://pypi.org/project/lstcalc/)
5
+ [![CI](https://github.com/YusufEminoglu/lstcalc_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/lstcalc_sdk/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/lstcalc_sdk/blob/main/LICENSE)
7
+
8
+ **Land surface temperature and urban heat island metrics from Landsat 8/9, on numpy arrays.**
9
+
10
+ `lstcalc` takes you from Landsat 8/9 Collection 2 digital numbers to land surface temperature
11
+ (LST) and on to the usual urban heat island indicators, with every formula traceable to its
12
+ source:
13
+
14
+ - **MTL parsing**: `MTL.txt` and `MTL.json`, group-aware (Level-1 and Level-2 coefficients that
15
+ share a key name are kept apart).
16
+ - **Radiometry**: DN to TOA radiance, TOA reflectance (sun-angle corrected), brightness
17
+ temperature; Level-2 `ST_B10` and `SR_Bn` scaling; Sentinel-2 reflectance.
18
+ - **Vegetation and emissivity**: NDVI, fractional vegetation cover and the NDVI threshold
19
+ emissivity method (Sobrino et al. 2004).
20
+ - **LST**: generalised single-channel algorithm with the Landsat 8 TIRS band 10 coefficients
21
+ (Jimenez-Munoz et al. 2014), and the emissivity-corrected Planck method.
22
+ - **Urban heat island**: SUHI intensity from urban/rural masks, UTFVI and its six-class
23
+ evaluation, hot spots, zonal statistics by a label raster.
24
+ - **CLI**: `lstcalc scene <folder>` processes a whole scene folder when `rasterio` is installed.
25
+
26
+ The core depends only on numpy. Raster I/O is an optional extra.
27
+
28
+ ```bash
29
+ pip install lstcalc # numpy only, works on arrays
30
+ pip install "lstcalc[raster]" # + rasterio for GeoTIFF I/O and the scene CLI
31
+ ```
32
+
33
+ Requires Python 3.9 or newer.
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ import numpy as np
39
+ from lstcalc import read_mtl, lst_from_level1, suhi_intensity, utfvi, classify_utfvi
40
+
41
+ mtl = read_mtl("LC08_L1TP_180032_20230715_20230725_02_T1_MTL.txt")
42
+ # b10, b4, b5: numpy arrays of the TIRS band 10 and OLI band 4/5 DNs (same 30 m grid)
43
+ res = lst_from_level1(b10, mtl, red_dn=b4, nir_dn=b5, water_vapour=1.4) # w in g/cm2
44
+
45
+ print(np.nanmean(res.lst_celsius)) # LST in degC
46
+ res.brightness_temperature, res.emissivity # intermediate products are kept
47
+
48
+ suhi = suhi_intensity(res.lst, urban_mask, rural_mask)
49
+ print(suhi.intensity, suhi.urban_mean, suhi.rural_mean)
50
+
51
+ classes = classify_utfvi(utfvi(res.lst)) # 0 = excellent ... 5 = worst
52
+ ```
53
+
54
+ Level-2 surface temperature needs no algorithm, just scaling:
55
+
56
+ ```python
57
+ from lstcalc import lst_from_level2
58
+ st_kelvin = lst_from_level2(st_b10_dn, mtl) # 0.00341802 * DN + 149.0, fill (0) -> NaN
59
+ ```
60
+
61
+ Sentinel-2 NDVI (10 m) can replace the Landsat NDVI once it is on the Landsat grid:
62
+
63
+ ```python
64
+ from lstcalc import sentinel2_reflectance, ndvi, block_mean
65
+ s2_ndvi = ndvi(sentinel2_reflectance(b08), sentinel2_reflectance(b04))
66
+ s2_ndvi_30m = block_mean(s2_ndvi, 3) # only if the grids are co-registered
67
+ res = lst_from_level1(b10, mtl, ndvi=s2_ndvi_30m, method="planck")
68
+ ```
69
+
70
+ A self-contained example on synthetic data is in [`examples/quickstart.py`](examples/quickstart.py).
71
+
72
+ ## Command line
73
+
74
+ ```bash
75
+ lstcalc mtl LC08_..._MTL.txt # coefficients as JSON (no rasterio)
76
+ lstcalc scene ./LC08_scene --out lst.tif --water-vapour 1.4 --celsius
77
+ lstcalc scene ./LC08_L2SP_scene --out lst.tif # auto: Level-2 ST_B10 scaling
78
+ lstcalc suhi lst.tif --urban urban.tif --rural rural.tif
79
+ lstcalc utfvi lst.tif --out utfvi_class.tif --hotspots-out hot.tif
80
+ lstcalc zonal lst.tif --labels districts.tif --csv districts.csv
81
+ ```
82
+
83
+ `scene --method auto` uses Level-2 `ST_B10` if the folder has it, otherwise the
84
+ single-channel method when `--water-vapour` is given, otherwise the Planck method.
85
+
86
+ ## API overview
87
+
88
+ | Area | Functions |
89
+ | --- | --- |
90
+ | Metadata | `parse_mtl`, `read_mtl`, `MTL.radiance_coeffs`, `.reflectance_coeffs`, `.surface_reflectance_coeffs`, `.thermal_constants`, `.surface_temperature_coeffs`, `.sun_elevation`, `.summary` |
91
+ | Radiometry | `dn_to_radiance`, `dn_to_toa_reflectance`, `brightness_temperature`, `l2_surface_temperature`, `l2_surface_reflectance`, `sentinel2_reflectance`, `kelvin_to_celsius`, `block_mean` |
92
+ | Emissivity | `ndvi`, `fractional_vegetation_cover`, `emissivity_ndvi_threshold`, `EmissivityParams` |
93
+ | LST | `single_channel_lst`, `atmospheric_functions`, `atmospheric_functions_from_parameters`, `planck_lst`, `lst_from_level1`, `lst_from_level2` |
94
+ | Heat island | `suhi_intensity`, `utfvi`, `classify_utfvi`, `hotspots`, `zonal_statistics` |
95
+ | Raster I/O (extra) | `lstcalc.io.read_band`, `write_band`, `find_mtl`, `find_band_file` |
96
+
97
+ All array functions return `float64`, propagate `NaN` and treat DN `0` as fill by default.
98
+
99
+ ## Methods and sources
100
+
101
+ **Radiometric conversion** (USGS, *Using the USGS Landsat Level-1 Data Product*):
102
+ `L = M_L * Q_cal + A_L`, `rho = (M_rho * Q_cal + A_rho) / sin(theta_SE)`,
103
+ `BT = K2 / ln(K1 / L + 1)`, with all coefficients read from the MTL file.
104
+
105
+ **Level-2 scaling** (USGS FAQ *How do I use a scale factor with Landsat Level-2 science
106
+ products?*, checked 2026-11-09): surface temperature `0.00341802 * DN + 149.0` K, surface
107
+ reflectance `0.0000275 * DN - 0.2`.
108
+
109
+ **Emissivity, NDVI threshold method** (Sobrino, Jimenez-Munoz & Paolini 2004, *Remote Sensing
110
+ of Environment* 90, 434-440, checked against the paper):
111
+
112
+ | NDVI | Emissivity |
113
+ | --- | --- |
114
+ | `< 0.2` | bare soil, `e_s` (default 0.97) or `a + b * rho_red` if you supply `(a, b)` |
115
+ | `0.2 - 0.5` | `e_v Pv + e_s (1 - Pv) + (1 - e_s)(1 - Pv) F e_v`, `F = 0.55` |
116
+ | `> 0.5` | `e_v` (0.99) |
117
+
118
+ with `Pv = ((NDVI - 0.2) / (0.5 - 0.2))^2`. The defaults give the paper's
119
+ `e = 0.004 Pv + 0.986` (eq. 13). Every value is a field of `EmissivityParams`.
120
+
121
+ **Single-channel LST** (Jimenez-Munoz & Sobrino 2003, *JGR* 108(D22); Landsat 8 coefficients
122
+ from Jimenez-Munoz, Sobrino, Skokovic, Mattar & Cristobal 2014, *IEEE GRSL* 11(10), 1840-1843,
123
+ checked against the paper):
124
+
125
+ ```
126
+ Ts = gamma * [ (psi1 * L + psi2) / e + psi3 ] + delta
127
+ gamma = T^2 / (b_gamma * L), delta = T - T^2 / b_gamma, b_gamma = 1324 K (TIRS-1)
128
+ psi = C [w^2, w, 1]^T, C = [[ 0.04019, 0.02916, 1.01523],
129
+ [-0.38333, -1.50294, 0.20324],
130
+ [ 0.00918, 1.36072, -0.27514]]
131
+ ```
132
+
133
+ `w` is total column water vapour in g cm-2. If you have radiative-transfer outputs (for
134
+ example from the NASA Atmospheric Correction Parameter Calculator) use
135
+ `atmospheric_functions_from_parameters(tau, Lu, Ld)` instead: `psi1 = 1/tau`,
136
+ `psi2 = -Ld - Lu/tau`, `psi3 = Ld`.
137
+
138
+ **Planck method** (Artis & Carnahan 1982, *Remote Sensing of Environment* 12, 313-329):
139
+ `LST = BT / (1 + (lambda BT / rho) ln e)`, `rho = h c / k_B = 14387.77 um K`,
140
+ `lambda = 10.904 um` (TIRS-1 effective wavelength, Jimenez-Munoz et al. 2014).
141
+
142
+ **SUHI intensity**: mean LST over the urban mask minus mean LST over the rural mask
143
+ (e.g. Peng et al. 2012, *Environ. Sci. Technol.* 46, 696-703). The masks are yours to define.
144
+
145
+ **UTFVI** (Liu & Zhang 2011, *Remote Sensing* 3, 1535-1552): `(Ts - T_mean) / Ts`, classified
146
+ with the breaks 0, 0.005, 0.010, 0.015, 0.020 into none/excellent ... strongest/worst.
147
+
148
+ **Hot spots**: `LST > mean + 2 std` (Guha et al. 2018, *European Journal of Remote Sensing*
149
+ 51, 667-678); `k` is a parameter.
150
+
151
+ ## Limitations
152
+
153
+ - The single-channel water-vapour coefficients are for Landsat 8 TIRS band 10. For band 11, or
154
+ for Landsat 9, pass your own `coeffs`/`b_gamma` or atmospheric parameters. The authors note
155
+ that the water-vapour fit degrades for `w > 3 g cm-2`.
156
+ - The default emissivities are the Sobrino et al. (2004) TM6 values, commonly reused for
157
+ TIRS band 10; supply band-specific values through `EmissivityParams` if you have them.
158
+ Water, snow and built-up materials are not modelled separately unless you set
159
+ `water_emissivity`.
160
+ - NDVI from TOA reflectance (as in `lst_from_level1`) is not atmospherically corrected.
161
+ - `block_mean` does not reproject; Sentinel-2 and Landsat grids must already be aligned.
162
+ - No cloud masking: apply the QA_PIXEL band yourself before computing statistics.
163
+ - The UTFVI class breaks, the Sentinel-2 offset (-1000 from processing baseline 04.00) and
164
+ quantification value (10000), and the fallback `THERMAL_CONSTANTS` table were not checked
165
+ against a primary source for this release; all are parameters you can override.
166
+
167
+ ## Development
168
+
169
+ ```bash
170
+ pip install -e ".[dev,raster]"
171
+ python -m pytest
172
+ python -m ruff check . && python -m mypy src
173
+ ```
174
+
175
+ ## Academic citation
176
+
177
+ ```bibtex
178
+ @software{eminoglu_lstcalc,
179
+ author = {Eminoğlu, Yusuf},
180
+ title = {lstcalc},
181
+ year = {2026},
182
+ version = {0.1.0},
183
+ url = {https://github.com/YusufEminoglu/lstcalc_sdk}
184
+ }
185
+ ```
186
+
187
+ See also [`CITATION.cff`](CITATION.cff).
188
+
189
+ ## License
190
+
191
+ MIT License. Copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
@@ -0,0 +1,74 @@
1
+ """lstcalc quick start on a synthetic Landsat 8 scene (no files or network needed).
2
+
3
+ Run with: python examples/quickstart.py
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import numpy as np
9
+
10
+ from lstcalc import (
11
+ classify_utfvi,
12
+ hotspots,
13
+ lst_from_level1,
14
+ parse_mtl,
15
+ suhi_intensity,
16
+ utfvi,
17
+ zonal_statistics,
18
+ )
19
+
20
+ MTL = """GROUP = LANDSAT_METADATA_FILE
21
+ GROUP = IMAGE_ATTRIBUTES
22
+ SPACECRAFT_ID = "LANDSAT_8"
23
+ SUN_ELEVATION = 55.0
24
+ END_GROUP = IMAGE_ATTRIBUTES
25
+ GROUP = LEVEL1_RADIOMETRIC_RESCALING
26
+ RADIANCE_MULT_BAND_10 = 3.3420E-04
27
+ RADIANCE_ADD_BAND_10 = 0.10000
28
+ REFLECTANCE_MULT_BAND_4 = 2.0000E-05
29
+ REFLECTANCE_ADD_BAND_4 = -0.100000
30
+ REFLECTANCE_MULT_BAND_5 = 2.0000E-05
31
+ REFLECTANCE_ADD_BAND_5 = -0.100000
32
+ END_GROUP = LEVEL1_RADIOMETRIC_RESCALING
33
+ GROUP = LEVEL1_THERMAL_CONSTANTS
34
+ K1_CONSTANT_BAND_10 = 774.8853
35
+ K2_CONSTANT_BAND_10 = 1321.0789
36
+ END_GROUP = LEVEL1_THERMAL_CONSTANTS
37
+ END_GROUP = LANDSAT_METADATA_FILE
38
+ END
39
+ """
40
+
41
+
42
+ def main() -> None:
43
+ rng = np.random.default_rng(42)
44
+ shape = (60, 60)
45
+ city = np.zeros(shape, dtype=bool)
46
+ city[20:40, 20:40] = True
47
+
48
+ # Synthetic DNs: the city core is warmer and less vegetated than its surroundings.
49
+ b10 = np.where(city, 31500, 29000) + rng.integers(-300, 300, shape)
50
+ b4 = np.where(city, 11000, 8000) + rng.integers(-200, 200, shape)
51
+ b5 = np.where(city, 14000, 22000) + rng.integers(-200, 200, shape)
52
+
53
+ mtl = parse_mtl(MTL)
54
+ res = lst_from_level1(b10, mtl, red_dn=b4, nir_dn=b5, water_vapour=1.5)
55
+ print(f"mean LST: {np.nanmean(res.lst_celsius):.2f} degC")
56
+ print(f"mean emissivity urban/rural: {res.emissivity[city].mean():.4f} / "
57
+ f"{res.emissivity[~city].mean():.4f}") # fmt: skip
58
+
59
+ rural = np.zeros(shape, dtype=bool)
60
+ rural[:10, :] = True
61
+ suhi = suhi_intensity(res.lst, city, rural)
62
+ print(f"SUHI intensity: {suhi.intensity:.2f} K")
63
+
64
+ classes = classify_utfvi(utfvi(res.lst))
65
+ print("UTFVI class counts:", np.bincount(classes[classes >= 0], minlength=6).tolist())
66
+ print("hot-spot pixels:", int(hotspots(res.lst).sum()))
67
+
68
+ zones = city.astype(int) + 1 # 1 = periphery, 2 = core
69
+ for label, stats in zonal_statistics(res.lst_celsius, zones).items():
70
+ print(f"zone {label}: mean {stats.mean:.2f} degC (n={stats.count})")
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()