| Previous changeset 5:4a49f74a3c14 (2025-05-12) |
|
Commit message:
planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/points2labelimage/ commit edac062b00490276ef00d094e0594abdb3a3f23c |
|
modified:
creators.xml points2label.py points2label.xml test-data/output2.tiff test-data/output4.tiff test-data/output5.tiff test-data/output6.tiff test-data/rois-illegal2.geojson test-data/rois-noname.geojson test-data/rois.geojson |
|
added:
REAME.md test-data/input5.tsv test-data/input6.tsv test-data/output7.tiff test-data/output8.tiff test_utils.py |
|
removed:
test-data/rois-illegal1.geojson |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 REAME.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/REAME.md Thu Nov 06 09:59:34 2025 +0000 |
| b |
| @@ -0,0 +1,5 @@ +# points2labelimage + +Unit-testable functions are implemented in utils.py. + +Run `python -m unittest` for the unit tests (for regression testing). |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 creators.xml --- a/creators.xml Mon May 12 14:01:26 2025 +0000 +++ b/creators.xml Thu Nov 06 09:59:34 2025 +0000 |
| b |
| @@ -30,4 +30,9 @@ <yield/> </xml> + <xml name="creators/tuncK"> + <person givenName="Tunc" familyName="Kayikcioglu"/> + <yield/> + </xml> + </macros> |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 points2label.py --- a/points2label.py Mon May 12 14:01:26 2025 +0000 +++ b/points2label.py Thu Nov 06 09:59:34 2025 +0000 |
| [ |
| b'@@ -1,12 +1,11 @@\n import argparse\n import json\n-import os\n import warnings\n from typing import (\n+ Any,\n Dict,\n- List,\n+ Optional,\n Tuple,\n- Union,\n )\n \n import giatools.pandas\n@@ -14,219 +13,276 @@\n import numpy.typing as npt\n import pandas as pd\n import scipy.ndimage as ndi\n+import skimage.draw\n import skimage.io\n import skimage.segmentation\n \n \n-def is_rectangular(points: Union[List[Tuple[float, float]], npt.NDArray]) -> bool:\n- points = np.asarray(points)\n-\n- # Rectangle must have 5 points, where first and last are identical\n- if len(points) != 5 or not (points[0] == points[-1]).all():\n- return False\n-\n- # Check that all edges align with the axes\n- edges = points[1:] - points[:-1]\n- if any((edge == 0).sum() != 1 for edge in edges):\n- return False\n-\n- # All checks have passed, the geometry is rectangular\n- return True\n+def get_list_depth(nested_list: Any) -> int:\n+ if isinstance(nested_list, list):\n+ if len(nested_list) > 0:\n+ return 1 + max(map(get_list_depth, nested_list))\n+ else:\n+ return 1\n+ else:\n+ return 0\n \n \n-def geojson_to_tabular(geojson: Dict):\n- rows = []\n- labels = []\n- for feature in geojson[\'features\']:\n- assert feature[\'geometry\'][\'type\'].lower() == \'polygon\', (\n- f\'Unsupported geometry type: "{feature["geometry"]["type"]}"\'\n- )\n- coords = feature[\'geometry\'][\'coordinates\'][0]\n+class AutoLabel:\n+ """\n+ Creates a sequence of unique labels (non-negative values).\n+ """\n \n- # Properties and name (label) are optional\n- try:\n- label = feature[\'properties\'][\'name\']\n- except KeyError:\n- label = max(labels, default=0) + 1\n- labels.append(label)\n+ def __init__(self, reserved_labels):\n+ self.reserved_labels = reserved_labels\n+ self.next_autolabel = 0\n \n- # Read geometry\n- xs = [pt[0] for pt in coords]\n- ys = [pt[1] for pt in coords]\n+ def next(self):\n+ """\n+ Retrieve the next auto-label (post-increment).\n+ """\n+ # Fast-forward `next_autolabel` to the first free label\n+ while self.next_autolabel in self.reserved_labels:\n+ self.next_autolabel += 1\n \n- x = min(xs)\n- y = min(ys)\n+ # Return the free label, then advance `next_autolabel`\n+ try:\n+ return self.next_autolabel\n+ finally:\n+ self.next_autolabel += 1\n \n- width = max(xs) + 1 - x\n- height = max(ys) + 1 - y\n-\n- # Validate geometry (must be rectangular)\n- assert is_rectangular(list(zip(xs, ys)))\n \n- # Append the rectangle\n- rows.append({\n- \'pos_x\': x,\n- \'pos_y\': y,\n- \'width\': width,\n- \'height\': height,\n- \'label\': label,\n- })\n- df = pd.DataFrame(rows)\n- point_file = \'./point_file.tabular\'\n- df.to_csv(point_file, sep=\'\\t\', index=False)\n- return point_file\n+def get_feature_label(feature: Dict) -> Optional[int]:\n+ """\n+ Get the label of a GeoJSON feature, or `None` if there is no proper label.\n+ """\n+ label = feature.get(\'properties\', {}).get(\'name\', None)\n+ if label is None:\n+ return None\n+\n+ # If the `label` is given as a string, try to parse as integer\n+ if isinstance(label, str):\n+ try:\n+ label = int(label)\n+ except ValueError:\n+ pass\n+\n+ # Finally, if `label` is an integer, only use it if it is non-negative\n+ if isinstance(label, int) and label >= 0:\n+ return label\n+ else:\n+ return None\n \n \n-def rasterize(point_file, out_file, shape, has_header=False, swap_xy=False, bg_value=0, fg_value=None):\n+def rasterize(\n+ geojson: Dict,\n+ shape: Tuple[int, int],\n+ bg_value: int = 0,\n+ fg_value: Optional[int] = None,\n+) -> npt.NDArray:\n+ """\n+ Rasterize GeoJSON into a pixel image, that is returned as a NumPy array.\n+ '..b'atershed(dist, img, mask=foreground)\n+ # Create a GeoJSON feature\n+ feature = {\n+ \'type\': \'Feature\',\n+ \'geometry\': {\n+ \'type\': geom_type,\n+ \'coordinates\': coords,\n+ },\n+ \'properties\': {\n+ \'name\': label,\n+ },\n+ }\n+ if radius > 0:\n+ feature[\'properties\'][\'radius\'] = radius\n+ feature[\'properties\'][\'subType\'] = \'Circle\'\n+ features.append(feature)\n \n- # Rasterize point (there is no overlapping area to be distributed)\n- else:\n- img[y, x] = label\n-\n- else:\n- raise Exception(\'{} is empty or does not exist.\'.format(point_file)) # appropriate built-in error?\n-\n- with warnings.catch_warnings():\n- warnings.simplefilter("ignore")\n- skimage.io.imsave(out_file, img, plugin=\'tifffile\') # otherwise we get problems with the .dat extension\n+ # Return the GeoJSON object\n+ return geojson\n \n \n if __name__ == \'__main__\':\n parser = argparse.ArgumentParser()\n- parser.add_argument(\'in_file\', type=argparse.FileType(\'r\'), help=\'Input point file or GeoJSON file\')\n- parser.add_argument(\'out_file\', type=str, help=\'out file (TIFF)\')\n- parser.add_argument(\'shapex\', type=int, help=\'shapex\')\n- parser.add_argument(\'shapey\', type=int, help=\'shapey\')\n- parser.add_argument(\'--has_header\', dest=\'has_header\', default=False, help=\'set True if point file has header\')\n+ parser.add_argument(\'in_ext\', type=str, help=\'Input file format\')\n+ parser.add_argument(\'in_file\', type=str, help=\'Input file path (tabular or GeoJSON)\')\n+ parser.add_argument(\'out_file\', type=str, help=\'Output file path (TIFF)\')\n+ parser.add_argument(\'shapex\', type=int, help=\'Output image width\')\n+ parser.add_argument(\'shapey\', type=int, help=\'Output image height\')\n+ parser.add_argument(\'--has_header\', dest=\'has_header\', default=False, help=\'Set True if tabular file has a header\')\n parser.add_argument(\'--swap_xy\', dest=\'swap_xy\', default=False, help=\'Swap X and Y coordinates\')\n parser.add_argument(\'--binary\', dest=\'binary\', default=False, help=\'Produce binary image\')\n-\n args = parser.parse_args()\n \n- point_file = args.in_file.name\n- has_header = args.has_header\n+ # Validate command-line arguments\n+ assert args.in_ext in (\'tabular\', \'geojson\'), (\n+ f\'Unexpected input file format: {args.in_ext}\'\n+ )\n \n- try:\n- with open(args.in_file.name, \'r\') as f:\n- content = json.load(f)\n- if isinstance(content, dict) and content.get(\'type\') == \'FeatureCollection\' and isinstance(content.get(\'features\'), list):\n- point_file = geojson_to_tabular(content)\n- has_header = True # header included in the converted file\n- else:\n- raise ValueError(\'Input is a JSON file but not a valid GeoJSON file\')\n- except json.JSONDecodeError:\n- print(\'Input is not a valid JSON file. Assuming it a tabular file.\')\n+ # Load the GeoJSON data (if the input file is tabular, convert to GeoJSON)\n+ if args.in_ext == \'tabular\':\n+ geojson = convert_tabular_to_geojson(args.in_file, args.has_header)\n+ else:\n+ with open(args.in_file) as f:\n+ geojson = json.load(f)\n \n- rasterize(\n- point_file,\n- args.out_file,\n- (args.shapey, args.shapex),\n- has_header=has_header,\n- swap_xy=args.swap_xy,\n+ # Rasterize the image from GeoJSON\n+ shape = (args.shapey, args.shapex)\n+ img = rasterize(\n+ geojson,\n+ shape if not args.swap_xy else shape[::-1],\n fg_value=0xffff if args.binary else None,\n )\n+ if args.swap_xy:\n+ img = img.T\n+\n+ # Write the rasterized image as TIFF\n+ with warnings.catch_warnings():\n+ warnings.simplefilter(\'ignore\')\n+ skimage.io.imsave(args.out_file, img, plugin=\'tifffile\') # otherwise we get problems with the .dat extension\n' |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 points2label.xml --- a/points2label.xml Mon May 12 14:01:26 2025 +0000 +++ b/points2label.xml Thu Nov 06 09:59:34 2025 +0000 |
| [ |
| b'@@ -1,10 +1,9 @@\n-<tool id="ip_points_to_label" name="Convert coordinates to label map" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="20.05">\n+<tool id="ip_points_to_label" name="Convert coordinates to label map" version="@TOOL_VERSION@" profile="20.05">\n <description></description>\n <macros>\n <import>creators.xml</import>\n <import>tests.xml</import>\n- <token name="@TOOL_VERSION@">0.4.1</token>\n- <token name="@VERSION_SUFFIX@">1</token>\n+ <token name="@TOOL_VERSION@">0.5.0</token>\n </macros>\n <creator>\n <expand macro="creators/bmcv" />\n@@ -16,15 +15,16 @@\n <xref type="bio.tools">galaxy_image_analysis</xref>\n </xrefs>\n <requirements>\n- <requirement type="package" version="0.21">scikit-image</requirement>\n- <requirement type="package" version="1.26.4">numpy</requirement>\n- <requirement type="package" version="1.2.4">pandas</requirement>\n- <requirement type="package" version="2024.6.18">tifffile</requirement>\n+ <requirement type="package" version="0.25.2">scikit-image</requirement>\n+ <requirement type="package" version="2.3.4">numpy</requirement>\n+ <requirement type="package" version="2.3.3">pandas</requirement>\n+ <requirement type="package" version="2025.10.16">tifffile</requirement>\n <requirement type="package" version="0.3.1">giatools</requirement>\n </requirements>\n <command detect_errors="aggressive"><![CDATA[\n \n python \'$__tool_directory__/points2label.py\'\n+ \'$input.file_ext\'\n \'$input\'\n \'$output\'\n $shapex\n@@ -35,12 +35,12 @@\n \n ]]></command>\n <inputs>\n- <param name="input" type="data" format="tabular,geojson" label="List of points in tabular or geojson format"/>\n- <param name="shapex" type="integer" value="500" min="1" label="Width of output image" />\n- <param name="shapey" type="integer" value="500" min="1" label="Height of output image" />\n- <param name="has_header" type="boolean" checked="true" truevalue="--has_header True" falsevalue="" optional="true" label="Tabular list of points has header" help="Turning this off will ignore the first row and assume that the X and Y coordinates correspond to the first and second column, respectively. Ignored, if GeoJSON is used for input." />\n- <param name="swap_xy" type="boolean" checked="false" falsevalue="" truevalue="--swap_xy True" optional="true" label="Swap X and Y coordinates" help="Swap the X and Y coordinates, regardless of whether the tabular list has a header or not." />\n- <param name="binary" type="boolean" checked="false" truevalue="--binary True" falsevalue="" optional="true" label="Produce binary image" help="Use the same label for all points (65535)." />\n+ <param name="input" type="data" format="tabular,geojson" label="Shapes to be rasterized"/>\n+ <param name="shapex" type="integer" value="500" min="1" label="Width of the output image" />\n+ <param name="shapey" type="integer" value="500" min="1" label="Height of the output image" />\n+ <param name="has_header" type="boolean" checked="true" truevalue="--has_header True" falsevalue="" optional="true" label="Tabular list of shapes has header" help="Only used if the input is a tabular file (ignored for GeoJSON). Turning this off will interpret the tabular file as a list of points, where the X and Y coordinates correspond to the first and second column, respectively." />\n+ <param name="swap_xy" type="boolean" checked="false" falsevalue="" truevalue="--swap_xy True" optional="true" label="Swap X and Y coordinates" help="Swap the X and Y coordinates before rasterization. The width and height of the output image is not affected." />\n+ <param name="binary" type="boolean" checked="false" truevalue="--binary True" falsevalue="" optional="true" label="Produce binary image" help="Use the same label for all shapes (65535)." />\n </inputs>\n '..b'am name="shapey" value="300" />\n <param name="has_header" value="false" />\n <param name="swap_xy" value="false" />\n <param name="binary" value="false" />\n+ <assert_stderr>\n+ <has_text text=\'Unsupported geometry type: "LineString"\' />\n+ </assert_stderr>\n </test>\n </tests>\n <help>\n \n- **Converts a list of points to a label map by rasterizing the coordinates.**\n+ **Converts a list of shapes to a label map via rasterization.**\n \n- The created image is a single-channel image with 16 bits per pixel (unsigned integer). The points are\n- rasterized with unique labels, or the value 65535 (white) for binary image output. Pixels not corresponding to\n- any points in the tabular file are assigned the value 0 (black).\n+ The created image is a single-channel image with 16 bits per pixel (unsigned integers). The shapes are\n+ rasterized with unique labels, explicitly given labels (custom), or the value 65535 (white) for binary image\n+ output. Pixels not corresponding to any shapes are assigned the value 0 (black).\n \n- **Using a tabular input file:** The tabular list of points can either be header-less. In this case, the first\n- and second columns are expected to be the X and Y coordinates, respectively. Otherwise, if a header is present,\n- it is searched for the following column names:\n+ **Using a GeoJSON input file (recommended).** Only features (shape specifications) of `Polygon` and `Point`\n+ type are supported. In conjunction with the `radius` property, a `Point` type feature can also be used to\n+ represent circles. Custom labels can be encoded in the `name` property (must be numeric and integer), and\n+ different features are allowed to use the same labels.\n \n- - ``pos_x`` or ``POS_X``: This column corresponds to the X coordinates.\n- - ``pos_y`` or ``POS_Y``: This column corresponds to the Y coordinates.\n- - If a ``radius`` or ``RADIUS`` column is present, then the points will be rasterized as circles of the\n- corresponding radii.\n- - If ``width`` or ``WIDTH`` and ``height`` or ``HEIGHT`` columns are present, then the points will be rasterized\n- as rectangles of the corresponding size.\n+ **Using a tabular input file (deprecated).** The tabular list of points can either be header-less. In this\n+ case, the first and second columns are expected to be the X and Y coordinates, respectively, and each row\n+ corresponds to a single point. Otherwise, if a header is present, it is searched for the following column\n+ names:\n+\n+ - Column ``pos_x`` or ``POS_X`` is mandatory and corresponds to the X coordinates.\n+ - Column ``pos_y`` or ``POS_Y`` is mandatory and corresponds to the Y coordinates.\n+ - If a ``radius`` or ``RADIUS`` column is present and the value in a row is positive, then the row will be\n+ rasterized as a circle of the corresponding size, centered at the given X and Y coordinates.\n+ - If ``width`` or ``WIDTH`` and ``height`` or ``HEIGHT`` columns are present and the values in a row are\n+ positive, then the rows will be rasterized as rectangles of the corresponding size, with the upper-left\n+ corner given by the X and Y coordinates.\n - If a ``label`` or ``LABEL`` column is present, then the corresponding labels will be used for rasterization\n- (unless "Produce binary image" is activated). Different points are allowed to use the same label. If used, the\n- label must be numeric and integer.\n-\n- **Using a GeoJSON input file:** Only rectangular specifications of `Polygon` type geometry is supported.\n+ (unless "Produce binary image" is activated). Different rows are allowed to use the same label. If used,\n+ the label must be numeric and integer.\n \n </help>\n <citations>\n' |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/input5.tsv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input5.tsv Thu Nov 06 09:59:34 2025 +0000 |
| b |
| @@ -0,0 +1,4 @@ +pos_x pos_y radius width height label +20 20 10 0 0 1 +50 50 0 30 40 2 +150 50 0 60 40 3 |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/input6.tsv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input6.tsv Thu Nov 06 09:59:34 2025 +0000 |
| b |
| @@ -0,0 +1,4 @@ +pos_x pos_y radius width height label +20 20 10 0 0 1 +50 50 10 30 40 2 +150 50 0 60 40 3 |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/output2.tiff |
| b |
| Binary file test-data/output2.tiff has changed |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/output4.tiff |
| b |
| Binary file test-data/output4.tiff has changed |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/output5.tiff |
| b |
| Binary file test-data/output5.tiff has changed |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/output6.tiff |
| b |
| Binary file test-data/output6.tiff has changed |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/output7.tiff |
| b |
| Binary file test-data/output7.tiff has changed |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/output8.tiff |
| b |
| Binary file test-data/output8.tiff has changed |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/rois-illegal1.geojson --- a/test-data/rois-illegal1.geojson Mon May 12 14:01:26 2025 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 |
| [ |
| @@ -1,40 +0,0 @@ -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "id": "a5d9de43-1a4a-4194-b06d-a6c6d0f81f91", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 201, - 48 - ], - [ - 292, - 48 - ], - [ - 292, - 184 - ], - [ - 201, - 48 - ], - [ - 201, - 48 - ] - ] - ] - }, - "properties": { - "objectType": "annotation", - "name": "1" - } - } - ] -} |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/rois-illegal2.geojson --- a/test-data/rois-illegal2.geojson Mon May 12 14:01:26 2025 +0000 +++ b/test-data/rois-illegal2.geojson Thu Nov 06 09:59:34 2025 +0000 |
| [ |
| @@ -5,29 +5,27 @@ "type": "Feature", "id": "a5d9de43-1a4a-4194-b06d-a6c6d0f81f91", "geometry": { - "type": "Point", + "type": "LineString", "coordinates": [ [ - [ - 201, - 48 - ], - [ - 292, - 48 - ], - [ - 292, - 184 - ], - [ - 201, - 184 - ], - [ - 201, - 48 - ] + 201, + 48 + ], + [ + 292, + 48 + ], + [ + 292, + 184 + ], + [ + 201, + 184 + ], + [ + 201, + 48 ] ] }, |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/rois-noname.geojson --- a/test-data/rois-noname.geojson Mon May 12 14:01:26 2025 +0000 +++ b/test-data/rois-noname.geojson Thu Nov 06 09:59:34 2025 +0000 |
| [ |
| @@ -3,6 +3,18 @@ "features": [ { "type": "Feature", + "id": "9ef0487b-29a5-4e53-8eca-5c9dbf7bfc81", + "geometry": { + "type": "Point", + "coordinates": [100, 50] + }, + "properties": { + "objectType": "annotation", + "radius": 100 + } + }, + { + "type": "Feature", "id": "9ef0487b-29a5-4e53-8eca-5c9dbf7bfc80", "geometry": { "type": "Polygon", @@ -17,11 +29,11 @@ 149 ], [ - 183, + 163, 275 ], [ - 124, + 104, 275 ], [ @@ -64,6 +76,9 @@ ] ] ] + }, + "properties": { + "objectType": "annotation" } }, { @@ -73,31 +88,18 @@ "type": "Polygon", "coordinates": [ [ - [ - 151, - 95 - ], - [ - 260, - 95 - ], - [ - 260, - 162 - ], - [ - 151, - 162 - ], - [ - 151, - 95 - ] + [151, 95], + [260, 95], + [260, 162], + [151, 162] + ], + [ + [156, 100], + [255, 100], + [255, 157], + [156, 157] ] ] - }, - "properties": { - "objectType": "annotation" } } ] |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test-data/rois.geojson --- a/test-data/rois.geojson Mon May 12 14:01:26 2025 +0000 +++ b/test-data/rois.geojson Thu Nov 06 09:59:34 2025 +0000 |
| [ |
| @@ -3,6 +3,18 @@ "features": [ { "type": "Feature", + "id": "9ef0487b-29a5-4e53-8eca-5c9dbf7bfc81", + "geometry": { + "type": "Point", + "coordinates": [100, 50] + }, + "properties": { + "objectType": "annotation", + "radius": 100 + } + }, + { + "type": "Feature", "id": "9ef0487b-29a5-4e53-8eca-5c9dbf7bfc80", "geometry": { "type": "Polygon", @@ -17,11 +29,11 @@ 149 ], [ - 183, + 163, 275 ], [ - 124, + 104, 275 ], [ @@ -78,26 +90,16 @@ "type": "Polygon", "coordinates": [ [ - [ - 151, - 95 - ], - [ - 260, - 95 - ], - [ - 260, - 162 - ], - [ - 151, - 162 - ], - [ - 151, - 95 - ] + [151, 95], + [260, 95], + [260, 162], + [151, 162] + ], + [ + [156, 100], + [255, 100], + [255, 157], + [156, 157] ] ] }, @@ -107,4 +109,4 @@ } } ] -} \ No newline at end of file +} |
| b |
| diff -r 4a49f74a3c14 -r 22bb32eae6a1 test_utils.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test_utils.py Thu Nov 06 09:59:34 2025 +0000 |
| [ |
| @@ -0,0 +1,13 @@ +import unittest + +import points2label + + +class get_list_depth(unittest.TestCase): + + def test(self): + self.assertEqual(points2label.get_list_depth(1234), 0) + self.assertEqual(points2label.get_list_depth([]), 1) + self.assertEqual(points2label.get_list_depth([1, 2, 3]), 1) + self.assertEqual(points2label.get_list_depth([1, [2, 3]]), 2) + self.assertEqual(points2label.get_list_depth([[1], [2, 3]]), 2) |