From 0ca6da7a63aa3f1582bcaa3e0f754286dce90842 Mon Sep 17 00:00:00 2001 From: Hamdah Shafqat Abbasi Date: Thu, 20 Mar 2025 11:15:09 -0400 Subject: [PATCH 1/3] updated examples in microjson --- .../examples/segmentations.json | 3865 ----------------- .../ome-to-microjson-tool/plugin.json | 14 +- .../ome-to-microjson-tool/pyproject.toml | 2 +- .../ome_to_microjson/__main__.py | 46 +- .../ome_to_microjson/ome_microjson.py | 121 +- .../visualization/ome_to_microjson/utils.py | 187 + 6 files changed, 279 insertions(+), 3956 deletions(-) delete mode 100644 visualization/ome-to-microjson-tool/examples/segmentations.json create mode 100644 visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/utils.py diff --git a/visualization/ome-to-microjson-tool/examples/segmentations.json b/visualization/ome-to-microjson-tool/examples/segmentations.json deleted file mode 100644 index 8716c3ca1..000000000 --- a/visualization/ome-to-microjson-tool/examples/segmentations.json +++ /dev/null @@ -1,3865 +0,0 @@ -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 822.0, - 79.0 - ], - [ - 866.0, - 79.0 - ], - [ - 866.0, - 105.0 - ], - [ - 822.0, - 105.0 - ], - [ - 822.0, - 79.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 1.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 965.0, - 97.0 - ], - [ - 998.0, - 97.0 - ], - [ - 998.0, - 118.0 - ], - [ - 965.0, - 118.0 - ], - [ - 965.0, - 97.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 2.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 749.0, - 121.0 - ], - [ - 772.0, - 121.0 - ], - [ - 772.0, - 149.0 - ], - [ - 749.0, - 149.0 - ], - [ - 749.0, - 121.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 3.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1021.0, - 176.0 - ], - [ - 1049.0, - 176.0 - ], - [ - 1049.0, - 214.0 - ], - [ - 1021.0, - 214.0 - ], - [ - 1021.0, - 176.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 4.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 753.0, - 204.0 - ], - [ - 805.0, - 204.0 - ], - [ - 805.0, - 232.0 - ], - [ - 753.0, - 232.0 - ], - [ - 753.0, - 204.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 5.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 871.0, - 255.0 - ], - [ - 910.0, - 255.0 - ], - [ - 910.0, - 287.0 - ], - [ - 871.0, - 287.0 - ], - [ - 871.0, - 255.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 6.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 338.0, - 259.0 - ], - [ - 350.0, - 259.0 - ], - [ - 350.0, - 305.0 - ], - [ - 338.0, - 305.0 - ], - [ - 338.0, - 259.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 7.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 433.0, - 262.0 - ], - [ - 458.0, - 262.0 - ], - [ - 458.0, - 295.0 - ], - [ - 433.0, - 295.0 - ], - [ - 433.0, - 262.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 8.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1025.0, - 270.0 - ], - [ - 1052.0, - 270.0 - ], - [ - 1052.0, - 297.0 - ], - [ - 1025.0, - 297.0 - ], - [ - 1025.0, - 270.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 9.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1010.0, - 288.0 - ], - [ - 1025.0, - 288.0 - ], - [ - 1025.0, - 307.0 - ], - [ - 1010.0, - 307.0 - ], - [ - 1010.0, - 288.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 10.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 142.0, - 291.0 - ], - [ - 170.0, - 291.0 - ], - [ - 170.0, - 334.0 - ], - [ - 142.0, - 334.0 - ], - [ - 142.0, - 291.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 11.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 612.0, - 305.0 - ], - [ - 656.0, - 305.0 - ], - [ - 656.0, - 332.0 - ], - [ - 612.0, - 332.0 - ], - [ - 612.0, - 305.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 12.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1034.0, - 308.0 - ], - [ - 1038.0, - 308.0 - ], - [ - 1038.0, - 312.0 - ], - [ - 1034.0, - 312.0 - ], - [ - 1034.0, - 308.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 13.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 279.0, - 320.0 - ], - [ - 306.0, - 320.0 - ], - [ - 306.0, - 345.0 - ], - [ - 279.0, - 345.0 - ], - [ - 279.0, - 320.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 14.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 822.0, - 330.0 - ], - [ - 851.0, - 330.0 - ], - [ - 851.0, - 369.0 - ], - [ - 822.0, - 369.0 - ], - [ - 822.0, - 330.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 15.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 14.0, - 336.0 - ], - [ - 44.0, - 336.0 - ], - [ - 44.0, - 372.0 - ], - [ - 14.0, - 372.0 - ], - [ - 14.0, - 336.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 16.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 452.0, - 358.0 - ], - [ - 488.0, - 358.0 - ], - [ - 488.0, - 377.0 - ], - [ - 452.0, - 377.0 - ], - [ - 452.0, - 358.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 17.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 962.0, - 361.0 - ], - [ - 987.0, - 361.0 - ], - [ - 987.0, - 397.0 - ], - [ - 962.0, - 397.0 - ], - [ - 962.0, - 361.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 18.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 796.0, - 370.0 - ], - [ - 819.0, - 370.0 - ], - [ - 819.0, - 394.0 - ], - [ - 796.0, - 394.0 - ], - [ - 796.0, - 370.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 19.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 743.0, - 376.0 - ], - [ - 765.0, - 376.0 - ], - [ - 765.0, - 396.0 - ], - [ - 743.0, - 396.0 - ], - [ - 743.0, - 376.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 20.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 592.0, - 377.0 - ], - [ - 623.0, - 377.0 - ], - [ - 623.0, - 399.0 - ], - [ - 592.0, - 399.0 - ], - [ - 592.0, - 377.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 21.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 913.0, - 377.0 - ], - [ - 942.0, - 377.0 - ], - [ - 942.0, - 405.0 - ], - [ - 913.0, - 405.0 - ], - [ - 913.0, - 377.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 22.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 581.0, - 379.0 - ], - [ - 593.0, - 379.0 - ], - [ - 593.0, - 390.0 - ], - [ - 581.0, - 390.0 - ], - [ - 581.0, - 379.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 23.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 727.0, - 380.0 - ], - [ - 739.0, - 380.0 - ], - [ - 739.0, - 395.0 - ], - [ - 727.0, - 395.0 - ], - [ - 727.0, - 380.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 24.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 897.0, - 386.0 - ], - [ - 900.0, - 386.0 - ], - [ - 900.0, - 389.0 - ], - [ - 897.0, - 389.0 - ], - [ - 897.0, - 386.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 25.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1037.0, - 394.0 - ], - [ - 1072.0, - 394.0 - ], - [ - 1072.0, - 429.0 - ], - [ - 1037.0, - 429.0 - ], - [ - 1037.0, - 394.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 26.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 571.0, - 407.0 - ], - [ - 604.0, - 407.0 - ], - [ - 604.0, - 436.0 - ], - [ - 571.0, - 436.0 - ], - [ - 571.0, - 407.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 27.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 667.0, - 409.0 - ], - [ - 694.0, - 409.0 - ], - [ - 694.0, - 446.0 - ], - [ - 667.0, - 446.0 - ], - [ - 667.0, - 409.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 28.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 144.0, - 417.0 - ], - [ - 171.0, - 417.0 - ], - [ - 171.0, - 440.0 - ], - [ - 144.0, - 440.0 - ], - [ - 144.0, - 417.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 29.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 899.0, - 426.0 - ], - [ - 918.0, - 426.0 - ], - [ - 918.0, - 441.0 - ], - [ - 899.0, - 441.0 - ], - [ - 899.0, - 426.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 30.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 921.0, - 433.0 - ], - [ - 935.0, - 433.0 - ], - [ - 935.0, - 446.0 - ], - [ - 921.0, - 446.0 - ], - [ - 921.0, - 433.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 31.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 979.0, - 438.0 - ], - [ - 1008.0, - 438.0 - ], - [ - 1008.0, - 457.0 - ], - [ - 979.0, - 457.0 - ], - [ - 979.0, - 438.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 32.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 960.0, - 446.0 - ], - [ - 979.0, - 446.0 - ], - [ - 979.0, - 464.0 - ], - [ - 960.0, - 464.0 - ], - [ - 960.0, - 446.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 33.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 883.0, - 463.0 - ], - [ - 913.0, - 463.0 - ], - [ - 913.0, - 491.0 - ], - [ - 883.0, - 491.0 - ], - [ - 883.0, - 463.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 34.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 535.0, - 464.0 - ], - [ - 572.0, - 464.0 - ], - [ - 572.0, - 498.0 - ], - [ - 535.0, - 498.0 - ], - [ - 535.0, - 464.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 35.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1059.0, - 469.0 - ], - [ - 1078.0, - 469.0 - ], - [ - 1078.0, - 510.0 - ], - [ - 1059.0, - 510.0 - ], - [ - 1059.0, - 469.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 36.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 261.0, - 479.0 - ], - [ - 273.0, - 479.0 - ], - [ - 273.0, - 501.0 - ], - [ - 261.0, - 501.0 - ], - [ - 261.0, - 479.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 37.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 275.0, - 480.0 - ], - [ - 288.0, - 480.0 - ], - [ - 288.0, - 501.0 - ], - [ - 275.0, - 501.0 - ], - [ - 275.0, - 480.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 38.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 155.0, - 491.0 - ], - [ - 178.0, - 491.0 - ], - [ - 178.0, - 521.0 - ], - [ - 155.0, - 521.0 - ], - [ - 155.0, - 491.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 39.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 333.0, - 503.0 - ], - [ - 354.0, - 503.0 - ], - [ - 354.0, - 535.0 - ], - [ - 333.0, - 535.0 - ], - [ - 333.0, - 503.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 40.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 813.0, - 510.0 - ], - [ - 844.0, - 510.0 - ], - [ - 844.0, - 535.0 - ], - [ - 813.0, - 535.0 - ], - [ - 813.0, - 510.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 41.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 487.0, - 528.0 - ], - [ - 517.0, - 528.0 - ], - [ - 517.0, - 556.0 - ], - [ - 487.0, - 556.0 - ], - [ - 487.0, - 528.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 42.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 385.0, - 531.0 - ], - [ - 410.0, - 531.0 - ], - [ - 410.0, - 561.0 - ], - [ - 385.0, - 561.0 - ], - [ - 385.0, - 531.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 43.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 866.0, - 533.0 - ], - [ - 904.0, - 533.0 - ], - [ - 904.0, - 564.0 - ], - [ - 866.0, - 564.0 - ], - [ - 866.0, - 533.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 44.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 979.0, - 540.0 - ], - [ - 1002.0, - 540.0 - ], - [ - 1002.0, - 575.0 - ], - [ - 979.0, - 575.0 - ], - [ - 979.0, - 540.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 45.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 678.0, - 572.0 - ], - [ - 698.0, - 572.0 - ], - [ - 698.0, - 614.0 - ], - [ - 678.0, - 614.0 - ], - [ - 678.0, - 572.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 46.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 617.0, - 582.0 - ], - [ - 641.0, - 582.0 - ], - [ - 641.0, - 612.0 - ], - [ - 617.0, - 612.0 - ], - [ - 617.0, - 582.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 47.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 0.0, - 587.0 - ], - [ - 11.0, - 587.0 - ], - [ - 11.0, - 636.0 - ], - [ - 0.0, - 636.0 - ], - [ - 0.0, - 587.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 48.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 310.0, - 595.0 - ], - [ - 347.0, - 595.0 - ], - [ - 347.0, - 616.0 - ], - [ - 310.0, - 616.0 - ], - [ - 310.0, - 595.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 49.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 213.0, - 608.0 - ], - [ - 241.0, - 608.0 - ], - [ - 241.0, - 638.0 - ], - [ - 213.0, - 638.0 - ], - [ - 213.0, - 608.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 50.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 343.0, - 614.0 - ], - [ - 392.0, - 614.0 - ], - [ - 392.0, - 645.0 - ], - [ - 343.0, - 645.0 - ], - [ - 343.0, - 614.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 51.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 145.0, - 629.0 - ], - [ - 180.0, - 629.0 - ], - [ - 180.0, - 660.0 - ], - [ - 145.0, - 660.0 - ], - [ - 145.0, - 629.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 52.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 520.0, - 679.0 - ], - [ - 556.0, - 679.0 - ], - [ - 556.0, - 708.0 - ], - [ - 520.0, - 708.0 - ], - [ - 520.0, - 679.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 53.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 298.0, - 698.0 - ], - [ - 319.0, - 698.0 - ], - [ - 319.0, - 737.0 - ], - [ - 298.0, - 737.0 - ], - [ - 298.0, - 698.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 54.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 762.0, - 702.0 - ], - [ - 800.0, - 702.0 - ], - [ - 800.0, - 740.0 - ], - [ - 762.0, - 740.0 - ], - [ - 762.0, - 702.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 55.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 36.0, - 705.0 - ], - [ - 69.0, - 705.0 - ], - [ - 69.0, - 740.0 - ], - [ - 36.0, - 740.0 - ], - [ - 36.0, - 705.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 56.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 916.0, - 711.0 - ], - [ - 943.0, - 711.0 - ], - [ - 943.0, - 751.0 - ], - [ - 916.0, - 751.0 - ], - [ - 916.0, - 711.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 57.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 436.0, - 715.0 - ], - [ - 469.0, - 715.0 - ], - [ - 469.0, - 748.0 - ], - [ - 436.0, - 748.0 - ], - [ - 436.0, - 715.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 58.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 958.0, - 731.0 - ], - [ - 984.0, - 731.0 - ], - [ - 984.0, - 773.0 - ], - [ - 958.0, - 773.0 - ], - [ - 958.0, - 731.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 59.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 761.0, - 785.0 - ], - [ - 804.0, - 785.0 - ], - [ - 804.0, - 810.0 - ], - [ - 761.0, - 810.0 - ], - [ - 761.0, - 785.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 60.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 248.0, - 794.0 - ], - [ - 278.0, - 794.0 - ], - [ - 278.0, - 828.0 - ], - [ - 248.0, - 828.0 - ], - [ - 248.0, - 794.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 61.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 632.0, - 795.0 - ], - [ - 664.0, - 795.0 - ], - [ - 664.0, - 822.0 - ], - [ - 632.0, - 822.0 - ], - [ - 632.0, - 795.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 62.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 191.0, - 796.0 - ], - [ - 223.0, - 796.0 - ], - [ - 223.0, - 823.0 - ], - [ - 191.0, - 823.0 - ], - [ - 191.0, - 796.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 63.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 106.0, - 801.0 - ], - [ - 126.0, - 801.0 - ], - [ - 126.0, - 841.0 - ], - [ - 106.0, - 841.0 - ], - [ - 106.0, - 801.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 64.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1008.0, - 807.0 - ], - [ - 1043.0, - 807.0 - ], - [ - 1043.0, - 830.0 - ], - [ - 1008.0, - 830.0 - ], - [ - 1008.0, - 807.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 65.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 899.0, - 814.0 - ], - [ - 933.0, - 814.0 - ], - [ - 933.0, - 837.0 - ], - [ - 899.0, - 837.0 - ], - [ - 899.0, - 814.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 66.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 490.0, - 821.0 - ], - [ - 527.0, - 821.0 - ], - [ - 527.0, - 858.0 - ], - [ - 490.0, - 858.0 - ], - [ - 490.0, - 821.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 67.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1048.0, - 834.0 - ], - [ - 1080.0, - 834.0 - ], - [ - 1080.0, - 858.0 - ], - [ - 1048.0, - 858.0 - ], - [ - 1048.0, - 834.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 68.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 410.0, - 842.0 - ], - [ - 430.0, - 842.0 - ], - [ - 430.0, - 872.0 - ], - [ - 410.0, - 872.0 - ], - [ - 410.0, - 842.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 69.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 45.0, - 844.0 - ], - [ - 71.0, - 844.0 - ], - [ - 71.0, - 877.0 - ], - [ - 45.0, - 877.0 - ], - [ - 45.0, - 844.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 70.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 859.0, - 849.0 - ], - [ - 891.0, - 849.0 - ], - [ - 891.0, - 886.0 - ], - [ - 859.0, - 886.0 - ], - [ - 859.0, - 849.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 71.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 897.0, - 850.0 - ], - [ - 948.0, - 850.0 - ], - [ - 948.0, - 864.0 - ], - [ - 897.0, - 864.0 - ], - [ - 897.0, - 850.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 72.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 162.0, - 852.0 - ], - [ - 187.0, - 852.0 - ], - [ - 187.0, - 888.0 - ], - [ - 162.0, - 888.0 - ], - [ - 162.0, - 852.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 73.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 823.0, - 877.0 - ], - [ - 848.0, - 877.0 - ], - [ - 848.0, - 904.0 - ], - [ - 823.0, - 904.0 - ], - [ - 823.0, - 877.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 74.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 938.0, - 881.0 - ], - [ - 984.0, - 881.0 - ], - [ - 984.0, - 909.0 - ], - [ - 938.0, - 909.0 - ], - [ - 938.0, - 881.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 75.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 6.0, - 892.0 - ], - [ - 42.0, - 892.0 - ], - [ - 42.0, - 918.0 - ], - [ - 6.0, - 918.0 - ], - [ - 6.0, - 892.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 76.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 791.0, - 900.0 - ], - [ - 816.0, - 900.0 - ], - [ - 816.0, - 930.0 - ], - [ - 791.0, - 930.0 - ], - [ - 791.0, - 900.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 77.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 701.0, - 907.0 - ], - [ - 736.0, - 907.0 - ], - [ - 736.0, - 934.0 - ], - [ - 701.0, - 934.0 - ], - [ - 701.0, - 907.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 78.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1015.0, - 921.0 - ], - [ - 1037.0, - 921.0 - ], - [ - 1037.0, - 960.0 - ], - [ - 1015.0, - 960.0 - ], - [ - 1015.0, - 921.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 79.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 129.0, - 927.0 - ], - [ - 157.0, - 927.0 - ], - [ - 157.0, - 956.0 - ], - [ - 129.0, - 956.0 - ], - [ - 129.0, - 927.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 80.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 263.0, - 938.0 - ], - [ - 301.0, - 938.0 - ], - [ - 301.0, - 958.0 - ], - [ - 263.0, - 958.0 - ], - [ - 263.0, - 938.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 81.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 393.0, - 953.0 - ], - [ - 438.0, - 953.0 - ], - [ - 438.0, - 972.0 - ], - [ - 393.0, - 972.0 - ], - [ - 393.0, - 953.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 82.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 1063.0, - 966.0 - ], - [ - 1080.0, - 966.0 - ], - [ - 1080.0, - 995.0 - ], - [ - 1063.0, - 995.0 - ], - [ - 1063.0, - 966.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 83.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 497.0, - 975.0 - ], - [ - 534.0, - 975.0 - ], - [ - 534.0, - 1007.0 - ], - [ - 497.0, - 1007.0 - ], - [ - 497.0, - 975.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 84.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 415.0, - 991.0 - ], - [ - 448.0, - 991.0 - ], - [ - 448.0, - 1012.0 - ], - [ - 415.0, - 1012.0 - ], - [ - 415.0, - 991.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 85.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 232.0, - 995.0 - ], - [ - 264.0, - 995.0 - ], - [ - 264.0, - 1025.0 - ], - [ - 232.0, - 1025.0 - ], - [ - 232.0, - 995.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 86.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 35.0, - 1009.0 - ], - [ - 62.0, - 1009.0 - ], - [ - 62.0, - 1038.0 - ], - [ - 35.0, - 1038.0 - ], - [ - 35.0, - 1009.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 87.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 310.0, - 1009.0 - ], - [ - 348.0, - 1009.0 - ], - [ - 348.0, - 1036.0 - ], - [ - 310.0, - 1036.0 - ], - [ - 310.0, - 1009.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 88.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 124.0, - 1029.0 - ], - [ - 151.0, - 1029.0 - ], - [ - 151.0, - 1068.0 - ], - [ - 124.0, - 1068.0 - ], - [ - 124.0, - 1029.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 89.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 205.0, - 1067.0 - ], - [ - 229.0, - 1067.0 - ], - [ - 229.0, - 1080.0 - ], - [ - 205.0, - 1080.0 - ], - [ - 205.0, - 1067.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 90.0 - } - } - }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - 597.0, - 1075.0 - ], - [ - 607.0, - 1075.0 - ], - [ - 607.0, - 1080.0 - ], - [ - 597.0, - 1080.0 - ], - [ - 597.0, - 1075.0 - ] - ] - ] - }, - "properties": { - "descriptive": { - "Plate": "41744", - "Image": "x00_y11_p07_c1.ome.tif" - }, - "numerical": { - "X": 1080.0, - "Y": 1080.0, - "Channel": 1.0, - "Label": 91.0 - } - } - } - ], - "coordinatesystem": { - "axes": [ - { - "name": "x", - "type": "cartesian", - "unit": "micrometer", - "description": "x-axis" - }, - { - "name": "y", - "type": "cartesian", - "unit": "micrometer", - "description": "y-axis" - } - ] - }, - "value_range": { - "X": { - "min": 1080.0, - "max": 1080.0 - }, - "Y": { - "min": 1080.0, - "max": 1080.0 - }, - "Channel": { - "min": 1.0, - "max": 1.0 - }, - "Label": { - "min": 1.0, - "max": 91.0 - } - }, - "descriptive_fields": [ - "Plate", - "Image" - ] -} diff --git a/visualization/ome-to-microjson-tool/plugin.json b/visualization/ome-to-microjson-tool/plugin.json index 5e31afc3a..46b89c45f 100644 --- a/visualization/ome-to-microjson-tool/plugin.json +++ b/visualization/ome-to-microjson-tool/plugin.json @@ -42,6 +42,12 @@ }, "required": "True" }, + { + "name": "tileJson", + "description": "TileJSON layer", + "type": "boolean", + "required": false + }, { "name": "preview", "type": "boolean", @@ -80,6 +86,12 @@ "default": "encoding", "required": "True" }, + { + "key": "inputs.tileJson", + "title": "tileJson", + "description": "TileJSON layer", + "default": false + }, { "key": "inputs.preview", "type": "boolean", @@ -88,4 +100,4 @@ "required": "False" } ] -} +} \ No newline at end of file diff --git a/visualization/ome-to-microjson-tool/pyproject.toml b/visualization/ome-to-microjson-tool/pyproject.toml index fe3c18310..5eb02e7ac 100644 --- a/visualization/ome-to-microjson-tool/pyproject.toml +++ b/visualization/ome-to-microjson-tool/pyproject.toml @@ -13,7 +13,7 @@ typer = "^0.7.0" scipy = ">=1.8,<1.9.2" filepattern = "^2.0.1" tqdm = "^4.65.0" -microjson = "^0.1.9" +microjson = "^0.4.1" fastapi = "^0.103.1" llvmlite = "^0.40.1" vaex = "^4.17.0" diff --git a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__main__.py b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__main__.py index 977b67242..aa01ae9fb 100644 --- a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__main__.py +++ b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__main__.py @@ -1,4 +1,5 @@ """Ome micojson package.""" + import logging import shutil import time @@ -6,11 +7,12 @@ from concurrent.futures import ProcessPoolExecutor from os import environ from pathlib import Path - import filepattern as fp import polus.images.visualization.ome_to_microjson.ome_microjson as sm +import polus.images.visualization.ome_to_microjson.utils as ut import typer from tqdm import tqdm +from typing import Optional warnings.filterwarnings("ignore") @@ -57,6 +59,11 @@ def main( "-t", help="Desired polygon type.", ), + tile_json: Optional[bool] = typer.Option( + False, + "--tileJson", + help="Tile JSON layer", + ), out_dir: Path = typer.Option( ..., "--outDir", @@ -74,7 +81,9 @@ def main( logger.info(f"inpDir: {inp_dir}") logger.info(f"filePattern: {file_pattern}") logger.info(f"polygonType: {polygon_type}") + logger.info(f"tile_json = {tile_json}") logger.info(f"outDir: {out_dir}") + starttime = time.time() if not inp_dir.exists(): @@ -82,8 +91,8 @@ def main( raise ValueError(msg, inp_dir) if not out_dir.exists(): - msg = "outDir does not exist" - raise ValueError(msg, out_dir) + msg = "Create outDir as it does not exist" + Path(out_dir).mkdir(exist_ok=True, parents=True) files = fp.FilePattern(inp_dir, file_pattern) @@ -91,22 +100,27 @@ def main( msg = "No image files are detected. Please check filepattern again!" raise ValueError(msg) - with ProcessPoolExecutor(max_workers=sm.NUM_THREADS) as executor: - for _, f in enumerate(tqdm(files())): - model = sm.OmeMicrojsonModel( - out_dir=out_dir, - file_path=str(f[1][0]), - polygon_type=polygon_type, - ) - executor.submit(model.write_single_json()) - if preview: - generate_preview(out_dir) + ut.preview_data(out_dir) logger.info(f"generating preview data in {out_dir}") - endtime = (time.time() - starttime) / 60 - logger.info(f"Total time taken for execution: {endtime:.4f} minutes") - logger.info(f"Total time taken for a single file: {endtime/len(files):.4f} minutes") + else: + + with ProcessPoolExecutor(max_workers=sm.NUM_THREADS) as executor: + for _, f in enumerate(tqdm(files())): + model = sm.OmeMicrojsonModel( + out_dir=out_dir, + file_path=str(f[1][0]), + polygon_type=polygon_type, + tile_json=tile_json, + ) + executor.submit(model.write_single_json()) + + endtime = (time.time() - starttime) / 60 + logger.info(f"Total time taken for execution: {endtime:.4f} minutes") + logger.info( + f"Total time taken for a single file: {endtime/len(files):.4f} minutes" + ) if __name__ == "__main__": diff --git a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/ome_microjson.py b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/ome_microjson.py index f2f88e9c9..fb49b6c58 100644 --- a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/ome_microjson.py +++ b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/ome_microjson.py @@ -1,4 +1,5 @@ """Ome micojson package.""" + import ast import enum import logging @@ -13,6 +14,7 @@ from sys import platform from typing import Any from typing import Union +from typing import Optional import filepattern as fp import microjson.model as mj @@ -22,12 +24,13 @@ from bfio import BioReader from skimage import measure from skimage import morphology +import polus.images.visualization.ome_to_microjson.utils as ut logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -TILE_SIZE = 1024 +TILE_SIZE = 512 if platform == "linux" or platform == "linux2": NUM_THREADS = len(os.sched_getaffinity(0)) # type: ignore else: @@ -56,11 +59,13 @@ def __init__( out_dir: Path, file_path: str, polygon_type: PolygonType, + tile_json: Optional[bool] = False, ) -> None: """Convert each object polygons (series of points, rectangle) to microjson.""" self.out_dir = out_dir self.file_path = file_path self.polygon_type = polygon_type + self.tile_json = tile_json self.min_label_length = 1 self.min_unique_labels = 0 self.max_unique_labels = 2 @@ -77,6 +82,7 @@ def _tile_read(self) -> None: range(0, self.br.X, TILE_SIZE), ), ): + y_max = min([self.br.Y, y + TILE_SIZE]) x_max = min([self.br.X, x + TILE_SIZE]) image = self.br[y:y_max, x:x_max, z : z + 1] @@ -152,7 +158,7 @@ def write_single_json(self) -> None: self._tile_read() out_combined = Path(self.out_dir, "tmp") out_file = ( - Path(self.file_path).name.split(".")[0] + Path(self.file_path).name.rsplit(".", 1)[0] + "_" + str(self.polygon_type.value) + ".json" @@ -165,59 +171,70 @@ def write_single_json(self) -> None: with Path.open(Path(out_combined, out_file), "w") as fw: for i, fl in zip(range(1, len(files) + 1), files()): file = fl[1][0] - line_number = self.get_line_number(file, "coordinatesystem") total_lines = 0 with Path.open(Path(file)) as file: for _ in file: total_lines += 1 - if line_number is not None: - index = (total_lines - line_number) + 3 + if total_lines is not None: outname = re.split(r"[_\.]+", file.name)[:-2] outname = "_".join(outname) + ".json" # type: ignore - df = Path.open(Path(file)) + df = Path.open(Path(file.name)) data = df.readlines() if i == 1: - endline = data[-index].rstrip() + "," - sfdata = data[:-index] + [endline] + endline = data[-3].rstrip() + "," + sfdata = data[:-3] + [endline] elif i > 1 and i < len(files): - endline = data[-index].rstrip() + "," - sfdata = data[3:-index] + [endline] + endline = data[-3].rstrip() + "," + sfdata = data[3:-3] + [endline] else: sfdata = data[3:] fw.writelines(sfdata) - Path(file).unlink() + Path(file.name).unlink() else: msg = "Invalid Microjson file!!! Please do check it again" raise ValueError( msg, ) + self.cleaning_directories() + outname = self.out_dir.joinpath(out_file) + if self.tile_json: + logger.info(f"Generating tileJSON: {outname}") + ut.convert_microjson_tile_json(Path(outname)) def segmentations_encodings( self, label_image: np.ndarray, x: int, y: int, - ) -> tuple[Any, list[list[list[Any]]]]: - """Calculate object boundries as series of vertices/points forming a polygon.""" + ) -> tuple[list[int], list[list[tuple[float, float]]]]: + """Calculate object boundaries as series of vertices/points forming a polygon.""" label, coordinates = [], [] - objects = scipy.ndimage.measurements.find_objects(label_image) - for i in range(len(objects) + 1): - mask = np.zeros((label_image.shape[0], label_image.shape[1])) - mask[(label_image == i)] = 1 - contour_thresh = 0.8 + unique_labels = np.unique(label_image) + + for i in unique_labels: + if i == 0: # Skip background + continue + mask = np.zeros_like(label_image, dtype=np.uint8) + mask[label_image == i] = 1 + contour_thresh = 0.0 contour = measure.find_contours(mask, contour_thresh) + if ( - len(contour) > self.min_unique_labels + len(contour) >= self.min_unique_labels and len(contour) < self.max_unique_labels - and len(contour[0] > self.min_label_length) + and (contour and len(contour[0]) >= self.min_label_length) ): - contour = np.flip(contour, axis=1) - seg_encodings = contour.ravel().tolist() + # Take the first contour (assuming one per object) + contour_array = np.flip(contour, axis=1) + seg_encodings = contour_array.ravel().tolist() poly = [ - [xi + x, yi + y] + (xi + x, yi + y) for xi, yi in zip(seg_encodings[1::2], seg_encodings[::2]) ] + if poly and poly[0] != poly[-1]: + poly.append(poly[0]) + label.append(i) coordinates.append(poly) @@ -264,7 +281,6 @@ def polygons_to_microjson( # noqa: PLR0915 """Create microjson overlays in JSON Format.""" x_dimension = np.repeat(self.br.X, len(label)) y_dimension = np.repeat(self.br.Y, len(label)) - channel = np.repeat(self.br.C, len(label)) filename = Path(self.file_path) image_name = np.repeat(filename.name, len(label)) @@ -272,7 +288,6 @@ def polygons_to_microjson( # noqa: PLR0915 Image=image_name, X=x_dimension, Y=y_dimension, - Channel=channel, Label=label, ) data["geometry_type"] = np.repeat("Polygon", data.shape[0]) @@ -282,7 +297,6 @@ def polygons_to_microjson( # noqa: PLR0915 "Image", "X", "Y", - "Channel", "Label", "geometry_type", "type", @@ -296,42 +310,33 @@ def polygons_to_microjson( # noqa: PLR0915 msg = "Invalid vaex dataframe!! Please do check path again" raise ValueError(msg) - str_columns = list( + columns = list( filter( - lambda feature: feature in ["Image", "X", "Y", "Channel"], - data.get_column_names(), - ), - ) - int_columns = list( - filter( - lambda feature: feature in ["Label"], + lambda feature: feature in ["Image", "X", "Y", "Label"], data.get_column_names(), ), ) - if len(int_columns) == 0: + if len(columns) == 0: msg = "Features with integer datatype do not exist" raise ValueError(msg) features: list[mj.Feature] = [] for (_, row), cor in zip(data.iterrows(), coordinates): # type: ignore - numerical = [{key: row[key]} for key in int_columns] + numerical = [{key: row[key]} for key in columns] - numeric_dict = {} + properties = {} for sub_dict in numerical: - numeric_dict.update(sub_dict) + properties.update(sub_dict) GeometryClass = getattr(mj, row["geometry_type"]) # noqa: N806 if self.polygon_type == PolygonType.RECTANGLE: - cor_value = list(ast.literal_eval(cor)) + cor_value = ast.literal_eval(cor) else: cor_value = cor geometry = GeometryClass(type=row["geometry_type"], coordinates=[cor_value]) - # create a new properties object dynamically - properties = mj.Properties(numeric=numeric_dict) - # Create a new Feature object feature = mj.MicroFeature( type=row["type"], @@ -340,47 +345,17 @@ def polygons_to_microjson( # noqa: PLR0915 ) features.append(feature) - valrange = [{i: {"min": 1.0, "max": data[i].max()}} for i in int_columns] - valrange_dict = {} - for sub_dict in valrange: - valrange_dict.update(sub_dict) - - desc_meta = {key: f"{data[key].values[0]}" for key in str_columns} - - # create a new properties for each image - properties = mj.Properties(string=desc_meta) - # Create a new FeatureCollection object feature_collection = mj.MicroFeatureCollection( type="FeatureCollection", - properties=properties, features=features, - value_range=valrange_dict, - coordinatesystem={ - "axes": [ - { - "name": "x", - "unit": "micrometer", - "type": "cartesian", - "pixelsPerUnit": 1, - "description": "x-axis", - }, - { - "name": "y", - "unit": "micrometer", - "type": "cartesian", - "pixelsPerUnit": 1, - "description": "y-axis", - }, - ], - "origo": "top-left", - }, ) fname = re.split(r"[\W']+", str(Path(self.file_path).name)) # type: ignore fname = "_".join(fname[:-2]) # type: ignore outname = ( str(fname) + "_" + str(self.polygon_type.value) + "_" + str(i) + ".json" ) + if len(feature_collection.model_dump_json()) == 0: msg = "JSON file is empty" raise ValueError(msg) diff --git a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/utils.py b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/utils.py new file mode 100644 index 000000000..e5008f41e --- /dev/null +++ b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/utils.py @@ -0,0 +1,187 @@ +"""Ome micojson package.""" + +import json +import logging +import os +from collections.abc import Sequence +from pathlib import Path +from typing import Any +from microjson.tilemodel import TileJSON +from microjson.tilemodel import TileLayer +from microjson.tilemodel import TileModel +from microjson.tilewriter import TileWriter +from microjson.tilewriter import extract_fields_ranges_enums +from microjson.tilewriter import getbounds + + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def convert_microjson_tile_json(microjson_path: Path) -> None: + """Converts a MicroJSON file to TileJSON format. + + Args: + microjson_path: Path to the input MicroJSON file. + + Outputs: + - A `metadata.json` file with TileJSON metadata. + - A `tiles` directory containing generated PBF tile files. + """ + if microjson_path: + # Extract fields, ranges, enums from the provided MicroJSON + field_names, field_ranges, field_enums = extract_fields_ranges_enums( + microjson_path, + ) + + # Create a TileLayer including the extracted fields + vector_layers = [ + TileLayer( + id="extracted-layer", + fields=field_names, + minzoom=0, + maxzoom=10, + description="Layer with extracted fields", + fieldranges=field_ranges, + fieldenums=field_enums, + ), + ] + + # # create the tiles directory + out_dir = microjson_path.parent.joinpath( + Path(Path(microjson_path.name).stem).joinpath("tiles") + ) + + if not out_dir.exists(): + out_dir.mkdir(parents=True, exist_ok=True) + + # get bounds + maxbounds = getbounds(microjson_path) + + center = [ + 0, + (maxbounds[0] + maxbounds[2]) / 2, + (maxbounds[1] + maxbounds[3]) / 2, + ] + + # Instantiate TileModel with your settings + tile_model = TileModel( + tilejson="3.0.0", + tiles=["{z}/{x}/{y}.pbf"], + name="Example Tile Layer", + description="A TileJSON example incorporating MicroJSON data", + version="1.0.0", + attribution="Polus AI", + minzoom=0, + maxzoom=7, + bounds=maxbounds, + center=center, + vector_layers=vector_layers, + ) + + # Create the root model with your TileModel instance + tileobj = TileJSON(root=tile_model) + + tilepath = out_dir.joinpath("metadata.json") + with Path.open(tilepath, "w") as f: + f.write(tileobj.model_dump_json(indent=2)) + + # # Initialize the TileHandler + handler = TileWriter(tile_model, pbf=True) + os.chdir(tilepath.parent) + handler.microjson2tiles(microjson_path, validate=False) + + +def generate_feature_collection(): + return { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [46.0, 481.7], + [46.7, 481.0], + [47.0, 480.7], + [48.0, 480.7], + [49.0, 480.7], + [50.0, 480.7], + [50.7, 480.0], + [51.0, 479.7], + [52.0, 479.7], + [52.7, 479.0], + [53.0, 478.7], + [54.0, 478.7], + [55.0, 478.7], + [56.0, 478.7], + [57.0, 478.7], + [57.7, 478.0], + [57.7, 477.0], + [57.7, 476.0], + [57.7, 475.0], + [57.0, 474.3], + [56.7, 474.0], + [56.7, 473.0], + [56.7, 472.0], + [56.0, 471.3], + [55.7, 471.0], + [55.0, 470.3], + [54.7, 470.0], + [54.0, 469.3], + [53.7, 469.0], + [53.7, 468.0], + [53.0, 467.3], + [52.0, 467.3], + [51.0, 467.3], + [50.0, 467.3], + [49.0, 467.3], + [48.0, 467.3], + [47.3, 468.0], + [47.0, 468.3], + [46.0, 468.3], + [45.0, 468.3], + [44.0, 468.3], + [43.3, 469.0], + [43.0, 469.3], + [42.3, 470.0], + [42.3, 471.0], + [42.0, 471.3], + [41.3, 472.0], + [41.3, 473.0], + [41.0, 473.3], + [40.3, 474.0], + [40.3, 475.0], + [40.3, 476.0], + [40.3, 477.0], + [40.3, 478.0], + [41.0, 478.7], + [41.3, 479.0], + [41.3, 480.0], + [42.0, 480.7], + [42.3, 481.0], + [43.0, 481.7], + [44.0, 481.7], + [45.0, 481.7], + [46.0, 481.7], + ] + ], + }, + "properties": { + "Image": "p4_y1_r(289-375)_c0.ome.tif", + "X": 2650, + "Y": 2384, + "Label": 1, + }, + } + ], + } + + +def preview_data(out_dir: Path) -> None: + microjson_output = generate_feature_collection() + out_file = out_dir.joinpath("example_data.json") + + with open(out_file, "w") as json_file: + json.dump(microjson_output, json_file, indent=2) From fb2483a3eb3b962481b84c198afb0f850c7ad48d Mon Sep 17 00:00:00 2001 From: Hamdah Shafqat Abbasi Date: Fri, 11 Apr 2025 13:43:58 -0400 Subject: [PATCH 2/3] integrated nyxus features --- .../ome-to-microjson-tool/.bumpversion.cfg | 2 +- ...tionstomicojson.cwl => OmeToMicrojson.cwl} | 28 +- visualization/ome-to-microjson-tool/README.md | 29 +- visualization/ome-to-microjson-tool/VERSION | 2 +- visualization/ome-to-microjson-tool/ict.yaml | 79 +++- .../ome-to-microjson-tool/plugin.json | 105 +++-- .../ome-to-microjson-tool/pyproject.toml | 11 +- .../ome-to-microjson-tool/run-plugin.sh | 21 +- .../ome_to_microjson/__init__.py | 2 +- .../ome_to_microjson/__main__.py | 89 ++++- .../ome_to_microjson/ome_microjson.py | 363 +++++++++++------- .../visualization/ome_to_microjson/utils.py | 105 ++++- .../ome-to-microjson-tool/tests/conftest.py | 62 ++- .../ome-to-microjson-tool/tests/test_cli.py | 34 +- .../tests/test_ome_micojson.py | 218 +++++++---- 15 files changed, 780 insertions(+), 370 deletions(-) rename visualization/ome-to-microjson-tool/{convertbinarysegmentationstomicojson.cwl => OmeToMicrojson.cwl} (57%) diff --git a/visualization/ome-to-microjson-tool/.bumpversion.cfg b/visualization/ome-to-microjson-tool/.bumpversion.cfg index 5ef4604b8..56ae6bded 100644 --- a/visualization/ome-to-microjson-tool/.bumpversion.cfg +++ b/visualization/ome-to-microjson-tool/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.6 +current_version = 0.1.7-dev0 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/visualization/ome-to-microjson-tool/convertbinarysegmentationstomicojson.cwl b/visualization/ome-to-microjson-tool/OmeToMicrojson.cwl similarity index 57% rename from visualization/ome-to-microjson-tool/convertbinarysegmentationstomicojson.cwl rename to visualization/ome-to-microjson-tool/OmeToMicrojson.cwl index 490a08a63..cd82d9041 100644 --- a/visualization/ome-to-microjson-tool/convertbinarysegmentationstomicojson.cwl +++ b/visualization/ome-to-microjson-tool/OmeToMicrojson.cwl @@ -1,18 +1,30 @@ class: CommandLineTool cwlVersion: v1.2 inputs: + features: + inputBinding: + prefix: --features + type: string? filePattern: inputBinding: prefix: --filePattern - type: string? - inpDir: + type: string + intDir: inputBinding: - prefix: --inpDir + prefix: --intDir type: Directory + neighborDist: + inputBinding: + prefix: --neighborDist + type: string? outDir: inputBinding: prefix: --outDir type: Directory + pixelPerMicron: + inputBinding: + prefix: --pixelPerMicron + type: double? polygonType: inputBinding: prefix: --polygonType @@ -21,6 +33,14 @@ inputs: inputBinding: prefix: --preview type: boolean? + segDir: + inputBinding: + prefix: --segDir + type: Directory + tileJson: + inputBinding: + prefix: --tileJson + type: boolean? outputs: outDir: outputBinding: @@ -28,7 +48,7 @@ outputs: type: Directory requirements: DockerRequirement: - dockerPull: polusai/ome-to-microjson-tool:0.1.5-dev0 + dockerPull: polusai/ome-to-microjson-tool:0.1.7-dev0 InitialWorkDirRequirement: listing: - entry: $(inputs.outDir) diff --git a/visualization/ome-to-microjson-tool/README.md b/visualization/ome-to-microjson-tool/README.md index 1106760ba..6628b1edc 100644 --- a/visualization/ome-to-microjson-tool/README.md +++ b/visualization/ome-to-microjson-tool/README.md @@ -1,9 +1,6 @@ -# Ome-to-microjson-plugin(v0.1.6) +# Ome-to-microjson-plugin(0.1.7-dev0) -This plugin generate a microjson of polygon coordinates of binary or label segmentations - -This plugin uses [MICROJSON](https://github.com/bengtl/microjson/tree/dev) python library to generate polygon encodings for each object (Region of Interest) of binary segmented image in microjson format which can be used in [RENDER UI](https://render.ci.ncats.io/?imageUrl=https://files.scb-ncats.io/pyramids/Idr0033/precompute/41744/x(00-15)_y(01-24)_p0(1-9)_c(1-5)/) -application for visualization of microscopy images overlay with segmentations encodings. +This plugin generates polygon coordinates (rectangle or encoding) for objects in binary or label images and extracts Nyxus features for each object, saving them in a JSON-based format using the [MICROJSON](https://github.com/bengtl/microjson/tree/dev) Python library. The output can be visualized in the [RENDER UI](https://render.ci.ncats.io/?imageUrl=https://files.scb-ncats.io/pyramids/Idr0033/precompute/41744/x(00-15)_y(01-24)_p0(1-9)_c(1-5)/) application, enabling overlay of segmentation encodings on microscopy images Currently this plugin supports two Polygon types 1. rectangle @@ -23,6 +20,8 @@ Contact [Hamdah Shafqat Abbasi](mailto:hamdahshafqat.abbasi@nih.gov) for more in For more information on WIPP, visit the [official WIPP page](https://isg.nist.gov/deepzoomweb/software/wipp). + + ## Building To build the Docker image for the conversion plugin, run @@ -39,9 +38,13 @@ This plugin can take four input arguments and one output argument: | Name | Description | I/O | Type | |-------------------|-------------------------------------------------------|--------|--------------| -| `inpDir` | Input directory | Input | string | +| `intDir` | Input directory containing intensity images | Input | string | +| `segpDir` | Input directory containing binary or label images | Input | string | | `filePattern` | Pattern to parse image filenames | Input | string | | `polygonType` | Polygon type (rectangle, encoding) | Input | enum | +| `features` | [Nyxus Features](https://pypi.org/project/nyxus/) | Input | string | +| `neighborDist` | Distance between two neighbor objects | Input | integer | +| `pixelPerMicron` | Pixel Size in micrometer | Input | float | | `outDir` | Output directory | Output | string | | `preview` | Generate a JSON file with outputs | Output | JSON | @@ -50,10 +53,16 @@ This plugin can take four input arguments and one output argument: ### Run the Docker Container ```bash -docker run -v /data:/data polusai/ome-to-microjson-plugin:0.1.6-dev \ - --inpDir /data/input \ - --filePattern ".*.ome.tif" \ +docker run -v /data:/data polusai/ome-to-microjson-tool:0.1.7-dev0 \ + --intDir /data/input \ + --segDir /data/segmentations \ + --filePattern "x{x:d+}_y{y:d+}_p{p:d+}_c{c:d+}.ome.tif" \ --polygonType "encoding" \ + --features "ALL" \ + --neighborDist 5.0 \ + --polygonType "encoding" \ + --pixelPerMicron 1.0 \ --outDir /data/output \ - --preview + --tileJson + ``` diff --git a/visualization/ome-to-microjson-tool/VERSION b/visualization/ome-to-microjson-tool/VERSION index c946ee616..be8ec7f46 100644 --- a/visualization/ome-to-microjson-tool/VERSION +++ b/visualization/ome-to-microjson-tool/VERSION @@ -1 +1 @@ -0.1.6 +0.1.7-dev0 diff --git a/visualization/ome-to-microjson-tool/ict.yaml b/visualization/ome-to-microjson-tool/ict.yaml index 26a1b4f6f..14233678b 100644 --- a/visualization/ome-to-microjson-tool/ict.yaml +++ b/visualization/ome-to-microjson-tool/ict.yaml @@ -1,22 +1,28 @@ author: - Hamdah Shafqat contact: hamdahshafqat.abbasi@nih.gov -container: polusai/ome-to-microjson-tool:0.1.5-dev0 +container: polusai/ome-to-microjson-tool:0.1.7-dev0 description: This plugin create a microjson of polygon coordinates (rectangle, encodings) of binary segmentations. entrypoint: python3 -m polus.images.visualization.ome_to_microjson inputs: -- description: Input image collection to be processed by this plugin. +- description: Path to input directory containing intensity images. format: - - collection - name: inpDir + - genericData + name: intDir + required: true + type: path +- description: Path to input directory containing binary or label images. + format: + - genericData + name: segDir required: true type: path - description: Filename pattern used to separate data. format: - string name: filePattern - required: false + required: true type: string - description: Select polygon type from [rectangle, encoding]. format: @@ -24,27 +30,55 @@ inputs: name: polygonType required: true type: string +- description: TileJSON layer + format: + - boolean + name: tileJson + required: false + type: boolean +- description: Features or feature groups to be extracted by nyxus plugin + format: + - array + name: features + required: false + type: array +- description: Pixel distance between neighboring cells + format: + - integer + name: neighborDist + required: false + type: number +- description: Pixel size in micrometer + format: + - number + name: pixelPerMicron + required: false + type: number - description: Generate an output preview. format: - boolean name: preview required: false type: boolean -name: polusai/Convertbinarysegmentationstomicojson +name: polusai/OmeToMicrojson outputs: - description: Output collection. format: - - collection + - genericData name: outDir required: true type: path -repository: https://github.com/labshare/polus-plugins +repository: https://github.com/polusai/image-tools specVersion: 1.0.0 -title: Convert binary segmentations to micojson +title: Ome To Microjson ui: -- description: Input image collection to be processed by this plugin. - key: inputs.inpDir - title: Input collection +- description: Path to input directory containing intensity images + key: inputs.intDir + title: Intensity Collection + type: path +- description: Path to input directory containing binary or label images + key: inputs.segDir + title: Segmentation Collection type: path - description: Filename pattern used to separate data. key: inputs.filePattern @@ -57,8 +91,27 @@ ui: key: inputs.polygonType title: Type of Polygon type: select +- default: false + description: TileJSON layer + key: inputs.tileJson + title: tileJson + type: checkbox +- description: Features or feature groups to be extracted by nyxus plugin + key: inputs.features + title: features + type: text +- default: 5 + description: Pixel distance between neighboring cells + key: inputs.neighborDist + title: neighborDist + type: number +- default: 1.0 + description: Pixel size in micrometer + key: inputs.pixelPerMicron + title: pixelPerMicron + type: number - description: Generate an output preview. key: inputs.preview title: Preview example output of this plugin type: checkbox -version: 0.1.5-dev0 +version: 0.1.7-dev0 diff --git a/visualization/ome-to-microjson-tool/plugin.json b/visualization/ome-to-microjson-tool/plugin.json index 46b89c45f..2cde32b3e 100644 --- a/visualization/ome-to-microjson-tool/plugin.json +++ b/visualization/ome-to-microjson-tool/plugin.json @@ -1,14 +1,14 @@ { - "name": "Ome_to_microjson", - "version": "0.1.6", - "title": "Convert binary segmentations to micojson", + "name": "Ome To Microjson", + "version": "0.1.7-dev0", + "title": "Ome To Microjson", "description": "This plugin create a microjson of polygon coordinates (rectangle, encodings) of binary segmentations.", "author": "Hamdah Shafqat abbasi (hamdahshafqat.abbasi@nih.gov)", "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", - "repository": "https://github.com/labshare/polus-plugins", + "repository": "https://github.com/polusai/image-tools", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "polusai/ome-to-microjson-tool:0.1.6", + "containerId": "polusai/ome-to-microjson-tool:0.1.7-dev0", "baseCommand": [ "python3", "-m", @@ -16,18 +16,25 @@ ], "inputs": [ { - "name": "inpDir", - "type": "collection", - "title": "Input collection", - "description": "Input image collection to be processed by this plugin.", - "required": "True" + "name": "intDir", + "type": "genericData", + "title": "Intensity Collection", + "description": "Path to input directory containing intensity images.", + "required": true + }, + { + "name": "segDir", + "type": "genericData", + "title": "Segmentation Collection", + "description": "Path to input directory containing binary or label images.", + "required": true }, { "name": "filePattern", "type": "string", "title": "Filename pattern", "description": "Filename pattern used to separate data.", - "required": "False" + "required": true }, { "name": "polygonType", @@ -40,7 +47,7 @@ "encoding" ] }, - "required": "True" + "required": true }, { "name": "tileJson", @@ -48,35 +55,69 @@ "type": "boolean", "required": false }, + { + "name": "features", + "type": "array", + "description": "Features or feature groups to be extracted by nyxus plugin", + "required": false, + "options": { + "items": { + "type": "string", + "title": "Features or feature groups to be extracted by nyxus plugin", + "widget": "string" + } + } + }, + { + "name": "neighborDist", + "description": "Pixel distance between neighboring cells", + "type": "integer", + "default": 5, + "required": false + }, + { + "name": "pixelPerMicron", + "description": "Pixel size in micrometer", + "type": "number", + "default": 1.0, + "required": false + }, { "name": "preview", "type": "boolean", "title": "Preview", "description": "Generate an output preview.", - "required": "False" + "required": false } ], "outputs": [ { "name": "outDir", - "type": "collection", + "type": "genericData", "description": "Output collection." } ], "ui": [ { - "key": "inputs.inpDir", - "type": "collection", - "title": "Input collection", - "description": "Input image collection to be processed by this plugin.", - "required": "True" + "key": "inputs.intDir", + "type": "genericData", + "title": "Intensity Collection", + "description": "Path to input directory containing intensity images", + "required": true + }, + { + "key": "inputs.segDir", + "type": "genericData", + "title": "Segmentation Collection", + "description": "Path to input directory containing binary or label images", + "required": true }, { "key": "inputs.filePattern", "type": "string", "title": "Filename pattern", "description": "Filename pattern used to separate data.", - "required": "False" + "required": true }, { "key": "inputs.polygonType", @@ -84,7 +125,7 @@ "title": "Type of Polygon", "description": "Select polygon type from [rectangle, encoding].", "default": "encoding", - "required": "True" + "required": true }, { "key": "inputs.tileJson", @@ -92,12 +133,30 @@ "description": "TileJSON layer", "default": false }, + { + "key": "inputs.features", + "title": "features", + "description": "Features or feature groups to be extracted by nyxus plugin", + "default": "ALL" + }, + { + "key": "inputs.neighborDist", + "title": "neighborDist", + "description": "Pixel distance between neighboring cells", + "default": 5 + }, + { + "key": "inputs.pixelPerMicron", + "title": "pixelPerMicron", + "description": "Pixel size in micrometer", + "default": 1.0 + }, { "key": "inputs.preview", "type": "boolean", "title": "Preview example output of this plugin", "description": "Generate an output preview.", - "required": "False" + "required": false } ] -} \ No newline at end of file +} diff --git a/visualization/ome-to-microjson-tool/pyproject.toml b/visualization/ome-to-microjson-tool/pyproject.toml index 5eb02e7ac..60336e6d7 100644 --- a/visualization/ome-to-microjson-tool/pyproject.toml +++ b/visualization/ome-to-microjson-tool/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "polus-images-visualization-ome-to-microjson" -version = "0.1.6" +version = "0.1.7-dev0" description = "This plugin create a microjson of polygon coordinates of binary and label segmentations" authors = ["Hamdah Shafqat abbasi "] readme = "README.md" @@ -8,18 +8,15 @@ packages = [{include = "polus", from = "src"}] [tool.poetry.dependencies] python = ">=3.9.15,<3.12" -bfio = {version = "2.3.3", extras = ["all"]} +bfio = {version = "2.4.7", extras = ["all"]} typer = "^0.7.0" +nyxus = "^0.8.3" scipy = ">=1.8,<1.9.2" filepattern = "^2.0.1" tqdm = "^4.65.0" microjson = "^0.4.1" -fastapi = "^0.103.1" -llvmlite = "^0.40.1" -vaex = "^4.17.0" +pandas = "^2.2.3" scikit-image = "^0.20.0" -memory-profiler = "^0.61.0" -pyarrow = ">=16.0,<17.0" [tool.poetry.group.dev.dependencies] bump2version = "^1.0.1" diff --git a/visualization/ome-to-microjson-tool/run-plugin.sh b/visualization/ome-to-microjson-tool/run-plugin.sh index 7139b9ae7..86c0264d3 100755 --- a/visualization/ome-to-microjson-tool/run-plugin.sh +++ b/visualization/ome-to-microjson-tool/run-plugin.sh @@ -5,17 +5,24 @@ datapath=$(readlink --canonicalize data) echo ${datapath} # Inputs -inp_dir=${datapath}/input -file_pattern=".*.ome.tif" +int_dir=${datapath}/input +seg_dir=${datapath}/segmentations +file_pattern="x{x:d+}_y{y:d+}_p{p:d+}_c{c:d+}.ome.tif" polygon_type='encoding' +features="MEAN,MEDIAN" +neighbor_dist=5 +pixel_per_micron=1.0 out_dir=${datapath}/output -# #Show the help options -# #docker run polusai/ome-to-microjson-plugin:${version} docker run -v ${datapath}:${datapath} \ - polusai/ome-to-microjson-plugin:${version} \ - --inpDir ${inp_dir} \ + polusai/ome-to-microjson-tool:${version} \ + --intDir ${inp_dir} \ + --segDir ${seg_dir} \ --filePattern ${file_pattern} \ --polygonType ${polygon_type} \ - --outDir ${out_dir} + --features ${features} \ + --neighborDist ${neighbor_dist} \ + --pixelPerMicron ${pixel_per_micron} \ + --outDir ${out_dir} \ + --tileJson diff --git a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__init__.py b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__init__.py index de7c39dab..5a8a0087c 100644 --- a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__init__.py +++ b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__init__.py @@ -1,3 +1,3 @@ """Ome micojson package.""" -__version__ = "0.1.6" +__version__ = "0.1.7-dev0" diff --git a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__main__.py b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__main__.py index aa01ae9fb..fd24fdd52 100644 --- a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__main__.py +++ b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/__main__.py @@ -1,18 +1,20 @@ """Ome micojson package.""" import logging +import re import shutil import time import warnings from concurrent.futures import ProcessPoolExecutor from os import environ from pathlib import Path +from typing import Optional + import filepattern as fp import polus.images.visualization.ome_to_microjson.ome_microjson as sm import polus.images.visualization.ome_to_microjson.utils as ut import typer from tqdm import tqdm -from typing import Optional warnings.filterwarnings("ignore") @@ -40,23 +42,25 @@ def generate_preview( @app.command() -def main( - inp_dir: Path = typer.Option( +def main( # noqa: PLR0913 + int_dir: Path = typer.Option( ..., - "--inpDir", - "-i", + "--intDir", help="Path to input directory containing binary images.", ), + seg_dir: Path = typer.Option( + ..., + "--segDir", + help="Input label images", + ), file_pattern: str = typer.Option( ".*", "--filePattern", - "-f", help="Filename pattern used to separate data.", ), polygon_type: sm.PolygonType = typer.Option( ..., "--polygonType", - "-t", help="Desired polygon type.", ), tile_json: Optional[bool] = typer.Option( @@ -64,63 +68,106 @@ def main( "--tileJson", help="Tile JSON layer", ), + features: Optional[list[str]] = typer.Option( + ["ALL"], + "--features", + help="Nyxus features to be extracted", + ), + neighbor_dist: Optional[int] = typer.Option( + 5, + "--neighborDist", + help="Number of Pixels between Neighboring cells", + ), + pixel_per_micron: Optional[float] = typer.Option( + 1.0, + "--pixelPerMicron", + help="Number of pixels per micrometer", + ), out_dir: Path = typer.Option( ..., "--outDir", - "-o", help="Output collection.", ), preview: bool = typer.Option( False, "--preview", - "-p", help="Generate preview of expected outputs.", ), ) -> None: """Convert binary segmentations to micojson.""" - logger.info(f"inpDir: {inp_dir}") + logger.info(f"intDir: {int_dir}") + logger.info(f"segDir: {seg_dir}") logger.info(f"filePattern: {file_pattern}") logger.info(f"polygonType: {polygon_type}") logger.info(f"tile_json = {tile_json}") + logger.info(f"features = {features}") + logger.info(f"neighborDist = {neighbor_dist}") + logger.info(f"pixelPerMicron = {pixel_per_micron}") logger.info(f"outDir: {out_dir}") starttime = time.time() - if not inp_dir.exists(): - msg = "inpDir does not exist" - raise ValueError(msg, inp_dir) + if not int_dir.exists(): + msg = "intDir does not exist" + raise ValueError(msg, int_dir) + + if not seg_dir.exists(): + msg = "segDir does not exist" + raise ValueError(msg, seg_dir) if not out_dir.exists(): msg = "Create outDir as it does not exist" Path(out_dir).mkdir(exist_ok=True, parents=True) - files = fp.FilePattern(inp_dir, file_pattern) + int_images = fp.FilePattern(int_dir, file_pattern) + seg_images = fp.FilePattern(seg_dir, file_pattern) - if not len(files) > 0: + if not len(int_images) > 0: msg = "No image files are detected. Please check filepattern again!" raise ValueError(msg) + if not len(seg_images) > 0: + msg = "No label image files are detected. Please check filepattern again!" + raise ValueError(msg) + + if features is not None: + features = [re.split(",", f) for f in features][0] + else: + features = ["ALL"] + + invalid_features = [ + f for f in features if f not in ut.FEATURE_GROUP.union(ut.FEATURE_LIST) + ] + if invalid_features: + error_msg = f"Invalid feature selections: {invalid_features}" + raise ValueError(error_msg) + + features = [(f"*{f}*") if f in ut.FEATURE_GROUP else f for f in features] + if preview: ut.preview_data(out_dir) logger.info(f"generating preview data in {out_dir}") else: - with ProcessPoolExecutor(max_workers=sm.NUM_THREADS) as executor: - for _, f in enumerate(tqdm(files())): + for _, f in enumerate(tqdm(seg_images())): + i_image = int_images.get_matching(**dict(f[0].items())) model = sm.OmeMicrojsonModel( out_dir=out_dir, - file_path=str(f[1][0]), + label_path=str(f[1][0]), + int_path=str(i_image[0][1][0]), polygon_type=polygon_type, tile_json=tile_json, + features=features, + neighbor_dist=neighbor_dist, + pixel_per_micron=pixel_per_micron, ) executor.submit(model.write_single_json()) endtime = (time.time() - starttime) / 60 + time_per_file = endtime / len(seg_images) logger.info(f"Total time taken for execution: {endtime:.4f} minutes") - logger.info( - f"Total time taken for a single file: {endtime/len(files):.4f} minutes" - ) + logger.info(f"File processing time: {time_per_file:.4f} min") if __name__ == "__main__": diff --git a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/ome_microjson.py b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/ome_microjson.py index fb49b6c58..73f25d5ed 100644 --- a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/ome_microjson.py +++ b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/ome_microjson.py @@ -13,24 +13,25 @@ from pathlib import Path from sys import platform from typing import Any -from typing import Union from typing import Optional +from typing import Union import filepattern as fp import microjson.model as mj import numpy as np +import pandas as pd +import polus.images.visualization.ome_to_microjson.utils as ut import scipy -import vaex from bfio import BioReader +from nyxus import Nyxus from skimage import measure from skimage import morphology -import polus.images.visualization.ome_to_microjson.utils as ut logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -TILE_SIZE = 512 +TILE_SIZE = 2048 if platform == "linux" or platform == "linux2": NUM_THREADS = len(os.sched_getaffinity(0)) # type: ignore else: @@ -54,24 +55,33 @@ class OmeMicrojsonModel: file_name: Binary image filename """ - def __init__( + def __init__( # noqa: PLR0913 self, out_dir: Path, - file_path: str, + label_path: str, + int_path: str, polygon_type: PolygonType, + features: list[str], tile_json: Optional[bool] = False, + neighbor_dist: Optional[int] = 5, + pixel_per_micron: Optional[float] = 1.0, ) -> None: """Convert each object polygons (series of points, rectangle) to microjson.""" self.out_dir = out_dir - self.file_path = file_path + self.label_path = label_path + self.int_path = int_path self.polygon_type = polygon_type self.tile_json = tile_json + self.features = features + self.neighbor_dist = neighbor_dist + self.pixel_per_micron = pixel_per_micron self.min_label_length = 1 self.min_unique_labels = 0 - self.max_unique_labels = 2 - self.br = BioReader(self.file_path) + self.max_unique_labels = 1 + self.br = BioReader(self.label_path) + self.br_int = BioReader(self.int_path) - def _tile_read(self) -> None: + def _tile_read(self) -> None: # noqa: C901 """Reading of Image in a tile and compute encodings for it.""" with ThreadPoolExecutor(max_workers=NUM_THREADS) as executor: idx = 0 @@ -82,58 +92,84 @@ def _tile_read(self) -> None: range(0, self.br.X, TILE_SIZE), ), ): - y_max = min([self.br.Y, y + TILE_SIZE]) x_max = min([self.br.X, x + TILE_SIZE]) - image = self.br[y:y_max, x:x_max, z : z + 1] - - unique_labels = len(np.unique(image)) + mask_image = self.br[y:y_max, x:x_max, z : z + 1] + int_image = self.br_int[y:y_max, x:x_max, z : z + 1] + unique_labels = np.unique(mask_image) + unique_labels = len(unique_labels[unique_labels != 0]) if unique_labels >= self.max_unique_labels: if unique_labels == self.max_unique_labels: msg = f"Binary image detected : tile {i}" logger.info(msg) - label_image = morphology.label(image) + label_image = morphology.label(mask_image) else: msg = f"Label image detected : tile {i}" - label_image = image + label_image = mask_image logger.info(msg) - if self.polygon_type != PolygonType.RECTANGLE: - future = executor.submit( - self.segmentations_encodings, - label_image, - x, - y, - ) - if as_completed(future): # type: ignore - label, coordinates = future.result() - if len(label) and len(coordinates) > 0: - label = [i + idx for i in range(1, len(label) + 1)] - idx = 0 - if len(label) == 1: - idx += label[0] - else: - idx += label[-1] - self.polygons_to_microjson(i, label, coordinates) + if self.polygon_type != PolygonType.RECTANGLE: + future = executor.submit( + self.segmentations_encodings, + label_image, + x, + y, + ) + + future1 = executor.submit( + self.extract_nyxusfeatures, + int_image, + label_image, + ) + if as_completed(future): # type: ignore + label, coordinates = future.result() + if as_completed(future1): # type: ignore + features = future1.result() - else: - future = executor.submit( - self.rectangular_polygons, # type: ignore - label_image, - x, - y, - ) - if as_completed(future): # type: ignore - label, coordinates = future.result() if len(label) and len(coordinates) > 0: label = [i + idx for i in range(1, len(label) + 1)] + idx = 0 if len(label) == 1: idx += label[0] else: idx += label[-1] - self.polygons_to_microjson(i, label, coordinates) + self.polygons_to_microjson( + i, + label, + coordinates, + features, + ) + else: + future = executor.submit( + self.rectangular_polygons, # type: ignore + label_image, + x, + y, + ) + future1 = executor.submit( # type: ignore + self.extract_nyxusfeatures, + int_image, + label_image, + ) + if as_completed(future): # type: ignore + label, coordinates = future.result() + if as_completed(future1): # type: ignore + features = future1.result() + if len(label) and len(coordinates) > 0: + label = [i + idx for i in range(1, len(label) + 1)] + idx = 0 + if len(label) == 1: + idx += label[0] + else: + idx += label[-1] + self.polygons_to_microjson( + i, + label, + coordinates, + features, + ) def get_line_number(self, filename: str, target_string: str) -> Union[int, None]: """Parsing microjsons.""" @@ -155,90 +191,83 @@ def cleaning_directories(self) -> None: def write_single_json(self) -> None: """Combine microjsons from tiled images into combined json file.""" + # Create temp directory first + temp_dir = Path(self.out_dir, "tmp") + temp_dir.mkdir(parents=True, exist_ok=True) + + # Process tiles, output to temp dir self._tile_read() - out_combined = Path(self.out_dir, "tmp") + out_file = ( - Path(self.file_path).name.rsplit(".", 1)[0] + Path(self.label_path).name.rsplit(".", 1)[0] + "_" + str(self.polygon_type.value) + ".json" ) - if not out_combined.exists(): - out_combined.mkdir(exist_ok=True) - fname = re.split(r"[\W']+", str(Path(self.file_path).name))[0] - files = fp.FilePattern(self.out_dir, f"{fname}.*json") + + # Get basename without extension + fname = re.split(r"[\W']+", str(Path(self.label_path).name))[0] + + # Look for files in temp directory, not out_dir + files = fp.FilePattern(temp_dir, f"{fname}.*json") + if len(files) > 1: - with Path.open(Path(out_combined, out_file), "w") as fw: + # Create final output file in main output directory + final_out_file = Path(self.out_dir, out_file) + with Path.open(final_out_file, "w") as fw: for i, fl in zip(range(1, len(files) + 1), files()): file = fl[1][0] total_lines = 0 - with Path.open(Path(file)) as file: - for _ in file: + with Path.open(file) as f: + for _ in f: total_lines += 1 if total_lines is not None: - outname = re.split(r"[_\.]+", file.name)[:-2] - outname = "_".join(outname) + ".json" # type: ignore - df = Path.open(Path(file.name)) - data = df.readlines() - if i == 1: - endline = data[-3].rstrip() + "," - sfdata = data[:-3] + [endline] - elif i > 1 and i < len(files): - endline = data[-3].rstrip() + "," - sfdata = data[3:-3] + [endline] - else: - sfdata = data[3:] - fw.writelines(sfdata) - Path(file.name).unlink() + with Path.open(file) as df: + data = df.readlines() + if i == 1: + endline = data[-3].rstrip() + "," + sfdata = data[:-3] + [endline] + elif i > 1 and i < len(files): + endline = data[-3].rstrip() + "," + sfdata = data[3:-3] + [endline] + else: + sfdata = data[3:] + fw.writelines(sfdata) else: msg = "Invalid Microjson file!!! Please do check it again" - raise ValueError( - msg, - ) - - self.cleaning_directories() - outname = self.out_dir.joinpath(out_file) + raise ValueError(msg) + elif len(files) == 1: + # If only one file, just move it to the final location + file = files()[0][1][0] + shutil.copy(file, Path(self.out_dir, out_file)) + + # Clean up temp directory at the end + if temp_dir.exists(): + shutil.rmtree(temp_dir) + + # Handle tile_json conversion if needed + outname = Path(self.out_dir, out_file) if self.tile_json: logger.info(f"Generating tileJSON: {outname}") ut.convert_microjson_tile_json(Path(outname)) - def segmentations_encodings( + def extract_nyxusfeatures( self, + int_image: np.ndarray, label_image: np.ndarray, - x: int, - y: int, - ) -> tuple[list[int], list[list[tuple[float, float]]]]: - """Calculate object boundaries as series of vertices/points forming a polygon.""" - label, coordinates = [], [] - unique_labels = np.unique(label_image) + ) -> pd.DataFrame: + """Calculate nyxus features for objects.""" + nyx = Nyxus(self.features) - for i in unique_labels: - if i == 0: # Skip background - continue - mask = np.zeros_like(label_image, dtype=np.uint8) - mask[label_image == i] = 1 - contour_thresh = 0.0 - contour = measure.find_contours(mask, contour_thresh) - - if ( - len(contour) >= self.min_unique_labels - and len(contour) < self.max_unique_labels - and (contour and len(contour[0]) >= self.min_label_length) - ): - # Take the first contour (assuming one per object) - contour_array = np.flip(contour, axis=1) - seg_encodings = contour_array.ravel().tolist() - poly = [ - (xi + x, yi + y) - for xi, yi in zip(seg_encodings[1::2], seg_encodings[::2]) - ] - if poly and poly[0] != poly[-1]: - poly.append(poly[0]) + nyx_params = { + "neighbor_distance": self.neighbor_dist, + "pixels_per_micron": self.pixel_per_micron, + "n_feature_calc_threads": 4, + } - label.append(i) - coordinates.append(poly) + nyx.set_params(**nyx_params) - return label, coordinates + return nyx.featurize(int_image, label_image) def rectangular_polygons( self, @@ -272,58 +301,106 @@ def rectangular_polygons( return label, coordinates - def polygons_to_microjson( # noqa: PLR0915 + def segmentations_encodings( + self, + label_image: np.ndarray, + x: int, + y: int, + ) -> tuple[list[int], list[list[tuple[float, float]]]]: + """Calculate object boundaries as series of vertices/points forming a polygon. + + Args: + label_image: NumPy array containing labeled regions + x: X-coordinate offset + y: Y-coordinate offset + + Returns: + tuple: (labels, coordinates) where labels is a list of label IDs + and coordinates is a list of polygon coordinates + """ + labels = [] + coordinates = [] + unique_labels = np.unique(label_image) + unique_labels = unique_labels[unique_labels > 0] # Skip background label + + for label_id in unique_labels: + # Create binary mask for current label + mask = (label_image == label_id).astype(np.uint8) + + # Find contours with a reasonable threshold + contours = measure.find_contours(mask, level=0.5) + contour_len = 3 + + if contours and len(contours[0]) >= contour_len: + contour = contours[0] + + # Convert contour to proper coordinate format + contour_array = np.flip(contour, axis=1) # Flip x,y to y,x + seg_encodings = contour_array.ravel().tolist() + + # Create polygon coordinates with offset + + poly = [ + (float(xi + x), float(yi + y)) + for xi, yi in zip(seg_encodings[1::2], seg_encodings[::2]) + ] + + # Ensure polygon is closed + if poly and poly[0] != poly[-1]: + poly.append(poly[0]) + + labels.append(int(label_id)) # Ensure integer label + coordinates.append(poly) + + return labels, coordinates + + def polygons_to_microjson( self, i: int, label: list[int], coordinates: list[Any], + features: pd.DataFrame, ) -> None: # : 183 """Create microjson overlays in JSON Format.""" x_dimension = np.repeat(self.br.X, len(label)) y_dimension = np.repeat(self.br.Y, len(label)) - filename = Path(self.file_path) + filename = Path(self.label_path) image_name = np.repeat(filename.name, len(label)) + features.drop( + columns=["intensity_image", "mask_image", "ROI_label"], + inplace=True, + ) - data = vaex.from_arrays( - Image=image_name, - X=x_dimension, - Y=y_dimension, - Label=label, + data = pd.DataFrame( + { + "intensity_image": image_name, + "X": x_dimension, + "Y": y_dimension, + "Label": label, + "geometry_type": np.repeat("Polygon", len(label)), + "type": np.repeat("Feature", len(label)), + }, + ) + + combined = pd.merge( + data, + features, + how="left", + left_index=True, + right_index=True, ) - data["geometry_type"] = np.repeat("Polygon", data.shape[0]) - data["type"] = np.repeat("Feature", data.shape[0]) - - varlist = [ - "Image", - "X", - "Y", - "Label", - "geometry_type", - "type", - ] - - if list(data.columns) != varlist: - msg = "Invalid vaex dataframe!! Please do check path again" - raise ValueError(msg) if data.shape[0] == 0: - msg = "Invalid vaex dataframe!! Please do check path again" + msg = "Invalid dataframe!! Please do check path again" raise ValueError(msg) - columns = list( - filter( - lambda feature: feature in ["Image", "X", "Y", "Label"], - data.get_column_names(), - ), - ) - - if len(columns) == 0: + if len(data.columns) == 0: msg = "Features with integer datatype do not exist" raise ValueError(msg) - features: list[mj.Feature] = [] - for (_, row), cor in zip(data.iterrows(), coordinates): # type: ignore - numerical = [{key: row[key]} for key in columns] + mj_features: list[mj.Feature] = [] + for (_, row), cor in zip(combined.iterrows(), coordinates): # type: ignore + numerical = [{key: row[key]} for key in combined.columns] properties = {} for sub_dict in numerical: @@ -343,18 +420,22 @@ def polygons_to_microjson( # noqa: PLR0915 geometry=geometry, properties=properties, ) - features.append(feature) + mj_features.append(feature) # Create a new FeatureCollection object feature_collection = mj.MicroFeatureCollection( type="FeatureCollection", - features=features, + features=mj_features, ) - fname = re.split(r"[\W']+", str(Path(self.file_path).name)) # type: ignore + temp_dir = Path(self.out_dir, "tmp") + temp_dir.mkdir(parents=True, exist_ok=True) + + fname = re.split(r"[\W']+", str(Path(self.int_path).name)) # type: ignore fname = "_".join(fname[:-2]) # type: ignore outname = ( str(fname) + "_" + str(self.polygon_type.value) + "_" + str(i) + ".json" ) + outname = f"{temp_dir}/{outname}" if len(feature_collection.model_dump_json()) == 0: msg = "JSON file is empty" diff --git a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/utils.py b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/utils.py index e5008f41e..3be3147b9 100644 --- a/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/utils.py +++ b/visualization/ome-to-microjson-tool/src/polus/images/visualization/ome_to_microjson/utils.py @@ -3,9 +3,9 @@ import json import logging import os -from collections.abc import Sequence from pathlib import Path -from typing import Any + +import microjson.model as mj from microjson.tilemodel import TileJSON from microjson.tilemodel import TileLayer from microjson.tilemodel import TileModel @@ -13,11 +13,98 @@ from microjson.tilewriter import extract_fields_ranges_enums from microjson.tilewriter import getbounds - logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) +FEATURE_GROUP = { + "ALL_INTENSITY", + "ALL_MORPHOLOGY", + "BASIC_MORPHOLOGY", + "ALL_GLCM", + "ALL_GLRLM", + "ALL_GLSZM", + "ALL_GLDM", + "ALL_NGTDM", + "ALL_EASY", + "ALL", +} +FEATURE_LIST = { + "INTEGRATED_INTENSITY", + "MEAN", + "MAX", + "MEDIAN", + "STANDARD_DEVIATION", + "MODE", + "SKEWNESS", + "KURTOSIS", + "HYPERSKEWNESS", + "HYPERFLATNESS", + "MEAN_ABSOLUTE_DEVIATION", + "ENERGY", + "ROOT_MEAN_SQUARED", + "ENTROPY", + "UNIFORMITY", + "UNIFORMITY_PIU", + "P01", + "P10", + "P25", + "P75", + "P90", + "P99", + "INTERQUARTILE_RANGE", + "ROBUST_MEAN_ABSOLUTE_DEVIATION", + "MASS_DISPLACEMENT", + "AREA_PIXELS_COUNT", + "COMPACTNESS", + "BBOX_YMIN", + "BBOX_XMIN", + "BBOX_HEIGHT", + "BBOX_WIDTH", + "MINOR_AXIS_LENGTH", + "MAGOR_AXIS_LENGTH", + "ECCENTRICITY", + "ORIENTATION", + "ROUNDNESS", + "NUM_NEIGHBORS", + "PERCENT_TOUCHING", + "EXTENT", + "CONVEX_HULL_AREA", + "SOLIDITY", + "PERIMETER", + "EQUIVALENT_DIAMETER", + "EDGE_MEAN", + "EDGE_MAX", + "EDGE_MIN", + "EDGE_STDDEV_INTENSITY", + "CIRCULARITY", + "EROSIONS_2_VANISH", + "EROSIONS_2_VANISH_COMPLEMENT", + "FRACT_DIM_BOXCOUNT", + "FRACT_DIM_PERIMETER", + "GLCM", + "GLRLM", + "GLSZM", + "GLDM", + "NGTDM", + "ZERNIKE2D", + "FRAC_AT_D", + "RADIAL_CV", + "MEAN_FRAC", + "GABOR", + "ALL_INTENSITY", + "ALL_MORPHOLOGY", + "BASIC_MORPHOLOGY", + "ALL_GLCM", + "ALL_GLRLM", + "ALL_GLSZM", + "ALL_GLDM", + "ALL_NGTDM", + "ALL_EASY", + "ALL", +} + + def convert_microjson_tile_json(microjson_path: Path) -> None: """Converts a MicroJSON file to TileJSON format. @@ -49,7 +136,7 @@ def convert_microjson_tile_json(microjson_path: Path) -> None: # # create the tiles directory out_dir = microjson_path.parent.joinpath( - Path(Path(microjson_path.name).stem).joinpath("tiles") + Path(Path(microjson_path.name).stem).joinpath("tiles"), ) if not out_dir.exists(): @@ -92,7 +179,8 @@ def convert_microjson_tile_json(microjson_path: Path) -> None: handler.microjson2tiles(microjson_path, validate=False) -def generate_feature_collection(): +def generate_feature_collection() -> mj.MicroFeatureCollection: + """Generate microjson feature collection.""" return { "type": "FeatureCollection", "features": [ @@ -165,7 +253,7 @@ def generate_feature_collection(): [44.0, 481.7], [45.0, 481.7], [46.0, 481.7], - ] + ], ], }, "properties": { @@ -174,14 +262,15 @@ def generate_feature_collection(): "Y": 2384, "Label": 1, }, - } + }, ], } def preview_data(out_dir: Path) -> None: + """Get Preview Data.""" microjson_output = generate_feature_collection() out_file = out_dir.joinpath("example_data.json") - with open(out_file, "w") as json_file: + with Path.open(out_file, "w") as json_file: json.dump(microjson_output, json_file, indent=2) diff --git a/visualization/ome-to-microjson-tool/tests/conftest.py b/visualization/ome-to-microjson-tool/tests/conftest.py index f69de7ba2..5f1990ad6 100644 --- a/visualization/ome-to-microjson-tool/tests/conftest.py +++ b/visualization/ome-to-microjson-tool/tests/conftest.py @@ -2,15 +2,17 @@ Set up all data used in tests. """ + import tempfile from pathlib import Path from typing import Union import numpy as np import pytest -import skimage as sk from polus.images.visualization.ome_to_microjson.ome_microjson import PolygonType +from skimage import filters from skimage import io +from skimage import measure TILE_SIZE = 1024 max_unique_labels = 2 @@ -33,6 +35,12 @@ def inp_dir() -> Union[str, Path]: return Path(tempfile.mkdtemp(dir=Path.cwd())) +@pytest.fixture() +def seg_dir() -> Union[str, Path]: + """Create directory for saving groundtruth labelled images.""" + return Path(tempfile.mkdtemp(dir=Path.cwd())) + + @pytest.fixture() def output_directory() -> Union[str, Path]: """Create output directory.""" @@ -48,21 +56,26 @@ def image_sizes(request: pytest.FixtureRequest) -> pytest.FixtureRequest: @pytest.fixture() def synthetic_images( inp_dir: Union[str, Path], + seg_dir: Union[str, Path], image_sizes: pytest.FixtureRequest, -) -> Union[str, Path]: +) -> tuple[Path, Path]: """Generate random synthetic images.""" + inp_dir = Path(inp_dir) + seg_dir = Path(seg_dir) for i in range(2): im = np.zeros((image_sizes, image_sizes)) - blobs = sk.data.binary_blobs( - length=image_sizes, - volume_fraction=0.01, - blob_size_fraction=0.03, - ) - im[blobs > 0] = 1 - binary_img = f"x01_y01_r{i}_c1.tif" - binary_img = Path(inp_dir, binary_img) # type: ignore - io.imsave(binary_img, im) - return inp_dir + points = image_sizes * np.random.random((2, 10**2)) + im[(points[0]).astype(int), (points[1]).astype(int)] = 1 + im = filters.gaussian(im, sigma=image_sizes / (20.0 * 10)) + blobs = im > im.mean() + lab_blobs = measure.label(blobs, background=0) + intname = f"y04_r{i}_c1.ome.tif" + segname = f"y04_r{i}_c1.ome.tif" + int_name = Path(inp_dir, intname) + seg_name = Path(seg_dir, segname) + io.imsave(int_name, im) + io.imsave(seg_name, lab_blobs) + return inp_dir, seg_dir @pytest.fixture(params=["rectangle", "encoding"]) @@ -71,31 +84,6 @@ def get_params(request: pytest.FixtureRequest) -> list[str]: return request.param -@pytest.fixture(params=[10000, 20000, 30000]) -def large_image_sizes(request: pytest.FixtureRequest) -> pytest.FixtureRequest: - """To get the parameter of the fixture.""" - return request.param - - -@pytest.fixture() -def large_synthetic_images( - inp_dir: Union[str, Path], - large_image_sizes: pytest.FixtureRequest, -) -> tuple[Union[str, Path], int]: - """Generate large random synthetic images.""" - im = np.zeros((large_image_sizes, large_image_sizes)) - blobs = sk.data.binary_blobs( - length=large_image_sizes, - volume_fraction=0.01, - blob_size_fraction=0.03, - ) - im[blobs > 0] = 1 - binary_img = "x01_y01_r1_c1.tif" - binary_img = Path(inp_dir, binary_img) # type: ignore - io.imsave(binary_img, im) - return inp_dir, large_image_sizes - - @pytest.fixture(params=[PolygonType.RECTANGLE, PolygonType.ENCODING]) def get_params_json(request: pytest.FixtureRequest) -> pytest.FixtureRequest: """To get the parameter of the ome to json.""" diff --git a/visualization/ome-to-microjson-tool/tests/test_cli.py b/visualization/ome-to-microjson-tool/tests/test_cli.py index 250a7df39..4880ff4b8 100644 --- a/visualization/ome-to-microjson-tool/tests/test_cli.py +++ b/visualization/ome-to-microjson-tool/tests/test_cli.py @@ -1,10 +1,14 @@ """Test Command line Tool.""" + import shutil from pathlib import Path from polus.images.visualization.ome_to_microjson.__main__ import app from typer.testing import CliRunner +intensity_dir = Path("/Users/abbasih2/Downloads/nyx/intensity") +segmentation_dir = Path("/Users/abbasih2/Downloads/nyx/segmentations") + def clean_directories() -> None: """Remove all temporary directories.""" @@ -16,14 +20,16 @@ def clean_directories() -> None: def test_cli(synthetic_images, output_directory, get_params) -> None: """Test the command line.""" runner = CliRunner() - inp_dir = synthetic_images + inp_dir, seg_dir = synthetic_images result = runner.invoke( app, [ - "--inpDir", + "--intDir", inp_dir, + "--segDir", + seg_dir, "--filePattern", - ".*", + "y04_r{r:d+}_c1.ome.tif", "--polygonType", get_params, "--outDir", @@ -33,25 +39,3 @@ def test_cli(synthetic_images, output_directory, get_params) -> None: assert result.exit_code == 0 clean_directories() - - -def test_cli_short(synthetic_images, output_directory, get_params) -> None: - """Test the command line.""" - runner = CliRunner() - - inp_dir = synthetic_images - - result = runner.invoke( - app, - [ - "-i", - inp_dir, - "-t", - get_params, - "-o", - output_directory, - ], - ) - - assert result.exit_code == 0 - clean_directories() diff --git a/visualization/ome-to-microjson-tool/tests/test_ome_micojson.py b/visualization/ome-to-microjson-tool/tests/test_ome_micojson.py index 2f49c5da2..e135c2c27 100644 --- a/visualization/ome-to-microjson-tool/tests/test_ome_micojson.py +++ b/visualization/ome-to-microjson-tool/tests/test_ome_micojson.py @@ -1,16 +1,16 @@ """Test Ome micojson package.""" + import ast import enum import json import shutil from pathlib import Path -import pytest +import filepattern as fp +import pandas as pd from bfio import BioReader -from memory_profiler import profile from polus.images.visualization.ome_to_microjson.ome_microjson import OmeMicrojsonModel from polus.images.visualization.ome_to_microjson.ome_microjson import PolygonType -from skimage import morphology def clean_directories() -> None: @@ -20,19 +20,30 @@ def clean_directories() -> None: shutil.rmtree(d) -def test_rectangular_polygons(synthetic_images: Path, output_directory: Path) -> None: +def test_rectangular_polygons( + synthetic_images: tuple[Path, Path], + output_directory: Path, +) -> None: """Testing of object boundries (rectangle vertices).""" - inp_dir = synthetic_images - for file in Path(inp_dir).iterdir(): + intensity_dir, segmentation_dir = synthetic_images + file_pattern = "y04_r{r:d+}_c1.ome.tif" + int_images = fp.FilePattern(intensity_dir, file_pattern) + seg_images = fp.FilePattern(segmentation_dir, file_pattern) + for _, f in enumerate(seg_images()): + i_image = int_images.get_matching(**dict(f[0].items())) + model = OmeMicrojsonModel( out_dir=output_directory, - file_path=file, + label_path=str(f[1][0]), + int_path=str(i_image[0][1][0]), polygon_type=PolygonType.RECTANGLE, + features=["MEAN", "MEDIAN"], ) x = 0 y = 0 - br = BioReader(file) - label_image = morphology.label(br.read()) + br = BioReader(str(f[1][0])) + # Get first Z-slice + label_image = br[0 : br.Y, 0 : br.X, 0:1] label, coordinates = model.rectangular_polygons(label_image, x, y) assert len(label) == len(coordinates) polygon_length = 5 @@ -41,90 +52,190 @@ def test_rectangular_polygons(synthetic_images: Path, output_directory: Path) -> def test_segmentations_encodings( - synthetic_images: Path, + synthetic_images: tuple[Path, Path], output_directory: Path, ) -> None: """Testing of object boundries (vertices).""" - inp_dir = synthetic_images - for file in Path(inp_dir).iterdir(): + intensity_dir, segmentation_dir = synthetic_images + file_pattern = "y04_r{r:d+}_c1.ome.tif" + int_images = fp.FilePattern(intensity_dir, file_pattern) + seg_images = fp.FilePattern(segmentation_dir, file_pattern) + for _, f in enumerate(seg_images()): + i_image = int_images.get_matching(**dict(f[0].items())) + model = OmeMicrojsonModel( out_dir=output_directory, - file_path=file, + label_path=str(f[1][0]), + int_path=str(i_image[0][1][0]), polygon_type=PolygonType.ENCODING, + features=["MEAN", "MEDIAN"], ) x = 0 y = 0 - br = BioReader(file) - label_image = morphology.label(br.read()) + br = BioReader(str(f[1][0])) + # Get first Z-slice + label_image = br[0 : br.Y, 0 : br.X, 0:1] label, coordinates = model.segmentations_encodings(label_image, x, y) assert len(label) == len(coordinates) clean_directories() def test__tile_read( - synthetic_images: Path, + synthetic_images: tuple[Path, Path], output_directory: Path, ) -> None: - """Testing of object boundries (vertices).""" - inp_dir = synthetic_images - for file in Path(inp_dir).iterdir(): + """Testing of tile reading functionality.""" + intensity_dir, segmentation_dir = synthetic_images + # Create temp directory + temp_dir = Path(output_directory, "tmp") + temp_dir.mkdir(exist_ok=True) + file_pattern = "y04_r{r:d+}_c1.ome.tif" + int_images = fp.FilePattern(intensity_dir, file_pattern) + seg_images = fp.FilePattern(segmentation_dir, file_pattern) + for _, f in enumerate(seg_images()): + i_image = int_images.get_matching(**dict(f[0].items())) model = OmeMicrojsonModel( out_dir=output_directory, - file_path=file, + label_path=str(f[1][0]), + int_path=str(i_image[0][1][0]), polygon_type=PolygonType.ENCODING, + features=["MEAN", "MEDIAN"], ) model._tile_read() - for jpath in list(Path(output_directory).rglob("*.json")): - with Path.open(jpath) as json_file: - json_data = json.load(json_file) - assert len(json_data) != 0 + + # Check if any JSON files were created + json_files = list(Path(output_directory).rglob("*.json")) + if json_files: + for jpath in json_files: + with Path.open(jpath) as json_file: + json_data = json.load(json_file) + assert len(json_data) != 0 clean_directories() def test_write_single_json( - synthetic_images: Path, + synthetic_images: tuple[Path, Path], output_directory: Path, ) -> None: """Testing of json outputs for tiled images.""" - inp_dir = synthetic_images - for file in Path(inp_dir).iterdir(): + intensity_dir, segmentation_dir = synthetic_images + # Create temp directory + temp_dir = Path(output_directory, "tmp") + temp_dir.mkdir(exist_ok=True) + file_pattern = "y04_r{r:d+}_c1.ome.tif" + int_images = fp.FilePattern(intensity_dir, file_pattern) + seg_images = fp.FilePattern(segmentation_dir, file_pattern) + for _, f in enumerate(seg_images()): + i_image = int_images.get_matching(**dict(f[0].items())) model = OmeMicrojsonModel( out_dir=output_directory, - file_path=file, + label_path=str(f[1][0]), + int_path=str(i_image[0][1][0]), polygon_type=PolygonType.ENCODING, + features=["MEAN", "MEDIAN"], ) model.write_single_json() - for jpath in list(Path(output_directory, "tmp").rglob("*.json")): + + # After write_single_json, files should be in main output_directory, not tmp + out_json_files = list(Path(output_directory).glob("*.json")) + assert len(out_json_files) > 0 + + for jpath in out_json_files: with Path.open(jpath) as json_file: json_data = json.load(json_file) assert len(json_data) != 0 + tmp_dir = Path(output_directory, "tmp") + assert not tmp_dir.exists() + + clean_directories() + + +def test_extract_nyxusfeatures( + synthetic_images: tuple[Path, Path], + output_directory: Path, +) -> None: + """Testing feature extraction using Nyxus.""" + intensity_dir, segmentation_dir = synthetic_images + # Create temp directory + temp_dir = Path(output_directory, "tmp") + temp_dir.mkdir(exist_ok=True) + file_pattern = "y04_r{r:d+}_c1.ome.tif" + int_images = fp.FilePattern(intensity_dir, file_pattern) + seg_images = fp.FilePattern(segmentation_dir, file_pattern) + for _, f in enumerate(seg_images()): + i_image = int_images.get_matching(**dict(f[0].items())) + model = OmeMicrojsonModel( + out_dir=output_directory, + label_path=str(f[1][0]), + int_path=str(i_image[0][1][0]), + polygon_type=PolygonType.ENCODING, + features=["MEAN", "MEDIAN"], + ) + + br = BioReader(str(f[1][0])) + br_int = BioReader(i_image[0][1][0]) + # Get first Z-slice + label_image = br[0 : br.Y, 0 : br.X, 0:1] + int_image = br_int[0 : br_int.Y, 0 : br_int.X, 0:1] + + features_df = model.extract_nyxusfeatures(int_image, label_image) + + # Check that features dataframe is not empty + assert not features_df.empty + # Check that it has required columns + assert "MEAN" in features_df.columns + assert "MEDIAN" in features_df.columns clean_directories() def test_polygons_to_microjson( - synthetic_images: Path, + synthetic_images: tuple[Path, Path], output_directory: Path, get_params_json: list[enum.Enum], ) -> None: """Testing of converting object boundries to microjson.""" - inp_dir = synthetic_images - for file in Path(inp_dir).iterdir(): + intensity_dir, segmentation_dir = synthetic_images + # Create temp directory + temp_dir = Path(output_directory, "tmp") + temp_dir.mkdir(exist_ok=True) + file_pattern = "y04_r{r:d+}_c1.ome.tif" + int_images = fp.FilePattern(intensity_dir, file_pattern) + seg_images = fp.FilePattern(segmentation_dir, file_pattern) + for _, f in enumerate(seg_images()): + i_image = int_images.get_matching(**dict(f[0].items())) model = OmeMicrojsonModel( out_dir=output_directory, - file_path=file, + label_path=str(f[1][0]), + int_path=str(i_image[0][1][0]), polygon_type=get_params_json, + features=["MEAN", "MEDIAN"], ) - br = BioReader(file) - label_image = morphology.label(br.read()) + br = BioReader(f[1][0]) + br_int = BioReader(i_image[0][1][0]) + + # Get first Z-slice + label_image = br[0 : br.Y, 0 : br.X, 0:1] + int_image = br_int[0 : br_int.Y, 0 : br_int.X, 0:1] + x = y = i = 0 if get_params_json == PolygonType.RECTANGLE: label, coordinates = model.rectangular_polygons(label_image, x, y) elif get_params_json == PolygonType.ENCODING: label, coordinates = model.segmentations_encodings(label_image, x, y) - model.polygons_to_microjson(i, label, coordinates) + # Extract features + features = model.extract_nyxusfeatures(int_image, label_image) + if features.empty and len(label) > 0: + data = { + "Label": label, + "MEAN": [100.0] * len(label), + "MEDIAN": [40.0] * len(label), + } + features = pd.DataFrame(data) + + model.polygons_to_microjson(i, label, coordinates, features) for jpath in list(Path(output_directory).rglob("*.json")): with Path.open(jpath) as json_file: @@ -132,38 +243,3 @@ def test_polygons_to_microjson( assert len(json_data) != 0 clean_directories() - - -fp = open("ome_microjson.log", "w+") # noqa: PTH123 SIM115 - - -@pytest.mark.skipif("not config.getoption('slow')") -@profile(stream=fp) -def test_memory_profiling( - large_synthetic_images: tuple[Path, int], - output_directory: Path, - get_params_json: list[enum.Enum], -) -> None: - """Test memory usage of creating microjsons of large images.""" - inp_dir, image_sizes = large_synthetic_images - - def memory_profile_func() -> None: - """To do memory profiling.""" - for file in Path(inp_dir).iterdir(): - model = OmeMicrojsonModel( - out_dir=output_directory, - file_path=file, - polygon_type=get_params_json, - ) - model.write_single_json() - - memory_profile_func() - logfile = Path(Path.cwd().joinpath("ome_microjson.log")) - with Path.open(logfile) as file: - lines = file.readlines() - for line in lines: - if "MiB" in line: - value = line.split()[1] - value = int(float(value)) # type: ignore - assert value > int(image_sizes) is False # type: ignore - clean_directories() From 729d9543f9f8210b4736c73e507d99bb3677c9d0 Mon Sep 17 00:00:00 2001 From: Hamdah Shafqat Abbasi Date: Fri, 11 Apr 2025 13:51:11 -0400 Subject: [PATCH 3/3] updating base container image --- visualization/ome-to-microjson-tool/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visualization/ome-to-microjson-tool/Dockerfile b/visualization/ome-to-microjson-tool/Dockerfile index 2392645f4..f5599e3a4 100644 --- a/visualization/ome-to-microjson-tool/Dockerfile +++ b/visualization/ome-to-microjson-tool/Dockerfile @@ -1,4 +1,4 @@ -FROM polusai/bfio:2.3.3 +FROM polusai/bfio:2.4.7 # environment variables defined in polusai/bfio ENV EXEC_DIR="/opt/executables"