使用自定义图像的3D散点图

时间:2019-03-22 22:10:40

标签: javascript python r ggplot2 data-visualization

我正在尝试使用ggplotggimage创建具有自定义图像的3D散点图。在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已作为将来的请求排队,但它目前不适用于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等。

2 个答案:

答案 0 :(得分:8)

这是一个骇人听闻的解决方案,可将图像转换为数据帧,其中每个像素都变成我们发送到图块中的体素(?)。基本上可以,但是还需要做更多的工作:

1)进一步调整图像(使用腐蚀步骤?)以排除更多的低alpha像素

2)在绘图中使用请求的颜色范围

第1步:导入图像并调整大小,然后滤除透明或部分透明的像素

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/toolbar_show_addresses_simple"
  style="@style/toolbar_dark"
  package="com.example.roboapp">

这是下面的样子:

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)

enter image description here

第2步:将这些像素作为体素输入

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

我们可以使用ggplot在2d空间中绘制该图:

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)))

enter image description here

或者将其密谋化。请注意,以图形方式显示的3d散点目前不支持可变的不透明度,因此,在您紧密缩放到一个精灵之前,图片当前会显示为实心椭圆形。

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


编辑:尝试使用图网格3d方法

似乎另一种方法是将SVG字形转换为可绘制的mesh3d曲面的坐标。

我最初的尝试是不切实际的手动操作:

  1. 在Inkscape中加载SVG,并使用“展平贝塞尔曲线”选项近似无贝塞尔曲线的形状。
  2. 导出SVG并用交叉手指指出该文件具有原始坐标。我是SVG的新手,看来输出通常可以是绝对点和相对点的混合。由于该字形具有两个断开的部分,因此在这种情况下会更加复杂。
  3. 将坐标重新格式化为数据框,以便使用ggplot2进行绘图或进行绘图。

例如,以下坐标表示一个bean的一半,我们可以对其进行转换以得到另一半:

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

但是,虽然在ggplot中看起来不错,但我无法使凹部正确显示在凹部中:

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

答案 1 :(得分:3)

这是一个非常粗糙的答案,不能完全解决您的问题,但是我相信这是一个好的开始,其他人可能会对此有所了解并找到一个好的解决方案。

有一种方法可以将图像作为custmo标记放置在python中。从this AMAZING answer开始,然后在框上稍作改动。
但是,此解决方案的问题在于您的图像未矢量化(太大而无法用作标记)。
此外,我没有测试一种根据颜色图为它着色的方法,因为它并未真正显示为输出:/。

这里的基本思想是在创建绘图后,用自定义图像 替换标记。为了将它们正确放置在图中,我们根据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(每种插值方法)调整大小,但没有帮助。
无法在当前工作站上尝试skimage

您可以尝试以下方法,然后看看有什么结果。

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

imgplot = plt.imshow(res)