3D-диаграмма рассеяния с использованием собственного изображения

Я пытаюсь использовать ggplot и ggimage для создания трехмерной диаграммы рассеяния с пользовательским изображением. Он отлично работает в 2D:

library(ggplot2)
library(ggimage)
library(rsvg)

set.seed(2017-02-21)
d <- data.frame(x = rnorm(10), y = rnorm(10), z=1:10,
  image = 'https://image.flaticon.com/icons/svg/31/31082.svg'
)

ggplot(d, aes(x, y)) + 
  geom_image(aes(image=image, color=z)) +
  scale_color_gradient(low='burlywood1', high='burlywood4')

enter image description here

Я попробовал два способа создания 3D-диаграммы:

  1. plotly - в настоящее время это не работает с geom_image, хотя оно ставится в очередь как будущий запрос.

  2. gg3D - это пакет R, но я не могу заставить его хорошо играть с пользовательскими изображениями. Вот как заканчивается объединение этих библиотек:

library(ggplot2)
library(ggimage)
library(gg3D)

ggplot(d, aes(x=x, y=y, z=z, color=z)) +
  axes_3D() +
  geom_image(aes(image=image, color=z)) +
  scale_color_gradient(low='burlywood1', high='burlywood4')

enter image description here

Любая помощь будет оценена. Я был бы в порядке с библиотекой Python, Javascript и т.д., Если существует решение там.

Ответ 1

Вот хакерское решение, которое преобразует изображение в информационный фрейм, где каждый пиксель становится вокселем (?), Который мы отправляем в графически. Это в основном работает, но нужно еще немного поработать, чтобы:

1) настроить изображение больше (с шагом эрозии?), Чтобы исключить больше пикселей с низким альфа-каналом

2) использовать запрошенную цветовую гамму на графике

Шаг 1: импортировать изображение и изменять его размер, а также отфильтровывать прозрачные или частично прозрачные пиксели

library(tidyverse)
library(magick)
sprite_frame <- image_read("coffee-bean-for-a-coffee-break.png") %>% 
  magick::image_resize("20x20") %>% 
  image_raster(tidy = T) %>%
  mutate(alpha = str_sub(col, start = 7) %>% strtoi(base = 16)) %>%
  filter(col != "transparent", 
     alpha > 240)

Вот как это выглядит:

ggplot(sprite_frame, aes(x,y, fill = col)) + 
  geom_raster() + 
  guides(fill = F) +
  scale_fill_identity()

enter image description here

Шаг 2: приведите эти пиксели в качестве вокселей

pixels_per_image <- nrow(sprite_frame)
scale <- 1/40  # How big should a pixel be in coordinate space?

set.seed(2017-02-21)
d <- data.frame(x = rnorm(10), y = rnorm(10), z=1:10)
d2 <- d %>%
  mutate(copies = pixels_per_image) %>%
  uncount(copies) %>%
  mutate(x_sprite = sprite_frame$x*scale + x,
         y_sprite = sprite_frame$y*scale + y,
         col = rep(sprite_frame$col, nrow(d)))

Мы можем построить это в 2d пространстве с помощью ggplot:

ggplot(d2, aes(x_sprite, y_sprite, z = z, alpha = col, fill = z)) + 
  geom_tile(width = scale, height = scale) + 
  guides(alpha = F) +
  scale_fill_gradient(low='burlywood1', high='burlywood4')

enter image description here

Или привнести это в заговор. Обратите внимание, что объемные трехмерные рассеиватели в настоящее время не поддерживают переменную непрозрачность, поэтому изображение в настоящее время отображается в виде сплошного овала, пока вы не приблизитесь к одному спрайту.

library(plotly)
plot_ly(d2, x = ~x_sprite, y = ~y_sprite, z = ~z, 
    size = scale, color = ~z, colors = c("#FFD39B", "#8B7355")) %>%
    add_markers()

enter image description here


Редактировать: попытка плотно подходить к mesh3d

Похоже, что другой подход заключается в преобразовании глифа SVG в координаты для поверхности mesh3d в виде графика.

Моя первая попытка сделать это была непрактичной:

  1. Загрузите SVG в Inkscape и используйте опцию "Свести Безье", чтобы приблизить форму без кривых Безье.
  2. Экспортируйте SVG и скрестите пальцы, чтобы файл имел необработанные координаты. Я новичок в SVG, и похоже, что результат часто может быть смесью абсолютных и относительных точек. Сложнее в этом случае, так как глиф имеет две несвязанные секции.
  3. Переформатируйте координаты как фрейм данных для построения графика с помощью ggplot2 или plotly.

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

library(dplyr)
half_bean <- read.table(
  header = T,
  stringsAsFactors = F,
  text = "x y
  153.714 159.412 
  95.490016 186.286 
  54.982625 216.85 
  28.976672 247.7425 
  14.257 275.602 
  0.49742188 229.14067 
  5.610375 175.89737 
  28.738141 120.85839 
  69.023 69.01 
  128.24827 24.564609 
  190.72412 2.382875 
  249.14492 3.7247031 
  274.55165 13.610674 
  296.205 29.85 
  296.4 30.064 
  283.67119 58.138937 
  258.36 93.03325 
  216.39731 128.77994 
  153.714 159.412"
) %>%
  mutate(z = 0)

other_half <- half_bean %>%
  mutate(x = 330 - x,
         y = 330 - y,
         z = z)

ggplot() + coord_equal() +
  geom_path(data = half_bean, aes(x,y)) +
  geom_path(data = other_half, aes(x,y))

enter image description here

Но хотя в ggplot это выглядит нормально, у меня возникают проблемы с правильным отображением вогнутых частей:

library(plotly)
plot_ly(type = 'mesh3d',
        split = c(rep(1, 19), rep(2, 19)),
             x = c(half_bean$x, other_half$x),
             y = c(half_bean$y, other_half$y),
             z = c(half_bean$z, other_half$z)
)

enter image description here

Ответ 2

Это очень грубый ответ, и он не полностью решает вашу проблему, но я считаю, что это хорошее начало, и кто-то другой может решить эту проблему и найти хорошее решение.

Есть способ разместить изображение как маркер custmo в python. Начиная с этого УДИВИТЕЛЬНОГО ответа и немного повозившись с коробкой.
Однако проблема с этим решением состоит в том, что ваше изображение не векторизовано (и слишком велико, чтобы использоваться в качестве маркера).
Кроме того, я не тестировал способ раскрасить его в соответствии с картой цветов, поскольку в действительности он не отображается как вывод:/.

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

from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d import proj3d
import matplotlib.pyplot as plt
from matplotlib import offsetbox
import numpy as np

Обратите внимание, что здесь я скачал изображение и импортирую его из локального файла.

import matplotlib.image as mpimg
#
img=mpimg.imread('coffeebean.png')
imgplot = plt.imshow(img)

coffeebeanoriginal

from PIL import Image
from resizeimage import resizeimage
with open('coffeebean.png', 'r+b') as f:
    with Image.open(f) as image:
        cover = resizeimage.resize_width(image, 20,validate=True)
        cover.save('resizedbean.jpeg', image.format)

img=mpimg.imread('resizedbean.jpeg')
imgplot = plt.imshow(img)

Изменение размера на самом деле не работает (или, по крайней мере, я не мог найти способ заставить его работать). resizedbean

xs = [1,1.5,2,2]
ys = [1,2,3,1]
zs = [0,1,2,0]
#c = #I guess copper would be a good colormap here


fig = plt.figure()
ax = fig.add_subplot(111, projection=Axes3D.name)

ax.scatter(xs, ys, zs, marker="None")

# Create a dummy axes to place annotations to
ax2 = fig.add_subplot(111,frame_on=False) 
ax2.axis("off")
ax2.axis([0,1,0,1])

class ImageAnnotations3D():
    def __init__(self, xyz, imgs, ax3d,ax2d):
        self.xyz = xyz
        self.imgs = imgs
        self.ax3d = ax3d
        self.ax2d = ax2d
        self.annot = []
        for s,im in zip(self.xyz, self.imgs):
            x,y = self.proj(s)
            self.annot.append(self.image(im,[x,y]))
        self.lim = self.ax3d.get_w_lims()
        self.rot = self.ax3d.get_proj()
        self.cid = self.ax3d.figure.canvas.mpl_connect("draw_event",self.update)

        self.funcmap = {"button_press_event" : self.ax3d._button_press,
                        "motion_notify_event" : self.ax3d._on_move,
                        "button_release_event" : self.ax3d._button_release}

        self.cfs = [self.ax3d.figure.canvas.mpl_connect(kind, self.cb) \
                        for kind in self.funcmap.keys()]

    def cb(self, event):
        event.inaxes = self.ax3d
        self.funcmap[event.name](event)

    def proj(self, X):
        """ From a 3D point in axes ax1, 
            calculate position in 2D in ax2 """
        x,y,z = X
        x2, y2, _ = proj3d.proj_transform(x,y,z, self.ax3d.get_proj())
        tr = self.ax3d.transData.transform((x2, y2))
        return self.ax2d.transData.inverted().transform(tr)

    def image(self,arr,xy):
        """ Place an image (arr) as annotation at position xy """
        im = offsetbox.OffsetImage(arr, zoom=2)
        im.image.axes = ax
        ab = offsetbox.AnnotationBbox(im, xy, xybox=(0., 0.),
                            xycoords='data', boxcoords="offset points",
                            pad=0.0)
        self.ax2d.add_artist(ab)
        return ab

    def update(self,event):
        if np.any(self.ax3d.get_w_lims() != self.lim) or \
                        np.any(self.ax3d.get_proj() != self.rot):
            self.lim = self.ax3d.get_w_lims()
            self.rot = self.ax3d.get_proj()
            for s,ab in zip(self.xyz, self.annot):
                ab.xy = self.proj(s)



ia = ImageAnnotations3D(np.c_[xs,ys,zs],img,ax, ax2 )

ax.set_xlabel('X Label')
ax.set_ylabel('Y Label')
ax.set_zlabel('Z Label')
plt.show()

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

broken_output

Дополнительная информация:
cv2 изменить размер с помощью cv2 (каждый метод интерполяции), не помогло.
Не могу попробовать skimage с текущей рабочей станцией.

Вы можете попробовать следующее и посмотреть, что получится.

from skimage.transform import resize
res = resize(img, (20, 20), anti_aliasing=True)

imgplot = plt.imshow(res)