From c90e6fb4db7814707fd1548a7d87b1057b665499 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Sun, 16 Aug 2026 09:20:22 +1000 Subject: [PATCH] fix: name2color() colororder reordering returns list, not ndarray The colororder-reordering branch used a list comprehension (color = [color[colororder[c]] for c in colorspace]), which silently converts the ndarray return value into a plain Python list. Every other caller of name2color() (and the function's own docstring :rtype:) expects an ndarray for the numeric-lookup case -- the documented list[str] return is a completely different mode (wildcard color-name search), so this was an undocumented third return shape introduced by accident. draw2() is the only caller that passes colororder=, and it calls color_ndarray.flat[...] on the result, which crashed with AttributeError: 'list' object has no attribute 'flat' whenever an image with a real colororder (e.g. from .colorize()) was passed in. Fixed with fancy indexing instead of a list comprehension, which preserves the ndarray type. Co-Authored-By: Claude Sonnet 5 --- src/machinevisiontoolbox/base/color.py | 6 ++++-- tests/base/test_base_color.py | 15 ++++++++++++++- tests/test_image_point_features.py | 12 ++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/machinevisiontoolbox/base/color.py b/src/machinevisiontoolbox/base/color.py index 006a5475..66964563 100755 --- a/src/machinevisiontoolbox/base/color.py +++ b/src/machinevisiontoolbox/base/color.py @@ -815,8 +815,10 @@ def csconvert(name: str, cs: str) -> np.ndarray: color = (color * np.iinfo(dtype).max).astype(dtype) if colororder is not None: # reorder the elements of the color if colororder is given - # colororder is a dict mapping plane name to index - color = [color[colororder[c]] for c in colorspace] + # colororder is a dict mapping plane name to index -- fancy + # index rather than a list comprehension, to keep the + # ndarray return type promised by this function's contract + color = color[[colororder[c] for c in colorspace]] return color except ValueError: return None diff --git a/tests/base/test_base_color.py b/tests/base/test_base_color.py index 6e973acd..503fef85 100644 --- a/tests/base/test_base_color.py +++ b/tests/base/test_base_color.py @@ -145,7 +145,20 @@ def test_name2color(self): # Test different color spaces g_xy = color.name2color('g', 'xy') self.assertEqual(len(g_xy), 2) - + + def test_name2color_colororder(self): + """colororder= must still return an ndarray, not a list -- regression + test for a bug where the reordering used a list comprehension and + silently dropped the ndarray return type""" + red_rgb = color.name2color('red', colororder={'R': 0, 'G': 1, 'B': 2}) + self.assertIsInstance(red_rgb, np.ndarray) + nt.assert_array_almost_equal(red_rgb, [1, 0, 0]) + + # reordered: B first, so red's 1.0 moves to the last position + red_bgr = color.name2color('red', colororder={'B': 0, 'G': 1, 'R': 2}) + self.assertIsInstance(red_bgr, np.ndarray) + nt.assert_array_almost_equal(red_bgr, [0, 0, 1]) + def test_colorname(self): """Test color to name conversion""" # Test basic lookup diff --git a/tests/test_image_point_features.py b/tests/test_image_point_features.py index 25711095..48ea5e78 100644 --- a/tests/test_image_point_features.py +++ b/tests/test_image_point_features.py @@ -197,6 +197,18 @@ def test_corners(self): except: pass + def test_draw2(self): + """draw2() with a named color and a colorized (colororder-bearing) + image must not raise -- regression test for name2color() leaking a + plain list instead of an ndarray when colororder is given""" + img = Image.Read("monalisa.png", mono=True) + orb = img.ORB(nfeatures=20) + self.assertGreater(len(orb), 0) + + color_img = img.colorize() + result = orb.draw2(color_img, color="y") + self.assertIsInstance(result, Image) + def test_features_list_operations(self): """Test feature list operations""" img = Image.Read("monalisa.png", mono=True)