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.
library(factoextra)
library(plotly)
library(dplyr)
library(gridExtra)
library(cluster)
library(jpeg)
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
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)
}
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.
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)
}
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).
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
)
}
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.
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')))
}
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:
Uwaga: Numery klastrów mogą się różnić przy każdym uruchomieniu algorytmu k-means, ale struktura podziału pozostaje podobna.
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.