Repository 'points2labelimage'
hg clone https://toolshed.g2.bx.psu.edu/repos/imgteam/points2labelimage

Changeset 6:22bb32eae6a1 (2025-11-06)
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)