Skip to content

Change detection

Differences between two dates of a habitat map: pixel transitions, structural change without edge noise, and the change of fragmentation and of landscape mosaics. Tutorial: change detection.

All functions take the two dates on the same grid. The change of connectivity indices over time is in ls.conn.eca_change.

Habitat change

simple_change

simple_change(
    t1: DataArray | ndarray | str | Path, t2: DataArray | ndarray | str | Path
) -> SimpleChangeResult

Pixel-by-pixel change of a habitat map between two dates.

Every pixel gets one of four transitions: stable background (11), gain (12), loss (21) or stable foreground (22); 255 where either date is missing.

Parameters:

Name Type Description Default
t1 DataArray | ndarray | str | Path

Maps of the two dates, coded 0 missing, 1 background, 2 foreground (DataArray, array or path), on the same grid.

required
t2 DataArray | ndarray | str | Path

Maps of the two dates, coded 0 missing, 1 background, 2 foreground (DataArray, array or path), on the same grid.

required

Returns:

Type Description
SimpleChangeResult
See Also

morphological_change : separates structural change from edge noise.

Examples:

>>> import landschaft as ls
>>> before = ls.load_raster(ls.example_data("input"))   # 0 missing, 1 background, 2 forest
>>> after = before.copy()
>>> cut = after.values[600:700, 100:260]
>>> cut[cut == 2] = 1                                   # a clear-cut
>>> plantation = after.values[250:330, 420:520]
>>> plantation[plantation == 1] = 2                     # a new plantation
>>> change = ls.simple_change(before, after)
>>> change.gain, change.loss, change.net_change
(6839, 5510, 1329)

SimpleChangeResult dataclass

SimpleChangeResult(
    change_map: DataArray | ndarray,
    stable_bg: int,
    gain: int,
    loss: int,
    stable_fg: int,
    missing: int,
    total_valid: int,
)

Bases: Saveable

Result of simple_change().

Attributes:

Name Type Description
change_map DataArray or ndarray

Byte raster with pixel values: 11 = Stable Background, 12 = Gain, 21 = Loss, 22 = Stable Foreground, 255 = Missing.

stable_bg int

Pixels of stable background.

gain int

Pixels of foreground gain.

loss int

Pixels of foreground loss.

stable_fg int

Pixels of stable foreground.

missing int

Pixels missing at one or both times.

total_valid int

Total valid pixels (stable_bg + gain + loss + stable_fg).

Examples:

>>> import landschaft as ls
>>> before = ls.load_raster(ls.example_data("input"))   # 0 missing, 1 background, 2 forest
>>> after = before.copy()
>>> cut = after.values[600:700, 100:260]
>>> cut[cut == 2] = 1                                   # a clear-cut
>>> plantation = after.values[250:330, 420:520]
>>> plantation[plantation == 1] = 2                     # a new plantation
>>> print(ls.simple_change(before, after).summary())
Simple Change Analysis Summary (A -> B)
...
Net Change [pixels]           : +1,329
...

net_change property

net_change: int

Net foreground change in pixels (gain - loss).

summary

summary() -> str

Text summary of the change.

Returns:

Type Description
str

morphological_change

morphological_change(
    t1: DataArray | ndarray | str | Path,
    t2: DataArray | ndarray | str | Path,
    *,
    n_threads: int | None = None,
) -> MorphologicalChangeResult

Change of a habitat map that separates real structural change from edge noise (GTB's Morphological Change Detection).

Small shifts along patch edges (e.g. from slightly different classifications of the two dates) are "spurious" (176); gains and losses that change the shape of the habitat are kept (12, 21). The method finds the cores of each date (pixels more than 2 pixels from the edge), reconstructs them by geodesic dilation and closes the result with a 3 x 3 window.

The landscape elasticity compares the change of the core interior with the total change.

Parameters:

Name Type Description Default
t1 DataArray | ndarray | str | Path

Maps of the two dates, coded 0 missing, 1 background, 2 foreground (DataArray, array or path).

required
t2 DataArray | ndarray | str | Path

Maps of the two dates, coded 0 missing, 1 background, 2 foreground (DataArray, array or path).

required
n_threads int | None

Threads (0 or None: all cores but one).

None

Returns:

Type Description
MorphologicalChangeResult

Examples:

>>> import landschaft as ls
>>> before = ls.load_raster(ls.example_data("input"))   # 0 missing, 1 background, 2 forest
>>> after = before.copy()
>>> cut = after.values[600:700, 100:260]
>>> cut[cut == 2] = 1                                   # a clear-cut
>>> plantation = after.values[250:330, 420:520]
>>> plantation[plantation == 1] = 2                     # a new plantation
>>> change = ls.morphological_change(before, after)
>>> change.mcd_gain, change.mcd_loss, change.spurious
(7348, 5504, 395)

MorphologicalChangeResult dataclass

MorphologicalChangeResult(
    change_map: DataArray | ndarray,
    fg_area_t1: int,
    fg_area_t2: int,
    net_change: int,
    stable_bg: int,
    stable_fg: int,
    mcd_gain: int,
    mcd_loss: int,
    spurious: int,
    missing: int,
    elasticity: float,
)

Bases: Saveable

Result of morphological_change().

Attributes:

Name Type Description
change_map DataArray or ndarray

Byte raster with pixel values: 11 = Stable BG, 12 = MCD Gain, 21 = MCD Loss, 22 = Stable FG, 176 = Spurious boundary chatter, 255 = Missing.

fg_area_t1 int

Total foreground area at time 1.

fg_area_t2 int

Total foreground area at time 2.

net_change int

Net foreground change (T2 - T1).

stable_bg int

Stable background pixels.

stable_fg int

Stable foreground pixels.

mcd_gain int

Structural foreground gain pixels.

mcd_loss int

Structural foreground loss pixels.

spurious int

Boundary chatter / spurious change pixels.

missing int

Missing pixels at one or both times.

elasticity float

Landscape elasticity index (ratio of core interior change to total change).

summary

summary() -> str

Text summary of the change.

Returns:

Type Description
str

Fragmentation change

fos_change

fos_change(
    t1: DataArray | ndarray | str | Path,
    t2: DataArray | ndarray | str | Path,
    *,
    n_threads: int | None = None,
) -> FOSChangeResult

Change of fragmentation between two dates (GTB's FOS change).

Compares two fos() maps: for the foreground at both dates, the shift of its fragmentation class (a 6 x 6 transition matrix from Rare to Intact), and whether it became more or less fragmented; gains and losses of foreground are marked separately.

Parameters:

Name Type Description Default
t1 DataArray | ndarray | str | Path

FOS maps of the two dates (fos(...).map: 0-100 foreground density, 101 background, 102 missing, 105 water).

required
t2 DataArray | ndarray | str | Path

FOS maps of the two dates (fos(...).map: 0-100 foreground density, 101 background, 102 missing, 105 water).

required
n_threads int | None

Threads (0 or None: all cores but one).

None

Returns:

Type Description
FOSChangeResult

Examples:

>>> import landschaft as ls
>>> before = ls.load_raster(ls.example_data("input"))   # 0 missing, 1 background, 2 forest
>>> after = before.copy()
>>> cut = after.values[600:700, 100:260]
>>> cut[cut == 2] = 1                                   # a clear-cut
>>> plantation = after.values[250:330, 420:520]
>>> plantation[plantation == 1] = 2                     # a new plantation
>>> shift = ls.fos_change(ls.fos(before, window_size=27).map, ls.fos(after, window_size=27).map)
>>> round(shift.frag_increase_pct, 2), round(shift.frag_decrease_pct, 2)
(54.71, 45.29)
>>> shift.matrix_6x6.shape
(6, 6)

FOSChangeResult dataclass

FOSChangeResult(
    change_map: DataArray | ndarray,
    fg_gain: int,
    fg_loss: int,
    stable_bg: int,
    forcom: int,
    diag_same_class: int,
    diff_class: int,
    frag_decrease_pct: float,
    frag_increase_pct: float,
    matrix_6x6: DataFrame,
)

Bases: Saveable

Result of fos_change().

Attributes:

Name Type Description
change_map DataArray or ndarray

Byte raster with pixel values: 0..200 = Delta FOS shifted by +100 (100 = No change), 250 = Foreground Gain, 251 = Foreground Loss, 252 = Stable BG, 253 = Water, 254 = Missing.

fg_gain int

Pixels transitioning from BG to FG.

fg_loss int

Pixels transitioning from FG to BG.

stable_bg int

Pixels remaining background at both times.

forcom int

Common foreground pixels evaluated for fragmentation shift.

diag_same_class int

Pixels maintaining the exact same FOS density class.

diff_class int

Pixels shifting between FOS density classes.

frag_decrease_pct float

Percentage of shifting pixels with decreased fragmentation (increased connectivity).

frag_increase_pct float

Percentage of shifting pixels with increased fragmentation (decreased connectivity).

matrix_6x6 DataFrame

6x6 transition matrix across FOS classes (T1 rows -> T2 columns).

summary

summary() -> str

Text summary of the change.

Returns:

Type Description
str

fad_multiscale_change

fad_multiscale_change(
    a, b, *, window_sizes: Sequence[int] = (7, 13, 27, 81, 243), n_threads: int = 0
) -> FADMultiscaleChangeResult

Fragmentation class change between two FAD-MS analyses (GTB Change -> FAD Multiscale).

Parameters:

Name Type Description Default
a

Times A and B. Each is a FADMultiscaleResult, an FAD input map (array, DataArray or raster path coded 0/1/2 and optionally 3/4, analysed with fad_multiscale()), or the path of a GTB *_fad_mscale.tif (its single-scale maps are read from the same directory).

required
b

Times A and B. Each is a FADMultiscaleResult, an FAD input map (array, DataArray or raster path coded 0/1/2 and optionally 3/4, analysed with fad_multiscale()), or the path of a GTB *_fad_mscale.tif (its single-scale maps are read from the same directory).

required
window_sizes Sequence[int]

FAD-MS observation scales (GTB: 7, 13, 27, 81, 243).

(7, 13, 27, 81, 243)
n_threads int

Threads (0 or None: all cores but one).

0

Returns:

Type Description
FADMultiscaleChangeResult

One transition table per observation scale and for the multiscale map.

Examples:

>>> import landschaft as ls
>>> before = ls.load_raster(ls.example_data("input"))   # 0 missing, 1 background, 2 forest
>>> after = before.copy()
>>> cut = after.values[600:700, 100:260]
>>> cut[cut == 2] = 1                                   # a clear-cut
>>> plantation = after.values[250:330, 420:520]
>>> plantation[plantation == 1] = 2                     # a new plantation
>>> change = ls.fad_multiscale_change(before, after)
>>> list(change.scales)
[7, 13, 27, 81, 243, 'mscale']
>>> scale = change.scales[27]
>>> scale.changed, round(scale.frag_increase_pct, 1)
(3470, 54.7)
>>> change.averages.loc[27, ["fad_av_a", "fad_av_b"]].round(2).tolist()
[73.54, 74.01]

FADMultiscaleChangeResult dataclass

FADMultiscaleChangeResult(scales: dict[int | str, FADChangeScale], averages: DataFrame)

Bases: Saveable

Result of fad_multiscale_change().

scales maps every window size and "mscale" (the multiscale map) to a FADChangeScale. averages holds GTB's FAD_AV (mean FAD of the foreground) and AVCON (FAD_AV x foreground / non-missing area) per scale at times A and B, and their differences.

summary

summary(scale: int | str = 'mscale') -> str

Report of one scale, in the layout of GTB's FAD_change_<scale>.txt.

Parameters:

Name Type Description Default
scale int | str

A window size or "mscale".

'mscale'

Returns:

Type Description
str

FADChangeScale dataclass

FADChangeScale(
    scale: int | str,
    matrix: DataFrame,
    percent: DataFrame,
    fg_area_a: int,
    fg_area_b: int,
    forcom: int,
    same_class: int,
    changed: int,
    frag_decrease_pct: float,
    frag_increase_pct: float,
)

Bases: Saveable

FAD change A -> B at one observation scale (one GTB FAD_change_<scale> table).

Attributes:

Name Type Description
matrix DataFrame

7x7 pixel counts; rows are the class at time A (A0-Background to A6-Intact), columns the class at time B. Pixels missing at either time are left out.

percent DataFrame

The 6x6 foreground part of matrix with its diagonal set to 0, in % of the changed pixels (rows A1..A6, columns B1..B6).

same_class, changed

Foreground pixels (at both times) on / off the diagonal.

frag_decrease_pct, frag_increase_pct

% of the changed pixels above / below the diagonal, i.e. moving to a less / more fragmented class (NaN when nothing changed).

net_change property

net_change: int

Net foreground change A -> B, pixels.

Landscape mosaic change

landscape_mosaic_change

landscape_mosaic_change(
    a, b, window_size: int = 27, *, is_lm103: bool = False, threads: int = 0
) -> LMHeatmapChange

Change of the Landscape Mosaic heatmap from A to B (GTB Change -> Landscape Mosaic Heatmap).

a and b are LMHeatmap objects or maps accepted by landscape_mosaic_heatmap() (with window_size / is_lm103).

Parameters:

Name Type Description Default
a

Times A and B: LMHeatmap objects or maps accepted by landscape_mosaic_heatmap().

required
b

Times A and B: LMHeatmap objects or maps accepted by landscape_mosaic_heatmap().

required
window_size int

As in landscape_mosaic_heatmap(), for maps.

27
is_lm103 int

As in landscape_mosaic_heatmap(), for maps.

27
threads int

Threads (0: all cores but one).

0

Returns:

Type Description
LMHeatmapChange

Examples:

Agriculture (1) around the north-west corner becomes developed (3):

>>> import landschaft as ls
>>> clc = ls.load_raster(ls.example_data("clc"))
>>> before = ls.reclassify(clc, [(1, 12, 3), (12, 23, 1), (23, 35, 2)], others=0)   # developed, agriculture, natural
>>> after = before.copy()
>>> corner = after.values[:600, :600]
>>> corner[corner == 1] = 3
>>> change = ls.landscape_mosaic_change(before, after, window_size=27)
>>> change.table.columns.tolist()
['percent_a', 'percent_b', 'change', 'recode_class_19']
>>> _ = change.to_csv("delta_heatmap.csv")

LMHeatmapChange dataclass

LMHeatmapChange(values: ndarray, a: LMHeatmap, b: LMHeatmap)

Bases: Saveable

Result of landscape_mosaic_change(): the heatmap difference B - A (GTB's delta_heatmap.csv).

Attributes:

Name Type Description
values ndarray

Change of the share [%] of every LM class, as LMHeatmap.values.

a, b

The heatmaps of the two times.

table property

table: DataFrame

Share [%] per LM class code at A and B and its change.

triangle property

triangle: ndarray

The change as GTB's 12 x 21 triangle grid (NaN outside the triangle).

csv_text

csv_text() -> str

Content of GTB's delta_heatmap.csv.

to_csv

to_csv(path: str | Path) -> Path

Write GTB's delta_heatmap.csv.