Отрисовка «пиксель в пиксель»
|
Страница в процессе перевода. |
Допустим, у вас имеются данные в матрице и вы хотите построить график на их основе в точности так, как есть. Если говорить более конкретно, нужно, чтобы каждый изображенный пиксель сэмплировался ровно из одного значения матрицы без интерполяции. Здесь вы узнаете, как это сделать.
heatmap и image
Чтобы построить матрицу значений, можно использовать изображение (image) или тепловую карту (heatmap). При настройках по умолчанию image интерполируется и использует цветовую карту в оттенках серого, а heatmap пикселизируется и использует цветную цветовую карту (viridis). Они также отличаются расположением ячеек или «пикселей». В image можно задать места начала и окончания графика, то есть задать, где находится левый край самого левого пикселя и правый край самого правого пикселя. (То же самое для нижних и верхних пикселей.) В heatmap обычно задаются центры ячеек, хотя также можно задать края, передав значения x и y size + 1. При правильных настройках оба могут выглядеть одинаково.
using CairoMakie
using CairoMakie
data = [1 2; 3 4; 5 6]
f = Figure()
a1, p = image(f[1, 1], data)
a2, p = heatmap(f[1, 2], data)
# 0..3, 0..2 задано умолчанию, можно опустить.
a3, p = image(f[2, 1], 0..3, 0..2, data, colormap = :viridis, interpolate = false)
a4, p = heatmap(f[2, 2], 0:3, 0:2, data, colormap = :viridis, interpolate = false)
# Обратите внимание, что length(0:3), length(0:2) == size(data) .+ 1
limits!.([a1, a2, a3, a4], -1, 4, -1, 3)
f
Полноэкранный график
Рассмотрим случай создания простого изображения на основе некоторых данных, без каких-либо обычных дополнительных элементов оси. Здесь не стоит работать с блоками Figure и Axis, так как они оба занимают пространство за счет заполнения и компоновки макета. Вместо них используем Scene напрямую. Пустая сцена определенного размера может быть создана следующим образом.
using CairoMakie
using CairoMakie
scene = Scene(size = (200, 100), camera = campixel!)
Мы явно задаем camera = campixel!, чтобы сцена использовала единицы пикселей. Если более конкретно, для левого нижнего угла сцены устанавливается (0, 0), а для правого нижнего — size. Используя эти значения, можно построить график image (или heatmap), точно заполняющий сцену.
using CairoMakie
using CairoMakie
data = [ifelse(x > 180, 0, x/100) * ifelse(y > 80, 0, y/50) for x in 1:200, y in 1:100]
scene = Scene(size = (200, 100), camera = campixel!)
# По умолчанию у изображения будут правильные пределы (0..200, 0..100)
image!(scene, data, colormap = :viridis, interpolate = false)
# альтернативно с тепловой картой:
# heatmap!(scene, 0:200, 0:100, data)
scene
Чтобы увеличить изображение, можно просто изменить размер сцены и пределы графика. С heatmap нужно быть немного осторожнее, потому что 0:600 даст 601 значение, а не 201, как нам нужно. Чтобы исправить эту ситуацию, требуется явно включить размер каждой ячейки в качестве шага диапазона.
using CairoMakie
using CairoMakie
data = [ifelse(x > 180, 0, x/100) * ifelse(y > 80, 0, y/50) for x in 1:200, y in 1:100]
scene = Scene(size = (3 * 200, 2 * 100), camera = campixel!)
image!(scene, 0..600, 0..200, data, colormap = :viridis, interpolate = false)
# heatmap!(scene, 0:3:600, 0:2:200, data)
scene
Другим вариантом является изменение px_per_unit при сохранении сцены. При использовании Makie.save(filename, scene, px_per_unit = 2) каждый «пиксель» в сцене представлен двумя пикселями на сохраненном изображении. Это не влияет на пределы графика, т. е. в сцене (200, 100) следует использовать (200, 100) в качестве границ в графиках. (Если просмотреть сгенерированные здесь изображения, можно увидеть, что они имеют вдвое больший размер, чем заданный для сцены, потому что в документации отрисовка выполняется с pixel_per_unit = 2.)
Примечания
Камера
Хотя пиксельная камера интуитивно понятна в использовании, в данном контексте она не нужна. Если сцена создается без камеры, по умолчанию будет использоваться камера пространства отсечения. В таком случае размер координат сцены всегда находится в диапазоне от --1 до 1. Это может немного упростить построение графиков, так как не придется настраивать пределы изображения при настройке пределов сцены.
using CairoMakie
using CairoMakie
data = [ifelse(x > 180, 0, x/100) * ifelse(y > 80, 0, y/50) for x in 1:200, y in 1:100]
scene = Scene(size = (3 * 200, 2 * 100))
image!(scene, -1..1, -1..1, data, colormap = :viridis, interpolate = false)
scene
Аналогичным образом можно использовать camera = cam_relative! для получения координат 0…1.
Сглаживание в GLMakie
GLMakie использует FXAA для сглаживания резких краев в отрисованном изображении. Этот метод будет интерполировать и (или) размывать пиксели со значительной разницей в яркости. Здесь нам это не требуется, поэтому отключим его.
using GLMakie
using GLMakie
data = [ifelse(x > 180, 0, x/100) * ifelse(y > 80, 0, y/50) for x in 1:200, y in 1:100]
scene = Scene(size = (3 * 200, 2 * 100))
image!(scene, -1..1, -1..1, data, colormap = :viridis, interpolate = false, fxaa = false)
scene
WGLMakie использует MSAA, который сэмплирует каждый пиксель на несколько субпикселей. При отображении с попиксельной точностью один и тот же цвет будет сэмплироваться несколько раз, в результате чего получится один и тот же конечный цвет. Поэтому в WGLMakie такой проблемы нет.
Построение «пиксель в пиксель» на рисунке
Использование LScene
При построении нескольких матриц с попиксельной точностью рекомендуется использовать Figure для компоновки макета. Можно продолжить поддерживать механику Scene, которую мы использовали выше, применив LScene. Здесь нужно задать width и height, а не size, чтобы указать, сколько места требуется LScene. Для подгонки рисунка под размер сцен хорошо подойдет функция resize_to_layout!().
using CairoMakie
using CairoMakie
# length(50:50) = 101
data = [x*y/1000 for x in -50:50, y in -50:50]
fig = Figure()
s1 = LScene(fig[1, 1], width = 101, height = 101, show_axis = false, scenekw = (camera = cam_relative!,))
image!(s1, 0..1, 0..1, data, colormap = :viridis, interpolate = false)
s2 = LScene(fig[1, 2], width = 101, height = 101, show_axis = false, scenekw = (camera = campixel!,))
heatmap!(s2, 0:101, 0:101, data)
resize_to_layout!(fig)
fig
Чтобы контролировать пробелы, создаваемые рисунком, можно настроить Figure(figure_padding = ...) для внешнего заполнения и rowgap!(fig, ...) и colgap!(fig.layout, ...) для внутренних промежутков.
Использование оси
using CairoMakie
using CairoMakie
# length(50:50) = 101
data = [x*y/1000 for x in -50:50, y in -50:50]
fig = Figure()
a1 = Axis(fig[1, 1], width = 101, height = 101)
image!(a1, data, colormap = :viridis, interpolate = false)
a2 = Axis(fig[1, 2], width = 101, height = 101)
heatmap!(a2, data)
resize_to_layout!(fig)
fig
Для image и heatmap ось будет выбирать пределы, плотно прилегающие к соответствующему графику. Поэтому не нужно обеспечивать соответствие значений x и y на графике размерам данных и оси. Однако их все же можно задать для тепловой карты (heatmap), чтобы деления не выравнивались по центру ячеек. Можно также отключить линии осей (leftspinevisible = false и т. д.), поскольку они перекрывают края изображения.