diff --git a/model_server/base/pipelines/segment.py b/model_server/base/pipelines/segment.py index 2ab20a649efa33b90072953fae457eccc45b60b8..3e0f76e64dbbb86f8812e6de9d040d91f09a1abf 100644 --- a/model_server/base/pipelines/segment.py +++ b/model_server/base/pipelines/segment.py @@ -60,6 +60,7 @@ def file_call(p: FileCallParams) -> FileCallRecord: timer=steps.times ) +# TODO: implement smoothing, move this away from model itself def classify_pixels(acc_in: GenericImageDataAccessor, model: SemanticSegmentationModel, **kwargs) -> PipelineTrace: """ Run a semantic segmentation model to compute a binary mask from an input image diff --git a/model_server/extensions/ilastik/pipelines/px_then_ob.py b/model_server/extensions/ilastik/pipelines/px_then_ob.py index 2908080403dfc4a951b4ca95dcede47e3bad0d24..bc158591aedf93c09889d37ab45d04cec89a3773 100644 --- a/model_server/extensions/ilastik/pipelines/px_then_ob.py +++ b/model_server/extensions/ilastik/pipelines/px_then_ob.py @@ -15,7 +15,6 @@ from model_server.base.accessors import generate_file_accessor, write_accessor_d router = APIRouter( prefix='/pipelines', - tags=['pipelines'], ) class Record(BaseModel): diff --git a/tests/base/test_workflow.py b/tests/base/test_pipelines.py similarity index 100% rename from tests/base/test_workflow.py rename to tests/base/test_pipelines.py diff --git a/model_server/extensions/ilastik/tests/test_ilastik.py b/tests/test_ilastik/test_ilastik.py similarity index 80% rename from model_server/extensions/ilastik/tests/test_ilastik.py rename to tests/test_ilastik/test_ilastik.py index cedc290d48f2add8a8362999466d3fd6dd1c6109..d428b0ffa77f787135e355567fc60765493e8bfd 100644 --- a/model_server/extensions/ilastik/tests/test_ilastik.py +++ b/tests/test_ilastik/test_ilastik.py @@ -1,15 +1,19 @@ -import pathlib import unittest import numpy as np -from model_server.conf.testing import czifile, ilastik_classifiers, output_path, roiset_test_data from model_server.base.accessors import CziImageFileAccessor, generate_file_accessor, InMemoryDataAccessor, PatchStack, write_accessor_data_to_file from model_server.extensions.ilastik import models as ilm from model_server.extensions.ilastik.pipelines.px_then_ob import infer_px_then_ob_model from model_server.base.roiset import _get_label_ids, RoiSet, RoiSetMetaParams from model_server.base.pipelines.segment import classify_pixels -from base.test_api import TestServerBaseClass +import model_server.conf.testing as conf + +data = conf.meta['image_files'] +output_path = conf.meta['output_path'] +params = conf.meta['roiset'] +czifile = conf.meta['image_files']['czifile'] +ilastik_classifiers = conf.meta['ilastik_classifiers'] def _random_int(*args): return np.random.randint(0, 2 ** 8, size=args, dtype='uint8') @@ -19,23 +23,14 @@ class TestIlastikPixelClassification(unittest.TestCase): self.cf = CziImageFileAccessor(czifile['path']) self.channel = 0 self.model = ilm.IlastikPixelClassifierModel( - params=ilm.IlastikPixelClassifierParams(project_file=ilastik_classifiers['px'].__str__()) + params=ilm.IlastikPixelClassifierParams(project_file=ilastik_classifiers['px']['path'].__str__()) ) self.mono_image = self.cf.get_mono(self.channel) - def test_faulthandler(self): # recreate error that is messing up ilastik - import io - import sys - import faulthandler - - with self.assertRaises(io.UnsupportedOperation): - faulthandler.enable(file=sys.stdout) - - def test_raise_error_if_autoload_disabled(self): model = ilm.IlastikPixelClassifierModel( - params=ilm.IlastikPixelClassifierParams(project_file=ilastik_classifiers['px'].__str__()), + params=ilm.IlastikPixelClassifierParams(project_file=ilastik_classifiers['px']['path'].__str__()), autoload=False ) w = 512 @@ -80,7 +75,7 @@ class TestIlastikPixelClassification(unittest.TestCase): def _run_seg(tr, sig): mod = ilm.IlastikPixelClassifierModel( params=ilm.IlastikPixelClassifierParams( - project_file=ilastik_classifiers['px'].__str__(), + project_file=ilastik_classifiers['px']['path'].__str__(), px_prob_threshold=tr, px_smoothing=sig, ), @@ -153,7 +148,7 @@ class TestIlastikPixelClassification(unittest.TestCase): self.test_run_pixel_classifier() fp = czifile['path'] model = ilm.IlastikObjectClassifierFromPixelPredictionsModel( - params=ilm.IlastikParams(project_file=ilastik_classifiers['pxmap_to_obj'].__str__()) + params=ilm.IlastikParams(project_file=ilastik_classifiers['pxmap_to_obj']['path'].__str__()) ) mask = self.model.label_pixel_class(self.mono_image) objmap, _ = model.infer(self.mono_image, mask) @@ -171,7 +166,7 @@ class TestIlastikPixelClassification(unittest.TestCase): self.test_run_pixel_classifier() fp = czifile['path'] model = ilm.IlastikObjectClassifierFromSegmentationModel( - params=ilm.IlastikParams(project_file=ilastik_classifiers['seg_to_obj'].__str__()) + params=ilm.IlastikParams(project_file=ilastik_classifiers['seg_to_obj']['path'].__str__()) ) mask = self.model.label_pixel_class(self.mono_image) objmap = model.label_instance_class(self.mono_image, mask) @@ -185,16 +180,27 @@ class TestIlastikPixelClassification(unittest.TestCase): self.assertEqual(objmap.data.max(), 2) def test_ilastik_pixel_classification_as_workflow(self): - result = classify_pixels( + res = classify_pixels( generate_file_accessor(czifile['path']), ilm.IlastikPixelClassifierModel( - params=ilm.IlastikPixelClassifierParams(project_file=ilastik_classifiers['px'].__str__()), + params=ilm.IlastikPixelClassifierParams(project_file=ilastik_classifiers['px']['path'].__str__()), ), channel=0, ) - self.assertGreater(result.timer['inference'], 1.0) + self.assertGreater(res.times['inference'], 0.1) -class TestIlastikOverApi(TestServerBaseClass): +class TestIlastikOverApi(conf.TestServerBaseClass): + + def _copy_input_file_to_server(self): + from pathlib import Path + from shutil import copyfile + + resp = self._get('paths') + pa = resp.json()['inbound_images'] + copyfile( + czifile['path'], + Path(pa) / czifile['name'] + ) def test_httpexception_if_incorrect_project_file_loaded(self): resp_load = self._put( @@ -207,7 +213,7 @@ class TestIlastikOverApi(TestServerBaseClass): def test_load_ilastik_pixel_model(self): resp_load = self._put( 'ilastik/seg/load/', - body={'project_file': str(ilastik_classifiers['px'])}, + body={'project_file': str(ilastik_classifiers['px']['path'])}, ) self.assertEqual(resp_load.status_code, 200, resp_load.json()) model_id = resp_load.json()['model_id'] @@ -223,20 +229,20 @@ class TestIlastikOverApi(TestServerBaseClass): self.assertEqual(len(resp_list_1st), 1, resp_list_1st) resp_load_2nd = self._put( 'ilastik/seg/load/', - body={'project_file': str(ilastik_classifiers['px']), 'duplicate': True}, + body={'project_file': str(ilastik_classifiers['px']['path']), 'duplicate': True}, ) resp_list_2nd = self._get('models').json() self.assertEqual(len(resp_list_2nd), 2, resp_list_2nd) resp_load_3rd = self._put( 'ilastik/seg/load/', - body={'project_file': str(ilastik_classifiers['px']), 'duplicate': False}, + body={'project_file': str(ilastik_classifiers['px']['path']), 'duplicate': False}, ) resp_list_3rd = self._get('models').json() self.assertEqual(len(resp_list_3rd), 2, resp_list_3rd) def test_load_ilastik_pixel_model_with_params(self): params = { - 'project_file': str(ilastik_classifiers['px']), + 'project_file': str(ilastik_classifiers['px']['path']), 'px_class': 0, 'px_prob_threshold': 0.5 } @@ -250,42 +256,11 @@ class TestIlastikOverApi(TestServerBaseClass): self.assertEqual(len(mods), 1) self.assertEqual(mods[model_id]['params']['px_prob_threshold'], 0.5) - def test_no_duplicate_model_with_different_path_formats(self): - self._get('session/restart') - resp_list_1 = self._get('models').json() - self.assertEqual(len(resp_list_1), 0) - ilp = ilastik_classifiers['px'] - - # create and validate two copies of the same pathname with different string formats - ilp_win = str(pathlib.PureWindowsPath(ilp)) - self.assertGreater(ilp_win.count('\\'), 0) # i.e. contains backslashes - self.assertEqual(ilp_win.count('/'), 0) - ilp_posx = ilastik_classifiers['px'].as_posix() - self.assertGreater(ilp_posx.count('/'), 0) - self.assertEqual(ilp_posx.count('\\'), 0) - self.assertEqual(pathlib.Path(ilp_win), pathlib.Path(ilp_posx)) - - # load models with these paths - resp1 = self._put( - 'ilastik/seg/load/', - body={'project_file': ilp_win, 'duplicate': False}, - ) - resp2 = self._put( - 'ilastik/seg/load/', - body={'project_file': ilp_posx, 'duplicate': False}, - ) - self.assertEqual(resp1.json(), resp2.json()) - - # assert that only one copy of the model is loaded - resp_list_2 = self._get('models').json() - print(resp_list_2) - self.assertEqual(len(resp_list_2), 1) - def test_load_ilastik_pxmap_to_obj_model(self): resp_load = self._put( 'ilastik/pxmap_to_obj/load/', - body={'project_file': str(ilastik_classifiers['pxmap_to_obj'])}, + body={'project_file': str(ilastik_classifiers['pxmap_to_obj']['path'])}, ) model_id = resp_load.json()['model_id'] @@ -299,7 +274,7 @@ class TestIlastikOverApi(TestServerBaseClass): def test_load_ilastik_seg_to_obj_model(self): resp_load = self._put( 'ilastik/seg_to_obj/load/', - body={'project_file': str(ilastik_classifiers['seg_to_obj'])}, + body={'project_file': str(ilastik_classifiers['seg_to_obj']['path'])}, ) model_id = resp_load.json()['model_id'] @@ -311,18 +286,18 @@ class TestIlastikOverApi(TestServerBaseClass): return model_id def test_ilastik_infer_pixel_probability(self): - self.copy_input_file_to_server() + self._copy_input_file_to_server() model_id = self.test_load_ilastik_pixel_model() resp_infer = self._put( - f'workflows/segment', - query={'model_id': model_id, 'input_filename': czifile['filename'], 'channel': 0}, + f'pipelines/segment', + body={'model_id': model_id, 'input_filename': czifile['name'], 'channel': 0}, ) self.assertEqual(resp_infer.status_code, 200, resp_infer.content.decode()) def test_ilastik_infer_px_then_ob(self): - self.copy_input_file_to_server() + self._copy_input_file_to_server() px_model_id = self.test_load_ilastik_pixel_model() ob_model_id = self.test_load_ilastik_pxmap_to_obj_model() @@ -331,33 +306,21 @@ class TestIlastikOverApi(TestServerBaseClass): body={ 'px_model_id': px_model_id, 'ob_model_id': ob_model_id, - 'input_filename': czifile['filename'], + 'input_filename': czifile['name'], 'channel': 0, } ) self.assertEqual(resp_infer.status_code, 200, resp_infer.content.decode()) -class TestIlastikOnMultichannelInputs(TestServerBaseClass): +class TestIlastikOnMultichannelInputs(conf.TestServerBaseClass): def setUp(self) -> None: super(TestIlastikOnMultichannelInputs, self).setUp() - self.pa_px_classifier = ilastik_classifiers['px_color_zstack'] - self.pa_ob_pxmap_classifier = ilastik_classifiers['ob_pxmap_color_zstack'] - self.pa_ob_seg_classifier = ilastik_classifiers['ob_seg_color_zstack'] - self.pa_input_image = roiset_test_data['multichannel_zstack']['path'] - self.pa_mask = roiset_test_data['multichannel_zstack']['mask_path_3d'] - - def _copy_input_file_to_server(self): - from shutil import copyfile - - pa_data = roiset_test_data['multichannel_zstack']['path'] - resp = self._get('paths') - pa = resp.json()['inbound_images'] - outpath = pathlib.Path(pa) / pa_data.name - copyfile( - czifile['path'], - outpath - ) + self.pa_px_classifier = ilastik_classifiers['px_color_zstack']['path'] + self.pa_ob_pxmap_classifier = ilastik_classifiers['ob_pxmap_color_zstack']['path'] + self.pa_ob_seg_classifier = ilastik_classifiers['ob_seg_color_zstack']['path'] + self.pa_input_image = data['multichannel_zstack_raw']['path'] + self.pa_mask = data['multichannel_zstack_mask3d']['path'] def test_classify_pixels(self): img = generate_file_accessor(self.pa_input_image) @@ -417,8 +380,8 @@ class TestIlastikOnMultichannelInputs(TestServerBaseClass): ob_model_id = resp_load.json()['model_id'] resp_infer = self._put( - 'ilastik/pixel_then_object_classification/infer/', - query={ + 'ilastik/pipelines/pixel_then_object_classification/infer/', + body={ 'px_model_id': px_model_id, 'ob_model_id': ob_model_id, 'input_filename': self.pa_input_image.__str__(), @@ -432,9 +395,9 @@ class TestIlastikOnMultichannelInputs(TestServerBaseClass): class TestIlastikObjectClassification(unittest.TestCase): def setUp(self): - stack = generate_file_accessor(roiset_test_data['multichannel_zstack']['path']) - stack_ch_pa = stack.get_mono(roiset_test_data['pipeline_params']['patches_channel']) - seg_mask = generate_file_accessor(roiset_test_data['multichannel_zstack']['mask_path']) + stack = generate_file_accessor(data['multichannel_zstack_raw']['path']) + stack_ch_pa = stack.get_mono(conf.meta['roiset']['patches_channel']) + seg_mask = generate_file_accessor(data['multichannel_zstack_mask2d']['path']) self.roiset = RoiSet( stack_ch_pa, @@ -447,7 +410,7 @@ class TestIlastikObjectClassification(unittest.TestCase): ) self.classifier = ilm.IlastikObjectClassifierFromSegmentationModel( - params=ilm.IlastikParams(project_file=ilastik_classifiers['seg_to_obj'].__str__()), + params=ilm.IlastikParams(project_file=ilastik_classifiers['seg_to_obj']['path'].__str__()), ) self.raw = self.roiset.get_patches_acc() self.masks = self.roiset.get_patch_masks_acc()