Repository: scikit-image/scikit-image

Issue:
`measure.regionprops_table` with `properties=['centroid_weighted']` fails on images with >1 color channel.
### Description:

Calling `skimage.measure.regionprops_table` with `properties=['centroid_weighted']` on an image with >1 color channel raises `ValueError: setting an array element with a sequence.` I expected to get a dictionary with keys `centroid_weighted-n-c` where `n` are the indices of the spatial dimensions and `c` are the indices of the color channels.

### Way to reproduce:

```python
import numpy as np
import skimage

# Create 3D label image with a sphere
label_img = np.zeros((9, 9, 9), dtype='uint8')
label_img[:, :, :] = skimage.morphology.ball(radius=4)
n_pixels = label_img.sum()

# Create random number generator
rng = np.random.default_rng(seed=123)

# Create intensity image of sphere filled with random values
intensity_img = np.zeros((9, 9, 9, 3))
intensity_img[:, :, :, 0][label_img == 1] = rng.uniform(low=10, high=20, size=n_pixels)
intensity_img[:, :, :, 1][label_img == 1] = rng.uniform(low=10, high=20, size=n_pixels)
intensity_img[:, :, :, 2][label_img == 1] = rng.uniform(low=10, high=20, size=n_pixels)

# Measure weighted centroid for 1 color channel - this works
skimage.measure.regionprops_table(label_img, intensity_img[:, :, :, 0:1], properties=['centroid_weighted'])
# output: {'centroid_weighted-0': array([4.0115177]), 'centroid_weighted-1': array([3.98670859]), 'centroid_weighted-2': array([3.99605087])}

# Measure weighted centroid for 2 color channels - this DOESN'T work
skimage.measure.regionprops_table(label_img, intensity_img[:, :, :, 0:2], properties=['centroid_weighted'])

# Measure weighted centroid for 3 color channels - this DOESN'T work
skimage.measure.regionprops_table(label_img, intensity_img[:, :, :, 0:3], properties=['centroid_weighted'])
```

The `ValueError` with traceback is below. The error is the same whether 2 or 3 color intensity image is used.
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
TypeError: only size-1 arrays can be converted to Python scalars

The above exception was the direct cause of the following exception:

ValueError                                Traceback (most recent call last)
Cell In[86], line 26
     16 intensity_img[:, :, :, 2][label_img == 1] = rng.uniform(low=10, high=20, size=n_pixels)
     18 # Measure weighted centroid for 1 color channel - this works
     19 # skimage.measure.regionprops_table(label_img, intensity_img[:, :, :, 0:1], properties=['centroid_weighted'])
     20 # output: {'centroid_weighted-0': array([4.0115177]), 'centroid_weighted-1': array([3.98670859]), 'centroid_weighted-2': array([3.99605087])}
   (...)
     24 
     25 # Measure weighted centroid for 3 color channels - this DOESN'T work
---> 26 skimage.measure.regionprops_table(label_img, intensity_img[:, :, :, 0:3], properties=['centroid_weighted'])

File /Applications/miniconda3/envs/idpimg/lib/python3.10/site-packages/skimage/measure/_regionprops.py:1045, in regionprops_table(label_image, intensity_image, properties, cache, separator, extra_properties, spacing)
   1041     out_d = _props_to_dict(regions, properties=properties,
   1042                            separator=separator)
   1043     return {k: v[:0] for k, v in out_d.items()}
-> 1045 return _props_to_dict(
   1046     regions, properties=properties, separator=separator
   1047 )

File /Applications/miniconda3/envs/idpimg/lib/python3.10/site-packages/skimage/measure/_regionprops.py:877, in _props_to_dict(regions, properties, separator)
    875     rp = regions[k][prop]
    876     for i, loc in enumerate(locs):
--> 877         column_data[k, i] = rp[loc]
    879 # add the columns to the output dictionary
    880 for i, modified_prop in enumerate(modified_props):

ValueError: setting an array element with a sequence.
```

### Version information:

```Shell
3.10.8 (main, Nov 11 2022, 08:11:25) [Clang 12.0.0 ]
macOS-10.16-x86_64-i386-64bit
scikit-image version: 0.20.0
numpy version: 1.22.3
```



Discussion:
Thanks for the report, @aslyon! The issue is here:

https://github.com/scikit-image/scikit-image/blob/c7fb4819d135c7b63eb13504a8815c4761801590/skimage/measure/_regionprops.py#L857-L860

It turns out that the output of `weighted_moments` is an array-like, a tuple of arrays, rather than an array, so instead of inferring a 2D array here, we infer a linear set of numbers. This just *happens* to work when the arrays contain one element, but fails when there's more, as you have observed. Here's how I modified your example to show these outputs:

```python
import numpy as np
import skimage

# Create 3D label image with a sphere
label_img = np.zeros((9, 9, 9), dtype='uint8')
label_img[:, :, :] = skimage.morphology.ball(radius=4)
n_pixels = label_img.sum()

# Create random number generator
rng = np.random.default_rng(seed=123)

# Create intensity image of sphere filled with random values
intensity_img = np.zeros((9, 9, 9, 3))
intensity_img[:, :, :, 0][label_img == 1] = rng.uniform(low=10, high=20, size=n_pixels)
intensity_img[:, :, :, 1][label_img == 1] = rng.uniform(low=10, high=20, size=n_pixels)
intensity_img[:, :, :, 2][label_img == 1] = rng.uniform(low=10, high=20, size=n_pixels)

# Measure props for no channel axis:
props0 = skimage.measure.regionprops(label_img, intensity_img[..., 0])[0]
print(f'no channel:\n {props0.centroid_weighted}')

# Measure props for single channel
props1 = skimage.measure.regionprops(label_img, intensity_img[..., 0:1])[0]
print(f'one channel:\n {props1.centroid_weighted}')

# Measure props for multiple channels
propsm = skimage.measure.regionprops(label_img, intensity_img)[0]
print(f'multichannel:\n {propsm.centroid_weighted}')
```

which gives:

```
no channel:
 (4.011517704412646, 3.9867085865741485, 3.996050865015718)
one channel:
 (array([4.0115177]), array([3.98670859]), array([3.99605087]))
multichannel:
 (array([4.0115177 , 4.03724327, 3.98084178]), array([3.98670859, 4.01261718, 3.96234087]), array([3.99605087, 4.0387654 , 3.99636875]))
```

So, it looks like we need a better way of identifying the type and shape of the output of each property when turning it into a table. My first instinct is to convert any non-scalar, non-object property to an array. I might give that a go.

Resolving patch:
diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py
--- a/skimage/measure/_regionprops.py
+++ b/skimage/measure/_regionprops.py
@@ -854,15 +854,10 @@ def _props_to_dict(regions, properties=('label', 'bbox'), separator='-'):
                 column_buffer[i] = regions[i][prop]
             out[orig_prop] = np.copy(column_buffer)
         else:
-            if isinstance(rp, np.ndarray):
-                shape = rp.shape
-            else:
-                shape = (len(rp),)
-
             # precompute property column names and locations
             modified_props = []
             locs = []
-            for ind in np.ndindex(shape):
+            for ind in np.ndindex(np.shape(rp)):
                 modified_props.append(
                     separator.join(map(str, (orig_prop,) + ind))
                 )
@@ -872,7 +867,9 @@ def _props_to_dict(regions, properties=('label', 'bbox'), separator='-'):
             n_columns = len(locs)
             column_data = np.empty((n, n_columns), dtype=dtype)
             for k in range(n):
-                rp = regions[k][prop]
+                # we coerce to a numpy array to ensure structures like
+                # tuple-of-arrays expand correctly into columns
+                rp = np.asarray(regions[k][prop])
                 for i, loc in enumerate(locs):
                     column_data[k, i] = rp[loc]
 


Test patch:
diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py
--- a/skimage/measure/tests/test_regionprops.py
+++ b/skimage/measure/tests/test_regionprops.py
@@ -33,6 +33,9 @@
 INTENSITY_SAMPLE = SAMPLE.copy()
 INTENSITY_SAMPLE[1, 9:11] = 2
 INTENSITY_FLOAT_SAMPLE = INTENSITY_SAMPLE.copy().astype(np.float64) / 10.0
+INTENSITY_FLOAT_SAMPLE_MULTICHANNEL = (
+        INTENSITY_FLOAT_SAMPLE[..., np.newaxis] * [1, 2, 3]
+        )
 
 SAMPLE_MULTIPLE = np.eye(10, dtype=np.int32)
 SAMPLE_MULTIPLE[3:5, 7:8] = 2
@@ -714,6 +717,27 @@ def test_solidity():
     assert_almost_equal(solidity, target_solidity)
 
 
+def test_multichannel_centroid_weighted_table():
+    """Test for https://github.com/scikit-image/scikit-image/issues/6860."""
+    intensity_image = INTENSITY_FLOAT_SAMPLE_MULTICHANNEL
+    rp0 = regionprops(SAMPLE, intensity_image=intensity_image[..., 0])[0]
+    rp1 = regionprops(SAMPLE, intensity_image=intensity_image[..., 0:1])[0]
+    rpm = regionprops(SAMPLE, intensity_image=intensity_image)[0]
+    np.testing.assert_almost_equal(rp0.centroid_weighted,
+                                   np.squeeze(rp1.centroid_weighted))
+    np.testing.assert_almost_equal(rp0.centroid_weighted,
+                                   np.array(rpm.centroid_weighted)[:, 0])
+    assert np.shape(rp0.centroid_weighted) == (SAMPLE.ndim,)
+    assert np.shape(rp1.centroid_weighted) == (SAMPLE.ndim, 1)
+    assert np.shape(rpm.centroid_weighted) == (SAMPLE.ndim,
+                                               intensity_image.shape[-1])
+
+    table = regionprops_table(SAMPLE, intensity_image=intensity_image,
+                              properties=('centroid_weighted',))
+    # check the number of returned columns is correct
+    assert len(table) == np.size(rpm.centroid_weighted)
+
+
 def test_moments_weighted_central():
     wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE
                       )[0].moments_weighted_central