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

Преобразования и представления

Совместное использование памяти: введение в представления

В разделе Массивы, числа и цвета мы обсуждали, как можно преобразовать тип элементов массива a = [1,2,3,4], используя синтаксис наподобие Float64.(a). Вам может быть интересно, какой эффект дает применение Int.(a), если он вообще есть:

julia> a = [1,2,3,4]
4-element Vector{Int64}:
 1
 2
 3
 4

julia> b = Int.(a)
4-element Vector{Int64}:
 1
 2
 3
 4

Никаких очевидных изменений, естественно, не произошло, и, как и ожидалось, b == a возвращает true. Помимо одинакового размера и элементов, существует и более широкое понимание «одинаковости»: ссылаются ли a и b на одну и ту же область в памяти? Это можно проверить следующими способами:

julia> a === b   # обратите внимание: 3 знака равенства!
false

или, в более общем случае, задав одно значение и посмотрев, отражается ли изменение в другом:

julia> b[1] = 5
5

julia> b
4-element Vector{Int64}:
 5
 2
 3
 4

julia> a
4-element Vector{Int64}:
 1
 2
 3
 4

Поскольку типы объектов a и b идентичны, обе проверки говорят нам о том, что a и b являются независимыми объектами, даже если у них (изначально) были одинаковые значения.

Связано это с тем, что вызов f.(a) (который вызывает функцию broadcast(f, a)) всегда выделяет в памяти новый массив для возврата значений. Однако так работают не все функции. Хорошим примером может служить view:

julia> v = view(a, :)
4-element view(::Vector{Int64}, :) with eltype Int64:
 1
 2
 3
 4

v и a имеют одинаковые значения, но это опять-таки разные объекты:

julia> v == a
true

julia> v === a
false

Однако они занимают одну и ту же область памяти:

julia> v[1] = 10
10

julia> v
4-element view(::Vector{Int64}, :) with eltype Int64:
 10
  2
  3
  4

julia> a
4-element Vector{Int64}:
 10
  2
  3
  4

Следовательно, v — это «представление» значений, хранящихся в a. Это простейший вариант использования функции view. В более общем случае ее можно использовать для выбора прямоугольной области интереса, что является обычной операцией при обработке изображений. Эта область выбирается без копирования каких-либо данных, и любые изменения значений в ней отражаются в исходном (родительском) массиве. Дополнительные сведения см. в документации по view, которую можно просмотреть, введя ?view.

view — это не единственная функция с таким свойством; еще одним хорошим примером является функция reshape, с помощью которой можно изменять измерения массива:

julia> r = reshape(a, 2, 2)
2×2 Matrix{Int64}:
 10  3
  2  4

julia> r[1,2] = 7
7

julia> r
2×2 Matrix{Int64}:
 10  7
  2  4

julia> a
4-element Vector{Int64}:
 10
  2
  7
  4

Обратите внимание, что возвращаемый тип reshape — это просто массив Array, который служит представлением a. Однако для некоторых входных данных невозможно создать представление в виде Array. Например:

julia> r = reshape(1:15, 3, 5)
3×5 reshape(::UnitRange{Int64}, 3, 5) with eltype Int64:
 1  4  7  10  13
 2  5  8  11  14
 3  6  9  12  15

UnitRange представлен компактно — хранятся только начальные и конечные значения, поэтому получить доступ ко всем значениям, обратившись к определенной области памяти, невозможно. В таких случаях reshape возвращает тип ReshapedArray, который является универсальным «типом представления», поддерживающим изменение формы любого AbstractArray.

Результатом обеих функций view и reshape всегда является представление: внесите изменение в родительский объект, и оно отразится в представлении, и наоборот.

Представления для «преобразования» между представлением с фиксированной запятой и необработанным представлением

В разделе Массивы, числа и цвета также были рассмотрены числа с фиксированной запятой, используемые в некоторых представлениях цветовой (или полутоновой) информации. Для изменения представления можно воспользоваться функцией reinterpret:

julia> using FixedPointNumbers

julia> x = 0.5N0f8
0.502N0f8

julia> y = reinterpret(x)  # либо используйте reinterpret(UInt8, x)
0x80

julia> reinterpret(N0f8, y)
0.502N0f8

Ее можно применять к массивам:

julia> a = [0.2N0f8, 0.8N0f8]
2-element Array{N0f8,1} with eltype N0f8:
 0.2N0f8
 0.8N0f8

julia> b = reinterpret.(a)
2-element Vector{UInt8}:
 0x33
 0xcc

Из-за вызова f.(a) объект b не находится в той же области памяти, что и a:

julia> b[2] = 0xff
0xff

julia> a
2-element Array{N0f8,1} with eltype N0f8:
 0.2N0f8
 0.8N0f8

Часто это не является проблемой, но иногда может потребоваться, чтобы они ссылались на один и тот же базовый объект. Для таких случаев в JuliaImages с помощью пакета ImageCore (который входит в состав Images) реализованы представления, которые могут выполнять такую переинтерпретацию:

julia> using Images

julia> v = rawview(a)
2-element reinterpret(UInt8, ::Array{N0f8,1}):
 0x33
 0xcc

julia> v[2] = 0xff
0xff

julia> a
2-element Array{N0f8,1} with eltype N0f8:
 0.2N0f8
 1.0N0f8

Из приведенного ниже кода ясно, что v является неизменяемым объектом или просто ссылкой на a. Для него не выделена отдельная память:

julia> a = [0.2N0f8,0.8N0f8]
2-element Array{N0f8,1} with eltype N0f8:
 0.2N0f8
 0.8N0f8

julia> v = rawview(a)
2-element reinterpret(UInt8, ::Array{N0f8,1}):
 0x33
 0xcc
julia> pointer_from_objref(a) #функция, используемая для нахождения адреса объекта
Ptr{Nothing} @0x000000011cbb19f0

julia> pointer_from_objref(v) #v — это просто неизменяемая ссылка на a, для нее не выделена отдельная память.
ERROR: pointer_from_objref cannot be used on immutable objects
#ERROR: pointer_from_objref cannot be used on immutable objects
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:33
 [2] pointer_from_objref(x::Any)
   @ Base ./pointer.jl:146
 [3] top-level scope
   @ none:1

Обратное преобразование — normedview:

julia> c = [0x11, 0x22]
2-element Vector{UInt8}:
 0x11
 0x22

julia> normedview(c)
2-element reinterpret(N0f8, ::Vector{UInt8}):
 0.067N0f8
 0.133N0f8

normedview позволяет передавать интерпретируемый тип в первом аргументе, то есть normedview(N0f8, A), и это действительно необходимо, если только A не имеет тип элементов UInt8; в этом случае normedview предполагает, что требуется N0f8.

Как и функция reshape, функции rawview и normedview могут возвращать Array или более сложный тип (ReinterpretArray или MappedArray из пакета MappedArrays) в зависимости от типа входных данных.

Цветоделение: представления для преобразования чисел в цвета и наоборот

В разделе Массивы, числа и цвета отмечалось, что числовой массив можно преобразовать в массив оттенков серого с помощью Gray.(a). Обратное преобразование можно выполнить с помощью real.(b). Обработка цветов RGB немного сложнее, поскольку меняется размерность массива. Один из подходов — использовать включения в Julia:

julia> a = reshape(collect(0.1:0.1:0.6), 3, 2)
3×2 Matrix{Float64}:
 0.1  0.4
 0.2  0.5
 0.3  0.6

julia> c = [RGB(a[1,j], a[2,j], a[3,j]) for j = 1:2]
2-element Array{RGB{Float64},1} with eltype RGB{Float64}:
 RGB{Float64}(0.1,0.2,0.3)
 RGB{Float64}(0.4,0.5,0.6)

julia> x = [getfield(c[j], i) for i = 1:3, j = 1:2]
3×2 Matrix{Float64}:
 0.1  0.4
 0.2  0.5
 0.3  0.6

Хотя такой подход работает, он не лишен недостатков:

  • Такая реализация предполагает двухмерность a; трехмерный массив (представляющий двухмерное цветное изображение) потребует другой реализации.

  • Использование getfield предполагает, что элементы c имеют поля, следующие в порядке r, g, b. Учитывая большое количество различных представлений RGB, поддерживаемых ColorTypes, ни в одном из этих предположений нет полной уверенности.

  • Всегда создается копия данных.

Для устранения этих недостатков JuliaImages предоставляет две взаимодополняющие функции представлений: colorview и channelview:

julia> colv = colorview(RGB, a)
2-element reinterpret(reshape, RGB{Float64}, ::Matrix{Float64}) with eltype RGB{Float64}:
 RGB{Float64}(0.1,0.2,0.3)
 RGB{Float64}(0.4,0.5,0.6)

julia> chanv = channelview(c)
3×2 reinterpret(reshape, Float64, ::Array{RGB{Float64},1}) with eltype Float64:
 0.1  0.4
 0.2  0.5
 0.3  0.6

colorview и channelview всегда возвращают представление исходного массива.

Использование colorview для создания цветных наложений

Еще одно применение colorview — объединение нескольких изображений в оттенках серого в одно цветное. Например:

using Colors, Images
r = range(0,stop=1,length=11)
b = range(1,stop=0,length=11)
img1d = colorview(RGB, r, zeroarray, b)

# output

11-element mappedarray(RGB{Float64}, ImageCore.extractchannels, ::StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}, ::ImageCore.ZeroArray{Float64, 1, Base.OneTo{Int64}}, ::StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}) with eltype RGB{Float64}:
 RGB{Float64}(0.0,0.0,1.0)
 RGB{Float64}(0.1,0.0,0.9)
 RGB{Float64}(0.2,0.0,0.8)
 RGB{Float64}(0.3,0.0,0.7)
 RGB{Float64}(0.4,0.0,0.6)
 RGB{Float64}(0.5,0.0,0.5)
 RGB{Float64}(0.6,0.0,0.4)
 RGB{Float64}(0.7,0.0,0.3)
 RGB{Float64}(0.8,0.0,0.2)
 RGB{Float64}(0.9,0.0,0.1)
 RGB{Float64}(1.0,0.0,0.0)

дает (в IJulia) такой результат:

linspace

zeroarray — это специальная константа, которая «расширяется» для возврата эквивалента массива, состоящего из одних нулей, с осями, соответствующими другим входным данным colorview.

Изменение порядка измерений

Когда цвета разделены на отдельные цветовые измерения, в том или ином коде может предполагаться, что цвет — это последнее (самое медленное) измерение. Преобразование можно выполнить напрямую с помощью функции Julia permutedims:

julia> pc = permutedims(a, (2,1))
2×3 Matrix{Float64}:
 0.1  0.2  0.3
 0.4  0.5  0.6

permutedims явным образом создает новый массив с данными, переупорядоченными в памяти. Нечто подобное можно сделать и в виде представления:

julia> pv = PermutedDimsArray(a, (2,1))
2×3 PermutedDimsArray(::Matrix{Float64}, (2, 1)) with eltype Float64:
 0.1  0.2  0.3
 0.4  0.5  0.6

Хотя результат выглядит похоже, pv (в отличие от pc) размещается в той же области памяти, что и a; это мнимая перестановка, достигаемая за счет того, что при обращении к отдельным элементам индексы в PermutedDimsArray переставляются относительно входных индексов.

Следует учитывать, что производительность при этих двух подходах может различаться по причинам, связанным с особенностями работы процессоров и памяти, а не с ограничениями Julia. Если a имеет большой размер и нужно получить доступ ко всем трем элементам, соответствующим цветовым каналам одного пикселя, вариант с pv, вероятно, будет более эффективным, поскольку значения расположены рядом в памяти и поэтому, скорее всего, будут находиться в одной строке кэша. И наоборот, если нужно последовательно получить доступ к разным пикселям одного цветового канала, вариант с pc может оказаться более эффективным (по той же причине).

Дополнение

Иногда сравниваемые изображения могут иметь разные размеры. Создать представления массивов с общими индексами можно с помощью функции paddedviews:

julia> a1 = reshape([1,2], 2, 1)
2×1 Matrix{Int64}:
 1
 2

julia> a2 = [1.0,2.0]'
1×2 adjoint(::Vector{Float64}) with eltype Float64:
 1.0  2.0

julia> a1p, a2p = paddedviews(0, a1, a2);   # 0 — это значение заполнения

julia> a1p
2×2 PaddedView(0, ::Matrix{Int64}, (Base.OneTo(2), Base.OneTo(2))) with eltype Int64:
 1  0
 2  0

julia> a2p
2×2 PaddedView(0.0, adjoint(::Vector{Float64}), (Base.OneTo(2), Base.OneTo(2))) with eltype Float64:
 1.0  2.0
 0.0  0.0

Это может быть особенно полезно в сочетании с colorview для сравнения двух (или более) изображений в оттенках серого. Дополнительные сведения см. в разделе Keeping track of location with unconventional indices.

StackedView

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

julia> img1 = reshape(1:8, (2,4))
2×4 reshape(::UnitRange{Int64}, 2, 4) with eltype Int64:
 1  3  5  7
 2  4  6  8

julia> img2 = reshape(11:18, (2,4))
2×4 reshape(::UnitRange{Int64}, 2, 4) with eltype Int64:
 11  13  15  17
 12  14  16  18

julia> sv = StackedView(img1, img2)
2×2×4 StackedView{Int64, 3, Tuple{Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}}:
[:, :, 1] =
  1   2
 11  12

[:, :, 2] =
  3   4
 13  14

[:, :, 3] =
  5   6
 15  16

[:, :, 4] =
  7   8
 17  18

julia> imgMatrix = reshape(sv, (2, 8))
2×8 reshape(::StackedView{Int64, 3, Tuple{Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}}, 2, 8) with eltype Int64:
  1   2   3   4   5   6   7   8
 11  12  13  14  15  16  17  18

Отделение представлений от родительской памяти

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

Составные представления (и краткие сводки)

Когда массив в Julia отображается в виде текста, в начале обычно выводится однострочная сводка с типом массива. Возможно, вы уже заметили, что в JuliaImages используется нетрадиционный синтаксис для обобщения информации об определенных видах массивов. Например, приведенный выше массив pv имеет следующий тип:

PermutedDimsArray{Float64,2,(2,1),(2,1),Matrix{Float64}}

Однако при отображении такого объекта в строке сводки выводится следующее:

2×3 PermutedDimsArray(::Matrix{Float64}, (2, 1)) with eltype Float64

Это сделано для того, чтобы информацию о типах было удобнее читать.

Основная причина в том, что представления различных типов можно свободно комбинировать, но тип при этом становится довольно длинным. Например, предположим, на диске есть файл с массивом m×n×3×t UInt8, представляющим собой фильм RGB (t — это ось времени). Чтобы отобразить его как фильм RGB, можно создать следующее представление массива A:

julia> A = rand(UInt8, 5, 6, 3, 10);


julia> mov = colorview(RGB, normedview(PermutedDimsArray(A, (3,1,2,4))));


julia> summary(mov)
"5×6×10 reinterpret(reshape, RGB{N0f8}, normedview(N0f8, PermutedDimsArray(::Array{UInt8, 4}, (3, 1, 2, 4)))) with eltype RGB{N0f8}"

julia> typeof(mov)
Base.ReinterpretArray{RGB{N0f8}, 3, N0f8, MappedArrays.MappedArray{N0f8, 4, PermutedDimsArray{UInt8, 4, (3, 1, 2, 4), (2, 3, 1, 4), Array{UInt8, 4}}, ImageCore.var"#41#42"{N0f8}, typeof(reinterpret)}, true}

Хотя использование представлений в JuliaImage почти или вообще не влияет на производительность, иногда типы могут становиться сложными!