diff --git a/model_server/extensions/chaeo/zmask.py b/model_server/extensions/chaeo/zmask.py
index a457516b17cad744797af6305aa444f34d514e66..f77ac431ab9cd5fc67f6ef5a83809e5153df75da 100644
--- a/model_server/extensions/chaeo/zmask.py
+++ b/model_server/extensions/chaeo/zmask.py
@@ -32,9 +32,41 @@ class RoiSet(object):
             acc_raw: GenericImageDataAccessor,
             params: RoiSetMetaParams = RoiSetMetaParams(),
     ):
-        self.zmask_meta, self.df, self.interm = build_zmask_from_object_mask(
+        # parse filters
+        filters = params.filters
+        query_str = 'label > 0'  # always true
+        if filters is not None:
+            for k, val in filters.dict(exclude_unset=True).items():
+                assert k in ('area', 'solidity')
+                vmin = val['min']
+                vmax = val['max']
+                assert vmin >= 0
+                query_str = query_str + f' & {k} > {vmin} & {k} < {vmax}'
+
+        # build dataframe of objects, assign z index to each object
+        argmax = acc_raw.data.argmax(axis=3, keepdims=True)[:, :, 0, 0].astype('uint16')
+        df = (
+            pd.DataFrame(
+                regionprops_table(
+                    acc_obj_ids,
+                    intensity_image=argmax,
+                    properties=('label', 'area', 'intensity_mean', 'solidity', 'bbox', 'centroid')
+                )
+            )
+            .rename(
+                columns={'bbox-0': 'y0', 'bbox-1': 'x0', 'bbox-2': 'y1', 'bbox-3': 'x1',}
+            )
+        )
+        df['zi'] = df['intensity_mean'].round().astype('int')
+        df['keeper'] = False
+        df.loc[df.query(query_str).index, 'keeper'] = True
+        self.df = df
+
+        # remaining zmask_meta write ops
+        self.zmask_meta, _, self.interm = build_zmask_from_object_mask(
             acc_obj_ids,
             acc_raw,
+            df,
             params=params,
         )
         self.acc_obj_ids = acc_obj_ids
@@ -61,6 +93,19 @@ class RoiSet(object):
     def get_object_mask_by_class(self, class_id):
         return self.object_id_labels == class_id
 
+
+
+    # def loc_mask(self, i):
+    #     # compute contours
+    #     # obmask = (lamap == ob.label) # TODO: on-the-fly
+    #     # contour = find_contours(obmask) # TODO: on-the-fly
+    #     # mask = obmask[ob.y0: ob.y1, ob.x0: ob.x1]
+    #     ob = self.df.loc[i]
+    #     return
+    #
+    # def loc_contour(self, i):
+    #     pass
+
     def get_patch_masks(self, **kwargs) -> MonoPatchStack:
         return get_patch_masks(self, **kwargs)
 
@@ -177,68 +222,40 @@ class RoiSet(object):
 def build_zmask_from_object_mask(
         obmask: GenericImageDataAccessor,
         zstack: GenericImageDataAccessor,
+        df,
         params: RoiSetMetaParams = RoiSetMetaParams(),
 ):
-    """
-    Given a 2D mask of objects, build a 3D mask, where each object's z-position is determined by the index of
-    maximum intensity in z.  Return this zmask and a list of each object's meta information.
-    :param obmask: GenericImageDataAccessor  2D map of objects IDs
-    :param zstack: GenericImageDataAccessor monochrome zstack of same Y, X dimension as obmask
-    :param params: RoiSetMetaParams
-        filters: dictionary of form {attribute: (min, max)}; valid attributes are 'area' and 'solidity'
-        mask_type: if 'boxes', zmask is True in each object's complete bounding box; otherwise 'contours'
-        expand_box_by: (xy, z) expands bounding box by (xy, z) pixels except where this hits a boundary
-    :return: tuple (zmask, meta)
-        np.ndarray:
-            boolean mask of same size as stack
-        List containing one Dict per object, with keys:
-            info: object's properties from skimage.measure.regionprops_table, including bounding box (y0, y1, x0, x1)
-            slice: named slice (np.s_) of (optionally) expanded bounding box
-            relative_bounding_box: bounding box (y0, y1, x0, x1) in relative frame of (optionally) expanded bounding box
-            contour: object's contour returned by skimage.measure.find_contours
-            mask: mask of object in relative frame of (optionally) expanded bounding box
-        pd.DataFrame: objects, including bounding, box information after filtering
-        Dict of intermediate image products:
-            label_map: np.ndarray (h x w) where each unique object has an integer label
-            argmax: np.ndarray (h x w x 1 x 1) z-index of highest intensity in zstack
-    """
+    # """
+    # Given a 2D mask of objects, build a 3D mask, where each object's z-position is determined by the index of
+    # maximum intensity in z.  Return this zmask and a list of each object's meta information.
+    # :param obmask: GenericImageDataAccessor  2D map of objects IDs
+    # :param zstack: GenericImageDataAccessor monochrome zstack of same Y, X dimension as obmask
+    # :param params: RoiSetMetaParams
+    #     filters: dictionary of form {attribute: (min, max)}; valid attributes are 'area' and 'solidity'
+    #     mask_type: if 'boxes', zmask is True in each object's complete bounding box; otherwise 'contours'
+    #     expand_box_by: (xy, z) expands bounding box by (xy, z) pixels except where this hits a boundary
+    # :return: tuple (zmask, meta)
+    #     np.ndarray:
+    #         boolean mask of same size as stack
+    #     List containing one Dict per object, with keys:
+    #         info: object's properties from skimage.measure.regionprops_table, including bounding box (y0, y1, x0, x1)
+    #         slice: named slice (np.s_) of (optionally) expanded bounding box
+    #         relative_bounding_box: bounding box (y0, y1, x0, x1) in relative frame of (optionally) expanded bounding box
+    #         contour: object's contour returned by skimage.measure.find_contours
+    #         mask: mask of object in relative frame of (optionally) expanded bounding box
+    #     pd.DataFrame: objects, including bounding, box information after filtering
+    #     Dict of intermediate image products:
+    #         label_map: np.ndarray (h x w) where each unique object has an integer label
+    #         argmax: np.ndarray (h x w x 1 x 1) z-index of highest intensity in zstack
+    # """
     filters = params.filters
     expand_box_by = params.expand_box_by
     # validate inputs
     assert zstack.hw == obmask.shape
 
     lamap = obmask
-    query_str = 'label > 0'  # always true
-    if filters is not None:
-        for k, val in filters.dict(exclude_unset=True).items():
-            assert k in ('area', 'solidity')
-            vmin = val['min']
-            vmax = val['max']
-            assert vmin >= 0
-            query_str = query_str + f' & {k} > {vmin} & {k} < {vmax}'
-
-    # build dataframe of objects, assign z index to each object
+
     argmax = zstack.data.argmax(axis=3, keepdims=True)[:, :, 0, 0].astype('uint16')
-    df = (
-        pd.DataFrame(
-            regionprops_table(
-                lamap,
-                intensity_image=argmax,
-                properties=('label', 'area', 'intensity_mean', 'solidity', 'bbox', 'centroid')
-            )
-        )
-        .rename(
-            columns={
-                'bbox-0': 'y0',
-                'bbox-1': 'x0',
-                'bbox-2': 'y1',
-                'bbox-3': 'x1',
-            }
-        )
-    )
-    df['zi'] = df['intensity_mean'].round().astype('int')
-    df['keeper'] = False
-    df.loc[df.query(query_str).index, 'keeper'] = True
 
     # convert bounding boxes to numpy slice objects
     ebxy, ebz = expand_box_by