Moran's I Image Analysis
View SourceMix.install([
{:kino, "~> 0.12.0"},
{:vega_lite, "~> 0.1.8"},
{:kino_vega_lite, "~> 0.1.11"},
{:moransi, path: __DIR__}
])
alias VegaLite, as: VlIntroduction
This LiveBook demonstrates the capabilities of the MoransI module for computing spatial autocorrelation in images using Moran's I statistics.
Moran's I helps identify spatial patterns and clusters in 2D data.
- Global Moran's I: Provides an overall measure of spatial autocorrelation
- Local Moran's I (LISA): Identifies specific locations of clusters and outliers
Moran's I Range: Values range from -1 to 1
- Positive values: Indicate spatial clustering (similar values near each other)
- Values near 0: Indicate random spatial distribution
- Negative values: Indicate spatial dispersion (dissimilar values near each other)
Statistical Significance: Z-scores and p-values help determine if observed patterns are statistically significant.
Cluster Types:
- HH (High-High): High values surrounded by high values
- LL (Low-Low): Low values surrounded by low values
- HL (High-Low): High values surrounded by low values (outliers)
- LH (Low-High): Low values surrounded by high values (outliers)
- NS (Not Significant): No significant spatial pattern
1. Creating Test Images
Let's start by creating different types of test images to demonstrate various spatial patterns:
defmodule TestData do
@doc """
Create a simple test image with spatial patterns for demonstration.
## Parameters
- `type`: Type of pattern (`:clustered`, `:random`, `:dispersed`)
- `size`: Size of the square image (default: 10)
## Returns
A 2D list representing the test image.
"""
def create_test_image(type \\ :clustered, size \\ 10) do
case type do
:clustered ->
# Create clustered pattern
for i <- 0..(size-1) do
for j <- 0..(size-1) do
cond do
i < div(size, 2) and j < div(size, 2) -> 1
i >= div(size, 2) and j >= div(size, 2) -> 1
true -> 0
end
end
end
:random ->
# Create random pattern
:rand.seed(:exsplus, {1, 2, 3})
for _i <- 0..(size-1) do
for _j <- 0..(size-1) do
:rand.uniform(2) - 1
end
end
:dispersed ->
# Create checkerboard pattern
for i <- 0..(size-1) do
for j <- 0..(size-1) do
rem(i + j, 2)
end
end
end
end
end
clustered_image = TestData.create_test_image(:clustered, 12)
random_image = TestData.create_test_image(:random, 12)
dispersed_image = TestData.create_test_image(:dispersed, 12)2. Visualizing Test Images
Let's create heatmaps to better visualize our test images:
defmodule Visualizer do
def image_to_heatmap_data(image, title) do
image
|> Enum.with_index()
|> Enum.flat_map(fn {row, i} ->
row
|> Enum.with_index()
|> Enum.map(fn {value, j} ->
%{x: j, y: i, value: value, image: title}
end)
end)
end
def create_heatmap(image, title) do
data = image_to_heatmap_data(image, title)
Vl.new(width: 200, height: 200, title: title)
|> Vl.data_from_values(data)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :ordinal, title: "Column")
|> Vl.encode_field(:y, "y", type: :ordinal, title: "Row", sort: :descending)
|> Vl.encode_field(:color, "value",
type: :quantitative,
scale: [scheme: "viridis"],
title: "Value")
end
end
# Create heatmaps for each pattern
clustered_chart = Visualizer.create_heatmap(clustered_image, "Clustered")
random_chart = Visualizer.create_heatmap(random_image, "Random")
dispersed_chart = Visualizer.create_heatmap(dispersed_image, "Dispersed")
# Display charts
Kino.Layout.grid([clustered_chart, random_chart, dispersed_chart], columns: 3)
3. Global Moran's I Analysis
Now let's compute the global Moran's I for each pattern:
clustered_global = MoransI.global_morans_i(clustered_image)
random_global = MoransI.global_morans_i(random_image)
dispersed_global = MoransI.global_morans_i(dispersed_image)
results_data = [
%{pattern: "Clustered", morans_i: clustered_global.morans_i,
z_score: clustered_global.z_score, p_value: clustered_global.p_value,
interpretation: if(clustered_global.morans_i > 0, do: "Positive Autocorr.", else: "Negative Autocorr.")},
%{pattern: "Random", morans_i: random_global.morans_i,
z_score: random_global.z_score, p_value: random_global.p_value,
interpretation: if(abs(random_global.morans_i) < 0.1, do: "No Autocorr.", else: "Some Autocorr.")},
%{pattern: "Dispersed", morans_i: dispersed_global.morans_i,
z_score: dispersed_global.z_score, p_value: dispersed_global.p_value,
interpretation: if(dispersed_global.morans_i < 0, do: "Negative Autocorr.", else: "Positive Autocorr.")}
]
Kino.DataTable.new(results_data)4. Detailed Analysis of Global Results
morans_chart =
Vl.new(width: 400, height: 300, title: "Global Moran's I by Pattern")
|> Vl.data_from_values(results_data)
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "pattern", type: :nominal, title: "Pattern Type")
|> Vl.encode_field(:y, "morans_i", type: :quantitative, title: "Moran's I")
|> Vl.encode_field(:color, "morans_i",
type: :quantitative,
scale: [scheme: "redblue", domain: [-1, 1]])
zscore_chart =
Vl.new(width: 400, height: 300, title: "Z-Scores by Pattern")
|> Vl.data_from_values(results_data)
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "pattern", type: :nominal, title: "Pattern Type")
|> Vl.encode_field(:y, "z_score", type: :quantitative, title: "Z-Score")
|> Vl.encode_field(:color, "z_score",
type: :quantitative,
scale: [scheme: "redblue"])
Kino.Layout.grid([morans_chart, zscore_chart], columns: 2)5. Local Moran's I Analysis
Let's compute local Moran's I (LISA) for the clustered pattern:
local_results = MoransI.local_morans_i(clustered_image)
local_i_values =
local_results
|> Enum.with_index()
|> Enum.flat_map(fn {row, i} ->
row
|> Enum.with_index()
|> Enum.map(fn {result, j} ->
%{x: j, y: i, local_i: result.local_i,
z_score: result.z_score, p_value: result.p_value,
cluster_type: result.cluster_type}
end)
end)
IO.puts("Local Moran's I Results (first few):")
local_i_values |> Enum.take(10) |> Enum.each(&IO.inspect/1)6. Visualizing Local Moran's I
Visualizing local Moran's I (LISA) for the clustered pattern:
# Create heatmap for local Moran's I values
local_i_chart =
Vl.new(width: 300, height: 300, title: "Local Moran's I Values")
|> Vl.data_from_values(local_i_values)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :ordinal, title: "Column")
|> Vl.encode_field(:y, "y", type: :ordinal, title: "Row", sort: :descending)
|> Vl.encode_field(:color, "local_i",
type: :quantitative,
scale: [scheme: "redblue"],
title: "Local I")
# Create heatmap for cluster types
cluster_chart =
Vl.new(width: 300, height: 300, title: "Cluster Types (LISA)")
|> Vl.data_from_values(local_i_values)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :ordinal, title: "Column")
|> Vl.encode_field(:y, "y", type: :ordinal, title: "Row", sort: :descending)
|> Vl.encode_field(:color, "cluster_type",
type: :nominal,
scale: [
domain: ["hh", "ll", "hl", "lh", "ns"],
range: ["#d62728", "#1f77b4", "#ff7f0e", "#2ca02c", "#cccccc"]
],
title: "Cluster Type")
# Display significance heatmap
sig_chart =
Vl.new(width: 300, height: 300, title: "Statistical Significance")
|> Vl.data_from_values(local_i_values)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :ordinal, title: "Column")
|> Vl.encode_field(:y, "y", type: :ordinal, title: "Row", sort: :descending)
|> Vl.encode_field(:color, "p_value",
type: :quantitative,
scale: [scheme: "reds", reverse: true],
title: "P-Value")
Kino.Layout.grid([local_i_chart, cluster_chart, sig_chart], columns: 2)7. Connectivity Comparison
Let's compare results using different connectivity patterns:
# Compare Queen (8-connectivity) vs Rook (4-connectivity)
queen_result = MoransI.global_morans_i(clustered_image, connectivity: :queen)
rook_result = MoransI.global_morans_i(clustered_image, connectivity: :rook)
connectivity_data = [
%{connectivity: "Queen (8-conn)", morans_i: queen_result.morans_i,
z_score: queen_result.z_score, p_value: queen_result.p_value},
%{connectivity: "Rook (4-conn)", morans_i: rook_result.morans_i,
z_score: rook_result.z_score, p_value: rook_result.p_value}
]
Kino.DataTable.new(connectivity_data)9. Real-World Example: Simulated Satellite Data
Let's create a more realistic example simulating satellite imagery:
defmodule SatelliteSimulator do
def create_vegetation_map(size \\ 20) do
# Simulate vegetation density with clusters
:rand.seed(:exsplus, {42, 17, 89})
# Create base random field
base = for _ <- 0..(size-1) do
for _ <- 0..(size-1) do
:rand.uniform() * 0.3
end
end
# Add vegetation clusters
clusters = [
{div(size, 4), div(size, 4), 0.8}, # Forest patch
{3 * div(size, 4), div(size, 4), 0.6}, # Grassland
{div(size, 2), 3 * div(size, 4), 0.9} # Dense forest
]
for {row, i} <- Enum.with_index(base) do
for {base_val, j} <- Enum.with_index(row) do
cluster_effect = Enum.reduce(clusters, 0, fn {cx, cy, intensity}, acc ->
distance = :math.sqrt((i - cx) * (i - cx) + (j - cy) * (j - cy))
if distance < size / 8 do
acc + intensity * :math.exp(-distance / 3)
else
acc
end
end)
min(1.0, base_val + cluster_effect)
end
end
end
end
# Create simulated satellite data
vegetation_map = SatelliteSimulator.create_vegetation_map(15)
# Analyze the vegetation map
veg_global = MoransI.global_morans_i(vegetation_map)
veg_local = MoransI.local_morans_i(vegetation_map)
# Visualize
veg_data = Visualizer.image_to_heatmap_data(vegetation_map, "Vegetation")
veg_chart =
Vl.new(width: 400, height: 400, title: "Simulated Vegetation Density")
|> Vl.data_from_values(veg_data)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :ordinal, title: "Longitude")
|> Vl.encode_field(:y, "y", type: :ordinal, title: "Latitude", sort: :descending)
|> Vl.encode_field(:color, "value",
type: :quantitative,
scale: [scheme: "greens"],
title: "Density")
# Extract cluster information
cluster_data =
veg_local
|> Enum.with_index()
|> Enum.flat_map(fn {row, i} ->
row
|> Enum.with_index()
|> Enum.map(fn {result, j} ->
%{x: j, y: i, cluster_type: result.cluster_type,
local_i: result.local_i, p_value: result.p_value}
end)
end)
cluster_analysis =
Vl.new(width: 400, height: 400, title: "Vegetation Clustering Analysis")
|> Vl.data_from_values(cluster_data)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :ordinal, title: "Longitude")
|> Vl.encode_field(:y, "y", type: :ordinal, title: "Latitude", sort: :descending)
|> Vl.encode_field(:color, "cluster_type",
type: :nominal,
scale: [
domain: ["hh", "ll", "hl", "lh", "ns"],
range: ["#2d8f2d", "#8b4513", "#ff6347", "#ffd700", "#d3d3d3"]
],
title: "Cluster Type")
Kino.Layout.grid([veg_chart, cluster_analysis], columns: 1)10. Summary and Interpretation
summary_data = [
%{analysis: "Clustered Pattern", morans_i: clustered_global.morans_i,
interpretation: "Strong positive autocorrelation - similar values cluster together"},
%{analysis: "Random Pattern", morans_i: random_global.morans_i,
interpretation: "Weak/no autocorrelation - values are randomly distributed"},
%{analysis: "Dispersed Pattern", morans_i: dispersed_global.morans_i,
interpretation: "Negative autocorrelation - checkerboard pattern"},
%{analysis: "Vegetation Map", morans_i: veg_global.morans_i,
interpretation: "Positive autocorrelation - vegetation forms natural clusters"}
]
Kino.DataTable.new(summary_data)Key Takeaways
This module is particularly useful for analyzing satellite imagery, medical imaging, geographic data, and any other 2D spatial data where understanding clustering patterns is important.