diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml new file mode 100644 index 0000000..82fb066 --- /dev/null +++ b/.github/workflows/run_tests.yaml @@ -0,0 +1,46 @@ +name: Run Unit Tests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ${{ matrix.os }} + if: github.repository == 'cmlibs-python/cmlibs.exporter' + continue-on-error: true + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install required system libraries + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt install libosmesa6 libglu1-mesa -y + + - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} + shell: bash + run: | + python -m pip install --upgrade pip + if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then + PY_VERSION_NODOT=$(echo "${{ matrix.python-version }}" | tr -d '.') + WHEEL_URL="https://github.com/cmlibs/swigzinc/releases/download/osmesa/cmlibs_zinc-4.2.1-cp${PY_VERSION_NODOT}-cp${PY_VERSION_NODOT}-linux_x86_64.whl" + pip install "${WHEEL_URL}" + pip install -e ".[opengl_software]" + else + pip install -e ".[opengl_hardware]" + fi + + - name: Run unit tests + shell: bash + run: | + python -m unittest discover -s tests -v diff --git a/README.rst b/README.rst index cddf4c4..5407884 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,7 @@ To install the thumbnail exporter with support for software rendering install *c To force the use of the software renderer even when hardware rendering is available, set an environment variable like so:: - OC_EXPORTER_RENDERER=osmesa + CMLIBS_EXPORTER_RENDERER=osmesa either in the environment the exporter is run in or before calling the export thumbnail method. diff --git a/pyproject.toml b/pyproject.toml index a6f12d8..1d1807e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ 'cmlibs.argon >= 0.4.0', 'cmlibs.zinc', 'exf2mbfxml', + 'packaging', 'svgpathtools_light', ] description = "Export CMLibs Zinc data to common mesh formats." diff --git a/src/cmlibs/exporter/baseimage.py b/src/cmlibs/exporter/baseimage.py index cdae871..0753015 100644 --- a/src/cmlibs/exporter/baseimage.py +++ b/src/cmlibs/exporter/baseimage.py @@ -16,7 +16,7 @@ class BaseImageExporter(BaseExporter): By default the export will be use PySide6 to render the scene. An alternative is to use OSMesa for software rendering. To use OSMesa as the renderer either set the environment variable - OC_EXPORTER_RENDERER to 'osmesa' or not have PySide6 available in the + CMLIBS_EXPORTER_RENDERER to 'osmesa' or not have PySide6 available in the calling environment. """ @@ -61,7 +61,7 @@ def export_image(self): Export graphics into an image format. """ pyside6_opengl_failed = True - if "OC_EXPORTER_RENDERER" not in os.environ or os.environ["OC_EXPORTER_RENDERER"] != "osmesa": + if os.environ.get("CMLIBS_EXPORTER_RENDERER", "") != "offscreen": try: from PySide6 import QtGui @@ -83,6 +83,7 @@ def export_image(self): mesa_opengl_failed = True if pyside6_opengl_failed: try: + os.environ["PYOPENGL_PLATFORM"] = "osmesa" from OpenGL import GL from OpenGL import arrays from OpenGL.osmesa import ( diff --git a/src/cmlibs/exporter/flatmapsvg.py b/src/cmlibs/exporter/flatmapsvg.py index 24cec1e..619c766 100644 --- a/src/cmlibs/exporter/flatmapsvg.py +++ b/src/cmlibs/exporter/flatmapsvg.py @@ -11,6 +11,7 @@ import random from decimal import Decimal +from packaging.version import Version from svgpathtools import svg2paths from xml.dom.minidom import parseString @@ -54,6 +55,7 @@ def __init__(self, output_target=None, output_prefix=None): super(ArgonSceneExporter, self).__init__("ArgonSceneExporterWavefrontSVG" if output_prefix is None else output_prefix) self._output_target = output_target self._annotations_csv_file = None + self._annotations_json_file = None def export(self, output_target=None): """ @@ -145,6 +147,9 @@ def export_flatmapsvg_from_scene(self, scene, scene_filter=None): def set_annotations_csv_file(self, filename): self._annotations_csv_file = filename + def set_annotations_json_file(self, filename): + self._annotations_json_file = filename + def _read_reversed_annotations_map(self): reversed_map = None if self._annotations_csv_file is not None: @@ -155,7 +160,17 @@ def _read_reversed_annotations_map(self): if is_annotation_csv_file: fh.seek(0) - reversed_map = _reverse_map_annotations(result) + reversed_map = _reverse_map_annotations_csv(result) + + if self._annotations_json_file is not None: + with open(self._annotations_json_file) as fh: + try: + result = json.load(fh) + except json.decoder.JSONDecodeError: + result = None + + if result is not None: + reversed_map = _reverse_map_annotations_json(result) return reversed_map @@ -851,7 +866,7 @@ def _define_background_regions(boundaries, view_box): return f'{brain_rect}{cervical_rect}{thoracic_rect}{lumbar_rect}', features -def _reverse_map_annotations(csv_reader): +def _reverse_map_annotations_csv(csv_reader): reverse_map = {} if csv_reader: first = True @@ -865,6 +880,21 @@ def _reverse_map_annotations(csv_reader): return reverse_map +def _reverse_map_annotations_json(json_data): + reverse_map = {} + if json_data: + if json_data.get('id', '') == 'scaffold creator settings' and _known_version(json_data.get('version', '0.0.0')): + metadata = json_data.get('metadata', {'annotations': []}) + for annotation in metadata.get('annotations', []): + reverse_map[annotation['name']] = annotation['id'] + + return reverse_map + + +def _known_version(version_in): + return not Version(version_in) < Version('0.1.0') + + def _label_has_annotations(entry, annotation_map): return entry in annotation_map and annotation_map[entry] and annotation_map[entry] != "None" diff --git a/tests/test_flatmapsvg.py b/tests/test_flatmapsvg.py index 889680a..6a7af27 100644 --- a/tests/test_flatmapsvg.py +++ b/tests/test_flatmapsvg.py @@ -15,6 +15,9 @@ def _resource_path(resource_name): return os.path.join(here, "resources", resource_name) +NULL = [0, 0] + + class Exporter(unittest.TestCase): def test_flatmap_svg(self): @@ -67,10 +70,21 @@ def test_write_svg_outline(self): os.remove(simple_svg_file) +def _define_test_points(): + p1 = [-38.76407990290047, 136.95711038948954] + p2 = [-38.66539406842078, 135.33885440116572] + p3 = [-38.66539406842079, 135.3388544011657] + p4 = [-38.57638526850839, 133.76842178271903] + p5 = [-38.57638526850839, 133.768421782719] + p6 = [-38.50103491179091, 132.19996302635846] + p7 = [-38.5010349117909, 132.1999630263585] + return p1, p2, p3, p4, p5, p6, p7 + + class FindConnectedSet(unittest.TestCase): def test_simple(self): - null = [0, 0] + p1 = [1, 1] p2 = [2, 2] p3 = [3, 3] @@ -79,18 +93,24 @@ def test_simple(self): p6 = [6, 6] p7 = [7, 7] - c1 = [[p1, null, null, p2], [p2, null, null, p3], [p3, null, null, p4], [p4, null, null, p5]] - c2 = [[p1, null, null, p2], [p2, null, null, p3], [p5, null, null, p6], [p6, null, null, p7]] - c3 = [[p2, null, null, p3], [p3, null, null, p4], [p1, null, null, p2], [p4, null, null, p5]] - - self.assertEqual(1, len(_connected_segments(c1))) - self.assertEqual(2, len(_connected_segments(c2))) + c3 = self._setup_data(p1, p2, p3, p4, p5, p6, p7) segmented_c3 = _connected_segments(c3) self.assertEqual(1, len(segmented_c3)) self.assertEqual(p1, segmented_c3[0][0][0]) + def _setup_data(self, p1, p2, p3, p4, p5, p6, p7, alt_c3=False): + c1 = [[p1, NULL, NULL, p2], [p2, NULL, NULL, p3], [p3, NULL, NULL, p4], [p4, NULL, NULL, p5]] + c2 = [[p1, NULL, NULL, p2], [p2, NULL, NULL, p3], [p5, NULL, NULL, p6], [p6, NULL, NULL, p7]] + if alt_c3: + c3 = [[p3, NULL, NULL, p4], [p2, NULL, NULL, p3], [p4, NULL, NULL, p5], [p1, NULL, NULL, p2]] + else: + c3 = [[p2, NULL, NULL, p3], [p3, NULL, NULL, p4], [p1, NULL, NULL, p2], [p4, NULL, NULL, p5]] + + self.assertEqual(1, len(_connected_segments(c1))) + self.assertEqual(2, len(_connected_segments(c2))) + return c3 + def test_real_data(self): - null = [0, 0] p1 = [-38.76407990290047, 136.95711038948954] p2 = [-38.66539406842079, 135.3388544011657] p3 = [-38.66539406842079, 135.3388544011657] @@ -99,40 +119,21 @@ def test_real_data(self): p6 = [-38.50103491179091, 132.19996302635846] p7 = [-38.5010349117909, 132.1999630263585] - c1 = [[p1, null, null, p2], [p2, null, null, p3], [p3, null, null, p4], [p4, null, null, p5]] - c2 = [[p1, null, null, p2], [p2, null, null, p3], [p5, null, null, p6], [p6, null, null, p7]] - c3 = [[p3, null, null, p4], [p2, null, null, p3], [p4, null, null, p5], [p1, null, null, p2]] - - self.assertEqual(1, len(_connected_segments(c1))) - self.assertEqual(2, len(_connected_segments(c2))) + c3 = self._setup_data(p1, p2, p3, p4, p5, p6, p7, alt_c3=True) segmented_c3 = _connected_segments(c3) self.assertEqual(2, len(segmented_c3)) self.assertEqual(p2, segmented_c3[0][0][0]) def test_real_data_single_section(self): - null = [0, 0] - p1 = [-38.76407990290047, 136.95711038948954] - p2 = [-38.66539406842078, 135.33885440116572] - p3 = [-38.66539406842079, 135.3388544011657] - p4 = [-38.57638526850839, 133.76842178271903] - p5 = [-38.57638526850839, 133.768421782719] - p6 = [-38.50103491179091, 132.19996302635846] - p7 = [-38.5010349117909, 132.1999630263585] + p1, p2, p3, p4, p5, p6, p7 = _define_test_points() - c1 = [[p1, null, null, p2], [p2, null, null, p3], [p3, null, null, p4], [p4, null, null, p5], [p6, null, null, p7]] + c1 = [[p1, NULL, NULL, p2], [p2, NULL, NULL, p3], [p3, NULL, NULL, p4], [p4, NULL, NULL, p5], [p6, NULL, NULL, p7]] self.assertEqual(2, len(_connected_segments(c1))) def test_real_data_fork(self): - null = [0, 0] - p1 = [-38.76407990290047, 136.95711038948954] - p2 = [-38.66539406842078, 135.33885440116572] - p3 = [-38.66539406842079, 135.3388544011657] - p4 = [-38.57638526850839, 133.76842178271903] - p5 = [-38.57638526850839, 133.768421782719] - p6 = [-38.50103491179091, 132.19996302635846] - p7 = [-38.5010349117909, 132.1999630263585] + p1, p2, p3, p4, p5, p6, p7 = _define_test_points() - c1 = [[p1, null, null, p2], [p2, null, null, p3], [p3, null, null, p4], [p4, null, null, p5], [p3, null, null, p6], [p6, null, null, p7]] + c1 = [[p1, NULL, NULL, p2], [p2, NULL, NULL, p3], [p3, NULL, NULL, p4], [p4, NULL, NULL, p5], [p3, NULL, NULL, p6], [p6, NULL, NULL, p7]] self.assertEqual(2, len(_connected_segments(c1)))