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.
- lstcalc-0.1.0/CHANGELOG.md +23 -0
- lstcalc-0.1.0/CITATION.cff +20 -0
- lstcalc-0.1.0/LICENSE +21 -0
- lstcalc-0.1.0/MANIFEST.in +9 -0
- lstcalc-0.1.0/PKG-INFO +229 -0
- lstcalc-0.1.0/README.md +191 -0
- lstcalc-0.1.0/examples/quickstart.py +74 -0
- lstcalc-0.1.0/pyproject.toml +118 -0
- lstcalc-0.1.0/setup.cfg +4 -0
- lstcalc-0.1.0/src/lstcalc/__init__.py +117 -0
- lstcalc-0.1.0/src/lstcalc/__main__.py +7 -0
- lstcalc-0.1.0/src/lstcalc/_version.py +1 -0
- lstcalc-0.1.0/src/lstcalc/cli.py +212 -0
- lstcalc-0.1.0/src/lstcalc/emissivity.py +138 -0
- lstcalc-0.1.0/src/lstcalc/errors.py +17 -0
- lstcalc-0.1.0/src/lstcalc/io.py +90 -0
- lstcalc-0.1.0/src/lstcalc/lst.py +158 -0
- lstcalc-0.1.0/src/lstcalc/mtl.py +228 -0
- lstcalc-0.1.0/src/lstcalc/pipeline.py +159 -0
- lstcalc-0.1.0/src/lstcalc/py.typed +0 -0
- lstcalc-0.1.0/src/lstcalc/radiometry.py +199 -0
- lstcalc-0.1.0/src/lstcalc/uhi.py +212 -0
- lstcalc-0.1.0/src/lstcalc.egg-info/PKG-INFO +229 -0
- lstcalc-0.1.0/src/lstcalc.egg-info/SOURCES.txt +33 -0
- lstcalc-0.1.0/src/lstcalc.egg-info/dependency_links.txt +1 -0
- lstcalc-0.1.0/src/lstcalc.egg-info/entry_points.txt +2 -0
- lstcalc-0.1.0/src/lstcalc.egg-info/requires.txt +10 -0
- lstcalc-0.1.0/src/lstcalc.egg-info/top_level.txt +1 -0
- lstcalc-0.1.0/tests/conftest.py +79 -0
- lstcalc-0.1.0/tests/test_cli.py +170 -0
- lstcalc-0.1.0/tests/test_emissivity_lst.py +127 -0
- lstcalc-0.1.0/tests/test_mtl.py +90 -0
- lstcalc-0.1.0/tests/test_pipeline.py +93 -0
- lstcalc-0.1.0/tests/test_radiometry.py +105 -0
- 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.
|
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
|
+
[](https://pypi.org/project/lstcalc/)
|
|
42
|
+
[](https://pypi.org/project/lstcalc/)
|
|
43
|
+
[](https://github.com/YusufEminoglu/lstcalc_sdk/actions/workflows/ci.yml)
|
|
44
|
+
[](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).
|
lstcalc-0.1.0/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# lstcalc
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/lstcalc/)
|
|
4
|
+
[](https://pypi.org/project/lstcalc/)
|
|
5
|
+
[](https://github.com/YusufEminoglu/lstcalc_sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](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()
|