1 of 26

AI-Profiles: Cloud Detection in Ceilometer Measurements

image segmentation, CNN, clustering

A. Mortier (augustinm@met.no), FoU-KL, MET Norway

2 of 26

📌 context

  • instruments: ceilometer / automated Lidars
  • network: E-PROFILE
  • measurement: attenuated backscatter profiles
  • data processing/visualization: A\V-Profiles

  • some use cases:
    • ☁️ cloud base height
    • aerosol profiles retrievals
      • 🌋 mass concentration profiles
      • 🌐 data assimilation / model evaluation

3 of 26

🫡 motivations

  • current approach (A-Profiles):
  • single profile analysis: compute vertical gradients and set thresholds to distinguish significant layers from noise, and aerosols from clouds (vertically sharper) → if > thr1 … if > thr2 …
  • the current method works reasonably well for mid/high-altitude clouds. It does not work well for low-level clouds or precipitation 🌧️🌨️ cases.

I could add more “if”s but.. I’ve seen some ML techniques used to distinguish cats 🐱 from dogs 🐶! Could I use this instead??

  • use non supervised ML techniques (no need for labelling) and get pixel-wise clusters → image classification segmentation
  • try different clustering techniques: k-means, DBSCAN, …

objective: detect clouds ☁️ in ceilometer measurements.

constraints: single wavelength measurements. distinction with dense aerosol layers. noise

4 of 26

k-means

concept

The k-means algorithm is one of the most widely recognized and implemented clustering techniques in machine learning. Its core principle revolves around partitioning a dataset into k distinct, non-overlapping clusters.

To understand the mechanics, consider a dataset X. The objective of k-means is to determine k centroids and assign each data point to the nearest centroid. These centroids, which represent the center of the clusters, are initialized randomly or based on specific heuristics.

The algorithm iteratively performs two primary steps:

  1. Assignment Step: Assign each data point in X to the nearest centroid. This assignment is typically based on the Euclidean distance, although other distance metrics can be employed.
  2. Update Step: Calculate the mean of the data points assigned to each centroid and set this mean as the new centroid.

This iterative process continues until the centroids no longer change significantly or a set number of iterations is reached.

5 of 26

k-means

landscape

4 clusters

——————————

img (1440, 1440, 3)

input img (2073600, 3)

labels (2073600,)

centers (4, 3)

——————————

6 of 26

k-means

attenuated backscatter profile

4 clusters

——————————

img (462, 1240, 1)

input img (1718640, 1)

labels (1718640,)

centers (4, 1)

——————————

7 of 26

🤔 nice but…

this “works” for a specific day… but we have various atmospheric conditions:

what would give 4 clusters in a “clear” day?

→ let’s just look at the previous day

8 of 26

k-means

attenuated backscatter profile

4 clusters

——————————

img (462, 1240, 1)

input img (1718640, 1)

labels (1718640,)

centers (4, 1)

——————————

9 of 26

💭 I want a model that

  • uses more information than just the intensity of the signal (color of the pixels): a cloud is usually different in terms of shape than an aerosol layer
  • is “trained” over different stations and in various atmospheric conditions

DEC (Deep Embedded Clustering)

→ feature extraction with CNN (Convolutional Neural Networks)

10 of 26

convolution

image x kernel

-1

0

1

-2

0

2

-1

0

1

-1

0

1

-1

0

1

-1

0

1

1

1

1

1

-8

1

1

1

1

1

0

0

-1

0

1

-1

0

[...]

11 of 26

CNN-based autoencoder architecture

12 of 26

⚙️ workflow

↪️ general idea

  1. encode pictures
  2. cluster those encoded pictures

📖 model training

  • input: attenuated backscatter profile image (B&W, logscale): 1st day of each month - CHM15k (150ish stations)
  • encoder/decoder → extract features: encoder.keras (save kernels)
  • clustering encoded features → kmeans.pkl (save centroids)

🔍 cloud detection

  • input: attenuated backscatter profile data (logscale)
  • encode and cluster image using trained models

13 of 26

autoencoder

# Encoder

input_img = Input(shape=(256, 512, 1)) # 256, 512, 1

x = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img) # 256, 512, 32

x = MaxPooling2D((2, 2), padding='same')(x) # 128, 256, 32

x = Conv2D(64, (3, 3), activation='relu', padding='same')(x) # 128, 256, 64

x = MaxPooling2D((2, 2), padding='same')(x) # 64, 128, 64

# Latent Space

x = Conv2D(128, (3, 3), activation='relu', padding='same')(x) # 64, 128, 128

encoded = MaxPooling2D((2, 2), padding='same')(x) # 32, 64, 128

# Decoder

x = Conv2D(128, (3, 3), activation='relu', padding='same')(encoded) # 32, 64, 128

x = UpSampling2D((2, 2))(x) # 64, 128, 128

x = Conv2D(64, (3, 3), activation='relu', padding='same')(x) # 64, 128, 64

x = UpSampling2D((2, 2))(x) # 128, 256, 64

x = Conv2D(32, (3, 3), activation='relu', padding='same')(x) # 128, 256, 32

x = UpSampling2D((2, 2))(x) # 256, 512, 32

decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x) # 256, 512, 1

# Model definition

autoencoder = Model(input_img, decoded)

autoencoder.compile(optimizer='adam', loss='mse')

# Train the autoencoder

autoencoder.fit(training_images, training_images, �validation_data=(validation_images, validation_images), �epochs=10, batch_size=16, shuffle=True)

encoder = Model(inputs=autoencoder.input, outputs=encoded)

encoder.save('dec/encoder.keras')

⏳25 minutes

14 of 26

k-means

# Path to the images

image_dir = 'images/rcs'

image_size = (256, 512) # Resize images to a consistent size

# Step 1: Load and Preprocess the Dataset

images = []

for filename in os.listdir(image_dir):

img_path = os.path.join(image_dir, filename)

img = load_img(img_path, target_size=image_size, color_mode='grayscale') # Load as grayscale

img_array = img_to_array(img) / 255.0 # Normalize to [0, 1]

images.append(img_array)

images = np.array(images)

# 1. Load encoder

encoder_path = 'unsupervised/cnn/encoder.keras'

encoder = load_model(encoder_path)

# 2. Encode each image to get pixel-level features

encoded_images = encoder.predict(images)

# 3. Flatten spatial dimensions but keep feature channels intact

# This will give (num_images * height * width, num_features)

num_images, enc_height, enc_width, num_features = encoded_images.shape

encoded_images_flat = encoded_images.reshape((num_images * enc_height * enc_width, num_features))

# 4. Apply KMeans to pixel features

kmeans = KMeans(n_clusters=8)

pixel_labels = kmeans.fit_predict(encoded_images_flat) # Clusters all pixels independently

joblib.dump(kmeans, 'dec/kmeans.pkl')

🚀 5ish seconds

15 of 26

results

0-100-20000-0000001-A

2024-07-02

v-profiles

16 of 26

results

0-100-20000-0000001-A

2024-07-02

v-profiles

17 of 26

results

0-100-20000-0000001-A

2024-07-01

v-profiles

18 of 26

results

0-100-20000-0000001-A

2024-07-02

v-profiles

19 of 26

results

0-100-20000-0000001-A

2024-07-03

v-profiles

20 of 26

results

0-100-20000-0000001-A

2024-07-04

v-profiles

21 of 26

results

0-100-20000-0000001-A

2024-07-05

v-profiles

22 of 26

results

0-100-20000-0000001-A

2024-07-16

v-profiles

23 of 26

what’s next

  • 🤯 understand what I’m doingin progress
  • ☀️ test results on various atm. conditionsin progress
  • #️⃣ k-means: test different number of clusters? ✅
  • 📖 try increasing learning dataset?different results if retraining model with same input
  • 🍂 diversify learning dataset over different seasons ✅
  • 🖼️ try increasing image resolution?
  • 🕵️ test DBSCAN clustering method? not enough memory (on my laptop)
  • 🧑‍💻 implement cloud detection in A-Profiles
    • how to deal with partial days? (padding/cropping)
    • how does it perform for instruments with different noise levels?

24 of 26

Mini-MPL

0-20000-0-07014-A

2024-07-16

v-profiles

25 of 26

Oslo, this morning

0-20000-0-01492-A

2024-12-16

v-profiles

26 of 26

some resources