Wstęp

W projekcie wykorzystano dane z: Kaggle Nature Dataset. Zbiór danych zawiera 100 000 zdjęć w rozmiarze 128x128 pikseli przedstawiających: miasto, ogniska, jeziora i góry.

Cel projektu: Praca opiera się na wczytaniu dużego zbioru zdjęć reprezentujących różne lokalizacje. U podstawy projektu leży założenie, że istnieje możliwość rozróżnienia wskazanych lokalizacji na podstawie cech zdjęć (np. dla zdjęć jezior dominuje kolor niebieski, a dla miast odcienie szarości).

Metodyka: W przeciwieństwie do konwersji na skalę szarości (która może powodować utratę informacji), tutaj analizujemy pełne dane kolorystyczne. W ramach projektu wylosowano po 250 zdjęć z każdej z 4 kategorii.

Wykorzystanie narzędzi: Niewielka część kodu została zaadaptowana z materiałów z zajęć (PCA_images_MDS_distances, dr Monika Kot oraz prof. Katarzyna Kopczewska) – np. przy analizie pojedynczego zdjęcia. W pracy wykorzystano częściowo chat Gemini do wygenerowania fragmentów kodu R (wykres 3D) oraz do konwersji niniejszego raportu do formatu R Markdown (.Rmd). Opis wyników oraz poszczególnych kroków wykonany całkowicie samodzielnie.

Ładowanie pakietów

library(factoextra)
library(plotly)
library(dplyr)
library(gridExtra)
library(cluster)
library(jpeg)

Wczytanie i przygotowanie danych

Pobieramy nazwy wszystkich plików w podfolderach kategorii, aby następnie wylosować z nich próbę badawczą (\(n=250\) dla każdej kategorii).

folder <- "Nature_x128"
categories <- c("City","Fire","Lake","Mountain")
n_sample <- 250

image_all <- list()
labels <- NULL

set.seed(123)
# Sprawdzenie czy folder istnieje, aby uniknąć błędów przy kompilacji
if(dir.exists(folder)) {
  for(cat in categories){
    cat_path <- file.path(folder, cat)
    all_files <- list.files(cat_path, full.names = TRUE)
    
    # losowanie n zdjęć z kategorii
    if(length(all_files) > 0) {
      files_to_add <- sample(all_files, n_sample)
      
      for(i in files_to_add){
        img <- readJPEG(i)
        image_all[[length(image_all) + 1]] <- img
        labels <- c(labels, cat)
      }
    } else {
      warning(paste("Brak plików w kategorii:", cat))
    }
  }
} else {
  warning("Folder 'Nature_x128' nie został znaleziony. Upewnij się, że ścieżka jest poprawna.")
}

# Po wczytaniu 1000 zdjęć ich łączny rozmiar to około 400 MB w pamięci

Analiza pojedynczego zdjęcia

Analogicznie jak na zajęciach, zdjęcie składa się z kanałów kolorów (RGB), które możemy rozbić i przeanalizować osobno.

# Wybór losowego zdjęcia (indeks 280)
if(length(image_all) >= 280) {
  selected_img <- image_all[[280]]
  
  # Sprawdzenie wymiarów
  dim(selected_img) 
  
  # Wyświetlenie zdjęcia
  plot(1, type = "n", xaxt = "n", yaxt = "n", xlab = "", ylab = "")
  rasterImage(selected_img, 0.6, 0.6, 1.4, 1.4)
}

PCA dla kanałów RGB (pojedyncze zdjęcie)

if(exists("selected_img")) {
  r <- selected_img[,,1]    
  g <- selected_img[,,2]
  b <- selected_img[,,3]
  
  r.pca <- prcomp(r, center = FALSE, scale. = FALSE)
  g.pca <- prcomp(g, center = FALSE, scale. = FALSE)
  b.pca <- prcomp(b, center = FALSE, scale. = FALSE)
  
  f1 <- fviz_eig(r.pca, main = "Red", barfill = "red", ncp = 5, addlabels = TRUE)
  f2 <- fviz_eig(g.pca, main = "Green", barfill = "green", ncp = 5, addlabels = TRUE)
  f3 <- fviz_eig(b.pca, main = "Blue", barfill = "blue", ncp = 5, addlabels = TRUE)
  
  grid.arrange(f1, f2, f3, ncol = 3)
}

Warto postawić pytanie: o ile mogę zmniejszyć “jakość” zdjęcia, aby nadal móc analizować kolory? W tym momencie mamy \(128 \times 128 \times 3 = 49\,152\) cech.

PCA dla całego zbioru zdjęć

Przygotowanie danych (spłaszczenie)

flatten_photo <- function(img_array) {
  as.vector(img_array)
}

if(length(image_all) > 0) {
  # Transpozycja, aby zdjęcia były w wierszach
  matrix_all <- t(sapply(image_all, flatten_photo))
  rownames(matrix_all) <- make.names(labels, unique = TRUE)
  
  # matrix_all zawiera n wierszy (zdjęcia) oraz 49152 kolumn (piksele)
}

Wykonanie PCA

if(exists("matrix_all")) {
  res_pca <- prcomp(matrix_all, center = TRUE, scale. = FALSE)
  
  # Analiza wartości własnych
  eig_val <- get_eigenvalue(res_pca)
  
  # Wykres osypiska (Scree plot)
  fviz_eig(res_pca, addlabels = TRUE, ncp = 10, 
           main = "Procent wariancji wyjaśnianej przez główne składowe")
}

Wnioski z redukcji wymiarów: 120 pierwszych zmiennych wyznacza około 90,2% wariancji (można to odczytać z head(eig_val, 120)). Zredukowano wymiarowość z 49 152 do 120 zmiennych PC, zachowując ponad 90% informacji. Pozwala to na istotne przyspieszenie obliczeń algorytmu k-means oraz usunięcie potencjalnego szumu danych (“ogon” wariancji).

Klastrowanie K-means

Wykonujemy klastrowanie na zredukowanych danych (PC1-PC120). Dane zostały zmniejszone z około 400 MB do ok. 1 MB. Zakładamy 4 klastry, ponieważ mamy 4 grupy lokalizacji (City, Fire, Lake, Mountain).

if(exists("res_pca")) {
  pca_data_for_clustering_k_means <- res_pca$x[, 1:120]
  
  set.seed(123)
  km <- kmeans(pca_data_for_clustering_k_means, centers = 4, nstart = 25)
  
  # Wizualizacja 2D
  fviz_cluster(km, data = pca_data_for_clustering_k_means,
               geom = "point",
               ellipse.type = "convex", 
               ggtheme = theme_bw(),
               main = "Wynik klastrowania K-means",
               choose.vars = c(1, 2) # dla osi PC1 i PC2
  )
}

Wielkości klastrów

if(exists("km")) {
  print(km$size)
}
## [1] 129 393 163 315

Teoretycznie każdy klaster powinien liczyć ok. 250 elementów. Rozbieżności oznaczają, że część zdjęć została błędnie przypisana.

Wizualizacja 3D

Poniżej reprezentacja w 3D (PC1, PC2, PC3). Wykres interaktywny pozwala najechać na punkt i sprawdzić: 1. Do jakiego klastra przypisał go algorytm. 2. Jaka jest jego prawdziwa etykieta (RealLabel).

if(exists("res_pca") && exists("km")) {
  df_3d <- data.frame(
    PC1 = res_pca$x[, 1],
    PC2 = res_pca$x[, 2],
    PC3 = res_pca$x[, 3],
    Cluster = as.factor(km$cluster), # Przypisanie algorytmu
    RealLabel = labels               # Prawdziwa etykieta
  )
  
  plot_ly(data = df_3d, 
          x = ~PC1, 
          y = ~PC2, 
          z = ~PC3, 
          color = ~Cluster,
          colors = c("#E7B800", "#2E9FDF", "#FC4E07", "#00AFBB"),
          text = ~paste("Klaster:", Cluster, "<br>Prawda:", RealLabel),
          type = "scatter3d", 
          mode = "markers",
          marker = list(size = 3)
  ) %>%
    layout(title = "Wynik K-Means w 3D",
           scene = list(xaxis = list(title = 'PC1'),
                        yaxis = list(title = 'PC2'),
                        zaxis = list(title = 'PC3')))
}

Analiza Wyników

Macierz pomyłek (Confusion Matrix)

Porównanie prawdziwych etykiet z wynikami klastrowania.

if(exists("km")) {
  conf_matrix <- table(Prawdziwa_Etykieta = labels, Klaster_Algorytmu = km$cluster)
  print(conf_matrix)
}
##                   Klaster_Algorytmu
## Prawdziwa_Etykieta   1   2   3   4
##           City       1   9   0 240
##           Fire     122   0 128   0
##           Lake       0 249   1   0
##           Mountain   6 135  34  75

Interpretacja macierzy pomyłek:

  1. City (Miasto): Bardzo dobrze rozróżnione. 240/250 zdjęć trafiło poprawnie do jednego klastra (w tym przypadku do klastra nr 4).
  2. Lake (Jezioro): Najlepiej wyróżniona kategoria. 249/250 zdjęć trafiło do klastra nr 2.
  3. Fire (Ogień) i Mountain (Góry): Tutaj nastąpiło największe wymieszanie analizowanych obrazków.
    • Blisko połowa zdjęć ognia (122) trafiła do klastra nr 1, a druga połowa (128) do klastra nr 3.
    • Zdjęcia gór (Mountain) zostały w dużej mierze (135/250) błędnie zaklasyfikowane jako jeziora (klaster nr 2) – prawdopodobnie z uwagi na obecność nieba lub śniegu przypominającego wodę. Tylko 34 zdjęcia gór trafiły do osobnego klastra.

Uwaga: Numery klastrów mogą się różnić przy każdym uruchomieniu algorytmu k-means, ale struktura podziału pozostaje podobna.

Podsumowanie i Wnioski

W projekcie zmniejszono wielkość wymiarów każdego z \(N\) obrazów z \(128 \times 128 \times 3\) na 120 dzięki zastosowaniu PCA. Algorytm ten pozwolił na efektywne zastosowanie k-means (blisko 400-krotne zmniejszenie ilości danych).

Skuteczność: Wysoka: dla kategorii City oraz Lake. Niska: dla kategorii Fire i Mountain. Manualna analiza sugeruje, że zdjęcia ognia często były wykonywane w zimowym/ciemnym otoczeniu, co upodobniło je kolorystycznie do zdjęć gór.

Wnioski na przyszłość: Samo PCA na surowych pikselach jest świetne do redukcji wymiarów, ale niewystarczające do rozróżnienia obiektów o podobnej kolorystyce (np. góry vs ogień w specyficznych warunkach). Należałoby wyznaczyć dodatkowe zmienne (np. wykrywanie krawędzi, tekstur), ponieważ sama analiza wariancji kolorów nie oddała w pełni różnic semantycznych między tymi grupami.