Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2111bf2
Renamce OC_EXPORTER_RENDERER to CMLIBS_EXPORTER_RENDERER.
hsorby Jul 24, 2025
b0ce046
Add a run tests GitHub actions workflow.
hsorby Aug 28, 2025
f1b85af
Install hardware and software requirements for image export.
hsorby Aug 28, 2025
fde5943
Try use moderngl package for offscreen rendering.
hsorby Aug 28, 2025
76b13ab
Update run_tests.yaml
hsorby Aug 29, 2025
12b3ecd
Update run_tests.yaml
hsorby Aug 29, 2025
416c155
Update run_tests.yaml
hsorby Aug 29, 2025
0b3491f
Update run_tests.yaml
hsorby Aug 29, 2025
8106085
Revert "Try use moderngl package for offscreen rendering."
hsorby Aug 29, 2025
82eebe4
Use offscreen for CMLIBS_EXPORTER_RENDERER to br more generic.
hsorby Aug 29, 2025
02900ab
Specifically set pyopengl platform to osmesa.
hsorby Aug 29, 2025
64f0c18
Update run_tests.yaml
hsorby Aug 29, 2025
35cb211
Install libgl1-mesa-de instead of mesa-utils in run_tests.yaml.
hsorby Aug 31, 2025
668c51d
Merge in changes from origin/main fixing conflicts.
hsorby Aug 31, 2025
7157fdf
Install osmesa cmlibs zinc when running tests.
hsorby Aug 31, 2025
0467827
Use python version 3.10 instead of 3.9 in testing.
hsorby Aug 31, 2025
7ebcb3c
Update flatmapsvg exporter to make use of annotations in a JSON scaff…
hsorby Sep 11, 2025
a34c372
Merge branch 'main' of github.com:hsorby/cmlibs.exporter
hsorby Sep 11, 2025
a11c2c3
Change github respository to run only when cmlibs-python.
hsorby Sep 11, 2025
e7a1eea
Run tests in bash shell, add verbose output.
hsorby Sep 11, 2025
1d0bf94
Run test command in multiline mode.
hsorby Sep 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/run_tests.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
5 changes: 3 additions & 2 deletions src/cmlibs/exporter/baseimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand Down Expand Up @@ -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", "<not-set>") != "offscreen":
try:
from PySide6 import QtGui

Expand All @@ -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 (
Expand Down
34 changes: 32 additions & 2 deletions src/cmlibs/exporter/flatmapsvg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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"

Expand Down
65 changes: 33 additions & 32 deletions tests/test_flatmapsvg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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)))
Loading