TLDR: Here is the final result!
EDIT: I ended up doing this process for all the planets. I also added rings to saturn (same exact concept as the planets, just with a surface of revolution rather than a sphere). Kept earth for scale!
The implementation:
For a project I’m working on, I needed a textured globe. This is something I’ve done a million times in MATLAB but I’m trying to move away from MATLAB. I was discouraged initially to see forum posts like this stating things like “Unlike matlab you cannot map a multicolor (rgb) image”. This chart studio post showed how to map 2d black and white (or one hue) images onto surfaces, and reading through it gave me an idea.
I wrote a quick script which maps an RGB image into a grayscale image, but does so such that each range of colors only occupies a specific range of intensities. For my earth example, I mapped anything blue (ocean-like) to <0.1, mapping everything green to 0.1-0.5, anything tan to 0.5-0.9, and then anything white to >0.9. That image looked like this:
Obviously, that looks a bit wonky! But then I just wrote a quick colormap like this:
colorscale =[[0.0, 'rgb(30, 59, 117)'],
[0.1, 'rgb(46, 68, 21)'],
[0.2, 'rgb(74, 96, 28)'],
[0.3, 'rgb(115,141,90)'],
[0.4, 'rgb(122, 126, 75)'],
[0.6, 'rgb(122, 126, 75)'],
[0.7, 'rgb(141,115,96)'],
[0.8, 'rgb(223, 197, 170)'],
[0.9, 'rgb(237,214,183)'],
[1.0, 'rgb(255, 255, 255)']]
And it worked perfectly! As for mapping it onto a sphere, I used this rather simple implementation:
import plotly.graph_objects as go
import numpy as np
from PIL import Image
def sphere(size, texture):
N_lat = int(texture.shape[0])
N_lon = int(texture.shape[1])
theta = np.linspace(0,2*np.pi,N_lat)
phi = np.linspace(0,np.pi,N_lon)
# Set up coordinates for points on the sphere
x0 = size * np.outer(np.cos(theta),np.sin(phi))
y0 = size * np.outer(np.sin(theta),np.sin(phi))
z0 = size * np.outer(np.ones(N_lat),np.cos(phi))
# Set up trace
return x0,y0,z0
texture = np.asarray(Image.open('earth.jpg'.format(planet_name))).T
x,y,z = sphere(radius,texture)
surf = go.Surface(x=x, y=y, z=z,
surfacecolor=texture,
colorscale=colorscale)
layout = go.Layout(scene=dict(aspectratio=dict(x=1, y=1, z=1)))
fig = go.Figure(data=[surf], layout=layout)
fig.show()
Anyways, I figured I’d share as I’ve seen other people interested in texturing shapes in plotly and I thought this was kind of a neat trick for achieving that. Obviously you lose some color depth, but for my purposes (I needed 3d shape models of all the planets), this worked great!
Edit 2:
In general, this would work pretty poorly. Since 8-bit color images (256 values per rgb channel) can represent 16,777,216 colors, yet our greyscale image can only represent 256 unique intensity values. So we’ll need to do some kind of subsampling of the full colorspace to get the mapping to work.
I believe however, this can be acceptable with some tricks. The earth globe above for example, while not perfect, is certainly a lot better than you’d get by naively subsampling 16.7M colors down to 256 values. And thats due to the fact that we didn’t subsample the color space evenly, because we didn’t need to. The blues of the ocean didn’t need much space to store, and so we could map all of the “blues” into small portion of our intensity map. The same goes for high value colors (near white), as we could condense those all down. And then by identifying two hues that represented the rest of the image (hues of red and green), we could store those hues in much more detail. Allowing us to recover the full image much more accurately.
I believe this could be automated for any image. By looking at the statistics of how various hues appear in the image, we could probably automate a way of identifying the which colors can be compressed down, and which can’t be. Then subsample and map them accordingly. Doing it this way though, would require that such an automation tool also construct an accompanying colormap (inorder to “unpack” to intensity texture back into rgb). That shouldn’t be too difficult, but adds an additional level of complexity.
I might give a shot at somepoint in the next few weeks of trying my hand at that with some textures. For my project, I ended up hand tuning the colormaps and constructed intensity map textures, since I only needed to do it for each planet and it didn’t take too long (especially since most planets are a single hue to begin with!). But I think this might be a cool thing to look into more in the future!