© All rights reserved. Powered by Florisera.

RSS Daily tech news
  • This battery self-destructs: Biodegradable power inspired by 'Mission: Impossible'
    Scientists at Binghamton University are bringing a sci-fi fantasy to life by developing tiny batteries that vanish after use inspired by Mission: Impossible. Led by Professor Seokheun Choi, the team is tackling one of the trickiest parts of biodegradable electronics: the power source. Instead of using toxic materials, they re exploring probiotics friendly bacteria often […]
  • Scientists freeze quantum motion using ultrafast laser trick
    Harvard and PSI scientists have managed to freeze normally fleeting quantum states in time, creating a pathway to control them using pure electronic tricks and laser precision.
  • Researchers develop recyclable, healable electronics
    Electronics often get thrown away after use because recycling them requires extensive work for little payoff. Researchers have now found a way to change the game.
  • Ultra-thin lenses that make infrared light visible
    Physicists have developed a lens with 'magic' properties. Ultra-thin, it can transform infrared light into visible light by halving the wavelength of incident light.
  • Discovery could boost solid-state battery performance
    Researchers have discovered that the mixing of small particles between two solid electrolytes can generate an effect called a 'space charge layer,' an accumulation of electric charge at the interface between the two materials. The finding could aid the development of batteries with solid electrolytes, called solid-state batteries, for applications including mobile devices and electric […]
  • Engineers develop self-healing muscle for robots
    Students recently unveiled their invention of a robotic actuator -- the 'muscle' that converts energy into a robot's physical movement -- that has the ability to detect punctures or pressure, heal the injury and repair its damage-detecting 'skin.'

HSV color chart for vallejo paint

by Florius

As the owner of a Bambu Lab A1 printer with the AMS Lite, I regularly print trinkets, busts, and other figurines that I enjoy painting. Over time, my paint collection has grown to nearly 28 different colors from the Vallejo Model Color range. I also have a few Citadel paints, but for some reason, I’ve naturally gravitated toward Vallejo. Sometimes I mix colors to get a specific shade, but in many cases, it’s easier—and more consistent—to just buy the exact color I need. Mixing the same shade twice is tricky. 

A few days ago, I printed a paint rack to organize and store all my colors. I wanted to arrange them in a meaningful order—either as a rainbow or from dark to light. The rack I printed has a fixed layout of 3 rows by 16 columns, so I was limited in how I could structure things. No matter how I tried, I couldn’t get the order to feel right.

So I looked online for inspiration. I remembered seeing color charts before and eventually stumbled upon an HSB pie chart. It’s essentially a 3D mapping of the RGB color model. In hindsight, maybe I should’ve looked into the CMYK color model instead—it’s more closely tied to pigments and paints, while RGB is meant for screens. My quest lead me to see that my color choices were quite poor and I definitely should look at different colors from now on. 

From RGB to the HSB pie chart.

Every digital color can be represented by combining Red (R), Green (G), and Blue (B) light. Each channel ranges from 0 (no intensity) to 255 (full intensity). For example, pure red is written as (255, 0, 0), white as (255, 255, 255) and black as (0, 0, 0). Instead of writing RGB values in decimal, it’s also common to use hexadecimal notation, where 255 is written as FF and 60 as 3C. Using this format, yellow—being a mix of red and green at full intensity—is represented as #FFFF00.

While RGB is widely used, a more intuitive way to describe color is through the HSV color model, which stands for Hue, Saturation, and Value. Sometimes “Brightness” is used in place of “Value”, but in this context, both refer to the same thing: the highest of the RGB components, or Cmax.

Before converting RGB to HSV, the RGB values are normalized to the range [0,1] by dividing each component by 255 (i.e., R’= R/255, and similar for G’ and B’). Based on these normalized values, we define:

\[
\begin{aligned}
C_{\text{max}} &= \max(R’, G’, B’) \\
C_{\text{min}} &= \min(R’, G’, B’) \\
\Delta &= C_{\text{max}} – C_{\text{min}}
\end{aligned}
\]

Hue

Hue describes the type of color and is computed based on which RGB component is the largest. It’s defined as:

\[
H =
\begin{cases}
0^\circ, & \Delta = 0 \\
60^\circ \times \left( \frac{G’ – B’}{\Delta} \bmod 6 \right), & C_{\text{max}} = R’ \\
60^\circ \times \left( \frac{B’ – R’}{\Delta} + 2 \right), & C_{\text{max}} = G’ \\
60^\circ \times \left( \frac{R’ – G’}{\Delta} + 4 \right), & C_{\text{max}} = B’
\end{cases}
\]

Note: If two components are tied for the maximum, the standard approach is to choose based on the order: R’ > G’ > B’.

Saturation

Saturation measures how vivid the color is:

\[
S =
\begin{cases}
0, & C_{\text{max}} = 0 \\
\frac{\Delta}{C_{\text{max}}}, & \text{otherwise}
\end{cases}
\]

Value

Value (or brightness) corresponds to the highest RGB component:

\[V=C_{max}\]

HSV model

Hue in the HSV model is measured in degrees from 0° to 360°, forming a circular spectrum—red appears at both 0° and 360°. In the 2D representation of the hue-saturation plane, hue corresponds to the angle around the circle, while saturation is the radial distance from the center: fully saturated colors lie on the edge, and desaturated colors (grays) lie near the center.

To represent value (or brightness), the model adds a third dimension. This creates a cylinder, where vertical position represents brightness: the bottom of the cylinder corresponds to black (value = 0), and the top represents full brightness (value = 1). Each horizontal slice of the cylinder at a given brightness level forms a 2D hue-saturation circle.

Figure X shows these horizontal slices taken at intervals of 0.2 in brightness. As brightness decreases, colors become darker and more muted, eventually blending into black. Thus, the full 3D HSV model is best visualized as a cylinder, where:

  • Hue is the angle around the vertical axis,
  • Saturation is the distance from the center axis outward,
  • Value is the height along the vertical axis.

This cylindrical form helps visualize how colors shift in hue, become more or less vivid (saturation), and change in brightness—all in a single geometric structure.

Vallejo Game Colors

My dataset consists of the 28 colors from the Vallejo Game Color set. These colors were converted into hexadecimal codes using the website encycolorpedia.com, which matches Vallejo’s color codes with their closest digital equivalents, to see which colors I have, I refer you to Figure X. One thing I noticed is that the brightness on the digital results appears slightly lower for nearly all the colors—this could be due to my display settings, so please take the data with a grain of salt. Nevertheless, the dataset provides a solid basis for analysis.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.

				
					import pyvista as pv
import numpy as np

# Cylinder parameters
radius = 1.0
height = 3.0

# Create a basic cylinder
cylinder = pv.Cylinder(center=(0, 0, 0), direction=(0, 0, 1), radius=radius, height=height, resolution=100)

# Function to generate a random point inside the cylinder
def random_point_in_cylinder(radius, height):
    while True:
        x, y = np.random.uniform(-radius, radius, 2)
        if x**2 + y**2 <= radius**2:
            break
    z = np.random.uniform(-height / 2, height / 2)
    return (x, y, z)

# Generate 4 random points
points = np.array([random_point_in_cylinder(radius, height) for _ in range(4)])

# Create small spheres at each point
spheres = [pv.Sphere(radius=0.05, center=pt) for pt in points]

# Start plotting
plotter = pv.Plotter()
plotter.add_mesh(cylinder, color="lightgray", opacity=0.5)

# Add spheres (dots)
for s in spheres:
    plotter.add_mesh(s, color="red")

plotter.show()

				
			
				
					import pyvista as pv
import numpy as np
import matplotlib.colors as mcolors

num_x = 360
num_y = 100

plane = pv.Plane(i_resolution=num_x-1, j_resolution=num_y-1, i_size=1, j_size=1)
points = plane.points

X = points[:, 0]
Y = points[:, 1]

X_norm = (X - X.min()) / (X.max() - X.min())  # Hue: 0-1
Y_norm = (Y - Y.min()) / (Y.max() - Y.min())  # Saturation: 0-1

hsv = np.stack([X_norm, Y_norm, np.ones_like(X_norm)], axis=1)
rgb = mcolors.hsv_to_rgb(hsv)
colors = (rgb * 255).astype(np.uint8)

plane.point_data['Colors'] = colors

plotter = pv.Plotter()
plotter.add_mesh(plane, scalars='Colors', rgb=True)
plotter.set_background("black")
plotter.show()

				
			
				
					import pyvista as pv
import numpy as np
import matplotlib.colors as mcolors

num_x = 360
num_y = 100

plane = pv.Plane(i_resolution=num_x-1, j_resolution=num_y-1, i_size=1, j_size=1)
points = plane.points

X = points[:, 0]
Y = points[:, 1]

X_norm = (X - X.min()) / (X.max() - X.min())  # Hue: 0-1
Y_norm = (Y - Y.min()) / (Y.max() - Y.min())  # Saturation: 0-1

value = np.full_like(X_norm, 0.5)  # Value = 0.5 (half brightness)

hsv = np.stack([X_norm, Y_norm, value], axis=1)
rgb = mcolors.hsv_to_rgb(hsv)
colors = (rgb * 255).astype(np.uint8)

plane.point_data['Colors'] = colors

plotter = pv.Plotter()
plotter.add_mesh(plane, scalars='Colors', rgb=True)
plotter.set_background("white")
plotter.show()

				
			
				
					import pyvista as pv
import numpy as np
import matplotlib.colors as mcolors

# Grid size
num_x = 360
num_y = 100

# Create HSV plane
plane = pv.Plane(i_resolution=num_x - 1, j_resolution=num_y - 1, i_size=1, j_size=1)
points = plane.points

X = points[:, 0]
Y = points[:, 1]

# Normalize for HSV
hue = (X + 0.5)         # from [-0.5, 0.5] → [0, 1]
sat = (Y + 0.5)         # from [-0.5, 0.5] → [0, 1]
val = np.ones_like(hue) # constant brightness = 1

# HSV to RGB
hsv = np.stack([hue, sat, val], axis=1)
rgb = mcolors.hsv_to_rgb(hsv)
colors = (rgb * 255).astype(np.uint8)
plane.point_data['Colors'] = colors

# Function to convert hex to dot on plane
def create_dot(hex_color):
    rgb = mcolors.to_rgb(f"#{hex_color}")
    h, s, v = mcolors.rgb_to_hsv(rgb)
    x = h - 0.5
    y = s - 0.5
    return pv.Sphere(radius=0.01, center=(x, y, 0)), f"#{hex_color}"

# 🎯 Dots for given hex codes
dots = [create_dot("FF00FF"), create_dot("80FFA4")]

# Plot
plotter = pv.Plotter()
plotter.add_mesh(plane, scalars='Colors', rgb=True)
for dot, color in dots:
    plotter.add_mesh(dot, color=color)
plotter.set_background("white")
plotter.show()

				
			

Circle hsv wheel below with brightness .5, this cna be changed.

				
					import pyvista as pv
import numpy as np
import matplotlib.colors as mcolors

# Grid resolution
n_radius = 100
n_theta = 360

# Create polar grid: r (0 to 1), theta (0 to 2π)
r = np.linspace(0, 1, n_radius)
theta = np.linspace(0, 2 * np.pi, n_theta)
r_grid, theta_grid = np.meshgrid(r, theta)

# Convert to Cartesian coordinates
x = (r_grid * np.cos(theta_grid)).flatten()
y = (r_grid * np.sin(theta_grid)).flatten()
z = np.zeros_like(x)

# Create PolyData with points
points = np.column_stack((x, y, z))
hs_disc = pv.PolyData(points)

# Build cells for surface (quads)
cells = []
for i in range(n_theta - 1):
    for j in range(n_radius - 1):
        p0 = i * n_radius + j
        p1 = p0 + 1
        p2 = p0 + n_radius + 1
        p3 = p0 + n_radius
        cells.append([4, p0, p1, p2, p3])
hs_disc = hs_disc.cast_to_unstructured_grid()
hs_disc.cells = np.array(cells, dtype=np.int64).flatten()

# HSV values per point
hue = theta_grid.flatten() / (2 * np.pi)        # from 0 to 1
sat = r_grid.flatten()                          # from 0 to 1
val = np.full_like(hue, 0.5)                    # brightness = 0.5

hsv = np.stack([hue, sat, val], axis=1)
rgb = mcolors.hsv_to_rgb(hsv)
colors = (rgb * 255).astype(np.uint8)

# Assign RGB to mesh
hs_disc.point_data["Colors"] = colors

# Plot
plotter = pv.Plotter()
plotter.add_mesh(hs_disc, scalars="Colors", rgb=True)
plotter.set_background("white")
plotter.show()

				
			

6 disks with random hex color

				
					import pyvista as pv
import numpy as np
import matplotlib.colors as mcolors

# Brightness levels (disks)
brightness_levels = [1.0, 0.8, 0.6, 0.4, 0.2, 0.0]
spacing = 3  # vertical spacing between disks

n_radius = 100
n_theta = 360

def create_disk(brightness):
    r = np.linspace(0, 1, n_radius)
    theta = np.linspace(0, 2 * np.pi, n_theta)
    r_grid, theta_grid = np.meshgrid(r, theta)
    x = (r_grid * np.cos(theta_grid)).flatten()
    y = (r_grid * np.sin(theta_grid)).flatten()
    z = np.full_like(x, brightness * spacing)
    points = np.column_stack((x, y, z))
    disk = pv.PolyData(points)

    cells = []
    for i in range(n_theta - 1):
        for j in range(n_radius - 1):
            p0 = i * n_radius + j
            p1 = p0 + 1
            p2 = p0 + n_radius + 1
            p3 = p0 + n_radius
            cells.append([4, p0, p1, p2, p3])
    disk = disk.cast_to_unstructured_grid()
    disk.cells = np.array(cells, dtype=np.int64).flatten()

    hue = theta_grid.flatten() / (2 * np.pi)
    sat = r_grid.flatten()
    val = np.full_like(hue, brightness)
    hsv = np.stack([hue, sat, val], axis=1)
    rgb = mcolors.hsv_to_rgb(hsv)
    colors = (rgb * 255).astype(np.uint8)
    disk.point_data["Colors"] = colors
    return disk

disks = [create_disk(b) for b in brightness_levels]

plotter = pv.Plotter()
for disk in disks:
    plotter.add_mesh(disk, scalars="Colors", rgb=True, opacity=0.8)

# Your input hex color
input_hex = "#F83D9E"

# Convert hex to RGB (0 to 1)
input_rgb = np.array(mcolors.to_rgb(input_hex))

# Convert RGB to HSV
input_hsv = mcolors.rgb_to_hsv(input_rgb.reshape(1, -1))[0]

input_hue = input_hsv[0]       # 0-1
input_sat = input_hsv[1]       # 0-1
input_val = input_hsv[2]       # 0-1

# Find closest brightness ring
closest_brightness = min(brightness_levels, key=lambda b: abs(b - input_val))

# Compute point coords
theta = input_hue * 2 * np.pi
radius = input_sat
x = radius * np.cos(theta)
y = radius * np.sin(theta)
z = closest_brightness * spacing

point_sphere = pv.Sphere(radius=0.03, center=(x, y, z))
plotter.add_mesh(point_sphere, color=input_rgb, specular=1.0, smooth_shading=True)

plotter.show()

				
			

wheel with several of my vallejo colros:

				
					import pyvista as pv
import numpy as np
import matplotlib.colors as mcolors

# Brightness levels (disks)
brightness_levels = [1.0, 0.8, 0.6, 0.4, 0.2, 0.0]
spacing = 5  # vertical spacing between disks

n_radius = 100
n_theta = 360

def create_disk(brightness):
    r = np.linspace(0, 1, n_radius)
    theta = np.linspace(0, 2 * np.pi, n_theta)
    r_grid, theta_grid = np.meshgrid(r, theta)
    x = (r_grid * np.cos(theta_grid)).flatten()
    y = (r_grid * np.sin(theta_grid)).flatten()
    z = np.full_like(x, brightness * spacing)
    points = np.column_stack((x, y, z))
    disk = pv.PolyData(points)

    cells = []
    for i in range(n_theta - 1):
        for j in range(n_radius - 1):
            p0 = i * n_radius + j
            p1 = p0 + 1
            p2 = p0 + n_radius + 1
            p3 = p0 + n_radius
            cells.append([4, p0, p1, p2, p3])
    disk = disk.cast_to_unstructured_grid()
    disk.cells = np.array(cells, dtype=np.int64).flatten()

    hue = theta_grid.flatten() / (2 * np.pi)
    sat = r_grid.flatten()
    val = np.full_like(hue, brightness)
    hsv = np.stack([hue, sat, val], axis=1)
    rgb = mcolors.hsv_to_rgb(hsv)
    colors = (rgb * 255).astype(np.uint8)
    disk.point_data["Colors"] = colors
    return disk

# Create and add disks
plotter = pv.Plotter()
for b in brightness_levels:
    disk = create_disk(b)
    plotter.add_mesh(disk, scalars="Colors", rgb=True, opacity=0.8)

# List of hex colors
hex_colors = [
    "#A54336",  # warm red
    "#DC5E35",  # orange-red
    "#F8C85E",  # warm yellow
    "#F8DB5D",  # pale yellow
    "#DDD1A4",  # tan
    "#BBB992",  # olive beige
    "#9A835F",  # muted tan
    "#99624B",  # burnt sienna
    "#805C3F",  # brown
    "#7F6355",  # dusty brown
    "#544231",  # earthy brown
    "#3B2C29",  # dark brown
    "#232A2C",  # almost black
    "#3C3431",  # charcoal
    "#402C33",  # dark plum-brown
    "#372743",  # violet-black
    "#5A8B78",  # muted teal-green
    "#32665A",  # cool teal
    "#324A32",  # forest green
    "#7C7B66",  # army green/grey
    "#757C70",  # moss grey
    "#9AA0A0",  # pale grey-blue
    "#DDDED7",  # off-white grey
    "#FDF8F3",  # ivory/cream
    "#3B4D77",  # deep denim blue
    "#3A4372",  # twilight blue
    "#3A2F4B",  # midnight violet
    "#252527"   # black
]

for hex_color in hex_colors:
    # Convert hex to RGB
    rgb = np.array(mcolors.to_rgb(hex_color))

    # Convert to HSV
    hsv = mcolors.rgb_to_hsv(rgb.reshape(1, -1))[0]
    hue, sat, val = hsv

    # Find closest brightness ring
    closest_brightness = min(brightness_levels, key=lambda b: abs(b - val))

    # Polar to Cartesian
    theta = hue * 2 * np.pi
    radius = sat
    x = radius * np.cos(theta)
    y = radius * np.sin(theta)
    z = closest_brightness * spacing

    # Add sphere at location
    sphere = pv.Sphere(radius=0.05, center=(x, y, z))
    plotter.add_mesh(sphere, color=rgb, specular=1.0, smooth_shading=True)

plotter.set_background("white")
plotter.show()

				
			

3D xyz graph wwith all colors

 

				
					import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import colorsys
from mpl_toolkits.mplot3d import Axes3D

hex_colors = [
    "#A54336", "#DC5E35", "#F8C85E", "#F8DB5D", "#DDD1A4", "#BBB992", "#9A835F",
    "#99624B", "#805C3F", "#7F6355", "#544231", "#3B2C29", "#232A2C", "#3C3431",
    "#402C33", "#372743", "#5A8B78", "#32665A", "#324A32", "#7C7B66", "#757C70",
    "#9AA0A0", "#DDDED7", "#FDF8F3", "#3B4D77", "#3A4372", "#3A2F4B", "#252527"
]

hsl_data = [colorsys.rgb_to_hls(*mcolors.hex2color(c)) for c in hex_colors]
hue = [h for h, l, s in hsl_data]
saturation = [s for h, l, s in hsl_data]
lightness = [l for h, l, s in hsl_data]

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')

for h, s, l, color in zip(hue, saturation, lightness, hex_colors):
    ax.scatter(h, s, l, color=color, s=150)
    ax.plot([h, h], [s, s], [0, l], color=color, linewidth=1)

# Larger fonts for title and labels
ax.set_title("3D Color Distribution with Lightness Lines", fontsize=18)
ax.set_xlabel("Hue", fontsize=14)
ax.set_ylabel("Saturation", fontsize=14)
ax.set_zlabel("Brightness", fontsize=14, labelpad=-30)

ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_zlim(0, 1)

# Increase tick label font size on all 3 axes
for axis in [ax.xaxis, ax.yaxis, ax.zaxis]:
    for tick in axis.get_ticklabels():
        tick.set_fontsize(14)

plt.tight_layout()
plt.show()

				
			

under here is the 2D map square

				
					# -*- coding: utf-8 -*-
"""
Created on Wed Jun 11 23:12:23 2025

@author: flori
"""

import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import colorsys

# Provided hex color list
hex_colors = [
    "#A54336", "#DC5E35", "#F8C85E", "#F8DB5D", "#DDD1A4", "#BBB992", "#9A835F",
    "#99624B", "#805C3F", "#7F6355", "#544231", "#3B2C29", "#232A2C", "#3C3431",
    "#402C33", "#372743", "#5A8B78", "#32665A", "#324A32", "#7C7B66", "#757C70",
    "#9AA0A0", "#DDDED7", "#FDF8F3", "#3B4D77", "#3A4372", "#3A2F4B", "#252527"
]

# Convert hex colors to HLS and collect hue and saturation
points = []
for hex_color in hex_colors:
    rgb = mcolors.hex2color(hex_color)
    h, l, s = colorsys.rgb_to_hls(*rgb)
    points.append((h, s, hex_color))

# Create scatter plot
plt.figure(figsize=(8, 6))
for h, s, color in points:
    plt.scatter(h, s, color=color, edgecolor='black', s=100)

plt.title("Color Distribution by Hue (X) and Saturation (Y)")
plt.xlabel("Hue")
plt.ylabel("Saturation")
plt.grid(True)
plt.xlim(0, 1)
plt.ylim(0, 1)
plt.tight_layout()
plt.show()

				
			

Analysis done here:

				
					import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import colorsys
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from sklearn.cluster import KMeans

# Step 0: Your list of hex colors
hex_colors = [
    "#A54336", "#DC5E35", "#F8C85E", "#F8DB5D", "#DDD1A4", "#BBB992", "#9A835F",
    "#99624B", "#805C3F", "#7F6355", "#544231", "#3B2C29", "#232A2C", "#3C3431",
    "#402C33", "#372743", "#5A8B78", "#32665A", "#324A32", "#7C7B66", "#757C70",
    "#9AA0A0", "#DDDED7", "#FDF8F3", "#3B4D77", "#3A4372", "#3A2F4B", "#252527"
]

# Step 1: Convert hex colors to HSL
hsl_data = [colorsys.rgb_to_hls(*mcolors.hex2color(c)) for c in hex_colors]
hsl_array = np.array(hsl_data)  # shape: (N_colors, 3)
hue = hsl_array[:, 0]
lightness = hsl_array[:, 1]
saturation = hsl_array[:, 2]

# Step 2: Basic statistics
mean_hsl = np.mean(hsl_array, axis=0)
std_hsl = np.std(hsl_array, axis=0)

print("=== Basic Statistics ===")
print("Mean HSL:", mean_hsl)
print("Std HSL: ", std_hsl)
print()

# Step 3: Histograms of Hue, Saturation, Lightness
fig, axs = plt.subplots(1, 3, figsize=(12, 4))
labels = ["Hue", "Lightness", "Saturation"]
data = [hue, lightness, saturation]
for i in range(3):
    axs[i].hist(data[i], bins=10, color='gray', edgecolor='black')
    axs[i].set_title(f"{labels[i]} Distribution")
    axs[i].set_xlim(0, 1)
plt.suptitle("Histograms of HSL Components")
plt.tight_layout()
plt.show()

from sklearn.cluster import KMeans
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# Step 4: KMeans clustering with visualization
hsl_array = np.array(hsl_data)

# Number of clusters
n_clusters = 4
kmeans = KMeans(n_clusters=n_clusters, n_init="auto", random_state=0)
labels = kmeans.fit_predict(hsl_array)
centers = kmeans.cluster_centers_

# Convert HSL centers to RGB and then to HEX
import colorsys
import matplotlib.colors as mcolors

center_hex_colors = []
for h, l, s in centers:
    r, g, b = colorsys.hls_to_rgb(h, l, s)
    center_hex_colors.append(mcolors.to_hex((r, g, b)))


# Create a colormap
cluster_colors = plt.cm.get_cmap("tab10", n_clusters)

# 3D plot
fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')

for i in range(n_clusters):
    cluster_points = hsl_array[labels == i]
    ax.scatter(
        cluster_points[:, 0],  # Hue
        cluster_points[:, 2],  # Saturation
        cluster_points[:, 1],  # Lightness
        color=cluster_colors(i),
        label=f'Cluster {i}',
        s=80
    )

# Plot the cluster centers using their actual colors
for (h, l, s), hex_color in zip(centers, center_hex_colors):
    ax.scatter(h, s, l, color=hex_color, edgecolor='black', marker='o', s=200, linewidth=1.5)


# Axes and labels
ax.set_xlabel("Hue")
ax.set_ylabel("Saturation")
ax.set_zlabel("Lightness")
ax.set_title("KMeans Clustering in HSL Space")
ax.legend()

plt.tight_layout()
plt.show()


# Step 5: Find underrepresented regions using binning
bins = (5, 5, 5)  # (H, S, L) grid resolution
hist, edges = np.histogramdd(hsl_array, bins=bins, range=[[0, 1], [0, 1], [0, 1]])

print("=== Underrepresented Regions ===")
underrepresented = np.argwhere(hist == 0)
for idx in underrepresented:
    h_bin = f"{edges[0][idx[0]]:.2f}-{edges[0][idx[0]+1]:.2f}"
    s_bin = f"{edges[1][idx[1]]:.2f}-{edges[1][idx[1]+1]:.2f}"
    l_bin = f"{edges[2][idx[2]]:.2f}-{edges[2][idx[2]+1]:.2f}"
    print(f"No colors in bin: Hue={h_bin}, Saturation={s_bin}, Lightness={l_bin}")


				
			

interactive map under here

				
					import plotly.graph_objects as go
import matplotlib.colors as mcolors
import colorsys
import numpy as np

# Your list of hex colors
hex_colors = [
    "#A54336", "#DC5E35", "#F8C85E", "#F8DB5D", "#DDD1A4", "#BBB992", "#9A835F",
    "#99624B", "#805C3F", "#7F6355", "#544231", "#3B2C29", "#232A2C", "#3C3431",
    "#402C33", "#372743", "#5A8B78", "#32665A", "#324A32", "#7C7B66", "#757C70",
    "#9AA0A0", "#DDDED7", "#FDF8F3", "#3B4D77", "#3A4372", "#3A2F4B", "#252527"
]

# Convert hex to HSL
hsl_data = [colorsys.rgb_to_hls(*mcolors.hex2color(c)) for c in hex_colors]
hue = np.array([h for h, l, s in hsl_data])
lightness = np.array([l for h, l, s in hsl_data])
saturation = np.array([s for h, l, s in hsl_data])

# Format tooltips
hover_texts = [
    f"Hex: {hex_colors[i]}<br>Hue: {hue[i]:.2f}<br>Saturation: {saturation[i]:.2f}<br>Lightness: {lightness[i]:.2f}"
    for i in range(len(hex_colors))
]

# Create 3D scatter plot
fig = go.Figure(data=[go.Scatter3d(
    x=hue,
    y=saturation,
    z=lightness,
    mode='markers',
    marker=dict(
        size=6,
        color=hex_colors,  # Actual color
        opacity=0.9,
        line=dict(width=0.5, color='black')
    ),
    text=hover_texts,
    hoverinfo='text'
)])

# Update layout
fig.update_layout(
    title="Interactive 3D Color Distribution (HSL)",
    scene=dict(
        xaxis=dict(title='Hue'),
        yaxis=dict(title='Saturation'),
        zaxis=dict(title='Lightness'),
    ),
    margin=dict(l=0, r=0, b=0, t=40),
)

# Show
fig.show(renderer="browser")

fig.write_html("color_plot.html", include_plotlyjs='cdn')

				
			

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Visual Portfolio, Posts & Image Gallery for WordPress