Convert RGB array to HSL - python

A disclaimer first, I'm not very skilled in Python, you guys have my admiration.
My problem:
I need to generate 10k+ images from templates (128px by 128px) with various hues and luminances.
I load the images and turn them into arrays
image = Image.open(dir + "/" + file).convert('RGBA')
arr=np.array(np.asarray(image).astype('float'))
From what I can understand, handling numpy arrays in this fashion is much faster than looping over every pixels and using colorsys.
Now, I've stumbled upon a couple functions to convert rgb to hsv.
This helped me generate my images with different hues, but I also need to play with the brightness so that some can be black, and others white.
def rgb_to_hsv(rgb):
# Translated from source of colorsys.rgb_to_hsv
hsv=np.empty_like(rgb)
hsv[...,3:]=rgb[...,3:]
r,g,b=rgb[...,0],rgb[...,1],rgb[...,2]
maxc = np.max(rgb[...,:2],axis=-1)
minc = np.min(rgb[...,:2],axis=-1)
hsv[...,2] = maxc
hsv[...,1] = (maxc-minc) / maxc
rc = (maxc-r) / (maxc-minc)
gc = (maxc-g) / (maxc-minc)
bc = (maxc-b) / (maxc-minc)
hsv[...,0] = np.select([r==maxc,g==maxc],[bc-gc,2.0+rc-bc],default=4.0+gc-rc)
hsv[...,0] = (hsv[...,0]/6.0) % 1.0
idx=(minc == maxc)
hsv[...,0][idx]=0.0
hsv[...,1][idx]=0.0
return hsv
def hsv_to_rgb(hsv):
# Translated from source of colorsys.hsv_to_rgb
rgb=np.empty_like(hsv)
rgb[...,3:]=hsv[...,3:]
h,s,v=hsv[...,0],hsv[...,1],hsv[...,2]
i = (h*6.0).astype('uint8')
f = (h*6.0) - i
p = v*(1.0 - s)
q = v*(1.0 - s*f)
t = v*(1.0 - s*(1.0-f))
i = i%6
conditions=[s==0.0,i==1,i==2,i==3,i==4,i==5]
rgb[...,0]=np.select(conditions,[v,q,p,p,t,v],default=v)
rgb[...,1]=np.select(conditions,[v,v,v,q,p,p],default=t)
rgb[...,2]=np.select(conditions,[v,p,t,v,v,q],default=p)
return rgb
How easy is it to modify these functions to convert to and from HSL?
Any trick to convert HSV to HSL?
Any info you can give me is greatly appreciated, thanks!

Yes, numpy, namely the vectorised code, can speed-up color conversions.
The more, for massive production of 10k+ bitmaps, you may want to re-use a ready made professional conversion, or sub-class it, if it is not exactly matching your preferred Luminance model.
a Computer Vision library OpenCV, currently available for python as a cv2 module, can take care of the colorsystem conversion without any additional coding just with:
a ready-made conversion one-liner
out = cv2.cvtColor( anInputFRAME, cv2.COLOR_YUV2BGR ) # a bitmap conversion
A list of some color-systems available in cv2 ( you may notice RGB to be referred to as BRG due to OpenCV convention of a different ordering of an image's Blue-Red-Green color-planes ),
( symmetry applies COLOR_YCR_CB2BGR <-|-> COLOR_BGR2YCR_CB not all pairs shown )
>>> import cv2
>>> for key in dir( cv2 ): # show all ready conversions
... if key[:7] == 'COLOR_Y':
... print key
COLOR_YCR_CB2BGR
COLOR_YCR_CB2RGB
COLOR_YUV2BGR
COLOR_YUV2BGRA_I420
COLOR_YUV2BGRA_IYUV
COLOR_YUV2BGRA_NV12
COLOR_YUV2BGRA_NV21
COLOR_YUV2BGRA_UYNV
COLOR_YUV2BGRA_UYVY
COLOR_YUV2BGRA_Y422
COLOR_YUV2BGRA_YUNV
COLOR_YUV2BGRA_YUY2
COLOR_YUV2BGRA_YUYV
COLOR_YUV2BGRA_YV12
COLOR_YUV2BGRA_YVYU
COLOR_YUV2BGR_I420
COLOR_YUV2BGR_IYUV
COLOR_YUV2BGR_NV12
COLOR_YUV2BGR_NV21
COLOR_YUV2BGR_UYNV
COLOR_YUV2BGR_UYVY
COLOR_YUV2BGR_Y422
COLOR_YUV2BGR_YUNV
COLOR_YUV2BGR_YUY2
COLOR_YUV2BGR_YUYV
COLOR_YUV2BGR_YV12
COLOR_YUV2BGR_YVYU
COLOR_YUV2GRAY_420
COLOR_YUV2GRAY_I420
COLOR_YUV2GRAY_IYUV
COLOR_YUV2GRAY_NV12
COLOR_YUV2GRAY_NV21
COLOR_YUV2GRAY_UYNV
COLOR_YUV2GRAY_UYVY
COLOR_YUV2GRAY_Y422
COLOR_YUV2GRAY_YUNV
COLOR_YUV2GRAY_YUY2
COLOR_YUV2GRAY_YUYV
COLOR_YUV2GRAY_YV12
COLOR_YUV2GRAY_YVYU
COLOR_YUV2RGB
COLOR_YUV2RGBA_I420
COLOR_YUV2RGBA_IYUV
COLOR_YUV2RGBA_NV12
COLOR_YUV2RGBA_NV21
COLOR_YUV2RGBA_UYNV
COLOR_YUV2RGBA_UYVY
COLOR_YUV2RGBA_Y422
COLOR_YUV2RGBA_YUNV
COLOR_YUV2RGBA_YUY2
COLOR_YUV2RGBA_YUYV
COLOR_YUV2RGBA_YV12
COLOR_YUV2RGBA_YVYU
COLOR_YUV2RGB_I420
COLOR_YUV2RGB_IYUV
COLOR_YUV2RGB_NV12
COLOR_YUV2RGB_NV21
COLOR_YUV2RGB_UYNV
COLOR_YUV2RGB_UYVY
COLOR_YUV2RGB_Y422
COLOR_YUV2RGB_YUNV
COLOR_YUV2RGB_YUY2
COLOR_YUV2RGB_YUYV
COLOR_YUV2RGB_YV12
COLOR_YUV2RGB_YVYU
COLOR_YUV420P2BGR
COLOR_YUV420P2BGRA
COLOR_YUV420P2GRAY
COLOR_YUV420P2RGB
COLOR_YUV420P2RGBA
COLOR_YUV420SP2BGR
COLOR_YUV420SP2BGRA
COLOR_YUV420SP2GRAY
COLOR_YUV420SP2RGB
COLOR_YUV420SP2RGBA
I did some prototyping for Luminance conversions ( based on >>> http://en.wikipedia.org/wiki/HSL_and_HSV )
But not tested for release.
def get_YUV_V_Cr_Rec601_BRG_frame( brgFRAME ): # For the Rec. 601 primaries used in gamma-corrected sRGB, fast, VECTORISED MUL/ADD CODE
out = numpy.zeros( brgFRAME.shape[0:2] )
out += 0.615 / 255 * brgFRAME[:,:,1] # // Red # normalise to <0.0 - 1.0> before vectorised MUL/ADD, saves [usec] ... on 480x640 [px] faster goes about 2.2 [msec] instead of 5.4 [msec]
out -= 0.515 / 255 * brgFRAME[:,:,2] # // Green
out -= 0.100 / 255 * brgFRAME[:,:,0] # // Blue # normalise to <0.0 - 1.0> before vectorised MUL/ADD
return out

# -*- coding: utf-8 -*-
# #File : rgb2hls.py
# #Info : # TSMC
# #Desc :
import colorsys
import numpy as np
import scipy.misc
import tensorflow as tf
from PIL import Image
def rgb2hls(img):
""" note: elements in img is a float number less than 1.0 and greater than 0.
:param img: an numpy ndarray with shape NHWC
:return:
"""
assert len(img.shape) == 3
hue = np.zeros_like(img[:, :, 0])
luminance = np.zeros_like(img[:, :, 0])
saturation = np.zeros_like(img[:, :, 0])
for x in range(height):
for y in range(width):
r, g, b = img[x, y]
h, l, s = colorsys.rgb_to_hls(r, g, b)
hue[x, y] = h
luminance[x, y] = l
saturation[x, y] = s
return hue, luminance, saturation
def np_rgb2hls(img):
r, g, b = img[:, :, 0], img[:, :, 1], img[:, :, 2]
maxc = np.max(img, -1)
minc = np.min(img, -1)
l = (minc + maxc) / 2.0
if np.array_equal(minc, maxc):
return np.zeros_like(l), l, np.zeros_like(l)
smask = np.greater(l, 0.5).astype(np.float32)
s = (1.0 - smask) * ((maxc - minc) / (maxc + minc)) + smask * ((maxc - minc) / (2.001 - maxc - minc))
rc = (maxc - r) / (maxc - minc + 0.001)
gc = (maxc - g) / (maxc - minc + 0.001)
bc = (maxc - b) / (maxc - minc + 0.001)
rmask = np.equal(r, maxc).astype(np.float32)
gmask = np.equal(g, maxc).astype(np.float32)
rgmask = np.logical_or(rmask, gmask).astype(np.float32)
h = rmask * (bc - gc) + gmask * (2.0 + rc - bc) + (1.0 - rgmask) * (4.0 + gc - rc)
h = np.remainder(h / 6.0, 1.0)
return h, l, s
def tf_rgb2hls(img):
""" note: elements in img all in [0,1]
:param img: a tensor with shape NHWC
:return:
"""
assert img.get_shape()[-1] == 3
r, g, b = img[:, :, 0], img[:, :, 1], img[:, :, 2]
maxc = tf.reduce_max(img, -1)
minc = tf.reduce_min(img, -1)
l = (minc + maxc) / 2.0
# if tf.reduce_all(tf.equal(minc, maxc)):
# return tf.zeros_like(l), l, tf.zeros_like(l)
smask = tf.cast(tf.greater(l, 0.5), tf.float32)
s = (1.0 - smask) * ((maxc - minc) / (maxc + minc)) + smask * ((maxc - minc) / (2.001 - maxc - minc))
rc = (maxc - r) / (maxc - minc + 0.001)
gc = (maxc - g) / (maxc - minc + 0.001)
bc = (maxc - b) / (maxc - minc + 0.001)
rmask = tf.equal(r, maxc)
gmask = tf.equal(g, maxc)
rgmask = tf.cast(tf.logical_or(rmask, gmask), tf.float32)
rmask = tf.cast(rmask, tf.float32)
gmask = tf.cast(gmask, tf.float32)
h = rmask * (bc - gc) + gmask * (2.0 + rc - bc) + (1.0 - rgmask) * (4.0 + gc - rc)
h = tf.mod(h / 6.0, 1.0)
h = tf.expand_dims(h, -1)
l = tf.expand_dims(l, -1)
s = tf.expand_dims(s, -1)
x = tf.concat([tf.zeros_like(l), l, tf.zeros_like(l)], -1)
y = tf.concat([h, l, s], -1)
return tf.where(condition=tf.reduce_all(tf.equal(minc, maxc)), x=x, y=y)
if __name__ == '__main__':
"""
HLS: Hue, Luminance, Saturation
H: position in the spectrum
L: color lightness
S: color saturation
"""
avatar = Image.open("hue.jpg")
width, height = avatar.size
print("width: {}, height: {}".format(width, height))
img = np.array(avatar)
img = img / 255.0
print(img.shape)
# # hue, luminance, saturation = rgb2hls(img)
# hue, luminance, saturation = np_rgb2hls(img)
img_tensor = tf.convert_to_tensor(img, tf.float32)
hls = tf_rgb2hls(img_tensor)
h, l, s = hls[:, :, 0], hls[:, :, 1], hls[:, :, 2]
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
hue, luminance, saturation = sess.run([h, l, s])
scipy.misc.imsave("hls_h_.jpg", hue)
scipy.misc.imsave("hls_l_.jpg", luminance)
scipy.misc.imsave("hls_s_.jpg", saturation)

In case someone is looking for a self-contained solution (I really didn't want to add OpenCV as a dependency), I rewrote the official python colorsys rgb_to_hls() and hls_to_rgb() functions to be usable for numpy:
import numpy as np
def rgb_to_hls(rgb_array: np.ndarray) -> np.ndarray:
"""
Expects an array of shape (X, 3), each row being RGB colours.
Returns an array of same size, each row being HLS colours.
Like `colorsys` python module, all values are between 0 and 1.
NOTE: like `colorsys`, this uses HLS rather than the more usual HSL
"""
assert rgb_array.ndim == 2
assert rgb_array.shape[1] == 3
assert np.max(rgb_array) <= 1
assert np.min(rgb_array) >= 0
r, g, b = rgb_array.T.reshape((3, -1, 1))
maxc = np.max(rgb_array, axis=1).reshape((-1, 1))
minc = np.min(rgb_array, axis=1).reshape((-1, 1))
sumc = (maxc+minc)
rangec = (maxc-minc)
with np.errstate(divide='ignore', invalid='ignore'):
rgb_c = (maxc - rgb_array) / rangec
rc, gc, bc = rgb_c.T.reshape((3, -1, 1))
h = (np.where(minc == maxc, 0, np.where(r == maxc, bc - gc, np.where(g == maxc, 2.0+rc-bc, 4.0+gc-rc)))
/ 6) % 1
l = sumc/2.0
with np.errstate(divide='ignore', invalid='ignore'):
s = np.where(minc == maxc, 0,
np.where(l < 0.5, rangec / sumc, rangec / (2.0-sumc)))
return np.concatenate((h, l, s), axis=1)
def hls_to_rgb(hls_array: np.ndarray) -> np.ndarray:
"""
Expects an array of shape (X, 3), each row being HLS colours.
Returns an array of same size, each row being RGB colours.
Like `colorsys` python module, all values are between 0 and 1.
NOTE: like `colorsys`, this uses HLS rather than the more usual HSL
"""
ONE_THIRD = 1 / 3
TWO_THIRD = 2 / 3
ONE_SIXTH = 1 / 6
def _v(m1, m2, h):
h = h % 1.0
return np.where(h < ONE_SIXTH, m1 + (m2 - m1) * h * 6,
np.where(h < .5, m2,
np.where(h < TWO_THIRD, m1 + (m2 - m1) * (TWO_THIRD - h) * 6,
m1)))
assert hls_array.ndim == 2
assert hls_array.shape[1] == 3
assert np.max(hls_array) <= 1
assert np.min(hls_array) >= 0
h, l, s = hls_array.T.reshape((3, -1, 1))
m2 = np.where(l < 0.5, l * (1 + s), l + s - (l * s))
m1 = 2 * l - m2
r = np.where(s == 0, l, _v(m1, m2, h + ONE_THIRD))
g = np.where(s == 0, l, _v(m1, m2, h))
b = np.where(s == 0, l, _v(m1, m2, h - ONE_THIRD))
return np.concatenate((r, g, b), axis=1)
def _test1():
import colorsys
rgb_array = np.array([[.5, .5, .8], [.3, .7, 1], [0, 0, 0], [1, 1, 1], [.5, .5, .5]])
hls_array = rgb_to_hls(rgb_array)
for rgb, hls in zip(rgb_array, hls_array):
assert np.all(abs(np.array(colorsys.rgb_to_hls(*rgb) - hls) < 0.001))
new_rgb_array = hls_to_rgb(hls_array)
for hls, rgb in zip(hls_array, new_rgb_array):
assert np.all(abs(np.array(colorsys.hls_to_rgb(*hls) - rgb) < 0.001))
assert np.all(abs(rgb_array - new_rgb_array) < 0.001)
print("tests part 1 done")
def _test2():
import colorsys
hls_array = np.array([[0.6456692913385826, 0.14960629921259844, 0.7480314960629921], [.3, .7, 1], [0, 0, 0], [0, 1, 0], [.5, .5, .5]])
rgb_array = hls_to_rgb(hls_array)
for hls, rgb in zip(hls_array, rgb_array):
assert np.all(abs(np.array(colorsys.hls_to_rgb(*hls) - rgb) < 0.001))
new_hls_array = rgb_to_hls(rgb_array)
for rgb, hls in zip(rgb_array, new_hls_array):
assert np.all(abs(np.array(colorsys.rgb_to_hls(*rgb) - hls) < 0.001))
assert np.all(abs(hls_array - new_hls_array) < 0.001)
print("All tests done")
def _test():
_test1()
_test2()
if __name__ == "__main__":
_test()
(see gist)
(off topic: converting the other functions in the same way is actually a great training for someone wanting to get their hands dirty with numpy (or other SIMD / GPU) programming). Let me know if you do so :)
edit: rgb_to_hsv and hsv_to_rgb now also in the gist.

Related

Somthing wrong with my python implementation of phong shading with only numpy and PIL

Recently, I tried to implement Phong shading with only NumPy and PIL using python. But there is some black-and-white noise in the rendered image. Can you point out what I should do to improve my code to fix the issue?
The resulting image is as follows:
The mesh model could be downloaded from https://github.com/google/nerfactor/blob/main/third_party/xiuminglib/data/models/teapot.obj.
You could try the code below by yourself.
import random
import numpy as np
import trimesh
from meshio import load_obj
from PIL import Image
def phong_shading(light_direction, view_direction, normal, material):
# Calculate the ambient color
ambient_color = material.ambient_color
# Calculate the diffuse color
diffuse_coefficient = max(np.dot(normal, light_direction), 0)
diffuse_color = diffuse_coefficient * material.diffuse_color
# Calculate the specular color
halfway_direction = normalize(light_direction + view_direction)
specular_coefficient = max(np.dot(normal, halfway_direction), 0)
specular_coefficient = specular_coefficient ** material.shininess
specular_color = specular_coefficient * material.specular_color
# Combine the ambient, diffuse and specular colors
final_color = specular_color + diffuse_color + ambient_color
return final_color
def normalize(v, axis=-1, epsilon=1e-12):
square_sum = np.sum(np.square(v), axis, keepdims=True)
v_inv_norm = 1. / np.sqrt(np.maximum(square_sum, epsilon))
return v * v_inv_norm
def rasterize_triangle(vertices):
# calculate the bounding box of the triangle
min_x = int(min(vertices[:, 0]))
max_x = int(max(vertices[:, 0])) + 1
min_y = int(min(vertices[:, 1]))
max_y = int(max(vertices[:, 1])) + 1
for x in range(min_x, max_x):
for y in range(min_y, max_y):
if point_in_triangle(vertices, x, y):
yield (x, y)
def is_point_in_triangle(vertices, x, y):
v0, v1, v2 = vertices
A = 1/2 * (-v1[1]*v2[0] + v0[1]*(-v1[0] + v2[0]) +
v0[0]*(v1[1] - v2[1]) + v1[0]*v2[1])
s = v0[1]*v2[0] - v0[0]*v2[1] + (v2[1] - v0[1])*x + (v0[0] - v2[0])*y
t = v0[0]*v1[1] - v0[1]*v1[0] + (v0[1] - v1[1])*x + (v1[0] - v0[0])*y
return 0 <= s and s <= A and 0 <= t and t <= A and (s + t) <= A
def point_in_triangle(vertices, x, y):
# x, y = point
v0, v1, v2 = vertices
x1, y1, x2, y2, x3, y3 = v0[0], v0[1], v1[0], v1[1], v2[0], v2[1]
# Compute barycentric coordinates
denom = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3)
l1 = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / denom
l2 = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / denom
l3 = 1 - l1 - l2
# Check if point is inside the triangle
return 0 <= l1 <= 1 and 0 <= l2 <= 1 and 0 <= l3 <= 1
def world_to_camera_coordinates(vertices, camera_position):
''' convert from world coordinate to camera_coordinate.
this function has the assumption that the camera is looking at the origin.
and the y axis of the camera is pointing down to the ground.
Args:
vertices (np.array): the vertices of the mesh in world coordinate.
Returns:
the vertices in camera coordinate.
'''
camera_z_axis = -normalize(camera_position) # (3,)
world_z_axis = np.array([0, 0, 1])
project_y_on_z = -(-world_z_axis # camera_z_axis.T) * camera_z_axis
camera_y_axis = project_y_on_z - world_z_axis # (3,)
camera_x_axis = np.cross(camera_y_axis, camera_z_axis) # (3,)
camera_matrix = np.stack([camera_x_axis, camera_y_axis, camera_z_axis])
return (camera_matrix # (vertices - camera_position).T).T
def camera_to_screen_coordinates(vertices, width, height, fov, near_clip, far_clip):
aspect_ratio = width / height
# Create the perspective projection matrix
projection_matrix = perspective(fov, aspect_ratio, near_clip, far_clip)
# create a matrix to store the transformed vertices
transformed_vertices = np.ones((len(vertices), 4))
transformed_vertices[:, :3] = vertices
# multiply each vertex by the projection matrix
transformed_vertices = np.matmul(transformed_vertices, projection_matrix.T)
# Convert from homogeneous coordinates to screen coordinates
transformed_vertices[:, 0] = (
transformed_vertices[:, 0] / transformed_vertices[:, 3]) * (width / 2) + (width / 2)
transformed_vertices[:, 1] = (
transformed_vertices[:, 1] / transformed_vertices[:, 3]) * (height / 2) + (height / 2)
return transformed_vertices[:, :2]
def perspective(fov, aspect_ratio, near_clip, far_clip):
fov = np.radians(fov)
t = np.tan(fov / 2) * near_clip
b = -t
r = t * aspect_ratio
l = -r
projection_matrix = np.array(
[
[(2 * near_clip) / (r - l), 0, (r + l) / (r - l), 0],
[0, (2 * near_clip) / (t - b), (t + b) / (t - b), 0],
[0, 0, -(far_clip + near_clip) / (far_clip - near_clip),
-(2 * far_clip * near_clip) / (far_clip - near_clip)],
[0, 0, -1, 0]
]
)
return projection_matrix
def transform_to_screen_space(vertices, camera_position, img_width, img_height):
assert img_width == img_height, 'The image must be square'
# Transform the vertices to camera space
camera_vertices = world_to_camera_coordinates(vertices, camera_position)
# Transform the vertices to perspective space
fov = 45
focal = img_width / (2 * np.tan(np.radians(fov / 2)))
screen_vertices = camera_vertices / camera_vertices[:, 2].reshape(-1, 1)
screen_vertices[:, :2] = screen_vertices[:, :2] * focal + img_height / 2
return screen_vertices, camera_vertices
def area_triangle(v1, v2, v3):
''' compute the area of a triangle.
'''
return 0.5 * np.linalg.norm(np.cross(v2 - v1, v3 - v1))
def compute_vertices_normals(vertices, faces):
''' compute the normal vector for each vertex.
Args:
vertices (np.array): the vertices of the mesh in world coordinate.
faces
'''
# method with trimesh
# '''
mesh = trimesh.Trimesh(vertices=vertices, faces=faces, processed=False)
vertices_normals = normalize(mesh.vertex_normals, epsilon=1e-160)
# '''
# method with numpy
'''
vertices_normals = np.zeros_like(vertices).astype(np.float128)
v1 = vertices[faces][:, 0]
v2 = vertices[faces][:, 1]
v3 = vertices[faces][:, 2]
normal_before_normalization = np.cross(v2 - v1, v3 - v1)
per_face_area = 0.5 * np.linalg.norm(
normal_before_normalization, axis=-1, keepdims=True
)
per_face_area_enlarged = per_face_area * \
per_face_area.shape[0] / per_face_area.sum()
per_face_normal = normalize(normal_before_normalization, epsilon=1e-160)
weighted_normal = per_face_normal * per_face_area_enlarged
weighted_normal_boardcast = np.reshape(
np.repeat(np.expand_dims(weighted_normal, 1), 3, axis=1), (-1, 3)
)
np.add.at(vertices_normals, faces.ravel(), weighted_normal_boardcast)
vertices_normals = normalize(vertices_normals, epsilon=1e-160)
'''
return vertices_normals
def barycentric_coords(triangle_vertices, x, y):
x1, y1, z1 = triangle_vertices[0]
x2, y2, z2 = triangle_vertices[1]
x3, y3, z3 = triangle_vertices[2]
# calculate barycentric coordinates
lambda1 = ((y2 - y3)*(x - x3) + (x3 - x2)*(y - y3)) / \
((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3))
lambda2 = ((y3 - y1)*(x - x3) + (x1 - x3)*(y - y3)) / \
((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3))
lambda3 = 1 - lambda1 - lambda2
return np.array([lambda1, lambda2, lambda3]).reshape(-1, 1)
def render_phong(vertices, faces, camera_position, light_position, width, height, material):
# compute the normal vector for each vertex
vertices_normals = compute_vertices_normals(vertices, faces)
# Transform the vertices to screen space
transformed_vertices, camera_vertices = transform_to_screen_space(
vertices, camera_position, width, height)
# Create an empty image
img = Image.new('RGB', (width, height), (0, 0, 0))
pixels = img.load()
pixel_depth = np.ones((width, height)) * np.inf
for face in faces:
v1 = transformed_vertices[face[0]]
v2 = transformed_vertices[face[1]]
v3 = transformed_vertices[face[2]]
if area_triangle(v1, v2, v3) == 0:
continue
# calculate the normal vector for the face
normal = vertices_normals[face]
# calculate the light and view direction vectors for each vertex
light_direction = normalize(light_position - vertices[face])
view_direction = normalize(camera_position - vertices[face])
# Rasterize the triangle
for x, y in rasterize_triangle(transformed_vertices[face]):
for i in range(20):
tubx = random.uniform(0, 1.0) + x
tuby = random.uniform(0, 1.0) + y
# calculate the barycentric coordinates of the pixel
barycentric = barycentric_coords(
transformed_vertices[face], tubx, tuby)
if np.min(barycentric) < 0: # Check if pixel is outside of the triangle
continue
# Interpolate the vertex attributes to get per-pixel attributes
interpolated_normal = (barycentric * normal).sum(axis=0)
interpolated_light_direction = (
barycentric * light_direction
).sum(axis=0)
interpolated_view_direction = (
barycentric * view_direction
).sum(axis=0)
interpolated_camera_vertices = (
barycentric * camera_vertices[face]).sum(axis=0)
# Calculate the color of the pixel
color = phong_shading(interpolated_light_direction,
interpolated_view_direction, interpolated_normal, material)
if x >= 0 and x < width and y >= 0 and y < height:
oldr, oldg, oldb = pixels[x, y]
newr, newg, newb = (np.clip(color, 0, 1)
* 255).astype(np.uint8)
# newr = newr if newr > oldr else oldr
# newg = newg if newg > oldg else oldg
# newb = newb if newb > oldb else oldb
depth = interpolated_camera_vertices[2]
if depth < pixel_depth[x, y]:
# print(depth, pixel_depth[x, y])
pixel_depth[x, y] = depth
pixels[x, y] = (newr, newg, newb)
# if x < 453 and x > 415 and y > 255 and y < 265:
# img.save(f"debug/f_{face}_x_{x}_y_{y}_d_{depth}.jpg")
return img
class PhongShader():
def __init__(self, light_position, camera_position, image_width=512, image_height=512):
# assert the camera position is not along z axis.
self.light_position = light_position
self.camera_position = camera_position
self.image_width = image_width
self.image_height = image_height
def render(self, vertices, faces, material):
return render_phong(vertices, faces, self.camera_position, self.light_position, self.image_width, self.image_height, material)
class Material():
def __init__(self) -> None:
self.ambient_color = np.array([0.1, 0.1, 0.1])
self.diffuse_color = np.array([1., 0.0, 0.5])
self.specular_color = np.array([0.5, 0.5, 0.5])
self.shininess = 50
def main():
# load the mesh
mesh = trimesh.load('teapot.obj')
vertices, faces = mesh.vertices, mesh.faces
# create a shader
shader = PhongShader(light_position=np.array(
[8, 0, 0]), camera_position=np.array([8, 0, 0]))
# render the image
material = Material()
img = shader.render(vertices, faces, material)
img.save("output.jpg")
if __name__ == '__main__':
main()
The possible reason could be discreazation in coding. But I am not sure how to fix it.

fast <image,time> linear interpolation

I'm trying to achieve linear interpolation, where the data points are N images of shape: HxWx3 (stored in buf (NxHxWx3)), and the points to interpolate are specified in another (2D) grid (interp_values).
Non-vectorizable approach:
In principle I have made interp_values a HxW grid with values 0..N-1 indicating for each i,j element from which image (in buf) to read it from, including fractional values meaning interpolation.
E.g.: a value of 3.6 means blend 40% (1-0.6) of image 3 with 60% (0.6) of image 4. However with this approach it is quite impossible to vectorize the code, and performance was poor.
One vectorization approach:
So I changed interp_values to be a NxHxWx3 grid with values 0..1. Each column :,i,j,c would specify blend coefficients for the N images, where only 1 or 2 elements are non-zero, e.g. for 3.6 we have: [0, 0, 0, 0.6, 0.4, 0, 0, ...]. I can convert interp_values from HxW to NxHxWx3 with:
def expand_interp_values(interp_values):
r = np.zeros((N,) + interp_values.shape + (3,))
for i in range(interp_values.shape[0]):
for j in range(interp_values.shape[1]):
v = interp_values[i, j]
a, b, x = math.floor(v), math.ceil(v), math.fmod(v, 1)
if int(a) == int(b):
r[a, i, j, :] = 3 * [1]
else:
r[a, i, j, :] = 3 * [1 - x]
r[b, i, j, :] = 3 * [x]
return r
This representation is more sparse (many zeros) but now interpolation can be computed as element-wise multiplication between buf and interp_values (the multiplication part of the linear interpolation) followed by a sum(..., axis=0) (i.e. the addition part of the linear interpolation):
def linear_interp(data, interp_values):
return np.sum(data * interp_values, axis=0)
With this approach, there is some performance improvement, however it seems with this approach the CPU will be most of the times busy computing x1*0, x2*0, ... or 0 + 0 + 0...
Can this be improved any better?
Additionally, the creation of the expanded interp_values grid is not vectorized, so perhaps performance would be bad if that grid has to be updated continuously.
Complete python+opencv code:
import cv2
import numpy as np
import math
vid = cv2.VideoCapture(0)
vid.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
vid.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
# store last N images into a NxHxWx3 grid (circular buffer):
N = 25
buf = None
interp_values = None
DOWNSAMPLING = 6
def linear_interp(data, interp_values):
return np.sum(data * interp_values / 256, axis=0)
def expand_interp_values(interp_values):
r = np.zeros((N,) + interp_values.shape + (3,))
for i in range(interp_values.shape[0]):
for j in range(interp_values.shape[1]):
v = interp_values[i, j]
a, b, x = math.floor(v), math.ceil(v), math.fmod(v, 1)
if int(a) == int(b):
r[a, i, j, :] = 3 * [1]
else:
r[a, i, j, :] = 3 * [1 - x]
r[b, i, j, :] = 3 * [x]
return r
while True:
ret, frame = vid.read()
H, W, Ch = frame.shape
frame = cv2.resize(frame, dsize=(W//DOWNSAMPLING, H//DOWNSAMPLING), interpolation=cv2.INTER_LINEAR)
# circular buffer:
if buf is None:
buf = np.zeros((N,) + frame.shape, dtype=np.uint8)
# there should be a simpler way to a FIFO-grid...
for i in reversed(range(1, N)):
buf[i] = buf[i - 1]
buf[0] = frame
if interp_values is None:
# create a lookup pattern here:
interp_values = np.zeros(frame.shape[:2])
for i in range(frame.shape[0]):
for j in range(frame.shape[1]):
y = i / (frame.shape[0] - 1) * 2 - 1
x = j / (frame.shape[1] - 1) * 2 - 1
#interp_values[i, j] = (N - 1) * min(1, math.hypot(x, y))
interp_values[i, j] = (N - 1) * (y + 1) / 2
interp_values = expand_interp_values(interp_values)
im = linear_interp(buf, interp_values)
im = cv2.resize(im, dsize=(W, H), interpolation=cv2.INTER_LANCZOS4)
cv2.imshow('image', im)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
vid.release()
cv2.destroyAllWindows()

Implementing smooth colouring in mandelbrot set

I am trying to colour MandelBrot using HSV values and the PIL Library.
Even after multiple tries fiddling with HSV values, I could not achieve the desired effect.
here is what I currently have
Here is the desired effect
This is the code that I am trying, It could also be beneficial if you could add some tips to optimise the below code to compute the set faster, I am new to python
from PIL import Image
import random
import math
from decimal import Decimal
# Size of the Image Canvas
HEIGHT = 500
ZOOM = 0.0
Y_PAN = 0.0
# Range of the Complex Plane
MIN_X = -2.0 + ZOOM
MAX_X = 2.0 - ZOOM
MAX_Y = 2.0 + Y_PAN - ZOOM
MIN_Y = -2.0 + Y_PAN + ZOOM
DATA = []
def map_to_scale_d(x, in_min, in_max, out_min, out_max):
# returns float
return float((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
def map_to_scale(x, in_min, in_max, out_min, out_max):
# returns int
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
# Max iterations till Zn
ITER = 200
# loop to traverse every single point in Canvas
for y in xrange(HEIGHT):
for x in xrange(HEIGHT):
# convert to complex plane scale
a = map_to_scale_d(x, 0, HEIGHT, MIN_X, MAX_X)
b = map_to_scale_d(y, 0, HEIGHT, MAX_Y, MIN_Y)
# original values
_a = a
_b = b
counter = 0
# start the iteration at (a,b) in complex plane
# calculate z^2 + c
while(counter < ITER):
aa = a * a - b * b
bb = 2 * a * b
a = aa + _a
b = bb + _b
if((abs(aa + bb)) > 4):
break
counter = counter + 1
# initialise color
h = 0
s = map_to_scale(counter, 0, ITER, 0, 100)
v = map_to_scale(counter, 0, ITER, 0, 100)
if(counter == ITER):
h = 0
s = 0
v = 0
# convert to 8-bit
h = map_to_scale(h, 0, 360, 0, 255)
s = map_to_scale(s, 0, 100, 0, 255)
v = map_to_scale(v, 0, 100, 0, 255)
DATA.append((h, s, v))
img = Image.new('HSV', (HEIGHT, HEIGHT))
img.putdata(DATA)
img.show()
img.convert('RGB').save('test.png')

Mapping polar coordinates to a color (RGB, etc.)

Say I have 80 (or n) polar coordinates that are pretty evenly distributed across a circular area. I want a unique color for each polar coordinate.
If you imagine a color wheel like this (though it could be a different transformation if you like), I'd like one of its colors given a polar coordinate.
At first I was not using the actual polar coordinates, and just scaled one of the channels by some even stride, like RGB (255, i * stride, 255). But now I'd like different colors from all over the spectrum (or at least more than a single color tone).
I thought of just using an image of a color wheel and then sampling it, but that seems kind of weak. Isn't there a formula I could use to convert the polar coordinates to some assumed/generated RGB, HSV, or CMYK space?
I'm working in Python 3, but I'm mostly interested in the formulas/algorithm. I'm not using any specific plotting API.
You could use a conversion from HSV or HSL to RGB, many packages such as Colour (Numpy Vectorised) or python-colormath (Vanilla Python) have implementations:
From Colour, assuming you have Numpy and the tsplit and tstack definitions:
def RGB_to_HSV(RGB):
"""
Converts from *RGB* colourspace to *HSV* colourspace.
Parameters
----------
RGB : array_like
*RGB* colourspace array.
Returns
-------
ndarray
*HSV* array.
Notes
-----
- Input *RGB* colourspace array is in domain [0, 1].
- Output *HSV* colourspace array is in range [0, 1].
References
----------
- :cite:`EasyRGBj`
- :cite:`Smith1978b`
- :cite:`Wikipediacg`
Examples
--------
>>> RGB = np.array([0.49019608, 0.98039216, 0.25098039])
>>> RGB_to_HSV(RGB) # doctest: +ELLIPSIS
array([ 0.2786738..., 0.744 , 0.98039216])
"""
maximum = np.amax(RGB, -1)
delta = np.ptp(RGB, -1)
V = maximum
R, G, B = tsplit(RGB)
S = np.asarray(delta / maximum)
S[np.asarray(delta == 0)] = 0
delta_R = (((maximum - R) / 6) + (delta / 2)) / delta
delta_G = (((maximum - G) / 6) + (delta / 2)) / delta
delta_B = (((maximum - B) / 6) + (delta / 2)) / delta
H = delta_B - delta_G
H = np.where(G == maximum, (1 / 3) + delta_R - delta_B, H)
H = np.where(B == maximum, (2 / 3) + delta_G - delta_R, H)
H[np.asarray(H < 0)] += 1
H[np.asarray(H > 1)] -= 1
H[np.asarray(delta == 0)] = 0
HSV = tstack((H, S, V))
return HSV
def HSV_to_RGB(HSV):
"""
Converts from *HSV* colourspace to *RGB* colourspace.
Parameters
----------
HSV : array_like
*HSV* colourspace array.
Returns
-------
ndarray
*RGB* colourspace array.
Notes
-----
- Input *HSV* colourspace array is in domain [0, 1].
- Output *RGB* colourspace array is in range [0, 1].
References
----------
- :cite:`EasyRGBn`
- :cite:`Smith1978b`
- :cite:`Wikipediacg`
Examples
--------
>>> HSV = np.array([0.27867384, 0.74400000, 0.98039216])
>>> HSV_to_RGB(HSV) # doctest: +ELLIPSIS
array([ 0.4901960..., 0.9803921..., 0.2509803...])
"""
H, S, V = tsplit(HSV)
h = np.asarray(H * 6)
h[np.asarray(h == 6)] = 0
i = np.floor(h)
j = V * (1 - S)
k = V * (1 - S * (h - i))
l = V * (1 - S * (1 - (h - i))) # noqa
i = tstack((i, i, i)).astype(np.uint8)
RGB = np.choose(
i, [
tstack((V, l, j)),
tstack((k, V, j)),
tstack((j, V, l)),
tstack((j, k, V)),
tstack((l, j, V)),
tstack((V, j, k)),
],
mode='clip')
return RGB
def RGB_to_HSL(RGB):
"""
Converts from *RGB* colourspace to *HSL* colourspace.
Parameters
----------
RGB : array_like
*RGB* colourspace array.
Returns
-------
ndarray
*HSL* array.
Notes
-----
- Input *RGB* colourspace array is in domain [0, 1].
- Output *HSL* colourspace array is in range [0, 1].
References
----------
- :cite:`EasyRGBl`
- :cite:`Smith1978b`
- :cite:`Wikipediacg`
Examples
--------
>>> RGB = np.array([0.49019608, 0.98039216, 0.25098039])
>>> RGB_to_HSL(RGB) # doctest: +ELLIPSIS
array([ 0.2786738..., 0.9489796..., 0.6156862...])
"""
minimum = np.amin(RGB, -1)
maximum = np.amax(RGB, -1)
delta = np.ptp(RGB, -1)
R, G, B = tsplit(RGB)
L = (maximum + minimum) / 2
S = np.where(L < 0.5, delta / (maximum + minimum),
delta / (2 - maximum - minimum))
S[np.asarray(delta == 0)] = 0
delta_R = (((maximum - R) / 6) + (delta / 2)) / delta
delta_G = (((maximum - G) / 6) + (delta / 2)) / delta
delta_B = (((maximum - B) / 6) + (delta / 2)) / delta
H = delta_B - delta_G
H = np.where(G == maximum, (1 / 3) + delta_R - delta_B, H)
H = np.where(B == maximum, (2 / 3) + delta_G - delta_R, H)
H[np.asarray(H < 0)] += 1
H[np.asarray(H > 1)] -= 1
H[np.asarray(delta == 0)] = 0
HSL = tstack((H, S, L))
return HSL

Perlin noise looks streaky and not coherent

Now that my perlin generator is 'working' I created noise, to find that it is nothing like what I see on the internets...
My noise:
Notice the streaks:
What I am aiming to get (obviously with corresponding colour):
1:
Why does mine look so noisy and nasty?
Code (sorry for no stub, the Perlin noise makes up most of the program so it's important to include the full program):
from PIL import Image
from tkinter import filedialog
from random import randint, random
#Initialise width / height
width = 625
height = 625
#Import gradient picture - 200*1 image used to texture perlin noise
#R,G,B,Alpha
gradient = Image.open("image.png")
gradlist = list(gradient.getdata())
#Create new image
img = Image.new('RGBA', (width, height), color=(255, 255, 255, 255))
#Perlin noise modules --------------------------------------------------------------------------------------------------------
#Modules
from random import sample
from math import floor
p = sample([x for x in range(0, (width * height))], (width * height)) * 2
#Antialising
def fade(t):
retval = 6*(t**5) - 15*(t**4) + 10*(t**3)
return retval
#Linear interpolation
def lerp(t,a,b):
retval = a + (t * (b - a))
return retval
#Clever bitwise hash stuff - picks a unit vector from 12 possible - (1,1,0),(-1,1,0),(1,-1,0),(-1,-1,0),(1,0,1),(-1,0,1),(1,0,-1),(-1,0,-1),(0,1,1),(0,-1,1),(0,1,-1),(0,-1,-1)
def grad(hash, x, y, z):
h = hash % 15
if h < 8:
u = x
else:
u = y
if h < 4:
v = y
elif h in (12, 14):
v = x
else:
v = z
return (u if (h & 1) == 0 else -u) + (v if (h & 2) == 0 else -v)
#Perlin function
def perlin(x,y,z):
ix = int(floor(x)) & 255
iy = int(floor(y)) & 255
iz = int(floor(z)) & 255
x -= int(floor(x))
y -= int(floor(y))
z -= int(floor(z))
u = fade(x)
v = fade(y)
w = fade(z)
#Complicated hash stuff
A = p[ix] + iy
AA = p[A] + iz
AB = p[A + 1] + iz
B = p[ix + 1] + iy
BA = p[B] + iz
BB = p[B + 1] + iz
return -lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z),grad(p[BA], x - 1, y, z)),lerp(u, grad(p[AB], x, y - 1, z),grad(p[BB], x - 1, y - 1, z))),lerp(v, lerp(u, grad(p[AA + 1], x, y, z - 1),grad(p[BA + 1], x - 1, y, z - 1)), lerp(u, grad(p[AB + 1], x, y - 1, z - 1),grad(p[BB + 1], x - 1, y - 1, z - 1))))
def octavePerlin(x,y,z,octaves,persistence):
total = 0
frequency = 1
amplitude = 1
maxValue = 0
for x in range(octaves):
total += perlin(x * frequency, y * frequency, z * frequency) * amplitude
maxValue += amplitude
amplitude *= persistence
frequency *= 2
return total / maxValue
z = random()
img.putdata([gradlist[int(octavePerlin((x + random() - 0.5) / 1000, (y + random() - 0.5) / 1000, z, 4, 2) * 100 + 100)] for x in range(width) for y in range(height)])
img.save(filedialog.asksaveasfilename() + ".png", "PNG")

Categories