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

Отрисовка «пиксель в пиксель»

Страница в процессе перевода.

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

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
8ee7924

Полноэкранный график

Рассмотрим случай создания простого изображения на основе некоторых данных, без каких-либо обычных дополнительных элементов оси. Здесь не стоит работать с блоками Figure и Axis, так как они оба занимают пространство за счет заполнения и компоновки макета. Вместо них используем Scene напрямую. Пустая сцена определенного размера может быть создана следующим образом.

using CairoMakie
using CairoMakie

scene = Scene(size = (200, 100), camera = campixel!)
a8d9653

Мы явно задаем 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
c2383b1

Чтобы увеличить изображение, можно просто изменить размер сцены и пределы графика. С 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
5109aa2

Другим вариантом является изменение 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
90851c6

Аналогичным образом можно использовать 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
c6461ab

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
920f221

Чтобы контролировать пробелы, создаваемые рисунком, можно настроить Figure(figure_padding = ...) для внешнего заполнения и rowgap!(fig, ...) и colgap!(fig.layout, ...) для внутренних промежутков.

Использование оси

Если нужно построить график по оси (Axis), просто замените LScene в приведенном выше примере.

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
2441f8a

Для image и heatmap ось будет выбирать пределы, плотно прилегающие к соответствующему графику. Поэтому не нужно обеспечивать соответствие значений x и y на графике размерам данных и оси. Однако их все же можно задать для тепловой карты (heatmap), чтобы деления не выравнивались по центру ячеек. Можно также отключить линии осей (leftspinevisible = false и т. д.), поскольку они перекрывают края изображения.