Skip to content

Instantly share code, notes, and snippets.

@rougier
Last active August 27, 2023 08:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rougier/9d5655e4d435d6c0e3ec6372ffcd81f8 to your computer and use it in GitHub Desktop.
Save rougier/9d5655e4d435d6c0e3ec6372ffcd81f8 to your computer and use it in GitHub Desktop.
Matplotlib 3D imshow
# Copyright 2023 Nicolas P. Rougier - BSD 2 Clauses licence
# This is an example of 3D projected images with matplotlib using imshow
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
from matplotlib.path import Path
from mpl_toolkits.mplot3d import proj3d
from matplotlib.patches import PathPatch
import matplotlib.transforms as mtransforms
def warp(T1, T2):
"""
Return an affine transform that warp triangle T1 into triangle T2.
Raises
------
`LinAlgError` if T1 or T2 are degenerated triangles
"""
T1 = np.c_[np.array(T1), np.ones(3)]
T2 = np.c_[np.array(T2), np.ones(3)]
M = np.linalg.inv(T1) @ T2
return mtransforms.Affine2D(M.T)
def textured_triangle(ax, T, UV, texture, interpolation="none", image=None):
"""
Draw a textured triangle T using UV coordinates and given texture.
Parameters
----------
T : (3,2) np.ndarray
Positions of the triangle vertices
UV : (3,2) np.ndarray
UV coordinates of the triangle vertices
texture:
Image to use for texture
image: AxesImage
Image if the triangle has been already drawn on axis
"""
w,h = texture.shape[:2]
Z = UV*(w,h)
xmin, xmax = int(np.floor(Z[:,0].min())), int(np.ceil(Z[:,0].max()))
ymin, ymax = int(np.floor(Z[:,1].min())), int(np.ceil(Z[:,1].max()))
texture = (texture[ymin:ymax, xmin:xmax,:]).astype(np.uint8)
extent = xmin/w, xmax/w, ymin/h, ymax/h
transform = warp (UV,T) + ax.transData
path = Path([UV[0], UV[1], UV[2], UV[0]], closed=True)
if image is not None:
image.set_transform(transform)
image.set_extent(extent)
image.set_clip_path((path,transform))
else:
image = ax.imshow(texture, interpolation=interpolation, origin='lower',
extent=extent, transform=transform, clip_path=(path,transform))
return image
with cbook.get_sample_data('grace_hopper.jpg') as image_file:
texture = plt.imread(image_file)
fig = plt.figure(figsize=(8,8))
ax = fig.add_axes([0,0,1,1], projection="3d")
triangles = np.zeros((6,3), dtype = [("xyz", float, 3),
("uv", float, 2)])
# XY plane
triangles[0] = [ ((0,0,0), (0,1)),
((1,0,0), (1,1)),
((1,1,0), (1,0)) ]
triangles[1] = [ ((0,0,0), (0,1)),
((1,1,0), (1,0)),
((0,1,0), (0,0)) ]
# XZ plane
triangles[2] = [ ((0,1,0), (0,1)),
((1,1,0), (1,1)),
((1,1,1), (1,0)) ]
triangles[3] = [ ((0,1,0), (0,1)),
((1,1,1), (1,0)),
((0,1,1), (0,0)) ]
# YZ plane
triangles[4] = [ ((0,0,0), (0,1)),
((0,1,0), (1,1)),
((0,1,1), (1,0)) ]
triangles[5] = [ ((0,0,0), (0,1)),
((0,1,1), (1,0)),
((0,0,1), (0,0)) ]
images = [None]*len(triangles)
# For some unknown reason, we need to have a first useless imshow
ax.imshow([[0]], extent=[-1.00,-1.001,-1.00,-1.001])
# Set out axis limit (in relation with our triangle coordinates)
ax.set_xlim(0,1), ax.set_ylim(0,1), ax.set_zlim(0,1)
def update(event=None):
"""
Update all triangles
"""
for i, (triangle, image) in enumerate(zip(triangles, images)):
uv = triangle["uv"]
xy = []
for xyz in triangle["xyz"]:
x,y,z = np.array(proj3d.proj_transform(*xyz, ax.get_proj()))
xy.append((x,y))
xy = np.array(xy)
images[i] = textured_triangle(ax, xy, uv, texture, interpolation="none", image=images[i])
def redraw(event=None):
update()
fig.canvas.draw()
update()
fig.canvas.mpl_connect('draw_event', update)
fig.canvas.mpl_connect('button_release_event', redraw)
# plt.savefig("imshow-3d.png")
plt.show()
@rougier
Copy link
Author

rougier commented Aug 27, 2023

imshow-3d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment