from __future__ import division
import numpy as np
from IPython.display import display
from pythreejs import *
from colour import RGB_to_XYZ
from colour.constants import DEFAULT_FLOAT_DTYPE
from colour.models import (
COLOURSPACE_MODELS_LABELS, XYZ_to_colourspace_model, XYZ_to_RGB)
from colour.notation import RGB_to_HEX
from colour.plotting import (COLOUR_STYLE_CONSTANTS, filter_cmfs,
filter_RGB_colourspaces)
from colour.plotting.volume import common_colourspace_model_axis_reorder
from colour.utilities import first_item, normalise_maximum
def create_plane(width=1,
height=1,
width_segments=1,
height_segments=1,
direction='+z'):
""" Generate vertices & indices for a filled and outlined plane.
Parameters
----------
width : float
Plane width.
height : float
Plane height.
width_segments : int
Plane segments count along the width.
height_segments : float
Plane segments count along the height.
direction: unicode
``{'-x', '+x', '-y', '+y', '-z', '+z'}``
Direction the plane will be facing.
Returns
-------
vertices : array
Array of vertices suitable for use as a VertexBuffer.
faces : array
Indices to use to produce a filled plane.
outline : array
Indices to use to produce an outline of the plane.
References
----------
.. [1] Cabello, R. (n.d.). PlaneBufferGeometry.js. Retrieved May 12, 2015,
from http://git.io/vU1Fh
"""
x_grid = width_segments
y_grid = height_segments
x_grid1 = x_grid + 1
y_grid1 = y_grid + 1
# Positions, normals and texcoords.
positions = np.zeros(x_grid1 * y_grid1 * 3)
normals = np.zeros(x_grid1 * y_grid1 * 3)
texcoords = np.zeros(x_grid1 * y_grid1 * 2)
y = np.arange(y_grid1) * height / y_grid - height / 2
x = np.arange(x_grid1) * width / x_grid - width / 2
positions[::3] = np.tile(x, y_grid1)
positions[1::3] = -np.repeat(y, x_grid1)
normals[2::3] = 1
texcoords[::2] = np.tile(np.arange(x_grid1) / x_grid, y_grid1)
texcoords[1::2] = np.repeat(1 - np.arange(y_grid1) / y_grid, x_grid1)
# Faces and outline.
faces, outline = [], []
for i_y in range(y_grid):
for i_x in range(x_grid):
a = i_x + x_grid1 * i_y
b = i_x + x_grid1 * (i_y + 1)
c = (i_x + 1) + x_grid1 * (i_y + 1)
d = (i_x + 1) + x_grid1 * i_y
faces.extend(((a, b, d), (b, c, d)))
outline.extend(((a, b), (b, c), (c, d), (d, a)))
positions = np.reshape(positions, (-1, 3))
texcoords = np.reshape(texcoords, (-1, 2))
normals = np.reshape(normals, (-1, 3))
faces = np.reshape(faces, (-1, 3)).astype(np.uint32)
outline = np.reshape(outline, (-1, 2)).astype(np.uint32)
direction = direction.lower()
if direction in ('-x', '+x'):
shift, neutral_axis = 1, 0
elif direction in ('-y', '+y'):
shift, neutral_axis = -1, 1
elif direction in ('-z', '+z'):
shift, neutral_axis = 0, 2
sign = -1 if '-' in direction else 1
positions = np.roll(positions, shift, -1)
normals = np.roll(normals, shift, -1) * sign
colors = np.ravel(positions)
colors = np.hstack((np.reshape(
np.interp(colors, (np.min(colors), np.max(colors)), (0, 1)),
positions.shape), np.ones((positions.shape[0], 1))))
colors[..., neutral_axis] = 0
vertices = np.zeros(positions.shape[0],
[('position', np.float32, 3),
('texcoord', np.float32, 2),
('normal', np.float32, 3), ('colour', np.float32, 4)])
vertices['position'] = positions
vertices['texcoord'] = texcoords
vertices['normal'] = normals
vertices['colour'] = colors
return vertices, faces, outline
def create_box(width=1,
height=1,
depth=1,
width_segments=1,
height_segments=1,
depth_segments=1,
planes=None):
""" Generate vertices & indices for a filled and outlined box.
Parameters
----------
width : float
Box width.
height : float
Box height.
depth : float
Box depth.
width_segments : int
Box segments count along the width.
height_segments : float
Box segments count along the height.
depth_segments : float
Box segments count along the depth.
planes: array_like
Any combination of ``{'-x', '+x', '-y', '+y', '-z', '+z'}``
Included planes in the box construction.
Returns
-------
vertices : array
Array of vertices suitable for use as a VertexBuffer.
faces : array
Indices to use to produce a filled box.
outline : array
Indices to use to produce an outline of the box.
"""
planes = (('+x', '-x', '+y', '-y', '+z', '-z')
if planes is None else [d.lower() for d in planes])
w_s, h_s, d_s = width_segments, height_segments, depth_segments
planes_m = []
if '-z' in planes:
planes_m.append(list(create_plane(width, depth, w_s, d_s, '-z')))
planes_m[-1][0]['position'][..., 2] -= height / 2
planes_m[-1][1] = np.fliplr(planes_m[-1][1])
if '+z' in planes:
planes_m.append(list(create_plane(width, depth, w_s, d_s, '+z')))
planes_m[-1][0]['position'][..., 2] += height / 2
if '-y' in planes:
planes_m.append(list(create_plane(height, width, h_s, w_s, '-y')))
planes_m[-1][0]['position'][..., 1] -= depth / 2
planes_m[-1][1] = np.fliplr(planes_m[-1][1])
if '+y' in planes:
planes_m.append(list(create_plane(height, width, h_s, w_s, '+y')))
planes_m[-1][0]['position'][..., 1] += depth / 2
if '-x' in planes:
planes_m.append(list(create_plane(depth, height, d_s, h_s, '-x')))
planes_m[-1][0]['position'][..., 0] -= width / 2
planes_m[-1][1] = np.fliplr(planes_m[-1][1])
if '+x' in planes:
planes_m.append(list(create_plane(depth, height, d_s, h_s, '+x')))
planes_m[-1][0]['position'][..., 0] += width / 2
positions = np.zeros((0, 3), dtype=np.float32)
texcoords = np.zeros((0, 2), dtype=np.float32)
normals = np.zeros((0, 3), dtype=np.float32)
faces = np.zeros((0, 3), dtype=np.uint32)
outline = np.zeros((0, 2), dtype=np.uint32)
offset = 0
for vertices_p, faces_p, outline_p in planes_m:
positions = np.vstack((positions, vertices_p['position']))
texcoords = np.vstack((texcoords, vertices_p['texcoord']))
normals = np.vstack((normals, vertices_p['normal']))
faces = np.vstack((faces, faces_p + offset))
outline = np.vstack((outline, outline_p + offset))
offset += vertices_p['position'].shape[0]
vertices = np.zeros(positions.shape[0],
[('position', np.float32, 3),
('texcoord', np.float32, 2),
('normal', np.float32, 3), ('colour', np.float32, 4)])
colors = np.ravel(positions)
colors = np.hstack((np.reshape(
np.interp(colors, (np.min(colors), np.max(colors)), (0, 1)),
positions.shape), np.ones((positions.shape[0], 1))))
vertices['position'] = positions
vertices['texcoord'] = texcoords
vertices['normal'] = normals
vertices['colour'] = colors
return vertices, faces, outline
def rotate_geometry(geometry):
geometry.rotateX(np.radians(-90))
geometry.rotateZ(np.radians(-90))
return geometry
def RGB_colourspace_volume_visual(colourspace='ITU-R BT.709',
reference_colourspace='CIE xyY',
segments=16,
uniform_colour=None,
uniform_opacity=0.75,
wireframe=True,
wireframe_colour=None,
wireframe_opacity=1.0):
colourspace = first_item(filter_RGB_colourspaces(colourspace))
cube = create_box(
width_segments=segments,
height_segments=segments,
depth_segments=segments)
vertices = cube[0]['position'] + 0.5
faces = cube[1].tolist()
colours = (
RGB_to_HEX(cube[0]['colour']).tolist()
if uniform_colour is None else
uniform_colour)
faces = [face + [None, [colours[i] for i in face]] for face in faces]
XYZ = RGB_to_XYZ(vertices, colourspace.whitepoint, colourspace.whitepoint,
colourspace.RGB_to_XYZ_matrix)
vertices = common_colourspace_model_axis_reorder(
XYZ_to_colourspace_model(XYZ, colourspace.whitepoint,
reference_colourspace), reference_colourspace)
vertices[np.isnan(vertices)] = 0
outline = vertices[cube[2]].reshape(-1, 3).tolist()
vertices = vertices.tolist()
geometry = Geometry(vertices=vertices, faces=faces, colors=colours)
geometry.exec_three_obj_method('computeFaceNormals')
mesh = rotate_geometry(Mesh(
geometry=geometry,
material=MeshBasicMaterial(vertexColors='VertexColors',
transparent=uniform_opacity != 1.0,
opacity=uniform_opacity),
))
if wireframe:
colours = (
RGB_to_HEX(cube[0]['colour'][cube[2]]).reshape(-1).tolist()
if wireframe_colour is None else
wireframe_colour)
geometry = Geometry(vertices=outline, colors=colours)
wireframe = rotate_geometry(LineSegments(
geometry=geometry,
material=LineBasicMaterial(
vertexColors='VertexColors',
transparent=uniform_opacity != 1.0,
opacity=wireframe_opacity
),
))
return [mesh, wireframe]
return [mesh]
def spectral_locus_visual(reference_colourspace='CIE xyY',
cmfs='CIE 1931 2 Degree Standard Observer',
width=10.0,
uniform_colour=None,
uniform_opacity=1.0):
"""
Returns a :class:`vispy.scene.visuals.Line` class instance representing
the spectral locus.
Parameters
----------
reference_colourspace : unicode, optional
**{'CIE XYZ', 'CIE xyY', 'CIE Lab', 'CIE Luv', 'CIE UCS', 'CIE UVW',
'IPT', 'Hunter Lab', 'Hunter Rdab'}**,
Reference colourspace to use for colour conversions / transformations.
cmfs : unicode, optional
Standard observer colour matching functions used to draw the spectral
locus.
width : numeric, optional
Line width.
uniform_colour : array_like, optional
Uniform symbol colour.
uniform_opacity : numeric, optional
Uniform symbol opacity.
Returns
-------
Line
Spectral locus visual.
"""
cmfs = first_item(filter_cmfs(cmfs))
XYZ = cmfs.values
XYZ = np.vstack((XYZ, XYZ[0, ...]))
colourspace = COLOUR_STYLE_CONSTANTS.colour.colourspace
illuminant = colourspace.whitepoint
points = common_colourspace_model_axis_reorder(
XYZ_to_colourspace_model(XYZ, illuminant, reference_colourspace),
reference_colourspace)
points[np.isnan(points)] = 0
points = points.tolist()
if uniform_colour is None:
RGB = RGB_to_HEX(
normalise_maximum(
XYZ_to_RGB(XYZ, colourspace.whitepoint, colourspace.whitepoint,
colourspace.XYZ_to_RGB_matrix),
axis=-1)).tolist()
else:
RGB = uniform_colour
geometry = Geometry(vertices=points, colors=RGB)
lines = rotate_geometry(Line(
geometry=geometry,
material=LineBasicMaterial(
linewidth=width, vertexColors='VertexColors',
transparent=uniform_opacity != 1.0, opacity=uniform_opacity),
type='LinePieces')
)
return [lines]
def reference_colourspace_axes_visual(reference_colourspace='CIE xyY'):
axes_visual = AxesHelper()
children = [axes_visual]
# for label in COLOURSPACE_MODELS_LABELS[reference_colourspace]:
# children.append(TextGeometry(label))
return children
def renderer(width=960, height=540):
fog = FogExp2(COLOUR_STYLE_CONSTANTS.colour.dark, 0.05)
grid_visual = GridHelper(2, 20, COLOUR_STYLE_CONSTANTS.colour.darkest,
COLOUR_STYLE_CONSTANTS.colour.darker)
camera = PerspectiveCamera(
position=[-3, 3, 3], fov=20, aspect=width / height)
controls = OrbitControls(controlling=camera, target=(1 / 3, 0.5, 1 / 3))
children = [grid_visual]
children += reference_colourspace_axes_visual()
children += RGB_colourspace_volume_visual()
children += spectral_locus_visual()
children += [camera]
scene = Scene(
children=children, fog=fog, background=COLOUR_STYLE_CONSTANTS.colour.dark)
return Renderer(
camera=camera,
scene=scene,
controls=[controls],
width=width,
height=height,
antialias=True)
display(renderer())