Документация Engee

Типы и трансформация изображений

Цель данной демонстрации – показать способы задания изображений и базовые принципы их преобразований, в особенности опираясь на аффинную трансформацию.

using Images # Библиотека обработки изображений
using ImageShow # Библиотека отрисовки изображений
using TestImages # Библиотека тестовых изображений

Типы цветовых пространств

Любое изображение – это просто массив пиксельных объектов. Элементы изображения называются пикселями, а Julia Images рассматривает пиксели как первоклассные объекты. Например, у нас есть Gray-пиксели в оттенках серого, RGB-пиксели цвета, Lab-пиксели цвета.

Начнём анализ с формата RGB (аббревиатура, образованная от английских слов red, green, blue – красный, зелёный, синий) – аддитивная цветовая модель, описывающая способ кодирования цвета для цветовоспроизведения с помощью трёх цветов, которые принято называть основными. Выбор основных цветов обусловлен особенностями физиологии восприятия цвета сетчаткой нашего глаза.

img_rgb = [RGB(1.0, 0.0, 0.0), RGB(0.0, 1.0, 0.0), RGB(0.0, 0.0, 1.0)]

interactive-scripts/images/image_processing_affine/578f545baddca3c41ae6b083b1fd6c4ed440de89

dump(img_rgb)
Array{RGB{Float64}}((3,))
  1: RGB{Float64}
    r: Float64 1.0
    g: Float64 0.0
    b: Float64 0.0
  2: RGB{Float64}
    r: Float64 0.0
    g: Float64 1.0
    b: Float64 0.0
  3: RGB{Float64}
    r: Float64 0.0
    g: Float64 0.0
    b: Float64 1.0

Gray – это однокональная матрица, описывающая изображения в оттенках серого цвета. По умолчанию используется 8-битное кодирование цвета.

img_gray = rand(Gray, 3, 3)

interactive-scripts/images/image_processing_affine/b6f85b8d91954d0612be7320bf2ce727d547b43f

dump(img_gray)
Array{Gray{Float64}}((3, 3))
  1: Gray{Float64}
    val: Float64 0.20548107200013277
  2: Gray{Float64}
    val: Float64 0.7190843096024139
  3: Gray{Float64}
    val: Float64 0.7675684128936745
  4: Gray{Float64}
    val: Float64 0.4840881004197154
  5: Gray{Float64}
    val: Float64 0.21892864471268947
  6: Gray{Float64}
    val: Float64 0.5776559337062981
  7: Gray{Float64}
    val: Float64 0.855915975791712
  8: Gray{Float64}
    val: Float64 0.7152103286181891
  9: Gray{Float64}
    val: Float64 0.9976538325486705

LAB – аббревиатура названия двух разных (хотя и похожих) цветовых пространств. Более известным и распространенным является CIELAB (точнее, CIE 1976 Lab*), другое – Hunter Lab (точнее, Hunter L, a, b). Таким образом, Lab – это неформальная аббревиатура, не определяющая цветовое пространство однозначно. В Engee, говоря о пространстве Lab, подразумевают CIELAB.

img_lab = rand(Lab, 3, 3)

interactive-scripts/images/image_processing_affine/6a2e6924835fe87490fab93ec46950a16f511ddc

dump(img_gray)
Array{Gray{Float64}}((3, 3))
  1: Gray{Float64}
    val: Float64 0.20548107200013277
  2: Gray{Float64}
    val: Float64 0.7190843096024139
  3: Gray{Float64}
    val: Float64 0.7675684128936745
  4: Gray{Float64}
    val: Float64 0.4840881004197154
  5: Gray{Float64}
    val: Float64 0.21892864471268947
  6: Gray{Float64}
    val: Float64 0.5776559337062981
  7: Gray{Float64}
    val: Float64 0.855915975791712
  8: Gray{Float64}
    val: Float64 0.7152103286181891
  9: Gray{Float64}
    val: Float64 0.9976538325486705

Перевод между типами объектов

Gray.(img_rgb) # RGB => Gray

interactive-scripts/images/image_processing_affine/dd76f3f082ebcba0c35be349e72a0d365bf840b9

RGB.(img_gray) # Gray => RGB

interactive-scripts/images/image_processing_affine/9b46d516c51be619a3f7170677b3d1be125cb42d

RGB.(img_lab) # Lab => RGB

interactive-scripts/images/image_processing_affine/6a2e6924835fe87490fab93ec46950a16f511ddc

Трансформация изображений

Для начала загрузим изображение из файла .jpg.

img = load( "$(@__DIR__)/4028965.jpg" )

interactive-scripts/images/image_processing_affine/0a25aee679e31a72de4facafa9f0df300f056026

Увеличим контрастность загруженного изображения. Функция adjust_histogram(Equalization(),…​) может обрабатывать различные типы входных данных. Тип возвращаемого изображения соответствует типу ввода. Для цветных изображений вход преобразуется в тип YIQ, а канал Y выравнивается, после чего он объединяется с каналами I и Q.

alg = Equalization(nbins = 256)
img_adjusted = adjust_histogram(img, alg)

interactive-scripts/images/image_processing_affine/ed98022d4b650920bce857fb4a93a61b27e68e1f

Уменьшим размер изображения в 4 раза относительно исходника. Imresize позволяет изменять размер, используя отношения относительно исходного изображения, как показано в примере ниже, а также позволяет изменять размер с использованием ручного задания размерностей нового изображения, например:

imresize(img, (400, 400)).

img_small = imresize(img_adjusted, ratio=1/4)

interactive-scripts/images/image_processing_affine/d68dbd405ef5fbaa01be6cca3b1915b6f4934d23

print(size(img_adjusted), " --> ", size(img_small))
(1148, 1243) --> (287, 311)

Аффинное преобразование (от лат. affinis «соприкасающийся, близкий, смежный») – отображение плоскости или пространства в себя, при котором параллельные прямые переходят в параллельные прямые, пересекающиеся – в пересекающиеся, скрещивающиеся – в скрещивающиеся. Базовые пребразования изображений используют решетку индексов для оперирования действиями над изображением. Преобразование задаётся матрицей трансформации изображения по принципу, описанному на картинке ниже. image.png

# Вспомогательная функция контроля размерностей
function C_B_V(x, max_val)
    x[x .> max_val - 1] .= max_val - 1
    x[x .< 1] .= 1
    return x
end
C_B_V (generic function with 1 method)

Далее объявим функцию аффинной трансформации изображения, в которой:

  1. theta – это матрица трансформации;

  2. img – это входное изображение;

  3. out_size – размеры выходного изображения;

  4. grid – решётка пиксельной индексации.

function transform(theta, img, out_size)
    grid = grid = zeros(3, out_size[1]*out_size[2])
    grid[1, :] = reshape(((-1:2/(out_size[1]-1):1)*ones(1,out_size[2])), 1, size(grid,2))
    grid[2, :] = reshape((ones(out_size[1],1)*(-1:2/(out_size[2]-1):1)'), 1, size(grid,2))
    grid[3, :] = ones(Int, size(grid, 2))

    # Умножение theta на grid
    T_g = theta * grid

    # Вычисление координат x, y
    x = (T_g[1, :] .+ 1) .* (out_size[2]) / 2
    y = (T_g[2, :] .+ 1) .* (out_size[1]) / 2

    # Округление координат
    x0 = ceil.(x)
    x1 = x0 .+ 1
    y0 = ceil.(y)
    y1 = y0 .+ 1

    # Обрезание значений x0, x1, y0, y1
    x0 = C_B_V(x0, out_size[2])
    x1 = C_B_V(x1, out_size[2])
    y0 = C_B_V(y0, out_size[1])
    y1 = C_B_V(y1, out_size[1])

    # Вычисление базовых координат
    base_y0 = y0 .* out_size[1]
    base_y1 = y1 .* out_size[1]

    # Работа с изображением
    im_flat = reshape(img, :)

    # Обрабатываем координаты
    A = (x1 .- x) .* (y1 .- y) .* im_flat[Int.(base_y0 .+ x0 .+ 1)]
    B = (x1 .- x) .* (y .- y0) .* im_flat[Int.(base_y1 .+ x0 .+ 1)]
    C = (x .- x0) .* (y1 .- y) .* im_flat[Int.(base_y0 .+ x1 .+ 1)]
    D = (x .- x0) .* (y .- y0) .* im_flat[Int.(base_y1 .+ x1 .+ 1)]

    # Расчет результата
    result = reshape((A .+ B .+ C .+ D), (out_size[1], out_size[2]))
    return result
end
transform (generic function with 1 method)

Для начала применим эту функцию к изображению в оттенках серого.

img_sg = Gray.(img_small)

interactive-scripts/images/image_processing_affine/ab2aa5208a6a7fff24caaf61b5425cd69cdd2287

Как мы видим из данных ниже, изображения серого цвета имеют 8-битную разрядность цветов, и размерность её представлена только шириной и высотой.

dump(img_sg[1])
Gray{N0f8}
  val: N0f8
    i: UInt8 0x0e
size(img_sg)
(287, 311)

Зададим для данного изображения матрицу трансформации.

theta = [2 0.3 0; -0.3 2 0]
2×3 Matrix{Float64}:
  2.0  0.3  0.0
 -0.3  2.0  0.0

Применим нашу функцию к изображению. Как мы видим, размер уменьшен в два раза и выполнен поворот.

img_transfor = transform(theta, img_sg, [size(img_sg,1),size(img_sg,2)])

interactive-scripts/images/image_processing_affine/8dc56c47e53c47a78a5f81fd7f9138df01f94e64

Для получения обратного преоброзования найдём обратную матрицу от матрицы трансформации и выполним её округления до четвёртого знака.

theta_inv = hcat(inv(theta[1:2,1:2]), [-0.1;0.1])
theta_inv = round.(theta_inv.*10^4)./10^4
2×3 Matrix{Float64}:
 0.489   -0.0733  -0.1
 0.0733   0.489    0.1
img_sg_new = transform(theta_inv, img_transfor, [size(img_transfor,1),size(img_transfor,2)])

interactive-scripts/images/image_processing_affine/a85c28bb07c71f1b4cd977ff6e4b3bbcabf2fd0d

Теперь применим данную функцию к изображению формата RGB. Для начала преобразуем изображения формата RGB к канальному представлению. Проанализируем возможности, которые перед нами открываются при таком варианте представлении изображения.

img_CHW = channelview(img_small);
print(size(img_small), " --> ", size(img_CHW))
(287, 311) --> (3, 287, 311)

Выбираем красный канал изображения и рисуем только красный канал.

RGB.(img_CHW[1,:,:], 0.0, 0.0) # red

interactive-scripts/images/image_processing_affine/fcacdaae77923c22894a7f55cd6af36d69600634

Если все каналы распределены равномерно, то мы получим изображения в оттенках серого, так как ни один из базовых цветов не преобладает над остальными.

RGB.(img_CHW[1,:,:], img_CHW[1,:,:], img_CHW[1,:,:]) # Gray

interactive-scripts/images/image_processing_affine/293f5e0841c83944ea65c620ab2a04d33df19438

Опираясь на пример с канальным изображением, мы можем реализовать аффинную транформацию для RGB-изображения, прогнав его через нашу функцию как три отдельных одномерных матрицы и объединив их результаты.

img_CHW_new = zeros(size(img_CHW))

for i in 1:size(img_CHW,1)
   img_CHW_new[i,:,:] = transform(theta, img_CHW[i,:,:], [size(img_CHW,2),size(img_CHW,3)])
end

RGB.(img_CHW_new[1,:,:], img_CHW_new[2,:,:], img_CHW_new[3,:,:])

interactive-scripts/images/image_processing_affine/1ee1e9152965c16ae3ed7f150f61a291fd99583f

Вывод

В данной демонстрации мы разобрались с канальными представлениями и различными типами матричного представления изображений, а также проанализировали некоторые возможности обработки изображений в Engee.