diff --git a/model_server/base/process.py b/model_server/base/process.py
index 3ee227d77b5355725ca130a4c4ea8567c3c9c3a9..992c9fb93140ab49859f0807b55a1f8e321cd80d 100644
--- a/model_server/base/process.py
+++ b/model_server/base/process.py
@@ -4,6 +4,7 @@ Image processing utility functions
 from math import ceil, floor
 
 import numpy as np
+import skimage
 from skimage.exposure import rescale_intensity
 
 
@@ -85,3 +86,43 @@ def make_rgb(nda):
     outdata = np.zeros((h, w, 3, nz), dtype=nda.dtype)
     outdata[:, :, 0:c, :] = nda[:, :, :, :]
     return outdata
+
+
+def mask_largest_object(
+        img: np.ndarray,
+        max_allowed: int = 10,
+        verbose: bool = True
+) -> np.ndarray:
+    """
+    Where more than one connected component is found in an image, return the largest object by area
+    :param img: (np.ndarray) containing object labels or binary mask
+    :param max_allowed: raise an error if more than this number of objects is found
+    :param verbose: print a message each time more than one object is found
+    :return: np.ndarray of same size as img
+    """
+    if is_mask(img): # assign object labels
+        ob_id = skimage.measure.label(img)
+    else:  # assume img is contains object labels
+        ob_id = img
+
+    num_obj = len(np.unique(ob_id)) - 1
+    if num_obj > max_allowed:
+        raise TooManyObjectError(f'Found {num_obj} objects in frame')
+    if num_obj > 1:
+        if verbose:
+            print(f'Found {num_obj} nonzero unique values in object map; keeping the one with the largest area')
+        val, cts = np.unique(ob_id, return_counts=True)
+        mask = ob_id == val[1 + cts[1:].argmax()]
+        return mask * img
+    else:
+        return img
+
+
+class Error(Exception):
+    pass
+
+
+class TooManyObjectError(Exception):
+    pass
+
+
diff --git a/model_server/base/roiset.py b/model_server/base/roiset.py
index bf40227a0629c2d3747e1faf20360565fa664a63..4aecae346e71ef56dc805b0b2549df589038563a 100644
--- a/model_server/base/roiset.py
+++ b/model_server/base/roiset.py
@@ -18,7 +18,8 @@ from model_server.base.models import InstanceSegmentationModel
 from model_server.base.process import pad, rescale, resample_to_8bit, make_rgb
 from model_server.extensions.chaeo.annotators import draw_box_on_patch, draw_contours_on_patch, draw_boxes_on_3d_image
 from model_server.extensions.chaeo.accessors import write_patch_to_file, MonoPatchStack, Multichannel3dPatchStack
-from model_server.extensions.chaeo.process import mask_largest_object
+from base.process import mask_largest_object
+
 
 class PatchParams(BaseModel):
     draw_bounding_box: bool = False
diff --git a/model_server/extensions/chaeo/process.py b/model_server/extensions/chaeo/process.py
deleted file mode 100644
index e41578eee35f7849e7d714326c59ca68e34042b0..0000000000000000000000000000000000000000
--- a/model_server/extensions/chaeo/process.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import numpy as np
-import skimage
-
-from model_server.base.process import is_mask
-
-def mask_largest_object(
-        img: np.ndarray,
-        max_allowed: int = 10,
-        verbose: bool = True
-) -> np.ndarray:
-    """
-    Where more than one connected component is found in an image, return the largest object by area
-    :param img: (np.ndarray) containing object labels or binary mask
-    :param max_allowed: raise an error if more than this number of objects is found
-    :param verbose: print a message each time more than one object is found
-    :return: np.ndarray of same size as img
-    """
-    if is_mask(img): # assign object labels
-        ob_id = skimage.measure.label(img)
-    else:  # assume img is contains object labels
-        ob_id = img
-
-    # import skimage
-    # from pathlib import Path
-    # where = Path('c:/Users/rhodes/projects/proj0011-plankton-seg/tmp')
-    # skimage.io.imsave(where / 'raw.png', img)
-    num_obj = len(np.unique(ob_id)) - 1
-    if num_obj > max_allowed:
-        raise TooManyObjectError(f'Found {num_obj} objects in frame')
-    if num_obj > 1:
-        if verbose:
-            print(f'Found {num_obj} nonzero unique values in object map; keeping the one with the largest area')
-        # pr = regionprops_table(ob_id, properties=['label', 'area'])
-        val, cts = np.unique(ob_id, return_counts=True)
-        mask = ob_id == val[1 + cts[1:].argmax()]
-        # idx_max_area = pr['area'].argmax()
-        # mask = ob_id == pr['label'][idx_max_area]
-        return mask * img
-    else:
-        return img
-
-
-class Error(Exception):
-    pass
-
-
-class TooManyObjectError(Exception):
-    pass
diff --git a/model_server/extensions/chaeo/tests/test_process.py b/model_server/extensions/chaeo/tests/test_process.py
deleted file mode 100644
index 79e6883c5f7099869b9b9901474c5c4cb833d158..0000000000000000000000000000000000000000
--- a/model_server/extensions/chaeo/tests/test_process.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import unittest
-
-import numpy as np
-
-from model_server.extensions.chaeo.process import mask_largest_object
-
-class TestMaskLargestObject(unittest.TestCase):
-    def test_mask_largest_touching_object(self):
-        arr = np.zeros([5, 5], dtype='uint8')
-        arr[0:3, 0:3] = 2
-        arr[3:, 2:] = 4
-        masked = mask_largest_object(arr)
-        self.assertTrue(np.all(np.unique(masked) == [0, 2]))
-        self.assertTrue(np.all(masked[4:5, 0:2] == 0))
-        self.assertTrue(np.all(masked[0:3, 3:5] == 0))
-
-    def test_no_change(self):
-        arr = np.zeros([5, 5], dtype='uint8')
-        arr[0:3, 0:3] = 2
-        masked = mask_largest_object(arr)
-        self.assertTrue(np.all(masked == arr))
-
-    def test_mask_multiple_objects_in_binary_maks(self):
-        arr = np.zeros([5, 5], dtype='uint8')
-        arr[0:3, 0:3] = 255
-        arr[4, 2:5] = 255
-        masked = mask_largest_object(arr)
-        print(np.unique(masked))
-        self.assertTrue(np.all(np.unique(masked) == [0, 255]))
-        self.assertTrue(np.all(masked[:, 3:5] == 0))
-        self.assertTrue(np.all(masked[3:5, :] == 0))
-
diff --git a/model_server/extensions/chaeo/workflows.py b/model_server/extensions/chaeo/workflows.py
index b837877979017384effdb840c1f171d7e6d1d808..c651f95ecdf68f4b8f3aa431e6d4e512a2467aa4 100644
--- a/model_server/extensions/chaeo/workflows.py
+++ b/model_server/extensions/chaeo/workflows.py
@@ -9,7 +9,7 @@ from skimage.morphology import dilation
 from sklearn.model_selection import train_test_split
 
 from base.roiset import RoiSetMetaParams, RoiSetExportParams
-from model_server.extensions.chaeo.process import mask_largest_object
+from base.process import mask_largest_object
 from base.roiset import _get_label_ids, RoiSet
 
 from model_server.base.accessors import generate_file_accessor, InMemoryDataAccessor, write_accessor_data_to_file
diff --git a/tests/test_process.py b/tests/test_process.py
index 6908e9d7c1b779065d4de226b7a203979570fb9a..454151bc4f84156348624318a43837b5e9a370c8 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -2,6 +2,7 @@ import unittest
 
 import numpy as np
 
+from base.process import mask_largest_object
 from model_server.base.process import pad
 
 class TestProcessingUtilityMethods(unittest.TestCase):
@@ -27,4 +28,31 @@ class TestProcessingUtilityMethods(unittest.TestCase):
         nc = self.data4d.shape[2]
         nz = self.data4d.shape[3]
         padded = pad(self.data4d, 256)
-        self.assertEqual(padded.shape, (256, 256, nc, nz))
\ No newline at end of file
+        self.assertEqual(padded.shape, (256, 256, nc, nz))
+
+
+class TestMaskLargestObject(unittest.TestCase):
+    def test_mask_largest_touching_object(self):
+        arr = np.zeros([5, 5], dtype='uint8')
+        arr[0:3, 0:3] = 2
+        arr[3:, 2:] = 4
+        masked = mask_largest_object(arr)
+        self.assertTrue(np.all(np.unique(masked) == [0, 2]))
+        self.assertTrue(np.all(masked[4:5, 0:2] == 0))
+        self.assertTrue(np.all(masked[0:3, 3:5] == 0))
+
+    def test_no_change(self):
+        arr = np.zeros([5, 5], dtype='uint8')
+        arr[0:3, 0:3] = 2
+        masked = mask_largest_object(arr)
+        self.assertTrue(np.all(masked == arr))
+
+    def test_mask_multiple_objects_in_binary_maks(self):
+        arr = np.zeros([5, 5], dtype='uint8')
+        arr[0:3, 0:3] = 255
+        arr[4, 2:5] = 255
+        masked = mask_largest_object(arr)
+        print(np.unique(masked))
+        self.assertTrue(np.all(np.unique(masked) == [0, 255]))
+        self.assertTrue(np.all(masked[:, 3:5] == 0))
+        self.assertTrue(np.all(masked[3:5, :] == 0))