1. Wstęp

W tej pracy została przedstawiona przykładowa analiza zastosowania analizy głównych składowych (PCA) do stratnej kompresji grafiki komputerowej. PCA to technika statystyczna wykorzystywana do redukcji wymiarów z zachowaniem jak największej informacji o danych. W grafice daje to możliwość zmniejszenia rozmiaru pliku kosztem utraty części detali. Wykorzystany format grafiki PNG, oprócz zwykłych kanałów kolorów R G i B, zawiera też kanał A (alpha), w którym znajduje się informacja o nieprzezroczystości danego piksela. Oprócz kompresji wszystkich kanałów została także przedstawiona analiza kompresji pojedynczych kanałów, dostosowana do wybranego obrazka.

Zostały wykorzystane biblioteki magick (do wizualizacji obrazków) oraz png (do wczytywania i zapisywania).

library(magick)
library(png)

2. Analiza

2.1. Przygotowanie

Na początku analizy wczytywany jest obraz (zdjęcie własne).

img_path <- "data_2.png"
img <- readPNG(img_path)
plot(image_read(img))

Wybrane dane mają wymiar 1024 x 1024 x 4 - rozdzielczość 1024x1024 pikseli i cztery kanały obrazu.

d <- dim(img)
d
## [1] 1024 1024    4

Dla uproszczenia dalszego kodu każdy kanał jest wydzielany do osobnej macierzy. Ponadto jest tworzona macierz czarnego kanału do późniejszych wizualizacji.

r <- img[,,1]
g <- img[,,2]
b <- img[,,3]
a <- img[,,4]
black <- matrix(0, nrow = d[1], ncol = d[2])

Na wykresie można zaobserwować rozkład jasności pikseli w poszczególnych kanałach. Kanałami z największym znaczeniem są tutaj czerwony i zielony, lecz choć rozkłady i mediany ich wartości są bardzo zbliżone, to jednak z uwagi na znacznie wyższą postrzeganą jasność zielonego koloru jest on na obrazku dominujący, co można zauważyć na obrazku podzielonym na kanały. Kanał niebieski ma mały udział na obrazku i występuje tylko w pewnej jego części. Wybrany obrazek jest całkowicie nieprzezroczysty, więc jego kanał alfa ma wartość 1 dla wszystkich pikseli i jest pomijany w analizie.

boxplot(as.numeric(r), as.numeric(g), as.numeric(b), as.numeric(a),
        names = c("R", "G", "B", "A"), col = c("red", "green", "blue", "gray"),
        main = "Rozkład wartości poszczególnych kanałów")

par(mfrow = c(1, 3), mar = c(0, 1, 0, 1))
channel_r <- array(c(r, black, black, a), dim = d)
plot(image_read(channel_r))
channel_g <- array(c(black, g, black, a), dim = d)
plot(image_read(channel_g))
title("\n\n\n\n\n\nObrazek podzielony na kanały")
channel_b <- array(c(black, black, b, a), dim = d)
plot(image_read(channel_b))

2.2. PCA

Dla każdego kanału koloru jest przeprowadzany algorytm PCA.

pca_r <- prcomp(r, center = FALSE, scale. = FALSE)
pca_g <- prcomp(g, center = FALSE, scale. = FALSE)
pca_b <- prcomp(b, center = FALSE, scale. = FALSE)

W analizie zostanie wykorzystana funkcja do kompresji kanałów za pomocą PCA, przyjmująca jako parametry dane PCA oraz liczbę składowych do wykorzystania. Oprócz standardowego mnożenia macierzy do uzyskania skompresowanego kanału, wynik jest ograniczany do przedziału od 0 do 1.

compress_channel <- function(pca, n) {
  value <- pca$x[, 1:n] %*% t(pca$rotation[, 1:n])
  value[value < 0] <- 0
  value[value > 1] <- 1
  return(value)
}

Analiza zostanie przeprowadzona dla pięciu różnych liczb składowych, z każdą iteracją czterokrotnie mniejszych - dla wybranego obrazka będą to 1024, 256, 64, 16 i 4 składowe.

ns = c(d[1], d[1] / 4, d[1] / 16, d[1] / 64, d[1] / 256)
ns = round(ns)

2.3. Kompresja wszystkich kanałów RGB

Na początku przeprowadzana jest kompresja dla wszystkich kanałów tą samą liczbą składowych. Pierwsze dwa obrazki niewiele się od siebie różnią, na trzecim jest widoczne rozmycie zielonych traw w środku wysokości obrazka oraz tła, natomiast przy dwóch ostatnich widać wyraźne pogorszenie jakości.

for (n in ns){
  r_comp <- compress_channel(pca_r, n)
  g_comp <- compress_channel(pca_g, n)
  b_comp <- compress_channel(pca_b, n)
  img_comp <- array(c(r_comp, g_comp, b_comp, a), dim = d)
  plot(image_read(img_comp))
  title(paste0(n, " principal components RGB"))
}

2.4. Kompresja kanału R

W następnych krokach przeprowadzane są po kolei kompresje tylko wybranych kanałów. W przypadku kompresji tylko kanału czerwonego, pierwsze trzy przypadki są praktycznie nierozróżnialne, a dopiero na dwóch ostatnich widoczne jest rozmycie czerwonego koloru, zwłaszcza w obszarach z żółtymi i różowymi kwiatami. Ogólny poziom detali jest jednak zachowany z uwagi na brak kompresji pozostałych kanałów.

par(mfrow = c(1, 2), mar = c(0, 1, 0, 1))
for (n in ns){
  r_comp <- compress_channel(pca_r, n)
  img_comp <- array(c(r_comp, g, b, a), dim = d)
  plot(image_read(img_comp))
  title(paste0("\n\n", n, " principal components R"))
  
  channel <- array(c(r_comp, black, black, a), dim = d)
  plot(image_read(channel))
}

2.5. Kompresja kanału G

W przypadku kompresji kanału zielonego efekty są podobne jak przy kompresji wszystkich kanałów jednocześnie - pogorszenie jakości jest szybsze i wyraźniejsze. Jest to związane z dominacją zielonego koloru na obrazku, co zostało wcześniej zauważone.

par(mfrow = c(1, 2), mar = c(0, 1, 0, 1))
for (n in ns){
  g_comp <- compress_channel(pca_g, n)
  img_comp <- array(c(r, g_comp, b, a), dim = d)
  plot(image_read(img_comp))
  title(paste0("\n\n", n, " principal components G"))
  
  channel <- array(c(black, g_comp, black, a), dim = d)
  plot(image_read(channel))
}

2.6. Kompresja kanału B

Z uwagi na bardzo mały udział niebieskiego kanału w obrazku, jego kompresja nie wpływa na jakość skompresowanej grafiki. Dopiero przy najmniejszej liczbie składowych widać zmianę koloru w górnej części obrazka, która jednak nie pogarsza poziomu detali.

par(mfrow = c(1, 2), mar = c(0, 1, 0, 1))
for (n in ns){
  b_comp <- compress_channel(pca_b, n)
  img_comp <- array(c(r, g, b_comp, a), dim = d)
  plot(image_read(img_comp))
  title(paste0("\n\n", n, " principal components B"))
  
  channel <- array(c(black, black, b_comp, a), dim = d)
  plot(image_read(channel))
}

3. Kompresja mieszana

Na podstawie powyższej analizy można dla badanego obrazka zaproponować następującą konfigurację kompresji: wykorzystanie “trzeciego poziomu” (64 składowych) dla kanału R, “drugiego poziomu” (256 składowych) dla kanału G oraz “czwartego poziomu” (16 składowych) dla kanału B. Jest to konfiguracja minimalizująca liczbę zastosowanych składowych przy jednoczesnym utrzymaniu jak najwyższej jakości grafiki i podobieństwa do oryginału.

par(mfrow = c(1, 2), mar = c(0, 1, 0, 1))
plot(image_read(img))
title("\n\nObrazek nieskompresowany\n(R 1024 G 1024 B 1024)")

r_comp <- compress_channel(pca_r, ns[3])
g_comp <- compress_channel(pca_g, ns[2])
b_comp <- compress_channel(pca_b, ns[4])
img_comp <- array(c(r_comp, g_comp, b_comp, a), dim = d)
plot(image_read(img_comp))
title("\n\nObrazek skompresowany\n(R 64 G 256 B 16)")

Skompresowany obrazek zajmuje około 13% mniej przestrzeni na dysku od oryginału.

s1 <- file.info(img_path)$size
s1
## [1] 2486243
img_comp_path <- "data_2_comp.png"
writePNG(img_comp, img_comp_path)
s2 <- file.info(img_comp_path)$size
s2
## [1] 2156881
(s1 - s2) / s1
## [1] 0.1324738