Cel i opis projektu

Celem projektu jest analiza i porównanie palet kolorów pięciu obrazów malarskich na podstawie ich wersji cyfrowej.
W projekcie:

Tego typu podejście może być stosowane m.in. w rozpoznawaniu stylu, wyszukiwaniu podobnych obrazów czy kompresji kolorów w grafice komputerowej.


Wczytanie i przygotowanie danych

W tej części wczytuję pięć obrazów, sprawdzam ich wymiary oraz tworzę ramki danych zawierające współrzędne pikseli oraz wartości kanałów R, G, B.

# Wczytuję pięć obrazów JPG jako tablice RGB do dalszej analizy.
woman_with_flower      <- readJPEG("WomanWithFlower.jpeg")
the_tragedy            <- readJPEG("TheTragedy.jpeg")
the_old_guitarist      <- readJPEG("TheOldGuitarist.jpeg")
portratit_of_dora_maar <- readJPEG("PortraitOfDoraMaar.jpeg")
still_life             <- readJPEG("StillLife.jpeg")

# Sprawdzam klasy (powinny być typu array).
class(woman_with_flower)
## [1] "array"
class(the_tragedy)
## [1] "array"
class(the_old_guitarist)
## [1] "array"
class(portratit_of_dora_maar)
## [1] "array"
class(still_life)
## [1] "array"
# Sprawdzam wymiary każdego obrazu, aby poprawnie zbudować siatkę pikseli.
dm1 <- dim(woman_with_flower)
dm2 <- dim(the_tragedy)
dm3 <- dim(the_old_guitarist)
dm4 <- dim(portratit_of_dora_maar)
dm5 <- dim(still_life)

dm1; dm2; dm3; dm4; dm5
## [1] 254 198   3
## [1] 278 181   3
## [1] 275 183   3
## [1] 270 187   3
## [1] 249 202   3
# Konwertuję każdy obraz na ramkę danych z pozycją piksela (x, y) i wartościami RGB.

rgb_woman_with_flowers <- data.frame(
  x       = rep(1:dm1[2], each = dm1[1]),
  y       = rep(dm1[1]:1, times = dm1[2]),
  r.value = as.vector(woman_with_flower[,,1]),
  g.value = as.vector(woman_with_flower[,,2]),
  b.value = as.vector(woman_with_flower[,,3])
)

rgb_the_tragedy <- data.frame(
  x       = rep(1:dm2[2], each = dm2[1]),
  y       = rep(dm2[1]:1, times = dm2[2]),
  r.value = as.vector(the_tragedy[,,1]),
  g.value = as.vector(the_tragedy[,,2]),
  b.value = as.vector(the_tragedy[,,3])
)

rgb_the_old_guitarist <- data.frame(
  x       = rep(1:dm3[2], each = dm3[1]),
  y       = rep(dm3[1]:1, times = dm3[2]),
  r.value = as.vector(the_old_guitarist[,,1]),
  g.value = as.vector(the_old_guitarist[,,2]),
  b.value = as.vector(the_old_guitarist[,,3])
)

rgb_portratit_of_dora_maar <- data.frame(
  x       = rep(1:dm4[2], each = dm4[1]),
  y       = rep(dm4[1]:1, times = dm4[2]),
  r.value = as.vector(portratit_of_dora_maar[,,1]),
  g.value = as.vector(portratit_of_dora_maar[,,2]),
  b.value = as.vector(portratit_of_dora_maar[,,3])
)

rgb_still_life <- data.frame(
  x       = rep(1:dm5[2], each = dm5[1]),
  y       = rep(dm5[1]:1, times = dm5[2]),
  r.value = as.vector(still_life[,,1]),
  g.value = as.vector(still_life[,,2]),
  b.value = as.vector(still_life[,,3])
)

head(rgb_woman_with_flowers)
##   x   y   r.value    g.value    b.value
## 1 1 254 0.1137255 0.05490196 0.03529412
## 2 1 253 0.1137255 0.05490196 0.03529412
## 3 1 252 0.1137255 0.05490196 0.03529412
## 4 1 251 0.1137255 0.05490196 0.03529412
## 5 1 250 0.1137255 0.05490196 0.03529412
## 6 1 249 0.1137255 0.05490196 0.03529412

Rekonstrukcja obrazów z danych RGB

Tutaj sprawdzam, czy poprawnie zrekonstruowałam obrazy z ramek danych: każdy piksel jest rysowany jako punkt na płaszczyźnie (x, y) w swoim kolorze RGB.

par(mfrow = c(3, 2), mar = c(2, 2, 3, 1))

plot(
  y ~ x,
  data = rgb_woman_with_flowers,
  main = "Woman with flowers",
  col  = rgb(rgb_woman_with_flowers$r.value,
             rgb_woman_with_flowers$g.value,
             rgb_woman_with_flowers$b.value),
  asp  = 1, pch = "."
)

plot(
  y ~ x,
  data = rgb_the_tragedy,
  main = "The Tragedy",
  col  = rgb(rgb_the_tragedy$r.value,
             rgb_the_tragedy$g.value,
             rgb_the_tragedy$b.value),
  asp  = 1, pch = "."
)

plot(
  y ~ x,
  data = rgb_the_old_guitarist,
  main = "The Old Guitarist",
  col  = rgb(rgb_the_old_guitarist$r.value,
             rgb_the_old_guitarist$g.value,
             rgb_the_old_guitarist$b.value),
  asp  = 1, pch = "."
)

plot(
  y ~ x,
  data = rgb_portratit_of_dora_maar,
  main = "Portrait of Dora Maar",
  col  = rgb(rgb_portratit_of_dora_maar$r.value,
             rgb_portratit_of_dora_maar$g.value,
             rgb_portratit_of_dora_maar$b.value),
  asp  = 1, pch = "."
)

plot(
  y ~ x,
  data = rgb_still_life,
  main = "Still Life",
  col  = rgb(rgb_still_life$r.value,
             rgb_still_life$g.value,
             rgb_still_life$b.value),
  asp  = 1, pch = "."
)

par(mfrow = c(1,1))


Dobór liczby klastrów metodą silhouette

W tej części dobieram optymalną liczbę klastrów dla każdego obrazu.
Dla k = 2, 3, …, 10 wykonuję algorytm CLARA na danych RGB danego obrazu i zapisuję średnią szerokość sylwetki. Najwyższa wartość silhouette sugeruje najlepszy k.

set.seed(123)

# Funkcja pomocnicza do obliczenia silhouette dla zakresu k
silhouette_for_k <- function(rgb_data, k_min = 2, k_max = 10){
  sil <- numeric(k_max)
  for (k in k_min:k_max) {
    cl <- clara(rgb_data[, c("r.value", "g.value", "b.value")], k)
    sil[k] <- cl$silinfo$avg.width
  }
  sil
}

n1 <- silhouette_for_k(rgb_woman_with_flowers)
n2 <- silhouette_for_k(rgb_the_tragedy)
n3 <- silhouette_for_k(rgb_the_old_guitarist)
n4 <- silhouette_for_k(rgb_portratit_of_dora_maar)
n5 <- silhouette_for_k(rgb_still_life)

par(mfrow = c(3, 2), mar = c(4,4,3,1))

k_vals <- 1:10

plot(
  k_vals, n1,
  type = "b",
  main = "Optimal k – Woman with flowers",
  xlab = "Number of clusters", ylab = "Average silhouette"
)

plot(
  k_vals, n2,
  type = "b",
  main = "Optimal k – The Tragedy",
  xlab = "Number of clusters", ylab = "Average silhouette"
)

plot(
  k_vals, n3,
  type = "b",
  main = "Optimal k – The Old Guitarist",
  xlab = "Number of clusters", ylab = "Average silhouette"
)

plot(
  k_vals, n4,
  type = "b",
  main = "Optimal k – Portrait of Dora Maar",
  xlab = "Number of clusters", ylab = "Average silhouette"
)

plot(
  k_vals, n5,
  type = "b",
  main = "Optimal k – Still Life",
  xlab = "Number of clusters", ylab = "Average silhouette"
)

par(mfrow = c(1,1))

Komentarz: Na podstawie powyższych wykresów wybieram liczbę klastrów dla każdego obrazu (np. 4 dla „Woman with flowers”, 2 dla „The Tragedy”, 2 dla „The Old Guitarist”, 2 dla „Portrait of Dora Maar”, 8 dla „Still Life”), kierując się maksymalną wartością silhouette oraz intuicyjną interpretacją obrazów.


Klasteryzacja kolorów i redukcja palety

Teraz wykonuję klasteryzację CLARA dla wybranych k dla każdego obrazu i tworzę „zredukowane” wersje obrazów, w których każdy piksel przyjmuje kolor medoidu swojego klastra. Dodatkowo wyświetlam próbnik (swatch) dominujących kolorów.

set.seed(123)

# Klasteryzuję kolory obrazu „Woman with flowers” na 4 klastry i wizualizuję wynik.
cl_woman <- clara(rgb_woman_with_flowers[, 3:5], k = 4)
plot(silhouette(cl_woman), main = "Silhouette – Woman with flowers (k = 4)")

colors_woman <- rgb(cl_woman$medoids[cl_woman$clustering, ])

plot(
  y ~ x, data = rgb_woman_with_flowers,
  main = "Woman with flowers – 4 colours",
  col  = colors_woman, pch = ".", cex = 2, asp = 1
)

swatch(rgb(cl_woman$medoids))

# Klasteryzuję kolory obrazu „The Tragedy” na 2 klastry i wizualizuję wynik.
cl_tragedy <- clara(rgb_the_tragedy[, 3:5], k = 2)
plot(silhouette(cl_tragedy), main = "Silhouette – The Tragedy (k = 2)")

colors_tragedy <- rgb(cl_tragedy$medoids[cl_tragedy$clustering, ])

plot(
  y ~ x, data = rgb_the_tragedy,
  main = "The Tragedy – 2 colours",
  col  = colors_tragedy, pch = ".", cex = 2, asp = 1
)

swatch(rgb(cl_tragedy$medoids))

# Klasteryzuję kolory obrazu „The Old Guitarist” na 2 klastry i wizualizuję wynik.
cl_guitarist <- clara(rgb_the_old_guitarist[, 3:5], k = 2)
plot(silhouette(cl_guitarist), main = "Silhouette – The Old Guitarist (k = 2)")

colors_guitarist <- rgb(cl_guitarist$medoids[cl_guitarist$clustering, ])

plot(
  y ~ x, data = rgb_the_old_guitarist,
  main = "The Old Guitarist – 2 colours",
  col  = colors_guitarist, pch = ".", cex = 2, asp = 1
)

swatch(rgb(cl_guitarist$medoids))

# Klasteryzuję kolory obrazu „Portrait of Dora Maar” na 2 klastry i wizualizuję wynik.
cl_portrait <- clara(rgb_portratit_of_dora_maar[, 3:5], k = 2)
plot(silhouette(cl_portrait), main = "Silhouette – Portrait of Dora Maar (k = 2)")

colors_portrait <- rgb(cl_portrait$medoids[cl_portrait$clustering, ])

plot(
  y ~ x, data = rgb_portratit_of_dora_maar,
  main = "Portrait of Dora Maar – 2 colours",
  col  = colors_portrait, pch = ".", cex = 2, asp = 1
)

swatch(rgb(cl_portrait$medoids))

# Klasteryzuję kolory obrazu „Still Life” na 8 klastrów i wizualizuję wynik.
cl_stilllife <- clara(rgb_still_life[, 3:5], k = 8)
plot(silhouette(cl_stilllife), main = "Silhouette – Still Life (k = 8)")

colors_stilllife <- rgb(cl_stilllife$medoids[cl_stilllife$clustering, ])

plot(
  y ~ x, data = rgb_still_life,
  main = "Still Life – 8 colours",
  col  = colors_stilllife, pch = ".", cex = 2, asp = 1
)

swatch(rgb(cl_stilllife$medoids))


Porównanie palet kolorów między obrazami

W tej części chcę porównać kolorystykę pięciu obrazów.
Dla porównywalności przyjmuję wspólną liczbę klastrów k = 2 i dla każdego obrazu wyznaczam paletę medoidów. Następnie definiuję miarę odległości między dwiema paletami na podstawie średniej odległości euklidesowej między najbliższymi kolorami.

# Funkcja wyznaczająca medoidy CLARA jako paletę kolorów przy zadanej liczbie klastrów.
extract_palette <- function(rgb_data, k){
  cl <- clara(rgb_data[, c("r.value","g.value","b.value")], k)
  return(cl$medoids)
}

# Ustalam wspólną liczbę klastrów (k = 2) do porównania palet.
k <- 2  

# Wyznaczam palety medoidów (dominujące kolory) dla wszystkich pięciu obrazów.
palette_woman     <- extract_palette(rgb_woman_with_flowers,     k)
palette_tragedy   <- extract_palette(rgb_the_tragedy,            k)
palette_guitarist <- extract_palette(rgb_the_old_guitarist,      k)
palette_portrait  <- extract_palette(rgb_portratit_of_dora_maar, k)
palette_stilllife <- extract_palette(rgb_still_life,             k)
# Funkcja obliczająca średnią odległość między dwiema paletami kolorów.
palette_distance <- function(p1, p2){
  dists <- apply(p1, 1, function(row1){
    min(apply(p2, 1, function(row2){
      sqrt(sum((row1 - row2)^2))
    }))
  })
  mean(dists)
}

# Tworzę wektor nazw obrazów i listę odpowiadających im palet.
img_names <- c("Woman", "Tragedy", "Guitarist", "Portrait", "Stilllife")

palette_list <- list(
  Woman     = palette_woman,
  Tragedy   = palette_tragedy,
  Guitarist = palette_guitarist,
  Portrait  = palette_portrait,
  Stilllife = palette_stilllife
)

# Buduję macierz odległości 5x5 między paletami pięciu obrazów.
n_img <- length(img_names)
dist_matrix <- matrix(0, nrow = n_img, ncol = n_img)
colnames(dist_matrix) <- rownames(dist_matrix) <- img_names

for(i in 1:(n_img-1)){
  for(j in (i+1):n_img){
    dist_matrix[i, j] <- palette_distance(palette_list[[i]], palette_list[[j]])
  }
}

# Uzupełniam macierz do postaci symetrycznej.
dist_matrix <- dist_matrix + t(dist_matrix)

print("ŚREDNIA ODLEGŁOŚĆ MIĘDZY PALETAMI (im mniejsza, tym obrazy bardziej podobne):")
## [1] "ŚREDNIA ODLEGŁOŚĆ MIĘDZY PALETAMI (im mniejsza, tym obrazy bardziej podobne):"
round(dist_matrix, 3)
##           Woman Tragedy Guitarist Portrait Stilllife
## Woman     0.000   0.347     0.356    0.308     0.241
## Tragedy   0.347   0.000     0.263    0.404     0.385
## Guitarist 0.356   0.263     0.000    0.404     0.255
## Portrait  0.308   0.404     0.404    0.000     0.369
## Stilllife 0.241   0.385     0.255    0.369     0.000

Wizualizacja palet i heatmapa odległości

Najpierw rysuję każdą paletę jako prosty pasek kolorów, a następnie przedstawiam macierz odległości między obrazami w formie heatmapy.

# Funkcja rysująca paletę jako pasek kolorów.
plot_palette <- function(pal, title){
  n <- nrow(pal)
  barplot(
    rep(1, n),
    col    = rgb(pal),
    border = NA,
    main   = title,
    yaxt   = "n"
  )
}

par(mfrow = c(5, 1), mar = c(2,2,3,1))
plot_palette(palette_woman,     "Palette – Woman")
plot_palette(palette_tragedy,   "Palette – Tragedy")
plot_palette(palette_guitarist, "Palette – Guitarist")
plot_palette(palette_portrait,  "Palette – Portrait")
plot_palette(palette_stilllife, "Palette – Still life")

par(mfrow = c(1,1))
# Heatmapa odległości między paletami kolorów pięciu obrazów.
pheatmap(
  dist_matrix,
  cluster_rows    = FALSE,
  cluster_cols    = FALSE,
  display_numbers = TRUE,
  number_format   = "%.3f",
  main            = "Heatmapa odległości między paletami kolorów",
  color           = colorRampPalette(c("white", "orange", "red"))(100)
)


Podsumowanie

W projekcie przeanalizowałam pięć obrazów malarskich, traktując każdy z nich jako zbiór pikseli w przestrzeni RGB. Zastosowałam metodę CLARA, będącą odmianą algorytmu k-medoidów, aby zredukować liczbę kolorów i wyodrębnić dominujące palety barw w każdym obrazie.

Na podstawie wykresów silhouette dobrałam liczbę klastrów osobno dla każdego obrazu, a następnie porównałam obrazy między sobą za pomocą odległości między ich paletami medoidów. Heatmapa odległości pokazała, które obrazy są do siebie najbardziej podobne kolorystycznie, a które różnią się pod względem używanej gamy barw.

Takie podejście może być wykorzystane w zadaniach związanych z analizą stylu malarskiego, grupowaniem obrazów, wyszukiwaniem podobnych dzieł sztuki oraz w zastosowaniach praktycznych, takich jak kompresja kolorów czy projektowanie palet barw.