Задание №5. Дискретное косинус-преобразование

Задача является очередным расширением задания №1 и в некоторой степени похожа на задание №3. Нужно реализовать диалог, который будет имитировать JPEG-подобное сжатие изображения. Имитация заключается в том, чтобы показать ожидаемый коэффициент сжатия и картинку с характерными искажениями, но реального сжатия не осуществлять.

Алгоритм

Кодирования

  1. Изображение из RGB переводится в YUV
  2. Каждый канал Y, U, V разбивается на квадратные блоки размера \(8 \times 8\) пикселей (можно \(12 \times 12\) по желанию)
  3. Над каждым блоком проделывается дискретное косинус-преобразование
  4. Результат преобразования квантуется согласно матрице квантования (одна для Y, другая для U, V)
  5. По результатам квантования вычисляется коэффициент сжатия

Декодирования

  1. Результаты квантования умножаются на соответствующий квант согласно матрице квантования
  2. Над каждым блоком проделывается обратное дискретное косинус-преобразование
  3. Из блоков собирается канал Y, U, V
  4. Изображение из YUV переводится в RGB (это и есть измененное изображение, которое нужно показать)

Пусть размер изображения \(w\) на \(h\) пикселей. Если реальные размеры не кратны \(8\) (или \(12\) в случае \(12 \times 12\)), то “лишние” пиксели далее в подсчетах не участвуют. Далее считаем, что размеры изображения кратны размеру блока. Следовательно, размер \(size\) исходного изображения равен \(3 \times w \times h\) байт.

Коэффициент сжатия

Вычисляется как отношение:

\[ratio = \frac{size}{entropy\_size}\]

Размер по энтропии \(entropy\_size\) для всего изображения равен сумме размеров по энтропии каждого канала. Как вычислить размер по энтропии для канала? Берем результат косинус-преобразования для каждого блока и получаем \(64\) (или \(144\) в случае \(12 \times 12\)) числовых последовательности. Каждая последовательность содержит значения из фиксированной позиции в матрице. Например, последовательность из всех коэффициентов в позиции \(\left(0, 0\right)\), т.е. левого верхнего угла матрицы. Для каждой последовательности считается энтропия и умножается на длину последовательности (количество блоков). Полученные результаты суммируются.

Точность вычислений

Все вычисления нужно делать с плавающей точкой (double) и производить округление до целых чисел только там, где это необходимо. Например, преобразование из RGB в YUV нужно делать без округления (значения Y, U и V каналов остаются нецелыми). Результат косинус-преобразования нужно поделить на квант, а только потом округлять до целого числа, которое будет участвовать в подсчете энтропии. В процессе декодирования округлять до целых следует лишь на последнем этапе преобразования из YUV в RGB. При этом нужно не забыть сделать процедуру \(clip\) — значения меньше \(0\) переходят в \(0\), значения больше \(255\) переходят в \(255\).

Преобразование

Пусть \(DCT\) — матрица дискретного косинус-преобразования (является ортогональной), \(DCT^T\) — транспонированная к ней (\(DCT^{-1} = DCT^T\) из-за ортогональности), \(BLOCK\) — матрица с пикселями изображения, \(RESULT\) — результат преобразования. Тогда прямое (direct) преобразование:

\[RESULT = DCT \cdot BLOCK \cdot DCT^T\]

и обратное (inverse) преобразование:

\[BLOCK = DCT^T \cdot RESULT \cdot DCT\]

В матрице \(RESULT\) каждый элемент делится на соответствующий квант из матрицы квантования или сразу зануляется, если в матрице квантования стоит 0.

Общая формула для дискретного косинус-преобразования вектора \(p = \left(p_0, p_1, \ldots, p_{n-1}\right)\) в \(G = \left(G_0, G_1, \ldots, G_{n-1}\right)\):

\[G_f = \sqrt{\frac{2}{n}}C_f \sum_{t=0}^{n-1}p_t \cos\left[\frac{(2t+1)f\pi}{2n}\right], \text{где } C_0 = \frac{1}{\sqrt{2}} \text{ и } C_1, \ldots, C_{n-1} = 1.\]

Обратное преобразование вектора \(G = \left(G_0, G_1, \ldots, G_{n-1}\right)\) в \(p = \left(p_0, p_1, \ldots, p_{n-1}\right)\):

\[p_t = \sqrt{\frac{2}{n}}\sum_{t=0}^{n-1}C_j G_j \cos\left[\frac{(2t+1)j\pi}{2n}\right].\]

В нашем случае преобразуется не вектор, а набор векторов — столбцы матрицы \(BLOCK\). Произведение \(DCT \cdot BLOCK\) дает матрицу, в который каждый столбец является результатом преобразования соответствующего столбца из \(BLOCK\). Далее \(DCT \cdot BLOCK\) подвергается повторному преобразованию, но в качестве векторов уже выступают строки, а не столбцы. Отсюда повторное умножение справа на \(DCT^T\). Дополнительно про матрицу \(DCT\) можно посмотреть, например, в стандарте JPEG, секция A.3.3.

Согласно формулам, приведенным выше, вычисление матрицы \(DCT\) произвольного размера на языке Java:

public static double[][] dct(int size) {
    final double a = Math.sqrt(2.0 / size);
    final double b = Math.PI / (2.0 * size);
    final double c = Math.sqrt(1.0 / size);

    final double[][] m = new double[size][size];
    for (int j = 0; j < size; ++j) {
        m[0][j] = c;
    }

    for (int i = 1; i < size; ++i) {
        for (int j = 0; j < size; ++j) {
      m[i][j] = a * Math.cos(b * (2 * j + 1) * i);
  }
    }

    return m;
}

Интерфейс

Диалог должен позволять задавать обе матрицы квантования (одна для Y, другая для U, V). Каждый элемент меняется в диапазоне от \(0\) до \(200\). Если стоит \(0\), то соответствующий элемент в результирующей матрице зануляется без квантования. Если стоит отличное от \(0\) число, то оно используется в качестве кванта. В диалоге выводится ожидаемый коэффициент сжатия. Примерный внешний вид:

DCT Dialog

В диалоге используется параметр \(Quality\), который позволяет быстро задать все элементы обеих матриц квантования. Делать этом можно любым разумным способом, основанном на следующих утверждениях:

Например, можно воспользоваться формулой \(1 + \left(i + j\right)\cdot Quality\) для получения кванта на месте \(\left(i, j\right)\) в матрице квантования. Если эту формулу использовать для канала Y, то для U, V можно взять \(1 + \left(i + j\right)\cdot \left(Quality + \Delta\right)\), где \(\Delta > 0\). В данном случае, чем больше параметр \(Quality\), тем больше сжатие и меньше реальное качество картинки.