Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@ requires-python = ">=3.8"
dependencies = [
"requests",
"pandas",
"jsonpath_rw",
"pysolr",
"navis",
"numpy>=1.26.4",
"seaborn>0.13",
"colormath",
"tqdm",
]

Expand Down
2 changes: 0 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ python-catmaid
nbformat>=4.2.0
ipywidgets
vispy
colormath
tqdm
sphinx-autodoc-typehints
numpy>=1.26.4
jsonpath_rw
bottleneck<=1.3.6
matplotlib>3.9
seaborn>0.13
Expand Down
60 changes: 60 additions & 0 deletions src/vfb_connect/color_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Minimal CIE Lab -> sRGB conversion.

Vendored replacement for the abandoned ``colormath`` dependency (see issue
#287). Reproduces colormath's ``convert_color(LabColor(l, a, b), sRGBColor)``
pipeline exactly: Lab (D50, 2 degree observer) -> XYZ (D50) -> Bradford
chromatic adaptation to D65 -> linear sRGB -> gamma encoding, with the result
clamped to [0, 1].
"""

import numpy as np

_WHITE_D50 = np.array([0.96422, 1.00000, 0.82521])
_WHITE_D65 = np.array([0.95047, 1.00000, 1.08883])
_BRADFORD = np.array([[0.8951, 0.2664, -0.1614],
[-0.7502, 1.7135, 0.0367],
[0.0389, -0.0685, 1.0296]])
_XYZ_TO_SRGB = np.array([[3.24071, -1.53726, -0.498571],
[-0.969258, 1.87599, 0.0415557],
[0.0556352, -0.203996, 1.05707]])


def _adaptation_matrix():
source = _BRADFORD @ _WHITE_D50
destination = _BRADFORD @ _WHITE_D65
scale = np.diag(destination / source)
return np.linalg.inv(_BRADFORD) @ scale @ _BRADFORD


_ADAPT = _adaptation_matrix()

# CIE constants (actual values, as used by colormath).
_EPSILON = 216.0 / 24389.0
_KAPPA = 24389.0 / 27.0


def lab_to_srgb_clamped(l, a, b):
"""Convert a CIE Lab (D50) colour to clamped sRGB floats.

:param l: Lightness, 0-100.
:param a: Green-red axis.
:param b: Blue-yellow axis.
:return: ``numpy.ndarray`` of (r, g, b) floats clamped to [0, 1].
"""
fy = (l + 16.0) / 116.0
fx = fy + a / 500.0
fz = fy - b / 200.0

def _f_inv(t):
t3 = t ** 3
return t3 if t3 > _EPSILON else (116.0 * t - 16.0) / _KAPPA

xyz = np.array([_f_inv(fx), _f_inv(fy), _f_inv(fz)]) * _WHITE_D50
xyz = _ADAPT @ xyz
rgb_linear = _XYZ_TO_SRGB @ xyz

def _gamma(c):
return 12.92 * c if c <= 0.0031308 else 1.055 * (c ** (1.0 / 2.4)) - 0.055

rgb = np.array([_gamma(c) for c in rgb_linear])
return np.clip(rgb, 0.0, 1.0)
12 changes: 5 additions & 7 deletions src/vfb_connect/cross_server_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
SIMILAR_NEURONS_COLUMNS, CONNECTED_NEURONS_BY_TYPE_COLUMNS)
import pandas as pd
import numpy as np
from colormath.color_objects import LabColor, sRGBColor
from colormath.color_conversions import convert_color
from .color_conversion import lab_to_srgb_clamped
from scipy.spatial import KDTree

VFB_DBS_2_SYMBOLS = {"JRC_OpticLobe":"neuprint_JRC_OpticLobe_v1_0_1", "FAFB":"catmaid_fafb", "L1EM":"catmaid_l1em", "MANC":"neuprint_JRC_Manc_1_2_1",
Expand Down Expand Up @@ -1876,11 +1875,10 @@ def generate_lab_colors(self, num_colors, min_distance=100, verbose=False):

# Convert Lab to RGB
for lab in selected_lab_colors:
lab_color = LabColor(lab[0], lab[1], lab[2])
rgb_color = convert_color(lab_color, sRGBColor)
rgb_tuple = (int(round(rgb_color.clamped_rgb_r * 255)),
int(round(rgb_color.clamped_rgb_g * 255)),
int(round(rgb_color.clamped_rgb_b * 255)))
rgb = lab_to_srgb_clamped(lab[0], lab[1], lab[2])
rgb_tuple = (int(round(rgb[0] * 255)),
int(round(rgb[1] * 255)),
int(round(rgb[2] * 255)))
rgb_colors.append(rgb_tuple)

if verbose:
Expand Down
1 change: 0 additions & 1 deletion src/vfb_connect/neo/query_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@



# from jsonpath_rw import parse as parse_jpath
from vfb_connect.neo.neo4j_tools import chunks, Neo4jConnect, dict_cursor, escape_string

# Connect to the VFB SOLR server
Expand Down
45 changes: 45 additions & 0 deletions src/vfb_connect/test/color_conversion_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Offline tests for the vendored Lab -> sRGB conversion (issue #287).

Reference values were generated with colormath 3.0.0's
``convert_color(LabColor(l, a, b), sRGBColor)`` (clamped channels), which this
module replaces.
"""

import unittest

import numpy as np

from vfb_connect.color_conversion import lab_to_srgb_clamped


class LabToSrgbTest(unittest.TestCase):

# ((L, a, b), (r, g, b)) pairs from colormath 3.0.0
REFERENCE = [
((0, 0, 0), (0.000000, 0.000000, 0.000000)),
((100, 0, 0), (1.000000, 0.999994, 0.999935)),
((50, 0, 0), (0.466344, 0.466324, 0.466295)),
((53.24, 80.09, 67.2), (0.982320, 0.000000, 0.025792)),
((87.74, -86.18, 83.18), (0.000000, 1.000000, 0.000000)),
((32.3, 79.19, -107.86), (0.355268, 0.000000, 1.000000)),
((50, -30, 40), (0.313814, 0.516759, 0.172645)),
((75, 20, -50), (0.711677, 0.684838, 1.000000)),
((20, 60, 10), (0.467718, 0.000000, 0.147970)),
]

def test_matches_colormath_reference(self):
for lab, expected in self.REFERENCE:
got = lab_to_srgb_clamped(*lab)
np.testing.assert_allclose(got, expected, atol=5e-6,
err_msg=f"Lab {lab}")

def test_output_clamped(self):
for l in np.linspace(0, 100, 5):
for a in np.linspace(-110, 110, 5):
for b in np.linspace(-110, 110, 5):
rgb = lab_to_srgb_clamped(l, a, b)
self.assertTrue(np.all(rgb >= 0.0) and np.all(rgb <= 1.0))


if __name__ == "__main__":
unittest.main()
Loading