Repository: petermr/pyamiimage (language: python)

Issue:
changed pyimage to pyamiimage in pmr_ami_image
First pull request as test


Discussion / hints:

Fix patch:
diff --git a/MANIFEST.in b/MANIFEST.in
index 82cbf85..7b74127 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,3 @@
 include LICENSE
 include README.md
-graft pyamiimage/test/resources
 include requirements.txt
diff --git a/README.md b/README.md
index 7c97406..95afc54 100644
--- a/README.md
+++ b/README.md
@@ -16,14 +16,19 @@ pip install pyamiimage
 ```
 ## Usage
 
+`pyamiimage` is a command-line tool and can be accessed via the terminal or command prompt. To bring up the help run:
+```
+pyamiimage --help
+```
+
+You can also include pyamiimage in your program using the provided classes.
+
 ### AmiImage
 AmiImage class provides methods for image manipulation. 
 ```
-from test.resources import Resources
 from pyamiimage.ami_image import AmiImage
 
-biosynth2_path = Resources.BIOSYNTH_2
-gray = AmiImage.create_grayscale_from_file(biosynth2_path)
+gray = AmiImage.create_grayscale_from_file(image_file_path)
 ```
 
 ### AmiGraph
@@ -31,3 +36,6 @@ AmiGraph class generate a graph from arrows in a diagram.
 
 ### AmiOCR
 AmiOCR class provides methods to extract words from the iamge. Uses Tesseract.
+
+## Timeline
+merged main into nodes_and_pixels and re-branched 
diff --git a/__init__.py b/pyamiimage/__init__.py
similarity index 100%
rename from __init__.py
rename to pyamiimage/__init__.py
diff --git a/src/pyamiimage/old_code/image_lib.py b/pyamiimage/_old_image_lib.py
similarity index 98%
rename from src/pyamiimage/old_code/image_lib.py
rename to pyamiimage/_old_image_lib.py
index fa320d1..ee453a8 100644
--- a/src/pyamiimage/old_code/image_lib.py
+++ b/pyamiimage/_old_image_lib.py
@@ -29,7 +29,7 @@ def __init__(self):
     def old_init(self):
         # should not be run on init
         self.image = data.binary_blobs()
-        self.image = io.imread('../../images/green.png')
+        self.image = io.imread('../../images/green_old.png')
         self.image = rgb2gray(self.image)
         self.image = 255 - self.image
         thresh = threshold_otsu(self.image)
@@ -104,9 +104,9 @@ def blobs(self):
         #        self.image = data.astronaut()
         #        plt.imshow(image)
 
-        self.image = io.imread('../../images/green.png')
+        self.image = io.imread('../../images/green_old.png')
         plt.imshow(self.image)
-        io.imsave('../../outputs/misc1/green.png', self.image)
+        io.imsave('../../outputs/misc1/green_old.png', self.image)
 
         #        images = io.ImageCollection('../images/*.png:../images/*.jpg')
         #        print('Type:', type(images))
@@ -333,7 +333,7 @@ def medial(cls):
         img = img > 20
         """
 
-        img = io.imread('../../images/green.png')
+        img = io.imread('../../images/green_old.png')
         img = rgb2gray(img)
         img = 1 - img
         print(sum(img), img)
diff --git a/pyimage/ami_arrow.py b/pyamiimage/ami_arrow.py
similarity index 99%
rename from pyimage/ami_arrow.py
rename to pyamiimage/ami_arrow.py
index a8a4a7b..4b687b7 100644
--- a/pyimage/ami_arrow.py
+++ b/pyamiimage/ami_arrow.py
@@ -3,10 +3,10 @@
 from lxml import etree
 from lxml.builder import ElementMaker
 # local
-from ..pyimage.ami_graph_all import AmiNode, AmiEdge
-from ..pyimage.ami_util import AmiUtil
-from ..pyimage.svg import SVGArrow, ns_xpath, SVG_NS, GPML_NS
-from ..pyimage.bbox import BBox
+from pyamiimage.ami_graph_all import AmiNode, AmiEdge
+from pyamiimage.ami_util import AmiUtil
+from pyamiimage.svg import SVGArrow, ns_xpath, SVG_NS, GPML_NS
+from pyamiimage.bbox import BBox
 
 logger = logging.getLogger(__name__)
 
diff --git a/pyamiimage/ami_edge_manager.py b/pyamiimage/ami_edge_manager.py
new file mode 100644
index 0000000..d47d483
--- /dev/null
+++ b/pyamiimage/ami_edge_manager.py
@@ -0,0 +1,342 @@
+import logging
+import pprint
+# local
+from pyamiimage.ami_plot import AmiLine, AmiPolyline, AmiLineTool, POLYLINE
+from pyamiimage.ami_graph_all import AmiEdge
+from pyamiimage.ami_util import AmiUtil
+from pyamiimage.bbox import BBox
+
+logger = logging.getLogger(__name__)
+
+COORD = 0
+COUNT = 1
+INTERSECT = "intersect"
+
+X = 0
+Y = 1
+XY = ["X", "y"]
+
+""" terms
+AmiEdge is a graph-edge between 2 AmiNodes. It need not be straight (but normally cannot intersect itself)
+There can be 2 or more AmiEdges between 2 AmiNodes. It normally contains a thread of pixels "points";
+ 
+AmiLine represents straight lines between 2 points (often AmiNodes, but often unnamed). 
+an AmiEdge containa 0, 1 or many AmiLines.
+AmiLines should retain backreferences to the AmiEdges - not fully implemented.
+
+X is horizontal
+Y is vertical (normally increasing down the page, origin is top-left)
+
+"""
+
+
+class AmiEdgeAnalyzer:
+    """
+    contains and analyzes edges, building higher-level objects
+    """
+
+    def __init__(self, tolerance=1, island=None):
+        self.horizontal_edges = None
+        self.horiz_ami_lines = None
+        self.horiz_ami_polylines = []
+        self.horiz_line_tool = None
+        self.horiz_dict = dict()
+        self.line_y_coords_by_count = None
+
+        self.vertical_edges = None
+        self.vert_ami_lines = None
+        self.vert_ami_polylines = []
+        self.vert_line_tool = None
+        self.vert_dict = dict()
+        self.line_x_coords_by_count = None
+
+        self.non_axial_edges = None
+        self.axial_polylines = None
+        self.tolerance = tolerance
+        self.island = island
+
+        self.bbox = None
+
+    def read_edges(self, ami_edges):
+        self.horizontal_edges = AmiEdge.get_horizontal_edges(ami_edges, tolerance=self.tolerance)
+        self.vertical_edges = AmiEdge.get_vertical_edges(ami_edges, tolerance=self.tolerance)
+        self.non_axial_edges = AmiEdge.get_non_axial_edges(ami_edges, tolerance=self.tolerance)
+        print(f"non_axial {len(self.non_axial_edges)}")
+        # remove later
+        for non_axial_edge in self.non_axial_edges:
+            logger.debug(f"non-axial {non_axial_edge.pixel_length()} {round(non_axial_edge.get_cartesian_length(), 2)}"
+                         f" {non_axial_edge.xdelta_direct} {non_axial_edge.ydelta_direct}")
+
+        self.horiz_ami_lines = AmiEdge.get_single_lines(self.horizontal_edges)
+        self.vert_ami_lines = AmiEdge.get_single_lines(self.vertical_edges)
+
+        self.axial_polylines = AmiEdge.get_axial_polylines(ami_edges, tolerance=self.tolerance)
+        self.non_axial_polylines = self.create_non_axial_polylines();
+        self.print_non_axial_edges(debug=False)
+
+        self.extract_lines_from_polylines()
+
+        self.vert_dict = AmiLine.get_horiz_vert_counter(self.vert_ami_lines, xy_index=0)
+        self.horiz_dict = AmiLine.get_horiz_vert_counter(self.horiz_ami_lines, xy_index=1)
+
+    def extract_lines_from_polylines(self):
+        """find horizonatl and verticakl segments in polylines/edges"""
+        logger.debug(f"extracting lines from axial polylines {len(self.axial_polylines)}")
+        for axial_polyline in self.axial_polylines:
+            logger.debug(f"axial polyline: {axial_polyline}")
+            for ami_line in axial_polyline:
+                if ami_line.is_vertical(tolerance=self.tolerance):
+                    self.vert_ami_lines.append(ami_line)
+                elif ami_line.is_horizontal(tolerance=self.tolerance):
+                    self.horiz_ami_lines.append(ami_line)
+                else:
+                    raise ValueError(f"line {ami_line} must be horizontal or vertical")
+
+    def merge_neighbouring_coords(self) -> tuple:
+        """
+        create Counters for vertical lines (x coord) and horizontal (y-coords)
+        :return: x_counter, y_counter
+        """
+        self.line_x_coords_by_count = self._merge_close_bins(self.vert_dict.most_common())
+        self.line_y_coords_by_count = self._merge_close_bins(self.horiz_dict.most_common())
+        return self.line_x_coords_by_count, self.line_y_coords_by_count
+
+    def _merge_close_bins(self, coord_counts):
+        """merge counts of bins within tolerance
+
+        :param coord_counts: Counter of count by coordinate
+        :return: updated counter"""
+
+        filtered_counts = []
+        while len(coord_counts) > 0:
+            coord_count = coord_counts[0]
+            change = False
+            for filtered_count in filtered_counts:
+                if abs(filtered_count[COORD] - coord_count[COORD]) <= self.tolerance:
+                    filtered_count[COUNT] += coord_count[COUNT]
+                    change = True
+                    break
+            if not change:
+                filtered_counts.append([coord_count[COORD], coord_count[COUNT]])
+            coord_counts.remove(coord_count)
+        return filtered_counts
+
+    # TODO move to AmiLineTool
+    def join_ami_lines(self, xy_flag):
+        """
+        join lines with constant coordinate (vert/X, or horiz/Y)
+
+        :param xy_flag: 0 or 1 for X/Y
+        :return: joined lines as list
+
+        default is lines must touch within self.tolerance. gap_factor = 1.0 gives
+        ___   ___   ___ (uniformly spaded white/black)
+
+        """
+
+        line_coords_by_count = self.line_x_coords_by_count if xy_flag == X else self.line_y_coords_by_count
+        line_tool = AmiLineTool(mode=POLYLINE, xy_flag=xy_flag)
+        for this_coord, count in line_coords_by_count:
+            ami_lines = self.find_sorted_ami_lines_with_coord(xy_flag, this_coord)
+            for segment in ami_lines:
+                line_tool.add_segment(segment)
+        return line_tool
+
+    @classmethod
+    def _delta(cls, point0, point1):
+        return min(abs(point0[0] - point1[0]), abs(point0[0] - point1[1]))
+
+    @classmethod
+    def _join(cls, long_ami_line, ami_line):
+        """joins growing end of long_ami_line to ami_line
+        """
+        long_ami_line.xy2 = ami_line.xy2
+        long_ami_line.mid_points.append(ami_line.xy1)
+        return long_ami_line
+
+    # def add_ami_lines_to_long_lines(self, ami_line, long_ami_line):
+    #     """
+    #
+    #     :param ami_line:
+    #     :param long_ami_line:
+    #     :return: list of long_ami_lines
+    #
+    #     """
+    #     long_ami_lines = []
+    #     if long_ami_line is None:
+    #         long_ami_line = ami_line
+    #         long_ami_lines.append(long_ami_line)
+    #         long_ami_line.mid_points = []
+    #         return
+    #     else:
+    #         point0 = long_ami_line.xy2  # joinable point
+    #         point1 = ami_line.xy1  # incoming joinable point
+    #         if self._delta(point0, point1) <= self.tolerance:
+    #             # close enough? join
+    #             long_ami_line = self._join(long_ami_line, ami_line)
+    #         else:
+    #             # no, create new line
+    #             long_ami_line = None
+    #
+    #     return long_ami_lines
+
+    # @classmethod
+    # def create_new_line_with_ascending_coords(cls, ami_line, xy_flag):
+    #     """NOT TESTED"""
+    #     # [[[66, 61], [66, 131]], [[66, 131], [66, 185]],...
+    #     other = 1 - xy_flag
+    #     swap = ami_line.xy1[other] < ami_line.xy2[other]
+    #     xy1 = [ami_line.xy1[xy_flag], ami_line.xy1[other]] if swap else [ami_line.xy2[xy_flag], ami_line.xy2[other]]
+    #     xy2 = [ami_line.xy2[xy_flag], ami_line.xy2[other]] if swap else [ami_line.xy1[xy_flag], ami_line.xy1[other]]
+    #     return AmiLine(xy12=[xy1, xy2])
+
+    def find_sorted_ami_lines_with_coord(self, xy_flag, coord):
+        """
+        finds axial lines with coord within self.tolerance
+        :param xy_flag: X or Y
+        :param coord:
+        :return: list of lines
+        """
+        ami_lines = []
+        lines_to_search = self.vert_ami_lines if xy_flag == X else self.horiz_ami_lines
+        for line in lines_to_search:
+            if abs(line.xy_mid[xy_flag] - coord) <= self.tolerance:
+                ami_lines.append(line)
+        other_coord = 1 - xy_flag
+        ami_lines = sorted(ami_lines, key=lambda linex: linex.xy1[other_coord])
+        return ami_lines
+
+    def create_horiz_vert_line_tools(self, island):
+        """create horizontal and vertical line_tools
+        :param island:
+        :return: horizontal line tool, vertical line tool
+
+        """
+        ami_edges = island.get_or_create_ami_edges()
+        self.read_edges(ami_edges)
+        self.merge_neighbouring_coords()
+        self.vert_line_tool = self.join_ami_lines(X)
+        self.horiz_line_tool = self.join_ami_lines(Y)
+
+    def make_horiz_vert_polylines(self, min_horiz_length=0, min_vert_length=0):
+        self.create_horiz_vert_line_tools(self.island)
+
+        self.horiz_ami_polylines = self._create_polylines(min_horiz_length, self.horiz_line_tool.line_points_list)
+        self.vert_ami_polylines = self._create_polylines(min_vert_length, self.vert_line_tool.line_points_list)
+
+        self.find_crossing_horiz_vert_polylines()
+
+    @classmethod
+    def _create_polylines(cls, min_length, points_list_x):
+        polylines = []
+        for line in points_list_x:
+            ami_polyline = AmiPolyline(points_list=line)
+            if ami_polyline.get_cartesian_length() > min_length:
+                polylines.append(ami_polyline)
+        return polylines
+
+    def find_crossing_horiz_vert_polylines(self):
+        """get crossings of polylines
+        Important use is to detect boxes- there will be a rectangle of 4
+
+        :return: horiz and vert intersection dicts
+        """
+        h_poly_dict = dict()
+        h_poly_dict[INTERSECT] = []
+        v_poly_dict = dict()
+        v_poly_dict[INTERSECT] = []
+        h_point_set = set()
+        v_point_set = set()
+        points_list = []
+        for h_ami_polyline in self.horiz_ami_polylines:
+            h_box = h_ami_polyline.get_bounding_box()
+            for v_ami_polyline in self.vert_ami_polylines:
+                v_box = v_ami_polyline.get_bounding_box()
+                intersect_box = h_box.intersect(v_box)
+                if intersect_box and intersect_box.is_valid():
+                    h_points = h_ami_polyline.find_points_in_box(intersect_box)
+                    if len(h_points) == 1:
+                        h_point = h_points[0]
+                        v_points = v_ami_polyline.find_points_in_box(intersect_box)
+                        if len(v_points) == 1:
+                            v_point = v_points[0]
+                            # print("len_v", len(v_point), v_point[2], v_point[2][0], v_point[2][1])
+                            # there are multiple places where coordinates occur, this is one
+                            v_point_set.add(v_point[2][1])
+                            h_point_set.add(v_point[2][0])
+                            points_list.append(v_point[2])
+                            points_list.append(h_point[2])
+                            h_poly_dict[INTERSECT].append((h_point, v_point, v_ami_polyline.id))
+                            h_lines = h_ami_polyline.split_line(h_point)
+                            v_poly_dict[INTERSECT].append((v_point, h_point, h_ami_polyline.id))
+                            v_lines = v_ami_polyline.split_line(v_point)
+                        else:
+                            logger.error(f"too many v_points {v_points}")
+                    else:
+                        logger.error(f"too many h_points {h_points}")
+        pprinter = pprint.PrettyPrinter(indent=4)
+        # print("h_poly_dict")
+        # pprinter.pprint(h_poly_dict)
+        # print("v_poly_dict")
+        # pprinter.pprint(v_poly_dict)
+        points_list = AmiUtil.make_unique_points_list(points_list, self.tolerance)
+        print(f"points_list {points_list}")
+        self.bbox = BBox.create_from_points(points_list, self.tolerance)
+        print(f"bbox {self.bbox}")
+        # v_point_list = AmiUtil.make_unique_points_list(list(v_point_set), self.tolerance)
+        # print(f"v_points {v_point_list}")
+        # print(f"h_points {h_point_list}")
+        return h_poly_dict, v_poly_dict
+
+    def create_non_axial_polylines(self, debug=False):
+        """merge touching non_axial_edges (hor and vert edges omitted)
+        if three or more non_axials meet at a node, the latter are skipped
+        the result will then be arbitrary
+        Mainly intended as a heuristuice to find plot lines
+        """
+        self.joined_non_axial_polylines = []
+        tolerance = self.tolerance
+        for i, edge_i in enumerate(self.non_axial_edges):
+            for j, edge_j in enumerate(self.non_axial_edges):
+                if j > i:
+                    for ii in range(2):
+                        point_i_ii = edge_i.end_point(ii)
+                        for jj in range(2):
+                            point_j_jj = edge_j.end_point(jj)
+                            if AmiUtil.are_coincident(point_i_ii, point_j_jj, tolerance):
+                                if debug:
+                                    print(f"{i}-{ii} {edge_i} coincides {j}-{jj} {edge_j}")
+                                # JOIN lines
+                                pass
+
+    def print_non_axial_edges(self, debug=False):
+        """prints non-axial edges"""
+        if debug:
+            for i, edge_i in enumerate(self.non_axial_edges):
+                print(f"EDGE {i} {edge_i}")
+
+
+    def explore_horiza_vert_lines(self, bbox_factor, island=None):
+        """creates horizontal and vertical polylines to explore higher-order objects
+        :param bbox_factor: scalefactor for min_horiz and min_vert engths for axial polylines
+        :param island: if None uses self.island else sets self.island"""
+        if island:
+            self.island = island
+        if not self.island:
+            logger.error("no island given")
+            return
+        ami_nodes = self.island.get_ami_nodes()
+        # for ami_node in ami_nodes:
+        #     print(f">>ami_node {ami_node}")
+        bbox = self.island.get_or_create_bbox()
+        # print(f"island bbox {bbox}")
+        self.make_horiz_vert_polylines(
+            min_horiz_length=bbox_factor * bbox.get_width(),
+            min_vert_length=bbox_factor * bbox.get_height(),
+        )
+
+
+
+
+
+
diff --git a/pyimage/ami_graph_all.py b/pyamiimage/ami_graph_all.py
similarity index 90%
rename from pyimage/ami_graph_all.py
rename to pyamiimage/ami_graph_all.py
index 7831d83..8196c8c 100644
--- a/pyimage/ami_graph_all.py
+++ b/pyamiimage/ami_graph_all.py
@@ -9,17 +9,19 @@
 import networkx as nx
 # library
 import numpy as np
-import sknw  # must pip install sknw
+
 from networkx.algorithms import tree
 from skimage import io
 from skimage.measure import approximate_polygon
 
 # local
-from ..pyimage.ami_image import AmiImage
-from ..pyimage.ami_plot import AmiLine
-from ..pyimage.ami_util import AmiUtil
-from ..pyimage.svg import BBox
-from ..pyimage.text_box import TextBox
+from pyamiimage.ami_image import AmiImage
+from pyamiimage.ami_plot import AmiLine, X, Y
+from pyamiimage.ami_util import AmiUtil
+from pyamiimage.svg import BBox
+from pyamiimage.text_box import TextBox
+# copied from sknw of yxdragram because PyPI package is not updated
+import pyamiimage.sknw as sknw
 
 logger = logging.getLogger(__name__)
 
@@ -131,7 +133,7 @@ def get_or_create_ami_node(self, node_id):
 
     # AmiGraph
 
-    def get_or_create_ami_edge_from_nx_edge(self, nx_edge):
+    def get_ami_edge_from_nx_id(self, nx_edge):
         """Wrapper for get_or_create_ami_edge_from_ids() to create ami_edge
         :param nx_edge: 
         :return: ami_edge (or None)
@@ -273,10 +275,10 @@ def debug_edges_and_nodes(cls, nx_edgelist, debug_count=5):
         pts_index = 2
         for edge in nx_edgelist[:debug_count]:
             pts_ = edge[pts_index]['pts']
-            print("points", pts_)
+            logger.warning("points", pts_)
         edgelist_pts_ = nx_edgelist[0][2]['pts']
         for step in edgelist_pts_[:debug_count]:
-            print("step", step)
+            logger.warning("step", step)
             pass
 
     def get_edge_list_ids_through_maximum_spanning_edges(self):
@@ -361,8 +363,6 @@ def get_or_create_all_ami_edges(self):
                     ami_edge = self.get_or_create_ami_edge_from_ids(start_id, end_id)
                     self.ami_edges.append(ami_edge)
 
-            # print(f"ami_edge_dict {self.ami_edge_dict.keys()} ")
-            #       f"=> {self.ami_edge_dict} ")
         return self.ami_edges
 
     def get_ami_edge_list_for_start_end(self, start_id, end_id):
@@ -551,10 +551,6 @@ def get_angles_round_node(self, node_id):
         assert node_ids is not None
         assert node_ids[0] is not None, f"node_ids {node_ids}"
 
-        # xys = {node_id: AmiNode(ami_graph=self, node_id=node_id).centroid_xy for node_id in node_ids}
-        # print(xys.keys())
-
-        # angle_dict = {node1: self.get_angle(node_ids[0], node_id, node1) for node1 in node_ids[1:]}
         angle_dict = {}
         for node1 in node_ids:
             angle = self.get_angle_between_nodes(node_ids[0], node_id, node1)
@@ -609,7 +605,7 @@ def pre_plot_edges(self, plot_target):
             for (start_id, end_id) in self.nx_graph.edges():
                 branch_ids = self.get_branch_ids_for_start_end(start_id, end_id)
                 for branch_id in branch_ids:
-                    print(f" {__name__} branch_ids / id {branch_ids} {branch_id}")
+                    logger.debug(f" {__name__} branch_ids / id {branch_ids} {branch_id}")
                     pts = self.get_points_on_line(start_id, end_id, branch_id=branch_id)
                     ami_edge = self.get_or_create_ami_edge_from_ids(start_id, end_id, branch_id=branch_id)
                     ami_edge.plot_edge(pts, plot_target, edge_id=branch_id)
@@ -620,7 +616,7 @@ def pre_plot_edges(self, plot_target):
                 ami_edge.plot_edge(pts, plot_target)
 
     def get_points_on_line(self, start_id, end_id, branch_id=None):
-        print(f"s/e/b {start_id} {end_id} {branch_id}")
+        logger.debug(f"s/e/b {start_id} {end_id} {branch_id}")
         if self.nx_graph.is_multigraph and type(branch_id) is int:
             try:
                 points = self.nx_graph[start_id][end_id][branch_id][AmiEdge.PTS]
@@ -691,13 +687,6 @@ def get_nx_edge_list_for_node(self, node_id):
 
         return nx_edges
 
-    # def get_angle_to_x(self, edge):
-    #
-    #     n0 = self.nx_graph.nodes[edge[0]]
-    #     n1 = self.nx_graph.nodes[edge[1]]
-    #     print(f"x0 {n0} x1 {n1}")
-    #
-
     # AmiGraph
 
     def get_direct_length(self, nx_edge):
@@ -774,15 +763,10 @@ def get_node_ids_from_graph_with_degree(cls, nx_graph, degree):
         assert type(nx_graph) is nx.Graph or type(nx_graph) is nx.MultiGraph, f"not a graph {type(nx_graph)} "
         return [node_id for node_id in nx_graph.nodes if nx_graph.degree(node_id) == degree]
 
-    # def get_node_ids_with_degree(self, degree):
-    #     """
-    #     iterates over graph.nodes to find those with given degree
-    #     graph may be a subgraph
-    #     :param degree:
-    #     :return: list of node_ids (may be empty)
-    #     """
-    #     return AmiGraph.get_node_ids_from_graph_with_degree(self.nx_graph, degree)
-    #
+    def get_ami_nodes(self):
+        """creates a list of AmniNodes from self.nx_graph.nodes"""
+        return self.create_ami_nodes_from_ids(self.nx_graph.nodes)
+
     def create_ami_nodes_from_ids(self, node_ids):
         """
         creates a list of AmiNodes from a list of node_ids
@@ -931,7 +915,7 @@ def __init__(self, ami_graph, start_id, end_id, branch_id=None, _private=False):
         self.line_points = None
         self.tolerance = 1
         self.segments = None
-        self.get_points()
+        self._extract_points_from_nx()
 
     def __eq__(self, other):
         """equality is based on node_ids and branch_id alone
@@ -970,7 +954,9 @@ def get_id(self):
         """
         return f"{self.start_id}_{self.end_id}_{self.branch_id}"
 
-    def get_points(self):
+    def _extract_points_from_nx(self):
+        """extract points from self.PTS and create self.points_xy
+        """
         assert self.start_id is not None
         assert self.end_id is not None
         edges = self.nx_graph[self.start_id][self.end_id]
@@ -986,7 +972,7 @@ def get_points(self):
         else:
             points = edges[self.PTS]
         if points is not None:
-            self.read_nx_edge_points_yx_into_self_points_xy(points)
+            self._read_nx_edge_points_yx_into_self_points_xy(points)
         return points
 
     # class AmiEdge:
@@ -1001,7 +987,15 @@ def get_cartesian_length(self):
         assert dist is not None
         return dist
 
-    def read_nx_edge_points_yx_into_self_points_xy(self, points_array_yx):
+    @property
+    def first_point(self):
+        return self.points_xy[0] if self.points_xy and len(self.points_xy) > 0 else None
+
+    @property
+    def last_point(self):
+        return self.points_xy[-1] if self.points_xy and len(self.points_xy) > 0 else None
+
+    def _read_nx_edge_points_yx_into_self_points_xy(self, points_array_yx):
         """
         convert from nx_points (held as yarray, xarray) to array(x, y)
         :param points_array_yx:
@@ -1022,8 +1016,8 @@ def __repr__(self):
         s = ""
         if self.points_xy is not None:
             ll = int(self.pixel_length() / 2)
-            s = f"ami_edge {self.start_id}...{self.end_id} ({self.pixel_length()}) " \
-                f"{self.points_xy[:1]}___{self.points_xy[ll - 0:ll + 1]}___{self.points_xy[-1:]}"
+            s = f"ami_edge ids: {self.start_id}...{self.end_id} pixlen ({self.pixel_length()}) " \
+                f"s {self.points_xy[:1]} m {self.points_xy[ll - 0:ll + 1]} e {self.points_xy[-1:]}"
         return s
 
     def has_start_lt_end(self):
@@ -1065,6 +1059,42 @@ def get_or_create_bbox(self):
         logger.debug(f"bbox {self.bbox}")
         return self.bbox
 
+    @property
+    def xrange_direct(self):
+        """The range between the end points
+        for complete range use BBox
+        :return: [xstart, xend]
+        """
+        return [self.first_point[X], self.last_point[X]] if self.first_point and self.last_point else None
+
+    @property
+    def yrange_direct(self):
+        """The range between the end points
+        for complete range use BBox
+        :return: [ystart, yend]
+        """
+        return [self.first_point[Y], self.last_point[Y]] if self.first_point and self.last_point else None
+
+    @property
+    def xdelta_direct(self):
+        """direct x-distance between end points
+        the sign is only meaningful for directed edges
+        :return: distance in pixels
+        """
+        return None if not self.xrange_direct or not self.xrange_direct[0] or not self.xrange_direct[1] \
+            else self.xrange_direct[1] - self.xrange_direct[0]
+
+    @property
+    def ydelta_direct(self):
+        """direct y-distance between end points
+        the sign is only meaningful for directed edges
+        :return: distance in pixels
+        """
+        return None if not self.yrange_direct or not self.yrange_direct[0] or not self.yrange_direct[1] \
+            else self.yrange_direct[1] - self.yrange_direct[0]
+
+    # class AmiEdge:
+
     def create_line_segments(self, tolerance=1):
         """create AmiLine segments from sknw points
         :param tolerance: totlerance in pixels
@@ -1136,7 +1166,6 @@ def plot_edge(self, pts, plot_region, edge_id=None, boxcolor=None):
         :return:
         """
         colors = ["green", "blue", "magenta", "cyan"]
-        # print(f"edge_id {edge_id} pts: {pts[:5]}")
 
         if boxcolor is not None:
             bbox = self.get_or_create_bbox()
@@ -1186,7 +1215,10 @@ def is_vertical(self, tolerance=1):
         segment = self.get_single_segment(tolerance=tolerance)
         return segment is not None and segment.is_vertical(tolerance=tolerance)
 
-    def _get_axial_corners(self, segments, tolerance):
+    # class AmiEdge:
+
+    @classmethod
+    def _get_axial_corners(cls, segments, tolerance):
         """
         finds Hor-Vert and Vert-Hor corners in segments
         :param tolerance:
@@ -1213,6 +1245,25 @@ def get_vertical_edges(cls, ami_edges, tolerance=1):
     def get_horizontal_edges(cls, ami_edges, tolerance=1):
         return list(filter(lambda ami_edge: ami_edge.is_horizontal(tolerance=tolerance), ami_edges))
 
+    @classmethod
+    def get_non_axial_edges(cls, ami_edges, tolerance=1):
+        return list(filter(
+            lambda ami_edge:
+            not ami_edge.is_horizontal(tolerance=tolerance) and not ami_edge.is_vertical(tolerance=tolerance),
+            ami_edges))
+
+    def end_point(self, endx):
+        """select end 0 (first) or 1 (last)
+         :param endx: 0 or 1
+         :return: point [x,y] or None
+         """
+        if not endx or endx < 0 or endx > 1 or not self.points_xy:
+            return None
+        return self.first_point if endx == 0 else self.last_point
+
+    def is_cyclic(self):
+        """start and end are identical and length > 1"""
+        return len(self.points_xy) > 1 and self.start_id and self.start_id == self.end_id
     # =========================================
 
     @classmethod
@@ -1263,9 +1314,7 @@ def move_line_ends_to_closest_node(cls, nx_graph, ij, points, move=False):
         node_pts = [nx_graph.nodes[ij[0]][AmiNode.CENTROID], nx_graph.nodes[ij[1]][AmiNode.CENTROID]]
         delta_dist = None
         if move:
-            # print(f"line end {points[0]} moved to {node_pts[0]}")
             points[0] = node_pts[0]
-            # print(f"line end {points[-1]} moved to {node_pts[1]}")
             points[-1] = node_pts[1]
         else:
             delta_dist = math.dist(pts[0], node_pts[0]) + math.dist(pts[1], node_pts[1])
@@ -1291,91 +1340,24 @@ def is_normalized_edge(cls, start_id, end_id):
     def get_coords(self):
         return self.points_xy
 
-    # def douglas_peucker2(self):
-    #     """https://towardsdatascience.com/simplify-polylines-with-the-douglas-peucker-algorithm-ac8ed487a4a1
-    #     Experimental - may not keep
-    #     """
-    #     import matplotlib.animation as animation
-    #     import numpy as np
-    #
-    #     def rdp(points, epsilon):
-    #         # get the start and end points
-    #         start = np.tile(np.expand_dims(points[0], axis=0), (points.shape[0], 1))
-    #         end = np.tile(np.expand_dims(points[-1], axis=0), (points.shape[0], 1))
-    #
-    #         # find distance from other_points to line formed by start and end
-    #         dist_point_to_line = np.abs(np.cross(end - start, points - start, axis=-1)) / np.linalg.norm(end - start,
-    #                                                                                                      axis=-1)
-    #         # get the index of the points with the largest distance
-    #         max_idx = np.argmax(dist_point_to_line)
-    #         max_value = dist_point_to_line[max_idx]
-    #
-    #         result = []
-    #         if max_value > epsilon:
-    #             partial_results_left = rdp(points[:max_idx + 1], epsilon)
-    #             result += [list(i) for i in partial_results_left if list(i) not in result]
-    #             partial_results_right = rdp(points[max_idx:], epsilon)
-    #             result += [list(i) for i in partial_results_right if list(i) not in result]
-    #         else:
-    #             result += [points[0], points[-1]]
-    #
-    #         return result
-    #
-    #     if __name__ == "__main__":
-    #         min_x = 0
-    #         max_x = 5
-    #
-    #         xs = np.linspace(min_x, max_x, num=200)
-    #         ys = np.exp(-xs) * np.cos(2 * np.pi * xs)
-    #         sample_points = np.concatenate([
-    #             np.expand_dims(xs, axis=-1),
-    #             np.expand_dims(ys, axis=-1)
-    #         ], axis=-1)
-    #
-    #         # First set up the figure, the axis, and the plot element we want to animate
-    #         fig = plt.figure()
-    #         ax = plt.axes(xlim=(min_x, max_x), ylim=(-1, 1))
-    #         plt.xlabel("x")
-    #         plt.ylabel("y")
-    #         text_values = ax.text(
-    #             0.70,
-    #             0.15,
-    #             "",
-    #             transform=ax.transAxes,
-    #             fontsize=12,
-    #             verticalalignment='top',
-    #             bbox=dict(boxstyle='round',
-    #                       facecolor='wheat',
-    #                       alpha=0.2)
-    #         )
-    #         original_line, = ax.plot(xs, ys, lw=2, label=r"$y = e^{-x}cos(2 \pi x)$")
-    #         simplified_line, = ax.plot([], [], lw=2, label="simplified", marker='o', color='r')
-    #
-    #         # initialization function: plot the background of each frame
-    #         def init():
-    #             simplified_line.set_data(xs, ys)
-    #             return original_line, simplified_line, text_values
-    #
-    #         # animation function.  This is called sequentially
-    #         def animate(i):
-    #             epsilon = 0 + (i * 0.1)
-    #             simplified = np.array(rdp(sample_points, epsilon))
-    #             print(f"i: {i}, episilon: {'%.1f' % epsilon}, n: {simplified.shape[0]}")
-    #             simplified_line.set_data(simplified[:, 0], simplified[:, 1])
-    #             text_values.set_text(fr"$\epsilon$: {'%.1f' % epsilon}, $n$: {simplified.shape[0]}")
-    #             return original_line, simplified_line, text_values
-    #
-    #         # call the animator.  blit=True means only re-draw the parts that have changed.
-    #         anim = animation.FuncAnimation(
-    #             fig,
-    #             animate,
-    #             init_func=init,
-    #             frames=21,
-    #             interval=1000,
-    #             repeat=True
-    #         )
-    #         plt.legend()
-    #         plt.show()
+    @classmethod
+    def get_common_node_id(cls, id0, id1):
+        """find node common to two edges
+        (ignores multiple edges at present)
+        (1,2,0 and (2,3,0) have 2 in common
+        (1,2,0) and (3,4,0) have None in common
+        (1,2,0) and (1,2,1)  will return 1 or 2 at random
+        :param id0: first edge (could be duple or triple)
+        :param id1: as for first edge
+        :return: common node id or None
+        """
+        if len(id0) < 2 or len(id1) < 2:
+            raise ValueError("bad ids {id0} {id1}")
+        if id0[0] == id1[0] or id0[0] == id1[1]:
+            return id0[0]
+        if id0[1] == id1[0] or id0[1] == id1[1]:
+            return id0[1]
+        return None
 
 
 """a wrapper for an sknw/nx node, still being developed"""
@@ -1412,7 +1394,7 @@ def __init__(self, node_id, ami_graph=None, nx_graph=None, _private=False):
         if node_id is None:
             raise ValueError("AmiNode must have node_id")
         if len(str(node_id)) > 4:
-            print(f"ami_graph {ami_graph}, nx_graph {nx_graph}")
+            logger.error(f"ami_graph {ami_graph}, nx_graph {nx_graph}")
             raise Exception(f"id should be simple {node_id}")
 
         self.ami_graph = ami_graph
@@ -1660,7 +1642,7 @@ def create_nx_edges(self):
 
         :return: list of graph edges
         """
-        logger.warning(f"{__name__} Probably should use subgraph instead")
+        # logger.warning(f"{__name__} Probably should use subgraph instead")
         assert self.ami_graph.nx_graph is not None
         nx_graph = self.ami_graph.nx_graph
         self.nx_edges = []
@@ -1681,11 +1663,17 @@ def get_node_ids_of_degree(self, node_count):
         """
         return AmiGraph.get_node_ids_from_graph_with_degree(self.island_nx_graph, node_count)
 
+    def get_ami_nodes(self):
+        """
+        :return: ami_nodes
+        """
+        return [self.ami_graph.get_or_create_ami_node(node_id) for node_id in self.island_nx_graph.nodes]
+
     def get_or_create_ami_edges(self):
         self.create_nx_edges()
         self.ami_edges = []
         for edge in self.nx_edges:
-            ami_edge = self.ami_graph.get_or_create_ami_edge_from_nx_edge(edge)
+            ami_edge = self.ami_graph.get_ami_edge_from_nx_id(edge)
             self.ami_edges.append(ami_edge)
         return self.ami_edges
 
diff --git a/pyimage/ami_image.py b/pyamiimage/ami_image.py
similarity index 81%
rename from pyimage/ami_image.py
rename to pyamiimage/ami_image.py
index 2b36530..2899177 100644
--- a/pyimage/ami_image.py
+++ b/pyamiimage/ami_image.py
@@ -11,22 +11,53 @@
 from pathlib import Path
 import os
 import matplotlib.pyplot as plt
+from sklearn.cluster import KMeans
+from PIL import Image
 
+TEMP_DIR = Path(__file__).parent.parent
+TEMP_DIR = Path(TEMP_DIR, "temp")
+
+FILE = "file"
+RGBA = "rgba"
+RGB = "rgb"
+GRAY = "gray"
+# print(f"TEMP {TEMP_DIR}")
 
 class AmiImage:
     """
     instance and class methods for holding and converting images
     """
+
     def __init__(self):
         """
         for when we need to store state
         :return:
         """
-        pass
+        self.image_dict = dict()
 
 # ========== legacy methods that need integrating
 
-# PMR
+    def read_file(self, file):
+        assert file, "file should not be None"
+        assert file.exists(), "{file} must exist"
+        try:
+            image = io.imread(file)
+        except Exception as e:
+            raise e
+        self.image_dict[FILE] = file
+        self.store_image(image)
+
+    def store_image(self, image):
+        if image is None:
+            return
+        if self.has_alpha_channel_shape(image):
+            self.image_dict[RGBA] = image
+        elif self.has_rgb_shape(image):
+            self.image_dict[RGB] = image
+        elif self.has_gray_shape(image):
+            self.image_dict[GRAY] = image
+
+    # PMR
     @classmethod
     def create_grayscale_from_file(cls, path):
         """
@@ -267,6 +298,45 @@ def write(cls, path, image, mkdir=False, overwrite=True):
             os.remove(path)
         io.imsave(path, image)
 
+    @classmethod
+    def kmeans(cls, raw_image, n_colors, background):
+        """finds kmeans in colour space and projects image into each mean
+        :param raw_image: raw multicolor image, gets reshaped
+        :param n_colors: number of kmeans to extract
+        :param background: to add in extracted images
+        :return: (labels, centers_i, quantized_images) ;
+            labels are per-pixel ints (don't know what)
+            centers_i are RGB values at kmeans-centers,
+            quantized_images are single colour+background
+        :except: may throw "cannot reshape"
+        """
+        print(f"raw {raw_image.shape}")
+        col_layers = raw_image.shape[2]  # find colour layers
+
+        if AmiImage.has_alpha_channel_shape(raw_image):
+            fname = Path(TEMP_DIR, "junk_rgba.png")
+            io.imsave(fname, raw_image)
+            raw_image = AmiImage.create_rgb_from_rgba(raw_image)
+            fname = Path(TEMP_DIR, "junk_rgb.png")
+            # this is awful, don't know why the rgb image can't be analysed
+            # save the image, and then re-read
+            io.imsave(fname, raw_image)
+            raw_image = io.imread(fname)
+        col_layers = raw_image.shape[2]
+        print(f"COLORS: {col_layers} {raw_image.shape}")
+        # this might raise error
+        reshaped_image = raw_image.reshape((-1, col_layers))
+        kmeans = KMeans(n_clusters=n_colors, random_state=42).fit(reshaped_image)
+        labels = kmeans.labels_
+        color_centers = kmeans.cluster_centers_
+        centers_i = [[int(center[0]), int(center[1]), int(center[2])] for center in color_centers]
+        colors_image = color_centers[labels].reshape(raw_image.shape).astype('uint8')
+        quantized_images = []
+        for i, center_i in enumerate(centers_i):
+            quantized_images.append(np.where(colors_image == centers_i[i], [centers_i[i]], background))
+        return (labels, centers_i, quantized_images)
+
+
     @classmethod
     def write_image_group(cls, dir, images, filename="default", mkdir=True, overwrite=True):
         """write multiple(n) images with the same filename numbered from 1-n"""
@@ -276,6 +346,7 @@ def write_image_group(cls, dir, images, filename="default", mkdir=True, overwrit
             print(path)
             AmiImage.write(path, image, mkdir, overwrite)
 
+
 class AmiImageDTO():
     """
     Data Transfer Object for images and downstream artefacts
diff --git a/pyimage/ami_ocr.py b/pyamiimage/ami_ocr.py
similarity index 98%
rename from pyimage/ami_ocr.py
rename to pyamiimage/ami_ocr.py
index 09fde27..d22f653 100644
--- a/pyimage/ami_ocr.py
+++ b/pyamiimage/ami_ocr.py
@@ -2,7 +2,6 @@
 from pathlib import Path
 from tkinter import BASELINE
 import numpy as np
-import codecs
 import pytesseract
 from lxml import etree as et
 from PIL import Image
@@ -10,16 +9,9 @@
 from skimage import io
 from matplotlib import pyplot as plt
 
-
-
-try:
-    from bbox import BBox
-    from cleaner import WordCleaner
-    from ami_image import AmiImage
-except: 
-    from ..pyimage.bbox import BBox
-    from ..pyimage.cleaner import WordCleaner
-    from ..pyimage.ami_image import AmiImage
+from pyamiimage.bbox import BBox
+from pyamiimage.cleaner import WordCleaner
+from pyamiimage.ami_image import AmiImage
 
 class TextBox():
     # TextBox inherits BBox
@@ -457,8 +449,6 @@ def extract_labels_from_plot(cls, image, plot_area_bbox):
         :returns: AmiOCR object for the whole plot
         """
         new_img = BBox.plot_bbox_on(image, plot_area_bbox)
-        io.imshow(new_img)
-        io.show()
         label_bboxes = AmiOCR.label_bboxes_from_plot_bbox(image, plot_area_bbox)
         # # x_label = AmiOCR.copy_bbox_from_img(image, label_bboxes['x'])
         # # y_label = AmiOCR.copy_bbox_from_img(image, label_bboxes['y'])
diff --git a/pyamiimage/ami_plot.py b/pyamiimage/ami_plot.py
new file mode 100644
index 0000000..58ca69e
--- /dev/null
+++ b/pyamiimage/ami_plot.py
@@ -0,0 +1,564 @@
+import logging
+from collections import Counter
+# local
+from pyamiimage.ami_util import AmiUtil
+from pyamiimage.bbox import BBox
+
+logger = logging.getLogger(__name__)
+
+HEAD = "head"
+
+DASHED = "dashed"
+POLY = "poly"  # polyline or polygon
+POLYGON = "polygon"
+POLYLINE = "polyline"
+TARGETS = [POLY, POLYGON, POLYLINE]
+
+SEGMENT = "segment"
+TAIL = "tail"
+X = 0
+Y = 1
+
+
+class AmiPlot:
+    pass
+
+
+class AmiLine:
+    """This will probably include a third-party tool supporting geometry for lines
+
+    It may also be a polyline
+    """
+
+    def __init__(self, xy12=None, ami_edge=None):
+        """xy12 of form [[x1, y1], [x2, y2]]
+        direction is xy1 -> xy2 if significant"""
+        self.ami_edge = ami_edge
+        self.xy1 = None
+        self.xy2 = None
+        if xy12 is not None:
+            if len(xy12) != 2 or len(xy12[0]) != 2 or len(xy12[1]) != 2:
+                raise ValueError(f"bad xy pair for line {xy12}")
+            self.xy1 = [xy12[0][X], xy12[0][Y]]
+            self.xy2 = [xy12[1][X], xy12[1][Y]]
+        # ami_nodes = []
+
+    def __repr__(self):
+        return str([self.xy1, self.xy2])
+
+    def __str__(self):
+        return str([self.xy1, self.xy2])
+
+    def set_ami_edge(self, ami_edge):
+        self.ami_edge = ami_edge
+
+    @property
+    def vector(self):
+        """vector between end points
+        :return: xy2 - xy1
+        """
+        return None if (self.xy1 is None or self.xy2 is None) else \
+            (self.xy2[X] - self.xy1[X], self.xy2[Y] - self.xy1[Y])
+
+    @property
+    def xy_mid(self):
+        """get midpoint of line
+        :return: 2-array [x, y] of None if coords not set"""
+
+        if self.xy1 is not None and self.xy2 is not None:
+            return [(self.xy1[X] + self.xy2[X]) / 2, (self.xy1[Y] + self.xy2[Y]) // 2]
+        return None
+
+    def is_horizontal(self, tolerance=1) -> int:
+        return abs(self.vector[Y]) <= tolerance < abs(self.vector[X])
+
+    def is_vertical(self, tolerance=1) -> int:
+        return abs(self.vector[X]) <= tolerance < abs(self.vector[Y])
+
+    @classmethod
+    def get_horiz_vert_counter(cls, ami_lines, xy_index) -> Counter:
+        """
+        counts midpoint coordinates of lines (normally ints)
+
+        :param ami_lines: horiz or vert ami_lines
+        :param xy_index: 0 (x) 0r 1 (y) (normally 0 for vert lines, 1 for horiz)
+        :return: Counter
+        """
+        hv_dict = Counter()
+        for ami_line in ami_lines:
+            xy_mid = ami_line.xy_mid[xy_index]
+            if xy_mid is not None:
+                hv_dict[int(xy_mid)] += 1
+        return hv_dict
+
+    def get_min(self, xy_flag):
+        return None if xy_flag is None else min(self.xy1[xy_flag], self.xy2[xy_flag])
+
+    def get_max(self, xy_flag):
+        return None if xy_flag is None else max(self.xy1[xy_flag], self.xy2[xy_flag])
+
+class AmiPolyline:
+    """polyline. can represent solid or dashed (NYI) lines. contains attachment points
+    """
+
+    def __init__(self, points_list=None, ami_edge=None, tolerance=1):
+        """
+        polyline contains a list of connected points and also
+        :param points_list:
+        :param ami_edge:
+        :param tolerance:
+        """
+        self.ami_edge = ami_edge
+        self.points_list = []  # 2D coords
+        self.bbox = None
+        if points_list:
+            for point in points_list:
+                self.points_list.append([point[0], point[1]])
+        self.tolerance = tolerance
+
+    @property
+    def id(self):
+        idd = None
+        if self.points_list:
+            p0 = self.points_list[0]
+            idd = str(p0[0]) + "_" + str(p0[1])
+            idd += "__"
+            p1 = self.points_list[-1]
+            idd += str(p1[0]) + "_" + str(p1[1])
+        return idd
+
+    def get_bounding_box(self):
+        if not self.bbox and self.points_list:
+            self.bbox = BBox()
+            for point in self.points_list:
+                self.bbox.add_coordinate(point)
+            if self.tolerance and self.tolerance > 0:
+                self.bbox.expand_by_margin(self.tolerance)
+        return self.bbox
+
+    def __repr__(self):
+        return str(self.points_list)
+
+    def __str__(self):
+        return str(self.points_list)
+
+    @property
+    def vector(self):
+        """vector between end points
+        :return: xy2 - xy1
+        """
+        pl = self.points_list
+        return [pl[0][X] - pl[-1][X], pl[1][Y] - pl[1][Y]]
+
+    @property
+    def xy_mid(self):
+        """get midpoint of line
+        :return: 2-array [x, y] of None if coords not set"""
+
+        if self.points_list and len(self.points_list) > 1:
+            pl = self.points_list
+            return [(pl[0][X] - pl[-1][X]) // 2, (pl[1][Y] - pl[1][Y]) // 2]
+        return None
+
+    def is_horizontal(self, tolerance=1) -> int:
+        return abs(self.vector[Y]) <= tolerance < abs(self.vector[X])
+
+    def is_vertical(self, tolerance=1) -> int:
+        return abs(self.vector[X]) <= tolerance < abs(self.vector[Y])
+
+    @classmethod
+    def get_horiz_vert_counter(cls, ami_lines, xy_index) -> Counter:
+        """
+        NYI
+        counts midpoint coordinates of lines (normally ints)
+
+        :param ami_lines: horiz or vert ami_lines
+        :param xy_index: 0 (x) 0r 1 (y) (normally 0 for vert lines, 1 for horiz)
+        :return: Counter
+        """
+        hv_dict = Counter()
+        for ami_line in ami_lines:
+            xy_mid = ami_line.xy_mid[xy_index]
+            if xy_mid is not None:
+                hv_dict[int(xy_mid)] += 1
+        return hv_dict
+
+    def range(self, tolerance=None):
+        """range from min to max; only works for axial lines
+        if start/end points are the same return None
+
+        """
+        if tolerance is None:
+            tolerance = self.tolerance
+        if not self.points_list or len(self.points_list) < 2:
+                return None
+        dx = self.points_list[-1][0] - self.points_list[0][0]
+        dy = self.points_list[-1][1] - self.points_list[0][1]
+
+        if abs(dx) <= tolerance and abs(dy) <= tolerance:
+            return None
+        if abs(dx) <= tolerance:
+            l = [self.points_list[-1][1], self.points_list[0][1]] if dy < 0 else [self.points_list[0][1], self.points_list[-1][1]]
+            return l
+        if abs(dy) <= tolerance:
+            l = [self.points_list[-1][0], self.points_list[0][0]] if dy < 0 else [self.points_list[0][0], self.points_list[-1][0]]
+            return l
+        return None
+        # raise ValueError(f"cannot calculate range {self.points_list}")
+
+    def get_attachment_points(self):
+        if self.points_list and len(self.points_list) >= 2:
+            return self.points_list[1:-1]
+        return None
+
+    def get_cartesian_length(self):
+        """gets length for axial polylines
+        :return: abs distance in axial coordinate else NaN"""
+        range = self.range()
+        return float("NaN") if range is None else abs(range[0] - range[1])
+
+    def find_points_in_box(self, bbox):
+        """iterates over all points including ends in polyline
+        :param bbox: BBox within which point must fit
+        :return: """
+        points_in_box = []
+        size = len(self.points_list)
+        for i, point in enumerate(self.points_list):
+            if bbox.contains_point(point):
+                points_in_box.append((i, i-size, point))
+        return points_in_box
+
+    def number_of_points(self):
+        """
+        :return: 0 if no points_list else number of points
+        """
+        return len(self.points_list) if self.points_list else 0
+
+    def split_line(self, point_triple):
+        """split polyline at point
+        :param point_triple: triple created by AmiPolyline.find_points_in_box() (index_left, index_right, coords)
+        :param polyline: to split
+        :return: two lines, if one is length 0, nul and orginal polyline
+        """
+        lines = [None, None]
+        l = self.number_of_points()
+        if point_triple[0] == 0:
+            lines[1] = self
+        elif point_triple[0] == -1:
+            lines[0] = self
+        else:
+            lines[0] = self.sub_polyline(0, point_triple[0])
+            lines[1] = self.sub_polyline(point_triple[0], l-1)
+
+        return lines
+
+    def sub_polyline(self, index0, index1):
+        """slice line at points , keeping both
+        not pythonic
+        """
+        polyline = AmiPolyline(self.points_list[index0:index1+1])
+        return polyline
+
+
+class AmiLineTool:
+    """joins points or straight line segments
+    final target can be:
+    may contain tools for further analysis
+    still being developed.
+    end
+    mode=SEGMENT assembles segments into one of more polylines
+    """
+
+    def __init__(self, tolerance=1, points=None, ami_edge=None, mode=POLYLINE, xy_flag=None):
+        """build higher-order objects frome line segments or points
+        :param tolerance: for determing overlapping points
+        :param points: points to make polyline/gons
+        :param ami_edge: possibly obsolete
+        :param mode: POLYLINE (default)
+        :"""
+        self.point_set = set()
+        self.points = []
+        self.tolerance = tolerance
+        self.mode = mode
+        self.xy_flag = xy_flag
+        if mode not in TARGETS:
+            raise ValueError(f"unknown target {mode}")
+        if ami_edge is not None:
+            raise ValueError("emi_edges not yet supported")
+        if points is not None:
+            for point in points:
+                self.add_point(point)
+        ami_polylines = []  # NYI
+        self.line_points_list = []  # obsolete?
+        self.polygons = []  # obsolete?
+
+    def __repr__(self):
+        return str(self.points)
+
+    def __str__(self):
+        return str(self.points)
+
+    def add_point(self, point):
+        """add point
+        :param point:
+        """
+        AmiLineTool._validate_point(point)
+        # if point in self.point_set:
+        #     raise ValueError("Cannot add point twice {point}")
+        if point in self.points:
+            raise ValueError("Cannot add point twice {point}")
+        self.points.append(point)
+
+    def points(self):
+        return self.points
+
+    def add_segment(self, segment):
+        """segment tail must match head
+
+        Segment is of form [  [x1,y1], [x2, y2] ]
+        Polyline is [[x1,y1], [x2, y2], [x3, y3] ... ]
+        poly_list - is [[[x1,y1], [x2, y2], [x3, y3] ... ], [[x4,y4], [x5, y5], [x6, y6] ... ]]
+        """
+        if type(segment) is AmiLine:
+            segment = [segment.xy1, segment.xy2]
+        self._validate_segment(segment)
+        if self.mode == POLYLINE or self.mode == POLYGON:
+            polyline = [segment[0], segment[1]]
+            self.add_merge_polyline_to_poly_list(polyline)
+        else:
+            # if len(self.points) == 0:
+            #     self.add_point(segment[0])
+            #     self.add_point(segment[1])
+            # elif self._are_coincident_points(self.points[-1], segment[0]):
+            #     self.add_point(segment[1])
+            # else:
+            #     raise ValueError("Cannot add segment {segment} (no overlap)")
+            pass
+
+    def add_segments(self, segments):
+        """
+        add segments piecewise to self.polylines
+        recommend sorting highest first e.g.
+        [
+        [[2, 3], [5, 7]],
+        [[1, 2], [2, 3]]
+        ]
+        rather than
+        [
+        [[1, 2], [2, 3]],
+        [[2, 3], [5, 7]]
+        ]
+        :param segments:
+        :return:
+        """
+        for segment in segments:
+            self.add_segment(segment)
+
+    def _are_coincident_points(self, point1, point2):
+        """
+
+        overlaps if either dx or dy >= self.tolerance
+        :param point1: [x, y]
+        :param point2: [x, y]
+        :return:
+        """
+        AmiLineTool._validate_point(point1)
+        AmiLineTool._validate_point(point2)
+        return AmiUtil.are_coincident(point1, point2, self.tolerance)
+
+
+    def _validate_segment(self, segment):
+        if type(segment) is list:
+            AmiLineTool._validate_point(segment[0])
+            AmiLineTool._validate_point(segment[1])
+        else:
+            assert type(segment) is AmiLine, f"segment was {type(segment)}"
+            AmiLineTool._validate_point(segment[0])
+            AmiLineTool._validate_point(segment[1])
+
+    @classmethod
+    def _validate_point(cls, point, message=""):
+        assert type(point) is list, f"{message} found {type(point)}"
+        assert len(point) == 2, f"{message} found {len(point)}"
+        assert AmiUtil.is_number(point[0])
+
+    def _copy_point(self, point):
+        AmiLineTool._validate_point(point)
+        return [point[0], point[1]]
+
+    def copy_segment(self, segment):
+        return [self._copy_point(segment[0]), self._copy_point(segment[1])]
+
+    def add_points(self, points):
+        assert type(points) is list
+        for point in points:
+            self.add_point(point)
+
+    # def insert_point(self, pos, point):
+    #     AmiLineTool._validate_point(point)
+    #     self.points.insert(0, [33, 44])
+    #     self.points.insert(pos, point)
+
+    def add_merge_polyline_to_poly_list(self, polyline_to_add):
+        """
+        adds/merges polylines using copy semantics.
+        assumes the lines to be added are suitable for merging into polylines or polygons
+        (i.e. all final nodes will be 1 or 2 - connected
+
+
+        :param polyline_to_add: polyline to add (may be anynumber of points, 1, 2, many
+        """
+        if self.line_points_list == []:
+            polylinex = []
+            for point in polyline_to_add:
+                polylinex.append([point[0], point[1]])
+            self.line_points_list.append(polylinex)
+        else:
+            added_polyline = None
+            # have to consider directions of lines
+            for polyline in self.line_points_list:
+                added_polyline = self.join_heads_and_tails(polyline, polyline_to_add)
+                if added_polyline is not None:
+                    break
+
+            if added_polyline is not None:
+                self.find_cycles(added_polyline, polyline)
+            else:
+                self.copy_and_append(polyline_to_add)
+
+    def find_cycles(self, added_polyline, polyline):
+        """is the new polyline cyclic
+        if so, remove polyline and convert to polygon
+        """
+        if self._are_coincident_points(added_polyline[0], added_polyline[-1]):
+            logger.debug(f"CYCLIC {added_polyline}")
+            if self.mode == POLYGON:
+                self.line_points_list.remove(polyline)
+                point0 = polyline[0]
+                polyline.remove(point0)
+                self.polygons.append(polyline)
+
+    def join_heads_and_tails(self, polyline, polyline_to_add):
+        """join two polylines with
+         :param polyline: existing loyline in self.polylines
+         :param polyline_to_add: polyline being added
+
+         Each line has a tail (0) and head (-1) There are 4 possible
+         joinings HH, HT, TH, TT (or none).
+
+         """
+        added_polyline = None
+        if self._are_coincident_points(polyline_to_add[-1], polyline[0]):
+            # tail-head
+            logger.debug(f"add truncated {polyline_to_add} to {polyline}")
+            self.copy_prepend(polyline_to_add[:-1][::-1], polyline)
+            added_polyline = polyline
+
+        elif self._are_coincident_points(polyline[0], polyline_to_add[0]):
+            # tail-tail
+            logger.debug(f"tail-tail {polyline_to_add} to {polyline}")
+            logger.debug(f"{polyline_to_add} => {polyline_to_add[1:]} to {polyline}")
+            self.copy_prepend(polyline_to_add[1:], polyline)
+            added_polyline = polyline
+
+        elif self._are_coincident_points(polyline_to_add[0], polyline[-1]):
+            # head-tail
+            self.copy_append(polyline_to_add[1:], polyline)
+            added_polyline = polyline
+
+        elif self._are_coincident_points(polyline_to_add[-1], polyline[-1]):
+            # head-head
+            add_ = polyline_to_add[::-1][1:]
+            logger.debug(f" adding {polyline_to_add} => {add_} to {polyline}")
+            self.copy_append(add_, polyline)
+            added_polyline = polyline
+
+        else:
+            logger.debug(f" polyline_to_add {polyline_to_add} does not overlap {polyline}")
+
+        return added_polyline
+
+    @classmethod
+    def copy_append(cls, line_points_to_add, target_line_points):
+        logger.debug(f"adding {line_points_to_add} to {target_line_points}")
+        for point in line_points_to_add:
+            point1 = [point[0], point[1]]
+            target_line_points.append(point1)
+
+    @classmethod
+    def copy_prepend(cls, line_points_to_add, line_points_list):
+        """prepends polyline_to_add to front of list
+        uses insert(0, ...) which is SLOW unless lines are deques
+        """
+        for point in line_points_to_add:
+            point1 = [point[0], point[1]]
+            line_points_list.insert(0, point1)
+
+    def copy_and_append(self, polyline_to_add):
+        """
+        copies into new polyline and adds this to self.polylines
+        :param polyline_to_add: polyline to add may have 1,2, more points
+        """
+        line_points = []
+        for point in polyline_to_add:
+            line_points.append([point[0], point[1]])
+        self.line_points_list.append(line_points)
+
+
+class AmiEdgeTool:
+    """refines edges (join, straighten, break, corners, segments, curves, etc some NYI)
+    Still being actively developed
+    """
+
+    def __init__(self, ami_graph=None, ami_edges=None, ami_nodes=None):
+        """Best to create this from the factory method create_tool"""
+        self.ami_graph = ami_graph
+        self.ami_edges = ami_edges
+        self.ami_nodes = ami_nodes
+
+    @classmethod
+    def create_tool(cls, ami_graph, ami_edges=None, ami_nodes=None):
+        """preferred method of instantiating tool
+        :param ami_graph: required graph
+        :param ami_edges: edges to process
+        :param ami_nodes: optional nodes (if none uses ends of edges)
+        """
+        edge_tool = AmiEdgeTool(ami_graph, ami_edges=ami_edges, ami_nodes=ami_nodes)
+        if not ami_nodes:
+            edge_tool.create_ami_nodes_from_edges()
+
+        return edge_tool
+
+    def analyze_topology(self):
+        """counts nodes and edges by recursively iterating over
+        noes and their edges -> edges and their nodes
+        also only includes start_id < end_id
+        (mainly a check)
+
+         :return: nodes, edges"""
+
+        if self.ami_edges is None:
+            logger.error(f"no edges, possible error")
+        new_ami_edges = set()
+        new_ami_nodes = set()
+        while self.ami_nodes:
+            ami_node = self.ami_nodes.pop()
+            new_ami_nodes.add(ami_node)
+            node_ami_edges = ami_node.get_or_create_ami_edges()
+            for ami_edge in node_ami_edges:
+                if ami_edge.has_start_lt_end():
+                    if ami_edge not in self.ami_edges:
+                        logger.error(f" cannot find {ami_edge} in edges")
+                    else:
+                        new_ami_edges.add(ami_edge)
+        return new_ami_nodes, new_ami_edges
+
+    def create_ami_nodes_from_edges(self):
+        """generates unique ami_nodes from node_ids at ends of edges"""
+        if not self.ami_nodes:
+            self.ami_nodes = set()
+            node_ids = set()
+            for ami_edge in self.ami_edges:
+                node_ids.add(ami_edge.start_id)
+                node_ids.add(ami_edge.end_id)
+            self.ami_nodes = self.ami_graph.create_ami_nodes_from_ids(node_ids)
diff --git a/src/pyamiimage/ami_skeleton.py b/pyamiimage/ami_skeleton.py
similarity index 97%
rename from src/pyamiimage/ami_skeleton.py
rename to pyamiimage/ami_skeleton.py
index ff38584..a44943e 100644
--- a/src/pyamiimage/ami_skeleton.py
+++ b/pyamiimage/ami_skeleton.py
@@ -12,12 +12,12 @@
 from skimage import io
 import logging
 # local
-from pyamiimage.pyimage.ami_image import AmiImage
-from ..pyimage.ami_util import AmiUtil
+from pyamiimage.ami_image import AmiImage
+from pyamiimage.ami_util import AmiUtil
 
-from ..pyimage.bbox import BBox
-from ..pyimage.flood_fill import FloodFill
-from ..pyimage.ami_graph_all import AmiGraph
+from pyamiimage.bbox import BBox
+from pyamiimage.flood_fill import FloodFill
+from pyamiimage.ami_graph_all import AmiGraph
 
 logger = logging.getLogger(__name__)
 
@@ -122,7 +122,7 @@ def plot_edges_nodes_and_title_GRAPH(self, title, plot_plot=True):
         :return:
         """
         for edge_xy in self.edge_xy_list:
-            plt.plot(edge_xy[:, 1], np.negative(edge_xy[:, 0]), 'green')
+            plt.plot(edge_xy[:, 1], np.negative(edge_xy[:, 0]), 'green_old')
         # draw node by small circle (".")
         plt.plot(self.node_xy[:, 1], np.negative(self.node_xy[:, 0]), 'r.')
         # title and show
@@ -157,7 +157,7 @@ def get_coords_for_nodes_and_edges_from_nx_graph_GRAPH(cls, nx_graph):
         return node_xy, edge_xy_list
 
     def extract_bbox_for_nodes_ISLAND(self, ami_island):
-        from pyimage import AmiIsland
+        from pyamiimage import AmiIsland
         """
         gets bounding box for a list of nodes in
 
diff --git a/pyimage/ami_util.py b/pyamiimage/ami_util.py
similarity index 64%
rename from pyimage/ami_util.py
rename to pyamiimage/ami_util.py
index 964a643..ce72a8e 100644
--- a/pyimage/ami_util.py
+++ b/pyamiimage/ami_util.py
@@ -6,6 +6,8 @@
 import numpy.linalg as LA
 import math
 
+X = 0
+Y = 1
 
 class AmiUtil:
 
@@ -50,6 +52,59 @@ def is_number(cls, s):
         except ValueError:
             return False
 
+    @classmethod
+    def int2hex(cls, ii):
+        """convert int (0-255) to 2-character hex string
+        :param ii: integer
+        :return: 2-digit hex string of form 01, 09, 0f, 10, ff , or None if not int 0-125
+        """
+        if ii is None or not type(ii) is int or ii < 0 or ii> 255:
+            return None
+        return ('0' + hex(ii)[2:])[-2:]
+
+    @classmethod
+    def is_white(cls, color, delta=20, sat=255):
+        """is color white within given tolerance
+        255 - color[i] < delta
+         """
+        if color is None or len(color) != 3 or not AmiUtil.is_number(color[0]) \
+                or not AmiUtil.is_number(delta):
+            return False
+        for i in range(3):
+            if sat - color[i] > delta:
+                return False
+        return True
+
+    @classmethod
+    def is_black(cls, color, delta=20):
+        """is color white within given tolerance
+        color[i] < delta
+         """
+        if color is None or len(color) != 3 or not AmiUtil.is_number(color[0]) \
+                or not AmiUtil.is_number(delta):
+            return False
+        for i in range(3):
+            if color[i] > delta:
+                return False
+        return True
+
+
+    @classmethod
+    def is_gray(cls, color, delta=20):
+        """is color gray within given tolerance
+        color is triple of numbers , mean is its mean
+        if abs(color[i] - mean) > delta) return False
+         """
+        if color is None or len(color) != 3 or not AmiUtil.is_number(color[0]) \
+                or not AmiUtil.is_number(delta):
+            return False
+        mean = (color[0] + color[1] + color[2]) / 3
+        for i in range(3):
+            if abs(color[i] - mean) > delta:
+                return False
+        return True
+
+
     @classmethod
     def get_xy_from_sknw_centroid(cls, yx):
         """
@@ -76,11 +131,11 @@ def make_numpy_assert(cls, numpy_array, shape=None, maxx=None, dtype=None):
         assert type(numpy_array) is np.ndarray, \
             f"object should be numpy.darray, found {type(numpy_array)} \n {numpy_array}"
         if shape:
-            assert numpy_array.shape == shape, f"shape should be {numpy_array.shape}"
+            assert numpy_array.shape == shape, f"shape was {numpy_array.shape} should be {shape}"
         if maxx:
-            assert np.max(numpy_array) == maxx, f"max should be {np.max(numpy_array)}"
+            assert np.max(numpy_array) == maxx, f"maxx was {np.max(numpy_array)}, shou,d be {maxx}"
         if dtype:
-            assert numpy_array.dtype == dtype, f"dtype should be {numpy_array.dtype}"
+            assert numpy_array.dtype == dtype, f"dtype was {numpy_array.dtype} should be {dtype}"
 
     @classmethod
     def get_angle(cls, p0, p1, p2):
@@ -190,6 +245,41 @@ def to_float_array(cls, arr):
     def swap_yx_to_xy(cls, yx):
         return [yx[1], yx[0]]
 
+    @classmethod
+    def are_coincident(cls, point1, point2, tolerance=0.001):
+        """are two points coincident within a tolerance?
+        uses abs(deltax) + abs(deltay) <= tolerance
+        :param point1:
+        :param point2:
+        :param tolerance: default 0.001
+        :return: true if sum of detas within tolerance
+        """
+        if not point1 or not point2:
+            return False
+        if abs(point1[X] - point2[X]) + abs(point1[Y] - point2[Y]) <= tolerance:
+            return True
+        else:
+            return False
+
+    @classmethod
+    def make_unique_points_list(cls, points, tolerance):
+        """merge points which are within tolerance
+        simplistic (probably O(n**2)
+        compares each point with each other
+        :param points: list of points
+        :param tolerance:
+        """
+        new_points = []
+        for point in points:
+            exist = False
+            for new_point in new_points:
+                if AmiUtil.are_coincident(point, new_point, tolerance):
+                    exist = True
+                    break
+            if not exist:
+                new_points.append(point)
+        return new_points
+
 class Vector2:
 
     def __init__(self, v2):
diff --git a/src/pyamiimage/bbox.py b/pyamiimage/bbox.py
similarity index 78%
rename from src/pyamiimage/bbox.py
rename to pyamiimage/bbox.py
index 5c82738..b8e6162 100644
--- a/src/pyamiimage/bbox.py
+++ b/pyamiimage/bbox.py
@@ -1,6 +1,9 @@
 """bounding box"""
 from skimage import draw
-# from ..pyimage.svg import SVGG, SVGRect
+
+import math
+from pyamiimage.ami_util import AmiUtil
+
 
 class BBox:
     """bounding box 2array of 2arrays, based on integers
@@ -41,9 +44,33 @@ def create_from_xy_w_h(cls, xy, width, height):
         try:
             xy_ranges = [[float(xy[0]), float(xy[0]) + float(width)], [float(xy[1]), float(xy[1]) + float(height)]]
         except Exception as e:
-            raise ValueError(f"cannot create bbox from {xy},{width},{height}")
+            raise ValueError(f"cannot create bbox from {xy},{width},{height}, {e}")
         return BBox(xy_ranges=xy_ranges)
 
+    @classmethod
+    def create_from_points(cls, points_list, tolerance=0.001):
+        """make bounding box if 4 points can be aligned in a rectangle within tolerance
+        choose low/low and high/high ; don't use othe values except as True/False
+        :param points_list: reqyires len=4
+        :param tolerance: must be aligned within tolerance; default 0.001
+        :return: BBox [lowx,  lowy] [highx, highy] => [[lowx, highx], [lowy, highy]] or None
+        """
+        if not points_list:
+            return None
+        if not points_list or len(points_list) != 4:
+            return None
+        high_high = cls._find_low_low_high_high(points_list, 1, tolerance)
+        low_low = cls._find_low_low_high_high(points_list, -1, tolerance)
+        print(f"high_high {high_high}")
+        print(f"lo_lo {low_low}")
+        bbox = None
+        if low_low and high_high:
+            bbox = BBox(xy_ranges=[
+                [low_low[0], high_high[0]],
+                [low_low[1], high_high[1]]
+            ])
+        return bbox
+
     def set_ranges(self, xy_ranges):
         if xy_ranges is None:
             raise ValueError("no lists given")
@@ -74,7 +101,7 @@ def get_width(self):
         """get width
         :return: width or None if x range invalid or not set"""
         if self.get_xrange() is None or len(self.get_xrange()) == 0:
-            return None;
+            return None
         assert self.get_xrange() is not None
         assert len(self.get_xrange()) == 2, f"xrange, got {len(self.get_xrange())}"
         return self.get_xrange()[1] - self.get_xrange()[0]
@@ -228,8 +255,8 @@ def change_range(self, index, margin):
         if index != 0 and index != 1:
             raise ValueError(f"Bad index for range {index}")
         rr = self.xy_ranges[index]
-        rr[0] -= margin[index]
-        rr[1] += margin[index]
+        rr[0] -= margin
+        rr[1] += margin
         # range cannot be <= 0
         if rr[0] >= rr[1]:
             mid = (rr[0] + rr[1]) / 2
@@ -331,13 +358,13 @@ def plot_bbox_on(cls, image, bbox):
         """
         Plots bbox on an image
         :param: image
-        :type: numpy array 
+        :type: numpy array
         :param: bbox
         :type: BBox or list
         :returns: fig, ax
         """
         # bbox can either be BBox object or in form of [[a, b][c, d]]
-        
+
         # if type(bbox) == BBox:
         #     assert bbox.is_valid()
         # elif type(bbox) == list:
@@ -352,7 +379,7 @@ def plot_bbox_on(cls, image, bbox):
         if point_pair[0][0] > image.shape[0] or point_pair[0][1] >image.shape[1]:
             # if the starting point is outside the image, ignore bbox
             return image
-        
+
         try:
             row, col = draw.rectangle_perimeter(start=point_pair[0], end=point_pair[1])
             image[row, col] = 0
@@ -398,6 +425,68 @@ def create_from_corners(cls, xy1, xy2):
         bbox = BBox(xy_ranges=[xrange, yrange])
         return bbox
 
+    def contains_point(self, point):
+        """does point lie within xy_ranges inclusive
+        :param point: 2D numeric array [x, y]
+        :return: False if point is None or self is invalid or point lies outside
+        """
+        if not BBox.validate_point(point) or not self.is_valid():
+            return False
+        if point[0] < self.xy_ranges[0][0] or point[0] > self.xy_ranges[0][1]:
+            return False
+        if point[1] < self.xy_ranges[1][0] or point[1] > self.xy_ranges[1][1]:
+            return False
+        return True
+
+    @classmethod
+    def validate_point(cls, point):
+        if point is None or len(point) != 2:
+            return False
+        return AmiUtil.is_number(point[0]) and AmiUtil.is_number(point[1])
+
+    @classmethod
+    def _find_low_low_high_high(cls, points_list, sign, tolerance=0.001):
+        """
+        :param points_list: list of 4 points [x,y]
+        :param sign: 1 for high_high or -1 for low_low
+        :param tolerance: default 0.001
+        :return: point with lowestx, lowesty (sign=-1) or highest, higest (sign=1)
+        """
+        point = None
+        if points_list and len(points_list) == 4:
+            for p in points_list:
+                if point is None:
+                    point = p
+                else:
+                    if cls._is_movable(p[0], point[0], tolerance, sign) and cls._is_movable(p[1], point[1], tolerance,
+                                                                                            sign):
+                        point = p
+        return point
+
+    @classmethod
+    def _is_movable(cls, coord_new, coord_old, tolerance, sign):
+        if abs(coord_new - coord_old) < tolerance:
+            return True
+        if math.copysign(1, coord_new - coord_old) == sign:
+            return True
+
+    @classmethod
+    def assert_xy_ranges(cls, bbox, target_ranges):
+        """asserts that bbox has given xy_ranges (main equality test)
+        :param bbox: Bbox
+        :param target_ranges: 2-D array of floats [[x0,x1], y0,y1]]
+        :except: throws variaous exceptions
+        """
+        if not bbox:
+            if not target_ranges:
+                # expected None
+                return
+            else:
+                raise ValueError("parameter/s are None")
+        if not bbox.is_valid():
+            raise ValueError("Bad bbox {bbox}")
+        assert bbox.xy_ranges == target_ranges, f"bbox_xy_ranges {bbox.xy_ranges}, target_ranges {target_ranges}"
+
 
 """If you looking for the overlap between two real-valued bounded intervals, then this is quite nice:
 
diff --git a/pyimage/cleaner.py b/pyamiimage/cleaner.py
similarity index 100%
rename from pyimage/cleaner.py
rename to pyamiimage/cleaner.py
diff --git a/pyimage/pyamiimage.py b/pyamiimage/commandline.py
similarity index 92%
rename from pyimage/pyamiimage.py
rename to pyamiimage/commandline.py
index 6ef9d83..e1a7d6e 100644
--- a/pyimage/pyamiimage.py
+++ b/pyamiimage/commandline.py
@@ -1,7 +1,7 @@
 import argparse
 import sys
 
-from ami_ocr import AmiOCR
+from pyamiimage.ami_ocr import AmiOCR
 
 
 class Pyamiimage:
@@ -47,7 +47,4 @@ def handlecli(self):
 def main():
     """Runs the CLI"""
     pyamiimage = Pyamiimage()
-    pyamiimage.handlecli()
-
-if __name__ == "__main__":
-    main()
\ No newline at end of file
+    pyamiimage.handlecli()
\ No newline at end of file
diff --git a/pyimage/flood_fill.py b/pyamiimage/flood_fill.py
similarity index 100%
rename from pyimage/flood_fill.py
rename to pyamiimage/flood_fill.py
diff --git a/src/pyamiimage/image_exploration.py b/pyamiimage/image_exploration.py
similarity index 72%
rename from src/pyamiimage/image_exploration.py
rename to pyamiimage/image_exploration.py
index 1380cad..4bfa073 100644
--- a/src/pyamiimage/image_exploration.py
+++ b/pyamiimage/image_exploration.py
@@ -8,7 +8,8 @@
 from skimage.morphology import disk  # noqa
 import numpy as np
 
-from pyimage.ami_image import AmiImage
+from pyamiimage.ami_image import AmiImage
+from pyamiimage.ami_util import AmiUtil
 
 class Exploration:
 
@@ -49,7 +50,7 @@ def explore_erode_dilate(self):
 
         white = Exploration.create_white_network_snippet("snippet_rgba.png")
 
-        Exploration.make_numpy_assert(white, shape=(341, 796), max=1, dtype=np.bool)
+        AmiUtil.make_numpy_assert(white, shape=(341, 796), maxx=255, dtype=np.int64)
 
         footprint = disk(1)
         eroded = erosion(white, footprint)
@@ -90,7 +91,7 @@ def create_white_network_snippet(cls, png):
         assert path.exists(), f"path {path} exists"
         white = AmiImage.create_white_binary_from_file(path)
 
-        Exploration.make_numpy_assert(white, shape=(341, 796), max=1, dtype=np.bool)
+        AmiUtil.make_numpy_assert(white, shape=(341, 796), maxx=255, dtype=np.int64)
 
         return white
 
@@ -148,44 +149,43 @@ def axis_layout(cls, ax, axis, fig):
 
 # =========== palettes ============
 
-"""
-From https://stackoverflow.com/questions/45523205/get-rgb-colors-from-color-palette-image-and-apply-to-binary-image
-
-You can use a combination of a reshape and np.unique to extract the unique RGB values from your color palette image:
-"""
-# Load the color palette
-from skimage import io
-raise NotImplemented("image explore, needs biosynth3??")
-palette = io.imread(os.image.join(os.getcwd(), 'color_palette.png'))
-
-# Use `np.unique` following a reshape to get the RGB values
-palette = palette.reshape(palette.shape[0]*palette.shape[1], palette.shape[2])
-palette_colors = np.unique(palette, axis=0)
-"""
-(Note that the axis argument for np.unique was added in numpy version 1.13.0, so you may need to upgrade numpy for this to work.)
+    def explore_palette(cls):
+        """
+        From https://stackoverflow.com/questions/45523205/get-rgb-colors-from-color-palette-image-and-apply-to-binary-image
 
-Once you have palette_colors, you can pretty much use the code you already have to save the image, except you now add the different RGB values instead of copies of ~img to your img_rgba array.
-"""
-img = None  # TODO
-for p in range(palette_colors.shape[0]):
+        You can use a combination of a reshape and np.unique to extract the unique RGB values from your color palette image:
+        """
+        # Load the color palette
+        from skimage import io
+        raise NotImplemented("image explore, needs biosynth3??")
+        palette = io.imread(os.image.join(os.getcwd(), 'color_palette.png'))
+
+        # Use `np.unique` following a reshape to get the RGB values
+        palette = palette.reshape(palette.shape[0]*palette.shape[1], palette.shape[2])
+        palette_colors = np.unique(palette, axis=0)
+        """
+        (Note that the axis argument for np.unique was added in numpy version 1.13.0, so you may need to upgrade numpy for this to work.)
+        
+        Once you have palette_colors, you can pretty much use the code you already have to save the image, except you now add the different RGB values instead of copies of ~img to your img_rgba array.
+        """
+        img = None  # TODO
+        for p in range(palette_colors.shape[0]):
 
-    # Create an MxNx4 array (RGBA)
-    img_rgba = np.zeros((img.shape[0], img.shape[1], 4), dtype=np.uint8)
+            # Create an MxNx4 array (RGBA)
+            img_rgba = np.zeros((img.shape[0], img.shape[1], 4), dtype=np.uint8)
 
-    # Fill R, G and B with appropriate colors
-    for c in range(3):
-        img_rgba[:,:,c] = img.astype(np.uint8) * palette_colors[p,c]
+            # Fill R, G and B with appropriate colors
+            for c in range(3):
+                img_rgba[:,:,c] = img.astype(np.uint8) * palette_colors[p,c]
 
-    # For alpha just use the image again (makes background transparent)
-    img_rgba[:,:,3] = img.astype(np.uint8) * 255
+            # For alpha just use the image again (makes background transparent)
+            img_rgba[:,:,3] = img.astype(np.uint8) * 255
 
-    # Save image
-    io.imsave('img_col'+str(p)+'.png', img_rgba)
+            # Save image
+            io.imsave('img_col'+str(p)+'.png', img_rgba)
 
 # (Note that you need to use np.uint8 as datatype for your image, since binary images obviously cannot represent different colors.)
-"""
 
-if __name__ == '__main__':
-    Exploration().sharpen_explore(axis=True)
-    Exploration().explore_erode_dilate()
-"""
\ No newline at end of file
+# if __name__ == '__main__':
+#     Exploration().sharpen_explore(axis=True)
+#     Exploration().explore_erode_dilate()
diff --git a/pyamiimage/imageanal.py b/pyamiimage/imageanal.py
new file mode 100644
index 0000000..a420236
--- /dev/null
+++ b/pyamiimage/imageanal.py
@@ -0,0 +1,82 @@
+import configargparse
+import sys
+
+
+class ImageAnalysis:
+    """This is to test PMR args, will probably disappear when Anuv creates some"""
+    version = "0.0.1"
+
+    def __init__(self):
+        pass
+
+    def handlecli(self):
+        """Handles the command line interface using argparse"""
+        version = self.version
+
+        parser = configargparse.ArgParser(
+            description=f"Welcome to ImageAnalysis version {version}. -h or --help for help",
+            add_config_file_help=False,
+        )
+        parser.add_argument(
+            "--image",
+            is_config_file=True,
+            help="image ... ",
+        )
+        parser.add_argument(
+            "-i",
+            "--input",
+            help="inout file",
+        )
+
+        parser.add_argument(
+            "--octree",
+            help="octree parameters as name=value pairs; ncol=<cols> ",
+            nargs="+"
+        )
+
+        parser.add_argument(
+            "-v",
+            "--version",
+            default=False,
+            action="store_true",
+            help="output the version number",
+        )
+        if len(sys.argv) == 1:
+            parser.print_help(sys.stderr)
+            return
+
+        args = parser.parse_args()
+        print(f" <input> {args.input} ; <octree> {args.octree}")
+        if args.input:
+            self.input(args.input)
+        if args.octree:
+            self.octree(args.octree)
+
+    def input(self, args):
+        print(f"INPUT: {args}")
+
+    def octree(self, args):
+        print(f"OCTREE: {len(args)}")
+        dikt = dict()
+        for arg in args:
+            bits = arg.split("=")
+            dikt[bits[0]]=bits[1]
+        print(f" {dikt}")
+
+def demo():
+    """
+
+    :return:
+    """
+    image_analysis = ImageAnalysis()
+    image_analysis.read_file()
+
+
+def main():
+    """Runs the CLI"""
+    image_analysis = ImageAnalysis()
+    image_analysis.handlecli()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/pyamiimage/octree.py b/pyamiimage/octree.py
similarity index 92%
rename from src/pyamiimage/octree.py
rename to pyamiimage/octree.py
index 8859eed..39101e1 100644
--- a/src/pyamiimage/octree.py
+++ b/pyamiimage/octree.py
@@ -5,8 +5,31 @@
 # library
 from pathlib import Path
 from PIL import Image
-# local
-from ..pyimage.old_code.ColorModule import Color
+
+class Color(object):
+    """
+    Simple RGB class
+    """
+
+    def __init__(self, red=0, green=0, blue=0, alpha=None):
+        """
+        Initialize color
+        """
+        self.red = red
+        self.green = green
+        self.blue = blue
+        self.alpha = alpha
+
+    def to_hex(self):
+        return f'{self.red:02x}' + f'{self.green:02x}' + f'{self.blue:02x}'
+
+    def is_equal(self, col):
+        # print("l", len(col))
+        return len(col) == 3 and col[0] == self.red and col[1] == self.green and col[2] == self.blue
+
+    def as_tuple(self):
+        return self.red, self.green, self.blue
+
 
 class OctreeNode(object):
     """
diff --git a/pyamiimage/sknw.py b/pyamiimage/sknw.py
new file mode 100644
index 0000000..f80c6a7
--- /dev/null
+++ b/pyamiimage/sknw.py
@@ -0,0 +1,187 @@
+import numpy as np
+from numba import jit
+import networkx as nx
+
+def neighbors(shape):
+    dim = len(shape)
+    block = np.ones([3]*dim)
+    block[tuple([1]*dim)] = 0
+    idx = np.where(block>0)
+    idx = np.array(idx, dtype=np.uint8).T
+    idx = np.array(idx-[1]*dim)
+    acc = np.cumprod((1,)+shape[::-1][:-1])
+    return np.dot(idx, acc[::-1])
+
+@jit(nopython=True) # my mark
+def mark(img, nbs): # mark the array use (0, 1, 2)
+    img = img.ravel()
+    for p in range(len(img)):
+        if img[p]==0:continue
+        s = 0
+        for dp in nbs:
+            if img[p+dp]!=0:s+=1
+        if s==2:img[p]=1
+        else:img[p]=2
+
+@jit(nopython=True) # trans index to r, c...
+def idx2rc(idx, acc):
+    rst = np.zeros((len(idx), len(acc)), dtype=np.int16)
+    for i in range(len(idx)):
+        for j in range(len(acc)):
+            rst[i,j] = idx[i]//acc[j]
+            idx[i] -= rst[i,j]*acc[j]
+    rst -= 1
+    return rst
+    
+@jit(nopython=True) # fill a node (may be two or more points)
+def fill(img, p, num, nbs, acc, buf):
+    img[p] = num
+    buf[0] = p
+    cur = 0; s = 1; iso = True;
+    
+    while True:
+        p = buf[cur]
+        for dp in nbs:
+            cp = p+dp
+            if img[cp]==2:
+                img[cp] = num
+                buf[s] = cp
+                s+=1
+            if img[cp]==1: iso=False
+        cur += 1
+        if cur==s:break
+    return iso, idx2rc(buf[:s], acc)
+
+@jit(nopython=True) # trace the edge and use a buffer, then buf.copy, if use [] numba not works
+def trace(img, p, nbs, acc, buf):
+    c1 = 0; c2 = 0;
+    newp = 0
+    cur = 1
+    while True:
+        buf[cur] = p
+        img[p] = 0
+        cur += 1
+        for dp in nbs:
+            cp = p + dp
+            if img[cp] >= 10:
+                if c1==0:
+                    c1 = img[cp]
+                    buf[0] = cp
+                else:
+                    c2 = img[cp]
+                    buf[cur] = cp
+            if img[cp] == 1:
+                newp = cp
+        p = newp
+        if c2!=0:break
+    return (c1-10, c2-10, idx2rc(buf[:cur+1], acc))
+   
+@jit(nopython=True) # parse the image then get the nodes and edges
+def parse_struc(img, nbs, acc, iso, ring):
+    img = img.ravel()
+    buf = np.zeros(131072, dtype=np.int64)
+    num = 10
+    nodes = []
+    for p in range(len(img)):
+        if img[p] == 2:
+            isiso, nds = fill(img, p, num, nbs, acc, buf)
+            if isiso and not iso: continue
+            num += 1
+            nodes.append(nds)
+    edges = []
+    for p in range(len(img)):
+        if img[p] <10: continue
+        for dp in nbs:
+            if img[p+dp]==1:
+                edge = trace(img, p+dp, nbs, acc, buf)
+                edges.append(edge)
+    if not ring: return nodes, edges
+    for p in range(len(img)):
+        if img[p]!=1: continue
+        img[p] = num; num += 1
+        nodes.append(idx2rc([p], acc))
+        for dp in nbs:
+            if img[p+dp]==1:
+                edge = trace(img, p+dp, nbs, acc, buf)
+                edges.append(edge)
+    return nodes, edges
+    
+# use nodes and edges build a networkx graph
+def build_graph(nodes, edges, multi=False, full=True):
+    os = np.array([i.mean(axis=0) for i in nodes])
+    if full: os = os.round().astype(np.uint16)
+    graph = nx.MultiGraph() if multi else nx.Graph()
+    for i in range(len(nodes)):
+        graph.add_node(i, pts=nodes[i], o=os[i])
+    for s,e,pts in edges:
+        if full: pts[[0,-1]] = os[[s,e]]
+        l = np.linalg.norm(pts[1:]-pts[:-1], axis=1).sum()
+        graph.add_edge(s,e, pts=pts, weight=l)
+    return graph
+
+def mark_node(ske):
+    buf = np.pad(ske, (1,1), mode='constant').astype(np.uint16)
+    nbs = neighbors(buf.shape)
+    acc = np.cumprod((1,)+buf.shape[::-1][:-1])[::-1]
+    mark(buf, nbs)
+    return buf
+    
+def build_sknw(ske, multi=False, iso=True, ring=True, full=True):
+    buf = np.pad(ske, (1,1), mode='constant').astype(np.uint16)
+    nbs = neighbors(buf.shape)
+    acc = np.cumprod((1,)+buf.shape[::-1][:-1])[::-1]
+    mark(buf, nbs)
+    nodes, edges = parse_struc(buf, nbs, acc, iso, ring)
+    return build_graph(nodes, edges, multi, full)
+    
+# draw the graph
+def draw_graph(img, graph, cn=255, ce=128):
+    acc = np.cumprod((1,)+img.shape[::-1][:-1])[::-1]
+    img = img.ravel()
+    for (s, e) in graph.edges():
+        eds = graph[s][e]
+        if isinstance(graph, nx.MultiGraph):
+            for i in eds:
+                pts = eds[i]['pts']
+                img[np.dot(pts, acc)] = ce
+        else: img[np.dot(eds['pts'], acc)] = ce
+    for idx in graph.nodes():
+        pts = graph.nodes[idx]['pts']
+        img[np.dot(pts, acc)] = cn
+
+if __name__ == '__main__':
+    import matplotlib.pyplot as plt
+
+    img = np.array([
+        [0,0,0,1,0,0,0,0,0],
+        [0,0,0,1,0,0,0,1,0],
+        [0,0,0,1,0,0,0,0,0],
+        [1,1,1,1,0,0,0,0,0],
+        [0,0,0,0,1,0,0,0,0],
+        [0,1,0,0,0,1,0,0,0],
+        [1,0,1,0,0,1,1,1,1],
+        [0,1,0,0,1,0,0,0,0],
+        [0,0,0,1,0,0,0,0,0]])
+
+    node_img = mark_node(img)
+    para = [{'iso':False}, {'ring':False}, {'full':False},
+            {'iso':True}, {'ring':True}, {'full':True}]
+    for i,p,k in zip([1,2,3,4,5,6], [231,232,233,234,235,236], para):
+        print(k)
+        graph = build_sknw(img, False, **k)
+        ax = plt.subplot(p)
+        ax.imshow(node_img[1:-1,1:-1], cmap='gray')
+
+        # draw edges by pts
+        for (s,e) in graph.edges():
+            ps = graph[s][e]['pts']
+            ax.plot(ps[:,1], ps[:,0], 'green')
+            
+        # draw node by o
+        nodes = graph.nodes()
+        ps = np.array([nodes[i]['o'] for i in nodes])
+        ax.plot(ps[:,1], ps[:,0], 'r.')
+
+        # title and show
+        ax.title.set_text(k)
+    plt.show()
\ No newline at end of file
diff --git a/src/pyamiimage/svg.py b/pyamiimage/svg.py
similarity index 99%
rename from src/pyamiimage/svg.py
rename to pyamiimage/svg.py
index cab5e73..737a13b 100644
--- a/src/pyamiimage/svg.py
+++ b/pyamiimage/svg.py
@@ -5,10 +5,7 @@
 from pathlib import Path
 import logging
 # local
-try:
-    from ..pyimage.bbox import BBox
-except:
-    from pyimage.bbox import BBox
+from pyamiimage.bbox import BBox
 
 logger = logging.getLogger(__name__)
 
diff --git a/src/pyamiimage/tesseract_hocr.py b/pyamiimage/tesseract_hocr.py
similarity index 96%
rename from src/pyamiimage/tesseract_hocr.py
rename to pyamiimage/tesseract_hocr.py
index ae16541..049daed 100644
--- a/src/pyamiimage/tesseract_hocr.py
+++ b/pyamiimage/tesseract_hocr.py
@@ -5,12 +5,7 @@
 from lxml.etree import Element, QName
 from lxml import etree
 
-from ..pyimage.svg import XMLNamespaces
-
-try:
-    from pyami.py4ami import wikimedia
-except ImportError:
-    print("Cannot load pyami wikimedia")
+from pyamiimage.svg import XMLNamespaces
 
 """
 This file is to play with the output of hocr
@@ -39,6 +34,15 @@
 TEXT_BOX_FONT = "sans-serif"
 
 
+
+class AmiHOCR:
+    """parses HOCR to create accessible datastructure"""
+    def __init__(self) -> None:
+        pass
+
+    
+
+
 class TesseractOCR:
     """Wrapper for pytesseract + addtional methods for phrase detection"""
     def __init__(self) -> None:
@@ -133,8 +137,8 @@ def extract_bbox_from_hocr(cls, hocr_element):
             if child.attrib['class'] == 'ocrx_word':
                 # coordinates for bbox has the following format: 'bbox 333 74 471 102; x_wconf 76'
                 # we are only interested in the coordinates, so we split at ; and append the first part
-                bboxes.append(child.attrib['title'].split(';')[0])
-                words.append(child.text)
+                    bboxes.append(child.attrib['title'].split(';')[0])
+                    words.append(child.text)
 
         # now we have a list with string elenments like 'bbox 333 74 471 102'
         # first we convert each string into a list, and then remove 'bbox', leaving just the coordinate
@@ -196,9 +200,11 @@ def wikidata_lookup(cls, phrases):
         :param phrases: to lookup
         :return: tuple (list of qitem ids, list of descriptions
         """
-        lookup = wikimedia.WikidataLookup()
-        qitems, desc = lookup.lookup_items(phrases)
-        return qitems, desc
+        # TODO add wikidata lookup via pyami
+        # lookup = wikimedia.WikidataLookup()
+        # qitems, desc = lookup.lookup_items(phrases)
+        # return qitems, desc
+        pass
 
     @classmethod
     def output_phrases_to_file(cls, phrase_list, output_file):
diff --git a/pyimage/text_box.py b/pyamiimage/text_box.py
similarity index 97%
rename from pyimage/text_box.py
rename to pyamiimage/text_box.py
index 53f75c9..e4cc1b7 100644
--- a/pyimage/text_box.py
+++ b/pyamiimage/text_box.py
@@ -5,12 +5,8 @@
 import matplotlib.pyplot as plt
 import matplotlib.patches as patches
 
-try:
-    from ..pyimage.bbox import BBox
-    from ..pyimage.tesseract_hocr import TesseractOCR
-except:
-    from pyimage.bbox import BBox
-    from pyimage.tesseract_hocr import TesseractOCR
+from pyamiimage.bbox import BBox
+from pyamiimage.tesseract_hocr import TesseractOCR
 
 logger = logging.getLogger(__name__)
 logger.setLevel(logging.DEBUG)
diff --git a/pyimage/__init__.py b/pyimage/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/pyimage/ami_edge.py-old b/pyimage/ami_edge.py-old
deleted file mode 100644
index ccc3310..0000000
--- a/pyimage/ami_edge.py-old
+++ /dev/null
@@ -1,39 +0,0 @@
-"""wrapper for edge from sknw/nx, still being developed"""
-
-from pyimage.svg import BBox
-
-
-class AmiEdge:
-    def __init__(self):
-        self.points_xy = None
-        self.bbox = None
-
-    def read_nx_edge_points_yx(self, points_array_yx):
-        """
-        convert from nx_points (held as yarray, xarray) to array(x, y)
-        :param points_array_yx:
-        :return:
-        """
-        # points are in separate columns (y, x)
-        # print("coord", points[:, 1], points[:, 0], 'green')
-        # I can't get list comprehension to work, help needed!
-        # self.points_xy = [   [point[1], point[0]] for point in points_array_yx]:
-        assert points_array_yx is not None and points_array_yx.ndim == 2 and points_array_yx.shape[1] == 2
-        self.points_xy = []
-        for point in points_array_yx:
-            self.points_xy.append([point[1], point[0]])
-        # print ("points_xy", self.points_xy)
-
-    def __repr__(self):
-        s = ""
-        if self.points_xy is not None:
-            s = f"ami edge pts: {self.points_xy[0]} .. {len(str(self.points_xy))} .. {self.points_xy[-1]}"
-        return s
-
-    def get_or_create_bbox(self):
-        if self.bbox is None and self.points_xy is not None:
-            self.bbox = BBox()
-            for point in self.points_xy:
-                self.bbox.add_coordinate(point)
-
-        return self.bbox
diff --git a/pyimage/ami_graph.py-old b/pyimage/ami_graph.py-old
deleted file mode 100644
index 1ea74cf..0000000
--- a/pyimage/ami_graph.py-old
+++ /dev/null
@@ -1,316 +0,0 @@
-"""supports/wraps nx_graphs from N etworkX"""
-import numpy as np
-import networkx as nx
-import copy
-from networkx.algorithms import tree
-from skimage import io
-import sknw  # must pip install sknw
-import logging
-from pathlib import PosixPath
-
-from pyimage.ami_image import AmiImage
-from pyimage.util import Util
-import pyimage.ami_island
-from pyimage.ami_node import AmiNode
-from pyimage.ami_skeleton import AmiSkeleton
-from pyimage.ami_edge import AmiEdge
-
-
-class AmiGraph:
-    """holds AmiNodes and AmiEdges
-    may also hold subgraphs
-    """
-
-    logger = logging.getLogger("ami_graph")
-
-    def __init__(self, nx_graph, generate_nodes=True, nd_skeleton=None):
-        """create fro nodes and edges"""
-        if nx_graph is None:
-            raise Exception(f"nx_graph cannot be None")
-        self.nx_graph = nx_graph
-        AmiGraph.debugx(self, "init")
-
-        self.ami_edges = None
-        self.ami_nodes = None
-        self.ami_island_list = None
-        self.nd_skeleton = nd_skeleton
-
-        self.read_nx_graph(nx_graph)
-        assert self.nx_graph is not None, f"ami_graph.nx_graph should not be None"
-        return
-
-    @classmethod
-    def debugx(cls, ami_graph, msg):
-        print(f"dir ami_graph {msg} {dir(ami_graph)} \n contains nx_graph {'nx_graph' in dir(ami_graph)}")
-        assert 'nx_graph' in dir(ami_graph)
-
-    def get_nx_graph(self):
-        """try to avoid circular imports or attribute not found"""
-        return self.nx_graph
-
-    def add_raw_node(self, raw_node, fail_on_duplicate=False):
-        """add a raw node either a string or string-indexed dict
-        if already a dict, deepcopy it
-        if a primitive make a node_dict and start it with raw_node as id
-        :raw_node: node to add, must have key
-        :fail_on_duplicate: if true fail if key already exists
-        """
-        if raw_node is not None:
-            # ami_node = AmiNode()
-            key = raw_node.key if type(raw_node) is dict else str(raw_node)
-            key = "n" + str(key)
-            if key in self.ami_node_dict and fail_on_duplicate:
-                raise AmiGraphError(f"cannot add same node twice {key}")
-            if type(raw_node) is dict:
-                self.ami_node_dict[key] = copy.deepcopy(raw_node)
-            else:
-                self.ami_node_dict[key] = "node"  # store just the key at present
-        else:
-            self.logger.warning("node cannot be None")
-
-    def read_edges(self, edges):
-        # self.ami_edges = edges
-        if len(self.ami_node_dict.keys()) == 0 and self.generate_nodes:
-            self.generate_nodes_from_edges()
-            print("after node generation", str(self))
-        for i, edge in enumerate(edges):
-            idx = "e" + str(i)
-            self.add_edge(edge, idx)
-
-    def add_edge(self, raw_edge, idx, fail_on_duplicate=True):
-        if fail_on_duplicate and idx in self.ami_edge_dict.keys():
-            raise ValueError("duplicate edge")
-
-        if raw_edge is None:
-            raise AmiGraphError("cannot add edge=None")
-        edge1 = ("n" + str(raw_edge[0]), "n" + str(raw_edge[1]))
-        self.ami_edge_dict[idx] = edge1
-
-    def generate_nodes_from_edges(self):
-        if self.ami_edges is not None:
-            for edge in self.ami_edges:
-                self.add_raw_node(edge[0])
-                self.add_raw_node(edge[1])
-
-    @classmethod
-    def create_ami_graph(cls, nd_skeleton):
-        """Uses Sknw to create a graph object within a new AmiGraph"""
-        # currently only called in a test
-        nx_graph = sknw.build_sknw(nd_skeleton)
-        ami_graph = AmiGraph(nx_graph, nd_skeleton=nd_skeleton)
-        # ami_graph.read_nx_graph(nx_graph )
-        print(f"***ami_graph\n {ami_graph} nx_graph: {ami_graph.nx_graph}\n")
-        return ami_graph
-
-    def ingest_graph_info(self):
-        if self.nx_graph is None:
-            self.logger.warning("Null graph")
-            return
-        # print("graph", self.nx_graph)
-        nx_island_list = list(nx.connected_components(self.nx_graph))
-        if nx_island_list is None or len(nx_island_list) == 0:
-            self.logger.warning("No islands")
-            return
-
-        self.assert_nx_island_info(nx_island_list)
-        nx_edgelist = self.get_edge_list_through_mininum_spanning_tree()
-        self.debug_edges_and_nodes(nx_edgelist, debug_count=7)
-        nodes = self.nx_graph.nodes
-        self.node_dict = {i: (nodes[node]["o"][0], nodes[node]["o"][1]) for i, node in enumerate(nodes)}
-
-        self.ami_island_list = []
-        for nx_island in nx_island_list:
-            ami_island = self.create_ami_island(nx_island, skeleton=self.nd_skeleton)
-            print(f"ami_island {ami_island}")
-            self.ami_island_list.append(ami_island)
-
-        return
-
-    def assert_nx_island_info(self, nx_island_list):
-        nx_island0 = nx_island_list[0]
-        assert type(nx_island0) is set
-        assert len(nx_island0) > 0
-        elem0 = list(nx_island0)[0]
-        assert type(elem0) is int, f"island elem are {type(elem0)}"
-        print("islands", self.ami_island_list)
-
-    def debug_edges_and_nodes(self, nx_edgelist, debug_count=5):
-        start_node = 0
-        end_node = 1
-        pts_index = 2
-        for edge in nx_edgelist[:debug_count]:
-            pts_ = edge[pts_index]['pts']
-            print(edge[start_node], edge[end_node], "pts in edge", len(pts_))
-        edgelist_pts_ = nx_edgelist[0][2]['pts']
-        for step in edgelist_pts_[:debug_count]:
-            print("step", step)
-
-    def get_edge_list_through_mininum_spanning_tree(self):
-        mst = tree.maximum_spanning_edges(self.nx_graph, algorithm="kruskal", data=True)
-        # mst = tree.minimum_spanning_tree(graph, algorithm="kruskal")
-        nx_edgelist = list(mst)
-        return nx_edgelist
-
-    @classmethod
-    def set_bbox_pixels_to_color(cls, bbox, image, colorx=255):
-        """sets all pixels in box to uniform color
-
-        :param bbox:
-        :param image:
-        :param colorx:
-        :return: modified image
-        """
-        xx = bbox[0]
-        yy = bbox[1]
-        image[xx[0]:xx[1], yy[0]:yy[1]] = colorx
-        return image
-
-    def __str__(self):
-        s = "nodes: " + str(self.ami_nodes) + \
-            "\n edges: " + str(self.ami_edges)
-        return s
-
-    def read_nx_graph(self, nx_graph):
-        """
-        Read and unpack NetworkX graph.
-        This may change as a result of changing data models
-        the nx_graph may be tyhe fundamental data structure
-        :param nx_graph:
-        :return:
-        """
-        # self.nodes_as_dicts = [nx_graph.node[ndidx] for ndidx in (nx_graph.nodes())]
-        # self.nodes_yx = [nx_graph.node[ndidx][AmiSkeleton.CENTROID] for ndidx in (nx_graph.nodes())]
-        self.read_nx_edges(nx_graph)
-        self.read_nx_nodes(nx_graph)
-        # this may be the critical data structure and the others are convenience
-        self.nx_graph = nx_graph
-        ingest = False
-        if ingest:
-            self.ingest_graph_info()
-        return
-
-    def read_nx_nodes(self, nx_graph):
-        self.ami_nodes = []
-        node_ids = nx_graph.nodes()
-        for node_id in node_ids:
-            node_dict = node_ids[node_id]
-            assert len(str(node_id)) < 4, f"node_id is {node_id}"
-            ami_node = AmiNode(ami_graph=self, node_id=node_id, nx_graph=nx_graph)
-            ami_node.set_centroid_yx(nx_graph.nodes[node_id][AmiSkeleton.CENTROID])
-            ami_node.read_nx_node(node_dict)
-            self.ami_nodes.append(ami_node)
-
-    def read_nx_edges(self, nx_graph):
-        self.ami_edges = []
-        for (start, end) in nx_graph.edges():
-            points_yx = nx_graph[start][end][AmiSkeleton.NODE_PTS]
-            ami_edge = AmiEdge()
-            ami_edge.read_nx_edge_points_yx(points_yx)
-            self.ami_edges.append(ami_edge)
-
-    def get_or_create_ami_islands(self):
-        """
-        AmiIslands are nx_graph 'components' with added functionality
-        :return: list of AmiIslands
-        """
-        if self.ami_island_list is None and self.nx_graph is not None:
-            self.ami_island_list = [self.create_ami_island(comp) for comp in
-                                    nx.algorithms.components.connected_components(self.nx_graph)]
-        return self.ami_island_list
-
-    @classmethod
-    def create_nx_graph_from_arbitrary_image_file(cls, path):
-        assert path.exists() and not path.is_dir(), f"{path} should be existing file"
-        Util.check_type_and_existence(path, PosixPath)
-        image1 = io.imread(path)
-        Util.check_type_and_existence(image1, np.ndarray)
-        gray_image = AmiImage.create_grayscale_0_1_float_from_image(image1)
-        skeleton_array = AmiImage.create_white_skeleton_from_image(gray_image)
-        nx_graph = AmiSkeleton.create_nx_graph_from_skeleton_wraps_sknw_NX_GRAPH(skeleton_array)
-        return nx_graph
-
-    def get_ami_islands_from_nx_graph(self):
-        """
-        Get the pixel-disjoint "islands" as from NetworkX
-        :return: list of AmiIslands
-        """
-
-        self.get_coords_for_nodes_and_edges_from_nx_graph(self.nx_graph)
-        assert self.nx_graph is not None
-        ami_islands = []
-        for node_ids in nx.algorithms.components.connected_components(self.nx_graph):
-            print("node_ids ", node_ids)
-            ami_island = self.create_ami_island(node_ids)
-            assert ami_island is not None
-            assert type(ami_island) is AmiIsland
-            ami_islands.append(ami_island)
-        return ami_islands
-
-    def extract_coords_for_node(self, idx):
-        """
-        gets coords for a single node with given id
-        :param idx: normally an int
-        :return:
-        """
-        node_data = self.nx_graph.nodes[idx]
-        centroid = node_data[AmiSkeleton.CENTROID]
-        centroid = (centroid[1], centroid[0])  # swap y,x as sknw seems to have this unusual order
-        return centroid
-
-    def create_islands(self):
-        """
-        needs nx_graph to exist
-
-        :return: list of islands
-        """
-
-        assert self.nx_graph is not None
-        self.islands = self.get_ami_islands_from_nx_graph()
-        return self.islands
-
-    @classmethod
-    def create_ami_island(self, node_ids, skeleton=None):
-
-        """
-        create from a list of node_ids (maybe from sknw)
-        maybe should be instance method of ami_graph
-        :param node_ids: set of node ids
-        :param ami_graph: essential
-        :param skeleton:
-        :return: AmiIsland object
-        """
-        assert type(node_ids) is set, "componente mus be of type set"
-        assert len(node_ids) > 0 , "components cannot be empty"
-
-        ami_island = pyimage.ami_island.AmiIsland()
-        ami_island.node_ids = node_ids
-        ami_island.ami_skeleton = skeleton
-        ami_island.ami_graph = self
-        print("ami_island", ami_island)
-        return ami_island
-
-
-
-class AmiGraphError(Exception):
-    def __init__(self, msg):
-        super().__init__(msg)
-
-
-if __name__ == '__main__':
-
-    img = np.array([
-        [0, 0, 0, 1, 0, 0, 0, 0, 0],
-        [0, 0, 0, 1, 0, 0, 0, 1, 0],
-        [0, 0, 0, 1, 0, 0, 0, 1, 0],
-        [0, 0, 0, 1, 0, 0, 0, 0, 0],
-        [1, 1, 1, 1, 0, 0, 0, 0, 0],
-        [0, 0, 0, 0, 1, 0, 0, 0, 0],
-        [0, 1, 0, 0, 0, 1, 0, 0, 0],
-        [1, 0, 1, 0, 0, 1, 1, 1, 1],
-        [0, 1, 0, 0, 1, 0, 0, 0, 0],
-        [0, 0, 0, 1, 0, 0, 0, 0, 0]])
-
-    # sknw.example1()
-    # sknw.example2horse()  # works
-    # sknw.example3() # needs flipping White to black
-    # sknw.example4() # needs flipping White to black
diff --git a/pyimage/ami_island.py-old b/pyimage/ami_island.py-old
deleted file mode 100644
index 7e173cc..0000000
--- a/pyimage/ami_island.py-old
+++ /dev/null
@@ -1,68 +0,0 @@
-"""AmiIsland is a set of node_ids that NetwworkX has listed as a "component"""
-
-from pyimage.svg import BBox
-from pyimage.util import Util
-import pyimage.ami_graph
-
-class AmiIsland:
-    def __init__(self):
-        # self.ami_skeleton = None
-        self.node_ids = None
-        self.ami_graph = None
-        self.coords_xy = None
-        self.bbox = None
-
-    def __str__(self):
-        s = "" + \
-            f"node_ids: {self.node_ids}; \n" + \
-            f"coords: {self.coords_xy}\n" + \
-            f"ami_graph: {self.ami_graph}" + \
-            "\n"
-
-        return s
-
-    def get_raw_box(self):
-        bbox = None
-        return bbox
-
-    def get_or_create_coords(self):
-        coords = []
-        if self.coords_xy is None:
-            # pyimage.ami_graph.AmiGraph.debugx(self.ami_graph, "get coords start")
-            for node_id in self.node_ids:
-                print(f"node id {node_id}")
-                print(f"ami_graph {type(self.ami_graph)} {self.ami_graph}")
-                print(f"dir ami_graph {dir(self.ami_graph)}")
-                # print(f"ami_graph.nx_graph nx_graph: {self.ami_graph.nx_graph}")
-                # print(f"ami_graph {type(self.ami_graph)} {self.ami_graph} nx_graph: {self.ami_graph.nx_graph}")
-                nx_graph = self.ami_graph.nx_graph
-                nx_graph = self.ami_graph.get_nx_graph()
-                yx = nx_graph.nodes[node_id]["o"]
-                xy = Util.get_xy_from_sknw_centroid(yx)
-                coords.append(xy)
-            # self.get_or_create_nodes()
-            # self.coords_xy = []
-            # for node in self.nodes:
-            #     coord_xy = node.coords_xy
-            #     self.coords_xy.append(coord_xy)
-
-    # def get_or_create_nodes(self):
-    #     if len(self.nodes) == 0 and self.node_ids is not None:
-    #         self.nodes = [AmiNode(node_id) for node_id in self.node_ids]
-    #     return self.nodes
-        return coords
-
-    def get_or_create_bbox(self):
-        """
-        create BBox object if not exists.
-        May give empty box if no coordinates
-        :return: BBox
-        """
-        if self.bbox is None:
-            coords_xy = self.get_or_create_coords()
-            print(f"coords_xy {coords_xy}")
-            self.bbox = BBox()
-            for coord in coords_xy:
-                self.bbox.add_coordinate(coord)
-        return self.bbox
-
diff --git a/pyimage/ami_node.py-old b/pyimage/ami_node.py-old
deleted file mode 100644
index 94d7650..0000000
--- a/pyimage/ami_node.py-old
+++ /dev/null
@@ -1,67 +0,0 @@
-"""a warpper for an sknw/nx node, still being developed"""
-
-import copy
-from pyimage.util import Util
-
-class AmiNode:
-    """Node holds coordinates
-    ["o"] for centrois (AmiSkeleton.CENTROID)
-    ["pts"] for multiple points (AmiSkeleton.POINTS)
-    ALL COORDINATES COMMUNICATED BY/TO USER ARE X,Y
-    (SKNW uses y,x coordinates)
-    """
-    CENTROID = "o"
-    def __init__(self, node_id=None, ami_graph=None, nx_graph=None):
-        """
-
-        :param node_id: mandatory
-        :param ami_graph: will use ami_graph.nx_graph
-        :param nx_graph: else will use nx_graph
-        """
-
-        if len(str(node_id)) > 4:
-            print(f"ami_graph {ami_graph}, nx_graph {nx_graph}")
-            raise Exception(f"id should be simple {node_id}")
-
-        self.ami_graph = ami_graph
-        self.nx_graph = nx_graph
-        if nx_graph is None and self.ami_graph is not None:
-            self.nx_graph = self.ami_graph.nx_graph
-        self.centroid_xy = None
-        self.node_id = node_id
-
-    def read_nx_node(self, node_dict):
-        """read dict for node, contains coordinates
-        typically: 'o': array([ 82., 844.]), 'pts': array([[ 82, 844]], dtype=int16)}
-        dict ket
-        """
-        self.node_dict = copy.deepcopy(node_dict)
-
-    def get_or_create_centroid_xy(self):
-        """
-        gets centroid from nx_graph.nodes[node_id]
-        :return:
-        """
-        if self.centroid_xy is None and self.nx_graph is not None:
-            assert len(str(self.node_id)) < 4, f"self.node_id {self.node_id}"
-            self.centroid_xy = Util.get_xy_from_sknw_centroid(
-                self.nx_graph.nodes[self.node_id][self.CENTROID])
-        return self.centroid_xy
-
-    def set_centroid_yx(self, point_yx):
-        """
-        set point in y,x, format
-        :param point_yx:
-        :return:
-        """
-        self.centroid_xy = [point_yx[1], point_yx[0]]  # note coords reverse in sknw
-        return
-
-    def __repr__(self):
-        s = str(self.coords_xy) + "\n" + str(self.centroid_xy)
-        return s
-
-    def __str__(self):
-        s = f"centroid {self.centroid_xy}"
-        return s
-    
diff --git a/pyimage/ami_plot.py b/pyimage/ami_plot.py
deleted file mode 100644
index f8b629d..0000000
--- a/pyimage/ami_plot.py
+++ /dev/null
@@ -1,328 +0,0 @@
-import logging
-from collections import Counter
-# local
-from ..pyimage.ami_util import AmiUtil
-
-logger = logging.getLogger(__name__)
-
-HEAD = "head"
-
-DASHED = "dashed"
-POLY = "poly"  # polyline or polygon
-POLYGON = "polygon"
-POLYLINE = "polyline"
-TARGETS = [POLY, POLYGON, POLYLINE]
-
-SEGMENT = "segment"
-TAIL = "tail"
-X = 0
-Y = 1
-
-
-class AmiPlot:
-    pass
-
-
-class AmiLine:
-    """This will probably include a third-party tool supporting geometry for lines
-
-    It may also be a polyline
-    """
-
-    def __init__(self, xy12=None, ami_edge=None):
-        """xy12 of form [[x1, y1], [x2, y2]]
-        direction is xy1 -> xy2 if significant"""
-        self.ami_edge = ami_edge
-        self.xy1 = None
-        self.xy2 = None
-        if xy12 is not None:
-            if len(xy12) != 2 or len(xy12[0]) != 2 or len(xy12[1]) != 2:
-                raise ValueError(f"bad xy pair for line {xy12}")
-            self.xy1 = [xy12[0][X], xy12[0][Y]]
-            self.xy2 = [xy12[1][X], xy12[1][Y]]
-        # ami_nodes = []
-
-    def __repr__(self):
-        return str([self.xy1, self.xy2])
-
-    def __str__(self):
-        return str([self.xy1, self.xy2])
-
-    def set_ami_edge(self, ami_edge):
-        self.ami_edge = ami_edge
-
-    @property
-    def vector(self):
-        """vector between end points
-        :return: xy2 - xy1
-        """
-        return None if (self.xy1 is None or self.xy2 is None) else \
-            (self.xy2[X] - self.xy1[X], self.xy2[Y] - self.xy1[Y])
-
-    @property
-    def xy_mid(self):
-        """get midpoint of line
-        :return: 2-array [x, y] of None if coords not set"""
-
-        if self.xy1 is not None and self.xy2 is not None:
-            return [(self.xy1[X] + self.xy2[X]) / 2, (self.xy1[Y] + self.xy2[Y]) // 2]
-        return None
-
-    def is_horizontal(self, tolerance=1) -> int:
-        return abs(self.vector[Y]) <= tolerance < abs(self.vector[X])
-
-    def is_vertical(self, tolerance=1) -> int:
-        return abs(self.vector[X]) <= tolerance < abs(self.vector[Y])
-
-    @classmethod
-    def get_horiz_vert_counter(cls, ami_lines, xy_index) -> Counter:
-        """
-        counts midpoint coordinates of lines (normally ints)
-
-        :param ami_lines: horiz or vert ami_lines
-        :param xy_index: 0 (x) 0r 1 (y) (normally 0 for vert lines, 1 for horiz)
-        :return: Counter
-        """
-        hv_dict = Counter()
-        for ami_line in ami_lines:
-            xy_mid = ami_line.xy_mid[xy_index]
-            if xy_mid is not None:
-                hv_dict[int(xy_mid)] += 1
-        return hv_dict
-
-    def get_min(self, xy_flag):
-        return None if xy_flag is None else min(self.xy1[xy_flag], self.xy2[xy_flag])
-
-    def get_max(self, xy_flag):
-        return None if xy_flag is None else max(self.xy1[xy_flag], self.xy2[xy_flag])
-
-
-class AmiLineTool:
-    """joins points or straight line segments
-    final target can be:
-    may contain tools for further analysis
-    still being developed.
-    end
-    mode=SEGMENT assembles segments into one of more polylines
-    """
-
-    def __init__(self, tolerance=1, points=None, ami_edge=None, mode=POLYLINE, xy_flag=None):
-        """build higher-order objects frome line segments or points
-        :param tolerance: for determing overlapping points
-        :param points: points to make polyline/gons
-        :param ami_edge: possibly obsolete
-        :param mode: POLYLINE (default)
-        :"""
-        self.point_set = set()
-        self.points = []
-        self.tolerance = tolerance
-        self.mode = mode
-        self.xy_flag = xy_flag
-        if mode not in TARGETS:
-            raise ValueError(f"unknown target {mode}")
-        if ami_edge is not None:
-            raise ValueError("emi_edges not yet supported")
-        if points is not None:
-            for point in points:
-                self.add_point(point)
-        self.polylines = []
-
-    def __repr__(self):
-        return str(self.points)
-
-    def __str__(self):
-        return str(self.points)
-
-    def add_point(self, point):
-        """add point
-        :param point:
-        """
-        self._validate_point(point)
-        # if point in self.point_set:
-        #     raise ValueError("Cannot add point twice {point}")
-        if point in self.points:
-            raise ValueError("Cannot add point twice {point}")
-        self.points.append(point)
-
-    def points(self):
-        return self.points
-
-    def add_segment(self, segment):
-        """segment tail must match head"""
-        self._validate_segment(segment)
-        if self.mode == POLYLINE:
-            self.add_segment_to_poly_list(segment)
-        else:
-            if len(self.points) == 0:
-                self.add_point(segment[0])
-                self.add_point(segment[1])
-            elif self._are_overlapping(self.points[-1], segment[0]):
-                self.add_point(segment[1])
-            else:
-                raise ValueError("Cannot add segment {segment} (no overlap)")
-
-    def add_segments(self, segments):
-        for segment in segments:
-            self.add_segment(segment)
-
-    def insert_segment(self, pos, segment):
-        self._validate_segment(segment)
-        if not self._are_overlapping(segment[1], self.points[0]):
-            raise ValueError("non-overlapping insertion at pos {pos} {segment}")
-        self.points.insert(pos, segment[0])
-
-    def _are_overlapping(self, point1, point2):
-        """
-
-        overlaps if either dx or dy >= self.tolerance
-        :param point1: [x, y]
-        :param point2: [x, y]
-        :return:
-        """
-        return abs(point1[X] - point2[X]) + abs(point1[Y] - point2[Y]) <= self.tolerance
-
-    def _validate_segment(self, segment):
-        assert type(segment) is AmiLine, f"segment was {type(segment)}"
-        self._validate_point(segment.xy1)
-        self._validate_point(segment.xy2)
-
-    def _validate_point(self, point):
-        assert type(point) is list, f"found {type(point)}"
-        assert len(point) == 2, f"found {len(point)}"
-        assert AmiUtil.is_number(point[0])
-
-    def _copy_point(self, point):
-        self._validate_point(point)
-        return [point[0], point[1]]
-
-    def copy_segment(self, segment):
-        return [self._copy_point(segment[0]), self._copy_point(segment[1])]
-
-    def add_points(self, points):
-        assert type(points) is list
-        for point in points:
-            self.add_point(point)
-
-    def insert_point(self, pos, point):
-        self._validate_point(point)
-        self.points.insert(0, [33, 44])
-        self.points.insert(pos, point)
-
-    def add_segment_to_poly_list(self, segment):
-        """add segment, assuming current list is sorted
-        polyline            polyline
-        lo-----hi           lo----------hi
-                   lo---hi
-                   segment
-        """
-        # seg_low = segment.get_min(self.xy_flag)
-        # seg_hi = segment.get_max(self.xy_flag)
-        if self.polylines == []:
-            self.polylines.append(segment)
-            return
-        last_polyline = None
-        xyf = self.xy_flag
-        inserted = False
-        for i, polyline in enumerate(self.polylines):
-            # does it overlap the next exactly?
-            if self._are_overlapping(segment.xy1, polyline.xy1):
-                if last_polyline is None:
-                    # end of first polyline
-                    self.prepend_segment_to_polyline(segment, polyline)
-                    inserted = True
-                elif self._are_overlapping(segment.xy1, last_polyline[1]):
-                    # join in the middle of last_polyline and polyline
-                    # join to predecessor
-                    self.prepend_segment_to_polyline(segment, polyline)
-                    # and then add last
-                    self.append_polyline_to_polyline_and_remove(last_polyline, polyline)
-                    # and clear the last
-                    self.polylines.remove(last_polyline)
-                    inserted = True
-
-            elif segment.xy2[xyf] < polyline[0][xyf]:
-                # falls behind preceding and leads last if any
-                if last_polyline is None or segment.xy0[xyf] > (last_polyline[-1][xyf] + self.tolerance):
-                    self.polylines.insert(i, segment)
-                    inserted = True
-                else:
-                    raise ValueError(f"too big {segment} cannot insert")
-
-            if inserted:
-                break
-
-            last_polyline = polyline
-
-        if not inserted:
-            if self._are_overlapping(last_polyline[-1], segment.xy2):
-                self.polylines.append(last_polyline, segment.xy2)
-            elif segment.xy1[xyf] < last_polyline[1][xyf]:
-                raise f"cannot add segment at head {segment}"
-            else:
-                self.polylines.append(segment)
-
-    def prepend_segment_to_polyline(self, segment, polyline):
-        polyline.insert(0, segment.xy1)
-
-    def append_polyline_to_polyline_and_remove(self, last_polyline, polyline):
-        last_polyline.add(polyline)
-        self.polylines.remove(polyline)
-
-
-class AmiEdgeTool:
-    """refines edges (join, straighten, break, corners, segments, curves, etc some NYI)
-    Still being actively developed
-    """
-
-    def __init__(self, ami_graph=None, ami_edges=None, ami_nodes=None):
-        """Best to create this from the factory method create_tool"""
-        self.ami_graph = ami_graph
-        self.ami_edges = ami_edges
-        self.ami_nodes = ami_nodes
-
-    @classmethod
-    def create_tool(cls, ami_graph, ami_edges=None, ami_nodes=None):
-        """preferred method of instantiating tool
-        :param ami_graph: required graph
-        :param ami_edges: edges to process
-        :param ami_nodes: optional nodes (if none uses ends of edges)
-        """
-        edge_tool = AmiEdgeTool(ami_graph, ami_edges=ami_edges, ami_nodes=ami_nodes)
-        if not ami_nodes:
-            edge_tool.create_ami_nodes_from_edges()
-
-        return edge_tool
-
-    def analyze_topology(self):
-        """counts nodes and edges by recursively iterating over
-        noes and their edges -> edges and their nodes
-        also only includes start_id < end_id
-        (mainly a check)
-
-         :return: nodes, edges"""
-
-        if self.ami_edges is None:
-            logger.error(f"no edges, possible error")
-        new_ami_edges = set()
-        new_ami_nodes = set()
-        while self.ami_nodes:
-            ami_node = self.ami_nodes.pop()
-            new_ami_nodes.add(ami_node)
-            node_ami_edges = ami_node.get_or_create_ami_edges()
-            for ami_edge in node_ami_edges:
-                if ami_edge.has_start_lt_end():
-                    if ami_edge not in self.ami_edges:
-                        print(f" cannot find {ami_edge} in edges")
-                    else:
-                        new_ami_edges.add(ami_edge)
-        return new_ami_nodes, new_ami_edges
-
-    def create_ami_nodes_from_edges(self):
-        """generates unique ami_nodes from node_ids at ends of edges"""
-        if not self.ami_nodes:
-            self.ami_nodes = set()
-            node_ids = set()
-            for ami_edge in self.ami_edges:
-                node_ids.add(ami_edge.start_id)
-                node_ids.add(ami_edge.end_id)
-            self.ami_nodes = self.ami_graph.create_ami_nodes_from_ids(node_ids)
diff --git a/pyimage/ami_skeleton.py b/pyimage/ami_skeleton.py
deleted file mode 100644
index ff38584..0000000
--- a/pyimage/ami_skeleton.py
+++ /dev/null
@@ -1,262 +0,0 @@
-"""skeleton image used by SKNW and then by AmiGraph
-
-This class had a lot of mess and has been refactored"""
-
-import logging
-from pathlib import Path
-import numpy as np
-import networkx as nx
-import sknw  # must pip install sknw
-import os
-import matplotlib.pyplot as plt
-from skimage import io
-import logging
-# local
-from pyamiimage.pyimage.ami_image import AmiImage
-from ..pyimage.ami_util import AmiUtil
-
-from ..pyimage.bbox import BBox
-from ..pyimage.flood_fill import FloodFill
-from ..pyimage.ami_graph_all import AmiGraph
-
-logger = logging.getLogger(__name__)
-
-class AmiSkeleton:
-
-    """manages workflow from file to plot.
-    creates:
-    * binary
-    * skeleton
-    * sknw nodes and edges
-    * networkx graph (often called nx_graph)
-    * plots
-
-
-    May need rationalizatiom with AmiGraph
-    """
-    # NODE_PTS = "pts"
-    # CENTROID = "o"
-
-    logger = logging.getLogger("ami_skeleton")
-
-    def __init__(self, plot_plot=False, title=None):
-        self.skeleton_image = None
-        self.binary = None
-        self.nx_graph = None
-        self.edge_xy_list = []
-        self.node_xy = []
-        self.nodes = []
-        self.image = None
-        self.path = None
-        self.new_binary = None
-        self.interactive = False
-        self.title = title
-        self.plot_plot = plot_plot
-        self.islands = None
-        self.bboxes = None
-        self.thresh = None
-        #
-        self.ami_graph = None
-        self.node_dict = {}
-        self.edge_dict = {}
-
-    @classmethod
-    def binarize_skeletonize_sknw_nx_graph_plot_TEST(cls, path, plot_plot=True):
-        """
-        Creates skeleton and nx_graph and plots it
-
-        :param path:
-        :param plot_plot:
-        :return: AmiSkeleton
-        """
-        assert path is not None
-        path = Path(path)
-        skeleton_image = AmiImage.create_white_skeleton_from_file(path)
-        # build graph from skeleton
-        nx_graph = AmiGraph.create_nx_graph_from_skeleton(skeleton_image)
-        if plot_plot:
-            self.plot_nx_graph_NX(nx_graph)
-        return skeleton_image
-
-    def create_nx_graph_via_skeleton_sknw_NX_GRAPH(self, path):
-        """
-        Creates a nx_graph
-        does it need a path?
-        :param path:
-        :return: AmiSkeleton
-        """
-        logger.warning("maybe obsolete")
-        assert path is not None
-        path = Path(path)
-        self.skeleton_image = AmiImage.create_white_skeleton_from_file(path)
-        io.imshow(self.skeleton_image)
-        io.show()
-        assert self.skeleton_image is not None
-        # build graph from skeleton
-        nx_graph = AmiGraph.create_nx_graph_from_skeleton(self.skeleton_image)
-        return nx_graph
-
-    def plot_nx_graph_NX(self, nx_graph, title="skeleton"):
-        """
-        :param nx_graph:
-        :param title:
-        :return: None
-        """
-        """
-        requires that nx_graph has been created
-graph.node[id]['pts'] : Numpy(x, n), coordinates of nodes points
-graph.node[id]['o']: Numpy(n), centried of the node
-graph.edge(id1, id2)['pts']: Numpy(x, n), sequence of the edge point
-graph.edge(id1, id2)['weight']: float, length of this edge        """
-
-        assert nx_graph is not None
-        AmiSkeleton.get_coords_for_nodes_and_edges_from_nx_graph_GRAPH(nx_graph)
-        self.plot_edges_nodes_and_title_GRAPH(title)
-        return None
-
-    def plot_edges_nodes_and_title_GRAPH(self, title, plot_plot=True):
-        """
-        Requires nodes and edges to have been created
-        :param title:
-        :param plot_plot:
-        :return:
-        """
-        for edge_xy in self.edge_xy_list:
-            plt.plot(edge_xy[:, 1], np.negative(edge_xy[:, 0]), 'green')
-        # draw node by small circle (".")
-        plt.plot(self.node_xy[:, 1], np.negative(self.node_xy[:, 0]), 'r.')
-        # title and show
-        plt.title(title)
-        if plot_plot:
-            plt.show()
-
-        path = Path(Path(__file__).parent.parent, "temp/figs")
-        if not path.exists():
-            path.mkdir()
-        fig = Path(path, f"{title}.png")
-        if fig.exists():
-            os.remove(fig)
-        plt.savefig(fig, format="png")
-        if self.interactive:
-            plt.show()
-
-    @classmethod
-    def get_coords_for_nodes_and_edges_from_nx_graph_GRAPH(cls, nx_graph):
-        """
-        creates nodes and edges from networkx graph
-        :return: Node
-        """
-        assert nx_graph is not None
-        nodes = nx_graph.nodes()
-        node_xy = np.array([nodes[i][AmiSkeleton.CENTROID] for i in nodes])
-        # edges by pts (s(tart),e(nd)) appear to be the nodes on each edge
-        edge_xy_list = []
-        for (s, e) in nx_graph.edges():
-            edge_xy = nx_graph[s][e][AmiSkeleton.NODE_PTS]
-            edge_xy_list.append(edge_xy)
-        return node_xy, edge_xy_list
-
-    def extract_bbox_for_nodes_ISLAND(self, ami_island):
-        from pyimage import AmiIsland
-        """
-        gets bounding box for a list of nodes in
-
-        requires nodes to have been created
-        :param ami_island:
-        :return: bounding box ((xmin, xmax), (ymin, ymax))
-        """
-        assert ami_island is not None
-        assert type(ami_island) is AmiIsland, f"expected {AmiIsland} found {type(ami_island)}"
-        node_xy = self.extract_coords_for_nodes_ISLAND(ami_island)
-        # print ("node_xy...", node_xy)
-        xx = node_xy[:, 0]
-        yy = node_xy[:, 1]
-        xmin = int(np.min(xx))
-        xmax = int(np.max(xx))
-        ymin = int(np.min(yy))
-        ymax = int(np.max(yy))
-        bbox = BBox(((xmin, xmax), (ymin, ymax)))
-        return bbox
-
-    def extract_coords_for_nodes_ISLAND(self, ami_island):
-        """
-        gets coordinates for a set of nx_graph nodes
-        *** NOTE it seems the sknw output has y,x rather than x,y ***
-
-        :param ami_island: normally ints but I suppose could be other
-        :return: node_xy as [npoints, 2] ndarray
-        """
-        assert ami_island is not None
-        assert type(ami_island) is AmiIsland, f"expected {AmiIsland} found {type(ami_island)}"
-        npoints = len(ami_island)
-        node_xy = np.empty([0, 2], dtype=float)
-        for isd in ami_island:
-            centroid = self.extract_coords_for_node_NX_GRAPH_CLS(isd)
-            node_xy = np.append(node_xy, centroid)
-        node_xy = np.reshape(node_xy, (npoints, 2))
-        return node_xy
-
-    def create_bbox_for_island_ISLAND(self, island):
-        bbox0 = self.extract_bbox_for_nodes_ISLAND(island)
-        bbox = BBox(bbox0)
-        return bbox
-
-    def read_image_plot_component(self, component_index, image):
-        """
-        Convenience method to read imag, get components and plot given one
-        :param component_index:
-        :param image:
-        :return:
-        """
-        nx_graph = self.create_nx_graph_via_skeleton_sknw_NX_GRAPH(image)
-        # self.get_coords_for_nodes_and_edges_from_nx_graph_GRAPH(nx_graph)
-        # ami_graph = AmiGraph.
-        # TODO needs AmiGraph adding
-        islands = self.get_ami_islands_from_nx_graph_GRAPH()
-        island = islands[component_index]
-        self.plot_island_ISLAND(island)
-
-    def plot_island_ISLAND(self, component):
-        """
-        Plots a given component
-        :param component:
-        :return:
-        """
-        start_node_index = list(component)[0]  # take first node
-        start_node = self.nodes[start_node_index]
-        start_pixel = start_node[self.NODE_PTS][0]  # may be a list of xy for a complex node always pick first
-        flooder = FloodFill()
-        pixels = flooder.flood_fill(self.binary, start_pixel)
-        if self.interactive:
-            flooder.plot_used_pixels()
-
-    def create_and_plot_all_components_TEST(self, path, min_size=None):
-        """
-
-        :param path:
-        :param min_size:
-        :return:
-        """
-        if min_size is None:
-            min_size = [30, 30]
-        self.create_nx_graph_via_skeleton_sknw_NX_GRAPH(path)
-        nodes_xy, edges_xy = self.get_coords_for_nodes_and_edges_from_nx_graph_GRAPH(self.nx_graph)
-        components = self.get_ami_islands_from_nx_graph_GRAPH()
-        assert self.nx_graph is not None
-        self.islands = self.get_ami_islands_from_nx_graph_GRAPH()
-        bboxes = self.islands
-        for component, bbox in zip(components, bboxes):
-            w, h = AmiSkeleton.get_width_height_BBOX(bbox)
-            if min_size[0] < w or min_size[1] < h:
-                self.plot_island_ISLAND(component)
-
-    def get_ami_islands_from_image_OBSOLETE(self, image):
-        """
-        read image, calculate islands
-
-        :param image:
-        :return: list of islands in arbitrary order
-        """
-        nx_graph = self.create_nx_graph_via_skeleton_sknw_NX_GRAPH(image)
-        self.get_coords_for_nodes_and_edges_from_nx_graph_GRAPH(nx_graph)
-        return self.get_ami_islands_from_nx_graph_GRAPH()
diff --git a/pyimage/bbox.py b/pyimage/bbox.py
deleted file mode 100644
index 5c82738..0000000
--- a/pyimage/bbox.py
+++ /dev/null
@@ -1,407 +0,0 @@
-"""bounding box"""
-from skimage import draw
-# from ..pyimage.svg import SVGG, SVGRect
-
-class BBox:
-    """bounding box 2array of 2arrays, based on integers
-    """
-
-    X = "x"
-    Y = "y"
-    WIDTH = "width"
-    HEIGHT = "height"
-
-    def __init__(self, xy_ranges=None, swap_minmax=False):
-        """
-        Must have a valid bbox
-        Still haven'tb worked out logic of default boxes (must include None's)
-        as [0.0],[0.0] is valid
-        :param xy_ranges: [[x1, x2], [y1, y2]] will be set to integers
-        """
-        self.xy_ranges = [[], []]
-        self.swap_minmax = swap_minmax
-        if xy_ranges is not None:
-            self.set_ranges(xy_ranges)
-
-    @classmethod
-    def create_from_xy_w_h(cls, xy, width, height):
-        """
-        create from xy, width height
-        all inputs must be floats
-        :param xy: origin a [float, float]
-        :param width:
-        :param height:
-        :return:
-        """
-        assert type(xy[0]) is float or type(xy[0]) is int, f"found {type(xy[0])}"
-        assert type(xy[1]) is float or type(xy[1]) is int, f"found {type(xy[1])}"
-        assert type(width) is float or type(width) is int, f"found {type(width)}"
-        assert type(height) is float or type(height) is int, f"found {type(height)}"
-
-        try:
-            xy_ranges = [[float(xy[0]), float(xy[0]) + float(width)], [float(xy[1]), float(xy[1]) + float(height)]]
-        except Exception as e:
-            raise ValueError(f"cannot create bbox from {xy},{width},{height}")
-        return BBox(xy_ranges=xy_ranges)
-
-    def set_ranges(self, xy_ranges):
-        if xy_ranges is None:
-            raise ValueError("no lists given")
-        if len(xy_ranges) != 2:
-            raise ValueError("must be 2 lists of lists")
-        if xy_ranges[0] is not None and len(xy_ranges[0]) == 0:
-            xy_ranges[0] = None
-        if xy_ranges[0] is not None and len(xy_ranges[0]) != 2:
-            raise ValueError(f"range {xy_ranges[0]} must be None or 2-tuple")
-        if xy_ranges[1] is not None and len(xy_ranges[1]) == 0:
-            xy_ranges[1] = None
-        if xy_ranges[1] is not None and len(xy_ranges[1]) != 2:
-            raise ValueError(f"range {xy_ranges[1]} must be None or 2-tuple")
-        self.set_xrange(xy_ranges[0])
-        self.set_yrange(xy_ranges[1])
-
-    def get_ranges(self):
-        """gets ranges as [xrange, yrange]"""
-        return self.xy_ranges
-
-    def set_xrange(self, rrange):
-        self.set_range(0, rrange)
-
-    def get_xrange(self):
-        return self.xy_ranges[0]
-
-    def get_width(self):
-        """get width
-        :return: width or None if x range invalid or not set"""
-        if self.get_xrange() is None or len(self.get_xrange()) == 0:
-            return None;
-        assert self.get_xrange() is not None
-        assert len(self.get_xrange()) == 2, f"xrange, got {len(self.get_xrange())}"
-        return self.get_xrange()[1] - self.get_xrange()[0]
-
-    def set_yrange(self, rrange):
-        self.set_range(1, rrange)
-
-    def get_yrange(self):
-        return self.xy_ranges[1]
-
-    def get_height(self):
-        if self.get_yrange() is None or len(self.get_yrange()) == 0:
-            return None
-        return self.get_yrange()[1] - self.get_yrange()[0] if len(self.get_yrange()) == 2 else None
-
-    def set_range(self, index, rrange):
-        if index != 0 and index != 1:
-            raise ValueError(f"bad tuple index {index}")
-        if rrange is None:
-            self.xy_ranges[index] = None
-            return
-        val0 = int(rrange[0])
-        val1 = int(rrange[1])
-        if val1 < val0:
-            if self.swap_minmax:
-                val1, val0 = val0, val1
-            else:
-                raise ValueError(f"ranges must be increasing {val0} !<= {val1}")
-        self.xy_ranges[index] = [val0, val1]
-
-    def __str__(self):
-        return str(self.xy_ranges)
-
-    def __repr__(self):
-        return str(self.xy_ranges)
-
-    def intersect(self, bbox):
-        """
-        inclusive intersection of boxes (AND)
-        if any fields are empty returns None
-        DOES NOT CHANGE self
-        :param bbox:
-        :return: new Bbox (max(min) ... (min(max)) or None if any  errors
-        """
-        bbox1 = None
-        if bbox is not None:
-            # print("XRanges",self.get_xrange(), bbox.get_xrange())
-            xrange = self.intersect_range(self.get_xrange(), bbox.get_xrange())
-            # print("XRange", xrange)
-            # print("YRanges",self.get_yrange(), bbox.get_yrange())
-            yrange = self.intersect_range(self.get_yrange(), bbox.get_yrange())
-            # print("YRange", yrange)
-            bbox1 = BBox([xrange, yrange])
-        return bbox1
-
-    def union(self, bbox):
-        """
-        inclusive merging of boxes (OR)
-        if any fields are empty returns None
-        DOES NOT CHANGE self
-
-        :param bbox:
-        :return: new Bbox (min(min) ... (max(max)) or None if any  errors
-        """
-        bbox1 = None
-        if bbox is not None:
-            xrange = self.union_range(self.get_xrange(), bbox.get_xrange())
-            yrange = self.union_range(self.get_yrange(), bbox.get_yrange())
-            bbox1 = BBox((xrange, yrange))
-        return bbox1
-
-    @classmethod
-    def intersect_range(cls, range0, range1):
-        """intersects 2 range tuples"""
-        rrange = ()
-        if len(range0) == 2 and len(range1) == 2:
-            maxmin = max(range0[0], range1[0])
-            minmax = min(range0[1], range1[1])
-            rrange = [maxmin, minmax] if minmax >= maxmin else None
-        return rrange
-
-    @classmethod
-    def union_range(cls, range0, range1):
-        """intersects 2 range tuples"""
-        rrange = []
-        if len(range0) == 2 and len(range1) == 2:
-            rrange = [min(range0[0], range1[0]), max(range0[1], range1[1])]
-        return rrange
-
-    def add_coordinate(self, xy_tuple):
-        assert xy_tuple is not None, f"xy_tuple must have coordinates"
-        self.add_to_range(0, self.get_xrange(), xy_tuple[0])
-        self.add_to_range(1, self.get_yrange(), xy_tuple[1])
-
-    def add_to_range(self, index, rrange, coord):
-        """if coord outside range , expand range
-
-        :param index: 0 or  =1 for axis
-        :param rrange: x or y range
-        :param coord: x or y coord
-        :return: None (changes range)
-        """
-        if index != 0 and index != 1:
-            raise ValueError(f"bad index {index}")
-        if len(rrange) != 2:
-            rrange = [None, None]
-        if rrange[0] is None or coord < rrange[0]:
-            rrange[0] = coord
-        if rrange[1] is None or coord > rrange[1]:
-            rrange[1] = coord
-        self.xy_ranges[index] = rrange
-        return rrange
-
-    @classmethod
-    def create_box(cls, xy, width, height):
-        if xy is None or width is None or height is None:
-            raise ValueError("All params must be not None")
-        width = int(width)
-        height = int(height)
-        if len(xy) != 2:
-            raise ValueError("xy must be an array of 2 values")
-        if width < 0 or height < 0:
-            raise ValueError("width and height must be non negative")
-        xrange = ([xy(0), xy[0] + width])
-        yrange = [xy(1), xy[1] + int(height)]
-        bbox = BBox.create_from_ranges(xrange, yrange)
-        return bbox
-
-    def expand_by_margin(self, margin):
-        """
-        if margin is scalar, apply to both axes
-        if margin is 2-tuple, apply to x and y separately
-        if margin is negative applies only if current range is >- 2*margin + 1
-        i
-        :param margin: scalar dx or tuple (dx, dy)
-        :return: None
-        """
-
-        if not isinstance(margin, list):
-            margin = [margin, margin]
-        self.change_range(0, margin[0])
-        self.change_range(1, margin[1])
-
-    def change_range(self, index, margin):
-        """
-        change range by margin
-        :param index:
-        :param margin:
-        :return:
-        """
-        if index != 0 and index != 1:
-            raise ValueError(f"Bad index for range {index}")
-        rr = self.xy_ranges[index]
-        rr[0] -= margin[index]
-        rr[1] += margin[index]
-        # range cannot be <= 0
-        if rr[0] >= rr[1]:
-            mid = (rr[0] + rr[1]) / 2
-            rr[0] = int(mid - 0.5)
-            rr[1] = int(mid + 0.5)
-        self.xy_ranges[index] = rr
-
-    @classmethod
-    def create_from_ranges(cls, xr, yr):
-        """
-        create from 2 2-arrays
-        :param xr: 2-list of range
-        :param yr: 2-list of range
-        :return:
-        """
-        bbox = BBox()
-        bbox.set_xrange(xr)
-        bbox.set_yrange(yr)
-        return bbox
-
-    def is_valid(self):
-        """
-        both ranges must be present and non-negative
-        :return:
-        """
-        if self.xy_ranges is None or len(self.xy_ranges) != 2:
-            return False
-        if self.xy_ranges[0] is None or self.xy_ranges[1] is None:
-            return False
-        try:
-            ok = self.get_width() >= 0 and self.get_height() >= 0
-            return ok
-        except Exception:
-            return False
-
-    def set_invalid(self):
-        """set xy_ranges to None"""
-        self.xy_ranges = None
-
-    @classmethod
-    def get_width_height(cls, bbox):
-        """
-        TODO MOVED
-
-        :param bbox: tuple of tuples ((x0,x1), (y0,y1))
-        :return: (width, height) tuple
-        """
-        """
-        needs to have its own class
-        """
-        width = bbox[0][1] - bbox[0][0]
-        height = bbox[1][1] - bbox[1][0]
-        return width, height
-
-    @classmethod
-    def fits_within(cls, bbox, bbox_gauge):
-        """
-        TODO MOVED
-
-        :param bbox: tuple of tuples ((x0,x1), (y0,y1))
-        :param bbox_gauge: tuple of (width, height) that bbox must fit in
-        :return: true if fits in rectangle
-        """
-        """
-        needs to have its own class
-        """
-        width, height = bbox.get_width_height()
-        return width < bbox_gauge[0] and height < bbox_gauge[1]
-
-    def min_dimension(self):
-        """
-        gets minimum of height and width
-        :return: min(height, width)
-        """
-        return min(self.get_width(), self.get_height())
-
-    def max_dimension(self):
-        """
-        gets maximum of height and width
-        :return: max(height, width)
-        """
-        return max(self.get_width(), self.get_height())
-
-    def get_point_pair(self):
-        """
-        BBox stores the location as ranges of x and y values:
-        [[x1, x2], [y1, y2]]
-        sometimes it is necessary to work with points instead:
-        [(y1, x1), (y2, x2)]
-        :returns: list of 2 tuples
-        """
-        return [(self.get_yrange()[0], self.get_xrange()[0]),
-         (self.get_yrange()[1], self.get_xrange()[1])]
-         # remember that the indexing is in terms of rows and columns
-         # hence x(columns) y(rows) values are flipped when returning point pair
-
-    @classmethod
-    def plot_bbox_on(cls, image, bbox):
-        """
-        Plots bbox on an image
-        :param: image
-        :type: numpy array 
-        :param: bbox
-        :type: BBox or list
-        :returns: fig, ax
-        """
-        # bbox can either be BBox object or in form of [[a, b][c, d]]
-        
-        # if type(bbox) == BBox:
-        #     assert bbox.is_valid()
-        # elif type(bbox) == list:
-        #     bbox = BBox(bbox)
-        #     assert bbox.is_valid()
-        # else:
-        #     # the bbox passed is not invalid
-        #     return None
-
-
-        point_pair = bbox.get_point_pair()
-        if point_pair[0][0] > image.shape[0] or point_pair[0][1] >image.shape[1]:
-            # if the starting point is outside the image, ignore bbox
-            return image
-        
-        try:
-            row, col = draw.rectangle_perimeter(start=point_pair[0], end=point_pair[1])
-            image[row, col] = 0
-        except IndexError as e:
-            point_pair = BBox.fit_point_pair_within_image(image, point_pair)
-            row, col = draw.rectangle_perimeter(start=point_pair[0], end=point_pair[1])
-            image[row, col] = 0
-
-        return image
-
-    @classmethod
-    def fit_point_pair_within_image(cls, image, point_pair):
-        max_row = image.shape[0]
-        max_col = image.shape[1]
-        bbox_row = point_pair[1][0]
-        bbox_col = point_pair[1][1]
-        if bbox_row >= max_row-1:
-            bbox_row = max_row - 2
-        if bbox_col >= max_col-1:
-            bbox_col = max_col - 2
-        point_pair[1] = (bbox_row, bbox_col)
-        return point_pair
-
-    # RECURSIVE imports...
-    # def create_svg(self):
-    #     """creates SVG (a <g> with a <rect>
-    #     :return: <g role="bbox"><rect .../></g>
-    #     """
-    #     g = SVGG()
-    #     g.set_attribute("role", "bbox")
-    #     svg_rect = SVGRect(self)
-    #     g.append(svg_rect)
-    #     return g
-
-    @classmethod
-    def create_from_corners(cls, xy1, xy2):
-        if xy1 is None or xy2 is None:
-            return None
-        if len(xy1) != 2 or len(xy2) != 2:
-            return None
-        xrange = [xy1[0], xy2[0]] if xy2[0] > xy1[0] else [xy2[0], xy1[0]]
-        yrange = [xy1[1], xy2[1]] if xy2[1] > xy1[1] else [xy2[1], xy1[1]]
-        bbox = BBox(xy_ranges=[xrange, yrange])
-        return bbox
-
-
-"""If you looking for the overlap between two real-valued bounded intervals, then this is quite nice:
-
-def overlap(start1, end1, start2, end2):
-    how much does the range (start1, end1) overlap with (start2, end2)
-    return max(max((end2-start1), 0) - max((end2-end1), 0) - max((start2-start1), 0), 0)
-I couldn't find this online anywhere so I came up with this and I'm posting here."""
diff --git a/pyimage/contour.py b/pyimage/contour.py
deleted file mode 100644
index 790394d..0000000
--- a/pyimage/contour.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from skimage.measure import find_contours
-from skimage import io
-from skimage.color import rgb2gray
-from matplotlib import pyplot as plt
-
-image = io.imread('contour_finding_test.png')
-# image = io.imread('FlowchartDiagram.png')
-image = rgb2gray(image)
-out = find_contours(image)
-print(len(out))
-
-# Find contours at a constant value of 0.8
-# contours = find_contours(image, 0.8)
-contours = find_contours(image, )
-
-# Display the image and plot all contours found
-fig, ax = plt.subplots()
-ax.imshow(image, cmap=plt.cm.gray)
-
-for contour in contours:
-    ax.plot(contour[:, 1], contour[:, 0], linewidth=2)
-
-ax.axis('image')
-ax.set_xticks([])
-ax.set_yticks([])
-plt.show()
-# io.imshow(image)
-# io.show()
diff --git a/pyimage/examples.py b/pyimage/examples.py
deleted file mode 100644
index ded38de..0000000
--- a/pyimage/examples.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""examples to be used by tests and tutorials"""
-
-class Examples:
-    pass
\ No newline at end of file
diff --git a/pyimage/examples_tesseract.py b/pyimage/examples_tesseract.py
deleted file mode 100644
index 7e598e7..0000000
--- a/pyimage/examples_tesseract.py
+++ /dev/null
@@ -1,161 +0,0 @@
-from pathlib import Path
-from tesseract_hocr import TesseractOCR
-from pyimage.old_code.preprocessing import ImageProcessor
-
-
-def example_extract_bbox_for_image_without_arrows():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_path = Path(resources_dir, "biosynth_path_1_cropped_arrows_removed.png")
-    bbox_coordinates = ocr.extract_bbox_from_image(image_path)
-    print("bbox coordinates: ", bbox_coordinates)
-
-
-def example_extract_bbox_from_hocr_file():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    hocr_path = Path(resources_dir, "hocr1.html")
-    root = ocr.read_hocr_file(hocr_path)
-    bbox_coordinates = ocr.extract_bbox_from_hocr(root)
-    print("bbox coordinates: ", bbox_coordinates)
-
-
-def example_fill_bbox_in_image():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_path = Path(resources_dir, "biosynth_path_1_cropped_arrows_removed.png")
-    # image_path = Path(resources_dir, "biosynth_path_1_cropped.png")
-    image_processor = ImageProcessor()
-    
-    image = image_processor.load_image(image_path)
-    bbox_coordinates = ocr.extract_bbox_from_image(image_path)
-    
-    bbox_around_words_image = ocr.draw_bbox_around_words(image, bbox_coordinates)
-    image_processor.show_image(bbox_around_words_image)
-    
-
-def example_2_fill_bbox_in_image():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_path = Path(resources_dir, "biosynth_path_3.png")
-    # image_path = Path(resources_dir, "biosynth_path_1_cropped.png")
-    image_processor = ImageProcessor()
-    
-    image = image_processor.load_image(image_path)
-    bbox_coordinates, words = ocr.extract_bbox_from_image(image_path)
-    
-    bbox_around_words_image = ocr.draw_bbox_around_words(image, bbox_coordinates)
-    image_processor.show_image(bbox_around_words_image)
-
-
-def example_2_fill_bbox_for_phrases():
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_path = Path(resources_dir, "biosynth_path_3.png")
-    # image_path = Path(resources_dir, "biosynth_path_1_cropped.png")
-    image_processor = ImageProcessor()
-    
-    ocr = TesseractOCR()
-
-    image = image_processor.load_image(image_path)
-    print(image.shape)
-    # bbox_coordinates, words = extract_bbox_from_image(image_path)
-    hocr = ocr.hocr_from_image_path(image_path)
-    root = ocr.parse_hocr_string(hocr)
-    phrases, bbox_coordinates = ocr.find_phrases(root)
-
-    words, bbox_coordinates_words = ocr.extract_bbox_from_hocr(root)
-
-    # Print out the phrase and its corresponding coordinates
-    for word, bbox in zip(words, bbox_coordinates_words):
-        print("Phrase: ", word, " Coordinates: ", bbox)
-    
-    bbox_around_words_image = ocr.draw_bbox_around_words(image, bbox_coordinates)
-    image_processor.show_image(bbox_around_words_image)
-
-
-def example_find_phrases():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_processor = ImageProcessor()
-    for num in range(4, 9):
-        print(f"Now processing biosynth_path_{num}.jpeg")
-        image_path = Path(resources_dir, f"biosynth_path_{num}.jpeg")
-        image = image_processor.load_image(image_path)
-    
-        hocr = ocr.hocr_from_image_path(image_path)
-        root = ocr.parse_hocr_string(hocr)
-        phrases, bbox_coordinates = ocr.find_phrases(root)
-
-        words, bbox_coordinates_words = ocr.extract_bbox_from_hocr(root)
-
-        # Print out the phrase and its corresponding coordinates
-        for word, bbox in zip(words, bbox_coordinates_words):
-            print("Phrase: ", word, " Coordinates: ", bbox)
-
-        ocr.output_phrases_to_file(phrases, f"biosynth_path_{num}.txt")
-        bbox_around_words_image = ocr.draw_bbox_around_words(image, bbox_coordinates)
-        image_processor.show_image(bbox_around_words_image)
-
-
-def example_alex_pico_images():
-    pass
-
-
-# not yet working
-"""
-Hi all,
-
-In preparation for our meeting next week, I went ahead and prepared a subset of our “25 years of pathway figures” database using some simple searches. Please know that I know nothing about terpene biochemistry, so I made two simple-minded subsets:
-	1. Based on “terpene” being found in paper title, figure title, caption, or chemical name
-	2. All of #1, plus figures with “ADH” found among the extracted gene symbols
-
-Here are shiny apps to browse these subsets and some summary stats:
-	1. Terpenes: https://gladstone-bioinformatics.shinyapps.io/shiny-terpene/
-		* Figures: 282
-		* Papers: 273
-		* Total genes: 1,276
-		* Unique genes: 491
-		* Total chemicals: 2,465
-		* Unique chemicals: 551
-	2. Terpenes + ADH: https://gladstone-bioinformatics.shinyapps.io/shiny-terpeneadh/
-		* Figures: 719
-		* Papers: 700
-		* Total genes: 10,667
-		* Unique genes: 2,699
-		* Total chemicals: 6,004
-		* Unique chemicals: 931
-
-These subsets are also available as RDS and TSV files if you want to browse them directly.
-
-Maybe these will help make some of our discussion points more tangible.
-
-Note the following caveats that we’d certainly want to improve upon for a “real” effort:
-	* Covers pathway figures from 1995 - 2019. We have a 2020 collection, but it’s not online yet.
-	* Based on a general literature search for all types of pathway biology; not targeted or trained for terpene biochemistry
-	* Based on HGNC human gene lexicon and pubtator chemicals; not targeted for plants or terpenes
-
-Cheers,
- - Alex
-
- PMC6348685	Comparative transcriptome analysis of a long-time ...	Luodong Huang, ...	2019	Fig. 4	Putative genes and their expression...
-PMC6360084	Identification, expression, and phylogenetic analy...	Jipeng Mao, Zid...	2019	Figure 8	Identification, expression, and phy...
-PMC6374618	Agarwood Induction: Current Developments and Futur...	Cheng Seng CS T...	2019	FIGURE 4	Schematic relationships between the...
-PMC6394206	A chromosomal-scale genome assembly of Tectona gra...	Dongyan Zhao, J...	2019	Figure 5:	Proposed diterpene pathway based on...
-PMC6412517	New Insights on Arabidopsis thaliana Root Adaption...	Inmaculada Cole...	2019	Figure 2	Induction of secondary metabolism i...
-PMC6423058	Why Algae Release Volatile Organic Compounds—The E...	Zhaojiang Zuo	2019	FIGURE 2	Pathway of terpene synthesis
-PMC6437448	Production of ginsenoside aglycone (protopanaxatri...	Yu Shin YS Gwak...	2019	Fig. 1	Biosynthetic pathway engineering fo...
-PMC6453882	Proteomic analysis reveals novel insights into tan...	Angela Contrera...	2019	Figure 4	Overview of the proposed biosynthet...
-PMC6475556	Protective Effect of the Total Triterpenes of Eusc...	Wei Huang, Hui ...	2019	Figure 7	The mechanisms of total triterpenes...
-PMC6486373	Emerging Antitumor Activities of the Bitter Melon ...	Evandro Fei EF ...	2019	Fig. (1)	Possible mechanisms of the anti-tum...
-"""
-
-
-def main():
-    # example_extract_bbox_for_image_without_arrows()
-    # example_extract_bbox_from_hocr_file()
-    # example_2_fill_bbox_in_image()
-    example_find_phrases()
-
-
-if __name__ == '__main__':
-    main()
diff --git a/pyimage/obsolete.py b/pyimage/obsolete.py
deleted file mode 100644
index 0b7f73b..0000000
--- a/pyimage/obsolete.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# maybe worked with network_x
-def get_maxx_maxy_non_pythonic(self, node_dict, nodes):
-    maxx = -999999
-    maxy = -999999
-    for node, i in enumerate(nodes):
-        # certainly a more pythonic way exists
-        x = node_dict[i][0]
-        if x > maxx:
-            maxx = x
-        y = node_dict[i][1]
-        if y > maxy:
-            maxy = y
-    return maxx, maxy
-
diff --git a/pyimage/octree.py b/pyimage/octree.py
deleted file mode 100644
index 8859eed..0000000
--- a/pyimage/octree.py
+++ /dev/null
@@ -1,266 +0,0 @@
-# !/usr/bin/env python
-#-*- coding: utf8 -*-
-# from https://github.com/delimitry/octree_color_quantizer CC0, thanks
-
-# library
-from pathlib import Path
-from PIL import Image
-# local
-from ..pyimage.old_code.ColorModule import Color
-
-class OctreeNode(object):
-    """
-    Octree Node class for color quantization
-    """
-
-    def __init__(self, level, parent):
-        """
-        Init new Octree Node
-        """
-        self.color = Color(0, 0, 0)
-        self.pixel_count = 0
-        self.palette_index = 0
-        self.children = [None for _ in range(8)]
-        # add node to current level
-        if level < OctreeQuantizer.MAX_DEPTH - 1:
-            parent.add_level_node(level, self)
-
-    def is_leaf(self):
-        """
-        Check that node is leaf
-        """
-        return self.pixel_count > 0
-
-    def get_leaf_nodes(self):
-        """
-        Get all leaf nodes
-        """
-        leaf_nodes = []
-        for i in range(8):
-            node = self.children[i]
-            if node:
-                if node.is_leaf():
-                    leaf_nodes.append(node)
-                else:
-                    leaf_nodes.extend(node.get_leaf_nodes())
-        return leaf_nodes
-
-    def get_nodes_pixel_count(self):
-        """
-        Get a sum of pixel count for node and its children
-        """
-        sum_count = self.pixel_count
-        for i in range(8):
-            node = self.children[i]
-            if node:
-                sum_count += node.pixel_count
-        return sum_count
-
-    def add_color(self, color, level, parent):
-        """
-        Add `color` to the tree
-        """
-        if level >= OctreeQuantizer.MAX_DEPTH:
-            self.color.red += color.red
-            self.color.green += color.green
-            self.color.blue += color.blue
-            self.pixel_count += 1
-            return
-        index = self.get_color_index_for_level(color, level)
-        if not self.children[index]:
-            self.children[index] = OctreeNode(level, parent)
-        self.children[index].add_color(color, level + 1, parent)
-
-    def get_palette_index(self, color, level):
-        """
-        Get palette index for `color`
-        Uses `level` to go one level deeper if the node is not a leaf
-        """
-        if self.is_leaf():
-            return self.palette_index
-        index = self.get_color_index_for_level(color, level)
-        if self.children[index]:
-            return self.children[index].get_palette_index(color, level + 1)
-        else:
-            # get palette index for a first found child node
-            for i in range(8):
-                if self.children[i]:
-                    return self.children[i].get_palette_index(color, level + 1)
-
-    def remove_leaves(self):
-        """
-        Add all children pixels count and color channels to parent node
-        Return the number of removed leaves
-        """
-        result = 0
-        for i in range(8):
-            node = self.children[i]
-            if node:
-                self.color.red += node.color.red
-                self.color.green += node.color.green
-                self.color.blue += node.color.blue
-                self.pixel_count += node.pixel_count
-                result += 1
-        return result - 1
-
-    def get_color_index_for_level(self, color, level):
-        """
-        Get index of `color` for next `level`
-        """
-        index = 0
-        mask = 0x80 >> level
-        if color.red & mask:
-            index |= 4
-        if color.green & mask:
-            index |= 2
-        if color.blue & mask:
-            index |= 1
-        return index
-
-    def get_color(self):
-        """
-        Get average color
-        """
-        return Color(
-            self.color.red // self.pixel_count,
-            self.color.green // self.pixel_count,
-            self.color.blue // self.pixel_count)
-
-
-class OctreeQuantizer(object):
-    """
-    Octree Quantizer class for image color quantization
-    Use MAX_DEPTH to limit a number of levels
-    """
-
-    MAX_DEPTH = 8
-
-    def __init__(self):
-        """
-        Init Octree Quantizer
-        """
-        self.levels = {i: [] for i in range(OctreeQuantizer.MAX_DEPTH)}
-        self.root = OctreeNode(0, self)
-
-    def get_leaves(self):
-        """
-        Get all leaves
-        """
-        return [node for node in self.root.get_leaf_nodes()]
-
-    def add_level_node(self, level, node):
-        """
-        Add `node` to the nodes at `level`
-        """
-        self.levels[level].append(node)
-
-    def add_color(self, color):
-        """
-        Add `color` to the Octree
-        """
-        # passes self value as `parent` to save nodes to levels dict
-        self.root.add_color(color, 0, self)
-
-    def make_palette(self, color_count):
-        """
-        Make color palette with `color_count` colors maximum
-        """
-        palette = []
-        palette_index = 0
-        leaf_count = len(self.get_leaves())
-        # reduce nodes
-        # up to 8 leaves can be reduced here and the palette will have
-        # only 248 colors (in worst case) instead of expected 256 colors
-        for level in range(OctreeQuantizer.MAX_DEPTH - 1, -1, -1):
-            if self.levels[level]:
-                for node in self.levels[level]:
-                    leaf_count -= node.remove_leaves()
-                    if leaf_count <= color_count:
-                        break
-                if leaf_count <= color_count:
-                    break
-                self.levels[level] = []
-        # build palette
-        for node in self.get_leaves():
-            if palette_index >= color_count:
-                break
-            if node.is_leaf():
-                palette.append(node.get_color())
-            node.palette_index = palette_index
-            palette_index += 1
-        return palette
-
-    def get_palette_index(self, color):
-        """
-        Get palette index for `color`
-        """
-        return self.root.get_palette_index(color, 0)
-
-def quantize(image, size=4):
-    import time
-    # from octree import OctreeQuantizer
-
-    # size = 16 => 256 colors for 8 bits per pixel output image
-
-    # print("pixels shape", image.shape)
-    pixels = image.load()
-    width, height = image.size
-    print(width, height)
-
-    octree = OctreeQuantizer()
-
-    # add colors to the octree
-    time0 = time.perf_counter()
-    for j in range(height):
-        for i in range(width):
-            octree.add_color(Color(*pixels[i, j]))
-    print("octree time", time.perf_counter()-time0)
-
-    palette, palette_image = create_palette_image(size, octree, width, height)
-    out_image = create_output_image(width, height, octree, palette, pixels)
-    return out_image, palette, palette_image
-
-def create_palette_image(size, octree, width, height):
-    from PIL import Image
-    import time
-    time0 = time.perf_counter()
-    palette = octree.make_palette(size * size)
-    palette_image = Image.new('RGB', (size, size))
-    palette_pixels = palette_image.load()
-    for i, color in enumerate(palette):
-        rgb = (color.red, color.green, color.blue)
-        palette_pixels[i % size, i // size] = rgb
-        # print("rgb", rgb)
-    print("palette time", time.perf_counter()-time0)
-    return palette, palette_image
-
-def create_output_image(width, height, octree, palette, pixels):
-    from PIL import Image
-    import time
-    time0 = time.perf_counter()
-    out_image = Image.new('RGB', (width, height))
-    out_pixels = out_image.load()
-    for j in range(height):
-        for i in range(width):
-            index = octree.get_palette_index(Color(*pixels[i, j]))
-            color = palette[index]
-            out_pixels[i, j] = (color.red, color.green, color.blue)
-
-    print("output time", time.perf_counter()-time0)
-    return out_image
-
-def main():
-    image_name = "red_black_cv.png"
-    image_name = "purple_ocimum_basilicum.png"
-    path = Path(Path(__file__).parent.parent, "assets", image_name)
-    assert path.exists()
-    # img = imageio.imread(path)
-    img = Image.open(path)
-
-    # ImageLib.image_show(img)
-    # print(img)
-
-    quantize(img, size=4)
-
-if __name__ == "__main__":
-    main()
\ No newline at end of file
diff --git a/pyimage/old_code/ColorModule.py b/pyimage/old_code/ColorModule.py
deleted file mode 100644
index 1cb9b69..0000000
--- a/pyimage/old_code/ColorModule.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env python
-#-*- coding: utf8 -*-
-
-
-class Color(object):
-    """
-    Simple RGB class
-    """
-
-    def __init__(self, red=0, green=0, blue=0, alpha=None):
-        """
-        Initialize color
-        """
-        self.red = red
-        self.green = green
-        self.blue = blue
-        self.alpha = alpha
-
-    def to_hex(self):
-        return f'{self.red:02x}' + f'{self.green:02x}' + f'{self.blue:02x}'
-
-    def is_equal(self, col):
-        # print("l", len(col))
-        return len(col) == 3 and col[0] == self.red and col[1] == self.green and col[2] == self.blue
-
-    def as_tuple(self):
-        return self.red, self.green, self.blue
diff --git a/pyimage/old_code/__init__.py b/pyimage/old_code/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/pyimage/old_code/graph_lib.py b/pyimage/old_code/graph_lib.py
deleted file mode 100644
index 37863b4..0000000
--- a/pyimage/old_code/graph_lib.py
+++ /dev/null
@@ -1,313 +0,0 @@
-import numpy as np
-import networkx as nx
-import copy
-from networkx.algorithms import tree
-from skimage import io
-import sknw  # must pip install sknw
-import logging
-from pathlib import PosixPath
-
-from pyimage.ami_image import AmiImage
-from pyimage.ami_util import AmiUtil
-# from pyimage.ami_island import AmiIsland
-# from pyimage.ami_node import AmiNode
-from pyimage.ami_skeleton import AmiSkeleton
-# from pyimage.ami_edge import AmiEdge
-
-
-class AmiGraph:
-    """holds AmiNodes and AmiEdges
-    may also hold subgraphs
-    """
-
-    logger = logging.getLogger("ami_graph")
-
-    def __init__(self, nx_graph, generate_nodes=True, nd_skeleton=None):
-        """create fro nodes and edges"""
-        # self.ami_node_dict = {}
-        # self.ami_edge_dict = {}
-        # self.generate_nodes = generate_nodes
-        self.nx_graph = None
-        self.ami_edges = None
-        self.ami_nodes = None
-        self.ami_island_list = None
-        # self.node_dict = None
-        self.nd_skeleton = nd_skeleton
-        # self.islands = None
-        if nx_graph is None:
-            raise Exception(f"nx_graph cannot be None")
-        self.read_nx_graph(nx_graph)
-        assert self.nx_graph is not None, f"ami_graph.nx_graph should not be None"
-        return
-
-    # def read_nodes(self, nodes):
-    #     """create a list of AmiNodes """
-    #     if nodes is not None:
-    #         for node in nodes:
-    #             self.add_raw_node(node)
-
-    def add_raw_node(self, raw_node, fail_on_duplicate=False):
-        """add a raw node either a string or string-indexed dict
-        if already a dict, deepcopy it
-        if a primitive make a node_dict and start it with raw_node as id
-        :raw_node: node to add, must have key
-        :fail_on_duplicate: if true fail if key already exists
-        """
-        if raw_node is not None:
-            # ami_node = AmiNode()
-            key = raw_node.key if type(raw_node) is dict else str(raw_node)
-            key = "n" + str(key)
-            if key in self.ami_node_dict and fail_on_duplicate:
-                raise AmiGraphError(f"cannot add same node twice {key}")
-            if type(raw_node) is dict:
-                self.ami_node_dict[key] = copy.deepcopy(raw_node)
-            else:
-                self.ami_node_dict[key] = "node"  # store just the key at present
-        else:
-            self.logger.warning("node cannot be None")
-
-    def read_edges(self, edges):
-        # self.ami_edges = edges
-        if len(self.ami_node_dict.keys()) == 0 and self.generate_nodes:
-            self.generate_nodes_from_edges()
-            print("after node generation", str(self))
-        for i, edge in enumerate(edges):
-            idx = "e" + str(i)
-            self.add_edge(edge, idx)
-
-    def add_edge(self, raw_edge, idx, fail_on_duplicate=True):
-        if fail_on_duplicate and idx in self.ami_edge_dict.keys():
-            raise ValueError("duplicate edge")
-
-        if raw_edge is None:
-            raise AmiGraphError("cannot add edge=None")
-        edge1 = ("n" + str(raw_edge[0]), "n" + str(raw_edge[1]))
-        self.ami_edge_dict[idx] = edge1
-
-    def generate_nodes_from_edges(self):
-        if self.ami_edges is not None:
-            for edge in self.ami_edges:
-                self.add_raw_node(edge[0])
-                self.add_raw_node(edge[1])
-
-    @classmethod
-    def create_ami_graph(cls, nd_skeleton):
-        """Uses Sknw to create a graph object within a new AmiGraph"""
-        # currently only called in a test
-        nx_graph = sknw.build_sknw(nd_skeleton)
-        ami_graph = AmiGraph(nx_graph, nd_skeleton=nd_skeleton)
-        # ami_graph.read_nx_graph(nx_graph )
-        print(f"***ami_graph\n {ami_graph}\n")
-        return ami_graph
-
-    def ingest_graph_info(self):
-        if self.nx_graph is None:
-            self.logger.warning("Null graph")
-            return
-        # print("graph", self.nx_graph)
-        nx_island_list = list(nx.connected_components(self.nx_graph))
-        if nx_island_list is None or len(nx_island_list) == 0:
-            self.logger.warning("No islands")
-            return
-
-        self.assert_nx_island_info(nx_island_list)
-        nx_edgelist = self.get_edge_list_through_mininum_spanning_tree()
-        self.debug_edges_and_nodes(nx_edgelist, debug_count=7)
-        nodes = self.nx_graph.nodes
-        self.node_dict = {i: (nodes[node]["o"][0], nodes[node]["o"][1]) for i, node in enumerate(nodes)}
-
-        self.ami_island_list = []
-        for nx_island in nx_island_list:
-            ami_island = self.create_ami_island(nx_island, skeleton=self.nd_skeleton)
-            print(f"ami_island {ami_island}")
-            self.ami_island_list.append(ami_island)
-
-        return
-
-    def assert_nx_island_info(self, nx_island_list):
-        nx_island0 = nx_island_list[0]
-        assert type(nx_island0) is set
-        assert len(nx_island0) > 0
-        elem0 = list(nx_island0)[0]
-        assert type(elem0) is int, f"island elem are {type(elem0)}"
-        print("islands", self.ami_island_list)
-
-    def debug_edges_and_nodes(self, nx_edgelist, debug_count=5):
-        start_node = 0
-        end_node = 1
-        pts_index = 2
-        for edge in nx_edgelist[:debug_count]:
-            pts_ = edge[pts_index]['pts']
-            print(edge[start_node], edge[end_node], "pts in edge", len(pts_))
-        edgelist_pts_ = nx_edgelist[0][2]['pts']
-        for step in edgelist_pts_[:debug_count]:
-            print("step", step)
-
-    def get_edge_list_through_mininum_spanning_tree(self):
-        mst = tree.maximum_spanning_edges(self.nx_graph, algorithm="kruskal", data=True)
-        # mst = tree.minimum_spanning_tree(graph, algorithm="kruskal")
-        nx_edgelist = list(mst)
-        return nx_edgelist
-
-    @classmethod
-    def set_bbox_pixels_to_color(cls, bbox, image, colorx=255):
-        """sets all pixels in box to uniform color
-
-        :param bbox:
-        :param image:
-        :param colorx:
-        :return: modified image
-        """
-        xx = bbox[0]
-        yy = bbox[1]
-        image[xx[0]:xx[1], yy[0]:yy[1]] = colorx
-        return image
-
-    def __str__(self):
-        s = "nodes: " + str(self.ami_nodes) + \
-            "\n edges: " + str(self.ami_edges)
-        return s
-
-    def read_nx_graph(self, nx_graph):
-        """
-        Read and unpack NetworkX graph.
-        This may change as a result of changing data models
-        the nx_graph may be tyhe fundamental data structure
-        :param nx_graph:
-        :return:
-        """
-        # self.nodes_as_dicts = [nx_graph.node[ndidx] for ndidx in (nx_graph.nodes())]
-        # self.nodes_yx = [nx_graph.node[ndidx][AmiSkeleton.CENTROID] for ndidx in (nx_graph.nodes())]
-        self.read_nx_edges(nx_graph)
-        self.read_nx_nodes(nx_graph)
-        # this may be the critical data structure and the others are convenience
-        self.nx_graph = nx_graph
-
-        self.ingest_graph_info()
-        return
-
-    def read_nx_nodes(self, nx_graph):
-        self.ami_nodes = []
-        node_ids = nx_graph.nodes()
-        for node_id in node_ids:
-            # node_dict = node_ids[node_id]
-            assert len(str(node_id)) < 4, f"node_id is {node_id}"
-            ami_node = AmiNode(ami_graph=self, node_id=node_id, nx_graph=nx_graph)
-            ami_node.set_centroid_yx(nx_graph.nodes[node_id][AmiSkeleton.CENTROID])
-            # ami_node.read_nx_node(node_dict)
-            self.ami_nodes.append(ami_node)
-
-    def read_nx_edges(self, nx_graph):
-        self.ami_edges = []
-        for (start, end) in nx_graph.nx_edges():
-            points_yx = nx_graph[start][end][AmiSkeleton.NODE_PTS]
-            ami_edge = AmiEdge()
-            ami_edge.read_nx_edge_points_yx_into_self_points_xy(points_yx)
-            self.ami_edges.append(ami_edge)
-
-    def get_or_create_ami_islands(self):
-        """
-        AmiIslands are nx_graph 'components' with added functionality
-        :return: list of AmiIslands
-        """
-        if self.ami_island_list is None and self.nx_graph is not None:
-            self.ami_island_list = [self.create_ami_island(comp) for comp in
-                                    nx.algorithms.components.connected_components(self.nx_graph)]
-        return self.ami_island_list
-
-    @classmethod
-    def create_nx_graph_from_arbitrary_image_file(cls, path):
-        assert path.exists() and not path.is_dir(), f"{path} should be existing file"
-        AmiUtil.check_type_and_existence(path, PosixPath)
-        image1 = io.imread(path)
-        AmiUtil.check_type_and_existence(image1, np.ndarray)
-        gray_image = AmiImage.create_grayscale_from_image(image1)
-        skeleton_array = AmiImage.create_white_skeleton_from_image(gray_image)
-        nx_graph = AmiGraph.create_nx_graph_from_skeleton(skeleton_array)
-        return nx_graph
-
-    def get_ami_islands_from_nx_graph(self):
-        """
-        Get the pixel-disjoint "islands" as from NetworkX
-        :return: list of AmiIslands
-        """
-
-        self.get_coords_for_nodes_and_edges_from_nx_graph(self.nx_graph)
-        assert self.nx_graph is not None
-        ami_islands = []
-        for node_ids in nx.algorithms.components.connected_components(self.nx_graph):
-            print("node_ids ", node_ids)
-            ami_island = self.create_ami_island(node_ids)
-            assert ami_island is not None
-            assert type(ami_island) is AmiIsland
-            ami_islands.append(ami_island)
-        return ami_islands
-
-    def extract_coords_for_node(self, idx):
-        """
-        gets coords for a single node with given id
-        :param idx: normally an int
-        :return:
-        """
-        node_data = self.nx_graph.nodes[idx]
-        centroid = node_data[AmiSkeleton.CENTROID]
-        centroid = (centroid[1], centroid[0])  # swap y,x as sknw seems to have this unusual order
-        return centroid
-
-    def create_islands(self):
-        """
-        needs nx_graph to exist
-
-        :return: list of islands
-        """
-
-        assert self.nx_graph is not None
-        self.islands = self.get_ami_islands_from_nx_graph()
-        return self.islands
-
-    @classmethod
-    def create_ami_island(self, node_ids, skeleton=None):
-        """
-        create from a list of node_ids (maybe from sknw)
-        maybe should be instance method of ami_graph
-        :param node_ids: set of node ids
-        :param ami_graph: essential
-        :param skeleton:
-        :return: AmiIsland object
-        """
-        assert type(node_ids) is set, "componente mus be of type set"
-        assert len(node_ids) > 0 , "components cannot be empty"
-
-        ami_island = AmiIsland()
-        ami_island.node_ids = node_ids
-        ami_island.ami_skeleton = skeleton
-        ami_island.ami_graph = self
-        print(f"ami_island =============  {ami_island.ami_graph}")
-        print("ami_island", ami_island)
-        return ami_island
-
-
-
-class AmiGraphError(Exception):
-    def __init__(self, msg):
-        super().__init__(msg)
-
-
-if __name__ == '__main__':
-
-    img = np.array([
-        [0, 0, 0, 1, 0, 0, 0, 0, 0],
-        [0, 0, 0, 1, 0, 0, 0, 1, 0],
-        [0, 0, 0, 1, 0, 0, 0, 1, 0],
-        [0, 0, 0, 1, 0, 0, 0, 0, 0],
-        [1, 1, 1, 1, 0, 0, 0, 0, 0],
-        [0, 0, 0, 0, 1, 0, 0, 0, 0],
-        [0, 1, 0, 0, 0, 1, 0, 0, 0],
-        [1, 0, 1, 0, 0, 1, 1, 1, 1],
-        [0, 1, 0, 0, 1, 0, 0, 0, 0],
-        [0, 0, 0, 1, 0, 0, 0, 0, 0]])
-
-    # sknw.example1()
-    # sknw.example2horse()  # works
-    # sknw.example3() # needs flipping White to black
-    # sknw.example4() # needs flipping White to black
diff --git a/pyimage/old_code/image_lib.py b/pyimage/old_code/image_lib.py
deleted file mode 100644
index fa320d1..0000000
--- a/pyimage/old_code/image_lib.py
+++ /dev/null
@@ -1,601 +0,0 @@
-
-import numpy as np
-import matplotlib.pyplot as plt
-from skimage import filters, color, io, data, draw
-from skimage.exposure import histogram
-import skimage.segmentation as seg
-from skimage.filters import threshold_otsu, gaussian
-from skimage.color import rgb2gray
-from skimage.segmentation import active_contour
-from pathlib import Path
-from PIL import Image
-
-# https://scikit-image.org/docs/dev/user_guide/tutorial_segmentation.html
-# https://scikit-image.org/docs/dev/auto_examples/edges/plot_active_contours.html
-
-from skimage.morphology import medial_axis, skeletonize, thin
-
-"""Code copied from earlier PMR ImageLib
-being gradually converted into ImageProcessor
-"""
-
-
-class ImageLib:
-    def __init__(self):
-        self.image = None
-        self.path = "assets/purple_ocimum_basilicum.png"
-        # self.old_init()
-
-    def old_init(self):
-        # should not be run on init
-        self.image = data.binary_blobs()
-        self.image = io.imread('../../images/green.png')
-        self.image = rgb2gray(self.image)
-        self.image = 255 - self.image
-        thresh = threshold_otsu(self.image)
-        thresh = 40
-        binary = self.image > thresh
-        self.image = binary
-        print("read image")
-
-    def image_import(self, path=None):
-        if path is None:
-            path = self.path
-        self.image = io.imread(path)
-
-    @classmethod
-    def image_show(cls, image, nrows=1, ncols=1, cmap='gray'):
-        print("image shape", image.shape)
-        fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=(14, 14))
-        ax.imshow(image, cmap='gray')
-        ax.axis('off')
-        return fig, ax
-
-    def image_show_top(self):
-        print("start image_show")
-
-        self.text = data.page()
-        print("text>>", self.text)
-        self.image_show(self.text)
-
-        fig, ax = plt.subplots(1, 1)
-        ax.hist(self.text.ravel(), bins=32, range=[0, 256])
-        ax.set_xlim(0, 256)
-
-        text_segmented = self.text > 50
-        self.image_show(text_segmented)
-
-        text_segmented = self.text > 70
-        self.image_show(text_segmented)
-
-        text_segmented = self.text > 120
-        self.image_show(text_segmented)
-
-        text_threshold = filters.threshold_otsu(self.text)
-        self.image_show(self.text > text_threshold)
-
-        text_threshold = filters.threshold_li(self.text)
-        self.image_show(self.text > text_threshold)
-
-        text_threshold = filters.threshold_local(self.text, block_size=51, offset=10)
-        self.image_show(self.text > text_threshold)
-        print("end image_show")
-
-
-class ImageExamples:
-
-    def circle_points(self, resolution, center, radius):
-        """
-        Generate points defining a circle on an image.
-        """
-        radians = np.linspace(0, 2 * np.pi, resolution)
-
-        c = center[1] + radius * np.cos(radians)
-        r = center[0] + radius * np.sin(radians)
-
-        return np.array([c, r]).T
-
-    def blobs(self):
-        print("start blobs")
-        self.image = data.binary_blobs()
-        plt.imshow(self.image, cmap='gray')
-        io.imsave('../../outputs/misc1/blobs.png', self.image)
-
-        #        self.image = data.astronaut()
-        #        plt.imshow(image)
-
-        self.image = io.imread('../../images/green.png')
-        plt.imshow(self.image)
-        io.imsave('../../outputs/misc1/green.png', self.image)
-
-        #        images = io.ImageCollection('../images/*.png:../images/*.jpg')
-        #        print('Type:', type(images))
-        #        images.files
-        #        Out[]: Type: <class ‘skimage.io.collection.ImageCollection’>
-
-        io.imsave('logo.png', self.image)
-        print("end blobs")
-
-    @classmethod
-    def supervised(cla):
-        print("start supervised")
-
-#        self.image = io.imread('girl.jpg')
-        image = data.astronaut()
-
-        plt.imshow(image)
-
-        image_gray = color.rgb2gray(image)
-        ImageLib.image_show(image_gray)
-
-        # Exclude last point because a closed path should not have duplicate points
-        points = ImageExamples.circle_points(200, [80, 250], 80)[:-1]
-
-        fig, ax = ImageLib.image_show(image)
-        ax.plot(points[:, 0], points[:, 1], '--r', lw=3)
-
-        snake = seg.active_contour(image_gray, points)
-        fig, ax = ImageLib.image_show(image)
-        ax.plot(points[:, 0], points[:, 1], '--r', lw=3)
-        ax.plot(snake[:, 0], snake[:, 1], '-b', lw=3)
-
-        snake = seg.active_contour(image_gray, points, alpha=0.06, beta=0.3)
-        fig, ax = ImageLib.image_show(image)
-        ax.plot(points[:, 0], points[:, 1], '--r', lw=3)
-        ax.plot(snake[:, 0], snake[:, 1], '-b', lw=3)
-
-        image_labels = np.zeros(image_gray.shape, dtype=np.uint8)
-        indices = draw.circle_perimeter(80, 250, 20)  # from here
-        image_labels[indices] = 1
-        image_labels[points[:, 1].astype(np.int), points[:, 0].astype(np.int)] = 2
-        ImageLib.image_show(image_labels)
-
-        image_segmented = seg.random_walker(image_gray, image_labels)
-        # Check our results
-        fig, ax = ImageLib.image_show(image_gray)
-        ax.imshow(image_segmented == 1, alpha=0.3)
-
-        image_segmented = seg.random_walker(image_gray, image_labels, beta=3000)
-        # Check our results
-        fig, ax = ImageLib.image_show(image_gray)
-        ax.imshow(image_segmented == 1, alpha=0.3)
-        print("end supervised")
-
-    @classmethod
-    def snake1(cls):
-
-        img = data.astronaut()
-        img = rgb2gray(img)
-
-        s = np.linspace(0, 2 * np.pi, 400)
-        r = 100 + 100 * np.sin(s)
-        c = 220 + 100 * np.cos(s)
-        init = np.array([r, c]).T
-
-        snake = active_contour(gaussian(img, 3),
-                               init, alpha=0.015, beta=10, gamma=0.001)
-
-        fig, ax = plt.subplots(figsize=(7, 7))
-        ax.imshow(img, cmap="Greys")
-        ax.plot(init[:, 1], init[:, 0], '--r', lw=3)
-        ax.plot(snake[:, 1], snake[:, 0], '-b', lw=3)
-        ax.set_xticks([]), ax.set_yticks([])
-        ax.axis([0, img.shape[1], img.shape[0], 0])
-
-        plt.show()
-
-    @classmethod
-    def snake2(cls):
-        img = data.text()
-
-        r = np.linspace(136, 50, 100)
-        c = np.linspace(5, 424, 100)
-        init = np.array([r, c]).T
-
-        snake = active_contour(gaussian(img, 1), init, boundary_condition='fixed',
-                               alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1)
-
-        fig, ax = plt.subplots(figsize=(9, 5))
-        ax.imshow(img, cmap="Grays")
-        ax.plot(init[:, 1], init[:, 0], '--r', lw=3)
-        ax.plot(snake[:, 1], snake[:, 0], '-b', lw=3)
-        ax.set_xticks([]), ax.set_yticks([])
-        ax.axis([0, img.shape[1], img.shape[0], 0])
-
-        plt.show()
-
-# unsupervised
-
-    @classmethod
-    def unsupervised(cls, image):
-        """
-        requires self.image
-        :return:
-        """
-        print("start unsupervised")
-        image_slic = seg.slic(image, n_segments=155)
-        ImageLib.image_show(color.label2rgb(image_slic, image, kind='avg'))
-
-        image_felzenszwalb = seg.felzenszwalb(image)
-        ImageLib.image_show(image_felzenszwalb)
-
-        np.unique(image_felzenszwalb).size
-
-        image_felzenszwalb_colored = color.label2rgb(image_felzenszwalb, image, kind='avg')
-        ImageLib.image_show(image_felzenszwalb_colored)
-        print("end unsupervised")
-
-# thin
-    @classmethod
-    def thin(cls):
-        print("start thin")
-
-        from skimage.morphology import skeletonize
-        from skimage.util import invert
-
-        # Invert the horse image
-        image = invert(data.horse())
-
-        # perform skeletonization
-        skeleton = skeletonize(image)
-
-        # display results
-        fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 4),
-                                 sharex=True, sharey=True)
-
-        ax = axes.ravel()
-
-        ax[0].imshow(image, cmap="Grays")
-        ax[0].axis('off')
-        ax[0].set_title('original', fontsize=20)
-
-        ax[1].imshow(skeleton, cmap="Grays")
-        ax[1].axis('off')
-        ax[1].set_title('skeleton', fontsize=20)
-
-        fig.tight_layout()
-        plt.show()
-        print("end thin")
-
-    @classmethod
-    def skel1(cls):
-        print("start skel1")
-        blobs = data.binary_blobs(200, blob_size_fraction=.2,
-                                  volume_fraction=.35, seed=1)
-
-        skeleton = skeletonize(blobs)
-        skeleton_lee = skeletonize(blobs, method='lee')
-
-        fig, axes = plt.subplots(1, 3, figsize=(8, 4), sharex=True, sharey=True)
-        ax = axes.ravel()
-
-        ax[0].imshow(blobs, cmap="Grays")
-        ax[0].set_title('original')
-        ax[0].axis('off')
-
-        ax[1].imshow(skeleton, cmap="Grays")
-        ax[1].set_title('skeletonize')
-        ax[1].axis('off')
-
-        ax[2].imshow(skeleton_lee, cmap="Grays")
-        ax[2].set_title('skeletonize (Lee 94)')
-        ax[2].axis('off')
-
-        fig.tight_layout()
-        plt.show()
-        print("end skel1")
-
-    @classmethod
-    def skel2(cls, image):
-        print("start skel2")
-        skeleton = skeletonize(image)
-        thinned = thin(image)
-        thinned_partial = thin(image, max_iter=25)
-
-        fig, axes = plt.subplots(2, 2, figsize=(8, 8), sharex=True, sharey=True)
-        ax = axes.ravel()
-
-        ax[0].imshow(image, cmap="Grays")
-        ax[0].set_title('original')
-        ax[0].axis('off')
-
-        ax[1].imshow(skeleton, cmap="Grays")
-        ax[1].set_title('skeleton')
-        ax[1].axis('off')
-
-        ax[2].imshow(thinned, cmap="Grays")
-        ax[2].set_title('thinned')
-        ax[2].axis('off')
-
-        ax[3].imshow(thinned_partial, cmap="Grays")
-        ax[3].set_title('partially thinned')
-        ax[3].axis('off')
-
-        fig.tight_layout()
-        plt.show()
-        print("end skel2")
-
-    @classmethod
-    def segment(cls):
-        print("start segment")
-        coins = data.coins()
-        hist, hist_centers = histogram(coins)
-        print("end segment")
-
-    @classmethod
-    def medial(cls):
-
-        # Generate the data
-        img = data.binary_blobs(200, blob_size_fraction=.2,
-                                volume_fraction=.35, seed=1)
-        """
-        img = io.imread('../../images/PMC5453356.png')
-        img = img > 20
-        """
-
-        img = io.imread('../../images/green.png')
-        img = rgb2gray(img)
-        img = 1 - img
-        print(sum(img), img)
-#        img = img < 128
-
-        # Compute the medial axis (skeleton) and the distance transform
-        skel, distance = medial_axis(img, return_distance=True)
-
-        # Compare with other skeletonization algorithms
-        skeleton = skeletonize(img)
-        skeleton_lee = skeletonize(img, method='lee')
-
-        # Distance to the background for pixels of the skeleton
-        dist_on_skel = distance * skel
-        print("dist on skel", dist_on_skel.shape, dist_on_skel)
-
-        fig, axes = plt.subplots(2, 2, figsize=(8, 8), sharex=True, sharey=True)
-        ax = axes.ravel()
-
-        ax[0].imshow(img, cmap="Grays")
-        ax[0].set_title('original')
-        ax[0].axis('off')
-
-        ax[1].imshow(dist_on_skel, cmap='magma')
-        ax[1].contour(img, [0.5], colors='w')
-        ax[1].set_title('medial_axis')
-        ax[1].axis('off')
-
-        dist_on_skel = dist_on_skel > 5.0
-
-        ax[2].imshow(skeleton, cmap="Grays")
-        ax[2].set_title('skeletonize')
-        ax[2].axis('off')
-
-        """
-        ax[3].imshow(skeleton_lee, cmap="Grays")
-        ax[3].set_title("skeletonize (Lee 94)")
-        ax[3].axis('off')
-        """
-        ax[3].imshow(dist_on_skel, cmap="Grays")
-        ax[3].set_title("thick lines")
-        ax[3].axis('off')
-
-#        fig.tight_layout()
-        plt.show()
-
-
-class Quantizer:
-    """colour quantizer for pixel images
-
-    a tortous journey to flattem images to a small set of colours
-    finally arriving at FASTOCTREE and convert()
-    """
-    OCTREE = "octree"
-
-    def __init__(self, input_dir, root=None, num_colors=8, method=None):
-        """   """
-        assert input_dir is not None and input_dir.exists(), "input dir must be an existing directory"
-        self.input_dir = input_dir
-        self.root = root
-        self.num_colors = num_colors
-        self.method = method
-        self.palette_dict = None
-
-    def create_and_write_color_streams(self, pil_img, num_colors=8, out_dir=None, out_form="png", out_root=None,
-                                       method=OCTREE, kmeans=8, dither=None):
-        """
-        Separates colours and flattens them.
-        The default method does this by histogram (I think). It is terrible for JPEGs
-        The octree separates by colour distance. recommended
-
-        :param pil_img:
-        :param num_colors:
-        :param out_dir:
-        :param out_form:
-        :param out_root:
-        :param method: if none uses octree
-                        else if method == "octree" uses PIL.Image.FASTOCTREE
-                        else uses given method
-
-        :param kmeans: default 8
-        :param dither: used in quantize, def = None, option  PIL.Image.FLOYDSTEINBERG
-
-        :return:
-        """
-        if method is not None:
-            self.method = method
-        if self.method:
-            if self.method == self.OCTREE:
-                self.method = Image.FASTOCTREE
-            img_out = pil_img.quantize(colors=self.num_colors, method=self.method, kmeans=kmeans, dither=dither)
-        else:
-            img_out = pil_img.convert('P', palette=Image.ADAPTIVE, colors=self.num_colors)
-        print(f"\nform {img_out.format}, size {img_out.size}, mode {img_out.mode}")
-        img_out.save(Path(out_dir, "palette"+"." + out_form), out_form)
-        img_rgb = img_out.convert("RGB")
-        img_rgb.save(Path(out_dir, "rgb"+"." + out_form), out_form)
-        # [146 209  80]
-        rgb_array = np.array(img_rgb)
-        single_chan = self.replace_single_color(rgb_array,
-                                                old_col=[146, 209, 80],
-                                                new_col=[255, 0, 0],
-                                                back_col=[220, 255, 255])
-        plt.imsave(Path(out_dir, "single" + "." + out_form), single_chan)
-        self.palette_dict = self.create_palette(img_out)
-        print("palette", self.palette_dict)
-        self.create_monochrome_images_of_color_streams(np.array(img_out), out_dir, out_form)
-        image_by_hx = self.create_monochrome_images_from_rgb(np.array(img_out))
-        print("image by hex", image_by_hx)
-
-    def create_palette(self, img):
-        """
-        extract palette for "P" image
-        index on hexstring
-        :param img:
-        :return: dict of counts with 6-char hex index
-        """
-        palette_dict = {}
-        palette = img.getpalette()
-        rgb_palette = np.reshape(palette, (-1, 3))
-        count_rgb_list = img.getcolors(self.num_colors)
-        print(f"colours {len(count_rgb_list)}")  # ca 48 non-zer0
-        for count_rgb in count_rgb_list:
-            rgb = rgb_palette[count_rgb[1]]
-            hx = rgb2hex(rgb)
-            # print(f"{count_rgb[0]} {hx} {rgb}")
-            count = count_rgb[0]
-            if count != 0:
-                palette_dict[hx] = count
-        return palette_dict
-
-    def replace_single_color(self, rgb_array, old_col, new_col, back_col=None):
-        if back_col is None:
-            back_col = [0., 0., 0.]
-        single_chan = np.where(rgb_array == old_col, new_col, back_col)
-        single_chan = np.multiply(single_chan, 1.0 / 255.)
-        return single_chan
-
-    def create_monochrome_images_of_color_streams(self, img_array, out_dir, out_form="png"):
-        for palette_index in range(self.num_colors):
-            if out_dir:
-                out_path = Path(out_dir, "p" + str(palette_index) + "." + out_form)
-                # img1 = np.where(img_array == color, True, False)
-                img1 = np.where(img_array == palette_index, palette_index, 254)
-                plt.imsave(out_path, img1)
-
-    def create_monochrome_images_from_rgb(self, rgb_array, back_col=None):
-        if back_col is None:
-            back_col = [0., 110., 220., ]
-        new_array_dict = {}
-        print("RGB ", rgb_array.shape)
-        for hex_col in self.palette_dict:
-            rgb = hex2rgb(hex_col)
-            rgbx = [float(rgb[0]), float(rgb[1]), float(rgb[2])]
-            new_array = None
-            # new_array = np.where(int(rgb_array) == rgb, rgbx, back_col)
-            # print("NP COUNT", np.count_nonzero(new_array))
-            # print ("rgb shape...", new_array.shape)
-            new_array_dict[rgb2hex(rgb)] = new_array
-        return new_array_dict
-
-    def extract_color_streams(self):
-        in_path = None
-        suffixes = ["png", "jpeg", "jpg"]
-        for suffix in suffixes:
-            in_path = Path(self.input_dir, self.root + "." + suffix)
-            if in_path.exists():
-                break
-        if in_path is None:
-            print(f"cannot find images with root {self.root}")
-            return
-        out_dir = self.make_out_dir(self.input_dir, self.root)
-        img = Image.open(in_path)
-        self.create_and_write_color_streams(img, num_colors=8, out_dir=out_dir)
-
-    def make_out_dir(self, in_dir, root):
-        out_root = Path(in_dir, root)
-        if not out_root.exists():
-            out_root.mkdir()
-        return out_root
-
-
-def rgb2hex(rgb):
-    """convert rgb 3-array to 8 char hex string
-    :param rgb: 3-array of ints
-    :return: "hhhhhh" string does NOT prepend "0x"
-    """
-    assert len(rgb) == 3
-    # assert type(rgb[0]) is int, f"found {type(rgb[0])} {rgb[0]}, in rgb"
-    assert 0 <= rgb[0] <= 255, f"found {rgb[0]}, in rgb"
-    s = ""
-    for r in rgb:
-        h = hex(r)[2:] if r >= 16 else "0" + hex(r)[2:]
-        s += h
-    return s
-
-
-def hex2rgb(hx):
-    """
-    transform 6-digit hex number into [r,g,b] integers
-
-    :param hx:
-    :return:
-    """
-    assert len(hx) == 6
-    rgb = []
-    for r in range(3):
-        ss = "0x" + hx[2 * r: 2 * r + 2]
-        rr = int(ss, 16)
-        rgb.append(rr)
-    return rgb
-
-
-def main():
-    print("started image_lib")
-    image_lib = ImageLib()
-    segment = False
-    blobs = False
-    show_top = False
-    supervised = False
-    snake1 = False
-    snake2 = False
-    unsupervised = False
-    thin = False
-    skel1 = False
-    skel2 = False
-    medial = True
-
-    if segment:
-        ImageExamples.segment()
-    if blobs:
-        ImageExamples.blobs()
-    if show_top:
-        ImageExamples.image_show_top()
-    if supervised:
-        ImageExamples.supervised()
-    if snake1:
-        ImageExamples.snake1()
-    if snake2:
-        ImageExamples.snake2()
-    if unsupervised:
-        ImageExamples.unsupervised()
-    if thin:
-        ImageExamples.thin()
-    if skel1:
-        ImageExamples.skel1()
-    if skel2:
-        ImageExamples.skel2()
-    if medial:
-        ImageExamples.medial()
-
-    """
-    while True:
-        print(".")
-        time.sleep(1.0)
-        return
-    """
-
-    print("END")
-
-    print("finished image_lib")
-
-
-if __name__ == "__main__":
-    main()
diff --git a/pyimage/old_code/old_octree.py b/pyimage/old_code/old_octree.py
deleted file mode 100644
index 4ef83d8..0000000
--- a/pyimage/old_code/old_octree.py
+++ /dev/null
@@ -1,272 +0,0 @@
-# !/usr/bin/env python
-#-*- coding: utf8 -*-
-# from https://github.com/delimitry/octree_color_quantizer CC0, thanks
-from pathlib import Path
-
-from pyimage.old_code.ColorModule import Color
-from PIL import Image
-
-
-class OctreeNode(object):
-    """
-    Octree Node class for color quantization
-    """
-
-    def __init__(self, level, parent):
-        """
-        Init new Octree Node
-        """
-        self.color = Color(0, 0, 0)
-        self.pixel_count = 0
-        self.palette_index = 0
-        self.children = [None for _ in range(8)]
-        # add node to current level
-        if level < OctreeQuantizer.MAX_DEPTH - 1:
-            parent.add_level_node(level, self)
-
-    def is_leaf(self):
-        """
-        Check that node is leaf
-        """
-        return self.pixel_count > 0
-
-    def get_leaf_nodes(self):
-        """
-        Get all leaf nodes
-        """
-        leaf_nodes = []
-        for i in range(8):
-            node = self.children[i]
-            if node:
-                if node.is_leaf():
-                    leaf_nodes.append(node)
-                else:
-                    leaf_nodes.extend(node.get_leaf_nodes())
-        return leaf_nodes
-
-    def get_nodes_pixel_count(self):
-        """
-        Get a sum of pixel count for node and its children
-        """
-        sum_count = self.pixel_count
-        for i in range(8):
-            node = self.children[i]
-            if node:
-                sum_count += node.pixel_count
-        return sum_count
-
-    def add_color(self, color, level, parent):
-        """
-        Add `color` to the tree
-        """
-        if level >= OctreeQuantizer.MAX_DEPTH:
-            self.color.red += color.red
-            self.color.green += color.green
-            self.color.blue += color.blue
-            self.pixel_count += 1
-            return
-        index = self.get_color_index_for_level(color, level)
-        if not self.children[index]:
-            self.children[index] = OctreeNode(level, parent)
-        self.children[index].add_color(color, level + 1, parent)
-
-    def get_palette_index(self, color, level):
-        """
-        Get palette index for `color`
-        Uses `level` to go one level deeper if the node is not a leaf
-        """
-        if self.is_leaf():
-            return self.palette_index
-        index = self.get_color_index_for_level(color, level)
-        if self.children[index]:
-            return self.children[index].get_palette_index(color, level + 1)
-        else:
-            # get palette index for a first found child node
-            for i in range(8):
-                if self.children[i]:
-                    return self.children[i].get_palette_index(color, level + 1)
-
-    def remove_leaves(self):
-        """
-        Add all children pixels count and color channels to parent node
-        Return the number of removed leaves
-        """
-        result = 0
-        for i in range(8):
-            node = self.children[i]
-            if node:
-                self.color.red += node.color.red
-                self.color.green += node.color.green
-                self.color.blue += node.color.blue
-                self.pixel_count += node.pixel_count
-                result += 1
-        return result - 1
-
-    @classmethod
-    def get_color_index_for_level(cls, color, level):
-        """
-        Get index of `color` for next `level`
-        """
-        index = 0
-        mask = 0x80 >> level
-        if color.red & mask:
-            index |= 4
-        if color.green & mask:
-            index |= 2
-        if color.blue & mask:
-            index |= 1
-        return index
-
-    def get_color(self):
-        """
-        Get average color
-        """
-        return Color(
-            self.color.red // self.pixel_count,
-            self.color.green // self.pixel_count,
-            self.color.blue // self.pixel_count)
-
-
-class OctreeQuantizer(object):
-    """
-    Octree Quantizer class for image color quantization
-    Use MAX_DEPTH to limit a number of levels
-    """
-
-    MAX_DEPTH = 8
-
-    def __init__(self):
-        """
-        Init Octree Quantizer
-        """
-        self.levels = {i: [] for i in range(OctreeQuantizer.MAX_DEPTH)}
-        self.root = OctreeNode(0, self)
-
-    def get_leaves(self):
-        """
-        Get all leaves
-        """
-        return [node for node in self.root.get_leaf_nodes()]
-
-    def add_level_node(self, level, node):
-        """
-        Add `node` to the nodes at `level`
-        """
-        self.levels[level].append(node)
-
-    def add_color(self, color):
-        """
-        Add `color` to the Octree
-        """
-        # passes self value as `parent` to save nodes to levels dict
-        self.root.add_color(color, 0, self)
-
-    def make_palette(self, color_count):
-        """
-        Make color palette with `color_count` colors maximum
-        """
-        palette = []
-        palette_index = 0
-        leaf_count = len(self.get_leaves())
-        # reduce nodes
-        # up to 8 leaves can be reduced here and the palette will have
-        # only 248 colors (in worst case) instead of expected 256 colors
-        for level in range(OctreeQuantizer.MAX_DEPTH - 1, -1, -1):
-            if self.levels[level]:
-                for node in self.levels[level]:
-                    leaf_count -= node.remove_leaves()
-                    if leaf_count <= color_count:
-                        break
-                if leaf_count <= color_count:
-                    break
-                self.levels[level] = []
-        # build palette
-        for node in self.get_leaves():
-            if palette_index >= color_count:
-                break
-            if node.is_leaf():
-                palette.append(node.get_color())
-            node.palette_index = palette_index
-            palette_index += 1
-        return palette
-
-    def get_palette_index(self, color):
-        """
-        Get palette index for `color`
-        """
-        return self.root.get_palette_index(color, 0)
-
-
-def quantize(image, size=4):
-    import time
-    from ColorModule import Color
-    # from Octree import OctreeQuantizer
-
-    # size = 16 => 256 colors for 8 bits per pixel output image
-
-    # print("pixels shape", image.shape)
-    pixels = image.load()
-    width, height = image.size
-    print(width, height)
-
-    octree = OctreeQuantizer()
-
-    # add colors to the octree
-    time0 = time.perf_counter()
-    for j in range(height):
-        for i in range(width):
-            octree.add_color(Color(*pixels[i, j]))
-    print("octree time", time.perf_counter()-time0)
-
-    palette, palette_image = create_palette_image(size, octree, width, height)
-    out_image = create_output_image(width, height, octree, palette, pixels)
-    return out_image, palette, palette_image
-
-
-def create_palette_image(size, octree, width, height):
-    from PIL import Image
-    import time
-    time0 = time.perf_counter()
-    palette = octree.make_palette(size * size)
-    palette_image = Image.new('RGB', (size, size))
-    palette_pixels = palette_image.load()
-    for i, color in enumerate(palette):
-        rgb = (color.red, color.green, color.blue)
-        palette_pixels[i % size, i // size] = rgb
-        print("rgb", rgb)
-    print("palette time", time.perf_counter()-time0)
-    return palette, palette_image
-
-
-def create_output_image(width, height, octree, palette, pixels):
-    from PIL import Image
-    import time
-    time0 = time.perf_counter()
-    out_image = Image.new('RGB', (width, height))
-    out_pixels = out_image.load()
-    for j in range(height):
-        for i in range(width):
-            index = octree.get_palette_index(Color(*pixels[i, j]))
-            color = palette[index]
-            out_pixels[i, j] = (color.red, color.green, color.blue)
-
-    print("output time", time.perf_counter()-time0)
-    return out_image
-
-
-def main():
-    # image_name = "red_black_cv.png"
-    image_name = "purple_ocimum_basilicum.png"
-    path = Path(Path(__file__).parent.parent, "assets", image_name)
-    assert path.exists()
-    # img = imageio.imread(path)
-    img = Image.open(path)
-
-    # ImageLib.image_show(img)
-    # print(img)
-
-    quantize(img, size=4)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/pyimage/old_code/pmrsknw.py b/pyimage/old_code/pmrsknw.py
deleted file mode 100644
index d5afe22..0000000
--- a/pyimage/old_code/pmrsknw.py
+++ /dev/null
@@ -1,309 +0,0 @@
-from skimage.morphology import skeletonize
-import numpy as np
-import networkx as nx
-import matplotlib.pyplot as plt
-from pathlib import Path
-from skimage.filters import threshold_otsu
-from skimage import data
-import skimage.io
-
-
-# various experiments by PMR
-
-# =======================================
-# This draws a graph from a map of 0/1 pixels . Uses networkx
-# looks useful
-# from https://github.com/Image-Py/sknw/blob/master/sknw/sknw.py
-
-class PmrSknw:
-    def __init__(self):
-        self.graph = None
-        self.img = None
-
-    @classmethod
-    def calc_mask_values_of_neighbors(cls, shape):
-        """not sure what this does yet
-        I think it might take a region and create margins
-        """
-        dim = len(shape)  # dimension of shape (?always 2)
-        # create 3*3 block around a point
-        block = np.ones([3] * dim)
-        # centre is now 0, so [1,1,1  1,0,1  1,1,1]
-        block[tuple([1] * dim)] = 0  # sets (1,1) = 0 (i.e. the centre)
-        idx = np.where(block > 0)  # (array([0,0,0 1,1 2,2,2]), array([0,1,2 0,2 0,1,2]))
-        idx1 = np.array(idx, dtype=np.uint8).T  # [[0,0], [0,1], [0,2],   [1.0], [1,2],   [2,0], [2,1], [2,2]]
-        idx2 = np.array(idx1 - [1] * dim)  # [[-1 -1] [-1 0] [-1 1]   [0, -1] [0 1]   [1 -1] [1 0] [1 1]]
-        shape_ = (1,) + shape[::-1][:-1]  # (1, 402)
-        acc = np.cumprod(shape_)  # [1 402]
-        neighbors = np.dot(idx2, acc[::-1])
-        print("neighbors", neighbors.shape, neighbors)  # (8,) [-403 -402 -401 -1 1 401 402 403
-        return neighbors
-
-    # my mark
-    @classmethod
-    def mark(cls, buf, nbs):  # mark the array use (0, 1, 2)
-        buf = buf.ravel()
-        for p in range(len(buf)):
-            if buf[p] == 0:
-                continue
-            s = 0
-            for dp in nbs:
-                if buf[p + dp] != 0:
-                    s += 1
-            if s == 2:
-                buf[p] = 1
-            else:
-                buf[p] = 2
-
-    # trans index to r, c...
-    @classmethod
-    def idx2rc(cls, idx, acc):
-        rst = np.zeros((len(idx), len(acc)), dtype=np.int16)
-        for i in range(len(idx)):
-            for j in range(len(acc)):
-                rst[i, j] = idx[i] // acc[j]
-                idx[i] -= rst[i, j] * acc[j]
-        rst -= 1
-        return rst
-
-    # fill a node (may be two or more points)
-    def fill(self, imgx, p, num, nbs, acc, buf):
-        imgx[p] = num
-        buf[0] = p
-        cur = 0
-        s = 1
-        iso = True
-
-        while True:
-            p = buf[cur]
-            for dp in nbs:
-                cp = p + dp
-                if imgx[cp] == 2:
-                    imgx[cp] = num
-                    buf[s] = cp
-                    s += 1
-                if imgx[cp] == 1:
-                    iso = False
-            cur += 1
-            if cur == s:
-                break
-        # transform ? 1-dim index to row + column indexes
-        rc = self.idx2rc(buf[:s], acc)
-        return iso, rc
-
-    # trace the edge and use a buffer, then buf.copy, if use [] numba not works
-    def trace_edge_of_something(self, img, p, nbs, acc, buf):
-        c1 = 0
-        c2 = 0
-        newp = 0
-        cur = 1
-        while True:
-            buf[cur] = p
-            img[p] = 0
-            cur += 1
-            for dp in nbs:
-                cp = p + dp
-                if img[cp] >= 10:
-                    if c1 == 0:
-                        c1 = img[cp]
-                        buf[0] = cp
-                    else:
-                        c2 = img[cp]
-                        buf[cur] = cp
-                if img[cp] == 1:
-                    newp = cp
-            p = newp
-            if c2 != 0:
-                break
-        # create row and column indexes
-        rc = self.idx2rc(buf[:cur + 1], acc)
-
-        rc1 = (c1 - 10, c2 - 10, rc)
-        # print("rc1.shape", rc1.shape)
-        return rc1
-
-    # parse the image then get the nodes and edges
-    def parse_struc(self, img, nbs, acc, iso, ring):
-        imgx = img.ravel()  # flattened matrix
-        # NO IDEA where this number comes from
-        buf = np.zeros(131072, dtype=np.int64)
-        num = 10  # no idea what this is
-        nodes, num = self.create_nodes(acc, buf, imgx, iso, nbs, num)
-        edges = self.create_edges(acc, buf, imgx, nbs)
-        if ring:
-            self.create_cyclic_edges(acc, buf, edges, imgx, nbs, nodes, num)
-        return nodes, edges
-
-    def create_cyclic_edges(self, acc, buf, edges, imgx, nbs, nodes, num):
-        for p in range(len(imgx)):
-            if imgx[p] != 1:
-                continue
-            imgx[p] = num
-            num += 1
-            nodes.append(self.idx2rc([p], acc))
-            for dp in nbs:
-                if imgx[p + dp] == 1:
-                    edge = self.trace_edge_of_something(imgx, p + dp, nbs, acc, buf)
-                    edges.append(edge)
-
-    def create_nodes(self, acc, buf, imgx, iso, nbs, num):
-        nodes = []
-        for p in range(len(imgx)):
-            if imgx[p] == 2:
-                isiso, nds = self.fill(imgx, p, num, nbs, acc, buf)
-                if isiso and not iso:
-                    continue
-                num += 1
-                nodes.append(nds)
-        return nodes, num
-
-    def create_edges(self, acc, buf, imgx, nbs):
-        edges = []
-        for p in range(len(imgx)):
-            if imgx[p] < 10:
-                continue
-            for dp in nbs:
-                if imgx[p + dp] == 1:
-                    edge = self.trace_edge_of_something(imgx, p + dp, nbs, acc, buf)
-                    edges.append(edge)
-        return edges
-
-    # use nodes and edges build a networkx graph
-    @classmethod
-    def build_nx_graph(cls, nodes, edges, multi=False, full=True):
-        os = np.array([i.mean(axis=0) for i in nodes])
-        if full:
-            os = os.round().astype(np.uint16)
-        nx_graph = nx.MultiGraph() if multi else nx.Graph()
-        for i in range(len(nodes)):
-            nx_graph.add_node(i, pts=nodes[i], o=os[i])
-        for s, e, pts in edges:
-            if full:
-                pts[[0, -1]] = os[[s, e]]
-            ll = np.linalg.norm(pts[1:] - pts[:-1], axis=1).sum()
-            nx_graph.add_edge(s, e, pts=pts, weight=ll)
-        return nx_graph
-
-    def mark_node(self, img):
-        node_buf = np.pad(img, (1, 1), mode='constant')
-        nbs = self.calc_mask_values_of_neighbors(node_buf.shape)
-        acc = np.cumprod((1,) + node_buf.shape[::-1][:-1])[::-1]
-        self.mark(node_buf, nbs)
-        return node_buf
-
-    def build_sknw(self, img, multi=False, iso=True, ring=True, full=True):
-        """
-
-        :param img: image (probably thinned , maybe binary will have to research)
-        :param multi: ??
-        :param iso:  ??
-        :param ring: I think True allows cycles
-        :param full: ??
-        :return: nx_graph, nodes, edges
-        """
-        buf = np.pad(img, (1, 1), mode='constant')  # (330, 402)
-        nbs = self.calc_mask_values_of_neighbors(buf.shape)
-        acc = np.cumprod((1,) + buf.shape[::-1][:-1])[::-1]
-        self.mark(buf, nbs)
-        nodes, edges = self.parse_struc(buf, nbs, acc, iso, ring)
-        nx_graph = self.build_nx_graph(nodes, edges, multi, full)
-        return nx_graph, nodes, edges
-
-    def read_thinned_image_calculate_graph_and_plot(self, img):
-        node_img = self.mark_node(img)
-        self.graph = self.build_sknw(img, False, iso=True, ring=True)
-        from networkx.algorithms import tree
-        mst = tree.maximum_spanning_edges(self.graph, algorithm="kruskal", data=False)
-        edgelist = list(mst)
-        print("edges", edgelist)
-
-        plt.imshow(node_img[1:-1, 1:-1], cmap='gray')
-        self.plot_edges()
-        self.plot_nodes()
-        plt.title('Build Graph')
-        plt.show()
-
-    def plot_nodes(self):
-        nodes = self.graph.nodes()
-        ps = np.array([nodes[i]['o'] for i in nodes])
-        plt.plot(ps[:, 1], ps[:, 0], 'r.')
-
-    def plot_edges(self):
-        # plt.imshow(node_img[1:-1, 1:-1], cmap='gray')
-        # draw edges by pts
-        for (s, e) in self.graph.nx_edges():
-            ps = self.graph[s][e]['pts']
-            plt.plot(ps[:, 1], ps[:, 0], 'green')
-
-    # def example1(self):
-    #     self.read_thinned_image_calculate_graph_and_plot(img)
-
-    @classmethod
-    def example2horse(cls):
-        # open and skeletonize
-        img = data.horse()
-        # ske = skeletonize(img).astype(np.uint16)
-        # self.read_thinned_image_calculate_graph_and_plot(ske)
-        ske = skeletonize(~img).astype(np.uint16)
-
-        # build graph from skeleton
-        graph, nodes, edges = PmrSknw().build_sknw(ske)
-
-        # draw image
-        plt.imshow(img, cmap='gray')
-
-        # draw edges by pts
-        for (s, e) in graph.edges():
-            ps = graph[s][e]['pts']
-            plt.plot(ps[:, 1], ps[:, 0], 'green')
-
-        # draw node by o
-        nodes = graph.nodes()
-        ps = np.array([nodes[i]['o'] for i in nodes])
-        plt.plot(ps[:, 1], ps[:, 0], 'r.')
-
-        # title and show
-        plt.title('Build Graph')
-        plt.show()
-
-    def example3(self):
-        img = Path(Path(__file__).parent.parent, "assets/red_black_cv.png")
-        self.skeleton_and_plot(img)
-        return  # so we can display in debug mode!
-
-    def example4(self):
-        img = Path(Path(__file__).parent.parent, "test/resources/biosynth_path_3.png")
-        self.skeleton_and_plot(img)
-        return  # so we can display in debug mode!
-
-    def skeleton_and_plot(self, img):
-        assert img is not None, "cannot read image"
-        img = skimage.io.imread(img, as_gray=True)
-        thresh = threshold_otsu(img)
-        binary = img < thresh  # swap <> to invert image
-        print(binary)
-        plt.imshow(binary)
-        # plt.imshow(inv_ske, cmap='gray')
-        plt.title('Build Graph1')
-        plt.show()
-        # ske = skeletonize(img).astype(np.uint16)
-        ske = skeletonize(binary)
-        print(ske)
-        print("========ske=========")
-        # inv_ske = util.invert(ske)
-        plt.imshow(ske)
-        # plt.imshow(inv_ske, cmap='gray')
-        plt.title('Build Graph2')
-        plt.show()
-        print("========inv=========")
-        inv_ske = ske
-        self.read_thinned_image_calculate_graph_and_plot(inv_ske)
-
-
-def main():
-    PmrSknw().example3()
-    PmrSknw().example4()
-
-
-if __name__ == '__main__':
-    main()
diff --git a/pyimage/old_code/preprocessing.py b/pyimage/old_code/preprocessing.py
deleted file mode 100644
index 51c2ee7..0000000
--- a/pyimage/old_code/preprocessing.py
+++ /dev/null
@@ -1,162 +0,0 @@
-import numpy as np
-from skimage import io
-from skimage.color.colorconv import rgb2gray
-import skimage
-from skimage import morphology
-from skimage import filters
-from pathlib import Path
-import matplotlib.pyplot as plt
-
-from pyimage.old_code.graph_lib import AmiGraph
-
-"""
-The ImageProcessor class is current in development by PMR and Anuv for preprocessing images
-as a part of the project: "Extraction of biosynthetic pathway from images"
-
-probably obsolete and being moved to AmiImage
-
-Some part of the code has been copied from ImageLib written by PMR for openDiagram
-We decided against continuing development on openDiagram library because the size
-of the repository exceeded 2 gigabytes
-
-The ImageLib module has been included in this repository for testing and reference 
-"""
-
-# setting a sample image for default path
-DEFAULT_PATH = "assets/purple_ocimum_basilicum.png"
-TEST_RESOURCES_DIR = Path(Path(__file__).parent.parent, "test/resources")
-# BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1.png")
-BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1_cropped_text_removed.png")
-
-
-class ImageProcessor:
-
-    def __init__(self) -> None:
-        self.image = None
-        self.inverted = None
-
-    # TODO is this required?
-    def load_image(self, path):
-        """
-        loads image with io.imread
-        resets self.image
-        input: path
-        returns: None if path is None
-        """
-        self.image = None
-        if path is not None:
-            self.image = io.imread(path)
-        return self.image
-
-    # TODO classmethod
-    def to_gray(self):
-        """convert existing self.image to grayscale
-        uses rgb2gray from skimage.color.colorconv
-        """
-        self.image_gray = None
-        if self.image is not None:
-            self.image_gray = rgb2gray(self.image)
-        return self.image_gray
-
-    # TODO classmethod
-    def invert(self, image):
-        """Inverts the brightness values of the image"""
-        self.inverted = skimage.util.invert(image)
-        return self.inverted
-
-    # TODO class method
-    def skeletonize(self, image):
-        """Returns a skeleton of the image"""
-        mask = morphology.skeletonize(image)
-        self.skeleton = np.zeros(self.image.shape)
-        self.skeleton[mask] = 1
-        # print("Skeleton Image: ", self.skeleton)
-        return self.skeleton
-
-    @classmethod
-    def threshold(cls, image):
-        """"Returns a binary image using a threshold value"""
-        threshold = filters.threshold_otsu(image)
-        # self.binary_image = np.zeros(self.image.shape)
-        # print(self.image.shape)
-        # idx = self.image > threshold
-        # print("Threshold Mask: ", idx)
-        # self.binary_image[idx] = 1
-        # print("Binary Image: ", self.binary_image)
-        binary_image = np.where(image >= threshold, 1, 0)
-        return binary_image
-
-    def show_image(self, image):
-        """
-        Shows self.image in a separate window
-        :param image:
-        """
-        if self.image is None:
-            self.load_image()
-        io.imshow(image)
-        io.show()
-        return True
-
-    def example1(self):
-        TEST_RESOURCES_DIR = Path(Path(__file__).parent.parent, "test/resources")
-        assert TEST_RESOURCES_DIR.isdir(), f"{TEST_RESOURCES_DIR} must be existing directory"
-        # BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1.png")
-        BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1_cropped_text_removed.png")
-        print(BIOSYNTH_PATH_IMAGE)
-        self.load_image(BIOSYNTH_PATH_IMAGE)
-        # print(self.image)
-        # print(self.image.shape)
-        # from skimage.exposure import histogram
-        # hist, hist_centers = histogram(self.threshold(self.image))
-        # print(hist)
-        # print(hist_centers)
-
-        self.show_image(self.image)
-        inverted_image = self.invert(self.image)
-        self.show_image(inverted_image)
-        binary_image = self.threshold(inverted_image)
-        self.show_image(binary_image)
-        skeleton = self.skeletonize(binary_image)
-        self.show_image(skeleton)
-
-    def example_skeletonize_extract_subgraphs(self):
-        resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-        image = Path(resources_dir, "biosynth_path_1_cropped_text_removed.png")
-        self.load_image(image)
-        skeleton1 = self.invert_threshold_skeletonize()
-        skeleton = skeleton1
-        skeleton = skeleton.astype(np.uint16)
-        # print("skeleton values: ", skeleton)
-        print("skeleton type: ", type(skeleton))
-        print("skeleton value type: ", type(skeleton[0][0]))
-        print("skeleton shape:", skeleton.shape)
-
-        graph = AmiGraph.create_ami_graph(skeleton)
-        print("node_dict", graph)
-
-        fig, ax = plt.subplots()  # note we must use plt.subplots, not plt.subplot
-        # maxx, maxy = self.get_maxx_maxy_non_pythonic(node_dict, nodes)
-        # for edge in self.edges:
-        #     self.plot_line(node_dict, edge[0], edge[1], maxy)
-        # fig.savefig(Path(Path(__file__).parent.parent, "temp", "plotarrows.png"))
-
-    @classmethod
-    def plot_line(cls, node_dict, node0, node1, ymax):
-        # print("node", node0, type(node0))
-        xy0 = node_dict[node0]
-        xy1 = node_dict[node1]
-        # print("xy0 xy1", xy0, xy1)
-        # x and y are swapped
-        plt.plot([xy0[1], xy1[1]], [ymax - xy0[0], ymax - xy1[0]], marker="")
-        # print(type(node0))
-
-
-
-def main():
-    image_processor = ImageProcessor()
-    image_processor.example1()
-    # image_processor.example_skeletonize_extract_subgraphs()
-
-
-if __name__ == '__main__':
-    main()
diff --git a/pyimage/plot_demo.py b/pyimage/plot_demo.py
deleted file mode 100644
index 5eef63f..0000000
--- a/pyimage/plot_demo.py
+++ /dev/null
@@ -1,1 +0,0 @@
-"""routines which contain interactive plots"""
\ No newline at end of file
diff --git a/pyimage/svg.py b/pyimage/svg.py
deleted file mode 100644
index cab5e73..0000000
--- a/pyimage/svg.py
+++ /dev/null
@@ -1,716 +0,0 @@
-"""Common SVG elements in lightweight code"""
-from lxml.etree import ElementTree, Element, XPathSyntaxError
-import lxml.etree
-from abc import ABC
-from pathlib import Path
-import logging
-# local
-try:
-    from ..pyimage.bbox import BBox
-except:
-    from pyimage.bbox import BBox
-
-logger = logging.getLogger(__name__)
-
-"""Seems that it's hard to subclass lxml so this is based on delegation
-None of the SVG libraries (svgwrite, Cairo) are good for creating subclassed
-elements. We may add functionality to the actual lxml elements
-This is only the common object classes ... at the moment
-
-"""
-"""
-https://lxml.de/api/lxml.builder.ElementMaker-class.html
-"""
-
-FILL = "fill"
-NONE = "none"
-RED = "red"
-
-STROKE = "stroke"
-STROKE_WIDTH = "stroke-width"
-SVG_NS = "http://www.w3.org/2000/svg"
-SVG_NS_PREF = 'svg'
-
-GPML_NS = "http://pathvisio.org/GPML/2013a"
-
-class AbsSVG(ABC):
-    lxml.etree.register_namespace(SVG_NS_PREF, SVG_NS)
-
-    def __init__(self, tag):
-        """
-        if bbox is None never calculate it
-        if bbox is invalid (e.g. for rect, circle) recalculate when requested
-        if parameters are changed, reset bbox to invalid (BBox())
-        changes
-        :param tag:
-        """
-        self.fill = None
-        self.stroke = None
-        self.stroke_width = None
-
-        # ns_tag = f"{{{SVG_NS}}}{tag}"
-
-        ns_tag = "{" + SVG_NS + "}" + tag
-        self.element = Element(ns_tag)
-        # if self.bbox is None this element cannot have a bbox
-        self.bbox = None
-        # if set use width, height, etc. to calculate bbox (a common default)
-        self.calculate_bbox_from_values = True
-        # indexes every class object by its lxml delegate
-        self.svg_by_lxml = {}
-
-    def tostring(self, pretty_print=False):
-        s = lxml.etree.tostring(self.element, pretty_print=pretty_print, encoding="UTF-8").decode()
-        return s
-
-    def append(self, elem):
-        self.element.append(elem.element)
-
-    def set_attribute(self, name, value):
-        if name is not None and value is not None:
-            if type(name) is str and name[0].isalpha():
-                self.element.attrib[name] = value
-
-    def set_int_attribute(self, name, val):
-        self.set_attribute(name, str(int(val)))
-
-    def set_float_attribute(self, name, val):
-        self.set_attribute(name, str(float(val)))
-
-    @classmethod
-    def get_float_attribute(cls, elem, attname):
-        """
-        gets attribute value from elem@attname as float
-        if elem is list of length 1 takes that
-        if it cannot, raises ValueError
-        :param elem:
-        :param attname:
-        :return:
-        """
-        # att_float = None
-        if type(elem) is list and len(elem) == 1:
-            elem = elem[0]
-
-        try:
-            attval = elem.get(attname)
-            att_float = float(attval)
-        except Exception as e:
-            print("raises", e)
-            print(f"type elem = {type(elem)}")
-            logger.warning(f"{lxml.etree.tostring(elem)}")
-            raise ValueError(f"cannot get float from @{attname}")
-        return att_float
-
-    # @abstractmethod
-    def calculate_bbox(self):
-        """
-        only applies to elements with bbox not None
-        :return:
-        """
-        pass
-
-    def get_or_create_bbox(self):
-        if self.bbox is None or (not self.bbox.is_valid() and self.calculate_bbox_from_values):
-            self.calculate_bbox()
-        return self.bbox
-
-    def set_fill(self, fill):
-        self.fill = fill
-        self.set_attribute(FILL, fill)
-
-    def set_stroke(self, stroke):
-        self.stroke = stroke
-        self.set_attribute(STROKE, stroke)
-
-    def set_stroke_width(self, stroke_width):
-        self.stroke_width = stroke_width
-        self.set_attribute(STROKE_WIDTH, stroke_width)
-
-
-class SVGSVG(AbsSVG):
-    TAG = "svg"
-    SVG_WIDTH = "width"
-    SVG_HEIGHT = "height"
-
-    def __init__(self):
-        super().__init__(self.TAG)
-        self.wrapper_by_lxml = {}  # dictionary of SVG class indexed by wrapped lxml
-        self.nxml = 0  # counter of lxml elements
-        self.bbox = BBox()
-        self.set_width(1200)
-        self.set_height(1200)
-
-    def set_width(self, value):
-        self.set_float_attribute(self.SVG_WIDTH, value)
-
-    def set_height(self, value):
-        self.set_float_attribute(self.SVG_HEIGHT, value)
-
-    def calculate_bbox(self):
-        raise NotImplementedError("code not written, BBox should recurse through descendants")
-
-    def add_arrowhead(self):
-        """
-        add simple triangular arrowhead
-        need to check it hasn't been added
-        :return:
-        """
-        defs_element = self.get_or_create_defs()
-        assert defs_element is not None
-
-        id = "arrowhead"
-        clark_xpath = f"{{{SVG_NS}}}{SVGMarker.TAG}[@id='{id}']"
-        assert clark_xpath == f"{{http://www.w3.org/2000/svg}}marker[@id='arrowhead']", f"should be {clark_xpath}"
-
-        marker_element = ns_xpath(defs_element, clark_xpath, xpath_type="clark")
-        if not marker_element:
-            marker_element = SVGMarker(id=id, marker_width=10, marker_height=7, refx=0, refy=3.5, orient="auto")
-            defs_element.append(marker_element.element)
-            polygon = SVGPolygon(points="0 0, 10 3.5, 0 7")
-            marker_element.append(polygon)
-
-    def get_or_create_defs(self):
-
-        """
-        ensure a single <defs> in <svg>
-        :return: the lxml Element (not SVGDefs)
-        """
-        # svg_defs_element = None
-
-        # local_name_xpath = f"*[local-name()='{SVGDefs.TAG}' and namespace-uri()='{SVG_NS}']"
-        clark_xpath = f"{{{SVG_NS}}}{SVGDefs.TAG}"
-        assert clark_xpath == "{http://www.w3.org/2000/svg}defs"
-
-        defs_search = ns_xpath(self.element, clark_xpath, xpath_type="clark")
-        assert defs_search is not None
-        if type(defs_search) is not list:
-            defs_search = []
-
-        if len(defs_search) == 0:
-            svg_defs = SVGDefs()
-            self.append(svg_defs)
-            logger.warning("adding single <defs> element")
-            svg_defs_element = svg_defs.element
-        elif len(defs_search) == 1:
-            svg_defs_element = defs_search[0]
-        else:
-            svg_defs_element = None
-            logger.warning(f"more than one <defs> in <svg>")
-
-        if type(svg_defs_element) is list and len(svg_defs_element) == 1:
-            svg_defs_element = svg_defs_element[0]
-        return svg_defs_element
-
-
-class SVGG(AbsSVG):
-    TAG = "g"
-
-    def __init__(self):
-        super().__init__(self.TAG)
-
-    def calculate_bbox(self):
-        raise NotImplementedError("code not written, BBox should recurse through descendants")
-
-
-class SVGRect(AbsSVG):
-    TAG = "rect"
-    HEIGHT = "height"
-    WIDTH = "width"
-    X = "x"
-    Y = "y"
-
-    def __init__(self, bbox=None, xy_ranges=None):
-        super().__init__(self.TAG)
-        self.xy = None
-        self.width = None
-        self.height = None
-        # BBox is formally independent of the xy,w,h i.e. could be set directly
-        self.bbox = bbox
-
-        if self.bbox is not None:
-            self.set_xy([bbox.get_xrange()[0], bbox.get_yrange()[0]])
-            self.set_height(bbox.get_height())
-            self.set_width(bbox.get_width())
-        elif xy_ranges is not None:
-            try:
-                self.set_xy([xy_ranges[0][0], xy_ranges[1][0]])
-                self.set_height(xy_ranges[1][1] - xy_ranges[1][0])
-                self.set_width(xy_ranges[0][1] - xy_ranges[0][0])
-            except Exception as e:
-                logger.error(f"Bad xyranges {xy_ranges} should be two pairs of numbers")
-                raise ValueError(e)
-
-
-    def set_height(self, h):
-        """
-        sets height by adjusting existing bbox
-        :param h:
-        :return:
-        """
-        if h >= 0:
-            self.height = float(h)
-            self.set_float_attribute(self.HEIGHT, self.height)
-        self.set_invalid()
-
-    def set_width(self, w):
-        """
-        sets width by adjusting existing bbox
-        :param w:
-        :return:
-        """
-        if w >= 0:
-            self.width = float(w)
-            self.set_float_attribute(self.WIDTH, self.width)
-        self.set_invalid()
-
-    def set_xy(self, xy):
-        assert len(xy) == 2
-        self.xy = [float(xy[0]), float(xy[1])]
-        self.set_invalid()
-        self.set_float_attribute(self.X, self.xy[0])
-        self.set_float_attribute(self.Y, self.xy[1])
-
-    def is_valid(self):
-        try:
-            float(self.width) >= 0 and float(self.height) >= 0 and \
-                float(self.xy[0]) and float(self.xy[1])
-            return True
-        except ValueError:
-            return False
-
-    def set_invalid(self):
-        self.bbox = BBox()
-
-    def calculate_bbox(self):
-        if self.xy and self.width and self.height and not self.bbox.is_valid():
-            self.bbox.set_xrange([self.xy[0], self.xy[0] + self.width])
-            self.bbox.set_yrange([self.xy[1], self.xy[1] + self.height])
-
-    def to_xml(self):
-        if self.is_valid():
-            pass
-
-    # @classmethod
-    # def create_svg(cls, bbox):
-    #     """
-    #     create SVGRect from bbox
-    #     :param bbox:
-    #     :return: svg_rect or None
-    #     """
-    #     if bbox is None:
-    #         return None
-    #     SVGRect()
-
-
-class SVGCircle(AbsSVG):
-    TAG = "circle"
-
-    RAD = "r"
-
-    def __init__(self, xy=None, rad=None):
-        super().__init__(self.TAG)
-        self.bbox = BBox()
-        self.rad = None
-        self.xy = None
-        self.set_xy(xy)
-        self.set_rad(rad)
-
-    def calculate_bbox(self):
-        try:
-            xrange = [self.xy[0] - self.rad, self.xy[0] + self.rad]
-            yrange = [self.xy[1] - self.rad, self.xy[1] + self.rad]
-        except ValueError:
-            xrange = None
-            yrange = None
-        self.bbox = BBox.create_from_ranges(xrange, yrange)
-
-    def is_valid(self):
-        try:
-            float(self.rad) >= 0 and float(self.xy[0]) and float(self.xy[1])
-            return True
-        except Exception:
-            return False
-
-    def set_rad(self, rad):
-        if rad is not None:
-            self.rad = rad
-            self.set_attribute(self.RAD, str(rad))
-
-    def set_xy(self, xy):
-        if xy is not None:
-            self.xy = xy
-            self.set_attribute("cx", str(xy[0]))
-            self.set_attribute("cy", str(xy[1]))
-
-
-class SVGPath(AbsSVG):
-    TAG = "path"
-
-    def __init__(self):
-        super().__init__(self.TAG)
-        self.box = BBox()
-
-    def calculate_bbox(self):
-        raise NotImplementedError("code not written, BBox should recurse through descendants")
-
-
-class SVGLine(AbsSVG):
-    TAG = "line"
-    X1 = "x1"
-    X2 = "x2"
-    Y1 = "y1"
-    Y2 = "y2"
-
-    def __init__(self, xy1, xy2):
-        super().__init__(self.TAG)
-        self.box = BBox()
-        self.set_xy1(xy1)
-        self.set_xy2(xy2)
-        set_default_styles(self)
-
-    def __repr__(self):
-        """[x1, y1], [x2, y2]"""
-        s = f"[{self.get_float_attribute(self, self.X1)}, {self.get_float_attribute(self, self.Y1)}], " \
-            f"[{self.get_float_attribute(self, self.Y1)}, {self.get_float_attribute(self, self.Y2)}]"
-        return s
-
-    def set_xy1(self, xy):
-        if xy is not None:
-            self.set_attribute(self.X1, str(xy[0]))
-            self.set_attribute(self.Y1, str(xy[1]))
-
-    def set_xy2(self, xy):
-        if xy is not None:
-            self.set_attribute(self.X2, str(xy[0]))
-            self.set_attribute(self.Y2, str(xy[1]))
-
-    def calculate_bbox(self):
-        raise NotImplementedError("code not written")
-
-
-class SVGText(AbsSVG):
-    TAG = "text"
-
-    X = "x"
-    Y = "y"
-
-    def __init__(self, xy=None, text=None):
-        super().__init__(self.TAG)
-        self.box = BBox()
-        self.xy = xy
-        self.text = text
-
-    def set_xy(self, xy):
-        if xy is not None:
-            self.set_attribute(self.X, str(xy[0]))
-            self.set_attribute(self.Y, str(xy[1]))
-
-    def calculate_bbox(self):
-        raise NotImplementedError("code not written")
-
-
-class SVGTitle(AbsSVG):
-    TAG = "title"
-
-    TITLE = "title"
-
-    def __init__(self, titl=None):
-        super().__init__(self.TAG)
-        self.set_title(titl)
-
-    def calculate_bbox(self):
-        return None
-
-    def set_title(self, titl):
-        if titl is not None:
-            self.set_attribute(self.TITLE, titl)
-
-    def get_bounding_box(self):
-        """overrides"""
-        foo = None
-        return foo
-
-
-class SVGTextBox(SVGG):
-    """This will contain text and a shape (for drawing)
-    Not an SVG primitive"""
-
-    def __init__(self, svg_text=None, svg_shape=None):
-        super().__init__()
-        self.box = BBox()
-        self.svgg = SVGG()
-        self.shape = None
-        self.set_text(svg_text)
-        self.set_shape(svg_shape)
-
-    def set_text(self, svg_text):
-        """
-        Need to check for uniqueness
-        :param svg_text:
-        :return:
-        """
-        if svg_text is not None:
-            assert type(svg_text) is SVGText
-            self.svgg.append(svg_text)
-
-    def set_shape(self, shape):
-        if type(shape) is SVGRect or type(shape) is SVGCircle:
-            self.shape = shape
-
-    def calculate_bbox(self):
-        raise NotImplementedError("code not written, ")
-
-
-class SVGDefs(AbsSVG):
-    TAG = "defs"
-
-    def __init__(self):
-        super().__init__(self.TAG)
-
-
-class SVGMarker(AbsSVG):
-    """
- <defs>
-    <marker id="arrowhead" markerWidth="10" markerHeight="7"
-    refX="0" refY="3.5" orient="auto">
-      <polygon points="0 0, 10 3.5, 0 7" />
-    </marker>
-  </defs>
-      """
-    TAG = "marker"
-
-    ID = "id"
-    MARKER_WIDTH = "markerWidth"
-    MARKER_HEIGHT = "markerHeight"
-    MARKER_END = "marker-end"
-    MARKER_START = "marker-start"
-    REFX = "refX"
-    REFY = "refY"
-    ORIENT = "orient"
-    AUTO = "auto"
-    ORIENT_VALUES = [AUTO]
-
-    def __init__(self, id, marker_width=None, marker_height=None, refx=None, refy=None, orient=AUTO):
-        super().__init__(self.TAG)
-        self.id = None
-        self.set_id(id)
-        self.set_marker_width(marker_width)
-        self.set_marker_height(marker_height)
-        self.set_refx(refx)
-        self.set_refy(refy)
-        self.set_orient(orient)
-
-    def set_id(self, value):
-        assert value is not None
-        self.id = value
-        self.set_attribute(self.ID, value)
-
-    def set_marker_width(self, value):
-        if value is not None:
-            self.set_float_attribute(self.MARKER_WIDTH, float(value))
-
-    def set_marker_height(self, value):
-        if value is not None:
-            self.set_float_attribute(self.MARKER_HEIGHT, float(value))
-
-    def set_refx(self, value):
-        if value is not None:
-            self.set_float_attribute(self.REFX, float(value))
-
-    def set_refy(self, value):
-        if value is not None:
-            self.set_float_attribute(self.REFY, float(value))
-
-    def set_orient(self, value):
-        if value in self.ORIENT_VALUES:
-            self.set_attribute(self.ORIENT, value)
-
-    def set_marker_start(self, svg_element):
-        svg_element.set_attribute(self.MARKER_START, "#" + self.id)
-
-
-class SVGPolygon(AbsSVG):
-    TAG = "polygon"
-
-    POINTS = "points"
-
-    def __init__(self, points=None):
-        super().__init__(self.TAG)
-        self.set_points(points)
-        set_default_styles(self)
-
-    # need to check/convert to float_array
-    def set_points(self, points):
-        if points is not None:
-            self.set_attribute(self.POINTS, points)
-
-
-class SVGArrow(SVGG):
-    X1 = "x1"
-    X2 = "x2"
-    Y1 = "y1"
-    Y2 = "y2"
-
-    MIN_WIDTH = 20
-
-    def __init__(self, head_xy=None, tail_xy=None):
-        """
-        SVGArrow requires the <svg> element to have a <defs> elememt
-
-        Typically:
-        svg = SVGSVG()
-        SVGArrow.create_arrowhead(svg)
-        g = SVGG()
-        svg.append(g)
-        for i, island in enumerate(big_islands):
-            ami_arrow = AmiArrow.create_simple_arrow(island)
-            g.append(ami_arrow.get_svg())
-
-        :param head_xy:
-        :param tail_xy:
-        """
-        super().__init__()
-        self.tail_xy = tail_xy
-        self.head_xy = head_xy
-        self.line = None
-        if self.head_xy and self.tail_xy:
-            self.line = SVGLine(xy1=tail_xy, xy2=head_xy)
-            self.line.set_attribute(SVGMarker.MARKER_END, "url(#arrowhead)")
-            self.append(self.line)
-
-    def __str__(self):
-        if self.tail_xy is None or self.head_xy is None:
-            return "None"
-        s0 = "tail: " + str(self.tail_xy[0]) + "," + str(self.tail_xy[1])  \
-            if self.tail_xy[0] is not None and self.tail_xy[1] is not None else "None"
-        s1 = " head: " + str(self.head_xy[0]) + "," + str(self.head_xy[1]) \
-            if self.head_xy[0] is not None and self.head_xy[1] is not None else "None"
-        return s0 + s1
-
-    def set_head_xy(self, xy):
-        self.head_xy = xy
-
-    def set_tail_xy(self, xy):
-        self.tail_xy = xy
-
-    @classmethod
-    def create_arrowhead(cls, svgsvg):
-        svgsvg.add_arrowhead()
-
-    def calculate_bbox(self):
-        """
-        assume that width has a minimum value if aligned with axes
-        :return:
-        """
-        bbox = BBox.create_from_corners(self.tail_xy, self.head_xy)
-
-
-    @classmethod
-    def create_from_svgg(cls, svgg):
-        """
-        expects an <svg:g role="arrow"><svg:line x1,y1 x2,y2></svg:g> object
-        x1,y1 is the tail and x2,y2 is the head
-        svg:marker is not required
-        Example:
-        <svg:g role="arrow">
-            <svg:line x1="241" y1="669" x2="243" y2="757" ... />
-        </svg:g>
-        maybe mainly for testing
-        :param svgg:
-        :return: SVGArraw object or None if svgg is None or Errors
-        """
-        if svgg is None:
-            return None
-        assert type(svgg) is lxml.etree._Element, "svgg must be namespaced element"
-        assert svgg.get("role") == "arrow", "must have role='arrow'"
-        xpath = f"{{{SVG_NS}}}line"
-        svg_line = ns_xpath(svgg, xpath=xpath)
-        svg_arrow = SVGArrow()
-        x1 = cls.get_float_attribute(svg_line, cls.X1)
-        x2 = cls.get_float_attribute(svg_line, cls.X2)
-        y1 = cls.get_float_attribute(svg_line, cls.Y1)
-        y2 = cls.get_float_attribute(svg_line, cls.Y2)
-        svg_arrow.tail_xy = [x1, y1]
-        svg_arrow.head_xy = [x2, y2]
-
-        return svg_arrow
-
-class SVGUtil:
-    """mainly classmethods"""
-    @classmethod
-    def write_to_path(cls, svg_object, path, pretty_print=True):
-        """write to path"""
-        with open(str(Path(path)), "w") as f:
-            f.write(svg_object.tostring(pretty_print=pretty_print))
-
-
-def set_default_styles(svg_element):
-    """
-    sets fill, stroke, stroke-width to default so that at least the elements
-    is visibly displayed
-    :param svg_element:
-    :return:
-    """
-    assert svg_element is not None
-    svg_element.set_fill(NONE)
-    svg_element.set_stroke(RED)
-    svg_element.set_stroke_width("1")
-
-
-def ns_xpath(element, xpath, xpath_type="Clark", unpack_1_elem_lists=True):
-    """
-    searches an element with possibly namespaced Xpath
-    here we use SVG namespace as an f-string substitution
-    Two styles:
-    * local_name_xpath = f"*[local-name()='defs' and namespace-uri()='{SVG_NS}']"
-    * clark_xpath = f"{{{SVG_NS}}}defs"
-
-    They use different tools (xpath and ETXPath) and different syntaxes
-
-    :param element: element to search
-    :param xpath: xpath expression
-    :param xpath_type: "Clark" or "local"
-    :param unpack_1_elem_lists: convert returned list [Foo] to Foo (default True)
-    :return: result of xpath (bool or number or string, or list)
-    """
-    assert element is not None
-    assert xpath is not None
-    assert xpath_type is not None
-
-    return_val = None
-    if xpath_type is None:
-        raise ValueError("xpath_type cannot be None")
-    elif xpath_type.lower() == "local":
-        try:
-            return_val = element.xpath(xpath)
-        except XPathSyntaxError as e:
-            logger.error(f"XPATH error {e} in {xpath}")
-            raise e
-    elif xpath_type.lower() == "clark":
-        try:
-            return_val = lxml.etree.ETXPath(xpath)(element)
-        except XPathSyntaxError as e:
-            logger.error(f"XPATH error {e} in {xpath}")
-            raise e
-    else:
-        logger.error(f"xpath_type must be Clark or local")
-
-    # unpack 1-element lists?
-    if unpack_1_elem_lists and type(return_val) is list and len(return_val) == 1:
-        return_val = return_val[0]
-    return return_val
-
-
-class XMLNamespaces:
-    SVG_NS = "http://www.w3.org/2000/svg"
-    XLINK_NS = "http://www.w3.org/1999/xlink"
-
-
-# --- utils ---
-
-def is_valid_xy(xy):
-    try:
-        len(xy) == 2 and float(xy[0]) and float(xy[1])
-        return True
-    except Exception:
-        return False
diff --git a/pyimage/tesseract_hocr.py b/pyimage/tesseract_hocr.py
deleted file mode 100644
index ae16541..0000000
--- a/pyimage/tesseract_hocr.py
+++ /dev/null
@@ -1,332 +0,0 @@
-import pytesseract
-from pathlib import Path
-from lxml import etree as et
-import numpy as np
-from lxml.etree import Element, QName
-from lxml import etree
-
-from ..pyimage.svg import XMLNamespaces
-
-try:
-    from pyami.py4ami import wikimedia
-except ImportError:
-    print("Cannot load pyami wikimedia")
-
-"""
-This file is to play with the output of hocr
-"""
-A_BBOX = "bbox"
-A_FILL = "fill"
-A_FONT_SIZE = "font-size"
-A_FONT_FAMILY = "font-family"
-A_HEIGHT = "height"
-A_STROKE = "stroke"
-A_STROKE_WIDTH = "stroke-width"
-A_TITLE = "title"
-A_WIDTH = "width"
-A_XLINK = 'xlink'
-A_X = "x"
-A_Y = "y"
-
-E_G = 'g'
-E_RECT = 'rect'
-E_SVG = 'svg'
-E_TEXT = "text"
-
-XHTML_OCRX_WORD = "//{http://www.w3.org/1999/xhtml}span[@class='ocrx_word']"
-
-TEXT_BOX_STROKE = "blue"
-TEXT_BOX_FONT = "sans-serif"
-
-
-class TesseractOCR:
-    """Wrapper for pytesseract + addtional methods for phrase detection"""
-    def __init__(self) -> None:
-        pass
-
-    @classmethod
-    def pretty_print_hocr(cls, hocr_element):
-        """Prints html string to console with proper indentation
-        input: object of lxml.etree.Element class
-        returns: None
-        """
-        print(et.tostring(hocr_element, pretty_print=True).decode("utf-8"))
-
-    @classmethod
-    def hocr_from_image_path(cls, path):
-        """Runs tesseract hocr on the given image
-        :param: Path
-        :returns: hocr string
-        """
-        # pytesseract only seems to accept string for image path
-        if path is not str:
-            path = str(path)
-
-        hocr = pytesseract.image_to_pdf_or_hocr(path, extension='hocr', config='11')
-        # TODO what is this doing?
-        cls.parse_hocr_string(hocr)
-        return hocr
-
-    @classmethod
-    def read_hocr_file(cls, path):
-        """Reads hocr html file and return root of hocr
-        :input: path
-        :returns: lxml.etree.Element object
-        """
-        # open file in bianry mode to prevent python from implicitly decoding
-        # the bytes in the file as unicode, lxml parser does not like unicode
-        with open(path, 'rb') as file:
-            hocr = file.read()
-        root = cls.parse_hocr_string(hocr)
-        return root
-
-    @classmethod
-    def parse_hocr_string(cls, hocr_string):
-        """Parses hocr output in string format as a tree using lxml
-        :input: hocr html as string
-        :returns: root of hocr as an object of lxml.etree.Element class
-        """
-        parser = et.HTMLParser()
-        root = et.HTML(hocr_string, parser)
-        return root
-
-    @classmethod
-    def draw_bbox_around_words(cls, image, bbox_coordinates):
-        """Given bbox coordinates, draw bounding box on the image
-        :param image: image (will be modified, is this OK?)
-        :param bbox_coordinates: as x1, y1, x2, y2
-        :returns: modified image
-        """
-        max_row = image.shape[0]
-        max_column = image.shape[1]
-        for bbox in bbox_coordinates:
-            for column in range(bbox[0], bbox[2]+1):
-                for row in range(bbox[1], bbox[3]+1):
-                    if row >= max_row or column >= max_column:
-                        continue
-                    if row == bbox[1] or row == bbox[3]:
-                        image[row][column] = 0
-                    if column == bbox[0] or column == bbox[2]:
-                        image[row][column] = 0
-        return image
-
-    @classmethod
-    def extract_bbox_from_hocr(cls, hocr_element):
-        """to extract bbox coordinates from html
-        :param hocr_element: lxml.etree.Element object
-        :returns: tuple of (list of bboxes, list of words)
-        numpy array has the format: [x1, y1, x2, y2]
-        where values corresponding to the bounding box are
-        x1,y1 ------
-        |          |
-        |          |
-        |          |
-        --------x2,y2    
-        """
-        bboxes = []
-        words = []
-
-        # Each of the bbox coordinates are in span
-        # iterdescendants('span') iterates over all the span elements in the tree
-        for child in hocr_element.iterdescendants('span'):
-            # the class of span for bounding box around each word is 'ocrx_word'
-            if child.attrib['class'] == 'ocrx_word':
-                # coordinates for bbox has the following format: 'bbox 333 74 471 102; x_wconf 76'
-                # we are only interested in the coordinates, so we split at ; and append the first part
-                bboxes.append(child.attrib['title'].split(';')[0])
-                words.append(child.text)
-
-        # now we have a list with string elenments like 'bbox 333 74 471 102'
-        # first we convert each string into a list, and then remove 'bbox', leaving just the coordinate
-        bboxes = [bbox.split()[1:] for bbox in bboxes]
-
-        # finally we convert the coordinates from string to integer
-        bboxes = [[int(coordinate) for coordinate in bbox] for bbox in bboxes]
-
-        # and return a numpy array from the generated list
-        bboxes = np.array(bboxes)
-        return bboxes, words
-
-    @classmethod
-    def find_phrases(cls, hocr_element, word_separation=20, y_tolerance=10):
-        """
-        finds phrases and their bboxes from HOCR output
-        adjusts the classification from Tesseract which is often fragile
-        :param hocr_element: output from Tesseract
-        :param word_separation: maximum? separation (in pixels) of words to create phrases
-        :param y_tolerance: maximum tolerance (in pixels) of words on same line
-        :return: tuple (list of phrases, list of boxes)
-        """
-        phrases = []
-        bbboxes = []
-        bboxes, words = cls.extract_bbox_from_hocr(hocr_element)
-        for i in range(len(words)):
-            phrase = [words[i]]
-            last_bbox = bboxes[i]
-            for j in range(i+1, len(words)):
-                # check if words are on the same line
-                # if words on a different line, break loop
-                if abs(bboxes[j][1] - bboxes[i][1]) > y_tolerance or abs(bboxes[j][3] - bboxes[i][3]) > y_tolerance:
-                    break
-                # If the separation is small enough, add the word to the phrase
-                if (bboxes[j][0] - last_bbox[2]) < word_separation:
-                    phrase.append(words[j])
-                    last_bbox = bboxes[j]
-                else:
-                    break
-            phrase = " ".join(phrase)
-            if phrase == ' ':
-                # if phrase is empty do not add to the list of phrases
-                continue
-            elif not phrases:
-                # if array is empty add element
-                phrases.append(phrase)
-                bbboxes.append([bboxes[i][0], bboxes[i][1], last_bbox[2], last_bbox[3]])
-            elif not phrases[-1].endswith(phrase):
-                # only add phrase if the last phrase added does not end with current phrase
-                phrases.append(phrase)
-                bbboxes.append([bboxes[i][0], bboxes[i][1], last_bbox[2], last_bbox[3]])
-        return phrases, bbboxes
-
-    @classmethod
-    def wikidata_lookup(cls, phrases):
-        """
-        looks up phrases in wikidata.
-        Requires Internet, May take a long time
-        :param phrases: to lookup
-        :return: tuple (list of qitem ids, list of descriptions
-        """
-        lookup = wikimedia.WikidataLookup()
-        qitems, desc = lookup.lookup_items(phrases)
-        return qitems, desc
-
-    @classmethod
-    def output_phrases_to_file(cls, phrase_list, output_file):
-        """
-        :param phrase_list: list of phrases_file
-        :param output file: full output filename
-        :returns: output file path
-        """
-        # TODO this doesn't look the right output directory
-        phrases_file = Path(Path(__file__).parent.parent, f"temp/{output_file}")
-        with open(phrases_file, 'w') as f:
-            for item in phrase_list:
-                f.write("%s\n" % item)
-        return phrases_file
-
-    @classmethod
-    def extract_bbox_from_image(cls, path):
-        """Given an image path, returns the coordinates for bboxes around the words
-        :input: path
-        :returns: numpy array
-        """
-        hocr = cls.hocr_from_image_path(path)
-        root = cls.parse_hocr_string(hocr)
-        bbox_for_words, words = cls.extract_bbox_from_hocr(root)
-        
-        return bbox_for_words, words
-
-    @classmethod
-    def parse_hocr_title(cls, title):
-        """
-         title="bbox 336 76 1217 111; baseline -0.006 -9; x_size 28; x_descenders 6; x_ascenders 7"
-
-        main function is to re-order the x, y values in bbox
-        :param title:
-        :return: dictionary with same kws
-        """
-        if title is None:
-            return None
-        parts = title.split("; ")
-        title_dict = {}
-        for part in parts:
-            vals = part.split()
-            kw = vals[0]
-            if kw == A_BBOX:
-                val = ((vals[1], vals[3]), (vals[2], vals[4]))
-            else:
-                val = vals[1:]
-            title_dict[kw] = val
-        return title_dict
-
-    @classmethod
-    def create_svg_from_hocr(cls, hocr_html):
-        """
-
-        :param hocr_html: input HOCR
-        :return:
-        """
-        html = etree.parse(hocr_html)
-        word_spans = html.findall(XHTML_OCRX_WORD)
-        svg = Element(QName(XMLNamespaces.SVG_NS, E_SVG), nsmap={
-            E_SVG: XMLNamespaces.SVG_NS,
-            A_XLINK: XMLNamespaces.XLINK_NS,
-        })
-        for word_span in word_spans:
-            title = word_span.attrib[A_TITLE]
-            title_dict = cls.parse_hocr_title_HOCR(title)
-            bbox = title_dict[A_BBOX]
-            text = word_span.text
-            g = cls.create_svg_text_box_from_hocr(bbox, text)
-            svg.append(g)
-        bb = etree.tostring(svg, encoding='utf-8', method='xml')
-        s = bb.decode("utf-8")
-        path_svg = Path(Path(__file__).parent.parent, "temp", "textbox.svg")
-        with open(path_svg, "w", encoding="UTF-8") as f:
-            f.write(s)
-            print(f"Wrote textboxes to {path_svg}")
-
-    @classmethod
-    def create_svg_text_box_from_hocr(cls, bbox, txt):
-        """
-        creates a 'text-box' of form
-        <g>
-          <rect x="x" y="y" width="w" height="h"/>
-          <text x="x" y="y" >string</text>
-        </g>
-        Assumes that bbox and txt have been created consistently,
-
-        :param bbox: nested 2-arrays [[x0, x1], [y0,y1]]
-        :param txt: string of constant y1 (since text is upward) starting at x1
-        :return: <g> element with rect and text children
-        """
-        g = Element(QName(XMLNamespaces.SVG_NS, E_G))
-        height = int(bbox[1][1]) - int(bbox[1][0])
-
-        rect = cls.create_svg_rect_from_bbox(bbox, height)
-        g.append(rect)
-
-        text = Element(QName(XMLNamespaces.SVG_NS, E_TEXT))
-        text.attrib[A_X] = bbox[0][0]
-        text.attrib[A_Y] = str(int(bbox[1][0]) + height)
-        text.attrib[A_FONT_SIZE] = str(0.9 * height)
-        text.attrib[A_STROKE] = TEXT_BOX_STROKE
-        text.attrib[A_FONT_FAMILY] = TEXT_BOX_FONT
-
-        text.text = txt
-
-        g.append(text)
-
-        return g
-
-    @classmethod
-    def create_svg_rect_from_bbox(cls, bbox, height=None):
-        """
-        Create from bounding box but override height if None
-
-        :param bbox:
-        :param height:
-        :return: svg:rect object
-        """
-        assert len(bbox) == 2
-        assert len(bbox[0]) == 2
-        assert len(bbox[1]) == 2
-        rect = Element(QName(XMLNamespaces.SVG_NS, E_RECT))
-        rect.attrib[A_X] = str(bbox[0][0])
-        rect.attrib[A_WIDTH] = str(int(bbox[0][1]) - int(bbox[0][0]))
-        rect.attrib[A_Y] = str(int(bbox[1][0]))  # kludge for offset of inverted text
-        rect.attrib[A_HEIGHT] = str(height) if height is not None else str(int(bbox[1][1]) - int(bbox[1][0]))
-        rect.attrib[A_STROKE_WIDTH] = "1.0"
-        rect.attrib[A_STROKE] = "red"
-        rect.attrib[A_FILL] = "none"
-        return rect
diff --git a/pyimage/tesseract_new.py b/pyimage/tesseract_new.py
deleted file mode 100644
index 50ed52a..0000000
--- a/pyimage/tesseract_new.py
+++ /dev/null
@@ -1,84 +0,0 @@
-import numpy as np
-from pathlib import Path
-from matplotlib.image import imsave
-from pyamiimage.test.resources import Resources
-from pyamiimage.pyimage.ami_ocr import AmiOCR
-from pyamiimage.pyimage.ami_image import AmiImage
-from matplotlib import pyplot as plt
-from skimage import io
-# tess = Resources.TESSERACT1
-# tess = Resources.TESSERACT_BENG
-tess = Resources.BIOSYNTH2
-# tess = Resources.TESSERACT_GER2
-# tess = Resources.TESSERACT_ITA
-tess_ocr = AmiOCR(tess)
-tess_img = io.imread(tess)
-
-
-
-# words_with_bounding_boxes = tess_ocr.plot_bboxes_on_image(tess_img, words)
-# io.imshow(words_with_bounding_boxes)
-# io.show()
-
-# phrases = tess_ocr.get_phrases()
-# phrases_with_bounding_boxes = tess_ocr.plot_bboxes_on_image(tess_img, phrases)
-
-# for phrase in phrases:
-#     print(phrase)
-
-# io.imshow(phrases_with_bounding_boxes)
-# io.show()
-
-
-tess_img_gray = AmiImage.create_grayscale_from_image(tess_img)
-tess_img_bin = AmiImage.create_white_binary_from_image(tess_img_gray)
-tess_ocr = AmiOCR(image=tess_img_bin)
-# io.imshow(tess_img_bin)
-# io.show()
-
-words = tess_ocr.get_words()
-words_with_baseline  = tess_ocr.find_baseline(tess_ocr.hocr)
-for word in words:
-    print(word)
-
-# box_bin = AmiOCR.plot_bboxes_on_image(tess_img_bin, words)
-# io.imshow(box_bin)
-# io.show()
-
-patches = AmiOCR.bounding_box_patches(tess_img_bin, words)
-TESSERACT_TEMP_PATH = Path(Path(__file__).parent.parent, "temp/tesseract/")
-AmiImage.write_image_group(TESSERACT_TEMP_PATH, patches, filename="black_on_white", mkdir=False)
-
-def patches_pixel_stats(patches, signal_val, axis=1):
-    """sum of signal values along a axis in an array"""
-    signal = []
-    for patch in patches:
-        signal.append(np.count_nonzero(patch==signal_val, axis=axis))
-    signal = [np.array(item) for item in signal]
-    return signal
-
-signal = patches_pixel_stats(patches, signal_val=255, axis=1)
-
-row = [np.arange(0, item.shape[0]) for item in signal]
-import matplotlib.pyplot as plt
-
-for idx in range(36, 48):
-    im = plt.imread(Path(TESSERACT_TEMP_PATH, f"black_on_white{idx}.png"))
-    # implot = plt.imshow(im)
-    import scipy.ndimage as ndimage
-
-    angle = 90 # in degrees
-
-    new_data = ndimage.rotate(im, angle, reshape=True)
-
-    plt.imshow(new_data)
-
-    plt.plot(row[idx], signal[idx], color='red')
-
-    plt.show()
-# for x, y in zip(row, signal):
-#     plt.plot(x, y)
-#     plt.show()
-
-# AmiOCR.pretty_print_hocr(tess_ocr.hocr)
-# print(words_with_baseline[0].baseline)
diff --git a/run_pyamiimage.py b/run_pyamiimage.py
new file mode 100644
index 0000000..df9b9a1
--- /dev/null
+++ b/run_pyamiimage.py
@@ -0,0 +1,9 @@
+from pyamiimage.commandline import Pyamiimage
+
+def main():
+    """Runs the CLI"""
+    pyamiimage = Pyamiimage()
+    pyamiimage.handlecli()
+
+if __name__ == "__main__":
+    main()
\ No newline at end of file
diff --git a/setup.py b/setup.py
index c5334bc..fc4c8c5 100644
--- a/setup.py
+++ b/setup.py
@@ -17,19 +17,19 @@
 setup(
     name='pyamiimage',
     url='https://github.com/petermr/pyamiimage',
-    version='0.0.8',
+    version='0.0.12',
     description='Image analysis for words and graphics.',
     long_description_content_type='text/markdown',
     long_description=readme,
-    author="Peter Murray-Rust, Anubhab Chakraborty",
+    author="Peter Murray-Rust, Anuv Chakraborty",
     author_email='petermurrayrust@googlemail.com',
     license='Apache2',
     install_requires=required,
     include_package_data=True,
     zip_safe=False,
     keywords='text and image mining',
-    package_dir={'': 'src'},
-    packages=setuptools.find_packages(where="src"),
+    package_dir={'pyamiimage': 'pyamiimage'},
+    packages=['pyamiimage'],
     classifiers=[
         'Development Status :: 2 - Pre-Alpha',
         'Intended Audience :: Developers',
@@ -42,7 +42,7 @@
     ],
     entry_points={
         'console_scripts': [
-            'pyamiimage=src.pyamiimage.pyamiimage:main',
+            'pyamiimage=pyamiimage.commandline:main',
         ],
     },
 )
\ No newline at end of file
diff --git a/src/pyamiimage/__init__.py b/src/pyamiimage/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/pyamiimage/ami_arrow.py b/src/pyamiimage/ami_arrow.py
deleted file mode 100644
index a8a4a7b..0000000
--- a/src/pyamiimage/ami_arrow.py
+++ /dev/null
@@ -1,658 +0,0 @@
-import logging
-import math
-from lxml import etree
-from lxml.builder import ElementMaker
-# local
-from ..pyimage.ami_graph_all import AmiNode, AmiEdge
-from ..pyimage.ami_util import AmiUtil
-from ..pyimage.svg import SVGArrow, ns_xpath, SVG_NS, GPML_NS
-from ..pyimage.bbox import BBox
-
-logger = logging.getLogger(__name__)
-
-
-class AmiArrow:
-    """
-    Holds the graphics primitives and the semantics of arrows
-    contains an SVGArrow (which conatins basic drawing coords tail_xy and head_xy) and additional fields
-    """
-    HEAD_WOBBLE = 0.8 * math.pi  # allowed deviation from straightness between shaft and head
-    BARB_ANGLE_DIFF = 0.1 * math.pi  # allowed asymmetry in barb angles
-    MAX_BARB_TO_TAIL_ANGLE = 0.9 * math.pi / 2  # maximum angle of barbs to tail
-
-    def __init__(self, ami_island=None):
-        """
-        SVG_Arrow may be created from different sources
-        :param ami_island:
-        """
-        self.svg_arrow = SVGArrow()
-
-        self.ami_island = ami_island
-        # nodes in island
-        self.hcentre_id = None  # centre of head (often a 4-branched node in skeleton graph
-        self.head_id = None  # extreme point (end of SVGLine)
-        self.tail_id = None  # extreme point
-        self.barb_ids = None  # points on side (connected to hcentre); 0, 1 or 2
-        # self.point_xy = None
-        # self.tail_xy = None
-
-    def __str__(self):
-        """
-        can't get this working for None
-        :return:
-        """
-        s = f"tail {self.tail_id} - head {self.head_id} > point {self.hcentre_id} barbs {self.barb_ids}"
-        if self.svg_arrow is None:
-            s += "None"
-        else:
-            s1 = str(self.svg_arrow)
-            s += " " + s1
-        return s
-
-    def set_tail_xy(self, yx):
-        tail_xy = AmiUtil.swap_yx_to_xy(yx)
-        self.svg_arrow.set_tail_xy(tail_xy)
-
-    def set_head_xy(self, yx):
-        head_xy = AmiUtil.swap_yx_to_xy(yx)
-        self.svg_arrow.set_head_xy(head_xy)
-
-    @classmethod
-    def create_simple_arrow(cls, island):
-        """
-        create simple arrow from networkX island
-        one head, one tail
-        :param island:
-        :return: AmiArrow or none
-        """
-        if not 5 >= len(island.node_ids) >= 4:
-            logger.warning(f"cannot create simple arrow from {island.node_ids}")
-            return None
-        node_dict = island.create_node_degree_dict()
-        logger.debug(f"\nnode dict {node_dict}")
-        neighbour_count = len(island.node_ids) - 1
-        try:
-            central_node_id = node_dict[neighbour_count][0]
-            logger.debug(f"central node {central_node_id}")
-        except Exception:
-            return None
-
-        edge_dict = island.create_edge_property_dikt(central_node_id)
-        logger.debug(f"edge_dict {edge_dict}")
-
-        longest_dict = cls._find_dict_with_longest_edge(edge_dict)
-        longest_edge = island.ami_graph.get_or_create_ami_edge_from_ids(central_node_id, longest_dict[AmiNode.REMOTE],
-                                                                        branch_id=0)
-        logger.debug(f"longest edge {longest_edge}")
-        logger.debug(f"longest dict {longest_dict}")
-
-        ami_arrow = AmiArrow(island)
-        svg_arrow = ami_arrow.svg_arrow
-        ami_arrow.head_id = central_node_id
-        ami_arrow.tail_id = longest_dict[AmiNode.REMOTE]
-
-        # find short lines
-        short_lines = [value for value in edge_dict.values() if value != longest_dict]
-        logger.debug(f"short lines {short_lines} {len(short_lines)}")
-
-        if len(short_lines) == 3:  # 5-node arrow - normally from thinned solid triangle
-
-            # find straight on
-            barbs1 = []
-            arrow_point_line = None
-            for line in short_lines:
-                line_tuple = AmiEdge.create_normalized_edge_id_tuple(central_node_id, line[AmiNode.REMOTE], 0)  # assume only one branch
-                angle = island.ami_graph.get_interedge_tuple_angle(longest_edge.get_tuple(), line_tuple)
-                assert angle is not None
-                if abs(angle) > AmiArrow.HEAD_WOBBLE:
-                    if arrow_point_line is not None:
-                        raise ValueError(f"competing lines for head line {longest_edge} to {short_lines}")
-                    arrow_point_line = line
-                else:
-                    barbs1.append(line)
-            if arrow_point_line is None:
-                raise ValueError(f"cannot find point line {longest_edge} to {short_lines}")
-            ami_arrow.hcentre_id = arrow_point_line[AmiNode.REMOTE]
-            if ami_arrow.hcentre_id is None:
-                raise ValueError(f"cannot find point {arrow_point_line} , {central_node_id}")
-        else:  # 4 node arrow
-            barbs1 = short_lines
-            ami_arrow.hcentre_id = ami_arrow.head_id
-        barbs = barbs1
-        svg_arrow.set_head_xy(AmiNode.get_xy_for_node_id(island.ami_graph.nx_graph, ami_arrow.hcentre_id))
-        svg_arrow.set_tail_xy(AmiNode.get_xy_for_node_id(island.ami_graph.nx_graph, ami_arrow.tail_id))
-        print(f"nodes head {svg_arrow.head_xy} tail {svg_arrow.tail_xy}")
-        logger.debug(f"longest {longest_edge} point {ami_arrow.hcentre_id} barbs {barbs}")
-        if len(barbs) != 2:
-            raise ValueError(f" require exactly 2 barbs on arrow {barbs}")
-        ami_arrow.barb_ids = [barb[AmiNode.REMOTE] for barb in barbs]
-
-        barb_angles = [island.ami_graph.get_angle_between_nodes(ami_arrow.tail_id, ami_arrow.head_id, barb_id) for
-                       barb_id in ami_arrow.barb_ids]
-        logger.debug(f"barb angles {barb_angles}")
-        if abs(barb_angles[0] + barb_angles[1]) > AmiArrow.BARB_ANGLE_DIFF:
-            raise ValueError(f"barb angles not symmetric {barb_angles}")
-        if abs(barb_angles[0] - barb_angles[1]) / 2 > AmiArrow.MAX_BARB_TO_TAIL_ANGLE:
-            raise ValueError(f"barb angles not acute {barb_angles}")
-
-        logger.debug(f"AMIARR {ami_arrow}")
-        return ami_arrow
-
-    @classmethod
-    def _find_dict_with_longest_edge(cls, edge_dict):
-        longest_dict = None
-        for key, value in edge_dict.items():
-            if longest_dict is None:
-                longest_dict = value
-            else:
-                if value[AmiNode.PIXLEN] > longest_dict[AmiNode.PIXLEN]:
-                    longest_dict = value
-        return longest_dict
-
-    def get_svg(self):
-        """
-        create line with arrowhead
-        :return:
-        """
-        # svg_arrow = SVGArrow(head_xy=self.point_xy, tail_xy=self.tail_xy)
-        return self.svg_arrow
-
-    def get_orient(self, deviation=10, min_length=50):
-        """
-        orientation of arrow along axes
-        PLUSX, PLUSY, MINUSX, MINUSY
-        arrow must be horizontal (abs(x1-x2) < deviatiom), or vertical (abs(y1-y2) < deviation
-        :param deviation: max difference between aligned coordinates
-        :return: None if not aligned else ArrowBBox.PLUSX/PLUSY/MINUSX/MINUSY
-
-
-        """
-        if self.svg_arrow is None or self.svg_arrow.head_xy is None or self.svg_arrow.tail_xy is None:
-            return None
-        tail_xy = self.svg_arrow.tail_xy
-        head_xy = self.svg_arrow.head_xy
-        if abs(tail_xy[0] - head_xy[0]) < deviation:
-            if abs(tail_xy[1] - head_xy[1]) > min_length:
-                return ArrowBBox.PLUSY if tail_xy[1] < head_xy[1] else ArrowBBox.MINUSY
-        if abs(tail_xy[1] - head_xy[1]) < deviation:
-            if abs(tail_xy[0] - head_xy[0]) > min_length:
-                return ArrowBBox.PLUSX if tail_xy[0] < head_xy[0] else ArrowBBox.MINUSX
-        return None
-
-    @classmethod
-    def create_from_svg_arrow(cls, svg_arrow):
-        """
-        Creates an AmiArrow from an SVGArrow
-        may be missing relation to a graph.
-        At present used for development
-
-        :param svg_arrow:
-        :return:
-        """
-        if svg_arrow is None:
-            logger.warning("null SVGArrow")
-            return None
-        ami_arrow = AmiArrow()
-        ami_arrow.svg_arrow = svg_arrow
-
-        return ami_arrow
-
-    def create_bbox(self, bbox_type, width=None, length=None):
-        """
-
-        boxes:
-
-
-               +-----------+  ^
-               |   LEFT    |  |  width
-        -------+-----------+-------|
-        | BACK | ------>   | FRONT |
-        -------+-----------+-------|
-               |  RIGHT    |  --> length
-               +-----------+
-
-         LEFT, RIGHT, FRONT, Back are relative to arrow direction
-         LEFT, RIGHT have adjustable WIDTH
-         FRONT, BACK have adjustable LENGTH
-
-
-        :param bbox_type: from ArrowBBox
-        :param length: only used for FRONT, BACK
-        :param width: only used for LEFT, RIGHT
-        :return:
-        """
-        core_bbox = self.svg_arrow.get_bbox() if self.svg_arrow is not None else None
-
-        if core_bbox is None or bbox_type is None:
-            return None
-
-        if bbox_type == ArrowBBox.CORE:
-            bbox = core_bbox
-        elif bbox_type == ArrowBBox.FRONT:
-            # = core_bbox
-            pass
-        elif bbox_type == ArrowBBox.BACK:
-            # bbox = translate_and_(bbox, translate=[delta, 0], )
-            pass
-        elif bbox_type == ArrowBBox.RIGHT:
-            # bbox = translate_and_(bbox, translate=[delta, 0], )
-            pass
-        elif bbox_type == ArrowBBox.LEFT:
-            # bbox = translate_and_(bbox, translate=[delta, 0], )
-            pass
-        else:
-            logger.warning("unknown direction {}")
-
-    def make_overlap_boxes(self, length=None, arrow_width=40, len_trim=10, colors=None):
-        orient = self.get_orient()
-        head_xy = self.svg_arrow.head_xy
-        tail_xy = self.svg_arrow.tail_xy
-        # core_bbox = BBox(xy_ranges=[
-        #     [tail_xy[0], head_xy[0]] - arrow_width / 2],[]])
-        xmid = (tail_xy[0] + head_xy[0]) / 2
-        ymid = (tail_xy[1] + head_xy[1]) / 2
-        half_width = arrow_width / 2
-        # horizontal
-        if orient == ArrowBBox.PLUSX:
-            ymax = ymid + half_width
-            ymin = ymid - half_width
-            xmin = tail_xy[0]
-            xmax = head_xy[0]
-            bbox_core = BBox(xy_ranges=[[xmin, xmax], [ymin, ymax]], swap_minmax=True)
-            bbox_front = BBox(xy_ranges=[[xmax, xmax + length], [ymin, ymax]], swap_minmax=True)
-            bbox_back = BBox(xy_ranges=[[xmin - length, xmin], [ymin, ymax]], swap_minmax=True)
-            bbox_right = BBox(xy_ranges=[[xmin + len_trim, xmax - len_trim], [ymax, ymax + 2 * arrow_width]], swap_minmax=True)
-            bbox_left = BBox(xy_ranges=[[xmin + len_trim, xmax - len_trim], [ymin - 2 * arrow_width, ymin]], swap_minmax=True)
-            if colors is not None:
-                bbox_core.fill = colors["core"]
-
-        elif orient == ArrowBBox.MINUSX:
-            ymax = ymid + half_width
-            ymin = ymid - half_width
-            xmin = head_xy[0]
-            xmax = tail_xy[0]
-            bbox_core = BBox(xy_ranges=[[xmin, xmax], [ymin, ymax]], swap_minmax=True)
-            bbox_front = BBox(xy_ranges=[[xmin - length, xmin], [ymin, ymax]], swap_minmax=True)
-            bbox_back = BBox(xy_ranges=[[xmax, xmax + length], [ymin, ymax]], swap_minmax=True)
-            bbox_right = BBox(xy_ranges=[[xmin + len_trim, xmax - len_trim], [ymax, ymax + 2 * arrow_width]], swap_minmax=True)
-            bbox_left = BBox(xy_ranges=[[xmin + len_trim, xmax - len_trim], [ymin - 2 * arrow_width, ymin]], swap_minmax=True)
-        # vertical
-        elif orient == ArrowBBox.PLUSY:
-            xmax = xmid + half_width
-            xmin = xmid - half_width
-            ymin = tail_xy[1]
-            ymax = head_xy[1]
-            bbox_core = BBox(xy_ranges=[[xmin, xmax], [ymin, ymax]], swap_minmax=True)
-            bbox_front = BBox(xy_ranges=[[xmin, xmax ], [ymax, ymax + length]], swap_minmax=True)
-            bbox_back = BBox(xy_ranges=[[xmin, xmax], [ymin - length, ymax]], swap_minmax=True)
-            bbox_right = BBox(xy_ranges=[[xmax, xmax + 2 * arrow_width], [ymin + len_trim, ymax - len_trim]], swap_minmax=True)
-            bbox_left = BBox(xy_ranges=[[xmin - 2 * arrow_width, xmin], [ymin + len_trim, ymax - len_trim]], swap_minmax=True)
-        elif orient == ArrowBBox.MINUSY:
-            xmax = xmid + half_width
-            xmin = xmid - half_width
-            ymin = head_xy[1]
-            ymax = tail_xy[1]
-            bbox_core = BBox(xy_ranges=[[xmin, xmax], [ymin, ymax]], swap_minmax=True)
-            bbox_front = BBox(xy_ranges=[[xmin, xmax ], [ymin-length, ymin]], swap_minmax=True)
-            bbox_back = BBox(xy_ranges=[[xmin, xmax], [ymax, ymax + length]], swap_minmax=True)
-            bbox_right = BBox(xy_ranges=[[xmax, xmax + 2 * arrow_width], [ymin + len_trim, ymax - len_trim]], swap_minmax=True)
-            bbox_left = BBox(xy_ranges=[[xmin - 2 * arrow_width, xmin], [ymin + len_trim, ymax - len_trim]], swap_minmax=True)
-
-        bbox_core.color="blue"
-        bbox_front.color="fuchsia"
-        bbox_back.color="blue"
-        bbox_left.color="lime"
-        bbox_right.color="red"
-
-        return bbox_core, bbox_left, bbox_right, bbox_front, bbox_back
-
-class AmiNetwork:
-    ARROW = "arrow"
-    ARROWS = "arrows"
-    BBOX = "bbox"
-    DATABASE = "Database"
-    ID = "id"
-    POSITIONS = "positions"
-    TEXT = "text"
-    TEXTBOXES = "textboxes"
-    TYPE = "type"
-    UPPER_ID = "ID"
-    VALUE = "value"
-
-    def __init__(self):
-        self.arrows_text_dict = dict()
-        self.svgsvg = None
-
-    @classmethod
-    def create_from_svgsvg(cls, svgsvg):
-        """
-        process output of pixel analysis
-        :param svgsvg:
-        :return:
-        """
-        ami_network = AmiNetwork()
-        ami_network.svgsvg = svgsvg
-        return ami_network
-
-    def overlap_arrows_and_text(self):
-        arrows = ns_xpath(self.svgsvg,
-                          f"{{{SVG_NS}}}g[@role='arrows']/{{{SVG_NS}}}g[@role='arrow']")
-        text_boxes = ns_xpath(self.svgsvg, f"{{{SVG_NS}}}g[@role='texts']/{{{SVG_NS}}}g[@role='text']")
-        self.arrows_text_dict = dict()
-
-        self.arrows_dict = dict()
-        self.arrows_text_dict[self.ARROWS] = self.arrows_dict
-
-        self.textboxes_dict = dict()
-        self.arrows_text_dict[self.TEXTBOXES] = self.textboxes_dict
-        for arrow_elem in arrows:
-            arrow_id = arrow_elem.get(self.ID)
-            for position in [ArrowBBox.FRONT, ArrowBBox.BACK, ArrowBBox.RIGHT, ArrowBBox.LEFT]:
-                arrow_box_elem = ns_xpath(arrow_elem,
-                                          f"{{{SVG_NS}}}rect[@position='{position}']")
-                arrow_bbox = AmiNetwork.get_bbox(arrow_box_elem)
-                for text_box in text_boxes:
-                    self.merge_texts_and_arrows(arrow_bbox, arrow_id, position, text_box)
-
-        self.clean_overlap_in_arrows()
-        self.create_reactions()
-
-    def merge_texts_and_arrows(self, arrow_bbox, arrow_id, position, textbox):
-        """merge text and arrows using position
-        :param arrow_bbox: save bbox for arrow, indexed by position
-        :param arrow_id: ID of arrow
-        :param position: of bbox (e.g. LEFT)
-        :param textbox: save textbox coordinates
-        """
-        textbox_id = textbox.get(self.ID)
-        textbox_bbox_elem = ns_xpath(textbox, f"{{{SVG_NS}}}rect[@role='bbox']")
-        textbox_bbox = AmiNetwork.get_bbox(textbox_bbox_elem)
-        overlap = textbox_bbox.intersect(arrow_bbox)
-        if overlap.is_valid():
-            self.add_text_fields_to_dict(textbox_bbox, textbox_id, textbox)
-            self.add_arrow_fields_to_dict(arrow_bbox, arrow_id, position, textbox_id)
-
-    def add_text_fields_to_dict(self, text_bbox, textbox_id, text_element):
-        """
-
-        :param text_bbox:
-        :param textbox_id:
-        :param text_element: element containing one or more strings
-        :return:
-        """
-        text_val = AmiNetwork.get_text_val(text_element)
-        if textbox_id not in self.textboxes_dict:
-            self.textboxes_dict[textbox_id] = dict()
-        print(f"added {text_val} for {textbox_id}")
-        self.textboxes_dict[textbox_id][self.VALUE] = text_val
-        self.textboxes_dict[textbox_id][self.BBOX] = text_bbox
-        print(f"{textbox_id} gives {self.textboxes_dict}")
-        return text_val
-
-    def add_arrow_fields_to_dict(self, arrow_bbox, arrow_id, position, text_id):
-        if arrow_id not in self.arrows_dict:
-            self.arrows_dict[arrow_id] = dict()
-        self.arrows_dict[arrow_id][self.BBOX] = arrow_bbox
-        # self.arrows_dict[arrow_id][self.TYPE] = self.ARROW
-        if self.POSITIONS not in self.arrows_dict[arrow_id]:
-            self.arrows_dict[arrow_id][self.POSITIONS] = dict()
-        self.arrows_dict[arrow_id][self.POSITIONS][position] = text_id
-
-    def write_graph(self, path):
-        """
-        write graph
-        output format determined by suffix of path
-        :param path: pathname, suffix can be *.gpml, *.dot (NYI), *.svg (NYI)
-        :return: None
-        """
-        if path.suffix == ".gpml":
-            self.write_graph_gpml(path)
-        elif path.suffix == ".dot":
-            self.write_graph_dot(path)
-        elif path.suffix == ".svg":
-            self.write_graph_svg(path)
-        else:
-            logger.error(f"unsupported format {path}")
-
-    def write_graph_gpml(self, path):
-        """
-https://github.com/PathVisio/libGPML/blob/main/org.pathvisio.lib/src/main/resources/GPML2013a.xsd
-
-    <Pathway xmlns="http://pathvisio.org/GPML/2013a" Name="Foo" Version="000" Organism="T. rex">
-      <Graphics BoardWidth="484.0" BoardHeight="234.25" />
-      <DataNode TextLabel="HDL-C" GraphId="bf9b8" Type="Metabolite">
-        <Graphics CenterX="348.5" CenterY="184.25" Width="93.0" Height="35.5" ZOrder="32768" FontSize="12"
-            Valign="Middle" Color="0000ff" />
-        <Xref Database="ChEBI" ID="CHEBI:47775" />
-      </DataNode>
-      ...
-      <Interaction>
-        <Graphics ZOrder="12288" LineThickness="1.0">
-          <Point X="151.0" Y="182.625" GraphRef="eb089" RelX="1.0" RelY="0.0" />
-          <Point X="302.0" Y="184.25" GraphRef="bf9b8" RelX="-1.0" RelY="0.0" ArrowHead="mim-conversion" />
-        </Graphics>
-        <Xref Database="" ID="" />
-      </Interaction>
-      <InfoBox CenterX="0.0" CenterY="0.0" />
-      <Biopax />
-    </Pathway>
-
-            """
-        """
-        E = ElementMaker(namespace=GPML_NS)
-        BoardWidth = "484.0"
-        BoardHeight = "234.25"
-        ami_network = "AmiNetwork"
-        Version = "000"
-        organism = "T. rex"
-        gpml_root = self.make_gpml_root(BoardHeight, BoardWidth, Version, ami_network, organism)
-        X = "348.5"
-        Y = "184.25"
-        text_label = "TextLabel"
-        text_id = "t0"
-        type = "Metabolite"
-        width = "93.0"
-        label = "35.5"
-        data_node1 = self.make_text_data_node(X, Y, text_label, text_id, type, width, label, Color="0000ff")
-        data_node2 = self.make_text_data_node(X, Y, text_label, text_id, type, width, label, Color="0000ff")
-        point_X1 = "123"
-        point_Y1 = "456"
-        point_X2 = "654"
-        point_Y2 = "321"
-        idref1 = "t0"
-        idref2 = "t2"
-        interact = self.make_interaction(idref1, idref2, point_X1, point_X2, point_Y1, point_Y2)
-        gpml_root.append(data_node1)
-        gpml_root.append(data_node2)
-        gpml_root.append(interact)
-        """
-        total_bbox = self.get_total_bbox()
-        print(f"total box: {total_bbox}")
-
-        gpml_root = self.make_gpml_root(total_bbox.get_xrange()[1], total_bbox.get_yrange()[1], network_name="unknown_network", organism="unknown")
-        self.create_and_add_text_nodes(gpml_root)
-        self.create_and_add_interactions(gpml_root)
-        with open(path, "w") as f:
-            f.write(etree.tostring(gpml_root, pretty_print=True).decode(encoding="UTF-8"))
-
-    def create_and_add_interactions(self, gpml_root):
-        for arrow_id in self.arrows_dict:
-            arrow = self.arrows_dict[arrow_id]
-            bbox = self.arrows_dict[arrow_id][self.BBOX]
-            print(f"arrow: {arrow_id} {arrow}, {bbox}")
-            # arrow: a0 {'bbox': [[220, 260], [500, 540]], 'positions': {'front': 't0', 'back': 't1'}}
-            positions_dict = arrow[self.POSITIONS]
-            front_id = positions_dict[ArrowBBox.FRONT]
-            back_id = positions_dict[ArrowBBox.BACK]
-            if front_id is None and back_id is None:
-                print(f"need front and back for arrow {positions_dict}")
-            else:
-                print(f" front {front_id} back {back_id}")
-                gpml_interaction = self.make_gpml_interaction(front_id, back_id, bbox.get_xrange()[0],
-                                                              bbox.get_xrange()[1], bbox.get_yrange()[0],
-                                                              bbox.get_yrange()[1])
-                gpml_root.append(gpml_interaction)
-
-    def create_and_add_text_nodes(self, gpml_root):
-        for textbox_id in self.textboxes_dict:
-            bbox = self.textboxes_dict[textbox_id][self.BBOX]
-            text = self.textboxes_dict[textbox_id][self.VALUE]
-            Type = "Metabolite"
-            gpml_data_node = self.make_text_data_node(bbox.get_xrange()[0], bbox.get_yrange()[0], text[:10],
-                                                      textbox_id, Type,
-                                                      Width="80", Height="30")
-            # gpml_point = self.make_gpml_point(bbox.get_xrange()[0], bbox.get_yrange()[0], textbox_id)
-            gpml_root.append(gpml_data_node)
-
-    def make_gpml_interaction(self, idref1, idref2, point_X1, point_Y1, point_X2, point_Y2, ZOrder="32768", LineThickness="1.0"):
-        E = ElementMaker(namespace=GPML_NS)
-        return E("Interaction",
-                 E("Graphics",
-                   self.make_gpml_point(X=point_X1, Y=point_Y1, GraphRef=idref1),
-                   self.make_gpml_point(X=point_X2, Y=point_Y2, GraphRef=idref2, ArrowHead="mim-conversion"),
-                   ),
-                 ZOrder=ZOrder, LineThickness=LineThickness,
-                 )
-
-    def make_gpml_root(self, BoardHeight, BoardWidth, Version="0.0.1", network_name="unknown_network", organism="unknown_organism"):
-        E = ElementMaker(namespace=GPML_NS)
-        return (
-            E("Pathway",
-              E("Graphics", BoardWidth=str(BoardWidth), BoardHeight=str(BoardHeight)),
-              Name=network_name, Version=Version, Organism=organism)
-        )
-
-    def make_gpml_point(self, X, Y, GraphRef, RelX="1.0", RelY="0.0", ArrowHead=None):
-        """
-        make GPML point
-        :param X: xcoord of point
-        :param Y: ycoord of point
-        :param GraphRef: id-ref which must correspond to a (point) elsewhere
-        :param RelX: don't know, default = 1.0
-        :param RelY: don't know, default = 0.0
-        :param ZOrder: presumably Z- in painter model; default 32768
-        :param LineThickness: default 1.0
-        :return: GPML point
-
-        """
-        E = ElementMaker(namespace=GPML_NS)
-        point = E("Point", X=str(X), Y=str(Y), GraphRef=GraphRef, RelX=str(RelX), RelY=str(RelY))
-        if ArrowHead is not None:
-            point.set("ArrowHead", ArrowHead)
-        return point
-
-    def make_gpml_xref(self, Database=None, ID=None):
-        """GPML Xref to database
-        :param Database: example - CHEBI
-        :param ID: example - "CHEBI:47775"
-        """
-        E = ElementMaker(namespace=GPML_NS)
-        xref = E("Xref")
-        if Database is not None:
-            xref.set(self.DATABASE, Database)
-        if Database is not None:
-            xref.set(self.UPPER_ID, ID)
-        return xref
-
-    def make_text_data_node(self, CenterX, CenterY, TextLabel, GraphId, Type, Width, Height, ZOrder="32768", FontSize="12",
-                            Valign="Middle", Color="0000ff", Database=None, ID=None):
-        """Creates a data_node for GPML
-        :param CenterX: x coord
-        :param CenterY: y coord
-        :param TextLabel: label for node
-        :param GraphId: id referenced from , e.g. gpml_point
-        :param Type: type (e.g. "Metabolite")
-        :param Width: presumably width of box
-        :param Height: presumably height of box
-        :param ZOrder: ZOrder (painters model?)
-        :param FontSize:
-        :param Valign: e.g. "Middle"
-        :param Color: I think the line color
-        :param Database: Database (may be None)
-        :param ID: database ID
-
-
-        """
-        E = ElementMaker(namespace=GPML_NS)
-        data_node = E("DataNode", TextLabel=TextLabel, GraphId=GraphId, Type=Type)
-        graphics = E("Graphics", CenterX=str(CenterX), CenterY=str(CenterY), Width=str(Width), Height=str(Height), ZOrder=ZOrder,
-                     FontSize=str(FontSize), Valign=str(Valign), Color=Color)
-        data_node.append(graphics)
-        if Database is not None:
-            xref = self.make_gpml_xref(Database=Database, ID=ID)
-            data_node.append(xref)
-        return data_node
-
-    @classmethod
-    def get_text_val(cls, text_container):
-        """gets text value from an element containing <text> element
-        :param text_container:
-        :return: text vallue
-
-        """
-        text_val = ns_xpath(text_container, f"{{{SVG_NS}}}text")
-        if type(text_val) is etree._Element:
-            text_val = text_val.text
-        elif type(text_val) is list:
-            text_val = text_val[0].text  # several texts in box
-        return text_val
-
-    @classmethod
-    def get_bbox(cls, bbox_elem):
-        """get BoundingBox from any element with X, Y , width, height
-        :param bbox_elem: element with these attributes
-        :return: BBox element
-        """
-        return BBox.create_from_xy_w_h(
-            [float(bbox_elem.get(BBox.X)), float(bbox_elem.get(BBox.Y))],
-            float(bbox_elem.get(BBox.WIDTH)),
-            float(bbox_elem.get(BBox.HEIGHT))
-        )
-
-    def clean_overlap_in_arrows(self):
-        """
-        Iterates over overlaps and applies heuristics to clean them
-        :return:
-        """
-        pass
-
-    def create_reactions(self):
-        """iterarates over dict keys for arrow and extracts back/front"""
-        for key in self.arrows_text_dict.keys():
-            if key == self.ARROW:
-                if ArrowBBox.FRONT in self.arrows_text_dict[key] and ArrowBBox.BACK in self.arrows_text_dict[key]:
-                    print(self.arrows_text_dict[key][ArrowBBox.BACK], key, self.arrows_text_dict[key][ArrowBBox.FRONT])
-
-    def get_total_bbox(self):
-        """iterates over all bboxes to get total extent
-        """
-        total_bbox = BBox()
-
-        print(f"arrows dict {self.arrows_dict}")
-        for arrow_id in self.arrows_dict:
-            bbox = self.arrows_dict[arrow_id][self.BBOX]
-            # print (f"arrow_id: {arrow_id} {bbox}")
-            total_bbox = bbox if not total_bbox.is_valid() else total_bbox.union(bbox)
-            # print(f"new box {total_bbox}")
-        for textbox_id in self.arrows_text_dict[self.TEXTBOXES]:
-            bbox = self.textboxes_dict[textbox_id][self.BBOX]
-            # print (f"textbox_id: {textbox_id} {bbox}")
-            total_bbox = bbox if not total_bbox.is_valid() else total_bbox.union(bbox)
-
-        return total_bbox
-        print(f"total_box {total_bbox}")
-
-class ArrowBBox:
-    CORE = "core"
-
-    # arrow frame
-    BACK = "back"
-    FRONT = "front"
-    LEFT = "left"
-    RIGHT = "right"
-
-    # graphics frame
-    PLUSX = "plus_x"
-    MINUSX = "minus_x"
-    PLUSY = "plus_y"
-    MINUSY = "minus_y"
diff --git a/src/pyamiimage/ami_edge.py-old b/src/pyamiimage/ami_edge.py-old
deleted file mode 100644
index ccc3310..0000000
--- a/src/pyamiimage/ami_edge.py-old
+++ /dev/null
@@ -1,39 +0,0 @@
-"""wrapper for edge from sknw/nx, still being developed"""
-
-from pyimage.svg import BBox
-
-
-class AmiEdge:
-    def __init__(self):
-        self.points_xy = None
-        self.bbox = None
-
-    def read_nx_edge_points_yx(self, points_array_yx):
-        """
-        convert from nx_points (held as yarray, xarray) to array(x, y)
-        :param points_array_yx:
-        :return:
-        """
-        # points are in separate columns (y, x)
-        # print("coord", points[:, 1], points[:, 0], 'green')
-        # I can't get list comprehension to work, help needed!
-        # self.points_xy = [   [point[1], point[0]] for point in points_array_yx]:
-        assert points_array_yx is not None and points_array_yx.ndim == 2 and points_array_yx.shape[1] == 2
-        self.points_xy = []
-        for point in points_array_yx:
-            self.points_xy.append([point[1], point[0]])
-        # print ("points_xy", self.points_xy)
-
-    def __repr__(self):
-        s = ""
-        if self.points_xy is not None:
-            s = f"ami edge pts: {self.points_xy[0]} .. {len(str(self.points_xy))} .. {self.points_xy[-1]}"
-        return s
-
-    def get_or_create_bbox(self):
-        if self.bbox is None and self.points_xy is not None:
-            self.bbox = BBox()
-            for point in self.points_xy:
-                self.bbox.add_coordinate(point)
-
-        return self.bbox
diff --git a/src/pyamiimage/ami_graph.py-old b/src/pyamiimage/ami_graph.py-old
deleted file mode 100644
index 1ea74cf..0000000
--- a/src/pyamiimage/ami_graph.py-old
+++ /dev/null
@@ -1,316 +0,0 @@
-"""supports/wraps nx_graphs from N etworkX"""
-import numpy as np
-import networkx as nx
-import copy
-from networkx.algorithms import tree
-from skimage import io
-import sknw  # must pip install sknw
-import logging
-from pathlib import PosixPath
-
-from pyimage.ami_image import AmiImage
-from pyimage.util import Util
-import pyimage.ami_island
-from pyimage.ami_node import AmiNode
-from pyimage.ami_skeleton import AmiSkeleton
-from pyimage.ami_edge import AmiEdge
-
-
-class AmiGraph:
-    """holds AmiNodes and AmiEdges
-    may also hold subgraphs
-    """
-
-    logger = logging.getLogger("ami_graph")
-
-    def __init__(self, nx_graph, generate_nodes=True, nd_skeleton=None):
-        """create fro nodes and edges"""
-        if nx_graph is None:
-            raise Exception(f"nx_graph cannot be None")
-        self.nx_graph = nx_graph
-        AmiGraph.debugx(self, "init")
-
-        self.ami_edges = None
-        self.ami_nodes = None
-        self.ami_island_list = None
-        self.nd_skeleton = nd_skeleton
-
-        self.read_nx_graph(nx_graph)
-        assert self.nx_graph is not None, f"ami_graph.nx_graph should not be None"
-        return
-
-    @classmethod
-    def debugx(cls, ami_graph, msg):
-        print(f"dir ami_graph {msg} {dir(ami_graph)} \n contains nx_graph {'nx_graph' in dir(ami_graph)}")
-        assert 'nx_graph' in dir(ami_graph)
-
-    def get_nx_graph(self):
-        """try to avoid circular imports or attribute not found"""
-        return self.nx_graph
-
-    def add_raw_node(self, raw_node, fail_on_duplicate=False):
-        """add a raw node either a string or string-indexed dict
-        if already a dict, deepcopy it
-        if a primitive make a node_dict and start it with raw_node as id
-        :raw_node: node to add, must have key
-        :fail_on_duplicate: if true fail if key already exists
-        """
-        if raw_node is not None:
-            # ami_node = AmiNode()
-            key = raw_node.key if type(raw_node) is dict else str(raw_node)
-            key = "n" + str(key)
-            if key in self.ami_node_dict and fail_on_duplicate:
-                raise AmiGraphError(f"cannot add same node twice {key}")
-            if type(raw_node) is dict:
-                self.ami_node_dict[key] = copy.deepcopy(raw_node)
-            else:
-                self.ami_node_dict[key] = "node"  # store just the key at present
-        else:
-            self.logger.warning("node cannot be None")
-
-    def read_edges(self, edges):
-        # self.ami_edges = edges
-        if len(self.ami_node_dict.keys()) == 0 and self.generate_nodes:
-            self.generate_nodes_from_edges()
-            print("after node generation", str(self))
-        for i, edge in enumerate(edges):
-            idx = "e" + str(i)
-            self.add_edge(edge, idx)
-
-    def add_edge(self, raw_edge, idx, fail_on_duplicate=True):
-        if fail_on_duplicate and idx in self.ami_edge_dict.keys():
-            raise ValueError("duplicate edge")
-
-        if raw_edge is None:
-            raise AmiGraphError("cannot add edge=None")
-        edge1 = ("n" + str(raw_edge[0]), "n" + str(raw_edge[1]))
-        self.ami_edge_dict[idx] = edge1
-
-    def generate_nodes_from_edges(self):
-        if self.ami_edges is not None:
-            for edge in self.ami_edges:
-                self.add_raw_node(edge[0])
-                self.add_raw_node(edge[1])
-
-    @classmethod
-    def create_ami_graph(cls, nd_skeleton):
-        """Uses Sknw to create a graph object within a new AmiGraph"""
-        # currently only called in a test
-        nx_graph = sknw.build_sknw(nd_skeleton)
-        ami_graph = AmiGraph(nx_graph, nd_skeleton=nd_skeleton)
-        # ami_graph.read_nx_graph(nx_graph )
-        print(f"***ami_graph\n {ami_graph} nx_graph: {ami_graph.nx_graph}\n")
-        return ami_graph
-
-    def ingest_graph_info(self):
-        if self.nx_graph is None:
-            self.logger.warning("Null graph")
-            return
-        # print("graph", self.nx_graph)
-        nx_island_list = list(nx.connected_components(self.nx_graph))
-        if nx_island_list is None or len(nx_island_list) == 0:
-            self.logger.warning("No islands")
-            return
-
-        self.assert_nx_island_info(nx_island_list)
-        nx_edgelist = self.get_edge_list_through_mininum_spanning_tree()
-        self.debug_edges_and_nodes(nx_edgelist, debug_count=7)
-        nodes = self.nx_graph.nodes
-        self.node_dict = {i: (nodes[node]["o"][0], nodes[node]["o"][1]) for i, node in enumerate(nodes)}
-
-        self.ami_island_list = []
-        for nx_island in nx_island_list:
-            ami_island = self.create_ami_island(nx_island, skeleton=self.nd_skeleton)
-            print(f"ami_island {ami_island}")
-            self.ami_island_list.append(ami_island)
-
-        return
-
-    def assert_nx_island_info(self, nx_island_list):
-        nx_island0 = nx_island_list[0]
-        assert type(nx_island0) is set
-        assert len(nx_island0) > 0
-        elem0 = list(nx_island0)[0]
-        assert type(elem0) is int, f"island elem are {type(elem0)}"
-        print("islands", self.ami_island_list)
-
-    def debug_edges_and_nodes(self, nx_edgelist, debug_count=5):
-        start_node = 0
-        end_node = 1
-        pts_index = 2
-        for edge in nx_edgelist[:debug_count]:
-            pts_ = edge[pts_index]['pts']
-            print(edge[start_node], edge[end_node], "pts in edge", len(pts_))
-        edgelist_pts_ = nx_edgelist[0][2]['pts']
-        for step in edgelist_pts_[:debug_count]:
-            print("step", step)
-
-    def get_edge_list_through_mininum_spanning_tree(self):
-        mst = tree.maximum_spanning_edges(self.nx_graph, algorithm="kruskal", data=True)
-        # mst = tree.minimum_spanning_tree(graph, algorithm="kruskal")
-        nx_edgelist = list(mst)
-        return nx_edgelist
-
-    @classmethod
-    def set_bbox_pixels_to_color(cls, bbox, image, colorx=255):
-        """sets all pixels in box to uniform color
-
-        :param bbox:
-        :param image:
-        :param colorx:
-        :return: modified image
-        """
-        xx = bbox[0]
-        yy = bbox[1]
-        image[xx[0]:xx[1], yy[0]:yy[1]] = colorx
-        return image
-
-    def __str__(self):
-        s = "nodes: " + str(self.ami_nodes) + \
-            "\n edges: " + str(self.ami_edges)
-        return s
-
-    def read_nx_graph(self, nx_graph):
-        """
-        Read and unpack NetworkX graph.
-        This may change as a result of changing data models
-        the nx_graph may be tyhe fundamental data structure
-        :param nx_graph:
-        :return:
-        """
-        # self.nodes_as_dicts = [nx_graph.node[ndidx] for ndidx in (nx_graph.nodes())]
-        # self.nodes_yx = [nx_graph.node[ndidx][AmiSkeleton.CENTROID] for ndidx in (nx_graph.nodes())]
-        self.read_nx_edges(nx_graph)
-        self.read_nx_nodes(nx_graph)
-        # this may be the critical data structure and the others are convenience
-        self.nx_graph = nx_graph
-        ingest = False
-        if ingest:
-            self.ingest_graph_info()
-        return
-
-    def read_nx_nodes(self, nx_graph):
-        self.ami_nodes = []
-        node_ids = nx_graph.nodes()
-        for node_id in node_ids:
-            node_dict = node_ids[node_id]
-            assert len(str(node_id)) < 4, f"node_id is {node_id}"
-            ami_node = AmiNode(ami_graph=self, node_id=node_id, nx_graph=nx_graph)
-            ami_node.set_centroid_yx(nx_graph.nodes[node_id][AmiSkeleton.CENTROID])
-            ami_node.read_nx_node(node_dict)
-            self.ami_nodes.append(ami_node)
-
-    def read_nx_edges(self, nx_graph):
-        self.ami_edges = []
-        for (start, end) in nx_graph.edges():
-            points_yx = nx_graph[start][end][AmiSkeleton.NODE_PTS]
-            ami_edge = AmiEdge()
-            ami_edge.read_nx_edge_points_yx(points_yx)
-            self.ami_edges.append(ami_edge)
-
-    def get_or_create_ami_islands(self):
-        """
-        AmiIslands are nx_graph 'components' with added functionality
-        :return: list of AmiIslands
-        """
-        if self.ami_island_list is None and self.nx_graph is not None:
-            self.ami_island_list = [self.create_ami_island(comp) for comp in
-                                    nx.algorithms.components.connected_components(self.nx_graph)]
-        return self.ami_island_list
-
-    @classmethod
-    def create_nx_graph_from_arbitrary_image_file(cls, path):
-        assert path.exists() and not path.is_dir(), f"{path} should be existing file"
-        Util.check_type_and_existence(path, PosixPath)
-        image1 = io.imread(path)
-        Util.check_type_and_existence(image1, np.ndarray)
-        gray_image = AmiImage.create_grayscale_0_1_float_from_image(image1)
-        skeleton_array = AmiImage.create_white_skeleton_from_image(gray_image)
-        nx_graph = AmiSkeleton.create_nx_graph_from_skeleton_wraps_sknw_NX_GRAPH(skeleton_array)
-        return nx_graph
-
-    def get_ami_islands_from_nx_graph(self):
-        """
-        Get the pixel-disjoint "islands" as from NetworkX
-        :return: list of AmiIslands
-        """
-
-        self.get_coords_for_nodes_and_edges_from_nx_graph(self.nx_graph)
-        assert self.nx_graph is not None
-        ami_islands = []
-        for node_ids in nx.algorithms.components.connected_components(self.nx_graph):
-            print("node_ids ", node_ids)
-            ami_island = self.create_ami_island(node_ids)
-            assert ami_island is not None
-            assert type(ami_island) is AmiIsland
-            ami_islands.append(ami_island)
-        return ami_islands
-
-    def extract_coords_for_node(self, idx):
-        """
-        gets coords for a single node with given id
-        :param idx: normally an int
-        :return:
-        """
-        node_data = self.nx_graph.nodes[idx]
-        centroid = node_data[AmiSkeleton.CENTROID]
-        centroid = (centroid[1], centroid[0])  # swap y,x as sknw seems to have this unusual order
-        return centroid
-
-    def create_islands(self):
-        """
-        needs nx_graph to exist
-
-        :return: list of islands
-        """
-
-        assert self.nx_graph is not None
-        self.islands = self.get_ami_islands_from_nx_graph()
-        return self.islands
-
-    @classmethod
-    def create_ami_island(self, node_ids, skeleton=None):
-
-        """
-        create from a list of node_ids (maybe from sknw)
-        maybe should be instance method of ami_graph
-        :param node_ids: set of node ids
-        :param ami_graph: essential
-        :param skeleton:
-        :return: AmiIsland object
-        """
-        assert type(node_ids) is set, "componente mus be of type set"
-        assert len(node_ids) > 0 , "components cannot be empty"
-
-        ami_island = pyimage.ami_island.AmiIsland()
-        ami_island.node_ids = node_ids
-        ami_island.ami_skeleton = skeleton
-        ami_island.ami_graph = self
-        print("ami_island", ami_island)
-        return ami_island
-
-
-
-class AmiGraphError(Exception):
-    def __init__(self, msg):
-        super().__init__(msg)
-
-
-if __name__ == '__main__':
-
-    img = np.array([
-        [0, 0, 0, 1, 0, 0, 0, 0, 0],
-        [0, 0, 0, 1, 0, 0, 0, 1, 0],
-        [0, 0, 0, 1, 0, 0, 0, 1, 0],
-        [0, 0, 0, 1, 0, 0, 0, 0, 0],
-        [1, 1, 1, 1, 0, 0, 0, 0, 0],
-        [0, 0, 0, 0, 1, 0, 0, 0, 0],
-        [0, 1, 0, 0, 0, 1, 0, 0, 0],
-        [1, 0, 1, 0, 0, 1, 1, 1, 1],
-        [0, 1, 0, 0, 1, 0, 0, 0, 0],
-        [0, 0, 0, 1, 0, 0, 0, 0, 0]])
-
-    # sknw.example1()
-    # sknw.example2horse()  # works
-    # sknw.example3() # needs flipping White to black
-    # sknw.example4() # needs flipping White to black
diff --git a/src/pyamiimage/ami_graph_all.py b/src/pyamiimage/ami_graph_all.py
deleted file mode 100644
index 7831d83..0000000
--- a/src/pyamiimage/ami_graph_all.py
+++ /dev/null
@@ -1,1732 +0,0 @@
-"""supports/wraps nx_graphs from NetworkX"""
-import copy
-import logging
-import math
-from pathlib import PurePath
-
-import matplotlib.patches as patches
-import matplotlib.pyplot as plt
-import networkx as nx
-# library
-import numpy as np
-import sknw  # must pip install sknw
-from networkx.algorithms import tree
-from skimage import io
-from skimage.measure import approximate_polygon
-
-# local
-from ..pyimage.ami_image import AmiImage
-from ..pyimage.ami_plot import AmiLine
-from ..pyimage.ami_util import AmiUtil
-from ..pyimage.svg import BBox
-from ..pyimage.text_box import TextBox
-
-logger = logging.getLogger(__name__)
-
-"""
-==========================================================================
-==============================GRAPH=======================================
-==========================================================================
-"""
-class AmiGraph:
-    """holds AmiNodes and AmiEdges
-    may also hold subgraphs
-    """
-    """nx_graph is a NetworkX graph which holds nodes and edges
-     and which can be used to compute other graph functionality (e.g. edges on nodes). 
-     Here we wrap its functionality in Ami* classes. This is because 
-     (a) it's hard for
-     newcomers like me to remember all the syntax (which is almost C-like "dict-of-dicts-of-dict-of-dicts)
-     or has many Views (rather than functions)
-     (b) There are 4 different types of graphs with different syntaxes. This code started as 
-     simple graphs nx_graph[i][j][properties] and then moved to multigraph nx_graph[i][j][branch][properties] 
-     
-     This is meant to help. If it doesn't, I'm sorry! and you can revert to native nx_graph. Here's a sample:
-      >>>
-      class MultiAdjacencyView(AdjacencyView):
-     An MultiAdjacencyView is a Read-only Map of Maps of Maps of Maps.
-
-    It is a View into a dict-of-dict-of-dict-of-dict data structure.
-    The inner level of dict is read-write. But the
-    outer levels are read-only.
-
-    See Also
-    ========
-    AtlasView: View into dict-of-dict
-    AdjacencyView: View into dict-of-dict-of-dict
-    
-    <<<
-    That's what AmiGraph tries to hide. I hope it works.
-    
-
-     
-     The components are:
-     * AmiGraph (wraps nx_graph and derived quantities such as lists of nodes and edges)
-     * AmiNode (wraps nx_graph.nodes[i]). nx_nodes are by default ints and should be kept as such
-     * AmiEdge (wraps nx_graph.edges - a pair of ints)
-     * AmiIsland (a discrete "component" of the graph)
-     
-     Our default is Multigraphs as images can contain paths which create loops and join two nodes in to or more
-     different ways. This means that edges require three indexes, start, end and branch. This means checking that 
-     this has been introduced universally.
-     
-     (I'm hoping that Simple graphs can also be switched on/off)
-     
-     Currently working with the idea that every edge has 
-     (i, j) for simple graph
-     (i, j, branch) for multigraph (requires keys=True)
-          
-     
-     """
-
-    logger = logging.getLogger("ami_graph")
-
-    def __init__(self, nx_graph, generate_nodes=False, nd_skeleton=None):
-        """create from nodes and edges"""
-        if nx_graph is None:
-            raise Exception(f"nx_graph cannot be None")
-        self.nx_graph = nx_graph
-
-        self.ami_edges = None
-        self.ami_nodes = None
-        self.ami_island_list = None
-        self.nd_skeleton = nd_skeleton
-        self.ami_edge_dict = None
-        self.generate_nodes = generate_nodes
-        self.node_dict = None
-        self.centroid_xy = None
-
-        # sets up all nodes and edges
-        self.read_nx_graph(nx_graph)
-        assert self.nx_graph is not None, f"ami_graph.nx_graph should not be None"
-        return
-
-    @classmethod
-    def create_ami_graph_from_arbitrary_image_file(cls, file, interactive=False):
-        assert file.exists()
-        nx_graph = AmiGraph.create_nx_graph_from_arbitrary_image_file(file, interactive=interactive)
-        return AmiGraph(nx_graph)
-
-    # def get_nx_graph(self):
-    #     """try to avoid circular imports or attribute not found"""
-    #     return self.nx_graph
-    #
-    def get_or_create_ami_node(self, node_id):
-        """create from node_id or rerieve from node_dict
-        use this method rather than AmiNode() constructor
-        Stores AmiNodes in self.node_dict
-        :param node_id: node_id (should exist in nx_graph but not checked yet
-        :return: AmiNode (None if node_id is None)
-        """
-        if node_id is None:
-            return None
-        if self.node_dict is None:
-            self.node_dict = dict()
-        if node_id not in self.node_dict:
-            ami_node = AmiNode(node_id, ami_graph=self, _private=True)
-            self.node_dict[node_id] = ami_node
-        else:
-            ami_node = self.node_dict[node_id]
-        return ami_node
-
-    # AmiGraph
-
-    def get_or_create_ami_edge_from_nx_edge(self, nx_edge):
-        """Wrapper for get_or_create_ami_edge_from_ids() to create ami_edge
-        :param nx_edge: 
-        :return: ami_edge (or None)
-        """
-        if nx_edge is None:
-            return None
-        branch_id = nx_edge[2] if self.nx_graph.is_multigraph() else None
-        ami_edge = self.get_or_create_ami_edge_from_ids(nx_edge[0], nx_edge[1], branch_id=branch_id)
-        return ami_edge
-
-    def get_or_create_ami_edge_from_ids(self, node_id1, node_id2, branch_id=None):
-        """create or lookup AmiEdge from node_ids, or retrievs from edge_dict
-        prefer this to AmiEdge constructor
-        key is (sorted(node_id1, node_id2)),
-        looks up AmiNode to check validity of node_ids
-        If there are multiple branches between two nodes they must have different branch_ids
-        (It's up to the user to manage this). Adding an edge without a branch_id will replace
-        the current one)
-
-        :param node_id1: start of edge
-        :param node_id2: end of edge
-        :param branch_id: id there are multiple branches; up to the user to manage these
-        :return: None if start_id or end_id is None
-        """
-        if type(node_id1) is not int or type(node_id2) is not int:
-            raise ValueError(f"node_ids must be ints , found: {node_id1}, {node_id2}")
-        if self.nx_graph.is_multigraph:
-            if type(branch_id) is not int:
-                raise ValueError(f"branch_id for multigraph must be int, found {type(branch_id)}")
-            key = (node_id1, node_id2, branch_id) if node_id1 < node_id2 \
-                else (node_id2, node_id1, branch_id)
-        else:
-            key = (node_id1, node_id2) if node_id1 < node_id2 else (node_id2, node_id1)
-
-        if self.ami_edge_dict is None:
-            self.ami_edge_dict = dict()
-        # new edge?
-        if key not in self.ami_edge_dict:
-            ami_edge = self.create_and_index_new_edge(key, node_id1, node_id2, branch_id)
-        # existing edge/s
-        else:
-            ami_edge = self.ami_edge_dict[key]
-        return ami_edge
-
-    def create_and_index_new_edge(self, key, node_id1, node_id2, branch_id):
-        """create new Edge and index it in edge_dict"""
-        ami_edge = AmiEdge(self, node_id1, node_id2, branch_id=branch_id, _private=True)
-        self.ami_edge_dict[key] = ami_edge
-        return ami_edge
-
-    def add_raw_node(self, raw_node, fail_on_duplicate=False):
-        """add a raw node either a string or string-indexed dict
-        if already a dict, deepcopy it
-        if a primitive make a node_dict and start it with raw_node as id
-        :raw_node: node to add, must have key
-        :fail_on_duplicate: if true fail if key already exists
-        """
-        if raw_node is not None:
-            # ami_node = AmiNode()
-            key = raw_node.key if type(raw_node) is dict else str(raw_node)
-            key = "n" + str(key)
-            if key in self.node_dict and fail_on_duplicate:
-                raise AmiGraphError(f"cannot add same node twice {key}")
-            if type(raw_node) is dict:
-                self.node_dict[key] = copy.deepcopy(raw_node)
-            else:
-                self.node_dict[key] = "node"  # store just the key at present
-        else:
-            self.logger.warning("node cannot be None")
-
-    # def read_edges(self, edges):
-    #     # self.ami_edges = edges
-    #     if len(self.node_dict.keys()) == 0 and self.generate_nodes:
-    #         self.generate_nodes_from_edges()
-    #     for i, edge in enumerate(edges):
-    #         idx = "e" + str(i)
-    #         self.add_edge(edge, idx)
-    #
-    # def add_edge(self, raw_edge, idx, fail_on_duplicate=True):
-    #     if fail_on_duplicate and idx in self.ami_edge_dict.keys():
-    #         raise ValueError("duplicate edge")
-    #
-    #     if raw_edge is None:
-    #         raise AmiGraphError("cannot add edge=None")
-    #     edge1 = ("n" + str(raw_edge[0]), "n" + str(raw_edge[1]))
-    #     self.ami_edge_dict[idx] = edge1
-    #
-    # AmiGraph
-
-    # def generate_nodes_from_edges(self):
-    #     if self.ami_edges is not None:
-    #         for edge in self.ami_edges:
-    #             self.add_raw_node(edge[0])
-    #             self.add_raw_node(edge[1])
-    #
-    # @classmethod
-    # def create_ami_graph_from_skeleton(cls, nd_skeleton):
-    #     """Uses Sknw to create a graph object within a new AmiGraph"""
-    #     # currently only called in a test
-    #     nx_graph = sknw.build_sknw(nd_skeleton)
-    #     ami_graph = AmiGraph(nx_graph, nd_skeleton=nd_skeleton)
-    #     return ami_graph
-
-    def _ingest_graph_info(self):
-        if self.nx_graph is None:
-            self.logger.warning("Null graph")
-            return
-        nx_island_list = list(nx.connected_components(self.nx_graph))
-        if nx_island_list is None or len(nx_island_list) == 0:
-            self.logger.warning("No islands")
-            return
-
-        AmiGraph.assert_nx_island_info(nx_island_list)
-        nx_edgelist = self.get_edge_list_ids_through_maximum_spanning_edges()
-        AmiGraph.debug_edges_and_nodes(nx_edgelist, debug_count=7)
-        nodes = self.nx_graph.nodes
-        self.node_dict = {i: (nodes[node][AmiNode.CENTROID][0], nodes[node][AmiNode.CENTROID][1])
-                          for i, node in enumerate(nodes)}
-
-        self.ami_island_list = []
-        for nx_island in nx_island_list:
-            ami_island = self.create_ami_island(nx_island)
-            self.ami_island_list.append(ami_island)
-
-        return
-
-    @classmethod
-    def assert_nx_island_info(cls, nx_island_list):
-        nx_island0 = nx_island_list[0]
-        assert type(nx_island0) is set
-        assert len(nx_island0) > 0
-        elem0 = list(nx_island0)[0]
-        assert type(elem0) is int, f"island elem are {type(elem0)}"
-
-    # AmiGraph
-
-    @classmethod
-    def debug_edges_and_nodes(cls, nx_edgelist, debug_count=5):
-        pts_index = 2
-        for edge in nx_edgelist[:debug_count]:
-            pts_ = edge[pts_index]['pts']
-            print("points", pts_)
-        edgelist_pts_ = nx_edgelist[0][2]['pts']
-        for step in edgelist_pts_[:debug_count]:
-            print("step", step)
-            pass
-
-    def get_edge_list_ids_through_maximum_spanning_edges(self):
-        """
-
-        :return: list of edges as ids
-        """
-        mst = tree.maximum_spanning_edges(self.nx_graph, algorithm="kruskal", data=True)
-        # mst = tree.minimum_spanning_tree(graph, algorithm="kruskal")
-        nx_edgelist = list(mst)
-        return nx_edgelist
-
-    @classmethod
-    def set_bbox_pixels_to_color(cls, bbox, image, colorx=255):
-        """sets all pixels in box to uniform color
-
-        :param bbox:
-        :param image:
-        :param colorx:
-        :return: modified image
-        """
-        xx = bbox[0]
-        yy = bbox[1]
-        image[xx[0]:xx[1], yy[0]:yy[1]] = colorx
-        return image
-
-    def __str__(self):
-        s = "nodes: " + str(self.ami_nodes) + \
-            "\n edges: " + str(self.ami_edges)
-        return s
-
-    def read_nx_graph(self, nx_graph):
-        """
-        Read and unpack NetworkX graph.
-        This may change as a result of changing data models
-        the nx_graph may be the fundamental data structure
-        :param nx_graph:
-        :return:
-        """
-        # this may be the critical data structure and the others are convenience
-        self.nx_graph = nx_graph
-        self.get_or_create_all_ami_edges()
-        self.read_nx_nodes()
-        ingest = False
-        if ingest:
-            self._ingest_graph_info()
-        return
-
-    def read_nx_nodes(self):
-        """read nx_graph and create AmiNodes"""
-        assert self.nx_graph is not None, "must set self.nx_graph"
-        max_digits = 5
-        self.ami_nodes = []
-        node_ids = self.nx_graph.nodes()
-        if self.node_dict is None:
-            self.node_dict = dict()
-        for node_id in node_ids:
-            assert len(str(node_id)) < max_digits, f"node_id is {node_id}"
-            ami_node = self.get_or_create_ami_node(node_id)
-            self.node_dict[node_id] = ami_node
-            ami_node.set_centroid_yx(self.nx_graph.nodes[node_id][AmiNode.CENTROID])
-            self.ami_nodes.append(ami_node)
-
-    # AmiGraph
-
-    def get_or_create_all_ami_edges(self):
-        """read nx_graph and create AmiEdges
-        do not confuse with self.get_ami_edges_for_start_end
-
-        :return: self.ami_edges
-        """
-        if not self.ami_edges:
-            self.ami_edges = []
-            if self.nx_graph.is_multigraph:
-                for (start_id, end_id) in self.nx_graph.edges():
-                    # this gives us a list of ami_edges for node pair
-                    ami_edges = self.get_ami_edge_list_for_start_end(start_id, end_id)
-                    for ami_edge in ami_edges:
-                        self.ami_edges.append(ami_edge)
-            else:
-                for (start_id, end_id) in self.nx_graph.edges():
-                    ami_edge = self.get_or_create_ami_edge_from_ids(start_id, end_id)
-                    self.ami_edges.append(ami_edge)
-
-            # print(f"ami_edge_dict {self.ami_edge_dict.keys()} ")
-            #       f"=> {self.ami_edge_dict} ")
-        return self.ami_edges
-
-    def get_ami_edge_list_for_start_end(self, start_id, end_id):
-        """return edges between given nodes
-        takes account of multigraph multiple edges
-        :param start_id: start id
-        :param end_id: may or may not be identical to start_id
-        """
-        if not self.nx_graph.is_multigraph:
-            raise ValueError(f"get_ami_edge_list_for_start_end requires a multigraph ")
-
-        ami_edges = []
-        branch_ids = self.get_branch_ids_for_start_end(start_id, end_id)
-        for branch_id in branch_ids:
-            ami_edge = self.get_or_create_ami_edge_for_start_end_branch(start_id, end_id, branch_id)
-            ami_edges.append(ami_edge)
-
-        return ami_edges
-
-    def get_branch_ids_for_start_end(self, start_id, end_id):
-        """this iterates over the keys of the edge[i][j]
-        which are AtlasViews
-        These views need calling as functions
-
-        An explanation from NetworkX:
-
-        An AtlasView is a Read-only Mapping of Mappings.
-
-        It is a View into a dict-of-dict data structure.
-        The inner level of dict is read-write. But the
-        outer level is read-only.
-
-        This is not the best way of finding keys but I think it works
-
-        :param start_id: start id numerica
-        :param end_id: end_id (for loops/circles this may be equal to start_id
-        """
-        branch_ids = []
-        # count each branch once only
-        if AmiEdge.is_normalized_edge(start_id, end_id):
-            branch_dict = self.nx_graph[start_id][end_id]
-            for key in branch_dict.keys():
-                branch_ids.append(key)
-        return branch_ids
-
-    def get_or_create_ami_islands(self, maxdim=None, mindim=None, minmaxdim=None, maxmindim=None, reset=True):
-        """
-        NOT YET TESTED
-        AmiIslands are nx_graph 'components' with added functionality
-
-        :param reset: recalculate island list and reset self.ami_island_list ; default = True
-        :param maxdim: maximum maximum dimension (BOTH dimensions <= maxdim)
-        :param mindim: minimum minimum dimension (BOTH dimensions >= mindim
-        :param minmaxdim: maximum maximum dimension (at least ONE >= minmaxdim)
-        :param maxmindim: maximum minimum dimension (at least ONE <= maxmindim
-        :return: list of AmiIslands
-
-        if no optional args returns all islands
-        Dimensions:
-        Assume islands of width, height:
-        A[1, 6] B[1,10], C[4,3], D[20, 30] E[1, 20] F[1,2]
-        maxdim = 5 selects C, F
-        mindim = 3 selects C D
-        maxmindim = 2 selects A B E F (i.e thin)
-        minmaxdim = 5 selects A B D E (i.e. not tiny)
-
-        They can be combined:
-        mindim = 3 and maxdim = 4 selects C
-
-        """
-        if (self.ami_island_list is None or reset) and self.nx_graph is not None:
-            self.ami_island_list = [self.create_ami_island(comp) for comp in
-                                    nx.algorithms.components.connected_components(self.nx_graph)]
-        if mindim is not None:
-            self.ami_island_list = [isl for isl in self.ami_island_list
-                                    if isl.get_or_create_bbox().min_dimension() >= mindim]
-        if maxdim is not None:
-            self.ami_island_list = [isl for isl in self.ami_island_list
-                                    if isl.get_or_create_bbox().max_dimension() <= maxdim]
-        if minmaxdim is not None:
-            self.ami_island_list = [isl for isl in self.ami_island_list
-                                    if isl.get_or_create_bbox().max_dimension() >= minmaxdim]
-        if maxmindim is not None:
-            self.ami_island_list = [isl for isl in self.ami_island_list
-                                    if isl.get_or_create_bbox().min_dimension() <= maxmindim]
-        return self.ami_island_list
-
-    # AmiGraph
-
-    @classmethod
-    def create_nx_graph_from_arbitrary_image_file(cls, path, interactive=False):
-        assert path.exists() and not path.is_dir(), f"{path} should be existing file"
-        assert isinstance(path, PurePath)
-
-        image1 = io.imread(str(path))
-        AmiUtil.check_type_and_existence(image1, np.ndarray)
-        gray_image = AmiImage.create_grayscale_from_image(image1)
-        assert AmiImage.check_grayscale(gray_image)
-
-        if interactive:
-            io.imshow(gray_image)
-            io.show()
-        skeleton_array = AmiImage.invert_binarize_skeletonize(gray_image)
-        if interactive:
-            io.imshow(skeleton_array)
-            io.show()
-        assert np.max(skeleton_array) == 255, f"max skeleton should be 255 found {np.max(skeleton_array)}"
-
-        nx_graph = AmiGraph.create_nx_graph_from_skeleton(skeleton_array)
-        return nx_graph
-
-    @classmethod
-    def create_nx_graph_from_skeleton(cls, skeleton_image):
-        """
-        DO NOT INLINE
-        multi: allows for multiple paths between two nodes (almost certainly required)
-        iso: finds isolated points. It would be valuable for dotted lines
-        ring: finds rings without nodes (e.g. boxes)
-        full: makes all edges connect to centre of extended node
-
-        :param skeleton_image:
-        :return:
-        """
-        AmiUtil.check_type_and_existence(skeleton_image, np.ndarray)
-
-        nx_graph = sknw.build_sknw(skeleton_image, multi=True, iso=True, ring=True, full=True)
-        return nx_graph
-
-    def get_ami_islands_from_nx_graph(self):
-        """
-        Get the pixel-disjoint "islands" as from NetworkX
-        :return: list of AmiIslands
-        """
-
-        # self.get_coords_for_nodes_and_edges_from_nx_graph(self.nx_graph)
-        assert self.nx_graph is not None
-        ami_islands = []
-        for node_ids in nx.algorithms.components.connected_components(self.nx_graph):
-            ami_island = self.create_ami_island(node_ids)
-            assert ami_island is not None
-            assert type(ami_island) is AmiIsland
-            ami_islands.append(ami_island)
-        return ami_islands
-
-    # AmiGraph
-
-    # def extract_coords_for_node(self, idx):
-    #     """
-    #     gets coords for a single node with given id
-    #     :param idx: normally an int
-    #     :return:
-    #     """
-    #     node_data = self.nx_graph.nodes[idx]
-    #     centroid = node_data[AmiNode.CENTROID]
-    #     centroid = (centroid[1], centroid[0])  # swap y,x as sknw seems to have this unusual order
-    #     return centroid
-    #
-
-    # -------- AmiIsland routines
-
-    def create_ami_island(self, node_ids):
-
-        """
-        create from a list of node_ids (maybe from sknw)
-        :param node_ids: set of node ids
-        :return: AmiIsland object
-        """
-        assert type(node_ids) is set, "componente mus be of type set"
-        assert len(node_ids) > 0, "components cannot be empty"
-
-        ami_island = AmiIsland(ami_graph=self, node_ids=node_ids)
-        return ami_island
-
-    # -------- AmiGraph/AmiNode routines
-
-    def get_angles_round_node(self, node_id):
-        """
-        gets ordered lists of angles
-        may not be efficient but for small numnbers of connections can be tolerated
-        :param node_id:
-        :return: ref_node, central_node, {node: val}
-        """
-        assert node_id is not None
-
-        node_ids = list(self.nx_graph.neighbors(node_id))
-        assert node_ids is not None
-        assert node_ids[0] is not None, f"node_ids {node_ids}"
-
-        # xys = {node_id: AmiNode(ami_graph=self, node_id=node_id).centroid_xy for node_id in node_ids}
-        # print(xys.keys())
-
-        # angle_dict = {node1: self.get_angle(node_ids[0], node_id, node1) for node1 in node_ids[1:]}
-        angle_dict = {}
-        for node1 in node_ids:
-            angle = self.get_angle_between_nodes(node_ids[0], node_id, node1)
-            angle_dict[node1] = angle
-        return node_ids[0], node_id, angle_dict
-
-    # AmiGraph
-
-    def get_angle_between_nodes(self, node_id0, node_id1, node_id2):
-        """
-        gets angle 0-1-2
-        :param node_id0:
-        :param node_id1:
-        :param node_id2:
-        :return: radians
-        """
-        assert node_id0 is not None
-        assert node_id1 is not None
-        assert node_id2 is not None
-
-        # TODO messy - recreating AmiNodes is very expensive
-        xy0 = AmiUtil.to_float_array(self.get_or_create_ami_node(node_id0).centroid_xy)
-        xy1 = AmiUtil.to_float_array(self.get_or_create_ami_node(node_id1).centroid_xy)
-        xy2 = AmiUtil.to_float_array(self.get_or_create_ami_node(node_id2).centroid_xy)
-        return AmiUtil.get_angle(xy0, xy1, xy2)
-
-    # -------- AmiEdge routines
-
-    def find_longest_edge(self, node_id):
-        edges = self.nx_graph.edges(node_id)
-
-        max_edge = None
-        max_length = -1.0
-        for edge in edges:
-            ami_edge = self.get_or_create_ami_edge_from_ids(edge[0], edge[1], branch_id=0)
-            length = ami_edge.get_cartesian_length()
-            assert length is not None
-            if length > max_length:
-                max_edge = edge
-                max_length = length
-        return max_edge, max_length
-
-    # -------- segmentation and plotting
-
-    def pre_plot_edges(self, plot_target):
-        """
-        recognizes different edge structures for simple and multigraph and prepares to plot
-        :param plot_target:
-        :return:
-        """
-        if self.nx_graph.is_multigraph:
-            for (start_id, end_id) in self.nx_graph.edges():
-                branch_ids = self.get_branch_ids_for_start_end(start_id, end_id)
-                for branch_id in branch_ids:
-                    print(f" {__name__} branch_ids / id {branch_ids} {branch_id}")
-                    pts = self.get_points_on_line(start_id, end_id, branch_id=branch_id)
-                    ami_edge = self.get_or_create_ami_edge_from_ids(start_id, end_id, branch_id=branch_id)
-                    ami_edge.plot_edge(pts, plot_target, edge_id=branch_id)
-        else:
-            for (start_id, end_id) in self.nx_graph.edges():
-                pts = self.get_points_on_line(start_id, end_id)
-                ami_edge = self.get_or_create_ami_edge_from_ids(start_id, end_id)
-                ami_edge.plot_edge(pts, plot_target)
-
-    def get_points_on_line(self, start_id, end_id, branch_id=None):
-        print(f"s/e/b {start_id} {end_id} {branch_id}")
-        if self.nx_graph.is_multigraph and type(branch_id) is int:
-            try:
-                points = self.nx_graph[start_id][end_id][branch_id][AmiEdge.PTS]
-            except KeyError as e:
-                logger.error(f"cannot get points {e} {start_id} {end_id} {branch_id}")
-                raise e
-        else:
-            points = self.nx_graph[start_id][end_id][AmiEdge.PTS]
-        return points
-
-    def pre_plot_nodes(self, plot_ids=False):
-        """prepares to plot matplotlib nodes for
-        :param plot_ids:
-        """
-        nodes = self.nx_graph.nodes()
-        ps = np.array([nodes[i][AmiNode.CENTROID] for i in nodes])
-        plt.plot(ps[:, 1], ps[:, 0], 'r.')
-        if plot_ids:
-            # probably a more pythomic approach
-            for i in nodes:
-                plt.text(ps[i, 1], ps[i, 0], str(i))
-
-    # AmiGraph
-
-    @classmethod
-    def add_bbox_rect(cls, axis, bbox, linewidth=1, edgecolor="red", facecolor="none"):
-        """
-        adds rectangle to axis subplot
-        :param axis: axis from matplotlib subplots
-        :param bbox: BBox from pyamiimage or its ranges
-        :param linewidth: linewidth of plotted rect (1)
-        :param edgecolor: stroke color of line ("red")
-        :param facecolor: fill of rect ("none")
-        :return:
-        """
-        assert type(bbox) is BBox, f"bbox should be BBox, found {type(bbox)}"
-        xyr = bbox.xy_ranges
-        rect = patches.Rectangle((xyr[0][0], xyr[1][0]), bbox.get_width(), bbox.get_height(),
-                                 linewidth=linewidth, edgecolor=edgecolor, facecolor=facecolor)
-        axis.add_patch(rect)
-
-    @classmethod
-    def plot_axis(cls, img_array, axis, islands, title=None):
-        if title:
-            axis.set_title(title)
-        for island in islands:
-            bbox = island.get_or_create_bbox()
-            AmiGraph.add_bbox_rect(axis, bbox)
-        axis.imshow(img_array)
-
-    @classmethod
-    def plot_text_box_boxes(cls, img_array, ax, text_boxes1):
-        for text_box in text_boxes1:
-            assert type(text_box) is TextBox, f"should be TextBox found {type(text_box)}"
-            assert type(text_box.bbox) is BBox, f"expected BBox found {type(text_box.bbox)}"
-            AmiGraph.add_bbox_rect(ax, text_box.bbox)
-        ax.imshow(img_array)
-
-    def get_nx_edge_list_for_node(self, node_id):
-        """.
-
-        :param node_id:
-        :return: list (not view) of edges as (node_id, node_id1) (node_id, node_id2) ...
-        """
-        assert self.nx_graph.is_multigraph(), "should be multigraph"
-        # keys gives ghe branch_ids
-        nx_edges = list(self.nx_graph.edges(node_id, keys=True))
-
-        return nx_edges
-
-    # def get_angle_to_x(self, edge):
-    #
-    #     n0 = self.nx_graph.nodes[edge[0]]
-    #     n1 = self.nx_graph.nodes[edge[1]]
-    #     print(f"x0 {n0} x1 {n1}")
-    #
-
-    # AmiGraph
-
-    def get_direct_length(self, nx_edge):
-        """
-        
-        :param nx_edge: a tuple of node_ids
-        :return: 
-        """
-        assert len(nx_edge) == 2
-        xy0 = self.get_or_create_centroid_xy(nx_edge[0])
-        xy1 = self.get_or_create_centroid_xy(nx_edge[1])
-        return AmiUtil.get_dist(xy0, xy1)
-
-    def get_nx_edge_lengths_by_edge_list_for_node(self, node_id):
-        length_by_nx_edge = {}
-        nx_edges = list(self.nx_graph.edges(node_id))
-        # lengths = [self.get_direct_length(nx_edge)
-        for nx_edge in nx_edges:
-            length = self.get_direct_length(nx_edge)
-            length_by_nx_edge[nx_edge] = length
-        return length_by_nx_edge
-
-    def get_interedge_tuple_angle(self, nx_edge0, nx_edge1):
-        """
-        angle between 2 nx_edges meeting at a common point
-        first component of each edge is the common node
-        :param nx_edge0:
-        :param nx_edge1:
-        :return:
-        """
-        assert (type(nx_edge0) is tuple), f"nx_edge0 is {type(nx_edge0)}"
-        assert (type(nx_edge1) is tuple), f"nx_edge1 is {type(nx_edge1)}"
-        assert len(nx_edge0) == 3, f"edge should be 3 integers {nx_edge0}"
-        assert len(nx_edge1) == 3, f"edge should be 3 integers {nx_edge1}"
-        # find the common node and the other ends
-        # this is not pretty, but works (nodes = end1, centre, end2)
-        if nx_edge0[0] == nx_edge1[0]:
-            nodes = nx_edge0[1], nx_edge0[0], nx_edge1[1]
-        elif nx_edge0[1] == nx_edge1[1]:
-            nodes = nx_edge0[0], nx_edge0[1], nx_edge1[0]
-        elif nx_edge0[1] == nx_edge1[0]:
-            nodes = nx_edge0[0], nx_edge0[1], nx_edge1[1]
-        elif nx_edge0[0] == nx_edge1[1]:
-            nodes = nx_edge0[1], nx_edge0[0], nx_edge1[0]
-        else:
-            raise ValueError(f"edges should have a common node {nx_edge0} {nx_edge1}")
-        xy0 = AmiUtil.float_list(self.get_or_create_centroid_xy(nodes[0]))
-        xyc = AmiUtil.float_list(self.get_or_create_centroid_xy(nodes[1]))
-        xy1 = AmiUtil.float_list(self.get_or_create_centroid_xy(nodes[2]))
-
-        angle = AmiUtil.get_angle(xy0, xyc, xy1)
-        return angle
-
-    # AmiGraph
-
-    def get_or_create_centroid_xy(self, node_id):
-        """
-        gets centroid from nx_graph.nodes[node_id]
-        :return:
-        """
-        self.centroid_xy = AmiUtil.get_xy_from_sknw_centroid(
-            self.nx_graph.nodes[node_id][AmiNode.CENTROID])
-        return self.centroid_xy
-
-    @classmethod
-    def get_node_ids_from_graph_with_degree(cls, nx_graph, degree):
-        """
-        iterates over graph.nodes to find those with given degree
-        graph may be a subgraph
-        :param nx_graph: might be a subgraph
-        :param degree: connnectivity
-        :return: list of node_ids (may be empty)
-        """
-        assert type(nx_graph) is nx.Graph or type(nx_graph) is nx.MultiGraph, f"not a graph {type(nx_graph)} "
-        return [node_id for node_id in nx_graph.nodes if nx_graph.degree(node_id) == degree]
-
-    # def get_node_ids_with_degree(self, degree):
-    #     """
-    #     iterates over graph.nodes to find those with given degree
-    #     graph may be a subgraph
-    #     :param degree:
-    #     :return: list of node_ids (may be empty)
-    #     """
-    #     return AmiGraph.get_node_ids_from_graph_with_degree(self.nx_graph, degree)
-    #
-    def create_ami_nodes_from_ids(self, node_ids):
-        """
-        creates a list of AmiNodes from a list of node_ids
-
-        :param node_ids:
-        :return: list of AmiNodes
-        """
-        return [self.get_or_create_ami_node(node_id) for node_id in node_ids]
-
-    def extract_aligned_node_lists(self, node_ids, pixel_error):
-        """Lists of horizontal, vertical, and other node-node edges
-
-        checks x- and y- coords of connected nodes to propose connections parallel to axes
-        :param node_ids: list of node_ids to analyse
-        :param pixel_error: max deviation from orthogonality in pixels
-        :return: list of horizontal, vectrical and other node-node connections
-        """
-        vertical_lines = []
-        horizontal_lines = []
-        non_hv_lines = []
-        # TODO need to iterate over edges. Get (start, end, branch)
-        for node_id in node_ids:
-            ami_node = self.get_or_create_ami_node(node_id)
-            node_xy = ami_node.centroid_xy
-            for neighbour_id in ami_node.get_neighbour_ids():
-                # only add each line once
-                if node_id < neighbour_id:
-                    # ami_edge = self.get_or_create_ami_edge(node_id, neighbour_id)
-                    neighbour_xy = self.get_or_create_ami_node(neighbour_id).centroid_xy
-                    line = AmiLine([node_xy, neighbour_xy])
-                    # line.set_ami_edge(ami_edge)
-                    if abs(neighbour_xy[0] - node_xy[0]) <= pixel_error:
-                        vertical_lines.append(line)
-                    elif abs(neighbour_xy[1] - node_xy[1]) <= pixel_error:
-                        horizontal_lines.append(line)
-                    else:
-                        non_hv_lines.append(line)
-        return horizontal_lines, vertical_lines, non_hv_lines
-
-    # AmiGraph
-
-    def get_unique_ami_edges_and_multibranches(self, node_ids):
-        """gets all unique edges including multibranches
-        only includes edges where start_id < end_id
-        :param node_ids: node_ids
-        :return: unique_edges, multibranches
-        """
-        # TODO have to sort out multiple branches to neighbours
-        unique_ami_edges = []
-        multibranches = []
-        for central_node_id in node_ids:
-            ami_node = self.get_or_create_ami_node(central_node_id)
-            for neighbour_id in ami_node.get_neighbour_ids():
-                ami_edges = self.get_ami_edge_list_for_start_end(central_node_id, neighbour_id)
-                # uniquify by  adding edges if start_id < end_id
-                if len(ami_edges) > 1:
-                    for ami_edge in ami_edges:
-                        if ami_edge.has_start_lt_end():
-                            multibranches.append(ami_edge)
-                for ami_edge in ami_edges:
-                    if ami_edge.has_start_lt_end():
-                        unique_ami_edges.append(ami_edge)
-        return unique_ami_edges, multibranches
-
-    def get_or_create_ami_edge_for_start_end_branch(self, start_id, end_id, branch_id):
-        """gets edge from ids
-        :param start_id:
-        :param end_id:
-        :param branch_id:
-        """
-        if self.nx_graph.is_multigraph:
-            edge_id = (start_id, end_id, branch_id)
-            self.get_or_create_ami_edge_dict()
-            if edge_id in self.ami_edge_dict:
-                ami_edge = self.ami_edge_dict[edge_id]
-            else:
-                ami_edge = self.get_or_create_ami_edge_from_ids(start_id, end_id, branch_id=branch_id)
-                self.ami_edge_dict[edge_id] = ami_edge
-        else:
-            # assume simple undirected graph - not fully tested
-            edge_id = (start_id, end_id)
-            ami_edge = self.get_or_create_ami_edge_from_ids(start_id, end_id)
-            self.ami_edge_dict[edge_id] = ami_edge
-        return ami_edge
-
-    def get_or_create_ami_edge_dict(self):
-        """ensures ami_graph.edge_dict exists
-        :return: self.ami_edge_dict
-        """
-        if self.ami_edge_dict is None:
-            self.ami_edge_dict = dict()
-        return self.ami_edge_dict
-
-    # ================================================
-
-    @classmethod
-    def assert_nodes_of_degree(cls, ami_graph, degree, node_id_list):
-        """
-        assert that nodes of given degree equal expected
-        :param ami_graph:
-        :param node_id_list: expected node_ids
-        :param degree: of nodes
-        :return:
-        """
-        assert ami_graph.get_node_ids_of_degree(degree) == node_id_list, \
-            f"expected nodes of degree {degree} +> {node_id_list}, found {ami_graph.get_node_ids_of_degree(degree)}"
-
-
-class AmiGraphError(Exception):
-    def __init__(self, msg):
-        super().__init__(msg)
-
-
-if __name__ == '__main__':
-    pass
-
-"""
-==========================================================================
-===============================EDGE=======================================
-==========================================================================
-"""
-
-"""wrapper for edge from sknw/nx, still being developed"""
-
-
-class AmiEdge:
-    PTS = "pts"
-
-    def __init__(self, ami_graph, start_id, end_id, branch_id=None, _private=False):
-        """ Only for initial development
-        Do not use this, but various ami_graph.create_edge instead
-        """
-        if not _private:
-            raise ValueError(f"Do not call AmiEdge() directly, use AmiGraph.get_or_create_edge*()")
-        self.ami_graph = ami_graph
-        self.start_id = start_id
-        self.end_id = end_id
-        self.points_xy = None
-        self.bbox = None
-        if ami_graph.nx_graph.is_multigraph():
-            if branch_id is None:
-                branch_id = 0
-                # raise ValueError("branch_id for {self.start} {self.end} should be set")
-            self.branch_id = branch_id
-        self.nx_graph = ami_graph.nx_graph
-        self.line_points = None
-        self.tolerance = 1
-        self.segments = None
-        self.get_points()
-
-    def __eq__(self, other):
-        """equality is based on node_ids and branch_id alone
-        later we want to add this object to the nx_graph which will guarantee uniqueness
-        :param other: another edge
-        """
-        if not isinstance(other, type(self)):
-            return False
-        if self.branch_id != other.branch_id:
-            return False
-        if self.start_id == other.start_id and self.end_id == other.end_id:
-            return True
-        # if self.start_id == other.end_id and self.end_id == other.start_id:
-        #     return True
-        return False
-
-    # class AmiEdge:
-
-    def __hash__(self):
-        """uses node_id"""
-        return hash(self.start_id) + hash(self.end_id)
-
-    def get_tuple(self):
-        """
-        get edge as (node_id, node_id2, branch_id)
-
-        :return: (self.start, self.end, self.branch_id)
-        """
-        return self.start_id, self.end_id, self.branch_id
-
-    def get_id(self):
-        """
-        unique id based on start, end and branch
-
-        :return: <start>_<end>_<branch>
-        """
-        return f"{self.start_id}_{self.end_id}_{self.branch_id}"
-
-    def get_points(self):
-        assert self.start_id is not None
-        assert self.end_id is not None
-        edges = self.nx_graph[self.start_id][self.end_id]
-        assert edges is not None
-        if self.nx_graph.is_multigraph():
-            assert self.branch_id is not None
-            try:
-                edge = edges[self.branch_id]
-                points = edge[self.PTS]
-            except KeyError as e:
-                logger.error(f"{__name__} key error {self.branch_id} {e}")
-                raise e
-        else:
-            points = edges[self.PTS]
-        if points is not None:
-            self.read_nx_edge_points_yx_into_self_points_xy(points)
-        return points
-
-    # class AmiEdge:
-
-    def get_cartesian_length(self):
-        assert self.ami_graph is not None
-        start_node = self.ami_graph.get_or_create_ami_node(self.start_id)
-        xy0 = start_node.centroid_xy
-        end_node = self.ami_graph.get_or_create_ami_node(self.end_id)
-        xy1 = end_node.centroid_xy
-        dist = AmiUtil.get_dist(xy0, xy1)
-        assert dist is not None
-        return dist
-
-    def read_nx_edge_points_yx_into_self_points_xy(self, points_array_yx):
-        """
-        convert from nx_points (held as yarray, xarray) to array(x, y)
-        :param points_array_yx:
-        :return:
-        """
-        assert type(points_array_yx) is np.ndarray, \
-            f"points must be numpy array from sknw, found {type(points_array_yx)}"
-        # points are in separate columns (y, x)
-        # TODO reshape this better - use np.flip
-        assert points_array_yx is not None and points_array_yx.ndim == 2 and points_array_yx.shape[1] == 2
-        self.points_xy = []
-        for point in points_array_yx:
-            self.points_xy.append([int(point[1]), int(point[0])])
-
-    # class AmiEdge:
-
-    def __repr__(self):
-        s = ""
-        if self.points_xy is not None:
-            ll = int(self.pixel_length() / 2)
-            s = f"ami_edge {self.start_id}...{self.end_id} ({self.pixel_length()}) " \
-                f"{self.points_xy[:1]}___{self.points_xy[ll - 0:ll + 1]}___{self.points_xy[-1:]}"
-        return s
-
-    def has_start_lt_end(self):
-        """True if start_id is numerically less than end_id
-        Used to prune duplicate edges (s, e) and (e,s)
-        """
-        return self.start_id < self.end_id
-
-    def pixel_length(self):
-        """
-        returns len(self.points_xy)
-        :return:
-        """
-        return len(self.points_xy)
-
-    def remote_node_id(self, node_id):
-        """
-        gets node_id at other end of edge
-        :param node_id:
-        :return: id of node at other end (None if node_id is None or not in edge)
-        """
-        if node_id is None:
-            return None
-        elif node_id == self.start_id:
-            return self.end_id
-        elif node_id == self.end_id:
-            return self.start_id
-        else:
-            return None
-
-    # class AmiEdge:
-
-    def get_or_create_bbox(self):
-        if self.bbox is None and self.points_xy is not None:
-            self.bbox = BBox()
-            for point in self.points_xy:
-                self.bbox.add_coordinate(point)
-
-        logger.debug(f"bbox {self.bbox}")
-        return self.bbox
-
-    def create_line_segments(self, tolerance=1):
-        """create AmiLine segments from sknw points
-        :param tolerance: totlerance in pixels
-        :return: array of lines
-        """
-
-        points = self.nx_graph[self.start_id][self.end_id][self.branch_id][AmiEdge.PTS]
-        points2 = approximate_polygon(points, tolerance=tolerance)
-        return points2
-
-    def find_single_line(self, tol=1) -> AmiLine:
-        """segments the edge into straight lines (AmiLine)
-
-        If segmentation gives a single line (of any orientation) returns it
-        else None
-        :param tol: max deviation of points from segments , def = 1
-        :return: AmiLine or None.
-        """
-        segments = self.get_segments(tol)
-        return segments[0] if len(segments) == 1 else None
-
-    @classmethod
-    def get_single_lines(cls, edges) -> list:
-        """extracts single line from any edges which have one
-        :return: list of AmiLines from edges which have exactly one"""
-        ami_lines = []
-        for ami_edge in edges:
-            ami_line = ami_edge.find_single_line()
-            if ami_line is not None:
-                ami_lines.append(ami_line)
-        return ami_lines
-
-    def get_axial_lines(self, tolerance=1) -> list:
-        """segments the edge into straight lines parallel to axes (AmiLine)
-
-        If All segments are aligned with axes, returns that list else None
-        :param tolerance: max deviation of points from segments
-        :return: list of AmiLines or None.
-        """
-        segments = self.get_segments(tolerance=tolerance)  # maybe cache this
-        if len(segments) > 1:
-            logger.debug(f"segments {len(segments)} ... {self}")
-            corners = self._get_axial_corners(segments, tolerance=tolerance)
-            if len(corners) == len(segments) - 1:
-                return segments
-
-        return None
-
-    @classmethod
-    def get_axial_polylines(cls, edges, tolerance=1) -> list:
-        """extracts axial polylines from any edges which consist of 2 or more axial lines
-        :return: list of polylines (lists of AmiLines) from edges which have 2 or more"""
-        axial_polylines = []
-        for ami_edge in edges:
-            ami_lines = ami_edge.get_axial_lines(tolerance=tolerance)
-            if ami_lines is not None and len(ami_lines) > 1:
-                axial_polylines.append(ami_lines)
-        return axial_polylines
-
-    # class AmiEdge:
-
-    def plot_edge(self, pts, plot_region, edge_id=None, boxcolor=None):
-        """
-        include bbox
-        :param edge_id:
-        :param pts: points in nx_graph format
-        :param plot_region:
-        :param boxcolor: if not None plot edge box in this colour
-        :return:
-        """
-        colors = ["green", "blue", "magenta", "cyan"]
-        # print(f"edge_id {edge_id} pts: {pts[:5]}")
-
-        if boxcolor is not None:
-            bbox = self.get_or_create_bbox()
-            AmiGraph.add_bbox_rect(plot_region, bbox, linewidth=1, edgecolor=boxcolor, facecolor="none")
-        edgecolor = colors[0] if edge_id is None else colors[edge_id % len(colors)]
-        plt.plot(pts[:, 1], pts[:, 0], edgecolor)
-
-    # class AmiEdge:
-
-    def get_segments(self, tolerance=1):
-        """extracts line segments by skimage.measure.approximate_polygon() (Douglas-Peucker)
-
-        :param tolerance: max deviation of curve from line segments
-        :return: list of line segments as AmiLines
-        """
-        points_array = np.array(self.get_coords())
-
-        self._get_or_create_segment_points(points_array, tolerance)
-        self.segments = []
-        for i, _ in enumerate(self.line_points[:-1]):
-            pt0 = self.line_points[i]
-            pt1 = self.line_points[i + 1]
-            ami_line = AmiLine([pt0, pt1])
-            self.segments.append(ami_line)
-        return self.segments
-
-    def _get_or_create_segment_points(self, points_array, tolerance):
-        """calculate segments with approximate_polygon()
-        recalculate if tolerance changes"""
-        if self.line_points is None or tolerance != self.tolerance:
-            self.line_points = approximate_polygon(points_array, tolerance)
-            self.tolerance = tolerance
-        return self.line_points
-
-    def get_single_segment(self, segments=None, tolerance=1):
-        """get edge as a single segment
-        :return: a single segment if D-P finds it within self.tolerance, else None
-        """
-        segments = self.get_segments(tolerance=tolerance) if segments is None else segments
-        return segments[0] if len(segments) == 1 else None
-
-    def is_horizontal(self, tolerance=1):
-        segment = self.get_single_segment(tolerance=tolerance)
-        return segment is not None and segment.is_horizontal(tolerance=tolerance)
-
-    def is_vertical(self, tolerance=1):
-        segment = self.get_single_segment(tolerance=tolerance)
-        return segment is not None and segment.is_vertical(tolerance=tolerance)
-
-    def _get_axial_corners(self, segments, tolerance):
-        """
-        finds Hor-Vert and Vert-Hor corners in segments
-        :param tolerance:
-        :return: list of corners (last_segment, next_segment)
-        """
-        last_segment = None
-        corners = []
-        for segment in segments:
-            if last_segment:
-                lvert = last_segment.is_vertical(tolerance=tolerance)
-                thoriz = segment.is_horizontal(tolerance=tolerance)
-                lhoriz = last_segment.is_horizontal(tolerance=tolerance)
-                tvert = segment.is_vertical(tolerance=tolerance)
-                if (lvert and thoriz) or (tvert and lhoriz):
-                    corners.append((last_segment, segment))
-            last_segment = segment
-        return corners
-
-    @classmethod
-    def get_vertical_edges(cls, ami_edges, tolerance=1):
-        return list(filter(lambda ami_edge: ami_edge.is_vertical(tolerance=tolerance), ami_edges))
-
-    @classmethod
-    def get_horizontal_edges(cls, ami_edges, tolerance=1):
-        return list(filter(lambda ami_edge: ami_edge.is_horizontal(tolerance=tolerance), ami_edges))
-
-    # =========================================
-
-    @classmethod
-    def plot_all_lines(cls, nx_graph, lines, tolerance):
-        """
-        plots edges as lines
-        compare with above, maybe merge
-        :param nx_graph:
-        :param lines: to plot - where from?
-        :param tolerance:
-        :return:
-        """
-        assert type(lines) is list, f"lines should be list {lines}"
-        fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(9, 4))
-        ax1.set_aspect('equal')
-        ax2.set_aspect('equal')
-
-        for line in lines:
-            # fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(9, 4))
-            for i, j in line:
-                assert type(i) is int, f"i should be int {type(i)}"
-                assert type(j) is int, f"j should be int {type(j)}"
-                AmiEdge.douglas_peucker_plot_line(nx_graph, i, j, tolerance, ax1, ax2)
-            plt.show()
-
-    @classmethod
-    def douglas_peucker_plot_line(cls, nx_graph, i, j, tolerance, ax1, ax2):
-        """this may be replaced by create_ami_lines()"""
-        points = nx_graph[i][j][AmiEdge.PTS]
-
-        # original wiggly line
-        # x and y are reversed in sknw
-        ax1.plot(points[:, 1], -points[:, 0])  # negative since down the page
-        points2 = approximate_polygon(points, tolerance=tolerance)
-
-        # the line is not directed so find which end fits which node is best
-        distij = cls.move_line_ends_to_closest_node(nx_graph, (i, j), points2, move=False)
-        distji = cls.move_line_ends_to_closest_node(nx_graph, (j, i), points2, move=False)
-        ij = (i, j) if distij < distji else (j, i)
-        AmiEdge.move_line_ends_to_closest_node(nx_graph, ij, points2, move=True)
-        ax2.plot(points2[:, 1], -points2[:, 0])
-
-    # class AmiEdge:
-
-    @classmethod
-    def move_line_ends_to_closest_node(cls, nx_graph, ij, points, move=False):
-        pts = [points[0], points[-1]]
-        node_pts = [nx_graph.nodes[ij[0]][AmiNode.CENTROID], nx_graph.nodes[ij[1]][AmiNode.CENTROID]]
-        delta_dist = None
-        if move:
-            # print(f"line end {points[0]} moved to {node_pts[0]}")
-            points[0] = node_pts[0]
-            # print(f"line end {points[-1]} moved to {node_pts[1]}")
-            points[-1] = node_pts[1]
-        else:
-            delta_dist = math.dist(pts[0], node_pts[0]) + math.dist(pts[1], node_pts[1])
-
-        return delta_dist
-
-    @classmethod
-    def create_normalized_edge_id_tuple(cls, node_id1, node_id2, branch_id):
-        """ensures that node_id1 <= node_id2
-        This counts each edge only once"""
-        if branch_id is None:
-            return (node_id1, node_id2) if node_id1 <= node_id2 else (node_id2, node_id1)
-        return (node_id1, node_id2, branch_id) if node_id1 <= node_id2 else (node_id2, node_id1, branch_id)
-
-    @classmethod
-    def is_normalized_edge(cls, start_id, end_id):
-        """requires not None and start_id <= end_id
-        :param start_id:
-        :param end_id:
-        """
-        return start_id is not None and end_id is not None and start_id <= end_id
-
-    def get_coords(self):
-        return self.points_xy
-
-    # def douglas_peucker2(self):
-    #     """https://towardsdatascience.com/simplify-polylines-with-the-douglas-peucker-algorithm-ac8ed487a4a1
-    #     Experimental - may not keep
-    #     """
-    #     import matplotlib.animation as animation
-    #     import numpy as np
-    #
-    #     def rdp(points, epsilon):
-    #         # get the start and end points
-    #         start = np.tile(np.expand_dims(points[0], axis=0), (points.shape[0], 1))
-    #         end = np.tile(np.expand_dims(points[-1], axis=0), (points.shape[0], 1))
-    #
-    #         # find distance from other_points to line formed by start and end
-    #         dist_point_to_line = np.abs(np.cross(end - start, points - start, axis=-1)) / np.linalg.norm(end - start,
-    #                                                                                                      axis=-1)
-    #         # get the index of the points with the largest distance
-    #         max_idx = np.argmax(dist_point_to_line)
-    #         max_value = dist_point_to_line[max_idx]
-    #
-    #         result = []
-    #         if max_value > epsilon:
-    #             partial_results_left = rdp(points[:max_idx + 1], epsilon)
-    #             result += [list(i) for i in partial_results_left if list(i) not in result]
-    #             partial_results_right = rdp(points[max_idx:], epsilon)
-    #             result += [list(i) for i in partial_results_right if list(i) not in result]
-    #         else:
-    #             result += [points[0], points[-1]]
-    #
-    #         return result
-    #
-    #     if __name__ == "__main__":
-    #         min_x = 0
-    #         max_x = 5
-    #
-    #         xs = np.linspace(min_x, max_x, num=200)
-    #         ys = np.exp(-xs) * np.cos(2 * np.pi * xs)
-    #         sample_points = np.concatenate([
-    #             np.expand_dims(xs, axis=-1),
-    #             np.expand_dims(ys, axis=-1)
-    #         ], axis=-1)
-    #
-    #         # First set up the figure, the axis, and the plot element we want to animate
-    #         fig = plt.figure()
-    #         ax = plt.axes(xlim=(min_x, max_x), ylim=(-1, 1))
-    #         plt.xlabel("x")
-    #         plt.ylabel("y")
-    #         text_values = ax.text(
-    #             0.70,
-    #             0.15,
-    #             "",
-    #             transform=ax.transAxes,
-    #             fontsize=12,
-    #             verticalalignment='top',
-    #             bbox=dict(boxstyle='round',
-    #                       facecolor='wheat',
-    #                       alpha=0.2)
-    #         )
-    #         original_line, = ax.plot(xs, ys, lw=2, label=r"$y = e^{-x}cos(2 \pi x)$")
-    #         simplified_line, = ax.plot([], [], lw=2, label="simplified", marker='o', color='r')
-    #
-    #         # initialization function: plot the background of each frame
-    #         def init():
-    #             simplified_line.set_data(xs, ys)
-    #             return original_line, simplified_line, text_values
-    #
-    #         # animation function.  This is called sequentially
-    #         def animate(i):
-    #             epsilon = 0 + (i * 0.1)
-    #             simplified = np.array(rdp(sample_points, epsilon))
-    #             print(f"i: {i}, episilon: {'%.1f' % epsilon}, n: {simplified.shape[0]}")
-    #             simplified_line.set_data(simplified[:, 0], simplified[:, 1])
-    #             text_values.set_text(fr"$\epsilon$: {'%.1f' % epsilon}, $n$: {simplified.shape[0]}")
-    #             return original_line, simplified_line, text_values
-    #
-    #         # call the animator.  blit=True means only re-draw the parts that have changed.
-    #         anim = animation.FuncAnimation(
-    #             fig,
-    #             animate,
-    #             init_func=init,
-    #             frames=21,
-    #             interval=1000,
-    #             repeat=True
-    #         )
-    #         plt.legend()
-    #         plt.show()
-
-
-"""a wrapper for an sknw/nx node, still being developed"""
-
-"""
-==========================================================================
-===============================NODE=======================================
-==========================================================================
-"""
-
-
-class AmiNode:
-    """Node holds coordinates
-    ["o"] for centrois (AmiNode.CENTROID)
-    ["pts"] for multiple points (AmiNode.POINTS)
-    ALL COORDINATES COMMUNICATED BY/TO USER ARE X,Y
-    (SKNW uses y,x coordinates)
-    """
-    CENTROID = "o"
-    NODE_PTS = "pts"
-    NEXT_ANG = "next_angle"
-    PIXLEN = "pixlen"
-    REMOTE = "remote"
-
-    def __init__(self, node_id, ami_graph=None, nx_graph=None, _private=False):
-        """
-
-        :param node_id: mandatory
-        :param ami_graph: will use ami_graph.nx_graph
-        :param nx_graph: else will use nx_graph
-        """
-        if not _private:
-            raise ValueError("Do not call AmiNode directly; ise ami_graph.get_or_create_node*() factories")
-        if node_id is None:
-            raise ValueError("AmiNode must have node_id")
-        if len(str(node_id)) > 4:
-            print(f"ami_graph {ami_graph}, nx_graph {nx_graph}")
-            raise Exception(f"id should be simple {node_id}")
-
-        self.ami_graph = ami_graph
-        self.nx_graph = nx_graph
-
-        if nx_graph is None and self.ami_graph is not None:
-            self.nx_graph = self.ami_graph.nx_graph
-        assert self.nx_graph is not None
-        assert node_id is not None
-
-        self.centroid_xy = None if self.nx_graph is None \
-            else AmiUtil.get_xy_from_sknw_centroid(self.nx_graph.nodes[node_id][self.CENTROID])
-        if self.centroid_xy is None:
-            raise ValueError(f"Null centroid {node_id}")
-        assert self.centroid_xy is not None
-
-        self.coords_xy = None
-        self.node_id = node_id
-        self.edges = None  # is this used?
-        self.node_dict = None  # is this used?
-        self.edge_dict = None
-        self.ami_edges = None
-
-    def __eq__(self, other):
-        if not isinstance(other, type(self)):
-            return False
-        if self.node_id != other.node_id:
-            return False
-        return True
-
-    # class AmiNode:
-
-    def __hash__(self):
-        """uses node_id"""
-        return hash(self.node_id)
-
-    # def read_nx_node(self, node_dict):
-    #     """read dict for node, contains coordinates
-    #     typically: 'o': array([ 82., 844.]), 'pts': array([[ 82, 844]], dtype=int16)}
-    #     dict ket
-    #     """
-    #     self.node_dict = copy.deepcopy(node_dict)
-
-    def set_centroid_yx(self, point_yx):
-        """
-        set point in y,x, format
-        :param point_yx:
-        :return:
-        """
-        self.centroid_xy = [point_yx[1], point_yx[0]]  # note coords reverse in sknw
-        return
-
-    def get_neighbour_ids(self):
-        """
-        returns list of neighbours of current node
-        :return: list of neighbours as node_ids
-        """
-        return list(self.nx_graph.neighbors(self.node_id))
-
-    # class AmiNode:
-
-    def __repr__(self):
-        s = str(self.coords_xy) + "\n" + str(self.centroid_xy)
-        return s
-
-    def __str__(self):
-        s = f"{self.node_id} centroid {self.centroid_xy}"
-        return s
-
-    def get_or_create_ami_edges(self):
-        """
-        get list of AmiEdges
-        :return: list of AmiEdges (empty list if none)
-        """
-        edge_list = self.ami_graph.get_nx_edge_list_for_node(self.node_id)
-        self.ami_edges = [self.ami_graph.get_or_create_ami_edge_from_ids(edge[0], edge[1], branch_id=edge[2])
-                          for edge in edge_list]
-        return self.ami_edges
-
-    def create_edge_property_dict(self):
-        """
-        Retrun a dictionary of the neighbouring edges and their properties
-        :return: dictionary of each edge by id, properties include pixel length and
-        angle to next edge
-        """
-        self.get_or_create_ami_edges()
-        self.edge_dict = {}
-        assert self.node_id is not None
-        for i, ami_edge in enumerate(self.ami_edges):
-            self._add_edge_to_dict(ami_edge, i)
-        logger.debug(f"dict: {self.edge_dict}")
-        return self.edge_dict
-
-    def _add_edge_to_dict(self, ami_edge, i):
-        logger.debug(f"edge {i}: {ami_edge}")
-        remote_node_id = ami_edge.remote_node_id(self.node_id)
-        assert remote_node_id is not None
-        pixel_len = ami_edge.pixel_length()
-        self.edge_dict[ami_edge.get_id()] = {self.PIXLEN: pixel_len}
-        # get interedge angles
-        self._add_next_edge(ami_edge, remote_node_id, i)
-
-    def _add_next_edge(self, ami_edge, remote_node_id, i):
-        """
-        adds following edge and angle to it
-        (noop if only one edge)
-        :param ami_edge:
-        :param remote_node_id:
-        :param i:
-        :return:
-        """
-        if len(self.ami_edges) > 1:
-            next_edge = self.ami_edges[(i + 1) % len(self.ami_edges)]
-            next_node_id = next_edge.remote_node_id(self.node_id)
-            assert next_node_id is not None
-            logger.debug("angle ", (remote_node_id, self.node_id, next_node_id))
-            angle = self.ami_graph.get_angle_between_nodes(remote_node_id, self.node_id, next_node_id)
-            self.edge_dict[ami_edge.get_id()][self.NEXT_ANG] = angle
-            self.edge_dict[ami_edge.get_id()][self.REMOTE] = remote_node_id
-
-    @classmethod
-    def get_xy_for_node_id(cls, nx_graph, node_id):
-        return nx_graph.nodes[node_id][AmiNode.CENTROID]
-
-
-# =====
-
-
-"""AmiIsland is a set of node_ids that NetwworkX has listed as a "component"""
-
-"""
-==========================================================================
-==============================ISLAND======================================
-==========================================================================
-"""
-
-
-class AmiIsland:
-    """
-    An isolated set of nodes and edges (nx calls them components)
-
-    Not sure whether this should subclass AmiGraph or whether we should repeat / functions
-    """
-
-    def __init__(self, ami_graph, node_ids=None, make_edges=True):
-        """creates island from nx_graph components and pupulates with nodes and edges
-
-        :param ami_graph: to create from (mandatory)
-        :param node_ids: node ids
-        :param make_edges: uses nx_graph edges to create list of edge ids
-
-        """
-        self.id = None
-        self.node_ids = node_ids
-        self.edge_ids = None
-        self.ami_graph = ami_graph
-
-        self.island_nx_graph = self.create_island_sub_graph()
-        assert self.island_nx_graph is not None
-        self.coords_xy = None
-        self.bbox = None
-        self.nx_edges = None
-        # self.degree_dict = None
-        self.node_degree_dict = None
-        self.ami_edges = None
-        if make_edges:
-            self.create_nx_edges()
-
-    def create_island_sub_graph(self, deep_copy=False):
-        """
-        from NetworkX https://networkx.org/documentation/stable/reference/classes/generated/networkx.Graph.subgraph.html
-        essentially a deep copy of the graph components
-        its island_nx_graph can be passed to class methods in AmiGraph
-        :return: self.island_nx_graph
-        """
-        if deep_copy:
-            nx_graph = self.ami_graph.nx_graph
-            largest_wcc = self.node_ids
-            subgraph = nx_graph.__class__()
-            subgraph.add_nodes_from((n, nx_graph.nodes[n]) for n in self.node_ids)
-            if subgraph.is_multigraph():
-                subgraph.add_edges_from((n, nbr, key, d)
-                                        for n, nbrs in nx_graph.adj.items() if n in largest_wcc
-                                        for nbr, keydict in nbrs.items() if nbr in largest_wcc
-                                        for key, d in keydict.items())
-            else:
-                subgraph.add_edges_from((n, nbr, d)
-                                        for n, nbrs in nx_graph.adj.items() if n in largest_wcc
-                                        for nbr, d in nbrs.items() if nbr in largest_wcc)
-            subgraph.graph.update(nx_graph.graph)
-            self.island_nx_graph = subgraph
-        else:
-            # shallow subgraph (shares attributes with main nx_graph
-            assert self.ami_graph is not None, f"Null ami_graph"
-            assert self.ami_graph.nx_graph is not None, f"Null nx_graph"
-            self.island_nx_graph = self.ami_graph.nx_graph.subgraph(self.node_ids)
-            assert self.island_nx_graph is not None
-
-        return self.island_nx_graph
-
-    def __str__(self):
-        s = "" + \
-            f"node_ids: {self.node_ids}; \n" + \
-            "\n"
-        return s
-
-    def get_or_create_coords(self):
-        coords = []
-        assert self.ami_graph is not None, "must have AmiGraph"
-        if self.coords_xy is None:
-            for node_id in self.node_ids:
-                yx = self.ami_graph.nx_graph.nodes[node_id][AmiNode.CENTROID]
-                xy = AmiUtil.get_xy_from_sknw_centroid(yx)
-                coords.append(xy)
-
-        return coords
-
-    def get_or_create_bbox(self):
-        """
-        create BBox object if not exists.
-        May give empty box if no coordinates
-        :return: BBox
-        """
-        if self.bbox is None:
-            node_coords_xy = self.get_or_create_coords()
-            self.bbox = BBox()
-            for coord in node_coords_xy:
-                coord[0] = int(coord[0])
-                coord[1] = int(coord[1])
-                self.bbox.add_coordinate(coord)
-
-            for node_id in self.node_ids:
-                edge_list = self.ami_graph.get_nx_edge_list_for_node(node_id)
-                logger.debug(f"iterating over {edge_list}")
-                for edge in edge_list:
-                    ami_edge = self.ami_graph.get_or_create_ami_edge_from_ids(edge[0], edge[1], branch_id=edge[2])
-                    bbox = ami_edge.get_or_create_bbox()
-                    self.bbox = self.bbox.union(bbox)
-
-        logger.debug(f"final {self.bbox}")
-        return self.bbox
-
-    def create_nx_edges(self):
-        """creates self.nx_edges for an island from its node ids and the graph edge generator
-
-        :return: list of graph edges
-        """
-        logger.warning(f"{__name__} Probably should use subgraph instead")
-        assert self.ami_graph.nx_graph is not None
-        nx_graph = self.ami_graph.nx_graph
-        self.nx_edges = []
-        for node_id in self.node_ids:
-            edges = nx_graph.edges(node_id, keys=self.ami_graph.nx_graph.is_multigraph())
-            for e in edges:
-                # normalize edges
-                if AmiEdge.is_normalized_edge(e[0], e[1]):
-                    self.nx_edges.append(e)
-        return self.nx_edges
-
-    def get_node_ids_of_degree(self, node_count):
-        """
-        gets node_ids in island with given count
-        delegates to  AmiGraph.get_node_ids_from_graph_with_degree
-        :param node_count:
-        :return:
-        """
-        return AmiGraph.get_node_ids_from_graph_with_degree(self.island_nx_graph, node_count)
-
-    def get_or_create_ami_edges(self):
-        self.create_nx_edges()
-        self.ami_edges = []
-        for edge in self.nx_edges:
-            ami_edge = self.ami_graph.get_or_create_ami_edge_from_nx_edge(edge)
-            self.ami_edges.append(ami_edge)
-        return self.ami_edges
-
-    @classmethod
-    def get_islands_with_max_dimension_greater_than(cls, max_dim, islands):
-        """
-        get islands where at least one of width and height >= max_dim
-        :param max_dim:
-        :param islands:
-        :return:
-        """
-        return [island for island in islands if island.get_or_create_bbox().max_dimension() >= max_dim]
-
-    @classmethod
-    def get_islands_with_max_min_dimension(cls, min_dim, islands):
-        """
-        get islands where at least one of width and height <= min_dim
-        :param min_dim:
-        :param islands:
-        :return:
-        """
-        return [island for island in islands if island.get_or_create_bbox().min_dimension() <= min_dim]
-
-    def create_node_degree_dict(self):
-        """
-        create diction of form <degree>: <nodes of degree>
-        up to degree == 5 (even 5 is unlikely after thinning)
-        :return:
-        """
-        self.node_degree_dict = {}
-        for degree in range(6):
-            nodes = self.get_node_ids_of_degree(degree)
-            self.node_degree_dict[degree] = nodes
-        return self.node_degree_dict
-
-    def create_edge_property_dikt(self, node_id):
-        """
-        create dictionary for eges connected to island
-        :param node_id:
-        :return:
-        """
-        ami_node = self.ami_graph.get_or_create_ami_node(node_id)
-        edge_dict = ami_node.create_edge_property_dict()
-        return edge_dict
diff --git a/src/pyamiimage/ami_image.py b/src/pyamiimage/ami_image.py
deleted file mode 100644
index 2b36530..0000000
--- a/src/pyamiimage/ami_image.py
+++ /dev/null
@@ -1,365 +0,0 @@
-"""
-Wrappers and other methods for image manipulation
-mainly classmathods, but may need some objects to preserve statementfor expensive functioms
-TODO authors Anuv Chakroborty and Peter Murray-Rust, 2021
-Apache2 Open Source licence
-"""
-from distutils import extension
-import numpy as np
-from skimage import io, color, morphology
-import skimage
-from pathlib import Path
-import os
-import matplotlib.pyplot as plt
-
-
-class AmiImage:
-    """
-    instance and class methods for holding and converting images
-    """
-    def __init__(self):
-        """
-        for when we need to store state
-        :return:
-        """
-        pass
-
-# ========== legacy methods that need integrating
-
-# PMR
-    @classmethod
-    def create_grayscale_from_file(cls, path):
-        """
-        Reads an image from path and creates a grayscale (w. skimage)
-        May throw image exceptions (not trapped)
-        :param path:
-        :return: single channel grayscale
-        """
-        assert path is not None
-        image = io.imread(path)
-        gray_image = cls.create_grayscale_from_image(image)
-        return gray_image
-
-    @classmethod
-    def create_grayscale_from_image(cls, image):
-        """
-        creates grayscale from input image
-        accepts following types:
-        shape = (*,*,4) assumed rgba and will convert to rgb
-        shape = (*,*,3) assumed rgb and will convert to grayscale
-        shape = (*,*) assumed grayscale or binary, no action
-        :param image:
-        :return: grayscale or possibly binary image
-        """
-        gray = None
-        if cls.has_gray_shape(image):
-            gray = image
-        elif cls.has_alpha_channel_shape(image):
-            image = color.rgba2rgb(image)
-
-        if gray is None and AmiImage.has_rgb_shape(image):
-            gray = color.rgb2gray(image)
-
-        gray = skimage.img_as_ubyte(gray)
-        return gray
-
-
-    @classmethod
-    def create_rgb_from_rgba(cls, image_rgba):
-        assert cls.has_alpha_channel_shape(image_rgba)
-        image_rgb = color.rgba2rgb(image_rgba)
-        assert not cls.has_alpha_channel_shape(image_rgb), f"converted rgb should have lost alpha channel"
-        assert cls.has_rgb_shape(image_rgb), f"converted rgb does not have rgb_shape"
-        return image_rgb
-
-    @classmethod
-    def create_inverted_image(cls, image):
-        """Inverts the brightness values of the image
-        uses skimage.util.invert
-        not yet tested
-        :param image: ype not yet defined
-        :return: inverted image
-        """
-        inverted = skimage.util.invert(image)
-        return inverted
-
-
-    @classmethod
-    def create_white_skeleton_from_file(cls, path):
-        """
-        the image may be inverted so the highlights are white
-        :param path: path with image
-        :return: AmiSkeleton
-        """
-        # image = io.imread(file)
-        assert path is not None
-        assert path.exists() and not path.is_dir(), f"{path} should be existing file"
-
-        # grayscale = AmiImage.create_grayscale_from_file(path)
-        # assert grayscale is not None, f"cannot create grayscale image from {path}"
-        # skeleton_image = AmiImage.create_white_skeleton_from_image(grayscale)
-        image = io.imread(path)
-        # print(f"path {path} has shape: {image.shape}")
-        # print(f"AmiImage.has_alpha_channel_shape() {AmiImage.has_alpha_channel_shape(path)} for {path} ")
-        skeleton_image = cls.create_white_skeleton_from_image(image)
-
-        return skeleton_image
-
-    @classmethod
-    def create_white_skeleton_from_image(cls, image):
-        """
-        create skeleton_image based on white components of image
-        :param image:
-        :return: skeleton image
-        """
-        assert image is not None
-
-        binary = AmiImage.create_white_binary_from_image(image)
-        binary = binary/255
-        mask = morphology.skeletonize(binary)
-        skeleton = np.zeros(image.shape)
-        skeleton[mask] = 255
-        return skeleton
-
-    @classmethod
-    def create_white_binary_from_file(cls, path):
-        assert path is not None
-        image = io.imread(path)
-        binary = AmiImage.create_white_binary_from_image(image)
-        return binary
-
-    @classmethod
-    def create_white_binary_from_image(cls, image, threshold=None):
-        """"Returns a binary image using a threshold value
-        threshold defaults to skimage.filters.threshold_otsu
-        :param image: grayscale (0-255?)
-        :param threshold: integer or float?
-        :return: integer 0/1 binary image
-        """
-        # check if image is grayscale
-        if len(image.shape) > 2:
-            # convert to grayscale if not grayscale
-            image = cls.create_grayscale_from_image(image)
-
-        # if no threshold is provided, assume default threshold: otsu
-        if threshold is None:
-            threshold = skimage.filters.threshold_otsu(image)
-
-        binary_image = np.where(image >= threshold, 255, 0)
-        return binary_image
-
-    @classmethod
-    def invert_binarize_skeletonize(cls, image):
-        """Inverts Thresholds and Skeletonize a single channel grayscale image
-        :show: display images for invert, threshold and skeletonize
-        :return: skeletonized image
-        """
-        inverted_image = cls.create_inverted_image(image)
-        # skeletonize has inbuilt binarization
-        skeleton = cls.create_white_skeleton_from_image(inverted_image)
-        assert np.max(skeleton) == 255, f"skeleton should have max 255 , found {np.max(skeleton)}"
-
-        return skeleton
-
-    @classmethod
-    def check_binary_or_grayscale(cls, gray_image, image):
-        if cls.heuristic_check_binary(image):
-            pass
-        elif cls.check_grayscale(image):
-            pass
-        else:
-            raise ValueError(f"not a gray or binary image {image.shape}")
-        assert not (gray_image.dtype == np.floating and np.max(gray_image) == 1.0) and \
-               not (gray_image.dtype == int and np.max(gray_image) == 255), \
-            f"checking range {gray_image.dtype} {np.max(gray_image)}"
-
-    @classmethod
-    def has_alpha_channel_shape(cls, image):
-        return type(image) is np.ndarray and len(image.shape) == 3 and image.shape[2] == 4
-
-    @classmethod
-    def has_rgb_shape(cls, image):
-        return type(image) is np.ndarray and len(image.shape) == 3 and image.shape[2] == 3
-
-    @classmethod
-    def has_gray_shape(cls, image):
-        """
-        checks if 2-D ndarray
-        :param image:
-        :return:
-        """
-        return type(image) is np.ndarray and len(image.shape) == 2
-
-    @classmethod
-    def get_image_dtype(cls, image):
-        """get numpy """
-        if type(image) is not np.ndarray or image.size == 0:
-            return None
-        return image.dtype
-
-    @classmethod
-    def heuristic_check_binary(cls, image):
-        """
-        Ths is very hacky, don't rely on it
-        if image is 2-D and max val is 1 or boolean retrun True
-        :param image:
-        :return:
-        """
-        if type(image) is not np.ndarray or len(image.shape) != 2:
-            return False
-        if image.dtype is bool:
-            return True
-        if image.dtype is int:
-            if np.max(image) == 1:
-                return True
-        if image.dtype is np.floating:
-            pass
-        return False
-
-    @classmethod
-    def check_grayscale(cls, image):
-        """
-        Very crude, currently just checks is 2-D and not bool
-        :param image:
-        :return:
-        """
-        if type(image) is not np.ndarray or len(image.shape) != 2:
-            return False
-        if image.dtype is bool:
-            return False
-        return True
-
-    @classmethod
-    def pre_plot_image(cls, image_file, erode_rad=0):
-        """
-        matplotlib plots the gray image, optionally with erosion
-        runs plt.imshow, so will need plt.show() afterwards.
-        :param erode_rad: erosion disk radius (0 => no erode)
-        :param image_file:
-        :return:
-        """
-        assert image_file.exists(), f"{image_file} does not exist"
-        rgb = io.imread(image_file)
-        img = color.rgb2gray(rgb)
-        if erode_rad > 0:
-            disk = morphology.disk(erode_rad)
-            img = morphology.erosion(img, disk)
-        plt.imshow(img, cmap='gray')
-
-    @classmethod
-    def write(cls, path, image, mkdir=False, overwrite=True):
-        """
-        Will throw io errors if cannot write file
-        :param image:
-        :param path:
-        :param mkdir: if True will mkdir for parent
-        :param overwrite: if True will overwrite existing file
-        :return:
-        """
-        path = Path(path)
-        print(f"image: {type(image)}")
-        assert type(image) is np.ndarray and image.ndim >= 2, f"not an image: {type(image)}"
-        if not mkdir:
-            assert path.parent.exists(), f"parent directory must exist {path.parent}"
-        else:
-            path.parent.mkdir()
-        if path.exists() and overwrite:
-            os.remove(path)
-        io.imsave(path, image)
-
-    @classmethod
-    def write_image_group(cls, dir, images, filename="default", mkdir=True, overwrite=True):
-        """write multiple(n) images with the same filename numbered from 1-n"""
-        file_extension = ".png"
-        for index, image in enumerate(images):
-            path = Path(dir, filename + str(index) + file_extension)
-            print(path)
-            AmiImage.write(path, image, mkdir, overwrite)
-
-class AmiImageDTO():
-    """
-    Data Transfer Object for images and downstream artefacts
-    """
-    def __init__(self):
-        self.image = None
-        self.image_binary = None
-        self.nx_graph = None
-        self.ami_graph = None
-        self.hocr = None
-        self.hocr_html_element = None
-
-#    TODO def get_image_type
-#    should return
-#       RGB 0-1
-#       binary 0-255 , etc
-# maybe is library somewhere
-
-"""python REPL commands
-replay these from this directory (resources) to see the distrib of gray values
-sniprgbafile = Path(os.getcwd(), "snippet_rgba.png")
-snipgrayim = io.imread(sniprgbafile)
-sniprgbim = skimage.color.rgba2rgb(sniprgbaim)
-snipgrayim = skimage.color.rgb2gray(sniprgbim)
-plt.hist(snipgray)
-plt.title("gray values")
-plt.ylabel("count")
-plt.xlabel("whiteness")
-plt.show()
-# HISTORY
-import readline; print('\n'.join([str(readline.get_history_item(i + 1)) for i in range(readline.get_current_history_length())]))
-"""
-
-# deprecated
-    # @classmethod
-    # #
-    # def create_white_binary_from_image(cls, image):
-    #     """
-    #     Create a thresholded, binary image from a grayscale
-
-    #     :param image: grayscale image
-    #     :return: binary with white pixels as signal (thresh is discarded)
-    #     """
-    #     gray = AmiImage.create_grayscale_from_image(image)
-    #     binary, thresh = AmiImage.create_auto_thresholded_image_and_value(gray)
-    #     binary = np.invert(binary)
-    #     return binary  # discard thresh
-
-    # @classmethod
-    # # TODO mark deprecated
-    # def create_auto_thresholded_image_and_value(cls, image):
-    #     """
-    #     Thresholded image and (attempt) to get threshold
-    #     The thresholded image is OK but the threshold value may not yet work
-    #     uses skan.pre
-
-    #     :param image: grayscale
-    #     :return: thresholded image, threshold value (latter may not work)
-    #     """
-    #     print(f"shape thresh {image.shape}")
-    #     t_image = threshold(image)
-    #     tt = np.where(t_image > 0)  # above threshold
-    #     return t_image, tt
-
-
-    # @classmethod
-    # def skeletonize(cls, image):
-    #     """Returns a skeleton of the image
-    #     uses skimage.morphology.skeletonize
-
-    #      result white signal black background?
-    #      TODO NOT YET TESTED
-    #      :param image: constraints?
-    #      :return: binary image white signal black background
-    #      """
-    #     mask = morphology.skeletonize(image)
-    #     skeleton = np.zeros(image.shape)
-    #     skeleton[mask] = 255
-    #     return skeleton
-
-        # @classmethod
-    # def create_grayscale_from_image(cls, image):
-    #     # requires 2 separate conversions
-    #     gray_image = cls.create_gray_from_image(image)
-    #     # TODO comment in
-    #     # cls.check_binary_or_grayscale(gray_image, image)
-    #     return gray_image
\ No newline at end of file
diff --git a/src/pyamiimage/ami_island.py-old b/src/pyamiimage/ami_island.py-old
deleted file mode 100644
index 7e173cc..0000000
--- a/src/pyamiimage/ami_island.py-old
+++ /dev/null
@@ -1,68 +0,0 @@
-"""AmiIsland is a set of node_ids that NetwworkX has listed as a "component"""
-
-from pyimage.svg import BBox
-from pyimage.util import Util
-import pyimage.ami_graph
-
-class AmiIsland:
-    def __init__(self):
-        # self.ami_skeleton = None
-        self.node_ids = None
-        self.ami_graph = None
-        self.coords_xy = None
-        self.bbox = None
-
-    def __str__(self):
-        s = "" + \
-            f"node_ids: {self.node_ids}; \n" + \
-            f"coords: {self.coords_xy}\n" + \
-            f"ami_graph: {self.ami_graph}" + \
-            "\n"
-
-        return s
-
-    def get_raw_box(self):
-        bbox = None
-        return bbox
-
-    def get_or_create_coords(self):
-        coords = []
-        if self.coords_xy is None:
-            # pyimage.ami_graph.AmiGraph.debugx(self.ami_graph, "get coords start")
-            for node_id in self.node_ids:
-                print(f"node id {node_id}")
-                print(f"ami_graph {type(self.ami_graph)} {self.ami_graph}")
-                print(f"dir ami_graph {dir(self.ami_graph)}")
-                # print(f"ami_graph.nx_graph nx_graph: {self.ami_graph.nx_graph}")
-                # print(f"ami_graph {type(self.ami_graph)} {self.ami_graph} nx_graph: {self.ami_graph.nx_graph}")
-                nx_graph = self.ami_graph.nx_graph
-                nx_graph = self.ami_graph.get_nx_graph()
-                yx = nx_graph.nodes[node_id]["o"]
-                xy = Util.get_xy_from_sknw_centroid(yx)
-                coords.append(xy)
-            # self.get_or_create_nodes()
-            # self.coords_xy = []
-            # for node in self.nodes:
-            #     coord_xy = node.coords_xy
-            #     self.coords_xy.append(coord_xy)
-
-    # def get_or_create_nodes(self):
-    #     if len(self.nodes) == 0 and self.node_ids is not None:
-    #         self.nodes = [AmiNode(node_id) for node_id in self.node_ids]
-    #     return self.nodes
-        return coords
-
-    def get_or_create_bbox(self):
-        """
-        create BBox object if not exists.
-        May give empty box if no coordinates
-        :return: BBox
-        """
-        if self.bbox is None:
-            coords_xy = self.get_or_create_coords()
-            print(f"coords_xy {coords_xy}")
-            self.bbox = BBox()
-            for coord in coords_xy:
-                self.bbox.add_coordinate(coord)
-        return self.bbox
-
diff --git a/src/pyamiimage/ami_node.py-old b/src/pyamiimage/ami_node.py-old
deleted file mode 100644
index 94d7650..0000000
--- a/src/pyamiimage/ami_node.py-old
+++ /dev/null
@@ -1,67 +0,0 @@
-"""a warpper for an sknw/nx node, still being developed"""
-
-import copy
-from pyimage.util import Util
-
-class AmiNode:
-    """Node holds coordinates
-    ["o"] for centrois (AmiSkeleton.CENTROID)
-    ["pts"] for multiple points (AmiSkeleton.POINTS)
-    ALL COORDINATES COMMUNICATED BY/TO USER ARE X,Y
-    (SKNW uses y,x coordinates)
-    """
-    CENTROID = "o"
-    def __init__(self, node_id=None, ami_graph=None, nx_graph=None):
-        """
-
-        :param node_id: mandatory
-        :param ami_graph: will use ami_graph.nx_graph
-        :param nx_graph: else will use nx_graph
-        """
-
-        if len(str(node_id)) > 4:
-            print(f"ami_graph {ami_graph}, nx_graph {nx_graph}")
-            raise Exception(f"id should be simple {node_id}")
-
-        self.ami_graph = ami_graph
-        self.nx_graph = nx_graph
-        if nx_graph is None and self.ami_graph is not None:
-            self.nx_graph = self.ami_graph.nx_graph
-        self.centroid_xy = None
-        self.node_id = node_id
-
-    def read_nx_node(self, node_dict):
-        """read dict for node, contains coordinates
-        typically: 'o': array([ 82., 844.]), 'pts': array([[ 82, 844]], dtype=int16)}
-        dict ket
-        """
-        self.node_dict = copy.deepcopy(node_dict)
-
-    def get_or_create_centroid_xy(self):
-        """
-        gets centroid from nx_graph.nodes[node_id]
-        :return:
-        """
-        if self.centroid_xy is None and self.nx_graph is not None:
-            assert len(str(self.node_id)) < 4, f"self.node_id {self.node_id}"
-            self.centroid_xy = Util.get_xy_from_sknw_centroid(
-                self.nx_graph.nodes[self.node_id][self.CENTROID])
-        return self.centroid_xy
-
-    def set_centroid_yx(self, point_yx):
-        """
-        set point in y,x, format
-        :param point_yx:
-        :return:
-        """
-        self.centroid_xy = [point_yx[1], point_yx[0]]  # note coords reverse in sknw
-        return
-
-    def __repr__(self):
-        s = str(self.coords_xy) + "\n" + str(self.centroid_xy)
-        return s
-
-    def __str__(self):
-        s = f"centroid {self.centroid_xy}"
-        return s
-    
diff --git a/src/pyamiimage/ami_ocr.py b/src/pyamiimage/ami_ocr.py
deleted file mode 100644
index 07db82b..0000000
--- a/src/pyamiimage/ami_ocr.py
+++ /dev/null
@@ -1,398 +0,0 @@
-from os import path
-from pathlib import Path
-from tkinter import BASELINE
-import numpy as np
-import codecs
-import pytesseract
-from lxml import etree as et
-from PIL import Image
-from skimage import io
-
-from pyimage.ami_image import AmiImage
-try:
-    from pyimage.bbox import BBox
-    from pyimage.cleaner import WordCleaner
-except: 
-    from ..pyimage.bbox import BBox
-    from ..pyimage.cleaner import WordCleaner
-
-class TextBox():
-    # TextBox inherits BBox
-    def __init__(self, text, xy_ranges, baseline=0) -> None:
-        self.text = text
-        self.bbox = BBox(xy_ranges)
-        self.baseline = baseline
-    
-    def __repr__(self): 
-        return f"Textbox({self.text}, {self.bbox.xy_ranges})"
-
-    def __str__(self): 
-        return f"text: {self.text} bbox: {self.bbox.xy_ranges}"
-
-    def __eq__(self, other):
-        if type(other) is not TextBox:
-            return False
-        return self.text == other.text and self.bbox.xy_ranges == other.bbox.xy_ranges
-
-    def set_text(self, text):
-        self.text = text
-
-    def set_bbox(self, bbox):
-        self.bbox = bbox
-
-    
-
-class AmiOCR:
-    TESSERACT_TEMP_PATH = Path(Path(__file__).parent.parent, "temp/tesseract/")
-    def __init__(self, path=None, image=None, filename="default.png") -> None:
-        """Creates an OCR object from path and images, if both path and images are given, path takes precendence"""
-        self.hocr_string = None
-        if path is not None:
-            self.hocr = self.run_ocr(path)
-        elif image is not None:
-            filepath = Path(AmiOCR.TESSERACT_TEMP_PATH, filename)
-            print(filepath)
-            AmiImage.write(filepath, image, mkdir=False)
-            self.hocr = self.run_ocr(filepath)
-        else:
-            self.hocr = None
-        self.words = []
-        self.phrases = []
-        self.groups = []
-
-    def run_ocr(self, path=None):
-        if path is None:
-            return None
-        else:
-            return self.hocr_from_image_path(path)
-
-
-    def get_words(self):
-        if self.words == []:
-            self.words = self.parse_hocr_tree()
-        return self.words
-        
-    def get_phrases(self):
-        if self.phrases == []:
-            self.phrases = self.find_phrases()
-        return self.phrases
-    
-    def get_groups(self):
-        if self.groups == []:
-            self.groups = self.find_word_groups()
-        return self.phrases
-
-    @classmethod
-    def pretty_print_hocr(cls, hocr_element):
-        """Prints html string to console with proper indentation
-        input: object of lxml.etree.Element class
-        returns: None
-        """
-        print(et.tostring(hocr_element, pretty_print=True).decode("utf-8"))
-
-    def hocr_from_image_path(self, path):
-        """Runs tesseract hocr on the given image
-        :param: Path
-        :returns: hocr
-        """
-        # pytesseract only seems to accept string for image path
-        if path is not str:
-            path = str(path)
-
-        self.hocr_string = pytesseract.image_to_pdf_or_hocr(path, extension='hocr', config='11')
-        # self.hocr_string = codecs.decode(self.hocr_string, 'UTF-8')
-        # print(self.hocr_string)
-        hocr = self.parse_hocr_string(self.hocr_string)
-        print(hocr)
-        return hocr
-
-    def parse_hocr_string(self, hocr_string):
-        """Parses hocr output in string format as a tree using lxml
-        :input: hocr html as string
-        :returns: root of hocr as an object of lxml.etree.Element class
-        """
-        parser = et.HTMLParser()
-        root = et.HTML(hocr_string, parser)
-        return root
-
-    def parse_hocr_tree(self, hocr_element=None):
-        """to extract bbox coordinates from html
-        :param hocr_element: lxml.etree.Element object
-        :returns: tuple of (list of bboxes, list of words)
-        numpy array has the format: [x1, y1, x2, y2]
-        where values corresponding to the bounding box are
-        x1,y1 ------
-        |          |
-        |          |
-        |          |
-        --------x2,y2    
-        """
-        if hocr_element is None:
-            hocr_element = self.hocr
-        words = []
-
-        # Each of the bbox coordinates are in span
-        # iterdescendants('span') iterates over all the span elements in the tree
-        for child in hocr_element.iterdescendants('span'):
-            # the class of span for bounding box around each word is 'ocrx_word'
-            if child.attrib['class'] == 'ocrx_word':
-                # coordinates for bbox has the following format: 'bbox 333 74 471 102; x_wconf 76'
-                # we are only interested in the coordinates, so we split at ; and append the first part
-                bbox_string = child.attrib['title'].split(';')[0]
-                xy_range = self.create_xy_range_from_bbox_string(bbox_string)
-                textbox = TextBox(child.text, xy_range)
-                words.append(textbox)
-        self.words = AmiOCR.clean_all(words)
-        return self.words
-    
-    def find_words_from_image_path(self, image_path):
-        self.hocr_from_image_path(image_path)
-        self.parse_hocr_tree(self.hocr)
-        return self.words
-
-    def create_xy_range_from_bbox_string(self, bbox_string):
-        # now we have a list with string elenments like 'bbox 333 74 471 102'
-        # first we convert each string into a list, and then remove 'bbox', leaving just the coordinate
-        bbox_list = bbox_string.split()[1:]
-
-        # finally we convert the coordinates from string to integer
-        bbox_list = [int(coordinate) for coordinate in bbox_list]
-        # bbox_list = [x1, y1, x2, y2]
-        xy_range = [[bbox_list[0],bbox_list[2]], [bbox_list[1], bbox_list[3]]]
-        return xy_range
-
-    def find_phrases(self, hocr_element=None, word_separation=20, min_y_overlap=0.99):
-        """
-        finds phrases and their bboxes from HOCR output
-        adjusts the classification from Tesseract which is often fragile
-        :param hocr_element: output from Tesseract
-        :param word_separation: maximum? separation (in pixels) of words to create phrases
-        :param y_tolerance: maximum tolerance (in pixels) of words on same line
-        :return: tuple (list of phrases, list of boxes)
-        """
-        phrases = []
-        words = self.get_words()
-        
-        for i in range(len(words)):
-            phrase = words[i] # phrase is a TextBox object like word
-            for j in range(i+1, len(words)):
-                # check if words are on the same line
-                # if words on a different line, break loop
-                if not AmiOCR.y_overlap(phrase, words[j]) >= min_y_overlap:
-                    break
-                # If the separation is small enough, add the word to the phrase
-                if AmiOCR.textbox_horizontal_seperation(phrase, words[j]) < word_separation:
-                    phrase.set_text(phrase.text + " " + words[j].text)
-                    phrase.set_bbox(phrase.bbox.union(words[j].bbox))
-                else:
-                    break
-
-            if phrase.text == ' ':
-                # if phrase is empty do not add to the list of phrases
-                continue
-            elif not phrases:
-                # if array is empty add element
-                phrases.append(phrase)
-            elif not phrases[-1].text.endswith(phrase.text):
-                # only add phrase if the last phrase added does not end with current phrase
-                phrases.append(phrase)
-        self.phrases = phrases
-        return phrases
-
-    @classmethod
-    def textbox_horizontal_seperation(cls, textbox_1, textbox_2):
-        """we assume that textbox_1 comes before textbox_2"""
-        return textbox_2.bbox.get_xrange()[0]-textbox_1.bbox.get_xrange()[1]
-
-    @classmethod
-    def textboxes_in_same_line(cls, textbox_1, textbox_2, y_tolerance):
-        textbox_1_yrange = textbox_1.bbox.get_yrange()
-        textbox_2_yrange = textbox_2.bbox.get_yrange()
-        if abs(textbox_1_yrange[0] - textbox_2_yrange[0]) <= y_tolerance and \
-            abs(textbox_1_yrange[1] - textbox_1_yrange[1]) <= y_tolerance:
-            return True
-        else:
-            return False
-
-    def find_word_groups(self, phrases=None, line_seperation=10, min_x_overlap=0.20):
-        """
-        :param bbox_of_phrases: bounding boxes of phrases in an image
-        :type bbox_of_phrases: list
-        :param line_seperation: allowable distance between two lines
-        :type line_seperation: int
-        :param min_x_overlap: ratio of horizontal overlap between two bboxes
-        :type min_x_overlap: float
-        """
-        if phrases == None:
-            phrases = self.get_phrases()
-
-        groups = []
-        
-        for i in range(len(phrases)):
-            group = phrases[i] # group is a TextBox object 
-            for j in range(i+1, len(phrases)):
-                # check if there is significant x range overlap between the boxes
-                if AmiOCR.x_overlap(group, phrases[j]) < min_x_overlap:
-                    break
-                # If the separation is small enough, add the phrase to the group
-                if AmiOCR.textbox_vertical_seperation(group, phrases[j]) < line_seperation:
-                    group.set_text(group.text + " " + phrases[j].text)
-                    group.set_bbox(group.bbox.union(phrases[j].bbox))
-                else:
-                    break
-
-            if group.text == ' ':
-                # if group is empty do not add to the list of groups
-                continue
-            elif not groups:
-                # if array is empty add element
-                groups.append(group)
-            elif not groups[-1].text.endswith(group.text):
-                # only add phrase if the last phrase added does not end with current phrase
-                groups.append(group)
-        return groups
-
-    @classmethod
-    def textbox_vertical_seperation(cls, textbox1, textbox2):
-        bbox1_y = textbox1.bbox.get_yrange()
-        bbox2_y = textbox2.bbox.get_yrange()
-
-        # find which box is top and which box is bottom
-        top_bbox = bbox1_y if bbox1_y[0] < bbox2_y[0] else bbox2_y
-        bottom_bbox = bbox2_y if top_bbox == bbox1_y else bbox1_y
-
-        # if the bounding boxes intersect then we will have a negative difference but that's fine
-        return bottom_bbox[0] - top_bbox[1]
-
-
-
-    @classmethod
-    def y_overlap(cls, textbox1, textbox2):
-        # find which bbox is wider
-        bbox1_y = textbox1.bbox.get_yrange()
-        bbox2_y = textbox2.bbox.get_yrange()
-
-        pixel_overlap = max(0, min(bbox1_y[1], bbox2_y[1]) - max(bbox1_y[0], bbox2_y[0]) + 1)
-        
-        if pixel_overlap == 0:
-            return 0
-
-        shorter_bbox_height = min(textbox1.bbox.get_height(), textbox2.bbox.get_height())
-        
-        overlap = pixel_overlap/shorter_bbox_height
-        return overlap
-
-    @classmethod
-    def x_overlap(cls, textbox1, textbox2):
-        # find which bbox is wider
-        bbox1_x = textbox1.bbox.get_xrange()
-        bbox2_x = textbox2.bbox.get_xrange()
-        pixel_overlap = max(0, min(bbox1_x[1], bbox2_x[1]) - max(bbox1_x[0], bbox2_x[0]) + 1)
-        
-        if pixel_overlap == 0:
-            return 0
-
-        narrower_bbox_width = min(textbox1.bbox.get_width(), textbox2.bbox.get_width())
-        
-        overlap = pixel_overlap/narrower_bbox_width
-        return overlap
-
-    @classmethod
-    def envelope_box(cls, bboxes):
-        # given list of bboxes gives the whole box enveloping all the bboxes
-        min_x, min_y, max_x, max_y = bboxes[0][0], bboxes[0][1], bboxes[0][2], bboxes[0][3] 
-        for bbox in bboxes:
-            if bbox[0] < min_x:
-                min_x = bbox[0]
-            if bbox[1] < min_y:
-                min_y = bbox[1]
-            if bbox[2] > max_x:
-                max_x = bbox[2]
-            if bbox[3] > max_y:
-                max_y = bbox[3]
-        return [min_x, min_y, max_x, max_y]
-
-    @classmethod
-    def plot_bboxes_on_image(self, image, textboxes):
-        """draws bboxes on image 
-        :param: image
-        :type: numpy array
-        :textboxes: array of TextBox objects
-        :returns: image
-        """
-        for textbox in textboxes:
-            try:
-                image = BBox.plot_bbox_on(image, textbox.bbox)
-            except IndexError as e:
-                continue
-        return image
-    
-    def parse_hocr_title(self, title):
-        """
-         title="bbox 336 76 1217 111; baseline -0.006 -9; x_size 28; x_descenders 6; x_ascenders 7"
-
-        :param title:
-        :return:
-        """
-        if title is None:
-            return None
-        parts = title.split("; ")
-        title_dict = {}
-        for part in parts:
-            vals = part.split()
-            kw = vals[0]
-            if kw == self.A_BBOX:
-                val = ((vals[1], vals[3]), (vals[2], vals[4]))
-            else:
-                val = vals[1:]
-            title_dict[kw] = val
-        return title_dict
-
-    def find_baseline(self, hocr_element):
-        if hocr_element is None:
-            hocr_element = self.hocr
-        words = []
-
-        # Each of the bbox coordinates are in span
-        # iterdescendants('span') iterates over all the span elements in the tree
-        for child in hocr_element.iterdescendants('span'):
-            # the class of span for bounding box around each word is 'ocrx_word'
-            if child.attrib['class'] == 'ocr_line':
-                # coordinates for bbox has the following format: 'bbox 333 74 471 102; x_wconf 76'
-                # we are only interested in the coordinates, so we split at ; and append the first part
-                print("pause here")
-                baseline = child.attrib['title'].split(';')[1]
-                bbox_string = child.attrib['title'].split(';')[0]
-                xy_range = self.create_xy_range_from_bbox_string(bbox_string)
-                textbox = TextBox(child.text, xy_range, baseline)
-                words.append(textbox)
-        self.words = AmiOCR.clean_all(words)
-        return self.words
-        
-
-    @classmethod
-    def bounding_box_patches(cls, image, textboxes):
-        """given an image and textboxes, it will extract all the bounding boxes
-        and return a list of numpy arrays"""
-        patches = []
-        for textbox in textboxes:
-            xy_range = textbox.bbox.xy_ranges
-            min_y = xy_range[1][0]
-            max_y = xy_range[1][1]
-            min_x = xy_range[0][0]
-            max_x = xy_range[0][1]
-            print(f"min_y: {min_y}", f"max_y: {max_y}", f"min_x: {min_x}", f"max_x: {max_x}")
-            patch = image[min_y:max_y, min_x:max_x]
-            print(patch.shape)
-            patches.append(patch)
-        
-        return patches
-
-    @classmethod
-    def clean_all(self, textboxes):
-        cleaned = WordCleaner.remove_leading_and_trailing_special_characters(textboxes)
-        cleaned = WordCleaner.remove_all_single_characters(cleaned)
-        cleaned = WordCleaner.remove_all_sequences_of_special_characters(cleaned)
-        cleaned = WordCleaner.remove_misread_letters(cleaned)
-        cleaned = WordCleaner.remove_numbers_only(cleaned)
-        return cleaned
\ No newline at end of file
diff --git a/src/pyamiimage/ami_plot.py b/src/pyamiimage/ami_plot.py
deleted file mode 100644
index f8b629d..0000000
--- a/src/pyamiimage/ami_plot.py
+++ /dev/null
@@ -1,328 +0,0 @@
-import logging
-from collections import Counter
-# local
-from ..pyimage.ami_util import AmiUtil
-
-logger = logging.getLogger(__name__)
-
-HEAD = "head"
-
-DASHED = "dashed"
-POLY = "poly"  # polyline or polygon
-POLYGON = "polygon"
-POLYLINE = "polyline"
-TARGETS = [POLY, POLYGON, POLYLINE]
-
-SEGMENT = "segment"
-TAIL = "tail"
-X = 0
-Y = 1
-
-
-class AmiPlot:
-    pass
-
-
-class AmiLine:
-    """This will probably include a third-party tool supporting geometry for lines
-
-    It may also be a polyline
-    """
-
-    def __init__(self, xy12=None, ami_edge=None):
-        """xy12 of form [[x1, y1], [x2, y2]]
-        direction is xy1 -> xy2 if significant"""
-        self.ami_edge = ami_edge
-        self.xy1 = None
-        self.xy2 = None
-        if xy12 is not None:
-            if len(xy12) != 2 or len(xy12[0]) != 2 or len(xy12[1]) != 2:
-                raise ValueError(f"bad xy pair for line {xy12}")
-            self.xy1 = [xy12[0][X], xy12[0][Y]]
-            self.xy2 = [xy12[1][X], xy12[1][Y]]
-        # ami_nodes = []
-
-    def __repr__(self):
-        return str([self.xy1, self.xy2])
-
-    def __str__(self):
-        return str([self.xy1, self.xy2])
-
-    def set_ami_edge(self, ami_edge):
-        self.ami_edge = ami_edge
-
-    @property
-    def vector(self):
-        """vector between end points
-        :return: xy2 - xy1
-        """
-        return None if (self.xy1 is None or self.xy2 is None) else \
-            (self.xy2[X] - self.xy1[X], self.xy2[Y] - self.xy1[Y])
-
-    @property
-    def xy_mid(self):
-        """get midpoint of line
-        :return: 2-array [x, y] of None if coords not set"""
-
-        if self.xy1 is not None and self.xy2 is not None:
-            return [(self.xy1[X] + self.xy2[X]) / 2, (self.xy1[Y] + self.xy2[Y]) // 2]
-        return None
-
-    def is_horizontal(self, tolerance=1) -> int:
-        return abs(self.vector[Y]) <= tolerance < abs(self.vector[X])
-
-    def is_vertical(self, tolerance=1) -> int:
-        return abs(self.vector[X]) <= tolerance < abs(self.vector[Y])
-
-    @classmethod
-    def get_horiz_vert_counter(cls, ami_lines, xy_index) -> Counter:
-        """
-        counts midpoint coordinates of lines (normally ints)
-
-        :param ami_lines: horiz or vert ami_lines
-        :param xy_index: 0 (x) 0r 1 (y) (normally 0 for vert lines, 1 for horiz)
-        :return: Counter
-        """
-        hv_dict = Counter()
-        for ami_line in ami_lines:
-            xy_mid = ami_line.xy_mid[xy_index]
-            if xy_mid is not None:
-                hv_dict[int(xy_mid)] += 1
-        return hv_dict
-
-    def get_min(self, xy_flag):
-        return None if xy_flag is None else min(self.xy1[xy_flag], self.xy2[xy_flag])
-
-    def get_max(self, xy_flag):
-        return None if xy_flag is None else max(self.xy1[xy_flag], self.xy2[xy_flag])
-
-
-class AmiLineTool:
-    """joins points or straight line segments
-    final target can be:
-    may contain tools for further analysis
-    still being developed.
-    end
-    mode=SEGMENT assembles segments into one of more polylines
-    """
-
-    def __init__(self, tolerance=1, points=None, ami_edge=None, mode=POLYLINE, xy_flag=None):
-        """build higher-order objects frome line segments or points
-        :param tolerance: for determing overlapping points
-        :param points: points to make polyline/gons
-        :param ami_edge: possibly obsolete
-        :param mode: POLYLINE (default)
-        :"""
-        self.point_set = set()
-        self.points = []
-        self.tolerance = tolerance
-        self.mode = mode
-        self.xy_flag = xy_flag
-        if mode not in TARGETS:
-            raise ValueError(f"unknown target {mode}")
-        if ami_edge is not None:
-            raise ValueError("emi_edges not yet supported")
-        if points is not None:
-            for point in points:
-                self.add_point(point)
-        self.polylines = []
-
-    def __repr__(self):
-        return str(self.points)
-
-    def __str__(self):
-        return str(self.points)
-
-    def add_point(self, point):
-        """add point
-        :param point:
-        """
-        self._validate_point(point)
-        # if point in self.point_set:
-        #     raise ValueError("Cannot add point twice {point}")
-        if point in self.points:
-            raise ValueError("Cannot add point twice {point}")
-        self.points.append(point)
-
-    def points(self):
-        return self.points
-
-    def add_segment(self, segment):
-        """segment tail must match head"""
-        self._validate_segment(segment)
-        if self.mode == POLYLINE:
-            self.add_segment_to_poly_list(segment)
-        else:
-            if len(self.points) == 0:
-                self.add_point(segment[0])
-                self.add_point(segment[1])
-            elif self._are_overlapping(self.points[-1], segment[0]):
-                self.add_point(segment[1])
-            else:
-                raise ValueError("Cannot add segment {segment} (no overlap)")
-
-    def add_segments(self, segments):
-        for segment in segments:
-            self.add_segment(segment)
-
-    def insert_segment(self, pos, segment):
-        self._validate_segment(segment)
-        if not self._are_overlapping(segment[1], self.points[0]):
-            raise ValueError("non-overlapping insertion at pos {pos} {segment}")
-        self.points.insert(pos, segment[0])
-
-    def _are_overlapping(self, point1, point2):
-        """
-
-        overlaps if either dx or dy >= self.tolerance
-        :param point1: [x, y]
-        :param point2: [x, y]
-        :return:
-        """
-        return abs(point1[X] - point2[X]) + abs(point1[Y] - point2[Y]) <= self.tolerance
-
-    def _validate_segment(self, segment):
-        assert type(segment) is AmiLine, f"segment was {type(segment)}"
-        self._validate_point(segment.xy1)
-        self._validate_point(segment.xy2)
-
-    def _validate_point(self, point):
-        assert type(point) is list, f"found {type(point)}"
-        assert len(point) == 2, f"found {len(point)}"
-        assert AmiUtil.is_number(point[0])
-
-    def _copy_point(self, point):
-        self._validate_point(point)
-        return [point[0], point[1]]
-
-    def copy_segment(self, segment):
-        return [self._copy_point(segment[0]), self._copy_point(segment[1])]
-
-    def add_points(self, points):
-        assert type(points) is list
-        for point in points:
-            self.add_point(point)
-
-    def insert_point(self, pos, point):
-        self._validate_point(point)
-        self.points.insert(0, [33, 44])
-        self.points.insert(pos, point)
-
-    def add_segment_to_poly_list(self, segment):
-        """add segment, assuming current list is sorted
-        polyline            polyline
-        lo-----hi           lo----------hi
-                   lo---hi
-                   segment
-        """
-        # seg_low = segment.get_min(self.xy_flag)
-        # seg_hi = segment.get_max(self.xy_flag)
-        if self.polylines == []:
-            self.polylines.append(segment)
-            return
-        last_polyline = None
-        xyf = self.xy_flag
-        inserted = False
-        for i, polyline in enumerate(self.polylines):
-            # does it overlap the next exactly?
-            if self._are_overlapping(segment.xy1, polyline.xy1):
-                if last_polyline is None:
-                    # end of first polyline
-                    self.prepend_segment_to_polyline(segment, polyline)
-                    inserted = True
-                elif self._are_overlapping(segment.xy1, last_polyline[1]):
-                    # join in the middle of last_polyline and polyline
-                    # join to predecessor
-                    self.prepend_segment_to_polyline(segment, polyline)
-                    # and then add last
-                    self.append_polyline_to_polyline_and_remove(last_polyline, polyline)
-                    # and clear the last
-                    self.polylines.remove(last_polyline)
-                    inserted = True
-
-            elif segment.xy2[xyf] < polyline[0][xyf]:
-                # falls behind preceding and leads last if any
-                if last_polyline is None or segment.xy0[xyf] > (last_polyline[-1][xyf] + self.tolerance):
-                    self.polylines.insert(i, segment)
-                    inserted = True
-                else:
-                    raise ValueError(f"too big {segment} cannot insert")
-
-            if inserted:
-                break
-
-            last_polyline = polyline
-
-        if not inserted:
-            if self._are_overlapping(last_polyline[-1], segment.xy2):
-                self.polylines.append(last_polyline, segment.xy2)
-            elif segment.xy1[xyf] < last_polyline[1][xyf]:
-                raise f"cannot add segment at head {segment}"
-            else:
-                self.polylines.append(segment)
-
-    def prepend_segment_to_polyline(self, segment, polyline):
-        polyline.insert(0, segment.xy1)
-
-    def append_polyline_to_polyline_and_remove(self, last_polyline, polyline):
-        last_polyline.add(polyline)
-        self.polylines.remove(polyline)
-
-
-class AmiEdgeTool:
-    """refines edges (join, straighten, break, corners, segments, curves, etc some NYI)
-    Still being actively developed
-    """
-
-    def __init__(self, ami_graph=None, ami_edges=None, ami_nodes=None):
-        """Best to create this from the factory method create_tool"""
-        self.ami_graph = ami_graph
-        self.ami_edges = ami_edges
-        self.ami_nodes = ami_nodes
-
-    @classmethod
-    def create_tool(cls, ami_graph, ami_edges=None, ami_nodes=None):
-        """preferred method of instantiating tool
-        :param ami_graph: required graph
-        :param ami_edges: edges to process
-        :param ami_nodes: optional nodes (if none uses ends of edges)
-        """
-        edge_tool = AmiEdgeTool(ami_graph, ami_edges=ami_edges, ami_nodes=ami_nodes)
-        if not ami_nodes:
-            edge_tool.create_ami_nodes_from_edges()
-
-        return edge_tool
-
-    def analyze_topology(self):
-        """counts nodes and edges by recursively iterating over
-        noes and their edges -> edges and their nodes
-        also only includes start_id < end_id
-        (mainly a check)
-
-         :return: nodes, edges"""
-
-        if self.ami_edges is None:
-            logger.error(f"no edges, possible error")
-        new_ami_edges = set()
-        new_ami_nodes = set()
-        while self.ami_nodes:
-            ami_node = self.ami_nodes.pop()
-            new_ami_nodes.add(ami_node)
-            node_ami_edges = ami_node.get_or_create_ami_edges()
-            for ami_edge in node_ami_edges:
-                if ami_edge.has_start_lt_end():
-                    if ami_edge not in self.ami_edges:
-                        print(f" cannot find {ami_edge} in edges")
-                    else:
-                        new_ami_edges.add(ami_edge)
-        return new_ami_nodes, new_ami_edges
-
-    def create_ami_nodes_from_edges(self):
-        """generates unique ami_nodes from node_ids at ends of edges"""
-        if not self.ami_nodes:
-            self.ami_nodes = set()
-            node_ids = set()
-            for ami_edge in self.ami_edges:
-                node_ids.add(ami_edge.start_id)
-                node_ids.add(ami_edge.end_id)
-            self.ami_nodes = self.ami_graph.create_ami_nodes_from_ids(node_ids)
diff --git a/src/pyamiimage/ami_util.py b/src/pyamiimage/ami_util.py
deleted file mode 100644
index 964a643..0000000
--- a/src/pyamiimage/ami_util.py
+++ /dev/null
@@ -1,215 +0,0 @@
-"""
-Utilities (mainly classmethods)
-"""
-from pathlib import Path
-import numpy as np
-import numpy.linalg as LA
-import math
-
-
-class AmiUtil:
-
-    @classmethod
-    def check_type_and_existence(cls, target, expected_type):
-        """
-        asserts not None for object and its type
-        if path asserts existence
-
-
-        :param target: object to check
-        :param expected_type: type of object
-        :return: None
-        """
-        assert target is not None
-        typ = type(target)
-        assert typ is expected_type, f"type {typ} should be {expected_type}"
-        if expected_type is Path:
-            assert target.exists(), f"{target} should exist"
-
-    @classmethod
-    def is_ordered_numbers(cls, limits2):
-        """
-        check limits2 is a numeric 2-tuple in increasing order
-        :param limits2:
-        :return: True tuple[1] > tuple[2]
-        """
-        return limits2 is not None and len(limits2) == 2 \
-               and AmiUtil.is_number(limits2[0]) and AmiUtil.is_number(limits2[1]) \
-               and limits2[1] > limits2[0]
-
-    @classmethod
-    def is_number(cls, s):
-        """
-        test if s is a number
-        :param s:
-        :return: True if float(s) succeeds
-        """
-        try:
-            float(s)
-            return True
-        except ValueError:
-            return False
-
-    @classmethod
-    def get_xy_from_sknw_centroid(cls, yx):
-        """
-        yx is a 2-array and coords need swapping
-        and changing from numpy to int
-        :param yx:
-        :return: [x,y]
-        """
-        assert yx is not None
-        assert len(yx) == 2 and type(yx) is np.ndarray, f"xy was {yx}"
-        return [int(yx[1]), int(yx[0])]
-
-    @classmethod
-    def make_numpy_assert(cls, numpy_array, shape=None, maxx=None, dtype=None):
-        """
-        Asserts properties of numpy_array
-        :param numpy_array:
-        :param shape:
-        :param maxx: max value (e.g. 255, or 1.0 for images)
-        :param dtype:
-        :return:
-        """
-        assert numpy_array is not None, f"numpy array should not be None"
-        assert type(numpy_array) is np.ndarray, \
-            f"object should be numpy.darray, found {type(numpy_array)} \n {numpy_array}"
-        if shape:
-            assert numpy_array.shape == shape, f"shape should be {numpy_array.shape}"
-        if maxx:
-            assert np.max(numpy_array) == maxx, f"max should be {np.max(numpy_array)}"
-        if dtype:
-            assert numpy_array.dtype == dtype, f"dtype should be {numpy_array.dtype}"
-
-    @classmethod
-    def get_angle(cls, p0, p1, p2):
-        '''
-        signed angle p0-p1-p2
-        :param p0:
-        :param p1: centre point
-        :param p2:
-        '''
-        AmiUtil.assert_is_float_array(p0)
-        AmiUtil.assert_is_float_array(p1)
-        AmiUtil.assert_is_float_array(p2)
-
-        linal = False
-        if linal:
-            np0 = np.array(p0, dtype=np.uint8)
-            np1 = np.array(p1, dtype=np.uint8)
-            np2 = np.array(p2, dtype=np.uint8)
-            v0 = np0 - np1
-            v1 = np2 - np1
-            angle = np.math.atan2(np.linalg.det([v0, v1]), np.dot(v0, v1))
-        else:
-            dx0 = p0[0] - p1[0]
-            dy0 = p0[1] - p1[1]
-            v01 = [p0[0] - p1[0], p0[1] - p1[1]]
-            v21 = [p2[0] - p1[0], p2[1] - p1[1]]
-            ang01 = math.atan2(v01[1], v01[0])
-            ang21 = math.atan2(v21[1], v21[0])
-            angle = ang21 - ang01
-            angle = AmiUtil.normalize_angle(angle)
-
-        return angle
-
-    @classmethod
-    def normalize_angle(cls, angle):
-        """
-        normalizes angle to -Pi 0 +Pi
-        :param angle:
-        :return:
-        """
-        if angle > math.pi:
-            angle -= 2 * math.pi
-        if angle <= -math.pi:
-            angle += 2 * math.pi
-        return angle
-
-    @classmethod
-    def get_dist(cls, xy0, xy1):
-        '''
-        length p0-p1
-        :param xy0:
-        :param xy1:
-        '''
-        fxy0 = AmiUtil.to_float_array(xy0)
-        fxy1 = AmiUtil.to_float_array(xy1)
-        if fxy0 is None or fxy1 is None:
-            return None
-        AmiUtil.assert_is_float_array(fxy0)
-        AmiUtil.assert_is_float_array(fxy1)
-
-        dx0 = fxy0[0] - fxy1[0]
-        dy0 = fxy0[1] - fxy1[1]
-        dist = math.sqrt(dx0 * dx0 + dy0 * dy0)
-
-        return dist
-
-
-    @classmethod
-    def assert_is_float_array(cls, arr, length=2):
-        """
-        assert arr[0] is float and has given length
-        :param arr:
-        :param length:
-        :return:
-        """
-        assert arr is not None
-        assert len(arr) == length and type(arr[0]) is float, f"arr must be 2-vector float {arr}"
-
-    @classmethod
-    def float_list(cls, int_lst):
-        """
-        converts a list of ints or np.uint16 or np.uint8 to floats
-        :param int_lst: 
-        :return: 
-        """
-        assert int_lst is not None and type(int_lst) is list and len(int_lst) > 0, f"not a list: {int_lst}"
-        tt = type(int_lst[0])
-        assert tt is int or tt is np.uint8 or tt is np.uint16, f"expected int, got {tt}"
-        return [float(i) for i in int_lst]
-
-    @classmethod
-    def to_float_array(cls, arr):
-        """
-        converts to array of floats if possible
-        :param arr:
-        :return:
-        """
-        if arr is None:
-            return None
-        try:
-            farr = [float(a) for a in arr]
-        except ValueError as e:
-            raise f"cannot convet to float {type(arr[0])}"
-        return farr
-
-    @classmethod
-    def swap_yx_to_xy(cls, yx):
-        return [yx[1], yx[0]]
-
-class Vector2:
-
-    def __init__(self, v2):
-        # self.vec2 = np.array()
-        self.vec2 = v2
-
-    @classmethod
-    def angle_to(cls, vv0, vv1):
-        """
-
-        :param vv0: Vector2
-        :param vv1: Vector2
-        :return: angle(rad) between vectors (maybe unsigned??)
-        """
-        v0 = vv0.vec2
-        v1 = vv1.vec2
-
-        inner = np.inner(v0, v1)
-        norms = LA.norm(v0) * LA.norm(v1)
-
-        cos = inner / norms
-        rad = np.arccos(np.clip(cos, -1.0, 1.0))
-        return rad
diff --git a/src/pyamiimage/cleaner.py b/src/pyamiimage/cleaner.py
deleted file mode 100644
index 9a55a79..0000000
--- a/src/pyamiimage/cleaner.py
+++ /dev/null
@@ -1,104 +0,0 @@
-
-class WordCleaner:
-    
-    special_char = ['|', '>', '<', '-', '_', ',', '.', '`',
-                     ';', ':', '\'', '=', '€', '~', '*', '!',
-                     '$', '%', '^', '&', '(', ')', '/', '\\']    
-    frequently_misread_letters = ['Y', 'v', 'V']
-
-    @classmethod
-    def remove_single_special_characters(cls, textboxes):
-        """
-        Remove all special characters from a list
-        :param: textboxes is a list of TextBox objects
-        :returns: list of TextBox objects 
-        """
-        return [item for item in textboxes if item.text not in cls.special_char] 
-
-    @classmethod
-    def remove_all_single_characters(cls, textboxes):
-        """
-        Removes all single characters in a list of textboxes
-        """
-        return [item for item in textboxes if len(item.text) > 1 ]
-
-    @classmethod
-    def remove_all_sequences_of_special_characters(cls, textboxes):
-        """
-        Removes sequences of special characters of any length
-        """
-        textboxes_cleaned = []
-        for textbox in textboxes:
-            special_character = True
-            for letter in textbox.text:
-                if letter.isalnum():
-                    special_character = False
-                    break
-            if not special_character:
-                textboxes_cleaned.append(textbox)
-            
-        return textboxes_cleaned
-
-    @classmethod
-    def remove_trailing_special_characters(cls, textboxes):
-        """removes all the special characters at the end of the word"""
-        for textbox in textboxes:
-            appexdix_length = 0
-            text = textbox.text
-            for c in reversed(text):
-                if not c.isalnum():
-                    text = text[:-1]
-                    appexdix_length += 1
-            textbox.set_text(text)
-        return textboxes
-        
-    @classmethod
-    def remove_leading_special_characters(cls, textboxes):
-        """removes all the special characters in front of a word"""
-        cleaned_textboxes = []
-        for textbox in textboxes:
-            appexdix_length = 0
-            text = textbox.text
-            for c in text:
-                if not c.isalnum():
-                    text = text[1:]
-                    appexdix_length += 1
-            cleaned_textboxes.append(textbox)
-        return cleaned_textboxes
-
-    @classmethod
-    def remove_numbers_only(cls, textboxes):
-        """remove all words which are composed of numbers and special characters"""
-        cleaned_textbox = []
-        for textbox in textboxes:
-            word = False
-            text = textbox.text
-            for c in text:
-                if c.isalpha():
-                    word = True
-                    break
-            if word:
-                cleaned_textbox.append(textbox)
-        return cleaned_textbox
-
-    @classmethod
-    def remove_misread_letters(cls, textboxes):
-        """Remove whole words consisting only of frequently misread letters such as vV"""    
-        cleaned_textbox = []
-        for textbox in textboxes:
-            flag = False
-            text = textbox.text
-            for c in text:
-                if c not in cls.frequently_misread_letters:
-                    flag = True
-                    break
-            if flag:
-                cleaned_textbox.append(textbox)
-        return cleaned_textbox
-
-    @classmethod
-    def remove_leading_and_trailing_special_characters(cls, textboxes):
-        """Convenience method for trimming special characters from words"""
-        removed_leading = cls.remove_leading_special_characters(textboxes)
-        removed_both = cls.remove_trailing_special_characters(removed_leading)
-        return removed_both
diff --git a/src/pyamiimage/contour.py b/src/pyamiimage/contour.py
deleted file mode 100644
index 790394d..0000000
--- a/src/pyamiimage/contour.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from skimage.measure import find_contours
-from skimage import io
-from skimage.color import rgb2gray
-from matplotlib import pyplot as plt
-
-image = io.imread('contour_finding_test.png')
-# image = io.imread('FlowchartDiagram.png')
-image = rgb2gray(image)
-out = find_contours(image)
-print(len(out))
-
-# Find contours at a constant value of 0.8
-# contours = find_contours(image, 0.8)
-contours = find_contours(image, )
-
-# Display the image and plot all contours found
-fig, ax = plt.subplots()
-ax.imshow(image, cmap=plt.cm.gray)
-
-for contour in contours:
-    ax.plot(contour[:, 1], contour[:, 0], linewidth=2)
-
-ax.axis('image')
-ax.set_xticks([])
-ax.set_yticks([])
-plt.show()
-# io.imshow(image)
-# io.show()
diff --git a/src/pyamiimage/examples.py b/src/pyamiimage/examples.py
deleted file mode 100644
index ded38de..0000000
--- a/src/pyamiimage/examples.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""examples to be used by tests and tutorials"""
-
-class Examples:
-    pass
\ No newline at end of file
diff --git a/src/pyamiimage/examples_tesseract.py b/src/pyamiimage/examples_tesseract.py
deleted file mode 100644
index 7e598e7..0000000
--- a/src/pyamiimage/examples_tesseract.py
+++ /dev/null
@@ -1,161 +0,0 @@
-from pathlib import Path
-from tesseract_hocr import TesseractOCR
-from pyimage.old_code.preprocessing import ImageProcessor
-
-
-def example_extract_bbox_for_image_without_arrows():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_path = Path(resources_dir, "biosynth_path_1_cropped_arrows_removed.png")
-    bbox_coordinates = ocr.extract_bbox_from_image(image_path)
-    print("bbox coordinates: ", bbox_coordinates)
-
-
-def example_extract_bbox_from_hocr_file():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    hocr_path = Path(resources_dir, "hocr1.html")
-    root = ocr.read_hocr_file(hocr_path)
-    bbox_coordinates = ocr.extract_bbox_from_hocr(root)
-    print("bbox coordinates: ", bbox_coordinates)
-
-
-def example_fill_bbox_in_image():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_path = Path(resources_dir, "biosynth_path_1_cropped_arrows_removed.png")
-    # image_path = Path(resources_dir, "biosynth_path_1_cropped.png")
-    image_processor = ImageProcessor()
-    
-    image = image_processor.load_image(image_path)
-    bbox_coordinates = ocr.extract_bbox_from_image(image_path)
-    
-    bbox_around_words_image = ocr.draw_bbox_around_words(image, bbox_coordinates)
-    image_processor.show_image(bbox_around_words_image)
-    
-
-def example_2_fill_bbox_in_image():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_path = Path(resources_dir, "biosynth_path_3.png")
-    # image_path = Path(resources_dir, "biosynth_path_1_cropped.png")
-    image_processor = ImageProcessor()
-    
-    image = image_processor.load_image(image_path)
-    bbox_coordinates, words = ocr.extract_bbox_from_image(image_path)
-    
-    bbox_around_words_image = ocr.draw_bbox_around_words(image, bbox_coordinates)
-    image_processor.show_image(bbox_around_words_image)
-
-
-def example_2_fill_bbox_for_phrases():
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_path = Path(resources_dir, "biosynth_path_3.png")
-    # image_path = Path(resources_dir, "biosynth_path_1_cropped.png")
-    image_processor = ImageProcessor()
-    
-    ocr = TesseractOCR()
-
-    image = image_processor.load_image(image_path)
-    print(image.shape)
-    # bbox_coordinates, words = extract_bbox_from_image(image_path)
-    hocr = ocr.hocr_from_image_path(image_path)
-    root = ocr.parse_hocr_string(hocr)
-    phrases, bbox_coordinates = ocr.find_phrases(root)
-
-    words, bbox_coordinates_words = ocr.extract_bbox_from_hocr(root)
-
-    # Print out the phrase and its corresponding coordinates
-    for word, bbox in zip(words, bbox_coordinates_words):
-        print("Phrase: ", word, " Coordinates: ", bbox)
-    
-    bbox_around_words_image = ocr.draw_bbox_around_words(image, bbox_coordinates)
-    image_processor.show_image(bbox_around_words_image)
-
-
-def example_find_phrases():
-    ocr = TesseractOCR()
-    resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-    image_processor = ImageProcessor()
-    for num in range(4, 9):
-        print(f"Now processing biosynth_path_{num}.jpeg")
-        image_path = Path(resources_dir, f"biosynth_path_{num}.jpeg")
-        image = image_processor.load_image(image_path)
-    
-        hocr = ocr.hocr_from_image_path(image_path)
-        root = ocr.parse_hocr_string(hocr)
-        phrases, bbox_coordinates = ocr.find_phrases(root)
-
-        words, bbox_coordinates_words = ocr.extract_bbox_from_hocr(root)
-
-        # Print out the phrase and its corresponding coordinates
-        for word, bbox in zip(words, bbox_coordinates_words):
-            print("Phrase: ", word, " Coordinates: ", bbox)
-
-        ocr.output_phrases_to_file(phrases, f"biosynth_path_{num}.txt")
-        bbox_around_words_image = ocr.draw_bbox_around_words(image, bbox_coordinates)
-        image_processor.show_image(bbox_around_words_image)
-
-
-def example_alex_pico_images():
-    pass
-
-
-# not yet working
-"""
-Hi all,
-
-In preparation for our meeting next week, I went ahead and prepared a subset of our “25 years of pathway figures” database using some simple searches. Please know that I know nothing about terpene biochemistry, so I made two simple-minded subsets:
-	1. Based on “terpene” being found in paper title, figure title, caption, or chemical name
-	2. All of #1, plus figures with “ADH” found among the extracted gene symbols
-
-Here are shiny apps to browse these subsets and some summary stats:
-	1. Terpenes: https://gladstone-bioinformatics.shinyapps.io/shiny-terpene/
-		* Figures: 282
-		* Papers: 273
-		* Total genes: 1,276
-		* Unique genes: 491
-		* Total chemicals: 2,465
-		* Unique chemicals: 551
-	2. Terpenes + ADH: https://gladstone-bioinformatics.shinyapps.io/shiny-terpeneadh/
-		* Figures: 719
-		* Papers: 700
-		* Total genes: 10,667
-		* Unique genes: 2,699
-		* Total chemicals: 6,004
-		* Unique chemicals: 931
-
-These subsets are also available as RDS and TSV files if you want to browse them directly.
-
-Maybe these will help make some of our discussion points more tangible.
-
-Note the following caveats that we’d certainly want to improve upon for a “real” effort:
-	* Covers pathway figures from 1995 - 2019. We have a 2020 collection, but it’s not online yet.
-	* Based on a general literature search for all types of pathway biology; not targeted or trained for terpene biochemistry
-	* Based on HGNC human gene lexicon and pubtator chemicals; not targeted for plants or terpenes
-
-Cheers,
- - Alex
-
- PMC6348685	Comparative transcriptome analysis of a long-time ...	Luodong Huang, ...	2019	Fig. 4	Putative genes and their expression...
-PMC6360084	Identification, expression, and phylogenetic analy...	Jipeng Mao, Zid...	2019	Figure 8	Identification, expression, and phy...
-PMC6374618	Agarwood Induction: Current Developments and Futur...	Cheng Seng CS T...	2019	FIGURE 4	Schematic relationships between the...
-PMC6394206	A chromosomal-scale genome assembly of Tectona gra...	Dongyan Zhao, J...	2019	Figure 5:	Proposed diterpene pathway based on...
-PMC6412517	New Insights on Arabidopsis thaliana Root Adaption...	Inmaculada Cole...	2019	Figure 2	Induction of secondary metabolism i...
-PMC6423058	Why Algae Release Volatile Organic Compounds—The E...	Zhaojiang Zuo	2019	FIGURE 2	Pathway of terpene synthesis
-PMC6437448	Production of ginsenoside aglycone (protopanaxatri...	Yu Shin YS Gwak...	2019	Fig. 1	Biosynthetic pathway engineering fo...
-PMC6453882	Proteomic analysis reveals novel insights into tan...	Angela Contrera...	2019	Figure 4	Overview of the proposed biosynthet...
-PMC6475556	Protective Effect of the Total Triterpenes of Eusc...	Wei Huang, Hui ...	2019	Figure 7	The mechanisms of total triterpenes...
-PMC6486373	Emerging Antitumor Activities of the Bitter Melon ...	Evandro Fei EF ...	2019	Fig. (1)	Possible mechanisms of the anti-tum...
-"""
-
-
-def main():
-    # example_extract_bbox_for_image_without_arrows()
-    # example_extract_bbox_from_hocr_file()
-    # example_2_fill_bbox_in_image()
-    example_find_phrases()
-
-
-if __name__ == '__main__':
-    main()
diff --git a/src/pyamiimage/flood_fill.py b/src/pyamiimage/flood_fill.py
deleted file mode 100644
index 9829b73..0000000
--- a/src/pyamiimage/flood_fill.py
+++ /dev/null
@@ -1,85 +0,0 @@
-"""bespoke floodfill algorithm as I couldn't find one"""
-
-from collections import deque
-import matplotlib.pyplot as plt
-import numpy as np
-
-
-class FloodFill:
-    """creates a list of flood_filling pixels given a seed"""
-
-    def __init__(self):
-        self.start_pixel = None
-        self.binary = None
-        self.filling_pixels = None
-
-    def flood_fill(self, binary_image, start_pixel):
-        """
-
-        :param binary_image: Not altered
-        :param start_pixel:
-        :return: (filled image, set of filling pixels)
-        """
-        # self.binary = self.binary.astype(int)
-
-        self.start_pixel = start_pixel
-        self.binary = binary_image
-        self.filling_pixels = self.get_filling_pixels()
-        return self.filling_pixels
-
-    def get_filling_pixels(self):
-        # new_image = np.copy(self.binary)
-        xy = self.start_pixel
-        xy_deque = deque()
-        xy_deque.append(xy)
-        filling_pixels = set()
-        while xy_deque:
-            xy = xy_deque.popleft()
-            self.binary[xy[0], xy[1]] = 0  # unset pixel
-            neighbours_list = self.get_neighbours(xy)
-            for neighbour in neighbours_list:
-                neighbour_xy = (neighbour[0], neighbour[1])  # is this necessary??
-                if neighbour_xy not in filling_pixels:
-                    filling_pixels.add(neighbour_xy)
-                    xy_deque.append(neighbour_xy)
-                else:
-                    pass
-        return filling_pixels
-
-    def get_neighbours(self, xy):
-        # i = xy[0]
-        # j = xy[1]
-        w = 3
-        h = 3
-        neighbours = []
-        # I am sure there's a more pythonic way
-        for i in range(w):
-            ii = xy[0] + i - 1
-            if ii < 0 or ii >= self.binary.shape[0]:
-                continue
-            for j in range(h):
-                jj = xy[1] + j - 1
-                if jj >= 0 or jj < self.binary.shape[1]:
-                    if self.binary[ii][jj] == 1:
-                        neighbours.append((ii, jj))
-        return neighbours
-
-    def plot_used_pixels(self):
-        used_image = self.create_image_of_filled_pixels()
-        fig, ax = plt.subplots()
-        ax.imshow(used_image)
-        plt.show()
-
-    def create_image_of_filled_pixels(self):
-        used_image = np.zeros(self.binary.shape, dtype=bool)
-        for pixel in self.filling_pixels:
-            used_image[pixel[0], pixel[1]] = 1
-        return used_image
-
-    def get_raw_box(self):
-        """
-        gets raw bounding box dimensions as an array of arrays.
-        will make this into BoundingBox soon
-
-        :return:
-        """
diff --git a/src/pyamiimage/obsolete.py b/src/pyamiimage/obsolete.py
deleted file mode 100644
index 0b7f73b..0000000
--- a/src/pyamiimage/obsolete.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# maybe worked with network_x
-def get_maxx_maxy_non_pythonic(self, node_dict, nodes):
-    maxx = -999999
-    maxy = -999999
-    for node, i in enumerate(nodes):
-        # certainly a more pythonic way exists
-        x = node_dict[i][0]
-        if x > maxx:
-            maxx = x
-        y = node_dict[i][1]
-        if y > maxy:
-            maxy = y
-    return maxx, maxy
-
diff --git a/src/pyamiimage/old_code/ColorModule.py b/src/pyamiimage/old_code/ColorModule.py
deleted file mode 100644
index 1cb9b69..0000000
--- a/src/pyamiimage/old_code/ColorModule.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env python
-#-*- coding: utf8 -*-
-
-
-class Color(object):
-    """
-    Simple RGB class
-    """
-
-    def __init__(self, red=0, green=0, blue=0, alpha=None):
-        """
-        Initialize color
-        """
-        self.red = red
-        self.green = green
-        self.blue = blue
-        self.alpha = alpha
-
-    def to_hex(self):
-        return f'{self.red:02x}' + f'{self.green:02x}' + f'{self.blue:02x}'
-
-    def is_equal(self, col):
-        # print("l", len(col))
-        return len(col) == 3 and col[0] == self.red and col[1] == self.green and col[2] == self.blue
-
-    def as_tuple(self):
-        return self.red, self.green, self.blue
diff --git a/src/pyamiimage/old_code/__init__.py b/src/pyamiimage/old_code/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/pyamiimage/old_code/graph_lib.py b/src/pyamiimage/old_code/graph_lib.py
deleted file mode 100644
index 37863b4..0000000
--- a/src/pyamiimage/old_code/graph_lib.py
+++ /dev/null
@@ -1,313 +0,0 @@
-import numpy as np
-import networkx as nx
-import copy
-from networkx.algorithms import tree
-from skimage import io
-import sknw  # must pip install sknw
-import logging
-from pathlib import PosixPath
-
-from pyimage.ami_image import AmiImage
-from pyimage.ami_util import AmiUtil
-# from pyimage.ami_island import AmiIsland
-# from pyimage.ami_node import AmiNode
-from pyimage.ami_skeleton import AmiSkeleton
-# from pyimage.ami_edge import AmiEdge
-
-
-class AmiGraph:
-    """holds AmiNodes and AmiEdges
-    may also hold subgraphs
-    """
-
-    logger = logging.getLogger("ami_graph")
-
-    def __init__(self, nx_graph, generate_nodes=True, nd_skeleton=None):
-        """create fro nodes and edges"""
-        # self.ami_node_dict = {}
-        # self.ami_edge_dict = {}
-        # self.generate_nodes = generate_nodes
-        self.nx_graph = None
-        self.ami_edges = None
-        self.ami_nodes = None
-        self.ami_island_list = None
-        # self.node_dict = None
-        self.nd_skeleton = nd_skeleton
-        # self.islands = None
-        if nx_graph is None:
-            raise Exception(f"nx_graph cannot be None")
-        self.read_nx_graph(nx_graph)
-        assert self.nx_graph is not None, f"ami_graph.nx_graph should not be None"
-        return
-
-    # def read_nodes(self, nodes):
-    #     """create a list of AmiNodes """
-    #     if nodes is not None:
-    #         for node in nodes:
-    #             self.add_raw_node(node)
-
-    def add_raw_node(self, raw_node, fail_on_duplicate=False):
-        """add a raw node either a string or string-indexed dict
-        if already a dict, deepcopy it
-        if a primitive make a node_dict and start it with raw_node as id
-        :raw_node: node to add, must have key
-        :fail_on_duplicate: if true fail if key already exists
-        """
-        if raw_node is not None:
-            # ami_node = AmiNode()
-            key = raw_node.key if type(raw_node) is dict else str(raw_node)
-            key = "n" + str(key)
-            if key in self.ami_node_dict and fail_on_duplicate:
-                raise AmiGraphError(f"cannot add same node twice {key}")
-            if type(raw_node) is dict:
-                self.ami_node_dict[key] = copy.deepcopy(raw_node)
-            else:
-                self.ami_node_dict[key] = "node"  # store just the key at present
-        else:
-            self.logger.warning("node cannot be None")
-
-    def read_edges(self, edges):
-        # self.ami_edges = edges
-        if len(self.ami_node_dict.keys()) == 0 and self.generate_nodes:
-            self.generate_nodes_from_edges()
-            print("after node generation", str(self))
-        for i, edge in enumerate(edges):
-            idx = "e" + str(i)
-            self.add_edge(edge, idx)
-
-    def add_edge(self, raw_edge, idx, fail_on_duplicate=True):
-        if fail_on_duplicate and idx in self.ami_edge_dict.keys():
-            raise ValueError("duplicate edge")
-
-        if raw_edge is None:
-            raise AmiGraphError("cannot add edge=None")
-        edge1 = ("n" + str(raw_edge[0]), "n" + str(raw_edge[1]))
-        self.ami_edge_dict[idx] = edge1
-
-    def generate_nodes_from_edges(self):
-        if self.ami_edges is not None:
-            for edge in self.ami_edges:
-                self.add_raw_node(edge[0])
-                self.add_raw_node(edge[1])
-
-    @classmethod
-    def create_ami_graph(cls, nd_skeleton):
-        """Uses Sknw to create a graph object within a new AmiGraph"""
-        # currently only called in a test
-        nx_graph = sknw.build_sknw(nd_skeleton)
-        ami_graph = AmiGraph(nx_graph, nd_skeleton=nd_skeleton)
-        # ami_graph.read_nx_graph(nx_graph )
-        print(f"***ami_graph\n {ami_graph}\n")
-        return ami_graph
-
-    def ingest_graph_info(self):
-        if self.nx_graph is None:
-            self.logger.warning("Null graph")
-            return
-        # print("graph", self.nx_graph)
-        nx_island_list = list(nx.connected_components(self.nx_graph))
-        if nx_island_list is None or len(nx_island_list) == 0:
-            self.logger.warning("No islands")
-            return
-
-        self.assert_nx_island_info(nx_island_list)
-        nx_edgelist = self.get_edge_list_through_mininum_spanning_tree()
-        self.debug_edges_and_nodes(nx_edgelist, debug_count=7)
-        nodes = self.nx_graph.nodes
-        self.node_dict = {i: (nodes[node]["o"][0], nodes[node]["o"][1]) for i, node in enumerate(nodes)}
-
-        self.ami_island_list = []
-        for nx_island in nx_island_list:
-            ami_island = self.create_ami_island(nx_island, skeleton=self.nd_skeleton)
-            print(f"ami_island {ami_island}")
-            self.ami_island_list.append(ami_island)
-
-        return
-
-    def assert_nx_island_info(self, nx_island_list):
-        nx_island0 = nx_island_list[0]
-        assert type(nx_island0) is set
-        assert len(nx_island0) > 0
-        elem0 = list(nx_island0)[0]
-        assert type(elem0) is int, f"island elem are {type(elem0)}"
-        print("islands", self.ami_island_list)
-
-    def debug_edges_and_nodes(self, nx_edgelist, debug_count=5):
-        start_node = 0
-        end_node = 1
-        pts_index = 2
-        for edge in nx_edgelist[:debug_count]:
-            pts_ = edge[pts_index]['pts']
-            print(edge[start_node], edge[end_node], "pts in edge", len(pts_))
-        edgelist_pts_ = nx_edgelist[0][2]['pts']
-        for step in edgelist_pts_[:debug_count]:
-            print("step", step)
-
-    def get_edge_list_through_mininum_spanning_tree(self):
-        mst = tree.maximum_spanning_edges(self.nx_graph, algorithm="kruskal", data=True)
-        # mst = tree.minimum_spanning_tree(graph, algorithm="kruskal")
-        nx_edgelist = list(mst)
-        return nx_edgelist
-
-    @classmethod
-    def set_bbox_pixels_to_color(cls, bbox, image, colorx=255):
-        """sets all pixels in box to uniform color
-
-        :param bbox:
-        :param image:
-        :param colorx:
-        :return: modified image
-        """
-        xx = bbox[0]
-        yy = bbox[1]
-        image[xx[0]:xx[1], yy[0]:yy[1]] = colorx
-        return image
-
-    def __str__(self):
-        s = "nodes: " + str(self.ami_nodes) + \
-            "\n edges: " + str(self.ami_edges)
-        return s
-
-    def read_nx_graph(self, nx_graph):
-        """
-        Read and unpack NetworkX graph.
-        This may change as a result of changing data models
-        the nx_graph may be tyhe fundamental data structure
-        :param nx_graph:
-        :return:
-        """
-        # self.nodes_as_dicts = [nx_graph.node[ndidx] for ndidx in (nx_graph.nodes())]
-        # self.nodes_yx = [nx_graph.node[ndidx][AmiSkeleton.CENTROID] for ndidx in (nx_graph.nodes())]
-        self.read_nx_edges(nx_graph)
-        self.read_nx_nodes(nx_graph)
-        # this may be the critical data structure and the others are convenience
-        self.nx_graph = nx_graph
-
-        self.ingest_graph_info()
-        return
-
-    def read_nx_nodes(self, nx_graph):
-        self.ami_nodes = []
-        node_ids = nx_graph.nodes()
-        for node_id in node_ids:
-            # node_dict = node_ids[node_id]
-            assert len(str(node_id)) < 4, f"node_id is {node_id}"
-            ami_node = AmiNode(ami_graph=self, node_id=node_id, nx_graph=nx_graph)
-            ami_node.set_centroid_yx(nx_graph.nodes[node_id][AmiSkeleton.CENTROID])
-            # ami_node.read_nx_node(node_dict)
-            self.ami_nodes.append(ami_node)
-
-    def read_nx_edges(self, nx_graph):
-        self.ami_edges = []
-        for (start, end) in nx_graph.nx_edges():
-            points_yx = nx_graph[start][end][AmiSkeleton.NODE_PTS]
-            ami_edge = AmiEdge()
-            ami_edge.read_nx_edge_points_yx_into_self_points_xy(points_yx)
-            self.ami_edges.append(ami_edge)
-
-    def get_or_create_ami_islands(self):
-        """
-        AmiIslands are nx_graph 'components' with added functionality
-        :return: list of AmiIslands
-        """
-        if self.ami_island_list is None and self.nx_graph is not None:
-            self.ami_island_list = [self.create_ami_island(comp) for comp in
-                                    nx.algorithms.components.connected_components(self.nx_graph)]
-        return self.ami_island_list
-
-    @classmethod
-    def create_nx_graph_from_arbitrary_image_file(cls, path):
-        assert path.exists() and not path.is_dir(), f"{path} should be existing file"
-        AmiUtil.check_type_and_existence(path, PosixPath)
-        image1 = io.imread(path)
-        AmiUtil.check_type_and_existence(image1, np.ndarray)
-        gray_image = AmiImage.create_grayscale_from_image(image1)
-        skeleton_array = AmiImage.create_white_skeleton_from_image(gray_image)
-        nx_graph = AmiGraph.create_nx_graph_from_skeleton(skeleton_array)
-        return nx_graph
-
-    def get_ami_islands_from_nx_graph(self):
-        """
-        Get the pixel-disjoint "islands" as from NetworkX
-        :return: list of AmiIslands
-        """
-
-        self.get_coords_for_nodes_and_edges_from_nx_graph(self.nx_graph)
-        assert self.nx_graph is not None
-        ami_islands = []
-        for node_ids in nx.algorithms.components.connected_components(self.nx_graph):
-            print("node_ids ", node_ids)
-            ami_island = self.create_ami_island(node_ids)
-            assert ami_island is not None
-            assert type(ami_island) is AmiIsland
-            ami_islands.append(ami_island)
-        return ami_islands
-
-    def extract_coords_for_node(self, idx):
-        """
-        gets coords for a single node with given id
-        :param idx: normally an int
-        :return:
-        """
-        node_data = self.nx_graph.nodes[idx]
-        centroid = node_data[AmiSkeleton.CENTROID]
-        centroid = (centroid[1], centroid[0])  # swap y,x as sknw seems to have this unusual order
-        return centroid
-
-    def create_islands(self):
-        """
-        needs nx_graph to exist
-
-        :return: list of islands
-        """
-
-        assert self.nx_graph is not None
-        self.islands = self.get_ami_islands_from_nx_graph()
-        return self.islands
-
-    @classmethod
-    def create_ami_island(self, node_ids, skeleton=None):
-        """
-        create from a list of node_ids (maybe from sknw)
-        maybe should be instance method of ami_graph
-        :param node_ids: set of node ids
-        :param ami_graph: essential
-        :param skeleton:
-        :return: AmiIsland object
-        """
-        assert type(node_ids) is set, "componente mus be of type set"
-        assert len(node_ids) > 0 , "components cannot be empty"
-
-        ami_island = AmiIsland()
-        ami_island.node_ids = node_ids
-        ami_island.ami_skeleton = skeleton
-        ami_island.ami_graph = self
-        print(f"ami_island =============  {ami_island.ami_graph}")
-        print("ami_island", ami_island)
-        return ami_island
-
-
-
-class AmiGraphError(Exception):
-    def __init__(self, msg):
-        super().__init__(msg)
-
-
-if __name__ == '__main__':
-
-    img = np.array([
-        [0, 0, 0, 1, 0, 0, 0, 0, 0],
-        [0, 0, 0, 1, 0, 0, 0, 1, 0],
-        [0, 0, 0, 1, 0, 0, 0, 1, 0],
-        [0, 0, 0, 1, 0, 0, 0, 0, 0],
-        [1, 1, 1, 1, 0, 0, 0, 0, 0],
-        [0, 0, 0, 0, 1, 0, 0, 0, 0],
-        [0, 1, 0, 0, 0, 1, 0, 0, 0],
-        [1, 0, 1, 0, 0, 1, 1, 1, 1],
-        [0, 1, 0, 0, 1, 0, 0, 0, 0],
-        [0, 0, 0, 1, 0, 0, 0, 0, 0]])
-
-    # sknw.example1()
-    # sknw.example2horse()  # works
-    # sknw.example3() # needs flipping White to black
-    # sknw.example4() # needs flipping White to black
diff --git a/src/pyamiimage/old_code/old_octree.py b/src/pyamiimage/old_code/old_octree.py
deleted file mode 100644
index 4ef83d8..0000000
--- a/src/pyamiimage/old_code/old_octree.py
+++ /dev/null
@@ -1,272 +0,0 @@
-# !/usr/bin/env python
-#-*- coding: utf8 -*-
-# from https://github.com/delimitry/octree_color_quantizer CC0, thanks
-from pathlib import Path
-
-from pyimage.old_code.ColorModule import Color
-from PIL import Image
-
-
-class OctreeNode(object):
-    """
-    Octree Node class for color quantization
-    """
-
-    def __init__(self, level, parent):
-        """
-        Init new Octree Node
-        """
-        self.color = Color(0, 0, 0)
-        self.pixel_count = 0
-        self.palette_index = 0
-        self.children = [None for _ in range(8)]
-        # add node to current level
-        if level < OctreeQuantizer.MAX_DEPTH - 1:
-            parent.add_level_node(level, self)
-
-    def is_leaf(self):
-        """
-        Check that node is leaf
-        """
-        return self.pixel_count > 0
-
-    def get_leaf_nodes(self):
-        """
-        Get all leaf nodes
-        """
-        leaf_nodes = []
-        for i in range(8):
-            node = self.children[i]
-            if node:
-                if node.is_leaf():
-                    leaf_nodes.append(node)
-                else:
-                    leaf_nodes.extend(node.get_leaf_nodes())
-        return leaf_nodes
-
-    def get_nodes_pixel_count(self):
-        """
-        Get a sum of pixel count for node and its children
-        """
-        sum_count = self.pixel_count
-        for i in range(8):
-            node = self.children[i]
-            if node:
-                sum_count += node.pixel_count
-        return sum_count
-
-    def add_color(self, color, level, parent):
-        """
-        Add `color` to the tree
-        """
-        if level >= OctreeQuantizer.MAX_DEPTH:
-            self.color.red += color.red
-            self.color.green += color.green
-            self.color.blue += color.blue
-            self.pixel_count += 1
-            return
-        index = self.get_color_index_for_level(color, level)
-        if not self.children[index]:
-            self.children[index] = OctreeNode(level, parent)
-        self.children[index].add_color(color, level + 1, parent)
-
-    def get_palette_index(self, color, level):
-        """
-        Get palette index for `color`
-        Uses `level` to go one level deeper if the node is not a leaf
-        """
-        if self.is_leaf():
-            return self.palette_index
-        index = self.get_color_index_for_level(color, level)
-        if self.children[index]:
-            return self.children[index].get_palette_index(color, level + 1)
-        else:
-            # get palette index for a first found child node
-            for i in range(8):
-                if self.children[i]:
-                    return self.children[i].get_palette_index(color, level + 1)
-
-    def remove_leaves(self):
-        """
-        Add all children pixels count and color channels to parent node
-        Return the number of removed leaves
-        """
-        result = 0
-        for i in range(8):
-            node = self.children[i]
-            if node:
-                self.color.red += node.color.red
-                self.color.green += node.color.green
-                self.color.blue += node.color.blue
-                self.pixel_count += node.pixel_count
-                result += 1
-        return result - 1
-
-    @classmethod
-    def get_color_index_for_level(cls, color, level):
-        """
-        Get index of `color` for next `level`
-        """
-        index = 0
-        mask = 0x80 >> level
-        if color.red & mask:
-            index |= 4
-        if color.green & mask:
-            index |= 2
-        if color.blue & mask:
-            index |= 1
-        return index
-
-    def get_color(self):
-        """
-        Get average color
-        """
-        return Color(
-            self.color.red // self.pixel_count,
-            self.color.green // self.pixel_count,
-            self.color.blue // self.pixel_count)
-
-
-class OctreeQuantizer(object):
-    """
-    Octree Quantizer class for image color quantization
-    Use MAX_DEPTH to limit a number of levels
-    """
-
-    MAX_DEPTH = 8
-
-    def __init__(self):
-        """
-        Init Octree Quantizer
-        """
-        self.levels = {i: [] for i in range(OctreeQuantizer.MAX_DEPTH)}
-        self.root = OctreeNode(0, self)
-
-    def get_leaves(self):
-        """
-        Get all leaves
-        """
-        return [node for node in self.root.get_leaf_nodes()]
-
-    def add_level_node(self, level, node):
-        """
-        Add `node` to the nodes at `level`
-        """
-        self.levels[level].append(node)
-
-    def add_color(self, color):
-        """
-        Add `color` to the Octree
-        """
-        # passes self value as `parent` to save nodes to levels dict
-        self.root.add_color(color, 0, self)
-
-    def make_palette(self, color_count):
-        """
-        Make color palette with `color_count` colors maximum
-        """
-        palette = []
-        palette_index = 0
-        leaf_count = len(self.get_leaves())
-        # reduce nodes
-        # up to 8 leaves can be reduced here and the palette will have
-        # only 248 colors (in worst case) instead of expected 256 colors
-        for level in range(OctreeQuantizer.MAX_DEPTH - 1, -1, -1):
-            if self.levels[level]:
-                for node in self.levels[level]:
-                    leaf_count -= node.remove_leaves()
-                    if leaf_count <= color_count:
-                        break
-                if leaf_count <= color_count:
-                    break
-                self.levels[level] = []
-        # build palette
-        for node in self.get_leaves():
-            if palette_index >= color_count:
-                break
-            if node.is_leaf():
-                palette.append(node.get_color())
-            node.palette_index = palette_index
-            palette_index += 1
-        return palette
-
-    def get_palette_index(self, color):
-        """
-        Get palette index for `color`
-        """
-        return self.root.get_palette_index(color, 0)
-
-
-def quantize(image, size=4):
-    import time
-    from ColorModule import Color
-    # from Octree import OctreeQuantizer
-
-    # size = 16 => 256 colors for 8 bits per pixel output image
-
-    # print("pixels shape", image.shape)
-    pixels = image.load()
-    width, height = image.size
-    print(width, height)
-
-    octree = OctreeQuantizer()
-
-    # add colors to the octree
-    time0 = time.perf_counter()
-    for j in range(height):
-        for i in range(width):
-            octree.add_color(Color(*pixels[i, j]))
-    print("octree time", time.perf_counter()-time0)
-
-    palette, palette_image = create_palette_image(size, octree, width, height)
-    out_image = create_output_image(width, height, octree, palette, pixels)
-    return out_image, palette, palette_image
-
-
-def create_palette_image(size, octree, width, height):
-    from PIL import Image
-    import time
-    time0 = time.perf_counter()
-    palette = octree.make_palette(size * size)
-    palette_image = Image.new('RGB', (size, size))
-    palette_pixels = palette_image.load()
-    for i, color in enumerate(palette):
-        rgb = (color.red, color.green, color.blue)
-        palette_pixels[i % size, i // size] = rgb
-        print("rgb", rgb)
-    print("palette time", time.perf_counter()-time0)
-    return palette, palette_image
-
-
-def create_output_image(width, height, octree, palette, pixels):
-    from PIL import Image
-    import time
-    time0 = time.perf_counter()
-    out_image = Image.new('RGB', (width, height))
-    out_pixels = out_image.load()
-    for j in range(height):
-        for i in range(width):
-            index = octree.get_palette_index(Color(*pixels[i, j]))
-            color = palette[index]
-            out_pixels[i, j] = (color.red, color.green, color.blue)
-
-    print("output time", time.perf_counter()-time0)
-    return out_image
-
-
-def main():
-    # image_name = "red_black_cv.png"
-    image_name = "purple_ocimum_basilicum.png"
-    path = Path(Path(__file__).parent.parent, "assets", image_name)
-    assert path.exists()
-    # img = imageio.imread(path)
-    img = Image.open(path)
-
-    # ImageLib.image_show(img)
-    # print(img)
-
-    quantize(img, size=4)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/src/pyamiimage/old_code/pmrsknw.py b/src/pyamiimage/old_code/pmrsknw.py
deleted file mode 100644
index d5afe22..0000000
--- a/src/pyamiimage/old_code/pmrsknw.py
+++ /dev/null
@@ -1,309 +0,0 @@
-from skimage.morphology import skeletonize
-import numpy as np
-import networkx as nx
-import matplotlib.pyplot as plt
-from pathlib import Path
-from skimage.filters import threshold_otsu
-from skimage import data
-import skimage.io
-
-
-# various experiments by PMR
-
-# =======================================
-# This draws a graph from a map of 0/1 pixels . Uses networkx
-# looks useful
-# from https://github.com/Image-Py/sknw/blob/master/sknw/sknw.py
-
-class PmrSknw:
-    def __init__(self):
-        self.graph = None
-        self.img = None
-
-    @classmethod
-    def calc_mask_values_of_neighbors(cls, shape):
-        """not sure what this does yet
-        I think it might take a region and create margins
-        """
-        dim = len(shape)  # dimension of shape (?always 2)
-        # create 3*3 block around a point
-        block = np.ones([3] * dim)
-        # centre is now 0, so [1,1,1  1,0,1  1,1,1]
-        block[tuple([1] * dim)] = 0  # sets (1,1) = 0 (i.e. the centre)
-        idx = np.where(block > 0)  # (array([0,0,0 1,1 2,2,2]), array([0,1,2 0,2 0,1,2]))
-        idx1 = np.array(idx, dtype=np.uint8).T  # [[0,0], [0,1], [0,2],   [1.0], [1,2],   [2,0], [2,1], [2,2]]
-        idx2 = np.array(idx1 - [1] * dim)  # [[-1 -1] [-1 0] [-1 1]   [0, -1] [0 1]   [1 -1] [1 0] [1 1]]
-        shape_ = (1,) + shape[::-1][:-1]  # (1, 402)
-        acc = np.cumprod(shape_)  # [1 402]
-        neighbors = np.dot(idx2, acc[::-1])
-        print("neighbors", neighbors.shape, neighbors)  # (8,) [-403 -402 -401 -1 1 401 402 403
-        return neighbors
-
-    # my mark
-    @classmethod
-    def mark(cls, buf, nbs):  # mark the array use (0, 1, 2)
-        buf = buf.ravel()
-        for p in range(len(buf)):
-            if buf[p] == 0:
-                continue
-            s = 0
-            for dp in nbs:
-                if buf[p + dp] != 0:
-                    s += 1
-            if s == 2:
-                buf[p] = 1
-            else:
-                buf[p] = 2
-
-    # trans index to r, c...
-    @classmethod
-    def idx2rc(cls, idx, acc):
-        rst = np.zeros((len(idx), len(acc)), dtype=np.int16)
-        for i in range(len(idx)):
-            for j in range(len(acc)):
-                rst[i, j] = idx[i] // acc[j]
-                idx[i] -= rst[i, j] * acc[j]
-        rst -= 1
-        return rst
-
-    # fill a node (may be two or more points)
-    def fill(self, imgx, p, num, nbs, acc, buf):
-        imgx[p] = num
-        buf[0] = p
-        cur = 0
-        s = 1
-        iso = True
-
-        while True:
-            p = buf[cur]
-            for dp in nbs:
-                cp = p + dp
-                if imgx[cp] == 2:
-                    imgx[cp] = num
-                    buf[s] = cp
-                    s += 1
-                if imgx[cp] == 1:
-                    iso = False
-            cur += 1
-            if cur == s:
-                break
-        # transform ? 1-dim index to row + column indexes
-        rc = self.idx2rc(buf[:s], acc)
-        return iso, rc
-
-    # trace the edge and use a buffer, then buf.copy, if use [] numba not works
-    def trace_edge_of_something(self, img, p, nbs, acc, buf):
-        c1 = 0
-        c2 = 0
-        newp = 0
-        cur = 1
-        while True:
-            buf[cur] = p
-            img[p] = 0
-            cur += 1
-            for dp in nbs:
-                cp = p + dp
-                if img[cp] >= 10:
-                    if c1 == 0:
-                        c1 = img[cp]
-                        buf[0] = cp
-                    else:
-                        c2 = img[cp]
-                        buf[cur] = cp
-                if img[cp] == 1:
-                    newp = cp
-            p = newp
-            if c2 != 0:
-                break
-        # create row and column indexes
-        rc = self.idx2rc(buf[:cur + 1], acc)
-
-        rc1 = (c1 - 10, c2 - 10, rc)
-        # print("rc1.shape", rc1.shape)
-        return rc1
-
-    # parse the image then get the nodes and edges
-    def parse_struc(self, img, nbs, acc, iso, ring):
-        imgx = img.ravel()  # flattened matrix
-        # NO IDEA where this number comes from
-        buf = np.zeros(131072, dtype=np.int64)
-        num = 10  # no idea what this is
-        nodes, num = self.create_nodes(acc, buf, imgx, iso, nbs, num)
-        edges = self.create_edges(acc, buf, imgx, nbs)
-        if ring:
-            self.create_cyclic_edges(acc, buf, edges, imgx, nbs, nodes, num)
-        return nodes, edges
-
-    def create_cyclic_edges(self, acc, buf, edges, imgx, nbs, nodes, num):
-        for p in range(len(imgx)):
-            if imgx[p] != 1:
-                continue
-            imgx[p] = num
-            num += 1
-            nodes.append(self.idx2rc([p], acc))
-            for dp in nbs:
-                if imgx[p + dp] == 1:
-                    edge = self.trace_edge_of_something(imgx, p + dp, nbs, acc, buf)
-                    edges.append(edge)
-
-    def create_nodes(self, acc, buf, imgx, iso, nbs, num):
-        nodes = []
-        for p in range(len(imgx)):
-            if imgx[p] == 2:
-                isiso, nds = self.fill(imgx, p, num, nbs, acc, buf)
-                if isiso and not iso:
-                    continue
-                num += 1
-                nodes.append(nds)
-        return nodes, num
-
-    def create_edges(self, acc, buf, imgx, nbs):
-        edges = []
-        for p in range(len(imgx)):
-            if imgx[p] < 10:
-                continue
-            for dp in nbs:
-                if imgx[p + dp] == 1:
-                    edge = self.trace_edge_of_something(imgx, p + dp, nbs, acc, buf)
-                    edges.append(edge)
-        return edges
-
-    # use nodes and edges build a networkx graph
-    @classmethod
-    def build_nx_graph(cls, nodes, edges, multi=False, full=True):
-        os = np.array([i.mean(axis=0) for i in nodes])
-        if full:
-            os = os.round().astype(np.uint16)
-        nx_graph = nx.MultiGraph() if multi else nx.Graph()
-        for i in range(len(nodes)):
-            nx_graph.add_node(i, pts=nodes[i], o=os[i])
-        for s, e, pts in edges:
-            if full:
-                pts[[0, -1]] = os[[s, e]]
-            ll = np.linalg.norm(pts[1:] - pts[:-1], axis=1).sum()
-            nx_graph.add_edge(s, e, pts=pts, weight=ll)
-        return nx_graph
-
-    def mark_node(self, img):
-        node_buf = np.pad(img, (1, 1), mode='constant')
-        nbs = self.calc_mask_values_of_neighbors(node_buf.shape)
-        acc = np.cumprod((1,) + node_buf.shape[::-1][:-1])[::-1]
-        self.mark(node_buf, nbs)
-        return node_buf
-
-    def build_sknw(self, img, multi=False, iso=True, ring=True, full=True):
-        """
-
-        :param img: image (probably thinned , maybe binary will have to research)
-        :param multi: ??
-        :param iso:  ??
-        :param ring: I think True allows cycles
-        :param full: ??
-        :return: nx_graph, nodes, edges
-        """
-        buf = np.pad(img, (1, 1), mode='constant')  # (330, 402)
-        nbs = self.calc_mask_values_of_neighbors(buf.shape)
-        acc = np.cumprod((1,) + buf.shape[::-1][:-1])[::-1]
-        self.mark(buf, nbs)
-        nodes, edges = self.parse_struc(buf, nbs, acc, iso, ring)
-        nx_graph = self.build_nx_graph(nodes, edges, multi, full)
-        return nx_graph, nodes, edges
-
-    def read_thinned_image_calculate_graph_and_plot(self, img):
-        node_img = self.mark_node(img)
-        self.graph = self.build_sknw(img, False, iso=True, ring=True)
-        from networkx.algorithms import tree
-        mst = tree.maximum_spanning_edges(self.graph, algorithm="kruskal", data=False)
-        edgelist = list(mst)
-        print("edges", edgelist)
-
-        plt.imshow(node_img[1:-1, 1:-1], cmap='gray')
-        self.plot_edges()
-        self.plot_nodes()
-        plt.title('Build Graph')
-        plt.show()
-
-    def plot_nodes(self):
-        nodes = self.graph.nodes()
-        ps = np.array([nodes[i]['o'] for i in nodes])
-        plt.plot(ps[:, 1], ps[:, 0], 'r.')
-
-    def plot_edges(self):
-        # plt.imshow(node_img[1:-1, 1:-1], cmap='gray')
-        # draw edges by pts
-        for (s, e) in self.graph.nx_edges():
-            ps = self.graph[s][e]['pts']
-            plt.plot(ps[:, 1], ps[:, 0], 'green')
-
-    # def example1(self):
-    #     self.read_thinned_image_calculate_graph_and_plot(img)
-
-    @classmethod
-    def example2horse(cls):
-        # open and skeletonize
-        img = data.horse()
-        # ske = skeletonize(img).astype(np.uint16)
-        # self.read_thinned_image_calculate_graph_and_plot(ske)
-        ske = skeletonize(~img).astype(np.uint16)
-
-        # build graph from skeleton
-        graph, nodes, edges = PmrSknw().build_sknw(ske)
-
-        # draw image
-        plt.imshow(img, cmap='gray')
-
-        # draw edges by pts
-        for (s, e) in graph.edges():
-            ps = graph[s][e]['pts']
-            plt.plot(ps[:, 1], ps[:, 0], 'green')
-
-        # draw node by o
-        nodes = graph.nodes()
-        ps = np.array([nodes[i]['o'] for i in nodes])
-        plt.plot(ps[:, 1], ps[:, 0], 'r.')
-
-        # title and show
-        plt.title('Build Graph')
-        plt.show()
-
-    def example3(self):
-        img = Path(Path(__file__).parent.parent, "assets/red_black_cv.png")
-        self.skeleton_and_plot(img)
-        return  # so we can display in debug mode!
-
-    def example4(self):
-        img = Path(Path(__file__).parent.parent, "test/resources/biosynth_path_3.png")
-        self.skeleton_and_plot(img)
-        return  # so we can display in debug mode!
-
-    def skeleton_and_plot(self, img):
-        assert img is not None, "cannot read image"
-        img = skimage.io.imread(img, as_gray=True)
-        thresh = threshold_otsu(img)
-        binary = img < thresh  # swap <> to invert image
-        print(binary)
-        plt.imshow(binary)
-        # plt.imshow(inv_ske, cmap='gray')
-        plt.title('Build Graph1')
-        plt.show()
-        # ske = skeletonize(img).astype(np.uint16)
-        ske = skeletonize(binary)
-        print(ske)
-        print("========ske=========")
-        # inv_ske = util.invert(ske)
-        plt.imshow(ske)
-        # plt.imshow(inv_ske, cmap='gray')
-        plt.title('Build Graph2')
-        plt.show()
-        print("========inv=========")
-        inv_ske = ske
-        self.read_thinned_image_calculate_graph_and_plot(inv_ske)
-
-
-def main():
-    PmrSknw().example3()
-    PmrSknw().example4()
-
-
-if __name__ == '__main__':
-    main()
diff --git a/src/pyamiimage/old_code/preprocessing.py b/src/pyamiimage/old_code/preprocessing.py
deleted file mode 100644
index 51c2ee7..0000000
--- a/src/pyamiimage/old_code/preprocessing.py
+++ /dev/null
@@ -1,162 +0,0 @@
-import numpy as np
-from skimage import io
-from skimage.color.colorconv import rgb2gray
-import skimage
-from skimage import morphology
-from skimage import filters
-from pathlib import Path
-import matplotlib.pyplot as plt
-
-from pyimage.old_code.graph_lib import AmiGraph
-
-"""
-The ImageProcessor class is current in development by PMR and Anuv for preprocessing images
-as a part of the project: "Extraction of biosynthetic pathway from images"
-
-probably obsolete and being moved to AmiImage
-
-Some part of the code has been copied from ImageLib written by PMR for openDiagram
-We decided against continuing development on openDiagram library because the size
-of the repository exceeded 2 gigabytes
-
-The ImageLib module has been included in this repository for testing and reference 
-"""
-
-# setting a sample image for default path
-DEFAULT_PATH = "assets/purple_ocimum_basilicum.png"
-TEST_RESOURCES_DIR = Path(Path(__file__).parent.parent, "test/resources")
-# BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1.png")
-BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1_cropped_text_removed.png")
-
-
-class ImageProcessor:
-
-    def __init__(self) -> None:
-        self.image = None
-        self.inverted = None
-
-    # TODO is this required?
-    def load_image(self, path):
-        """
-        loads image with io.imread
-        resets self.image
-        input: path
-        returns: None if path is None
-        """
-        self.image = None
-        if path is not None:
-            self.image = io.imread(path)
-        return self.image
-
-    # TODO classmethod
-    def to_gray(self):
-        """convert existing self.image to grayscale
-        uses rgb2gray from skimage.color.colorconv
-        """
-        self.image_gray = None
-        if self.image is not None:
-            self.image_gray = rgb2gray(self.image)
-        return self.image_gray
-
-    # TODO classmethod
-    def invert(self, image):
-        """Inverts the brightness values of the image"""
-        self.inverted = skimage.util.invert(image)
-        return self.inverted
-
-    # TODO class method
-    def skeletonize(self, image):
-        """Returns a skeleton of the image"""
-        mask = morphology.skeletonize(image)
-        self.skeleton = np.zeros(self.image.shape)
-        self.skeleton[mask] = 1
-        # print("Skeleton Image: ", self.skeleton)
-        return self.skeleton
-
-    @classmethod
-    def threshold(cls, image):
-        """"Returns a binary image using a threshold value"""
-        threshold = filters.threshold_otsu(image)
-        # self.binary_image = np.zeros(self.image.shape)
-        # print(self.image.shape)
-        # idx = self.image > threshold
-        # print("Threshold Mask: ", idx)
-        # self.binary_image[idx] = 1
-        # print("Binary Image: ", self.binary_image)
-        binary_image = np.where(image >= threshold, 1, 0)
-        return binary_image
-
-    def show_image(self, image):
-        """
-        Shows self.image in a separate window
-        :param image:
-        """
-        if self.image is None:
-            self.load_image()
-        io.imshow(image)
-        io.show()
-        return True
-
-    def example1(self):
-        TEST_RESOURCES_DIR = Path(Path(__file__).parent.parent, "test/resources")
-        assert TEST_RESOURCES_DIR.isdir(), f"{TEST_RESOURCES_DIR} must be existing directory"
-        # BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1.png")
-        BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1_cropped_text_removed.png")
-        print(BIOSYNTH_PATH_IMAGE)
-        self.load_image(BIOSYNTH_PATH_IMAGE)
-        # print(self.image)
-        # print(self.image.shape)
-        # from skimage.exposure import histogram
-        # hist, hist_centers = histogram(self.threshold(self.image))
-        # print(hist)
-        # print(hist_centers)
-
-        self.show_image(self.image)
-        inverted_image = self.invert(self.image)
-        self.show_image(inverted_image)
-        binary_image = self.threshold(inverted_image)
-        self.show_image(binary_image)
-        skeleton = self.skeletonize(binary_image)
-        self.show_image(skeleton)
-
-    def example_skeletonize_extract_subgraphs(self):
-        resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-        image = Path(resources_dir, "biosynth_path_1_cropped_text_removed.png")
-        self.load_image(image)
-        skeleton1 = self.invert_threshold_skeletonize()
-        skeleton = skeleton1
-        skeleton = skeleton.astype(np.uint16)
-        # print("skeleton values: ", skeleton)
-        print("skeleton type: ", type(skeleton))
-        print("skeleton value type: ", type(skeleton[0][0]))
-        print("skeleton shape:", skeleton.shape)
-
-        graph = AmiGraph.create_ami_graph(skeleton)
-        print("node_dict", graph)
-
-        fig, ax = plt.subplots()  # note we must use plt.subplots, not plt.subplot
-        # maxx, maxy = self.get_maxx_maxy_non_pythonic(node_dict, nodes)
-        # for edge in self.edges:
-        #     self.plot_line(node_dict, edge[0], edge[1], maxy)
-        # fig.savefig(Path(Path(__file__).parent.parent, "temp", "plotarrows.png"))
-
-    @classmethod
-    def plot_line(cls, node_dict, node0, node1, ymax):
-        # print("node", node0, type(node0))
-        xy0 = node_dict[node0]
-        xy1 = node_dict[node1]
-        # print("xy0 xy1", xy0, xy1)
-        # x and y are swapped
-        plt.plot([xy0[1], xy1[1]], [ymax - xy0[0], ymax - xy1[0]], marker="")
-        # print(type(node0))
-
-
-
-def main():
-    image_processor = ImageProcessor()
-    image_processor.example1()
-    # image_processor.example_skeletonize_extract_subgraphs()
-
-
-if __name__ == '__main__':
-    main()
diff --git a/src/pyamiimage/plot_demo.py b/src/pyamiimage/plot_demo.py
deleted file mode 100644
index 5eef63f..0000000
--- a/src/pyamiimage/plot_demo.py
+++ /dev/null
@@ -1,1 +0,0 @@
-"""routines which contain interactive plots"""
\ No newline at end of file
diff --git a/src/pyamiimage/tesseract_new.py b/src/pyamiimage/tesseract_new.py
deleted file mode 100644
index 50ed52a..0000000
--- a/src/pyamiimage/tesseract_new.py
+++ /dev/null
@@ -1,84 +0,0 @@
-import numpy as np
-from pathlib import Path
-from matplotlib.image import imsave
-from pyamiimage.test.resources import Resources
-from pyamiimage.pyimage.ami_ocr import AmiOCR
-from pyamiimage.pyimage.ami_image import AmiImage
-from matplotlib import pyplot as plt
-from skimage import io
-# tess = Resources.TESSERACT1
-# tess = Resources.TESSERACT_BENG
-tess = Resources.BIOSYNTH2
-# tess = Resources.TESSERACT_GER2
-# tess = Resources.TESSERACT_ITA
-tess_ocr = AmiOCR(tess)
-tess_img = io.imread(tess)
-
-
-
-# words_with_bounding_boxes = tess_ocr.plot_bboxes_on_image(tess_img, words)
-# io.imshow(words_with_bounding_boxes)
-# io.show()
-
-# phrases = tess_ocr.get_phrases()
-# phrases_with_bounding_boxes = tess_ocr.plot_bboxes_on_image(tess_img, phrases)
-
-# for phrase in phrases:
-#     print(phrase)
-
-# io.imshow(phrases_with_bounding_boxes)
-# io.show()
-
-
-tess_img_gray = AmiImage.create_grayscale_from_image(tess_img)
-tess_img_bin = AmiImage.create_white_binary_from_image(tess_img_gray)
-tess_ocr = AmiOCR(image=tess_img_bin)
-# io.imshow(tess_img_bin)
-# io.show()
-
-words = tess_ocr.get_words()
-words_with_baseline  = tess_ocr.find_baseline(tess_ocr.hocr)
-for word in words:
-    print(word)
-
-# box_bin = AmiOCR.plot_bboxes_on_image(tess_img_bin, words)
-# io.imshow(box_bin)
-# io.show()
-
-patches = AmiOCR.bounding_box_patches(tess_img_bin, words)
-TESSERACT_TEMP_PATH = Path(Path(__file__).parent.parent, "temp/tesseract/")
-AmiImage.write_image_group(TESSERACT_TEMP_PATH, patches, filename="black_on_white", mkdir=False)
-
-def patches_pixel_stats(patches, signal_val, axis=1):
-    """sum of signal values along a axis in an array"""
-    signal = []
-    for patch in patches:
-        signal.append(np.count_nonzero(patch==signal_val, axis=axis))
-    signal = [np.array(item) for item in signal]
-    return signal
-
-signal = patches_pixel_stats(patches, signal_val=255, axis=1)
-
-row = [np.arange(0, item.shape[0]) for item in signal]
-import matplotlib.pyplot as plt
-
-for idx in range(36, 48):
-    im = plt.imread(Path(TESSERACT_TEMP_PATH, f"black_on_white{idx}.png"))
-    # implot = plt.imshow(im)
-    import scipy.ndimage as ndimage
-
-    angle = 90 # in degrees
-
-    new_data = ndimage.rotate(im, angle, reshape=True)
-
-    plt.imshow(new_data)
-
-    plt.plot(row[idx], signal[idx], color='red')
-
-    plt.show()
-# for x, y in zip(row, signal):
-#     plt.plot(x, y)
-#     plt.show()
-
-# AmiOCR.pretty_print_hocr(tess_ocr.hocr)
-# print(words_with_baseline[0].baseline)
diff --git a/src/pyamiimage/text_box.py b/src/pyamiimage/text_box.py
deleted file mode 100644
index 53f75c9..0000000
--- a/src/pyamiimage/text_box.py
+++ /dev/null
@@ -1,216 +0,0 @@
-from lxml import etree
-from lxml.etree import Element, QName
-from pathlib import Path
-import logging
-import matplotlib.pyplot as plt
-import matplotlib.patches as patches
-
-try:
-    from ..pyimage.bbox import BBox
-    from ..pyimage.tesseract_hocr import TesseractOCR
-except:
-    from pyimage.bbox import BBox
-    from pyimage.tesseract_hocr import TesseractOCR
-
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.DEBUG)
-
-
-class HocrText:
-
-    E_G = 'g'
-    E_RECT = 'rect'
-    E_SVG = 'svg'
-    E_TEXT = "text"
-
-    A_BBOX = "bbox"
-    A_FILL = "fill"
-    A_FONT_SIZE = "font-size"
-    A_FONT_FAMILY = "font-family"
-    A_HEIGHT = "height"
-    A_STROKE = "stroke"
-    A_STROKE_WIDTH = "stroke-width"
-    A_TITLE = "title"
-    A_WIDTH = "width"
-    A_XLINK = 'xlink'
-    A_X = "x"
-    A_Y = "y"
-
-    def parse_hocr_title(self, title):
-        """
-         title="bbox 336 76 1217 111; baseline -0.006 -9; x_size 28; x_descenders 6; x_ascenders 7"
-
-        :param title:
-        :return:
-        """
-        if title is None:
-            return None
-        parts = title.split("; ")
-        title_dict = {}
-        for part in parts:
-            vals = part.split()
-            kw = vals[0]
-            if kw == self.A_BBOX:
-                val = ((vals[1], vals[3]), (vals[2], vals[4]))
-            else:
-                val = vals[1:]
-            title_dict[kw] = val
-        return title_dict
-
-    def create_svg_from_hocr(self, hocr_html, target_svg=None):
-        """
-
-        :param hocr_html:
-        :type: string
-        :param target_svg: not yet used
-        :return:
-        """
-        html = etree.parse(hocr_html) 
-        word_spans = html.findall("//{http://www.w3.org/1999/xhtml}span[@class='ocrx_word']")
-        svg = Element(QName(XMLNamespaces.svg, self.E_SVG), nsmap={
-            self.E_SVG: XMLNamespaces.svg,
-            self.A_XLINK: XMLNamespaces.xlink,
-        })
-        for word_span in word_spans:
-            title = word_span.attrib[self.A_TITLE]
-            title_dict = self.parse_hocr_title(title)
-            bbox = title_dict[self.A_BBOX]
-            text = word_span.text
-            g = self.create_svg_text_box_from_hocr(bbox, text)
-            svg.append(g)
-        bb = etree.tostring(svg, encoding='utf-8', method='xml')
-        s = bb.decode("utf-8")
-        path_svg = Path(Path(__file__).parent.parent, "temp", "textbox.svg")
-        with open(path_svg, "w", encoding="UTF-8") as f:
-            f.write(s)
-            print(f"Wrote textboxes to {path_svg}")
-
-    def create_svg_text_box_from_hocr(self, bbox, txt):
-
-        g = Element(QName(XMLNamespaces.svg, self.E_G))
-        height = int(bbox[1][1]) - int(bbox[1][0])
-        print("height", height)
-
-        rect = Element(QName(XMLNamespaces.svg, self.E_RECT))
-        rect.attrib[self.A_X] = bbox[0][0]
-        rect.attrib[self.A_WIDTH] = str(int(bbox[0][1]) - int(bbox[0][0]))
-        rect.attrib[self.A_Y] = str(int(bbox[1][0]))  # kludge for offset of inverted text
-        rect.attrib[self.A_HEIGHT] = str(height)
-        rect.attrib[self.A_STROKE_WIDTH] = "1.0"
-        rect.attrib[self.A_STROKE] = "red"
-        rect.attrib[self.A_FILL] = "none"
-        g.append(rect)
-
-        text = Element(QName(XMLNamespaces.svg, self.E_TEXT))
-        text.attrib[self.A_X] = bbox[0][0]
-        text.attrib[self.A_Y] = str(int(bbox[1][0]) + height)
-        text.attrib[self.A_FONT_SIZE] = str(0.9 * height)
-        text.attrib[self.A_STROKE] = "blue"
-        text.attrib[self.A_FONT_FAMILY] = "sans-serif"
-
-        text.text = txt
-        g.append(text)
-        return g
-
-
-class TextBox:
-    def __init__(self):
-        self.text = None
-        self.bbox = None
-
-    @classmethod
-    def find_text_boxes(cls, elem):
-        text_boxes = []
-        phrases, bboxes = TesseractOCR.find_phrases(elem)
-        # TODO mend this
-        for phrase, bbox in zip(phrases, bboxes):
-            assert type(phrase) is str, f"type of phrase {type(phrase)}"
-            text_box = TextBox.create_text_box(phrase, bbox)
-            text_boxes.append(text_box)
-        return text_boxes
-
-    @classmethod
-    def create_text_box(cls, text, bbox):
-        assert type(text) is str, f"{type(text)} of {text} should be str"
-        assert type(bbox) is list
-        assert len(bbox) == 4
-        text_box = TextBox()
-        text_box.text = text
-        text_box.bbox = BBox([[bbox[0], bbox[2]], [bbox[1], bbox[3]]])
-        return text_box
-
-    def create_svg(self):
-        logger.warning("SVG NYI")
-
-    @classmethod
-    def find_and_plot_text_boxes(cls, elem, image):
-        text_boxes = TextBox.find_text_boxes(elem)
-        text_boxes1 = []
-        for text_box in text_boxes:
-            if TextUtil.is_text_from_tesseract(text_box.text):
-                assert type(text_box) is TextBox, f"cannot add {type(text_box)} as TextBox"
-                text_boxes1.append(text_box)
-        assert len(text_boxes1) > 0, "require non_zero count of text_boxes"
-        logger.info(f"{__name__} plotting {len(text_boxes1)} text_boxes of type {type(text_boxes1[0])}")
-        fig, ax = plt.subplots()
-        TextBox.plot_text_box_boxes(image, ax, text_boxes1)
-        fig.tight_layout()
-        plt.show()
-
-    @classmethod
-    def plot_text_box_boxes(cls, img_array, ax, text_boxes1):
-        for text_box in text_boxes1:
-            assert type(text_box) is TextBox, f"should be TextBox found {type(text_box)}"
-            assert type(text_box.bbox) is BBox, f"expected BBox found {type(text_box.bbox)}"
-            TextBox.add_bbox_rect(ax, text_box.bbox)
-        ax.imshow(img_array)
-
-    @classmethod
-    def add_bbox_rect(cls, axis, bbox, linewidth=1, edgecolor="red", facecolor="none"):
-        """
-        adds rectangle to axis subplot
-        :param axis: axis from matplotlib subplots
-        :param bbox: BBox from pyamiimage or its ranges
-        :param linewidth: linewidth of plotted rect (1)
-        :param edgecolor: stroke color of line ("red")
-        :param facecolor: fill of rect ("none")
-        :return:
-        """
-        assert type(bbox) is BBox, f"bbox should be BBox, found {type(bbox)}"
-        xyr = bbox.xy_ranges
-        rect = patches.Rectangle((xyr[0][0], xyr[1][0]), bbox.get_width(), bbox.get_height(),
-                                 linewidth=linewidth, edgecolor=edgecolor, facecolor=facecolor)
-        axis.add_patch(rect)
-
-
-class TextUtil:
-    @classmethod
-    def is_text_from_tesseract(cls, text):
-        """
-        some empirical acceptance of Tesseract output
-        allows isalnum characters, spaces, commas
-        :param text: extracted text
-        :return: False if probably a garble
-        """
-        # print("text util ", text)
-        # logger.warning(f"txt: {text}")
-        txt = text
-        if len(txt) <= 1:
-            return False
-        # fail on repeated '-' or '.' as probably a garble from horizontal graphic line
-        if '--' in txt or '..' in txt:
-            return False
-        # remove common punctuation
-        for s in "-.,;?()[]+ ":
-            txt = txt.replace(s, "")
-        # fail on any non-alphanum string (includin
-        if not txt.isalnum():
-            logger.warning(f"{__name__} rejected {text}")
-            return False
-
-        return True
-
-
-class XMLNamespaces:
-    svg = "http://www.w3.org/2000/svg"
-    xlink = "http://www.w3.org/1999/xlink"


Test patch:
diff --git a/pyimage/old_code/test_graph_lib.py b/pyimage/old_code/test_graph_lib.py
deleted file mode 100644
index 3750fcd..0000000
--- a/pyimage/old_code/test_graph_lib.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""graph classes and algorithms for managing pixel analyses"""
-
-
-# import numpy
-from pathlib import Path
-from skimage import io
-
-"""
-These tests are designed to test the preprocessing module
-containing the graph_lib module 
-
-These tests are for Test Driven Development
-"""
-
-
-class TestGraphLib:
-
-    @classmethod
-    def create_from(cls, pathname):
-        img = io.imread(pathname)
-        return img
-
-    # apparently doesn't like init in Test class
-    # def __init__(self):
-    TEST_DIR = Path(Path(__file__).parent, "../../test/resources")
-    SIMPLE_2N1E = "simple_2n1e"
-    SIMPLE_2N1E_PATH = Path(TEST_DIR, SIMPLE_2N1E+".png")
-    GRAPH_DICT = {
-    }
-    EDGES1 = [(0, 2), (1, 4), (2, 4), (2, 3), (2, 7), (4, 5), (4, 6), (8, 19), (9, 19), (10, 12),
-              (11, 13), (12, 13), (12, 18), (13, 14), (13, 15), (16, 18), (17, 18), (18, 20), (19, 26),
-              (21, 24), (22, 24), (23, 24), (24, 25)]
-    NODES1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
-
-    def setup_method(self, method):
-        """setup any state tied to the execution of the given method in a
-        class.  setup_method is invoked for every test method of a class.
-        """
-        assert self.SIMPLE_2N1E_PATH.exists(), f"{self.SIMPLE_2N1E_PATH} not found"
-        png = self.create_from(self.SIMPLE_2N1E_PATH)
-        assert png is not None, f"cannot read PNG {self.SIMPLE_2N1E_PATH} "
-        self.GRAPH_DICT = {
-            self.SIMPLE_2N1E: png,
-        }
-
-    def teardown_method(self, method):
-        """teardown any state that was previously setup with a setup_method
-        call."""
-        pass
-
-    def test_load_create_image(self):
-        img = self.GRAPH_DICT[self.SIMPLE_2N1E]
-        assert img is not None
-        width = 5
-        height = 4
-        nplanes = 3
-        assert img.shape == (height, width, nplanes)
-
-    def test_img_contents(self):
-        img = self.GRAPH_DICT[self.SIMPLE_2N1E]
-        # print("img", img)
-
diff --git a/pyimage/old_code/test_image_lib.py b/pyimage/old_code/test_image_lib.py
deleted file mode 100644
index d0588a5..0000000
--- a/pyimage/old_code/test_image_lib.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# tests existing routines in image_lib
-
-from pyimage.old_code.image_lib import ImageLib
-import numpy
-
-"""
-These tests are for experimenting with ImageLib class 
-"""
-
-
-def test_circle_points():
-    """Old circle_points generates an array of numpy points on a circle
-    This is primarily to test thetb the test system is working
-    """
-    image_lib = ImageLib()
-    # circle_points(200, [80, 250], 80)[:-1]
-    resolution = 200  # ? number of points?
-    center = [80, 250]
-    radius = 80
-    points = image_lib.circle_points(resolution, center, radius)[:-1]
-    assert type(points) is numpy.ndarray
-    assert len(points) == 199
-    assert points[0][0] == 330.
-    assert points[0][1] == 80.
-
-
-def test_image_import():
-    # """Check that ImageLib reads an image and retains the shape"""
-    # image_lib = ImageLib()
-    # image_lib.image_import()
-    # assert image_lib.image is not None
-    # #  use == for equality, not "is"
-    # assert image_lib.image.shape == (923, 709, 3)
-    pass
diff --git a/pyimage/old_code/test_preprocessing.py b/pyimage/old_code/test_preprocessing.py
deleted file mode 100644
index 59f9562..0000000
--- a/pyimage/old_code/test_preprocessing.py
+++ /dev/null
@@ -1,100 +0,0 @@
-from pyimage.old_code.preprocessing import ImageProcessor
-import numpy
-from pathlib import Path
-
-"""
-These tests are desinged to test the preprocessing module
-containing the ImageProcessing class 
-
-These tests are for Test Driven Development
-"""
-
-class TestImageCode():
-
-    # apparently doesn't like init in Test class
-    # def __init__(self):
-    #     self.image_processor = None
-    #     self.default_image = None
-    #     self.default_image_gray = None
-
-    TEST_DIR = Path(__file__).parent
-    PYAMIIMAGE_DIR = TEST_DIR.parent
-    ASSETS_DIR = Path(PYAMIIMAGE_DIR, "assets").absolute()
-    assert ASSETS_DIR.exists(), "assets dir should exist"
-    OCIMUM_IMAGE = Path(ASSETS_DIR, "purple_ocimum_basilicum.png")
-    assert OCIMUM_IMAGE.exists(), "ocimum image should exist"
-    TEST_RESOURCES_DIR = Path(Path(__file__).parent, "../../test/resources")
-    RED_BLACK_IMAGE = Path(TEST_RESOURCES_DIR, "red_black_cv.png")
-    assert RED_BLACK_IMAGE.exists(), f"red_black_image {RED_BLACK_IMAGE} should exist"
-    BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1.png")
-    assert BIOSYNTH_PATH_IMAGE.exists(), f"biosynthetic pathway {BIOSYNTH_PATH_IMAGE} should exist"
-
-    def setup_method(self, method):
-        """setup any state tied to the execution of the given method in a
-        class.  setup_method is invoked for every test method of a class.
-        """
-        self.image_processor = ImageProcessor()
-        self.default_image = self.image_processor.load_image(self.BIOSYNTH_PATH_IMAGE)
-        assert self.default_image is not None
-        self.default_image_gray = self.image_processor.to_gray()
-        assert self.default_image_gray is not None
-
-
-    def teardown_method(self, method):
-        """teardown any state that was previously setup with a setup_method
-        call."""
-        self.image_processor = None
-        self.default_image = None
-        self.default_image_gray = None
-
-
-    # @pytest.fixture()
-    # def resource():
-    #     print("setup")
-    #     yield "resource"
-    #     print("teardown")
-
-    def test_load_image(self):
-        self.image_processor.load_image(self.OCIMUM_IMAGE)
-        assert self.image_processor.image is not None
-
-    def test_to_gray_type(self):
-        assert self.default_image_gray is not None
-        assert type(self.default_image_gray) is numpy.ndarray
-
-    def test_to_gray_shape(self):
-        # converts a 3 dimensional array(length, width & channel) to 2 dimensional array (length, width)
-        # this is BIOSYNTH_PATH_IMAGE
-        assert self.default_image_gray is not None
-        assert len(self.default_image_gray.shape) == 2
-        height = 1167
-        width = 1515
-        assert self.default_image_gray.shape == (height, width)
-
-    def test_invert(self):
-        """Inverts the default image"""
-        # assert self.image_processor.image is not None
-        # assert self.image_processor.invert(self.image_processor.image)
-        pass
-
-    def test_skeletonize(self):
-        # assert self.image_processor.skeletonize(self.image_processor.image) is not None
-        pass
-
-    def test_threshold(self):
-        image = self.image_processor.image
-        assert self.image_processor.threshold(image) is not None
-
-    def test_interactive_show_image(self):
-        """displays grayscale image and blocks on user """
-        interactive = False
-        if interactive:
-            # will block on user input
-            assert self.image_processor.show_image(self.image_processor.image)
-        assert True, "finished display"
-
-# NOTES
-# progress on extracting graph?
-# https://stackoverflow.com/questions/57029293/calculating-a-graph-from-a-skeletonized-image
-# https://github.com/Image-Py/sknw#find-path
-# https://stackoverflow.com/questions/63653267/how-to-create-a-graph-with-an-images-pixel
\ No newline at end of file
diff --git a/src/pyamiimage/old_code/test_graph_lib.py b/src/pyamiimage/old_code/test_graph_lib.py
deleted file mode 100644
index 3750fcd..0000000
--- a/src/pyamiimage/old_code/test_graph_lib.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""graph classes and algorithms for managing pixel analyses"""
-
-
-# import numpy
-from pathlib import Path
-from skimage import io
-
-"""
-These tests are designed to test the preprocessing module
-containing the graph_lib module 
-
-These tests are for Test Driven Development
-"""
-
-
-class TestGraphLib:
-
-    @classmethod
-    def create_from(cls, pathname):
-        img = io.imread(pathname)
-        return img
-
-    # apparently doesn't like init in Test class
-    # def __init__(self):
-    TEST_DIR = Path(Path(__file__).parent, "../../test/resources")
-    SIMPLE_2N1E = "simple_2n1e"
-    SIMPLE_2N1E_PATH = Path(TEST_DIR, SIMPLE_2N1E+".png")
-    GRAPH_DICT = {
-    }
-    EDGES1 = [(0, 2), (1, 4), (2, 4), (2, 3), (2, 7), (4, 5), (4, 6), (8, 19), (9, 19), (10, 12),
-              (11, 13), (12, 13), (12, 18), (13, 14), (13, 15), (16, 18), (17, 18), (18, 20), (19, 26),
-              (21, 24), (22, 24), (23, 24), (24, 25)]
-    NODES1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
-
-    def setup_method(self, method):
-        """setup any state tied to the execution of the given method in a
-        class.  setup_method is invoked for every test method of a class.
-        """
-        assert self.SIMPLE_2N1E_PATH.exists(), f"{self.SIMPLE_2N1E_PATH} not found"
-        png = self.create_from(self.SIMPLE_2N1E_PATH)
-        assert png is not None, f"cannot read PNG {self.SIMPLE_2N1E_PATH} "
-        self.GRAPH_DICT = {
-            self.SIMPLE_2N1E: png,
-        }
-
-    def teardown_method(self, method):
-        """teardown any state that was previously setup with a setup_method
-        call."""
-        pass
-
-    def test_load_create_image(self):
-        img = self.GRAPH_DICT[self.SIMPLE_2N1E]
-        assert img is not None
-        width = 5
-        height = 4
-        nplanes = 3
-        assert img.shape == (height, width, nplanes)
-
-    def test_img_contents(self):
-        img = self.GRAPH_DICT[self.SIMPLE_2N1E]
-        # print("img", img)
-
diff --git a/src/pyamiimage/old_code/test_image_lib.py b/src/pyamiimage/old_code/test_image_lib.py
deleted file mode 100644
index d0588a5..0000000
--- a/src/pyamiimage/old_code/test_image_lib.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# tests existing routines in image_lib
-
-from pyimage.old_code.image_lib import ImageLib
-import numpy
-
-"""
-These tests are for experimenting with ImageLib class 
-"""
-
-
-def test_circle_points():
-    """Old circle_points generates an array of numpy points on a circle
-    This is primarily to test thetb the test system is working
-    """
-    image_lib = ImageLib()
-    # circle_points(200, [80, 250], 80)[:-1]
-    resolution = 200  # ? number of points?
-    center = [80, 250]
-    radius = 80
-    points = image_lib.circle_points(resolution, center, radius)[:-1]
-    assert type(points) is numpy.ndarray
-    assert len(points) == 199
-    assert points[0][0] == 330.
-    assert points[0][1] == 80.
-
-
-def test_image_import():
-    # """Check that ImageLib reads an image and retains the shape"""
-    # image_lib = ImageLib()
-    # image_lib.image_import()
-    # assert image_lib.image is not None
-    # #  use == for equality, not "is"
-    # assert image_lib.image.shape == (923, 709, 3)
-    pass
diff --git a/src/pyamiimage/old_code/test_preprocessing.py b/src/pyamiimage/old_code/test_preprocessing.py
deleted file mode 100644
index 59f9562..0000000
--- a/src/pyamiimage/old_code/test_preprocessing.py
+++ /dev/null
@@ -1,100 +0,0 @@
-from pyimage.old_code.preprocessing import ImageProcessor
-import numpy
-from pathlib import Path
-
-"""
-These tests are desinged to test the preprocessing module
-containing the ImageProcessing class 
-
-These tests are for Test Driven Development
-"""
-
-class TestImageCode():
-
-    # apparently doesn't like init in Test class
-    # def __init__(self):
-    #     self.image_processor = None
-    #     self.default_image = None
-    #     self.default_image_gray = None
-
-    TEST_DIR = Path(__file__).parent
-    PYAMIIMAGE_DIR = TEST_DIR.parent
-    ASSETS_DIR = Path(PYAMIIMAGE_DIR, "assets").absolute()
-    assert ASSETS_DIR.exists(), "assets dir should exist"
-    OCIMUM_IMAGE = Path(ASSETS_DIR, "purple_ocimum_basilicum.png")
-    assert OCIMUM_IMAGE.exists(), "ocimum image should exist"
-    TEST_RESOURCES_DIR = Path(Path(__file__).parent, "../../test/resources")
-    RED_BLACK_IMAGE = Path(TEST_RESOURCES_DIR, "red_black_cv.png")
-    assert RED_BLACK_IMAGE.exists(), f"red_black_image {RED_BLACK_IMAGE} should exist"
-    BIOSYNTH_PATH_IMAGE = Path(TEST_RESOURCES_DIR, "biosynth_path_1.png")
-    assert BIOSYNTH_PATH_IMAGE.exists(), f"biosynthetic pathway {BIOSYNTH_PATH_IMAGE} should exist"
-
-    def setup_method(self, method):
-        """setup any state tied to the execution of the given method in a
-        class.  setup_method is invoked for every test method of a class.
-        """
-        self.image_processor = ImageProcessor()
-        self.default_image = self.image_processor.load_image(self.BIOSYNTH_PATH_IMAGE)
-        assert self.default_image is not None
-        self.default_image_gray = self.image_processor.to_gray()
-        assert self.default_image_gray is not None
-
-
-    def teardown_method(self, method):
-        """teardown any state that was previously setup with a setup_method
-        call."""
-        self.image_processor = None
-        self.default_image = None
-        self.default_image_gray = None
-
-
-    # @pytest.fixture()
-    # def resource():
-    #     print("setup")
-    #     yield "resource"
-    #     print("teardown")
-
-    def test_load_image(self):
-        self.image_processor.load_image(self.OCIMUM_IMAGE)
-        assert self.image_processor.image is not None
-
-    def test_to_gray_type(self):
-        assert self.default_image_gray is not None
-        assert type(self.default_image_gray) is numpy.ndarray
-
-    def test_to_gray_shape(self):
-        # converts a 3 dimensional array(length, width & channel) to 2 dimensional array (length, width)
-        # this is BIOSYNTH_PATH_IMAGE
-        assert self.default_image_gray is not None
-        assert len(self.default_image_gray.shape) == 2
-        height = 1167
-        width = 1515
-        assert self.default_image_gray.shape == (height, width)
-
-    def test_invert(self):
-        """Inverts the default image"""
-        # assert self.image_processor.image is not None
-        # assert self.image_processor.invert(self.image_processor.image)
-        pass
-
-    def test_skeletonize(self):
-        # assert self.image_processor.skeletonize(self.image_processor.image) is not None
-        pass
-
-    def test_threshold(self):
-        image = self.image_processor.image
-        assert self.image_processor.threshold(image) is not None
-
-    def test_interactive_show_image(self):
-        """displays grayscale image and blocks on user """
-        interactive = False
-        if interactive:
-            # will block on user input
-            assert self.image_processor.show_image(self.image_processor.image)
-        assert True, "finished display"
-
-# NOTES
-# progress on extracting graph?
-# https://stackoverflow.com/questions/57029293/calculating-a-graph-from-a-skeletonized-image
-# https://github.com/Image-Py/sknw#find-path
-# https://stackoverflow.com/questions/63653267/how-to-create-a-graph-with-an-images-pixel
\ No newline at end of file
diff --git a/test/__init__.py b/test/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML.jpeg b/test/alex_pico/13068_2019_1355_Fig4_HTML.jpeg
deleted file mode 100644
index 7894d51..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML.jpeg and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p0.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p0.png
deleted file mode 100644
index 0196dc1..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p0.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p1.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p1.png
deleted file mode 100644
index 4c42601..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p1.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p10.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p10.png
deleted file mode 100644
index 34a19f4..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p10.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p11.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p11.png
deleted file mode 100644
index 31ee4fb..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p11.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p12.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p12.png
deleted file mode 100644
index 6fa66d0..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p12.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p13.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p13.png
deleted file mode 100644
index 1a8b6b1..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p13.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p14.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p14.png
deleted file mode 100644
index ef9dfdb..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p14.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p15.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p15.png
deleted file mode 100644
index 036687d..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p15.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p2.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p2.png
deleted file mode 100644
index d11fa5f..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p2.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p3.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p3.png
deleted file mode 100644
index 3c1f93e..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p3.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p4.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p4.png
deleted file mode 100644
index 86df271..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p4.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p5.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p5.png
deleted file mode 100644
index 926d2e3..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p5.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p6.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p6.png
deleted file mode 100644
index bd16bcf..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p6.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p7.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p7.png
deleted file mode 100644
index 99e2839..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p7.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p8.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p8.png
deleted file mode 100644
index 07a1864..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p8.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/p9.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/p9.png
deleted file mode 100644
index 868aea2..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/p9.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/palette.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/palette.png
deleted file mode 100644
index 403515d..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/palette.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/palette_rgb.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/palette_rgb.png
deleted file mode 100644
index 85fae2f..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/palette_rgb.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/rgb.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/rgb.png
deleted file mode 100644
index 80749d7..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/rgb.png and /dev/null differ
diff --git a/test/alex_pico/13068_2019_1355_Fig4_HTML/single.png b/test/alex_pico/13068_2019_1355_Fig4_HTML/single.png
deleted file mode 100644
index 806a48d..0000000
Binary files a/test/alex_pico/13068_2019_1355_Fig4_HTML/single.png and /dev/null differ
diff --git a/test/alex_pico/13068_default.txt b/test/alex_pico/13068_default.txt
deleted file mode 100644
index c300383..0000000
--- a/test/alex_pico/13068_default.txt
+++ /dev/null
@@ -1,388 +0,0 @@
-Satta!
-
-Petes
-
-se
-
-4
-
-Caran
-
-MEP pathway
-
-MVA pathway
-
-cas
-
->
-
-a
-
-seonamee ws
-
-¢
-
-RQ
-
-Zeina
-
-ts
-
-acco
-
-ee ee
-
-‘waneeeee] o>
-
-—
-
-ees
-
-ie
-
-EEE Ss.
-
--!
-
-Bx
-
-ts
-
-—
-
-ve
-
-Es
-
-co:
-
-Troy D yan phapne
-
-Atego
-
-ay
-
-=
-
-TGs
-
-cre
-
-iin
-
-i
-
-a
-
-a
-
-=
-
-eee
-
-Lye
-
-seaman oosscowes
-
-vor
-
-280
-
-‘HEE
-
-jest
-
-Lid
-
-8
-
-Dcwoiee
-
-essai
-
-a nee one
-
-‘saasanaa
-
-Ye
-
-KA
-
-tuts
-
-ier
-
-amtaaee /0IGR
-
-oe an
-
-=
-
-er.
-
-“ae
-
-Pd
-
-Ge
-
-IEEE
-
-bee
-
-st
-
-ses
-
-veoueo wa
-
-Tipo
-
-A
-
-jac
-
-eee
-
-RTO
-
-Hi
-
-tt aay
-
-Fao
-
-ei
-
-=
-
-aKr
-
-ee eee
-
-=
-
-“apenpbocyatyh2C mc
-
-Degli pope
-
-|
-
-B-cyposanti
-
-co
-
-a
-
-‘Garbeopene )
-
-DS
-
-cs)
-
-=
-
-nz]
-
-Saat wos
-
-=] ex
-
--_
-
-EW
-
-aa
-
-HHEE)
-
-ex
-
-wk
-
-Enna
-
-Fim
-
-ona
-
-ee
-
-Feet
-
-Tr)
-
-oom
-
-=o
-
-Newen
-
-=
-
-Cee
-
-slap
-
-7s,
-
-—
-
-ioe
-
-DE
-
-oa!
-
-a
-
-pcan 3m
-
-Col haipbose
-
-perborate
-
-‘pen
-
-Oil-body
-
-es
-
-le
-
-"
-
-As
-
-xanthin
-
-ester
-
-BY
-
-Dinette
-
-a
-
-HEHE Ss
-
-=a
-
-an
-
-oy
-
-inospat
-
-ee:
-
-eu
-
-rene
-
-sipvopine
-
-—
-
-as
-
-‘Ceamsesany
-
-roi
-
-==.
-
-ennanae| oe -
-
-rors
-
-com
-
-ue
-
-4
-
-eee
-
-‘Ubi oa te
-
-SSeg0
-
-geal
-
-‘aces
-
-‘pte
-
-osm
-
-unene
-
-/
-
-Poot
-
-ara
-
-2
-
-vy
-
-iphone
-
-Faneyt
-
-oy
-
-1s
-
-meson
-
-ADDS
-
-Se,
-
-bo
-
-<8,
-
-ee
-
-ea
-
-ze
-
-come oS.
-
-PR
-
-==
-
-ao
-
-Darrell
-
-Neve
-
-Preion
-
-(
-
-‘now
-
-i
-
-ochre
-
-yy
-
-poste
-
-[somes
-
-——
-
-isd OMCs ye NS, oper. SRE er ee
-
-}
-
\ No newline at end of file
diff --git a/test/alex_pico/emss-81481-f001.png b/test/alex_pico/emss-81481-f001.png
deleted file mode 100644
index 90bec97..0000000
Binary files a/test/alex_pico/emss-81481-f001.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p0.png b/test/alex_pico/emss-81481-f001/p0.png
deleted file mode 100644
index ae1ae08..0000000
Binary files a/test/alex_pico/emss-81481-f001/p0.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p1.png b/test/alex_pico/emss-81481-f001/p1.png
deleted file mode 100644
index 364ee08..0000000
Binary files a/test/alex_pico/emss-81481-f001/p1.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p10.png b/test/alex_pico/emss-81481-f001/p10.png
deleted file mode 100644
index 275f979..0000000
Binary files a/test/alex_pico/emss-81481-f001/p10.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p11.png b/test/alex_pico/emss-81481-f001/p11.png
deleted file mode 100644
index f38ce2b..0000000
Binary files a/test/alex_pico/emss-81481-f001/p11.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p12.png b/test/alex_pico/emss-81481-f001/p12.png
deleted file mode 100644
index c00d719..0000000
Binary files a/test/alex_pico/emss-81481-f001/p12.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p13.png b/test/alex_pico/emss-81481-f001/p13.png
deleted file mode 100644
index e627bbe..0000000
Binary files a/test/alex_pico/emss-81481-f001/p13.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p14.png b/test/alex_pico/emss-81481-f001/p14.png
deleted file mode 100644
index b3d8962..0000000
Binary files a/test/alex_pico/emss-81481-f001/p14.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p15.png b/test/alex_pico/emss-81481-f001/p15.png
deleted file mode 100644
index 94882d0..0000000
Binary files a/test/alex_pico/emss-81481-f001/p15.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p2.png b/test/alex_pico/emss-81481-f001/p2.png
deleted file mode 100644
index e678fc2..0000000
Binary files a/test/alex_pico/emss-81481-f001/p2.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p3.png b/test/alex_pico/emss-81481-f001/p3.png
deleted file mode 100644
index 685a3d9..0000000
Binary files a/test/alex_pico/emss-81481-f001/p3.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p4.png b/test/alex_pico/emss-81481-f001/p4.png
deleted file mode 100644
index 56e057f..0000000
Binary files a/test/alex_pico/emss-81481-f001/p4.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p5.png b/test/alex_pico/emss-81481-f001/p5.png
deleted file mode 100644
index c4cefe7..0000000
Binary files a/test/alex_pico/emss-81481-f001/p5.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p6.png b/test/alex_pico/emss-81481-f001/p6.png
deleted file mode 100644
index 814e670..0000000
Binary files a/test/alex_pico/emss-81481-f001/p6.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p7.png b/test/alex_pico/emss-81481-f001/p7.png
deleted file mode 100644
index bcf5d80..0000000
Binary files a/test/alex_pico/emss-81481-f001/p7.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p8.png b/test/alex_pico/emss-81481-f001/p8.png
deleted file mode 100644
index c1d3c12..0000000
Binary files a/test/alex_pico/emss-81481-f001/p8.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/p9.png b/test/alex_pico/emss-81481-f001/p9.png
deleted file mode 100644
index 847f9a6..0000000
Binary files a/test/alex_pico/emss-81481-f001/p9.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/palette.png b/test/alex_pico/emss-81481-f001/palette.png
deleted file mode 100644
index 09575d3..0000000
Binary files a/test/alex_pico/emss-81481-f001/palette.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/rgb.png b/test/alex_pico/emss-81481-f001/rgb.png
deleted file mode 100644
index b440729..0000000
Binary files a/test/alex_pico/emss-81481-f001/rgb.png and /dev/null differ
diff --git a/test/alex_pico/emss-81481-f001/single.png b/test/alex_pico/emss-81481-f001/single.png
deleted file mode 100644
index 3eabdbe..0000000
Binary files a/test/alex_pico/emss-81481-f001/single.png and /dev/null differ
diff --git a/test/alex_pico/emss_default.txt b/test/alex_pico/emss_default.txt
deleted file mode 100644
index 00a36c5..0000000
--- a/test/alex_pico/emss_default.txt
+++ /dev/null
@@ -1,112 +0,0 @@
-Chloroplast
-
-Shikimate
-
-Cytosol
-
-cep ——+ pathway
-
-G6P
-
-GeP
-
-Ze
-
-P,
-
-Starch
-
--PGA
-
-'3-PGA
-
-4 Heo;
-
-2PGA +—> PEP
-
-PEP.
-
-> OAR
-
-Ona
-
-aon
-
-na
-
-?
-
-|—>Malate
-
-GaP + Pyr,
-
-‘yr
-
-Oxidative pentose
-
-Amino
-
-N
-
-MEP
-
-om
-
-pathway
-
-Acetaldehyde
-
-Acetaldehyde
-
-kp
-
-co,
-
-MoO 5 RE FSM
-
-etic acid
-
-‘Methanol
-
-MCT
-
-Formaldehyde:
-
-CpP-ME
-
-sto
-
-Acetyl CoA
-
-eye
-
-)P-ME2P
-
-E2
-
-Fally acid
-
-HMG-CoA
-
-C, and C;
-
-metabolism phosphate pathway
-
-acid
-
-Aransas
-
-HGR H-Lov
-
-sonal,
-
-fE-cDP
-
-HOR
-
-Mevalonate
-
-MVA +
-
-x
-
\ No newline at end of file
diff --git a/test/alex_pico/fmicb-09-02460-g001.jpeg b/test/alex_pico/fmicb-09-02460-g001.jpeg
deleted file mode 100644
index 7cadd11..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001.jpeg and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p0.png b/test/alex_pico/fmicb-09-02460-g001/p0.png
deleted file mode 100644
index a5ac242..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p0.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p1.png b/test/alex_pico/fmicb-09-02460-g001/p1.png
deleted file mode 100644
index 6c36ea8..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p1.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p10.png b/test/alex_pico/fmicb-09-02460-g001/p10.png
deleted file mode 100644
index 69f3e59..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p10.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p11.png b/test/alex_pico/fmicb-09-02460-g001/p11.png
deleted file mode 100644
index 36d72d6..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p11.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p12.png b/test/alex_pico/fmicb-09-02460-g001/p12.png
deleted file mode 100644
index 5b24ca7..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p12.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p13.png b/test/alex_pico/fmicb-09-02460-g001/p13.png
deleted file mode 100644
index c38ad9c..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p13.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p14.png b/test/alex_pico/fmicb-09-02460-g001/p14.png
deleted file mode 100644
index 8e69712..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p14.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p15.png b/test/alex_pico/fmicb-09-02460-g001/p15.png
deleted file mode 100644
index 2a1584e..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p15.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p2.png b/test/alex_pico/fmicb-09-02460-g001/p2.png
deleted file mode 100644
index 9fc0d39..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p2.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p3.png b/test/alex_pico/fmicb-09-02460-g001/p3.png
deleted file mode 100644
index 29d3bcc..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p3.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p4.png b/test/alex_pico/fmicb-09-02460-g001/p4.png
deleted file mode 100644
index 5f67174..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p4.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p5.png b/test/alex_pico/fmicb-09-02460-g001/p5.png
deleted file mode 100644
index f47f506..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p5.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p6.png b/test/alex_pico/fmicb-09-02460-g001/p6.png
deleted file mode 100644
index 32bd622..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p6.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p7.png b/test/alex_pico/fmicb-09-02460-g001/p7.png
deleted file mode 100644
index 3415de6..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p7.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p8.png b/test/alex_pico/fmicb-09-02460-g001/p8.png
deleted file mode 100644
index 53c4cdc..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p8.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/p9.png b/test/alex_pico/fmicb-09-02460-g001/p9.png
deleted file mode 100644
index ce2be85..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/p9.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/palette.png b/test/alex_pico/fmicb-09-02460-g001/palette.png
deleted file mode 100644
index 328fc63..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/palette.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/palette_rgb.png b/test/alex_pico/fmicb-09-02460-g001/palette_rgb.png
deleted file mode 100644
index e649fb9..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/palette_rgb.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/rgb.png b/test/alex_pico/fmicb-09-02460-g001/rgb.png
deleted file mode 100644
index 6bf27d2..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/rgb.png and /dev/null differ
diff --git a/test/alex_pico/fmicb-09-02460-g001/single.png b/test/alex_pico/fmicb-09-02460-g001/single.png
deleted file mode 100644
index b7cbdb7..0000000
Binary files a/test/alex_pico/fmicb-09-02460-g001/single.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004.jpg b/test/alex_pico/pone.0054762.g004.jpg
deleted file mode 100644
index 26d79a7..0000000
Binary files a/test/alex_pico/pone.0054762.g004.jpg and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p0.png b/test/alex_pico/pone.0054762.g004/p0.png
deleted file mode 100644
index f521a1d..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p0.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p1.png b/test/alex_pico/pone.0054762.g004/p1.png
deleted file mode 100644
index 63ec2c0..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p1.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p10.png b/test/alex_pico/pone.0054762.g004/p10.png
deleted file mode 100644
index 8fc8735..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p10.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p11.png b/test/alex_pico/pone.0054762.g004/p11.png
deleted file mode 100644
index 5de4d60..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p11.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p12.png b/test/alex_pico/pone.0054762.g004/p12.png
deleted file mode 100644
index 8b057c7..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p12.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p13.png b/test/alex_pico/pone.0054762.g004/p13.png
deleted file mode 100644
index 832e8d7..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p13.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p14.png b/test/alex_pico/pone.0054762.g004/p14.png
deleted file mode 100644
index c73b671..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p14.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p15.png b/test/alex_pico/pone.0054762.g004/p15.png
deleted file mode 100644
index 2d7d5a4..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p15.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p2.png b/test/alex_pico/pone.0054762.g004/p2.png
deleted file mode 100644
index 7344907..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p2.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p3.png b/test/alex_pico/pone.0054762.g004/p3.png
deleted file mode 100644
index 2fbcfc3..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p3.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p4.png b/test/alex_pico/pone.0054762.g004/p4.png
deleted file mode 100644
index 9dfdfe4..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p4.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p5.png b/test/alex_pico/pone.0054762.g004/p5.png
deleted file mode 100644
index 0a62537..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p5.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p6.png b/test/alex_pico/pone.0054762.g004/p6.png
deleted file mode 100644
index c8eff7e..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p6.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p7.png b/test/alex_pico/pone.0054762.g004/p7.png
deleted file mode 100644
index 740b511..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p7.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p8.png b/test/alex_pico/pone.0054762.g004/p8.png
deleted file mode 100644
index 3f32e88..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p8.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/p9.png b/test/alex_pico/pone.0054762.g004/p9.png
deleted file mode 100644
index e3c7a7d..0000000
Binary files a/test/alex_pico/pone.0054762.g004/p9.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/palette.png b/test/alex_pico/pone.0054762.g004/palette.png
deleted file mode 100644
index 4b262c1..0000000
Binary files a/test/alex_pico/pone.0054762.g004/palette.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/palette_rgb.png b/test/alex_pico/pone.0054762.g004/palette_rgb.png
deleted file mode 100644
index 9c4c166..0000000
Binary files a/test/alex_pico/pone.0054762.g004/palette_rgb.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/rgb.png b/test/alex_pico/pone.0054762.g004/rgb.png
deleted file mode 100644
index e105801..0000000
Binary files a/test/alex_pico/pone.0054762.g004/rgb.png and /dev/null differ
diff --git a/test/alex_pico/pone.0054762.g004/single.png b/test/alex_pico/pone.0054762.g004/single.png
deleted file mode 100644
index 1018727..0000000
Binary files a/test/alex_pico/pone.0054762.g004/single.png and /dev/null differ
diff --git a/test/alex_pico/test1.png b/test/alex_pico/test1.png
deleted file mode 100644
index 36f2c2e..0000000
Binary files a/test/alex_pico/test1.png and /dev/null differ
diff --git a/test/resources/biosynth_path_2.jpg b/test/biosynth_path_2.jpg
similarity index 100%
rename from test/resources/biosynth_path_2.jpg
rename to test/biosynth_path_2.jpg
diff --git a/test/context.py b/test/context.py
new file mode 100644
index 0000000..15dfdb3
--- /dev/null
+++ b/test/context.py
@@ -0,0 +1,17 @@
+import os
+import sys
+
+top_level = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+
+# list of packges to be included under the top level
+include_packages = []
+
+# add top level root directory to path
+sys.path.insert(0, top_level)
+
+# add each package to be included in sys.path for execution during test
+for package in include_packages:
+    sys.path.append(os.path.join(top_level, package))
+
+# test if import works, if not will throw error
+import pyamiimage
\ No newline at end of file
diff --git a/test/example_ami_ocr.py b/test/example_ami_ocr.py
index 10933e0..28f575f 100644
--- a/test/example_ami_ocr.py
+++ b/test/example_ami_ocr.py
@@ -1,4 +1,4 @@
-from pyimage.ami_ocr import TextBox, AmiOCR
+from pyamiimage.ami_ocr import TextBox, AmiOCR
 from test.resources import Resources
 from skimage import io
 
@@ -6,13 +6,39 @@
 biosynth2_img = io.imread(biosynth2)
 biosynth2_ocr = AmiOCR(biosynth2)
 
-words = biosynth2_ocr.get_words()
-# biosynth2_ocr.find_baseline()
+med_xrd = Resources.MED_XRD_FIG_A_LABELS
+med_xrd_img = io.imread(med_xrd)
+med_xrd_ocr = AmiOCR(med_xrd)
+
+# words = biosynth2_ocr.get_words()
+# # biosynth2_ocr.find_baseline()
+# for word in words:
+#     print(word)
+
+
+# phrases = biosynth2_ocr.get_phrases()
+# for phrase in phrases:
+#     print(phrase)
+
+# # biosynth2_img_bboxes = AmiOCR.plot_bboxes_on_image(biosynth2_img, words)
+# # io.imshow(biosynth2_img_bboxes)
+# # io.show()
+
+# biosynth2_img_bboxes = AmiOCR.plot_bboxes_on_image(biosynth2_img, phrases)
+# io.imshow(biosynth2_img_bboxes)
+# io.show()
+
+# # groups = biosynth2_ocr.get_groups()
+# # for group in groups:
+# #     print(group)
+
+
+words = med_xrd_ocr.get_words()
 for word in words:
     print(word)
 
 
-phrases = biosynth2_ocr.get_phrases()
+phrases = med_xrd_ocr.get_phrases()
 for phrase in phrases:
     print(phrase)
 
@@ -20,8 +46,8 @@
 # io.imshow(biosynth2_img_bboxes)
 # io.show()
 
-biosynth2_img_bboxes = AmiOCR.plot_bboxes_on_image(biosynth2_img, phrases)
-io.imshow(biosynth2_img_bboxes)
+med_xrd_img_bboxes = AmiOCR.plot_bboxes_on_image(med_xrd_img, words)
+io.imshow(med_xrd_img_bboxes)
 io.show()
 
 # groups = biosynth2_ocr.get_groups()
diff --git a/test/example_tesseract.py b/test/example_tesseract.py
index 37415be..ddd4cdf 100644
--- a/test/example_tesseract.py
+++ b/test/example_tesseract.py
@@ -1,9 +1,9 @@
-from pyimage.tesseract_hocr import TesseractOCR
+from pyamiimage.tesseract_hocr import TesseractOCR
 from test.resources import Resources
-from pyimage.cleaner import WordCleaner
+from pyamiimage.cleaner import WordCleaner
 
 image_list = [Resources.BIOSYNTH1, Resources.BIOSYNTH2, Resources.BIOSYNTH3, Resources.BIOSYNTH4,
-                Resources.BIOSYNTH5, Resources.BIOSYNTH6, Resources.BIOSYNTH7, Resources.BIOSYNTH8]gi
+                Resources.BIOSYNTH5, Resources.BIOSYNTH6, Resources.BIOSYNTH7, Resources.BIOSYNTH8]
 
 for path in image_list:
     image_hocr = TesseractOCR.hocr_from_image_path(path)
diff --git a/test/explore.py b/test/explore.py
index 9ee2d0b..e732805 100644
--- a/test/explore.py
+++ b/test/explore.py
@@ -1,6 +1,7 @@
-from collections import defaultdict
-import pprint
 import json
+import pprint
+from collections import defaultdict
+
 taxonomy = None
 
 
@@ -12,9 +13,9 @@ def test_colours():
     print("colours")
 
     tt = tree()
-    tt['house']['car']['red']['hubcap'] = 1950
+    tt["house"]["car"]["red"]["hubcap"] = 1950
 
-    s = [('red', 1), ('blue', 2), ('red', 3), ('blue', 4), ('red', 1), ('blue', 4)]
+    s = [("red", 1), ("blue", 2), ("red", 3), ("blue", 4), ("red", 1), ("blue", 4)]
     d = defaultdict(set)
     for k, v in s:
         d[k].add(v)
@@ -25,15 +26,19 @@ def test_colours():
 # https://gist.github.com/hrldcpr/2012250
 def make_taxonomy():
     taxonomy = tree()
-    taxonomy['Animalia']['Chordata']['Mammalia']['Carnivora']['Felidae']['Felis']['cat']
-    taxonomy['Animalia']['Chordata']['Mammalia']['Carnivora']['Felidae']['Panthera']['lion']
-    taxonomy['Animalia']['Chordata']['Mammalia']['Carnivora']['Canidae']['Canis']['dog']
-    taxonomy['Animalia']['Chordata']['Mammalia']['Carnivora']['Canidae']['Canis']['coyote']
-    taxonomy['Plantae']['Solanales']['Solanaceae']['Solanum']['tomato']
-    taxonomy['Plantae']['Solanales']['Solanaceae']['Solanum']['potato']
-    taxonomy['Plantae']['Solanales']['Convolvulaceae']['Ipomoea']['sweet potato']
+    taxonomy["Animalia"]["Chordata"]["Mammalia"]["Carnivora"]["Felidae"]["Felis"]["cat"]
+    taxonomy["Animalia"]["Chordata"]["Mammalia"]["Carnivora"]["Felidae"]["Panthera"][
+        "lion"
+    ]
+    taxonomy["Animalia"]["Chordata"]["Mammalia"]["Carnivora"]["Canidae"]["Canis"]["dog"]
+    taxonomy["Animalia"]["Chordata"]["Mammalia"]["Carnivora"]["Canidae"]["Canis"][
+        "coyote"
+    ]
+    taxonomy["Plantae"]["Solanales"]["Solanaceae"]["Solanum"]["tomato"]
+    taxonomy["Plantae"]["Solanales"]["Solanaceae"]["Solanum"]["potato"]
+    taxonomy["Plantae"]["Solanales"]["Convolvulaceae"]["Ipomoea"]["sweet potato"]
     print(json.dumps(taxonomy, indent=4))
-    del(taxonomy['Animalia'])
+    del taxonomy["Animalia"]
     print("del:", json.dumps(taxonomy, indent=4))
     return taxonomy
 
@@ -59,8 +64,12 @@ def test_taxonomy():
 def test_taxonomy1():
     taxonomy = make_taxonomy()
     print("\n>taxonomy1>\n", taxonomy)
-    tax1 = addx(taxonomy,
-                'Animalia>Chordata>Mammalia>Cetacea>Balaenopteridae>Balaenoptera>blue whale'.split('>'))
+    tax1 = addx(
+        taxonomy,
+        "Animalia>Chordata>Mammalia>Cetacea>Balaenopteridae>Balaenoptera>blue whale".split(
+            ">"
+        ),
+    )
     print("\n>tax1>\n....", tax1, "\n>taxonomy2>\n", taxonomy)
     taxonomy["test"] = "test"
     print("\n>taxonomy4>\n", taxonomy["test"])
@@ -86,4 +95,4 @@ def test_taxonomy1():
  'Plantae': {'Solanales': {'Convolvulaceae': {'Ipomoea': {'sweet potato': {}}},
                            'Solanaceae': {'Solanum': {'potato': {},
                                                       'tomato': {}}}}}}
-"""
\ No newline at end of file
+"""
diff --git a/test/generate_compare_files.py b/test/generate_compare_files.py
index b19df34..98b6f99 100644
--- a/test/generate_compare_files.py
+++ b/test/generate_compare_files.py
@@ -1,16 +1,19 @@
+import os
 from pathlib import Path
-from pyimage.ami_image import AmiImage
+
 from skimage import io
-import os
+
+from pyamiimage.ami_image import AmiImage
 
 RESOURCE_DIR = Path(Path(__file__).parent, "resources")
 COMPARE_DIR = Path(Path(__file__).parent, "comparison_images")
 
 RGB_SNIPPET = Path(RESOURCE_DIR, "snippet_rgb.png")
 
-class AmiImageFileGenerator():
+
+class AmiImageFileGenerator:
     """This class generates image files for comparison in test_ami_image"""
-    
+
     def __init__(self, image_path) -> None:
         self.image = io.imread(image_path)
         if not COMPARE_DIR.exists():
@@ -63,10 +66,12 @@ def generate_all(self):
     def write_image_to_path(image, path):
         io.imsave(path, image)
 
+
 def main():
     image_path = RGB_SNIPPET
     file_generator = AmiImageFileGenerator(image_path)
     file_generator.generate_all()
 
-if __name__ == '__main__':
-    main()
\ No newline at end of file
+
+if __name__ == "__main__":
+    main()
diff --git a/test/resources.py b/test/resources.py
index 699cc57..f53c981 100644
--- a/test/resources.py
+++ b/test/resources.py
@@ -1,99 +1,116 @@
 """Resources such as data used by other modules
 This may develop into a dataclass"""
 
-from pathlib import Path
-from skimage import io
 import logging
+from pathlib import Path
+
 import numpy as np
+from skimage import io
+
 # local
-try:
-    from ..pyimage.ami_graph_all import AmiGraph
-    from ..pyimage.tesseract_hocr import TesseractOCR
-    from ..pyimage.ami_image import AmiImageDTO
-except:
-    from pyimage.ami_graph_all import AmiGraph
-    from pyimage.tesseract_hocr import TesseractOCR
-    from pyimage.ami_image import AmiImageDTO
+import context
+from pyamiimage.ami_graph_all import AmiGraph
+from pyamiimage.ami_image import AmiImageDTO
+from pyamiimage.tesseract_hocr import TesseractOCR
 
 logger = logging.getLogger(__name__)
 
+
 class Resources:
     TEST_RESOURCE_DIR = Path(Path(__file__).parent, "resources")
     assert TEST_RESOURCE_DIR.exists(), f"dir exists {TEST_RESOURCE_DIR}"
     # biosynth1 is the most analysed image. In places it is cropped to provide a subset
-    BIOSYNTH1 = Path(TEST_RESOURCE_DIR, "biosynth_path_1.png")
-    assert BIOSYNTH1.exists(), f"file exists {BIOSYNTH1}"
-    BIOSYNTH1_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_1.hocr")
+
+# biosynth1
+    BIOSYNTH1_DIR = Path(TEST_RESOURCE_DIR, "biosynth_path_1")
+    BIOSYNTH1_RAW = Path(BIOSYNTH1_DIR, "raw.png")
+    assert BIOSYNTH1_RAW.exists(), f"file exists {BIOSYNTH1_RAW}"
+    BIOSYNTH1_HOCR = Path(BIOSYNTH1_DIR, "hocr.html")
     assert BIOSYNTH1_HOCR.exists(), f"file exists {BIOSYNTH1_HOCR}"
-    BIOSYNTH1_CROPPED = Path(TEST_RESOURCE_DIR, "biosynth_path_1_cropped.png")
-    assert BIOSYNTH1_CROPPED.exists(), f"file exists {BIOSYNTH1_CROPPED}"
-    BIOSYNTH1_TEXT = Path(TEST_RESOURCE_DIR, "biosynth_path_1_cropped_arrows_removed.png")
+
+# cropped
+    BIOSYNTH1_CROPPED_DIR = Path(TEST_RESOURCE_DIR, "biosynth1_cropped")
+    BIOSYNTH1_CROPPED_PNG = Path(BIOSYNTH1_CROPPED_DIR , "raw.png")
+    assert BIOSYNTH1_CROPPED_PNG.exists(), f"file exists {BIOSYNTH1_CROPPED_PNG}"
+    BIOSYNTH1_TEXT = Path(BIOSYNTH1_CROPPED_DIR, "arrows_removed.png")
     assert BIOSYNTH1_TEXT.exists(), f"file exists {BIOSYNTH1_TEXT}"
-    BIOSYNTH1_ARROWS = Path(TEST_RESOURCE_DIR, "biosynth_path_1_cropped_text_removed.png")
-    assert BIOSYNTH1_ARROWS.exists(), f"file exists {BIOSYNTH1_ARROWS}"
-    BIOSYNTH1_ARROWS_TEXT_SVG = Path(TEST_RESOURCE_DIR, "biosynth1_arrows_text.svg")
-    assert BIOSYNTH1_ARROWS_TEXT_SVG.exists(), f"file exists {BIOSYNTH1_ARROWS_TEXT_SVG}"
-    BIOSYNTH1_RAW_ARROWS_SVG = Path(TEST_RESOURCE_DIR, "biosynth1_raw_arrows.svg")
+
+    BIOSYNTH1_CROPPED_ARROWS_RAW = Path(BIOSYNTH1_CROPPED_DIR, "text_removed.png")
+    assert BIOSYNTH1_CROPPED_ARROWS_RAW.exists(), f"file exists {BIOSYNTH1_CROPPED_ARROWS_RAW}"
+
+    BIOSYNTH1_ARROWS_DIR = Path(TEST_RESOURCE_DIR, "biosynth1_arrows")
+    BIOSYNTH1_ARROWS_TEXT_SVG = Path(BIOSYNTH1_ARROWS_DIR, "text.svg")
+    assert (BIOSYNTH1_ARROWS_TEXT_SVG.exists()), f"file exists {BIOSYNTH1_ARROWS_TEXT_SVG}"
+    BIOSYNTH1_RAW_ARROWS_SVG = Path(BIOSYNTH1_ARROWS_DIR, "raw_arrows.svg")
     assert BIOSYNTH1_RAW_ARROWS_SVG.exists(), f"file exists {BIOSYNTH1_RAW_ARROWS_SVG}"
 
-    BIOSYNTH2 = Path(TEST_RESOURCE_DIR, "biosynth_path_2.jpg")
-    assert BIOSYNTH2.exists(), f"file exists {BIOSYNTH2}"
-    BIOSYNTH3 = Path(TEST_RESOURCE_DIR, "biosynth_path_3.png")
-    assert BIOSYNTH3.exists(), f"file exists {BIOSYNTH3}"
-    BIOSYNTH4 = Path(TEST_RESOURCE_DIR, "biosynth_path_4.jpeg")
-    assert BIOSYNTH4.exists(), f"file exists {BIOSYNTH4}"
-    BIOSYNTH5 = Path(TEST_RESOURCE_DIR, "biosynth_path_5.jpeg")
-    assert BIOSYNTH5.exists(), f"file exists {BIOSYNTH5}"
-    BIOSYNTH6 = Path(TEST_RESOURCE_DIR, "biosynth_path_6.jpeg")
-    assert BIOSYNTH6.exists(), f"file exists {BIOSYNTH6}"
-    BIOSYNTH6COMPOUND = Path(TEST_RESOURCE_DIR, "biosynth_path_6_compounds_only.jpeg")
-    assert BIOSYNTH6COMPOUND.exists(), f"file exists {BIOSYNTH6COMPOUND}"
-    BIOSYNTH7 = Path(TEST_RESOURCE_DIR, "biosynth_path_7.jpeg")
-    assert BIOSYNTH7.exists(), f"file exists {BIOSYNTH7}"
-    BIOSYNTH8 = Path(TEST_RESOURCE_DIR, "biosynth_path_8.jpeg")
-    assert BIOSYNTH8.exists(), f"file exists {BIOSYNTH8}"
-
-    BIOSYNTH3_HOCR = Path(TEST_RESOURCE_DIR, "tesseract_biosynth_path_3.hocr.html")
+    BIOSYNTH2_DIR = Path(TEST_RESOURCE_DIR, "biosynth_path_2")
+    BIOSYNTH2_RAW = Path(BIOSYNTH2_DIR, "raw.jpg")
+    assert BIOSYNTH2_RAW.exists(), f"file exists {BIOSYNTH2_RAW}"
+    BIOSYNTH3_DIR = Path(TEST_RESOURCE_DIR, "biosynth_path_3")
+    BIOSYNTH3_RAW = Path(BIOSYNTH3_DIR, "raw.png")
+    assert BIOSYNTH3_RAW.exists(), f"file exists {BIOSYNTH3_RAW}"
+    BIOSYNTH4_DIR = Path(TEST_RESOURCE_DIR, "biosynth_path_4")
+    BIOSYNTH4_RAW = Path(BIOSYNTH4_DIR, "raw.jpeg")
+    assert BIOSYNTH4_RAW.exists(), f"file exists {BIOSYNTH4_RAW}"
+    BIOSYNTH5_DIR = Path(TEST_RESOURCE_DIR, "biosynth_path_5")
+    BIOSYNTH5_RAW = Path(BIOSYNTH5_DIR, "raw.jpeg")
+    assert BIOSYNTH5_RAW.exists(), f"file exists {BIOSYNTH5_RAW}"
+    BIOSYNTH6_DIR = Path(TEST_RESOURCE_DIR, "biosynth_path_6")
+    BIOSYNTH6_RAW = Path(BIOSYNTH6_DIR, "raw.jpeg")
+    assert BIOSYNTH6_RAW.exists(), f"file exists {BIOSYNTH6_RAW}"
+    BIOSYNTH6COMPOUND_RAW = Path(BIOSYNTH6_DIR, "compounds_only.jpeg")
+    assert BIOSYNTH6COMPOUND_RAW.exists(), f"file exists {BIOSYNTH6COMPOUND_RAW}"
+    BIOSYNTH7_DIR = Path(TEST_RESOURCE_DIR, "biosynth_path_7")
+    BIOSYNTH7_RAW = Path(BIOSYNTH7_DIR, "raw.jpeg")
+    assert BIOSYNTH7_RAW.exists(), f"file exists {BIOSYNTH7_RAW}"
+    BIOSYNTH8_DIR = Path(TEST_RESOURCE_DIR, "biosynth_path_8")
+    BIOSYNTH8_RAW = Path(BIOSYNTH8_DIR, "raw.jpeg")
+    assert BIOSYNTH8_RAW.exists(), f"file exists {BIOSYNTH8_RAW}"
+
+    BIOSYNTH3_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_3", "hocr.html")
     assert BIOSYNTH3_HOCR.exists(), f"file exists {BIOSYNTH3_HOCR}"
-    BIOSYNTH4_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_4.hocr")
+    BIOSYNTH4_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_4", "hocr.html")
     assert BIOSYNTH4_HOCR.exists(), f"file exists {BIOSYNTH4_HOCR}"
-    BIOSYNTH5_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_5.hocr")
+    BIOSYNTH5_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_5", "hocr.html")
     assert BIOSYNTH5_HOCR.exists(), f"file exists {BIOSYNTH5_HOCR}"
-    BIOSYNTH6_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_6.hocr")
+    BIOSYNTH6_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_6", "hocr.html")
     assert BIOSYNTH6_HOCR.exists(), f"file exists {BIOSYNTH6_HOCR}"
-    BIOSYNTH7_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_7.hocr")
+    BIOSYNTH7_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_7", "hocr.html")
     assert BIOSYNTH7_HOCR.exists(), f"file exists {BIOSYNTH7_HOCR}"
-    BIOSYNTH8_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_8.hocr")
+    BIOSYNTH8_HOCR = Path(TEST_RESOURCE_DIR, "biosynth_path_8", "hocr.html")
     assert BIOSYNTH8_HOCR.exists(), f"file exists {BIOSYNTH8_HOCR}"
 
-    ISLANDS_5_SKEL = Path(TEST_RESOURCE_DIR, "islands_5.png")
-    assert ISLANDS_5_SKEL.exists(), f"file exists {ISLANDS_5_SKEL}"
-
-    PRISMA = Path(TEST_RESOURCE_DIR, "prisma.png")
-    assert PRISMA.exists(), f"file exists {PRISMA}"
-
-    BATTERY1 = Path(TEST_RESOURCE_DIR, "green.png")
-    assert BATTERY1.exists(), f"file exists {BATTERY1}"
-    BATTERY1BSQUARE = Path(TEST_RESOURCE_DIR, "battery1bsquare.png")
-    assert BATTERY1BSQUARE.exists(), f"file exists {BATTERY1BSQUARE}"
-    PRIMITIVES = Path(TEST_RESOURCE_DIR, "primitives.png")
-    assert PRIMITIVES.exists(), f"file exists {PRIMITIVES}"
-    TESSERACT1 = Path(TEST_RESOURCE_DIR, "tesseract_test.png")
-    assert TESSERACT1.exists(), f"file exists {TESSERACT1}"
-    TESSERACT_BENG = Path(TEST_RESOURCE_DIR, "tesseract_beng.jpg")
-    assert TESSERACT_BENG.exists(), f"file doesn't exist {TESSERACT_BENG}"
-    TESSERACT_GER = Path(TEST_RESOURCE_DIR, "tesseract_ger.jpg")
-    assert TESSERACT_GER.exists(), f"file doesn't exist {TESSERACT_GER}"
-    TESSERACT_GER2 = Path(TEST_RESOURCE_DIR, "tesseract_ger2.png")
-    assert TESSERACT_GER2.exists(), f"file doesn't exist {TESSERACT_GER2}"
-    TESSERACT_ITA = Path(TEST_RESOURCE_DIR, "tesseract_ita.png")
-    assert TESSERACT_ITA.exists(), f"file doesn't exist {TESSERACT_ITA}"
-    
-
-    YW5003_5 = Path(TEST_RESOURCE_DIR, "iucr_yw5003_fig5.png")
-    assert YW5003_5.exists(), f"file exists {YW5003_5}"
-
-# =====================
+    ISLANDS_5_SKEL_RAW = Path(TEST_RESOURCE_DIR, "islands_5.png")
+    assert ISLANDS_5_SKEL_RAW.exists(), f"file exists {ISLANDS_5_SKEL_RAW}"
+
+    PRISMA_RAW = Path(TEST_RESOURCE_DIR, "prisma.png")
+    assert PRISMA_RAW.exists(), f"file exists {PRISMA_RAW}"
+
+    # https://europepmc.org/article/MED/34909142#figures-and-tables
+    MED_34909142_3_RAW = Path(TEST_RESOURCE_DIR, "MED_34909142_3.jpeg")
+    assert MED_34909142_3_RAW.exists(), f"file exists {MED_34909142_3_RAW}"
+    MED_XRD_FIG_A_RAW = Path(TEST_RESOURCE_DIR, "MED_34909142_3_figA.jpeg")
+    assert MED_XRD_FIG_A_RAW.exists(), f"file {MED_XRD_FIG_A_RAW} doesn't exist"
+
+    BATTERY_DIR = Path(TEST_RESOURCE_DIR, "battery")
+    BATTERY1_RAW = Path(BATTERY_DIR, "capacity_r_g_b.png")
+    assert BATTERY1_RAW.exists(), f"file exists {BATTERY1_RAW}"
+    BATTERY1BSQUARE_RAW = Path(BATTERY_DIR, "battery1bsquare.png")
+    assert BATTERY1BSQUARE_RAW.exists(), f"file exists {BATTERY1BSQUARE_RAW}"
+    BATTERY2_RAW = Path(BATTERY_DIR, "battery2.png")
+    assert BATTERY2_RAW.exists(), f"file exists {BATTERY2_RAW}"
+
+    PRIMITIVES_RAW = Path(TEST_RESOURCE_DIR, "primitives.png")
+    assert PRIMITIVES_RAW.exists(), f"file exists {PRIMITIVES_RAW}"
+
+    YW5003_5_RAW = Path(TEST_RESOURCE_DIR, "iucr_yw5003_fig5.png")
+    assert YW5003_5_RAW.exists(), f"file exists {YW5003_5_RAW}"
+
+    SHAPES_1_RAW = Path(TEST_RESOURCE_DIR, "test_img_shapes.png")
+    assert SHAPES_1_RAW.exists(), f"file {SHAPES_1_RAW} doesn't exist"
+
+    # =====================
 
     TEMP_DIR = Path(TEST_RESOURCE_DIR.parent.parent, "temp")
     assert TEMP_DIR.is_dir(), f"file exists {TEMP_DIR}"
@@ -110,61 +127,93 @@ def __init__(self):
         self.biosynth3_dto = None
 
     def create_ami_graph_objects(self):
-        """creates image derivatives
-        """
+        """creates image derivatives"""
         logger.debug(f"{__name__} create_ami_graph_objects {self.cached}")
         if not self.cached:
-            logger.warning(f"{__name__} setting up Resources" )
+            logger.warning(f"{__name__} setting up Resources")
             self.cached = True
-            self.arrows1_image = io.imread(Resources.BIOSYNTH1_ARROWS)
+            self.arrows1_image = io.imread(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
             assert self.arrows1_image.shape == (315, 1512)
             self.arrows1_image = np.where(self.arrows1_image < 127, 0, 255)
-            self.nx_graph_arrows1 = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
-            self.arrows1_ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
-
-# biosynth1 has solid arrowheads and (unfortunately) some primitives overlap
-            self.biosynth1 = io.imread(Resources.BIOSYNTH1)
+            self.nx_graph_arrows1 = AmiGraph.create_nx_graph_from_arbitrary_image_file(
+                Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+            )
+            self.arrows1_ami_graph = (
+                AmiGraph.create_ami_graph_from_arbitrary_image_file(
+                    Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+                )
+            )
+
+            # biosynth1 has solid arrowheads and (unfortunately) some primitives overlap
+            self.biosynth1 = io.imread(Resources.BIOSYNTH1_RAW)
             assert self.biosynth1.shape == (1167, 1515)
             self.biosynth1_binary = np.where(self.biosynth1 < 127, 0, 255)
-            self.nx_graph_biosynth1 = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.BIOSYNTH1)
-            self.biosynth1_hocr = TesseractOCR.hocr_from_image_path(Resources.BIOSYNTH1)
+            self.nx_graph_biosynth1 = (
+                AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_RAW)
+            )
+            self.biosynth1_hocr = TesseractOCR.hocr_from_image_path(Resources.BIOSYNTH1_RAW)
             self.biosynth1_elem = TesseractOCR.parse_hocr_string(self.biosynth1_hocr)
-            self.biosynth1_ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1)
-
-
+            self.biosynth1_ami_graph = (
+                AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_RAW)
+            )
 
-            prisma = io.imread(Resources.PRISMA)
+            prisma = io.imread(Resources.PRISMA_RAW)
             assert prisma.shape == (667, 977, 4)
-            self.nx_graph_prisma = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.PRISMA)
+            self.nx_graph_prisma = AmiGraph.create_nx_graph_from_arbitrary_image_file(
+                Resources.PRISMA_RAW
+            )
 
-            self.battery1_image = io.imread(Resources.BATTERY1)
+            self.battery1_image = io.imread(Resources.BATTERY1_RAW)
             assert self.battery1_image.shape == (546, 1354, 3)
             self.battery1_binary = np.where(self.battery1_image < 127, 0, 255)
-            self.nx_graph_battery1 = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.BATTERY1)
+            self.nx_graph_battery1 = AmiGraph.create_nx_graph_from_arbitrary_image_file(
+                Resources.BATTERY1_RAW
+            )
 
-            self.battery1bsquare = io.imread(Resources.BATTERY1BSQUARE)
+            self.battery1bsquare = io.imread(Resources.BATTERY1BSQUARE_RAW)
             assert self.battery1_image.shape == (546, 1354, 3)
             # self.battery1_binary = np.where(self.battery1 < 127, 0, 255)
-            self.nx_graph_battery1bsquare = AmiGraph.create_nx_graph_from_arbitrary_image_file(
-                Resources.BATTERY1BSQUARE)
+            self.nx_graph_battery1bsquare = (
+                AmiGraph.create_nx_graph_from_arbitrary_image_file(
+                    Resources.BATTERY1BSQUARE_RAW
+                )
+            )
 
-            self.primitives = io.imread(Resources.PRIMITIVES)
+            self.primitives = io.imread(Resources.PRIMITIVES_RAW)
             assert self.primitives.shape == (405, 720, 3)
-            self.nx_graph_primitives = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.PRIMITIVES)
+            self.nx_graph_primitives = (
+                AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.PRIMITIVES_RAW)
+            )
 
-# DTO approach
+            # DTO approach
             if not self.biosynth1_dto:
-                self.biosynth1_dto = self.get_image_dto(raw_image_file = Resources.BIOSYNTH1, raw_image_shape = (1167, 1515), threshold = 127)
+                self.biosynth1_dto = self.get_image_dto(
+                    raw_image_file=Resources.BIOSYNTH1_RAW,
+                    raw_image_shape=(1167, 1515),
+                    threshold=127,
+                )
             # if not self.biosynth2_dto:
             #     self.biosynth2_dto = self.get_image_dto(raw_image_file = Resources.BIOSYNTH2, raw_image_shape = (1391, 1420, 3), threshold = 127)
             if not self.biosynth3_dto:
-                self.biosynth3_dto = self.get_image_dto(raw_image_file = Resources.BIOSYNTH3, raw_image_shape = (972, 1020), threshold = 127)
-            self.biosynth6_compounds_dto = self.get_image_dto(raw_image_file = \
-                                                    Resources.BIOSYNTH6COMPOUND, raw_image_shape = (967, 367, 3), threshold = 127)
+                self.biosynth3_dto = self.get_image_dto(
+                    raw_image_file=Resources.BIOSYNTH3_RAW,
+                    raw_image_shape=(972, 1020),
+                    threshold=127,
+                )
+            self.biosynth6_compounds_dto = self.get_image_dto(
+                raw_image_file=Resources.BIOSYNTH6COMPOUND_RAW,
+                raw_image_shape=(967, 367, 3),
+                threshold=127,
+            )
 
             return self
 
-    def get_image_dto(self, raw_image_file, raw_image_shape=None, threshold=127, ):
+    def get_image_dto(
+        self,
+        raw_image_file,
+        raw_image_shape=None,
+        threshold=127,
+    ):
         """
         return Data Transfer Object containin downstream image artefacts
         create one of these for each image being processed
@@ -176,10 +225,16 @@ def get_image_dto(self, raw_image_file, raw_image_shape=None, threshold=127, ):
         image_dto.image_file = raw_image_file
         image_dto.image = io.imread(raw_image_file)
         if raw_image_shape is not None:
-            assert image_dto.image.shape == raw_image_shape, f"expected {image_dto.image.shape}"
+            assert (
+                image_dto.image.shape == raw_image_shape
+            ), f"expected {image_dto.image.shape}"
         image_dto.image_binary = np.where(image_dto.image < threshold, 0, 255)
-        image_dto.nx_graph = AmiGraph.create_nx_graph_from_arbitrary_image_file(raw_image_file)
-        image_dto.ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(raw_image_file)
+        image_dto.nx_graph = AmiGraph.create_nx_graph_from_arbitrary_image_file(
+            raw_image_file
+        )
+        image_dto.ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(
+            raw_image_file
+        )
         image_dto.hocr = TesseractOCR.hocr_from_image_path(raw_image_file)
         image_dto.hocr_html_element = TesseractOCR.parse_hocr_string(image_dto.hocr)
 
diff --git a/test/resources/--psm.txt b/test/resources/--psm.txt
new file mode 100644
index 0000000..fdd0936
--- /dev/null
+++ b/test/resources/--psm.txt
@@ -0,0 +1,9 @@
+10 20 30 40 50 60
+
+© + oe) N = °
+
+=) r—) -) ° —)
+yun “qe / (01 x) Asueyul GX
+
+9 theta / dearee
+
\ No newline at end of file
diff --git a/test/resources/MED_34909142_3.jpeg b/test/resources/MED_34909142_3.jpeg
new file mode 100644
index 0000000..12851c3
Binary files /dev/null and b/test/resources/MED_34909142_3.jpeg differ
diff --git a/test/resources/MED_34909142_3_figA.jpeg b/test/resources/MED_34909142_3_figA.jpeg
new file mode 100644
index 0000000..c6286cf
Binary files /dev/null and b/test/resources/MED_34909142_3_figA.jpeg differ
diff --git a/test/resources/MED_34909142_3_figA_horz_label.jpg b/test/resources/MED_34909142_3_figA_horz_label.jpg
new file mode 100644
index 0000000..a6137c8
Binary files /dev/null and b/test/resources/MED_34909142_3_figA_horz_label.jpg differ
diff --git a/test/resources/MED_34909142_3_figA_labels.jpg b/test/resources/MED_34909142_3_figA_labels.jpg
new file mode 100644
index 0000000..b72d48d
Binary files /dev/null and b/test/resources/MED_34909142_3_figA_labels.jpg differ
diff --git a/test/resources/MED_34909142_3_figA_vert_label.jpg b/test/resources/MED_34909142_3_figA_vert_label.jpg
new file mode 100644
index 0000000..dbc7d28
Binary files /dev/null and b/test/resources/MED_34909142_3_figA_vert_label.jpg differ
diff --git a/test/resources/MED_34909142_3_figA_vert_label_num.png b/test/resources/MED_34909142_3_figA_vert_label_num.png
new file mode 100644
index 0000000..8be73a5
Binary files /dev/null and b/test/resources/MED_34909142_3_figA_vert_label_num.png differ
diff --git a/test/resources/MED_34909142_3_figA_vert_label_words.jpg b/test/resources/MED_34909142_3_figA_vert_label_words.jpg
new file mode 100644
index 0000000..8588fe5
Binary files /dev/null and b/test/resources/MED_34909142_3_figA_vert_label_words.jpg differ
diff --git a/test/resources/battery1.png b/test/resources/battery/battery1.png
similarity index 100%
rename from test/resources/battery1.png
rename to test/resources/battery/battery1.png
diff --git a/test/resources/battery1a.png b/test/resources/battery/battery1a.png
similarity index 100%
rename from test/resources/battery1a.png
rename to test/resources/battery/battery1a.png
diff --git a/test/resources/battery1bsquare.png b/test/resources/battery/battery1bsquare.png
similarity index 100%
rename from test/resources/battery1bsquare.png
rename to test/resources/battery/battery1bsquare.png
diff --git a/test/resources/battery/battery2.png b/test/resources/battery/battery2.png
new file mode 100644
index 0000000..0452e2a
Binary files /dev/null and b/test/resources/battery/battery2.png differ
diff --git a/test/resources/capacity_r_g_b.png b/test/resources/battery/capacity_r_g_b.png
similarity index 100%
rename from test/resources/capacity_r_g_b.png
rename to test/resources/battery/capacity_r_g_b.png
diff --git a/test/resources/battery/green.png b/test/resources/battery/green.png
new file mode 100644
index 0000000..67642a3
Binary files /dev/null and b/test/resources/battery/green.png differ
diff --git a/test/resources/green/p0.png b/test/resources/battery/green/p0.png
similarity index 100%
rename from test/resources/green/p0.png
rename to test/resources/battery/green/p0.png
diff --git a/test/resources/green/p1.png b/test/resources/battery/green/p1.png
similarity index 100%
rename from test/resources/green/p1.png
rename to test/resources/battery/green/p1.png
diff --git a/test/resources/green/p2.png b/test/resources/battery/green/p2.png
similarity index 100%
rename from test/resources/green/p2.png
rename to test/resources/battery/green/p2.png
diff --git a/test/resources/green/p3.png b/test/resources/battery/green/p3.png
similarity index 100%
rename from test/resources/green/p3.png
rename to test/resources/battery/green/p3.png
diff --git a/test/resources/green/p4.png b/test/resources/battery/green/p4.png
similarity index 100%
rename from test/resources/green/p4.png
rename to test/resources/battery/green/p4.png
diff --git a/test/resources/green/p5.png b/test/resources/battery/green/p5.png
similarity index 100%
rename from test/resources/green/p5.png
rename to test/resources/battery/green/p5.png
diff --git a/test/resources/green/p6.png b/test/resources/battery/green/p6.png
similarity index 100%
rename from test/resources/green/p6.png
rename to test/resources/battery/green/p6.png
diff --git a/test/resources/green/p7.png b/test/resources/battery/green/p7.png
similarity index 100%
rename from test/resources/green/p7.png
rename to test/resources/battery/green/p7.png
diff --git a/test/resources/green/palette.png b/test/resources/battery/green/palette.png
similarity index 100%
rename from test/resources/green/palette.png
rename to test/resources/battery/green/palette.png
diff --git a/test/resources/green/rgb.png b/test/resources/battery/green/rgb.png
similarity index 100%
rename from test/resources/green/rgb.png
rename to test/resources/battery/green/rgb.png
diff --git a/test/resources/green/single.png b/test/resources/battery/green/single.png
similarity index 100%
rename from test/resources/green/single.png
rename to test/resources/battery/green/single.png
diff --git a/test/resources/arrows_removed.hocr b/test/resources/biosynth1_arrows/arrows_removed.hocr
similarity index 100%
rename from test/resources/arrows_removed.hocr
rename to test/resources/biosynth1_arrows/arrows_removed.hocr
diff --git a/test/resources/biosynth1_arrows_text_annot.svg b/test/resources/biosynth1_arrows/biosynth1_arrows_text_annot.svg
similarity index 100%
rename from test/resources/biosynth1_arrows_text_annot.svg
rename to test/resources/biosynth1_arrows/biosynth1_arrows_text_annot.svg
diff --git a/test/resources/biosynth1_arrows_text_dict.txt b/test/resources/biosynth1_arrows/biosynth1_arrows_text_dict.txt
similarity index 100%
rename from test/resources/biosynth1_arrows_text_dict.txt
rename to test/resources/biosynth1_arrows/biosynth1_arrows_text_dict.txt
diff --git a/test/resources/biosynth1_network.gpml b/test/resources/biosynth1_arrows/biosynth1_network.gpml
similarity index 100%
rename from test/resources/biosynth1_network.gpml
rename to test/resources/biosynth1_arrows/biosynth1_network.gpml
diff --git a/test/resources/biosynth1_raw_arrows.svg b/test/resources/biosynth1_arrows/raw_arrows.svg
similarity index 100%
rename from test/resources/biosynth1_raw_arrows.svg
rename to test/resources/biosynth1_arrows/raw_arrows.svg
diff --git a/test/resources/biosynth1_arrows_text.svg b/test/resources/biosynth1_arrows/text.svg
similarity index 100%
rename from test/resources/biosynth1_arrows_text.svg
rename to test/resources/biosynth1_arrows/text.svg
diff --git a/test/resources/biosynth_path_1_cropped_arrows_removed.png b/test/resources/biosynth1_cropped/arrows_removed.png
similarity index 100%
rename from test/resources/biosynth_path_1_cropped_arrows_removed.png
rename to test/resources/biosynth1_cropped/arrows_removed.png
diff --git a/test/resources/biosynth_path_1_cropped.png b/test/resources/biosynth1_cropped/raw.png
similarity index 100%
rename from test/resources/biosynth_path_1_cropped.png
rename to test/resources/biosynth1_cropped/raw.png
diff --git a/test/resources/biosynth_path_1_cropped_text_removed.png b/test/resources/biosynth1_cropped/text_removed.png
similarity index 100%
rename from test/resources/biosynth_path_1_cropped_text_removed.png
rename to test/resources/biosynth1_cropped/text_removed.png
diff --git a/test/resources/biosynth_path_1.txt b/test/resources/biosynth_path_1/biosynth_path_1.txt
similarity index 100%
rename from test/resources/biosynth_path_1.txt
rename to test/resources/biosynth_path_1/biosynth_path_1.txt
diff --git a/test/resources/biosynth_path_1_default.txt b/test/resources/biosynth_path_1/biosynth_path_1_default.txt
similarity index 100%
rename from test/resources/biosynth_path_1_default.txt
rename to test/resources/biosynth_path_1/biosynth_path_1_default.txt
diff --git a/test/resources/biosynth_path_1.hocr b/test/resources/biosynth_path_1/hocr.html
similarity index 100%
rename from test/resources/biosynth_path_1.hocr
rename to test/resources/biosynth_path_1/hocr.html
diff --git a/test/resources/biosynth_path_1/p0.png b/test/resources/biosynth_path_1/p0.png
index 89a1844..f9942ec 100644
Binary files a/test/resources/biosynth_path_1/p0.png and b/test/resources/biosynth_path_1/p0.png differ
diff --git a/test/resources/biosynth_path_1/p1.png b/test/resources/biosynth_path_1/p1.png
index 4e13af5..041bbd6 100644
Binary files a/test/resources/biosynth_path_1/p1.png and b/test/resources/biosynth_path_1/p1.png differ
diff --git a/test/resources/biosynth_path_1/p2.png b/test/resources/biosynth_path_1/p2.png
index df5fd2a..0f0db99 100644
Binary files a/test/resources/biosynth_path_1/p2.png and b/test/resources/biosynth_path_1/p2.png differ
diff --git a/test/resources/biosynth_path_1/p3.png b/test/resources/biosynth_path_1/p3.png
index 2e71936..45bd469 100644
Binary files a/test/resources/biosynth_path_1/p3.png and b/test/resources/biosynth_path_1/p3.png differ
diff --git a/test/resources/biosynth_path_1/p4.png b/test/resources/biosynth_path_1/p4.png
index 188d4b9..14c22eb 100644
Binary files a/test/resources/biosynth_path_1/p4.png and b/test/resources/biosynth_path_1/p4.png differ
diff --git a/test/resources/biosynth_path_1/p5.png b/test/resources/biosynth_path_1/p5.png
index b4719f3..0e8df03 100644
Binary files a/test/resources/biosynth_path_1/p5.png and b/test/resources/biosynth_path_1/p5.png differ
diff --git a/test/resources/biosynth_path_1/p6.png b/test/resources/biosynth_path_1/p6.png
index b97e54f..3aa244a 100644
Binary files a/test/resources/biosynth_path_1/p6.png and b/test/resources/biosynth_path_1/p6.png differ
diff --git a/test/resources/biosynth_path_1/p7.png b/test/resources/biosynth_path_1/p7.png
index b5144c6..64f9af7 100644
Binary files a/test/resources/biosynth_path_1/p7.png and b/test/resources/biosynth_path_1/p7.png differ
diff --git a/test/resources/biosynth_path_1/palette.png b/test/resources/biosynth_path_1/palette.png
index 0b63150..4402ff8 100644
Binary files a/test/resources/biosynth_path_1/palette.png and b/test/resources/biosynth_path_1/palette.png differ
diff --git a/test/resources/biosynth_path_1.png b/test/resources/biosynth_path_1/raw.png
similarity index 100%
rename from test/resources/biosynth_path_1.png
rename to test/resources/biosynth_path_1/raw.png
diff --git a/test/resources/biosynth_path_1/rgb.png b/test/resources/biosynth_path_1/rgb.png
deleted file mode 100644
index 0172d65..0000000
Binary files a/test/resources/biosynth_path_1/rgb.png and /dev/null differ
diff --git a/test/resources/biosynth_path_1/single.png b/test/resources/biosynth_path_1/single.png
index 06780c1..bbe09bb 100644
Binary files a/test/resources/biosynth_path_1/single.png and b/test/resources/biosynth_path_1/single.png differ
diff --git a/test/resources/biosynth_path_1/tesseract.svg b/test/resources/biosynth_path_1/tesseract.svg
deleted file mode 100644
index 22552b6..0000000
--- a/test/resources/biosynth_path_1/tesseract.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<svg:svg xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><svg:g><svg:rect x="687" width="120" y="44" height="22" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="687" y="66" font-size="19.8" stroke="blue" font-family="sans-serif">Isomerase</svg:text></svg:g><svg:g><svg:rect x="815" width="31" y="43" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="815" y="71" font-size="25.2" stroke="blue" font-family="sans-serif">(?)</svg:text></svg:g><svg:g><svg:rect x="336" width="138" y="80" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="336" y="108" font-size="25.2" stroke="blue" font-family="sans-serif">Isopentenyl</svg:text></svg:g><svg:g><svg:rect x="482" width="144" y="79" height="29" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="482" y="108" font-size="26.1" stroke="blue" font-family="sans-serif">diphosphate</svg:text></svg:g><svg:g><svg:rect x="650" width="44" y="72" height="43" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="650" y="115" font-size="38.7" stroke="blue" font-family="sans-serif">~</svg:text></svg:g><svg:g><svg:rect x="662" width="206" y="87" height="24" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="662" y="111" font-size="21.6" stroke="blue" font-family="sans-serif">_______»</svg:text></svg:g><svg:g><svg:rect x="898" width="165" y="76" height="29" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="898" y="105" font-size="26.1" stroke="blue" font-family="sans-serif">Dimethylallyl</svg:text></svg:g><svg:g><svg:rect x="1072" width="145" y="76" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1072" y="104" font-size="25.2" stroke="blue" font-family="sans-serif">diphosphate</svg:text></svg:g><svg:g><svg:rect x="195" width="148" y="357" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="195" y="385" font-size="25.2" stroke="blue" font-family="sans-serif">Phytosterols</svg:text></svg:g><svg:g><svg:rect x="197" width="162" y="529" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="197" y="557" font-size="25.2" stroke="blue" font-family="sans-serif">Triterpenoids</svg:text></svg:g><svg:g><svg:rect x="198" width="109" y="633" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="198" y="661" font-size="25.2" stroke="blue" font-family="sans-serif">Squalene</svg:text></svg:g><svg:g><svg:rect x="334" width="114" y="637" height="21" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="334" y="658" font-size="18.900000000000002" stroke="blue" font-family="sans-serif">&lt;———</svg:text></svg:g><svg:g><svg:rect x="455" width="102" y="632" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="455" y="659" font-size="24.3" stroke="blue" font-family="sans-serif">Farnasy]</svg:text></svg:g><svg:g><svg:rect x="565" width="144" y="631" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="565" y="659" font-size="25.2" stroke="blue" font-family="sans-serif">diphosphate</svg:text></svg:g><svg:g><svg:rect x="715" width="319" y="629" height="29" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="715" y="658" font-size="26.1" stroke="blue" font-family="sans-serif">——Sesquiterpenes—"</svg:text></svg:g><svg:g><svg:rect x="201" width="109" y="806" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="201" y="834" font-size="25.2" stroke="blue" font-family="sans-serif">Saponins</svg:text></svg:g><svg:g><svg:rect x="394" width="566" y="232" height="13" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="394" y="245" font-size="11.700000000000001" stroke="blue" font-family="sans-serif"> </svg:text></svg:g><svg:g><svg:rect x="379" width="36" y="598" height="21" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="379" y="619" font-size="18.900000000000002" stroke="blue" font-family="sans-serif">2X</svg:text></svg:g><svg:g><svg:rect x="440" width="60" y="114" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="440" y="142" font-size="25.2" stroke="blue" font-family="sans-serif">(IPP;</svg:text></svg:g><svg:g><svg:rect x="509" width="39" y="114" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="509" y="142" font-size="25.2" stroke="blue" font-family="sans-serif">Cs)</svg:text></svg:g><svg:g><svg:rect x="1000" width="119" y="111" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1000" y="139" font-size="25.2" stroke="blue" font-family="sans-serif">(DMAPP;</svg:text></svg:g><svg:g><svg:rect x="1128" width="38" y="111" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1128" y="138" font-size="24.3" stroke="blue" font-family="sans-serif">Cs)</svg:text></svg:g><svg:g><svg:rect x="1079" width="166" y="180" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1079" y="208" font-size="25.2" stroke="blue" font-family="sans-serif">Hemiterpenes</svg:text></svg:g><svg:g><svg:rect x="457" width="109" y="218" height="21" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="457" y="239" font-size="18.900000000000002" stroke="blue" font-family="sans-serif">Head-tajl</svg:text></svg:g><svg:g><svg:rect x="576" width="155" y="217" height="20" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="576" y="237" font-size="18.0" stroke="blue" font-family="sans-serif">condensation</svg:text></svg:g><svg:g><svg:rect x="1087" width="134" y="214" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1087" y="242" font-size="25.2" stroke="blue" font-family="sans-serif">(Isoprenes)</svg:text></svg:g><svg:g><svg:rect x="571" width="53" y="287" height="22" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="571" y="309" font-size="19.8" stroke="blue" font-family="sans-serif">GPP</svg:text></svg:g><svg:g><svg:rect x="633" width="102" y="286" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="633" y="314" font-size="25.2" stroke="blue" font-family="sans-serif">synthase</svg:text></svg:g><svg:g><svg:rect x="994" width="20" y="258" height="46" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="994" y="304" font-size="41.4" stroke="blue" font-family="sans-serif">y</svg:text></svg:g><svg:g><svg:rect x="863" width="127" y="319" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="863" y="347" font-size="25.2" stroke="blue" font-family="sans-serif">Prenylated</svg:text></svg:g><svg:g><svg:rect x="998" width="138" y="318" height="23" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="998" y="341" font-size="20.7" stroke="blue" font-family="sans-serif">metabolites</svg:text></svg:g><svg:g><svg:rect x="855" width="40" y="361" height="21" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="855" y="382" font-size="18.900000000000002" stroke="blue" font-family="sans-serif">e.g.</svg:text></svg:g><svg:g><svg:rect x="905" width="137" y="353" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="905" y="381" font-size="25.2" stroke="blue" font-family="sans-serif">Cytokinins,</svg:text></svg:g><svg:g><svg:rect x="1051" width="192" y="352" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1051" y="380" font-size="25.2" stroke="blue" font-family="sans-serif">Anthraquinones</svg:text></svg:g><svg:g><svg:rect x="547" width="20" y="396" height="24" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="547" y="420" font-size="21.6" stroke="blue" font-family="sans-serif">Vv</svg:text></svg:g><svg:g><svg:rect x="446" width="95" y="425" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="446" y="452" font-size="24.3" stroke="blue" font-family="sans-serif">Geranyl</svg:text></svg:g><svg:g><svg:rect x="549" width="256" y="424" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="549" y="452" font-size="25.2" stroke="blue" font-family="sans-serif">diphosphate»</svg:text></svg:g><svg:g><svg:rect x="813" width="215" y="423" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="813" y="450" font-size="24.3" stroke="blue" font-family="sans-serif">Monoterpenes__,</svg:text></svg:g><svg:g><svg:rect x="506" width="72" y="459" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="506" y="487" font-size="25.2" stroke="blue" font-family="sans-serif">(GPP;</svg:text></svg:g><svg:g><svg:rect x="587" width="48" y="459" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="587" y="486" font-size="24.3" stroke="blue" font-family="sans-serif">Cio)</svg:text></svg:g><svg:g><svg:rect x="571" width="47" y="528" height="22" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="571" y="550" font-size="19.8" stroke="blue" font-family="sans-serif">FPP</svg:text></svg:g><svg:g><svg:rect x="628" width="103" y="527" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="628" y="555" font-size="25.2" stroke="blue" font-family="sans-serif">synthase</svg:text></svg:g><svg:g><svg:rect x="1032" width="44" y="532" height="20" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1032" y="552" font-size="18.0" stroke="blue" font-family="sans-serif">|-»</svg:text></svg:g><svg:g><svg:rect x="1084" width="108" y="524" height="23" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1084" y="547" font-size="20.7" stroke="blue" font-family="sans-serif">Essential</svg:text></svg:g><svg:g><svg:rect x="1200" width="38" y="523" height="23" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1200" y="546" font-size="20.7" stroke="blue" font-family="sans-serif">Oil</svg:text></svg:g><svg:g><svg:rect x="574" width="26" y="562" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="574" y="589" font-size="24.3" stroke="blue" font-family="sans-serif">(+</svg:text></svg:g><svg:g><svg:rect x="609" width="51" y="562" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="609" y="589" font-size="24.3" stroke="blue" font-family="sans-serif">IPP)</svg:text></svg:g><svg:g><svg:rect x="1027" width="7" y="440" height="205" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1027" y="645" font-size="184.5" stroke="blue" font-family="sans-serif"> </svg:text></svg:g><svg:g><svg:rect x="516" width="67" y="666" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="516" y="694" font-size="25.2" stroke="blue" font-family="sans-serif">(FPP;</svg:text></svg:g><svg:g><svg:rect x="591" width="49" y="666" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="591" y="693" font-size="24.3" stroke="blue" font-family="sans-serif">Cis)</svg:text></svg:g><svg:g><svg:rect x="807" width="148" y="664" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="807" y="692" font-size="25.2" stroke="blue" font-family="sans-serif">Ubiquinone,</svg:text></svg:g><svg:g><svg:rect x="963" width="176" y="663" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="963" y="691" font-size="25.2" stroke="blue" font-family="sans-serif">Plastoquinone,</svg:text></svg:g><svg:g><svg:rect x="809" width="101" y="699" height="23" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="809" y="722" font-size="20.7" stroke="blue" font-family="sans-serif">Abscisic</svg:text></svg:g><svg:g><svg:rect x="919" width="57" y="699" height="26" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="919" y="725" font-size="23.400000000000002" stroke="blue" font-family="sans-serif">acid,</svg:text></svg:g><svg:g><svg:rect x="984" width="128" y="698" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="984" y="726" font-size="25.2" stroke="blue" font-family="sans-serif">Prenylated</svg:text></svg:g><svg:g><svg:rect x="1120" width="96" y="697" height="29" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1120" y="726" font-size="26.1" stroke="blue" font-family="sans-serif">proteins</svg:text></svg:g><svg:g><svg:rect x="577" width="75" y="736" height="22" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="577" y="758" font-size="19.8" stroke="blue" font-family="sans-serif">GGPP</svg:text></svg:g><svg:g><svg:rect x="661" width="102" y="735" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="661" y="763" font-size="25.2" stroke="blue" font-family="sans-serif">synthase</svg:text></svg:g><svg:g><svg:rect x="578" width="26" y="770" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="578" y="798" font-size="25.2" stroke="blue" font-family="sans-serif">(+</svg:text></svg:g><svg:g><svg:rect x="612" width="52" y="770" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="612" y="798" font-size="25.2" stroke="blue" font-family="sans-serif">IPP)</svg:text></svg:g><svg:g><svg:rect x="553" width="12" y="225" height="616" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="553" y="841" font-size="554.4" stroke="blue" font-family="sans-serif"> </svg:text></svg:g><svg:g><svg:rect x="553" width="20" y="815" height="46" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="553" y="861" font-size="41.4" stroke="blue" font-family="sans-serif">v</svg:text></svg:g><svg:g><svg:rect x="422" width="185" y="909" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="422" y="937" font-size="25.2" stroke="blue" font-family="sans-serif">Geranylgeranyl</svg:text></svg:g><svg:g><svg:rect x="616" width="264" y="906" height="30" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="616" y="936" font-size="27.0" stroke="blue" font-family="sans-serif">diphosphate»</svg:text></svg:g><svg:g><svg:rect x="887" width="129" y="907" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="887" y="934" font-size="24.3" stroke="blue" font-family="sans-serif">Diterpenes</svg:text></svg:g><svg:g><svg:rect x="513" width="93" y="944" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="513" y="971" font-size="24.3" stroke="blue" font-family="sans-serif">(GGPP;</svg:text></svg:g><svg:g><svg:rect x="615" width="48" y="943" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="615" y="970" font-size="24.3" stroke="blue" font-family="sans-serif">C20)</svg:text></svg:g><svg:g><svg:rect x="888" width="154" y="940" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="888" y="967" font-size="24.3" stroke="blue" font-family="sans-serif">Gibberellins,</svg:text></svg:g><svg:g><svg:rect x="1051" width="152" y="940" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1051" y="968" font-size="25.2" stroke="blue" font-family="sans-serif">Chlorophyll,</svg:text></svg:g><svg:g><svg:rect x="889" width="127" y="975" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="889" y="1003" font-size="25.2" stroke="blue" font-family="sans-serif">Prenylated</svg:text></svg:g><svg:g><svg:rect x="1024" width="103" y="974" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1024" y="1002" font-size="25.2" stroke="blue" font-family="sans-serif">proteins,</svg:text></svg:g><svg:g><svg:rect x="1137" width="144" y="973" height="23" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="1137" y="996" font-size="20.7" stroke="blue" font-family="sans-serif">Carotenoids</svg:text></svg:g><svg:g><svg:rect x="582" width="26" y="1012" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="582" y="1040" font-size="25.2" stroke="blue" font-family="sans-serif">(+</svg:text></svg:g><svg:g><svg:rect x="617" width="62" y="1012" height="27" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="617" y="1039" font-size="24.3" stroke="blue" font-family="sans-serif">PP).</svg:text></svg:g><svg:g><svg:rect x="508" width="154" y="1116" height="28" stroke-width="1.0" stroke="red" fill="none"/><svg:text x="508" y="1144" font-size="25.2" stroke="blue" font-family="sans-serif">Polyterpenes</svg:text></svg:g></svg:svg>
\ No newline at end of file
diff --git a/test/resources/biosynth_path_2/raw.jpg b/test/resources/biosynth_path_2/raw.jpg
new file mode 100644
index 0000000..f423425
Binary files /dev/null and b/test/resources/biosynth_path_2/raw.jpg differ
diff --git a/test/resources/biosynth_path_2/single.png b/test/resources/biosynth_path_2/single.png
deleted file mode 100644
index d8791be..0000000
Binary files a/test/resources/biosynth_path_2/single.png and /dev/null differ
diff --git a/test/resources/tesseract_biosynth_path_3.hocr.html b/test/resources/biosynth_path_3/hocr.html
similarity index 100%
rename from test/resources/tesseract_biosynth_path_3.hocr.html
rename to test/resources/biosynth_path_3/hocr.html
diff --git a/test/resources/biosynth_path_3/p0.png b/test/resources/biosynth_path_3/p0.png
deleted file mode 100644
index ac4902c..0000000
Binary files a/test/resources/biosynth_path_3/p0.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3/p1.png b/test/resources/biosynth_path_3/p1.png
deleted file mode 100644
index a790afd..0000000
Binary files a/test/resources/biosynth_path_3/p1.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3/p2.png b/test/resources/biosynth_path_3/p2.png
deleted file mode 100644
index 6b25401..0000000
Binary files a/test/resources/biosynth_path_3/p2.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3/p3.png b/test/resources/biosynth_path_3/p3.png
deleted file mode 100644
index 0877754..0000000
Binary files a/test/resources/biosynth_path_3/p3.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3/p4.png b/test/resources/biosynth_path_3/p4.png
deleted file mode 100644
index 2a5bc66..0000000
Binary files a/test/resources/biosynth_path_3/p4.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3/p5.png b/test/resources/biosynth_path_3/p5.png
deleted file mode 100644
index 808114d..0000000
Binary files a/test/resources/biosynth_path_3/p5.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3/p6.png b/test/resources/biosynth_path_3/p6.png
deleted file mode 100644
index e980b2d..0000000
Binary files a/test/resources/biosynth_path_3/p6.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3/p7.png b/test/resources/biosynth_path_3/p7.png
deleted file mode 100644
index abaf2ad..0000000
Binary files a/test/resources/biosynth_path_3/p7.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3/palette.png b/test/resources/biosynth_path_3/palette.png
deleted file mode 100644
index f175a1a..0000000
Binary files a/test/resources/biosynth_path_3/palette.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3.png b/test/resources/biosynth_path_3/raw.png
similarity index 100%
rename from test/resources/biosynth_path_3.png
rename to test/resources/biosynth_path_3/raw.png
diff --git a/test/resources/biosynth_path_3/rgb.png b/test/resources/biosynth_path_3/rgb.png
deleted file mode 100644
index 7016814..0000000
Binary files a/test/resources/biosynth_path_3/rgb.png and /dev/null differ
diff --git a/test/resources/biosynth_path_3/single.png b/test/resources/biosynth_path_3/single.png
deleted file mode 100644
index dbd2684..0000000
Binary files a/test/resources/biosynth_path_3/single.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4.hocr b/test/resources/biosynth_path_4/hocr.html
similarity index 100%
rename from test/resources/biosynth_path_4.hocr
rename to test/resources/biosynth_path_4/hocr.html
diff --git a/test/resources/biosynth_path_4/p0.png b/test/resources/biosynth_path_4/p0.png
deleted file mode 100644
index 1e9e516..0000000
Binary files a/test/resources/biosynth_path_4/p0.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4/p1.png b/test/resources/biosynth_path_4/p1.png
deleted file mode 100644
index 9e3c8fc..0000000
Binary files a/test/resources/biosynth_path_4/p1.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4/p2.png b/test/resources/biosynth_path_4/p2.png
deleted file mode 100644
index f3a8318..0000000
Binary files a/test/resources/biosynth_path_4/p2.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4/p3.png b/test/resources/biosynth_path_4/p3.png
deleted file mode 100644
index dc56554..0000000
Binary files a/test/resources/biosynth_path_4/p3.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4/p4.png b/test/resources/biosynth_path_4/p4.png
deleted file mode 100644
index 39758dd..0000000
Binary files a/test/resources/biosynth_path_4/p4.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4/p5.png b/test/resources/biosynth_path_4/p5.png
deleted file mode 100644
index 2ce801d..0000000
Binary files a/test/resources/biosynth_path_4/p5.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4/p6.png b/test/resources/biosynth_path_4/p6.png
deleted file mode 100644
index dbd61dc..0000000
Binary files a/test/resources/biosynth_path_4/p6.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4/p7.png b/test/resources/biosynth_path_4/p7.png
deleted file mode 100644
index c4c37a6..0000000
Binary files a/test/resources/biosynth_path_4/p7.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4/palette.png b/test/resources/biosynth_path_4/palette.png
deleted file mode 100644
index b85ad8d..0000000
Binary files a/test/resources/biosynth_path_4/palette.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4.jpeg b/test/resources/biosynth_path_4/raw.jpeg
similarity index 100%
rename from test/resources/biosynth_path_4.jpeg
rename to test/resources/biosynth_path_4/raw.jpeg
diff --git a/test/resources/biosynth_path_4/rgb.png b/test/resources/biosynth_path_4/rgb.png
deleted file mode 100644
index 1d83e34..0000000
Binary files a/test/resources/biosynth_path_4/rgb.png and /dev/null differ
diff --git a/test/resources/biosynth_path_4/single.png b/test/resources/biosynth_path_4/single.png
deleted file mode 100644
index d016267..0000000
Binary files a/test/resources/biosynth_path_4/single.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5.hocr b/test/resources/biosynth_path_5/hocr.html
similarity index 100%
rename from test/resources/biosynth_path_5.hocr
rename to test/resources/biosynth_path_5/hocr.html
diff --git a/test/resources/biosynth_path_5/p0.png b/test/resources/biosynth_path_5/p0.png
deleted file mode 100644
index 6285c4a..0000000
Binary files a/test/resources/biosynth_path_5/p0.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5/p1.png b/test/resources/biosynth_path_5/p1.png
deleted file mode 100644
index 49656e0..0000000
Binary files a/test/resources/biosynth_path_5/p1.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5/p2.png b/test/resources/biosynth_path_5/p2.png
deleted file mode 100644
index c15c011..0000000
Binary files a/test/resources/biosynth_path_5/p2.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5/p3.png b/test/resources/biosynth_path_5/p3.png
deleted file mode 100644
index 02cab53..0000000
Binary files a/test/resources/biosynth_path_5/p3.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5/p4.png b/test/resources/biosynth_path_5/p4.png
deleted file mode 100644
index 50c3c40..0000000
Binary files a/test/resources/biosynth_path_5/p4.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5/p5.png b/test/resources/biosynth_path_5/p5.png
deleted file mode 100644
index bd17a4c..0000000
Binary files a/test/resources/biosynth_path_5/p5.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5/p6.png b/test/resources/biosynth_path_5/p6.png
deleted file mode 100644
index fc15179..0000000
Binary files a/test/resources/biosynth_path_5/p6.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5/p7.png b/test/resources/biosynth_path_5/p7.png
deleted file mode 100644
index 6cf81ad..0000000
Binary files a/test/resources/biosynth_path_5/p7.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5/palette.png b/test/resources/biosynth_path_5/palette.png
deleted file mode 100644
index f1c3195..0000000
Binary files a/test/resources/biosynth_path_5/palette.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5.jpeg b/test/resources/biosynth_path_5/raw.jpeg
similarity index 100%
rename from test/resources/biosynth_path_5.jpeg
rename to test/resources/biosynth_path_5/raw.jpeg
diff --git a/test/resources/biosynth_path_5/rgb.png b/test/resources/biosynth_path_5/rgb.png
deleted file mode 100644
index 5c552eb..0000000
Binary files a/test/resources/biosynth_path_5/rgb.png and /dev/null differ
diff --git a/test/resources/biosynth_path_5/single.png b/test/resources/biosynth_path_5/single.png
deleted file mode 100644
index ef7a61b..0000000
Binary files a/test/resources/biosynth_path_5/single.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6_compounds_only.jpeg b/test/resources/biosynth_path_6/compounds_only.jpeg
similarity index 100%
rename from test/resources/biosynth_path_6_compounds_only.jpeg
rename to test/resources/biosynth_path_6/compounds_only.jpeg
diff --git a/test/resources/biosynth_path_6.hocr b/test/resources/biosynth_path_6/hocr.html
similarity index 100%
rename from test/resources/biosynth_path_6.hocr
rename to test/resources/biosynth_path_6/hocr.html
diff --git a/test/resources/biosynth_path_6/p0.png b/test/resources/biosynth_path_6/p0.png
deleted file mode 100644
index 26ea79f..0000000
Binary files a/test/resources/biosynth_path_6/p0.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6/p1.png b/test/resources/biosynth_path_6/p1.png
deleted file mode 100644
index b5dfd63..0000000
Binary files a/test/resources/biosynth_path_6/p1.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6/p2.png b/test/resources/biosynth_path_6/p2.png
deleted file mode 100644
index a7e2629..0000000
Binary files a/test/resources/biosynth_path_6/p2.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6/p3.png b/test/resources/biosynth_path_6/p3.png
deleted file mode 100644
index ed2d7bd..0000000
Binary files a/test/resources/biosynth_path_6/p3.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6/p4.png b/test/resources/biosynth_path_6/p4.png
deleted file mode 100644
index e4fc7ab..0000000
Binary files a/test/resources/biosynth_path_6/p4.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6/p5.png b/test/resources/biosynth_path_6/p5.png
deleted file mode 100644
index 38ebc3c..0000000
Binary files a/test/resources/biosynth_path_6/p5.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6/p6.png b/test/resources/biosynth_path_6/p6.png
deleted file mode 100644
index e1549ec..0000000
Binary files a/test/resources/biosynth_path_6/p6.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6/p7.png b/test/resources/biosynth_path_6/p7.png
deleted file mode 100644
index d59ac6e..0000000
Binary files a/test/resources/biosynth_path_6/p7.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6/palette.png b/test/resources/biosynth_path_6/palette.png
deleted file mode 100644
index 5e387a7..0000000
Binary files a/test/resources/biosynth_path_6/palette.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6.jpeg b/test/resources/biosynth_path_6/raw.jpeg
similarity index 100%
rename from test/resources/biosynth_path_6.jpeg
rename to test/resources/biosynth_path_6/raw.jpeg
diff --git a/test/resources/biosynth_path_6/rgb.png b/test/resources/biosynth_path_6/rgb.png
deleted file mode 100644
index 1336eaa..0000000
Binary files a/test/resources/biosynth_path_6/rgb.png and /dev/null differ
diff --git a/test/resources/biosynth_path_6/single.png b/test/resources/biosynth_path_6/single.png
deleted file mode 100644
index ceb9192..0000000
Binary files a/test/resources/biosynth_path_6/single.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7.hocr b/test/resources/biosynth_path_7/hocr.html
similarity index 100%
rename from test/resources/biosynth_path_7.hocr
rename to test/resources/biosynth_path_7/hocr.html
diff --git a/test/resources/biosynth_path_7/p0.png b/test/resources/biosynth_path_7/p0.png
deleted file mode 100644
index 2468262..0000000
Binary files a/test/resources/biosynth_path_7/p0.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7/p1.png b/test/resources/biosynth_path_7/p1.png
deleted file mode 100644
index 8508a94..0000000
Binary files a/test/resources/biosynth_path_7/p1.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7/p2.png b/test/resources/biosynth_path_7/p2.png
deleted file mode 100644
index 474bb7f..0000000
Binary files a/test/resources/biosynth_path_7/p2.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7/p3.png b/test/resources/biosynth_path_7/p3.png
deleted file mode 100644
index 5c49728..0000000
Binary files a/test/resources/biosynth_path_7/p3.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7/p4.png b/test/resources/biosynth_path_7/p4.png
deleted file mode 100644
index 3e1495e..0000000
Binary files a/test/resources/biosynth_path_7/p4.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7/p5.png b/test/resources/biosynth_path_7/p5.png
deleted file mode 100644
index e2e57a2..0000000
Binary files a/test/resources/biosynth_path_7/p5.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7/p6.png b/test/resources/biosynth_path_7/p6.png
deleted file mode 100644
index af1788c..0000000
Binary files a/test/resources/biosynth_path_7/p6.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7/p7.png b/test/resources/biosynth_path_7/p7.png
deleted file mode 100644
index b04c2b3..0000000
Binary files a/test/resources/biosynth_path_7/p7.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7/palette.png b/test/resources/biosynth_path_7/palette.png
deleted file mode 100644
index 66914da..0000000
Binary files a/test/resources/biosynth_path_7/palette.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7.jpeg b/test/resources/biosynth_path_7/raw.jpeg
similarity index 100%
rename from test/resources/biosynth_path_7.jpeg
rename to test/resources/biosynth_path_7/raw.jpeg
diff --git a/test/resources/biosynth_path_7/rgb.png b/test/resources/biosynth_path_7/rgb.png
deleted file mode 100644
index f9b55de..0000000
Binary files a/test/resources/biosynth_path_7/rgb.png and /dev/null differ
diff --git a/test/resources/biosynth_path_7/single.png b/test/resources/biosynth_path_7/single.png
deleted file mode 100644
index 5337e3a..0000000
Binary files a/test/resources/biosynth_path_7/single.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8_default.hocr b/test/resources/biosynth_path_8/default_hocr.html
similarity index 100%
rename from test/resources/biosynth_path_8_default.hocr
rename to test/resources/biosynth_path_8/default_hocr.html
diff --git a/test/resources/biosynth_path_8.hocr b/test/resources/biosynth_path_8/hocr.html
similarity index 100%
rename from test/resources/biosynth_path_8.hocr
rename to test/resources/biosynth_path_8/hocr.html
diff --git a/test/resources/biosynth_path_8/p0.png b/test/resources/biosynth_path_8/p0.png
deleted file mode 100644
index 29280d9..0000000
Binary files a/test/resources/biosynth_path_8/p0.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8/p1.png b/test/resources/biosynth_path_8/p1.png
deleted file mode 100644
index 76f6c1e..0000000
Binary files a/test/resources/biosynth_path_8/p1.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8/p2.png b/test/resources/biosynth_path_8/p2.png
deleted file mode 100644
index c93bd01..0000000
Binary files a/test/resources/biosynth_path_8/p2.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8/p3.png b/test/resources/biosynth_path_8/p3.png
deleted file mode 100644
index aedcad2..0000000
Binary files a/test/resources/biosynth_path_8/p3.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8/p4.png b/test/resources/biosynth_path_8/p4.png
deleted file mode 100644
index 02aff5b..0000000
Binary files a/test/resources/biosynth_path_8/p4.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8/p5.png b/test/resources/biosynth_path_8/p5.png
deleted file mode 100644
index 85585b5..0000000
Binary files a/test/resources/biosynth_path_8/p5.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8/p6.png b/test/resources/biosynth_path_8/p6.png
deleted file mode 100644
index 609c267..0000000
Binary files a/test/resources/biosynth_path_8/p6.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8/p7.png b/test/resources/biosynth_path_8/p7.png
deleted file mode 100644
index 30523fe..0000000
Binary files a/test/resources/biosynth_path_8/p7.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8/palette.png b/test/resources/biosynth_path_8/palette.png
deleted file mode 100644
index 025ce71..0000000
Binary files a/test/resources/biosynth_path_8/palette.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8.jpeg b/test/resources/biosynth_path_8/raw.jpeg
similarity index 100%
rename from test/resources/biosynth_path_8.jpeg
rename to test/resources/biosynth_path_8/raw.jpeg
diff --git a/test/resources/biosynth_path_8/rgb.png b/test/resources/biosynth_path_8/rgb.png
deleted file mode 100644
index a5c3ef1..0000000
Binary files a/test/resources/biosynth_path_8/rgb.png and /dev/null differ
diff --git a/test/resources/biosynth_path_8/single.png b/test/resources/biosynth_path_8/single.png
deleted file mode 100644
index 9b98493..0000000
Binary files a/test/resources/biosynth_path_8/single.png and /dev/null differ
diff --git a/test/resources/green_old/p0.png b/test/resources/green_old/p0.png
new file mode 100644
index 0000000..b3288f0
Binary files /dev/null and b/test/resources/green_old/p0.png differ
diff --git a/test/resources/green_old/p1.png b/test/resources/green_old/p1.png
new file mode 100644
index 0000000..1d12152
Binary files /dev/null and b/test/resources/green_old/p1.png differ
diff --git a/test/resources/green_old/p2.png b/test/resources/green_old/p2.png
new file mode 100644
index 0000000..d26efba
Binary files /dev/null and b/test/resources/green_old/p2.png differ
diff --git a/test/resources/green_old/p3.png b/test/resources/green_old/p3.png
new file mode 100644
index 0000000..a339900
Binary files /dev/null and b/test/resources/green_old/p3.png differ
diff --git a/test/resources/green_old/p4.png b/test/resources/green_old/p4.png
new file mode 100644
index 0000000..bd72cab
Binary files /dev/null and b/test/resources/green_old/p4.png differ
diff --git a/test/resources/green_old/p5.png b/test/resources/green_old/p5.png
new file mode 100644
index 0000000..b3323c2
Binary files /dev/null and b/test/resources/green_old/p5.png differ
diff --git a/test/resources/green_old/p6.png b/test/resources/green_old/p6.png
new file mode 100644
index 0000000..90c9c4c
Binary files /dev/null and b/test/resources/green_old/p6.png differ
diff --git a/test/resources/green_old/p7.png b/test/resources/green_old/p7.png
new file mode 100644
index 0000000..560f0ad
Binary files /dev/null and b/test/resources/green_old/p7.png differ
diff --git a/test/resources/green/palette_rgb.png b/test/resources/green_old/palette.png
similarity index 100%
rename from test/resources/green/palette_rgb.png
rename to test/resources/green_old/palette.png
diff --git a/test/resources/green_old/palette_rgb.png b/test/resources/green_old/palette_rgb.png
new file mode 100644
index 0000000..df82cb3
Binary files /dev/null and b/test/resources/green_old/palette_rgb.png differ
diff --git a/test/resources/green_old/rgb.png b/test/resources/green_old/rgb.png
new file mode 100644
index 0000000..a808997
Binary files /dev/null and b/test/resources/green_old/rgb.png differ
diff --git a/test/resources/green_old/single.png b/test/resources/green_old/single.png
new file mode 100644
index 0000000..11606c8
Binary files /dev/null and b/test/resources/green_old/single.png differ
diff --git a/test/resources/biosynth_path_2/p0.png b/test/resources/old_octree/biosynth_path_2/p0.png
similarity index 100%
rename from test/resources/biosynth_path_2/p0.png
rename to test/resources/old_octree/biosynth_path_2/p0.png
diff --git a/test/resources/biosynth_path_2/p1.png b/test/resources/old_octree/biosynth_path_2/p1.png
similarity index 100%
rename from test/resources/biosynth_path_2/p1.png
rename to test/resources/old_octree/biosynth_path_2/p1.png
diff --git a/test/resources/biosynth_path_2/p2.png b/test/resources/old_octree/biosynth_path_2/p2.png
similarity index 100%
rename from test/resources/biosynth_path_2/p2.png
rename to test/resources/old_octree/biosynth_path_2/p2.png
diff --git a/test/resources/biosynth_path_2/p3.png b/test/resources/old_octree/biosynth_path_2/p3.png
similarity index 100%
rename from test/resources/biosynth_path_2/p3.png
rename to test/resources/old_octree/biosynth_path_2/p3.png
diff --git a/test/resources/biosynth_path_2/p4.png b/test/resources/old_octree/biosynth_path_2/p4.png
similarity index 100%
rename from test/resources/biosynth_path_2/p4.png
rename to test/resources/old_octree/biosynth_path_2/p4.png
diff --git a/test/resources/biosynth_path_2/p5.png b/test/resources/old_octree/biosynth_path_2/p5.png
similarity index 100%
rename from test/resources/biosynth_path_2/p5.png
rename to test/resources/old_octree/biosynth_path_2/p5.png
diff --git a/test/resources/biosynth_path_2/p6.png b/test/resources/old_octree/biosynth_path_2/p6.png
similarity index 100%
rename from test/resources/biosynth_path_2/p6.png
rename to test/resources/old_octree/biosynth_path_2/p6.png
diff --git a/test/resources/biosynth_path_2/p7.png b/test/resources/old_octree/biosynth_path_2/p7.png
similarity index 100%
rename from test/resources/biosynth_path_2/p7.png
rename to test/resources/old_octree/biosynth_path_2/p7.png
diff --git a/test/resources/biosynth_path_2/palette.png b/test/resources/old_octree/biosynth_path_2/palette.png
similarity index 100%
rename from test/resources/biosynth_path_2/palette.png
rename to test/resources/old_octree/biosynth_path_2/palette.png
diff --git a/test/resources/old_octree/biosynth_path_2/raw.jpg b/test/resources/old_octree/biosynth_path_2/raw.jpg
new file mode 100644
index 0000000..f423425
Binary files /dev/null and b/test/resources/old_octree/biosynth_path_2/raw.jpg differ
diff --git a/test/resources/biosynth_path_2/rgb.png b/test/resources/old_octree/biosynth_path_2/rgb.png
similarity index 100%
rename from test/resources/biosynth_path_2/rgb.png
rename to test/resources/old_octree/biosynth_path_2/rgb.png
diff --git a/test/resources/output.txt.txt b/test/resources/output.txt.txt
new file mode 100644
index 0000000..31e78ef
--- /dev/null
+++ b/test/resources/output.txt.txt
@@ -0,0 +1,10 @@
+0.5
+
+0.4
+
+0.3
+
+0.2
+
+0.1
+
\ No newline at end of file
diff --git a/test/resources/pmc8839570.jpeg b/test/resources/pmc8839570.jpeg
new file mode 100644
index 0000000..1f4cc28
Binary files /dev/null and b/test/resources/pmc8839570.jpeg differ
diff --git a/test/resources/test_img_shapes.png b/test/resources/test_img_shapes.png
new file mode 100644
index 0000000..3b0c869
Binary files /dev/null and b/test/resources/test_img_shapes.png differ
diff --git a/test/test_ami_graph.py b/test/test_ami_graph.py
index 48eb808..ddc74ff 100644
--- a/test/test_ami_graph.py
+++ b/test/test_ami_graph.py
@@ -1,7 +1,6 @@
 """
 tests AmiGraph, AmiNode, AmiEdge, AmiIsland
 """
-
 import logging
 import unittest
 from collections import Counter
@@ -10,28 +9,32 @@
 
 import matplotlib.pyplot as plt
 import networkx as nx
+import numpy
 import numpy as np
 import pytest
 import sknw
-from skimage import data
-from skimage import morphology
+from skimage import data, morphology
 from skimage.measure import approximate_polygon, subdivide_polygon
 from skimage.morphology import skeletonize
 
 # local
-# from ..pyimage.ami_edge_manager import AmiEdgeAnalyzer, X, Y
-from ..pyimage.ami_graph_all import AmiNode, AmiIsland, AmiGraph, AmiEdge
-from ..pyimage.ami_image import AmiImage
-from ..pyimage.ami_plot import AmiEdgeTool, AmiLine
-from ..pyimage.ami_util import AmiUtil
-from ..pyimage.bbox import BBox
-from ..pyimage.text_box import TextBox, TextUtil
-from ..test.resources import Resources
+import context 
+from pyamiimage.ami_edge_manager import AmiEdgeAnalyzer, X, Y
+from pyamiimage.ami_graph_all import AmiNode, AmiIsland, AmiGraph, AmiEdge
+from pyamiimage.ami_image import AmiImage
+from pyamiimage.ami_plot import AmiEdgeTool, AmiLine
+from pyamiimage.ami_util import AmiUtil
+from pyamiimage.bbox import BBox
+from pyamiimage.text_box import TextBox, TextUtil
+from resources import Resources
 
 logger = logging.getLogger(__name__)
 
 interactive = False
 
+RAW = 0
+BOXED = 1
+
 
 class TestAmiGraph:
 
@@ -63,7 +66,7 @@ def setup_method(self, method):
         return self
 
     @unittest.skip("background")
-    def test_sknw_example(self):
+    def test_sknw_example(self, interact=interactive):
         """
         From the SKNW docs
         not really a test, more a debug
@@ -189,14 +192,14 @@ def test_sknw_5_islands(self):
 
         :return:
         """
-        skel_path = Resources.BIOSYNTH1_ARROWS
+        skel_path = Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
         assert isinstance(skel_path, PurePath)
 
         skeleton_array = AmiImage.create_white_skeleton_from_file(skel_path)
         AmiUtil.check_type_and_existence(skeleton_array, np.ndarray)
 
         # nx_graph = AmiGraph.create_nx_graph_from_skeleton(skeleton_array)
-        nx_graph = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        nx_graph = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
         AmiUtil.check_type_and_existence(nx_graph, nx.classes.multigraph.MultiGraph)
 
         assert list(nx_graph.nodes) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
@@ -236,13 +239,10 @@ def test_sknw_5_islands(self):
     def test_ami_edges(self):
         """wrappers for nx_graph
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
         print("------------------")
         ami_edges = ami_graph.get_or_create_all_ami_edges()
-        print("==================")
         assert len(ami_edges) == 23, f"found {len(ami_edges)}"
-        print(f"len edges {len(ami_edges)}")
-        # print (f"ami_edge 0 {ami_edges[0]}")
 
     # @unittest.skip("needs multigraph adding NYI")
     def test_segmented_edges(self):
@@ -256,7 +256,7 @@ def test_segmented_edges(self):
         Note: double backslash is an escape, not meaningful
         :return:
         """
-        nx_graph = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        nx_graph = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
 
         """
         {0, 1, 2, 3, 4, 5, 6, 7},  # double arrow
@@ -286,12 +286,12 @@ def test_segmented_edges(self):
                                    [89, 856],
                                    [91, 857]])
         assert type(expected_numpy[0]) is np.ndarray
-        assert type(expected_numpy[0][0]) is np.int32
+        assert type(expected_numpy[0][0]) is np.int64
         assert np.array_equal(points0_2, expected_numpy), f"found {points0_2}"
 
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
         ami_edges = ami_graph.get_or_create_all_ami_edges()
-        print(f"len(edges) {len(ami_edges)}")
+        assert len(ami_edges) == 23
         for ami_edge in ami_edges:
             for tol in range(1, 10, 3):
                 segments = ami_edge.create_line_segments(tolerance=tol)
@@ -337,14 +337,16 @@ def test_arrows(self):
         * half arrow. point with one edge backwards (e.g. in chemical equilibrium
         :return:
         """
-        AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
 
     def test_islands(self):
         """
         Create island_node_id_sets using sknw/NetworkX and check basic properties
         :return:
         """
-        nx_graph = AmiGraph.create_nx_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        nx_graph = AmiGraph.create_nx_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
 
         connected_components = list(nx.algorithms.components.connected_components(nx_graph))
         assert nx.algorithms.components.number_connected_components(nx_graph) == 4
@@ -354,7 +356,7 @@ def test_islands(self):
             {0, 1, 2, 3, 4, 5, 6, 7},
             {8, 9, 26, 19},
             {10, 11, 12, 13, 14, 15, 16, 17, 18, 20},
-            {21, 22, 23, 24, 25}
+            {21, 22, 23, 24, 25},
         ]
 
         assert type(connected_components[0]) is set and len(connected_components[0]) == 8, \
@@ -373,40 +375,47 @@ def test_node_centroid(self):
         Tests
         :return:
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
         xy = ami_graph.get_or_create_centroid_xy(0)
         assert xy == [844.0, 82.0]
 
     def test_get_nx_edge_list_for_node(self):
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
         edges = ami_graph.get_nx_edge_list_for_node(24)
         assert edges == [(24, 21, 0), (24, 22, 0), (24, 23, 0), (24, 25, 0)], \
             f"found {edges} expected {[(24, 21, 0), (24, 22, 0), (24, 23, 0), (24, 25, 0)]}"
 
     def test_get_nx_edge_lengths_for_node(self):
-        ami_graph = self.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = self.create_ami_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
         lengths = ami_graph.get_nx_edge_lengths_by_edge_list_for_node(24)
         print(f"lengths {lengths}")
         lengthsx = [length for length in lengths.values()]
         assert [0.1 + 0.2, 0.2 + 0.4] == pytest.approx([0.3, 0.6])
         aaa = [30.00, 8.944, 9.848, 12.041]
         expect = pytest.approx(aaa, 0.001)
-        assert lengthsx == expect, \
-            f"found {lengthsx} expected {expect}"
+        assert lengthsx == expect, f"found {lengthsx} expected {expect}"
 
     def test_get_nodes_with_degree(self):
         """
         uses get_nodes_with_degree on each node to create lists
         :return:
         """
-        ami_graph = self.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = self.create_ami_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
         self.assert_degrees(ami_graph, 4, [2, 4, 13, 18, 24])
         self.assert_degrees(ami_graph, 3, [12, 19])
         self.assert_degrees(ami_graph, 2, [])
         self.assert_degrees(ami_graph, 1, [0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 20, 21, 22, 23, 25, 26])
 
     def test_distal_node(self):
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
         edge = ami_graph.get_nx_edge_list_for_node(1)[0]
         ami_edge0 = ami_graph.get_or_create_ami_edge_from_ids(edge[0], edge[1], edge[2])
         assert ami_edge0.start_id == 1
@@ -416,13 +425,16 @@ def test_distal_node(self):
         assert ami_edge0.remote_node_id(3) is None
 
     def test_get_neighbours(self):
-        ami_graph = self.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = self.create_ami_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
         assert [2] == ami_graph.get_or_create_ami_node(0).get_neighbour_ids()
         assert [4] == ami_graph.get_or_create_ami_node(1).get_neighbour_ids()
         assert [0, 4, 3, 7] == ami_graph.get_or_create_ami_node(2).get_neighbour_ids()
         assert [10, 13, 18] == ami_graph.get_or_create_ami_node(12).get_neighbour_ids()
 
-    def create_ami_graph_from_arbitrary_image_file(self, path):
+    @classmethod
+    def create_ami_graph_from_arbitrary_image_file(cls, path):
         """
         creates nx_graph and wraps it in AmiGraph object
         :return: AmiGraph
@@ -431,18 +443,45 @@ def create_ami_graph_from_arbitrary_image_file(self, path):
         ami_graph = AmiGraph(nx_graph=nx_graph)
         return ami_graph
 
-    @unittest.skip("NYI")
     def test_get_angles_of_edges_node(self):
-        ami_graph = self.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
-        edges = ami_graph.get_nx_edge_list_for_node(24)
-        angles = AmiNode.calculate_angles_to_edges(edges)
+        ami_graph = self.create_ami_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
+        edge_ids = ami_graph.get_nx_edge_list_for_node(24)
+        assert edge_ids == [(24, 21, 0), (24, 22, 0), (24, 23, 0), (24, 25, 0)]
+        angles = []
+        for i, edgei_id in enumerate(edge_ids):
+            edgei = ami_graph.get_ami_edge_from_nx_id(edgei_id)
+            for j, edgej_id in enumerate(edge_ids):
+                if j > i:
+                    edgej = ami_graph.get_ami_edge_from_nx_id(edgej_id)
+                    common_node_id = AmiEdge.get_common_node_id(edgei_id, edgej_id)
+                    if common_node_id is not None:
+                        print(f"{edgei_id} {edgej_id} common_node {common_node_id}")
+                        angle_i_j = ami_graph.get_angle_between_nodes(
+                            edgei.remote_node_id(common_node_id),
+                            common_node_id,
+                            edgej.remote_node_id(common_node_id),
+                        )
+                        angleicj = (i, common_node_id, j, round(angle_i_j, 3))
+                        angles.append(angleicj)
+        assert angles == [
+            (0, 24, 1, -1.107),
+            (0, 24, 2, 1.153),
+            (0, 24, 3, 3.058),
+            (1, 24, 2, 2.26),
+            (1, 24, 3, -2.118),
+            (2, 24, 3, 1.906),
+        ]
 
     def test_bboxes(self):
         """
         Create bounding boxes for islands
         :return:
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
         islands = ami_graph.get_or_create_ami_islands()
         assert len(islands) == 4, "arrows"
         bbox_list = []
@@ -454,7 +493,7 @@ def test_bboxes(self):
         # this is horrible and fragile, need __eq__ for bbox
         assert str(bbox_list[0]) == "[[661, 863], [82, 102]]", f"bbox_list[0] is {bbox_list[0]}"
 
-    def test_line_segments(self):
+    def test_line_segments(self, interact=interactive):
         """
         split edges into segments (Douglas-Paucker) - Python tutorial
         :return:
@@ -464,28 +503,32 @@ def test_line_segments(self):
         fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(9, 4))
 
         if not ignore_hand:
-            hand = np.array([[1.64516129, 1.16145833],
-                             [1.64516129, 1.59375],
-                             [1.35080645, 1.921875],
-                             [1.375, 2.18229167],
-                             [1.68548387, 1.9375],
-                             [1.60887097, 2.55208333],
-                             [1.68548387, 2.69791667],
-                             [1.76209677, 2.56770833],
-                             [1.83064516, 1.97395833],
-                             [1.89516129, 2.75],
-                             [1.9516129, 2.84895833],
-                             [2.01209677, 2.76041667],
-                             [1.99193548, 1.99479167],
-                             [2.11290323, 2.63020833],
-                             [2.2016129, 2.734375],
-                             [2.25403226, 2.60416667],
-                             [2.14919355, 1.953125],
-                             [2.30645161, 2.36979167],
-                             [2.39112903, 2.36979167],
-                             [2.41532258, 2.1875],
-                             [2.1733871, 1.703125],
-                             [2.07782258, 1.16666667]])
+            hand = np.array(
+                [
+                    [1.64516129, 1.16145833],
+                    [1.64516129, 1.59375],
+                    [1.35080645, 1.921875],
+                    [1.375, 2.18229167],
+                    [1.68548387, 1.9375],
+                    [1.60887097, 2.55208333],
+                    [1.68548387, 2.69791667],
+                    [1.76209677, 2.56770833],
+                    [1.83064516, 1.97395833],
+                    [1.89516129, 2.75],
+                    [1.9516129, 2.84895833],
+                    [2.01209677, 2.76041667],
+                    [1.99193548, 1.99479167],
+                    [2.11290323, 2.63020833],
+                    [2.2016129, 2.734375],
+                    [2.25403226, 2.60416667],
+                    [2.14919355, 1.953125],
+                    [2.30645161, 2.36979167],
+                    [2.39112903, 2.36979167],
+                    [2.41532258, 2.1875],
+                    [2.1733871, 1.703125],
+                    [2.07782258, 1.16666667],
+                ]
+            )
 
             # subdivide polygon using 2nd degree B-Splines (green)
             new_hand = hand.copy()
@@ -500,14 +543,15 @@ def test_line_segments(self):
             ax1.plot(hand[:, 0], hand[:, 1])
             ax1.plot(appr_hand[:, 0], appr_hand[:, 1])
 
-        points = np.array([
-            [1., 1.],
-            [1.1, 2.],
-            [0.9, 3.],
-            [2., 2.9],
-            [3., 3.],
-
-        ])
+        points = np.array(
+            [
+                [1.0, 1.0],
+                [1.1, 2.0],
+                [0.9, 3.0],
+                [2.0, 2.9],
+                [3.0, 3.0],
+            ]
+        )
         ax2.plot(points[:, 0], points[:, 1])
         tolerance = 0.5
         points2 = approximate_polygon(points, tolerance=tolerance)
@@ -515,41 +559,6 @@ def test_line_segments(self):
         if interactive:
             plt.show()
 
-    # @unittest.skip("not needed as skimage works")
-    # def test_rdp_line_segments(self):
-    #     """rdp method may interface better than the approximate_polygon one"""
-    #     arr = np.array([1, 1, 2, 2, 3, 3, 4, 4]).reshape(4, 2)
-    #     mask = rdp(arr, algo="iter", return_mask=True)
-    #     print(f"mask {mask}")
-    #     print(f"arr {arr[mask]}")
-    #     ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
-    #     islands = ami_graph.get_or_create_ami_islands(mindim=50, maxmindim=300)
-    #     assert len(islands) == 1, f"expected single islands {len(islands)}"
-    #     small_plot = islands[0]
-    #     node_ids = small_plot.node_ids
-    #     assert len(node_ids) == 36, f"nodes in small graph {len(node_ids)}"
-    #     assert node_ids == {258, 132, 389, 136, 24, 288, 546, 36, 40, 424, 46, 177, 569, 63, 448, 193,
-    #                         323, 590, 591, 592, 593, 594, 595, 596, 86, 605, 606, 607, 608, 609, 354,
-    #                         101, 103, 487, 114, 510}, f"found {node_ids}"
-    #     assert small_plot.nx_edges == [
-    #         (258, 323, 0), (132, 177, 0), (389, 448, 0), (136, 177, 0), (136, 193, 0), (24, 36, 0), (24, 36, 1),
-    #         (24, 40, 0),
-    #         (288, 323, 0), (288, 354, 0), (546, 569, 0), (546, 595, 0), (36, 46, 0), (40, 46, 0), (40, 63, 0),
-    #         (424, 448, 0),
-    #         (424, 487, 0), (46, 86, 0), (177, 258, 0), (569, 590, 0), (63, 86, 0), (63, 101, 0), (448, 510, 0),
-    #         (193, 258, 0),
-    #         (193, 288, 0), (323, 389, 0), (590, 591, 0), (590, 608, 0), (591, 592, 0), (591, 605, 0), (592, 593, 0),
-    #         (592, 606, 0),
-    #         (593, 594, 0), (593, 596, 0), (594, 595, 0), (594, 607, 0), (595, 609, 0), (86, 103, 0), (354, 389, 0),
-    #         (354, 424, 0),
-    #         (101, 103, 0), (101, 114, 0), (103, 132, 0), (487, 510, 0), (487, 546, 0), (114, 132, 0), (114, 136, 0),
-    #         (510, 569, 0)], \
-    #         f"found {small_plot.nx_edges}"
-    #
-    #     ami_edges = small_plot.get_or_create_ami_edges()
-    #     print(f"ami_edges {ami_edges}")
-    #     ami_edges = AmiEdge.get_ami_edge_start_end
-
     @unittest.skipUnless(interactive, "ignore plotting in routine tests")
     def test_plot_line(self):
         """straightens lines by Douglas Peucker and plots"""
@@ -563,7 +572,7 @@ def test_plot_line(self):
         ]
         AmiEdge.plot_all_lines(nx_graph, lines, tolerance)
 
-    def test_plot_lines_with_nodes(self):
+    def test_plot_lines_with_nodes(self, interactive=interactive):
         """adds nodes straightens lines by Douglas Peucker and plots"""
         nx_graph = self.nx_graph_arrows1
         tolerance = 2
@@ -582,7 +591,7 @@ def test_plot_lines_with_nodes(self):
 
     def test_prisma(self):
         """extract primitives from partial prisma diagram"""
-        assert Resources.PRISMA.exists()
+        assert Resources.PRISMA_RAW.exists()
         ami_graph = AmiGraph(self.nx_graph_prisma)
         islands = ami_graph.get_or_create_ami_islands()
         assert len(islands) == 382
@@ -593,13 +602,6 @@ def test_prisma(self):
                 big_islands.append(island)
         assert len(big_islands) == 6
 
-    # def test_extract_raw_image(self):
-    #     """extract the raw pixels (not the skeletonm) underlying the extracted lines
-    #     plot boxes
-    #     erode and dilate
-    #     """
-    #     TestAmiGraph.display_erode_dilate(self.arrows1, self.nx_graph_arrows1)
-
     def test_extract_raw_image(self):
         """extract the raw pixels (not the skeletonm) underlying the extracted lines
         plot boxes
@@ -616,7 +618,7 @@ def test_erode_battery(self):
         TestAmiGraph.display_erode_dilate(self.battery1_image, nx_graph)
 
     @unittest.skip("Moved to AmiOCR, NYI")
-    def test_find_bboxes_with_text(self):
+    def test_find_bboxes_with_text(self, interact=interactive):
         """find text boxes and remove those with more than one character
         so the remaining lines can be analyses
         """
@@ -642,10 +644,12 @@ def test_get_nx_edge_lengths_list_for_node(self):
         asserts lengths of edges to node
         :return:
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
         edge_length_by_nx_edge = ami_graph.get_nx_edge_lengths_by_edge_list_for_node(24)
-        assert {'a': 2.000001} == pytest.approx({'a': 2})
-        assert {'a': 2.01} == pytest.approx({'a': 2}, 0.1)
+        assert {"a": 2.000001} == pytest.approx({"a": 2})
+        assert {"a": 2.01} == pytest.approx({"a": 2}, 0.1)
         expected1 = pytest.approx(
             {(24, 21): 30.004166377354995, (24, 22): 9.394147114027968, (24, 23): 9.394147114027968,
              (24, 25): 12.010412149464313}, 0.001)
@@ -657,19 +661,28 @@ def test_battery1_elements(self):
         :return:
         """
         # TODO package commands into AmiGraph
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BATTERY1, interactive=False)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(
+            Resources.BATTERY1_RAW, interactive=False
+        )
         nx_graph = ami_graph.nx_graph
         assert len(nx_graph.nodes) == 647  # multi, iso, ring full
         # assert len(nx_graph.nodes) == 569
 
         connected_components = list(nx.algorithms.components.connected_components(nx_graph))
         # assert nx.algorithms.components.number_connected_components(nx_graph) == 212  #
-        assert nx.algorithms.components.number_connected_components(nx_graph) == 290  # multyi iso ring full
-        connected_components = list(nx.algorithms.components.connected_components(nx_graph))
-        assert type(connected_components) is list, f"type of connected components should be list"
-
-        assert type(connected_components[0]) is set and len(connected_components[0]) == 4, \
-            f"components should be sets and first len == 8"
+        assert (
+            nx.algorithms.components.number_connected_components(nx_graph) == 290
+        )  # multyi iso ring full
+        connected_components = list(
+            nx.algorithms.components.connected_components(nx_graph)
+        )
+        assert (
+            type(connected_components) is list
+        ), f"type of connected components should be list"
+
+        assert (
+            type(connected_components[0]) is set and len(connected_components[0]) == 4
+        ), f"components should be sets and first len == 8"
         assert type(list(connected_components[0])[0]) is int, f"members should be int"
 
         ami_graph = AmiGraph(nx_graph)
@@ -698,7 +711,9 @@ def test_battery1square(self):
         :return:
         """
         # TODO package commands into AmiGraph
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BATTERY1BSQUARE, interactive=False)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(
+            Resources.BATTERY1BSQUARE_RAW, interactive=False
+        )
         nx_graph = ami_graph.nx_graph
         assert type(nx_graph) is nx.MultiGraph
         assert len(nx_graph.nodes) == 3  # multi, iso, ring full  (square has an artificial node)
@@ -742,7 +757,9 @@ def test_battery1square(self):
 
     def test_create_ami_nodes_from_ids(self):
         """wrap node_ids in AmiNodes"""
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH1_ARROWS)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
         node_ids = [0, 1, 2, 3, 4, 5, 6, 7]
         ami_node_list = ami_graph.create_ami_nodes_from_ids(node_ids)
         assert 8 == len(ami_node_list)
@@ -750,7 +767,7 @@ def test_create_ami_nodes_from_ids(self):
 
     # utils ----------------
 
-    def test_primitives(self):
+    def test_primitives(self, interact=interactive):
         """
         analyses a figure with ~15 simple diagrams and returns their analyses
         :return:
@@ -772,14 +789,17 @@ def test_primitives(self):
             "mido",
             "babyo",
             "rect",
-            "inrect"
+            "inrect",
         ]
-        colors = ["green", "blue", "purple", "cyan"]
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.PRIMITIVES, interactive=False)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(
+            Resources.PRIMITIVES_RAW, interactive=interact
+        )
         nx_graph = ami_graph.nx_graph
         assert type(nx_graph) is nx.MultiGraph
-        assert len(nx_graph.nodes) == 42  # multi, iso, ring full  (square has an artificial node)
-        print("\nPrimitives: ", nx_graph)
+        assert (
+            len(nx_graph.nodes) == 42
+        )  # multi, iso, ring full  (square has an artificial node)
+        logger.debug("\nPrimitives: ", nx_graph)
         islands = ami_graph.get_or_create_ami_islands()
         assert len(islands) == 16
 
@@ -787,18 +807,23 @@ def test_primitives(self):
         for i, island in enumerate(islands):
             cyc = None
             # island.get_or_create_bbox()
-            print("\n", "******* island:", i, island_names[i],
-                  island.get_or_create_bbox(),
-                  island.node_ids)
+            logger.debug(
+                "\n",
+                "******* island:",
+                i,
+                island_names[i],
+                island.get_or_create_bbox(),
+                island.node_ids,
+            )
             try:
                 cyc = nx.algorithms.cycles.find_cycle(island.island_nx_graph)
-                print(f"cycles {cyc}")
+                logger.debug(f"cycles {cyc}")
             except nx.exception.NetworkXNoCycle:
                 # only way of trapping acyclic graph
                 pass
 
         # draw image
-        AmiImage.pre_plot_image(Resources.PRIMITIVES, erode_rad=2)
+        AmiImage.pre_plot_image(Resources.PRIMITIVES_RAW, erode_rad=2)
 
         # draw edges by pts
         ami_graph.pre_plot_edges(plt.gca())
@@ -810,7 +835,8 @@ def test_primitives(self):
         if interactive:
             plt.show()
 
-    def assert_degrees(self, ami_graph, degree, result_nodes):
+    @classmethod
+    def assert_degrees(cls, ami_graph, degree, result_nodes):
         """
         tests degree of connectivity of nodes in graph
         uses ami_graph.get_nodes_with_degree
@@ -841,17 +867,25 @@ def test_multigraph_edges(self):
         assert list(nx_graph.edges(2)) == [(2, 0), (2, 0), (2, 0)]
 
         edge_list = list(nx_graph.edges.data())
-        assert edge_list[0] == (0, 1, {'foo': 3})
-        assert edge_list[1] == (0, 2, {'weight': 5})
-        assert edge_list[2] == (0, 2, {'weight': 10})
-        assert edge_list[3] == (0, 2, {'weight': 15})
-        assert list(nx_graph.edges.data('weight', default=1)) == \
-               [(0, 1, 1), (0, 2, 5), (0, 2, 10), (0, 2, 15)]
-        assert list(nx_graph.edges.data(keys=True)) == \
-               [(0, 1, 0, {'foo': 3}), (0, 2, 0, {'weight': 5}), (0, 2, 1, {'weight': 10}), (0, 2, 2, {'weight': 15})]
+        assert edge_list[0] == (0, 1, {"foo": 3})
+        assert edge_list[1] == (0, 2, {"weight": 5})
+        assert edge_list[2] == (0, 2, {"weight": 10})
+        assert edge_list[3] == (0, 2, {"weight": 15})
+        assert list(nx_graph.edges.data("weight", default=1)) == [
+            (0, 1, 1),
+            (0, 2, 5),
+            (0, 2, 10),
+            (0, 2, 15),
+        ]
+        assert list(nx_graph.edges.data(keys=True)) == [
+            (0, 1, 0, {"foo": 3}),
+            (0, 2, 0, {"weight": 5}),
+            (0, 2, 1, {"weight": 10}),
+            (0, 2, 2, {"weight": 15}),
+        ]
         edges = list(nx_graph.edges.data(keys=True))
         assert len(edges) == 4
-        assert edges[0] == (0, 1, 0, {'foo': 3})
+        assert edges[0] == (0, 1, 0, {"foo": 3})
         key = 2
         assert edges[0][key] == 0
         assert edges[1][key] == 0
@@ -887,9 +921,8 @@ def test_whole_image1(self):
             assert len(islands_big) == counts_by_mindim[min_dim]
 
     def test_island_sizes(self):
-        """uses mindim, maxdim, to filter in/out islands. etc.
-        """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
+        """uses mindim, maxdim, to filter in/out islands. etc."""
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5_RAW)
         # all islands
         islands = ami_graph.get_or_create_ami_islands()
         assert len(islands) == 227, f"expected total islands {len(islands)}"
@@ -918,7 +951,7 @@ def test_graph_plots(self):
         """
         # NOTE these tests seem correct but pixel-fragile
 
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5_RAW)
         # second largest island is a boxed plot
         # all islands
         islands = ami_graph.get_or_create_ami_islands(mindim=50, maxmindim=300)
@@ -999,7 +1032,7 @@ def test_graph_plots(self):
 
     def test_enumerate_unique_edges(self):
         """separates 3- connected nodes into separate lines """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5_RAW)
         small_plot_island_0 = ami_graph.get_or_create_ami_islands(mindim=50, maxmindim=300)[0]
         triply_connected_ids = AmiGraph.get_node_ids_from_graph_with_degree(small_plot_island_0.island_nx_graph, 3)
         assert len(triply_connected_ids) == 30, f"found {len(triply_connected_ids)}"
@@ -1010,7 +1043,7 @@ def test_enumerate_unique_edges(self):
     def test_analyze_topology(self):
         """merges short horizontal and verstical lines from sknw
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5_RAW)
         small_plot_island = ami_graph.get_or_create_ami_islands(mindim=50, maxmindim=300)[0]
         node_ids = small_plot_island.node_ids
         ami_edges, multibranches = ami_graph.get_unique_ami_edges_and_multibranches(node_ids)
@@ -1027,7 +1060,7 @@ def test_analyze_topology(self):
     def test_create_straight_edges(self):
         """tests straightness between nodes (horiz and vert)
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5_RAW)
         small_plot_island = ami_graph.get_or_create_ami_islands(mindim=50, maxmindim=300)[0]
         node_ids = small_plot_island.node_ids
         pixel_error = 2
@@ -1043,7 +1076,7 @@ def test_create_straight_edges(self):
     def test_create_line_segments(self):
         """segments the edge into straight-lines (AmiLine) and finds axially aligned corners
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5_RAW)
         small_plot_island = ami_graph.get_or_create_ami_islands(mindim=50, maxmindim=300)[0]
         ami_edges = small_plot_island.get_or_create_ami_edges()
         assert len(ami_edges) == 48, f"found {len(ami_edges)}"
@@ -1059,7 +1092,7 @@ def test_create_line_segments(self):
     def test_filter_line_segments(self):
         """filters segments the edge into straight-lines (AmiLine) and finds axially aligned corners
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5_RAW)
         small_plot_island = ami_graph.get_or_create_ami_islands(mindim=50, maxmindim=300)[0]
         ami_edges = small_plot_island.get_or_create_ami_edges()
 
@@ -1107,7 +1140,7 @@ def test_edge_manager(self):
 
         :return:
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5_RAW)
         small_plot_island = ami_graph.get_or_create_ami_islands(mindim=50, maxmindim=300)[0]
         ami_edges = small_plot_island.get_or_create_ami_edges()
         assert len(ami_edges) == 48, f"found {len(ami_edges)}"
@@ -1128,7 +1161,7 @@ def test_join_horiz_vert_lines(self):
 
         :return:
         """
-        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5)
+        ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.YW5003_5_RAW)
         small_plot_island = ami_graph.get_or_create_ami_islands(mindim=50, maxmindim=300)[0]
         ami_edges = small_plot_island.get_or_create_ami_edges()
 
@@ -1146,7 +1179,7 @@ def test_join_horiz_vert_lines(self):
     # =====================================
 
     @classmethod
-    def display_erode_dilate(cls, image, nx_graph, radius=3, erode=False, dilate=False):
+    def display_erode_dilate(cls, image, nx_graph, radius=3, erode=False, dilate=False, interact=interactive):
         islands = AmiGraph(nx_graph).get_ami_islands_from_nx_graph()
         fig, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=4, figsize=(9, 6))
         ax1.set_title("raw image")
diff --git a/test/test_ami_image.py b/test/test_ami_image.py
index ebf2349..2b95b1b 100644
--- a/test/test_ami_image.py
+++ b/test/test_ami_image.py
@@ -4,14 +4,14 @@
 """
 from pathlib import Path
 
+import matplotlib.pyplot as plt
 import numpy as np
-from skimage import io, morphology
-from skimage import data
+from skimage import data, io, morphology
 from skimage.filters import unsharp_mask
-import matplotlib.pyplot as plt
 
-from ..pyimage.ami_image import AmiImage
-from ..test.resources import Resources
+import context
+from pyamiimage.ami_image import AmiImage
+from resources import Resources
 
 RESOURCE_DIR = Path(Path(__file__).parent, "resources")
 COMPARE_DIR = Path(Path(__file__).parent, "comparison_images")
@@ -26,7 +26,6 @@
 
 
 class TestAmiImage:
-
     def setup_method(self, method):
         """setup any state tied to the execution of the given method in a
         class.  setup_method is invoked for every test method of a class.
@@ -36,8 +35,10 @@ def setup_method(self, method):
         assert RGB_SNIPPET.exists(), f"image should exist {RGB_SNIPPET}"
         self.image = io.imread(RGB_SNIPPET)
 
-        assert COMPARE_DIR.exists(), "Comparison directory does not exist, run generate_compare_files.py"
-    
+        assert (
+            COMPARE_DIR.exists()
+        ), "Comparison directory does not exist, run generate_compare_files.py"
+
     def teardown_method(self, method):
         """teardown any state that was previously setup with a setup_method
         call."""
@@ -50,41 +51,49 @@ def teardown_method(self, method):
     def test_check_image_values(self):
         """check the values of imported image against comparison"""
 
-        TestAmiImage.assert_image_equals_repository_image_file(self.image, "original.png", COMPARE_DIR)
+        TestAmiImage.assert_image_equals_repository_image_file(
+            self.image, "original.png", COMPARE_DIR
+        )
 
     def test_rgb2gray(self):
         """convert raw "gray" image to grayscale"""
         # TODO create pure rgb image
         gray_image = AmiImage.create_grayscale_from_image(self.image)
 
-        TestAmiImage.assert_image_equals_repository_image_file(gray_image, "gray.png", COMPARE_DIR)
+        TestAmiImage.assert_image_equals_repository_image_file(
+            gray_image, "gray.png", COMPARE_DIR
+        )
 
     def test_white_binary(self):
         """tests image to white binary image"""
         white_binary = AmiImage.create_white_binary_from_image(self.image)
 
-        TestAmiImage.assert_image_equals_repository_image_file(white_binary, "white_binary.png", COMPARE_DIR)
+        TestAmiImage.assert_image_equals_repository_image_file(
+            white_binary, "white_binary.png", COMPARE_DIR
+        )
 
     def test_create_inverted_image(self):
         """tests image to inverted image"""
         inverted = AmiImage.create_inverted_image(self.image)
 
-        TestAmiImage.assert_image_equals_repository_image_file(inverted, "inverted.png", COMPARE_DIR)
+        TestAmiImage.assert_image_equals_repository_image_file(
+            inverted, "inverted.png", COMPARE_DIR
+        )
 
     def test_create_white_skeleton_from_image(self):
         """tests image to skeleton image"""
         inverted = AmiImage.create_inverted_image(self.image)
         white_skeleton = AmiImage.create_white_skeleton_from_image(inverted)
 
-        TestAmiImage.assert_image_equals_repository_image_file(white_skeleton, "white_skeleton.png", COMPARE_DIR)
-
+        TestAmiImage.assert_image_equals_repository_image_file(
+            white_skeleton, "white_skeleton.png", COMPARE_DIR
+        )
 
     def test_sharpen_iucr(self):
-        """shows effect of sharpening (interactive display)
-        """
-        image = io.imread(str(Path(Resources.YW5003_5)))
+        """shows effect of sharpening (interactive display)"""
+        image = io.imread(str(Path(Resources.YW5003_5_RAW)))
         assert image is not None
-        print (image.shape)
+        print(image.shape)
         image = AmiImage.create_rgb_from_rgba(image)
         image = AmiImage.create_grayscale_from_image(image)
 
@@ -92,28 +101,29 @@ def test_sharpen_iucr(self):
         result_2 = unsharp_mask(image, radius=5, amount=2)
         result_3 = unsharp_mask(image, radius=20, amount=1)
 
-
-        fig, axes = plt.subplots(nrows=2, ncols=2,
-                                 sharex=True, sharey=True, figsize=(10, 10))
+        fig, axes = plt.subplots(
+            nrows=2, ncols=2, sharex=True, sharey=True, figsize=(10, 10)
+        )
         ax = axes.ravel()
 
         ax[0].imshow(image, cmap=plt.cm.gray)
-        ax[0].set_title('Original image')
+        ax[0].set_title("Original image")
         ax[1].imshow(result_1, cmap=plt.cm.gray)
-        ax[1].set_title('Enhanced image, radius=1, amount=1.0')
+        ax[1].set_title("Enhanced image, radius=1, amount=1.0")
         ax[2].imshow(result_2, cmap=plt.cm.gray)
-        ax[2].set_title('Enhanced image, radius=5, amount=2.0') # best
+        ax[2].set_title("Enhanced image, radius=5, amount=2.0")  # best
         ax[3].imshow(result_3, cmap=plt.cm.gray)
-        ax[3].set_title('Enhanced image, radius=20, amount=1.0')
+        ax[3].set_title("Enhanced image, radius=20, amount=1.0")
         print(f"shape {result_3}")
 
         if interactive:
             plt.show()
 
-
-# ========== helper methods ==============
+    # ========== helper methods ==============
     @classmethod
-    def assert_image_equals_repository_image_file(cls, original_image, expected_filename, repository_dir):
+    def assert_image_equals_repository_image_file(
+        cls, original_image, expected_filename, repository_dir
+    ):
         """
         uses assert_image_equals to compare images
         expected images are in a communal directory
@@ -125,12 +135,16 @@ def assert_image_equals_repository_image_file(cls, original_image, expected_file
         :return: None
         """
         compare_filepath = Path(repository_dir, expected_filename)
-        assert compare_filepath.exists(), f"{expected_filename} does not exist, have you run generate_compare_files.py?"
+        assert (
+            compare_filepath.exists()
+        ), f"{expected_filename} does not exist, have you run generate_compare_files.py?"
         compare_image = io.imread(compare_filepath)
         cls.assert_image_equals(original_image, compare_image, expected_filename)
 
     @classmethod
-    def assert_image_equals(cls, comparable_image, compare_image, expected_filename=None):
+    def assert_image_equals(
+        cls, comparable_image, compare_image, expected_filename=None
+    ):
         """
         convenience method to compare 2 images using np.array_equal
 
@@ -140,5 +154,6 @@ def assert_image_equals(cls, comparable_image, compare_image, expected_filename=
         :return:
         """
         msg = f"{expected_filename} image" if expected_filename is not None else ""
-        assert np.array_equal(comparable_image, compare_image), f"Image does not match {msg}"
-
+        assert np.array_equal(
+            comparable_image, compare_image
+        ), f"Image does not match {msg}"
diff --git a/test/test_ami_ocr.py b/test/test_ami_ocr.py
index e7f1421..c9e689a 100644
--- a/test/test_ami_ocr.py
+++ b/test/test_ami_ocr.py
@@ -1,7 +1,15 @@
 import unittest
 from skimage import io
-from ..pyimage.ami_ocr import TextBox, AmiOCR
-from ..test.resources import Resources # Asserting all images take time
+
+from resources import Resources # Asserting all images take time
+
+import context
+from pyamiimage.bbox import BBox
+from pyamiimage.ami_ocr import TextBox, AmiOCR
+from pyamiimage.ami_image import AmiImage
+
+
+Interactive = False
 
 class TestTextBox:
     def setup_method(self, method):
@@ -25,39 +33,97 @@ def plot_bbox():
 
 class TestAmiOCR:
     def setup_method(self, method):
-        self.biosynth2 = Resources.BIOSYNTH2
+        self.biosynth2 = Resources.BIOSYNTH2_RAW
         self.biosynth2_img = io.imread(self.biosynth2)
         self.biosynth2_ocr = AmiOCR(self.biosynth2)
 
+        self.med_xrd = Resources.MED_XRD_FIG_A_RAW
+        self.med_xrd_img = io.imread(self.med_xrd)
+
     def teardown_method(self, method):
         self.biosynth2 = None
         self.img_ocr = None
 
     def test_words(self):
         words = self.biosynth2_ocr.get_words()
+        # clean unbalanced quotes out of output
+        words = AmiOCR.clean_all(words)
         assert len(words) == 56, f"words are {len(words)}"
         expected_textbox = TextBox("Glycolysis", [[405, 638], [1, 57]])
-        assert words[0] == expected_textbox, f"{expected_textbox} and found: {words[0]}" 
-        assert words[0:5] ==[TextBox("Glycolysis", [[405, 638], [1, 57]]), 
-                             TextBox("Terpene", [[182, 349], [57, 99]]),
-                             TextBox("Biosynthetic", [[140, 390], [111, 145]]), 
+        assert words[0] == expected_textbox, f"{expected_textbox} and found: {words[0]}"
+        # assert words[0:3] == [TextBox("Glycolysis", [[405, 638], [1, 57]]),
+        #                      TextBox("Terpene", [[182, 349], [57, 99]]),
+        #                      TextBox("Biosynthetic", [[140, 390], [111, 145]]),
+        #                      # TextBox("Bethea", [[178, 329], [122, 200]]),
+        #                      # TextBox("Acetyl-Co", [[606, 798], [149, 187]])], \
+        #                      ], f"words and bounds are {words[0:3]}"
+        assert words[0] == expected_textbox, f"{expected_textbox} and found: {words[0]}"
+        print(f"temp {words[0:5]}")
+        assert words[0:5] ==[TextBox("Glycolysis", [[405, 638], [1, 57]]),
+                             TextBox("Terpene", [[182, 349], [53, 103]]),
+                             TextBox("Biosynthetic", [[140, 390], [111, 145]]),
                              TextBox("Bethea", [[178, 329], [122, 200]]),
                              TextBox("Acetyl-Co", [[606, 798], [149, 187]])], f"words and bounds are {words[:5]}"
+        """E       assert [Textbox(Glycolysis, [[405, 638], [1, 57]]),\n Textbox(Terpene, [[182, 349], [53, 103]]),\n Textbox(Biosynthetic, [[140, 390], [111, 145]]),\n Textbox(Bethea, [[178, 329], [122, 200]]),\n Textbox(Acetyl-Co, [[606, 798], [149, 187]])] ==
+                          [Textbox(Glycolysis, [[405, 638], [1, 57]]),\n Textbox(Terpene, [[182, 349], [57, 99]]),\n Textbox(Biosynthetic, [[140, 390], [111, 145]]),\n Textbox(Bethea, [[178, 329], [122, 200]]),\n Textbox(Acetyl-Co, [[606, 798], [149, 187]])]
+"""
 
     def test_phrases(self):
         phrases = self.biosynth2_ocr.get_phrases()
-        assert len(phrases) == 46, f"phrases are {len(phrases)}"
+        assert len(phrases) == 59, f"phrases are {len(phrases)}"
 
     def test_groups(self):
         groups = self.biosynth2_ocr.get_groups()
-        assert len(groups) == 46, f"groups are {len(groups)}"
+        assert len(groups) == 59, f"groups are {len(groups)}"
 
     def test_clean(self):
         pass
 
+    @unittest.skipUnless(Interactive, "interactive" )
     def test_plot_bbox_on_image(self):
         words = self.biosynth2_ocr.get_words()
         biosynth2_img_bboxes = AmiOCR.plot_bboxes_on_image(self.biosynth2_img, words)
         io.imshow(biosynth2_img_bboxes)
         io.show()
+    
+    @unittest.skipUnless(Interactive, "interactive" )
+    def test_bbox_fill(self):
+        """tests filling background in a given bbox in an image"""
+        box = BBox([[82, 389], [28, 386]])
+        test_img = AmiOCR.set_bbox_to_bg(self.med_xrd_img, box)
+        io.imshow(test_img)
+        io.show()
+
+    @unittest.skipUnless(Interactive, "interactive" )
+    def test_extract_labels_from_plot(self):
+        """test that labels are correctly OCRd in a plot"""
+        box = BBox([[82, 389], [28, 386]])
+        labels = AmiOCR.extract_labels_from_plot(self.med_xrd_img, box)
+
+    @unittest.skipUnless(Interactive, "interactive" )
+    def test_plot_pixel_stats(self):
+        """tests that plots the pixel stats in an image"""
+        AmiOCR.plot_image_pixel_stats(self.med_xrd_img, 255, axis=1)
+
+    @unittest.skipUnless(Interactive, "interactive" )
+    def test_img_rotation(self):
+        """tests if an image can be rotated"""
+        med_xrd_img_45 = AmiOCR.image_rotate(self.med_xrd_img, 45)
+        io.imshow(med_xrd_img_45)
+        io.show()
+
+    @unittest.skipUnless(Interactive, "interactive" )
+    def test_rotated_image_pixel_stats(self):
+        """test to check if pixel statistics work on rotated image"""
+        med_xrd_img_45 = AmiOCR.image_rotate(self.med_xrd_img, 45)
+        AmiOCR.plot_image_pixel_stats(med_xrd_img_45, 255, axis=1)
+
+    @unittest.skipUnless(Interactive, "interactive" )
+    def test_shapes_pixel_stats(self):
+        """pixel statistics of common shapes"""
+        shapes1 = Resources.SHAPES_1_RAW
+        shapes1_img = io.imread(shapes1)
+        shapes1_img_bin = AmiImage.create_white_binary_from_image(shapes1_img)
+        AmiOCR.plot_image_pixel_stats(shapes1_img_bin, 255, axis=1)
 
+    
\ No newline at end of file
diff --git a/test/test_ami_plot.py b/test/test_ami_plot.py
index f12616b..95f2278 100644
--- a/test/test_ami_plot.py
+++ b/test/test_ami_plot.py
@@ -1,67 +1,74 @@
 import unittest
 
-from ..pyimage.ami_plot import AmiLineTool
+import context
+from pyamiimage.ami_plot import POLYGON, POLYLINE, AmiLine, AmiLineTool, X, Y
 
 
-@unittest.skip("Not implemented")
 class TestAmiLineTool:
-    """ test AmilineTool"""
+    """test AmilineTool"""
 
     def test_empty_polyline(self):
         line_tool = AmiLineTool()
-        assert line_tool.points == [], f"found {line_tool.points}"
+        assert line_tool.line_points_list == []
+        assert line_tool.polygons == []
+        assert line_tool.mode == POLYLINE
 
-    def test_single_points(self):
-        line_tool = AmiLineTool()
-        line_tool.add_point([1, 2])
-        assert line_tool.points == [[1, 2]], f"found {line_tool}"
+    # def test_single_points(self):
+    #     line_tool = AmiLineTool()
+    #     line_tool.add_point([1, 2])
+    #     assert line_tool.points == [[1, 2]], f"found {line_tool}"
 
-    @unittest.skip("Not implemented")
     def test_single_segment(self):
+        # creates fresh polyline
         line_tool = AmiLineTool()
         line_tool.add_segment([[1, 2], [2, 3]])
-        assert line_tool.points == [[1, 2], [2, 3]]
+        assert line_tool.line_points_list == [
+            [[1, 2], [2, 3]]
+        ], f"found {line_tool.line_points_list}"
 
-    @unittest.skip("Not implemented")
     def test_multiple_segments(self):
-        line_tool = AmiLineTool()
-        line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
-        assert line_tool.points == [[1, 2], [2, 3], [5, 7]]
+        line_tool = AmiLineTool(xy_flag=X)
+        segments = [[[1, 2], [2, 3]], [[2, 3], [5, 7]]]
+        line_tool.add_segments(segments)
+        assert line_tool.line_points_list == [
+            [[1, 2], [2, 3], [5, 7]]
+        ], f"found {line_tool.line_points_list}"
 
-    @unittest.skip("Not implemented")
-    def test_fail_multiple_segments(self):
-        line_tool = AmiLineTool()
-        try:
-            line_tool.add_segments([[[1, 2], [2, 3]], [[5, 8], [5, 7]]])
-            raise ValueError(f"should raise ValueError ppoints do not overlap")
-        except ValueError:
-            pass
+    def test_multiple_non_overlapping_segments(self):
+        line_tool = AmiLineTool(xy_flag=X)
+        # already sorted
+        line_tool.add_segments([[[1, 2], [2, 3]], [[5, 8], [5, 7]]])
+        assert line_tool.line_points_list == [[[1, 2], [2, 3]], [[5, 8], [5, 7]]]
+        # not sorted
+        line_tool = AmiLineTool(xy_flag=X)
+        line_tool.add_segments([[[5, 8], [5, 7]], [[1, 2], [2, 3]]])
+        assert line_tool.line_points_list == [[[5, 8], [5, 7]], [[1, 2], [2, 3]]]
 
-    @unittest.skip("Not implemented")
-    def test_add_right_point(self):
-        line_tool = AmiLineTool()
-        line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
-        line_tool.add_point([9, 5])
-        assert line_tool.points == [[1, 2], [2, 3], [5, 7], [9, 5]]
+    # @unittest.skip("Cannot add single points")
+    # def test_add_right_point(self):
+    #     line_tool = AmiLineTool(xy_flag=X)
+    #     line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
+    #     line_tool.add_point([9, 5])
+    #     assert line_tool.points == [[1, 2], [2, 3], [5, 7], [9, 5]]
 
-    @unittest.skip("prepending to a simple list is not allowed")
-    def test_add_left_point(self):
-        line_tool = AmiLineTool()
-        line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
-        assert line_tool.points == [[1, 2], [2, 3], [5, 7]]
-        line_tool.insert_point(0, [12, 3])
-        assert line_tool.points == [[12, 3], [1, 2], [2, 3], [5, 7]]
+    # @unittest.skip("cannot add single points")
+    # def test_add_left_point(self):
+    #
+    #     line_tool = AmiLineTool(xy_flag=X)
+    #     line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
+    #     assert line_tool.points == [[1, 2], [2, 3], [5, 7]]
+    #     line_tool.insert_point(0, [12, 3])
+    #     assert line_tool.points == [[12, 3], [1, 2], [2, 3], [5, 7]]
 
-    @unittest.skip("Not implemented")
     def test_add_right_segment(self):
-        line_tool = AmiLineTool()
+        line_tool = AmiLineTool(xy_flag=X)
         line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
+        assert line_tool.line_points_list == [[[1, 2], [2, 3], [5, 7]]]
         line_tool.add_segment([[5, 7], [9, 5]])
-        assert line_tool.points == [[1, 2], [2, 3], [5, 7], [9, 5]]
+        assert line_tool.line_points_list == [[[1, 2], [2, 3], [5, 7], [9, 5]]]
 
-    @unittest.skip("Not implemented")
     def test_fail_right_segment(self):
-        line_tool = AmiLineTool()
+        line_tool = AmiLineTool(xy_flag=X)
         line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
         try:
             line_tool.add_segment([[15, 7], [9, 5]])
@@ -69,23 +76,88 @@ def test_fail_right_segment(self):
         except ValueError:
             pass
 
-    @unittest.skip("Not implemented")
-    def test_add_left_segment(self):
+    # @unittest.skip("insert not defined")
+    # def test_add_left_segment(self):
+    #     line_tool = AmiLineTool(xy_flag=X)
+    #     line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
+    #     assert line_tool.polylines == [[[1, 2], [2, 3], [5, 7]]]
+    #     line_tool.insert_segment(0, [[25, 37], [1, 2]])
+    #     assert line_tool.polylines == [[[25, 37], [1, 2], [2, 3], [5, 7]]]
+    #
+    # @unittest.skip("add points not well defined")
+    # def test_add_points_to_empty(self):
+    #     line_tool = AmiLineTool(xy_flag=X)
+    #     line_tool.add_points([[1, 2], [2, 3], [5, 7]])
+    #     assert line_tool.polylines == [[[1, 2], [2, 3], [5, 7]]]
+    #
+    # @unittest.skip("add points not well defined")
+    # def test_add_points_to_existing(self):
+    #     line_tool = AmiLineTool(xy_flag=X)
+    #     line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
+    #     assert line_tool.points == [[1, 2], [2, 3], [5, 7]]
+    #     line_tool.add_points([[12, 3], [7, 2]])
+    #     assert line_tool.points == [[1, 2], [2, 3], [5, 7], [12, 3], [7, 2]]
+
+    def test_make_unclosed_box_and_close(self):
+        line_tool = AmiLineTool(xy_flag=X, mode=POLYGON)
+        line_tool.add_segments(
+            [[[10, 20], [10, 30]], [[10, 30], [40, 30]], [[40, 30], [40, 20]]]
+        )
+        assert line_tool.line_points_list == [[[10, 20], [10, 30], [40, 30], [40, 20]]]
+        # close box
+        line_tool.add_segments([[[10, 20], [40, 20]]])
+        assert line_tool.polygons == [[[10, 20], [10, 30], [40, 30], [40, 20]]]
+
+    def test_make_closed_box(self):
+        line_tool = AmiLineTool(xy_flag=X, mode=POLYGON)
+        line_tool.add_merge_polyline_to_poly_list(
+            [[10, 20], [10, 30], [40, 30], [40, 20]]
+        )
+        assert line_tool.line_points_list == [[[10, 20], [10, 30], [40, 30], [40, 20]]]
+        line_tool.add_segments([[[10, 20], [40, 20]]])
+        assert line_tool.polygons == [[[10, 20], [10, 30], [40, 30], [40, 20]]]
+
+    def test_joining_lines_head_tail(self):
+        """head-tail"""
         line_tool = AmiLineTool()
-        line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
-        assert line_tool.points == [[1, 2], [2, 3], [5, 7]]
-        line_tool.insert_segment(0, [[25, 37], [1, 2]])
-        assert line_tool.points == [[25, 37], [1, 2], [2, 3], [5, 7]]
+        polyline0 = [[0, 10], [0, 20], [10, 20]]
+        line_tool.add_merge_polyline_to_poly_list(polyline0)
+        polyline1 = [[10, 20], [10, 30], [20, 30]]
+        line_tool.add_merge_polyline_to_poly_list(polyline1)
+        assert line_tool.line_points_list == [
+            [[0, 10], [0, 20], [10, 20], [10, 30], [20, 30]]
+        ]
+
+    def test_joining_lines_head_head(self):
+        """head-head"""
 
-    def test_add_points_to_empty(self):
         line_tool = AmiLineTool()
-        line_tool.add_points([[1, 2], [2, 3], [5, 7]])
-        assert line_tool.points == [[1, 2], [2, 3], [5, 7]]
+        polyline0 = [[0, 10], [0, 20], [10, 20]]
+        line_tool.add_merge_polyline_to_poly_list(polyline0)
+        polyline1 = [[20, 30], [10, 30], [10, 20]]
+        line_tool.add_merge_polyline_to_poly_list(polyline1)
+        assert line_tool.line_points_list == [
+            [[0, 10], [0, 20], [10, 20], [10, 30], [20, 30]]
+        ]
 
-    @unittest.skip("Not implemented")
-    def test_add_points_to_existing(self):
+    def test_joining_lines_tail_tail(self):
+        """tail-tail"""
         line_tool = AmiLineTool()
-        line_tool.add_segments([[[1, 2], [2, 3]], [[2, 3], [5, 7]]])
-        assert line_tool.points == [[1, 2], [2, 3], [5, 7]]
-        line_tool.add_points([[12, 3], [7, 2]])
-        assert line_tool.points == [[1, 2], [2, 3], [5, 7], [12, 3], [7, 2]]
+        polyline0 = [[10, 20], [0, 20], [0, 10]]
+        line_tool.add_merge_polyline_to_poly_list(polyline0)
+        polyline1 = [[10, 20], [10, 30], [20, 30]]
+        line_tool.add_merge_polyline_to_poly_list(polyline1)
+        assert line_tool.line_points_list == [
+            [[20, 30], [10, 30], [10, 20], [0, 20], [0, 10]]
+        ]
+
+    def test_joining_lines_tail_head(self):
+        """tail-head"""
+        line_tool = AmiLineTool()
+        polyline0 = [[10, 20], [0, 20], [0, 10]]
+        line_tool.add_merge_polyline_to_poly_list(polyline0)
+        polyline1 = [[20, 30], [10, 30], [10, 20]]
+        line_tool.add_merge_polyline_to_poly_list(polyline1)
+        assert line_tool.line_points_list == [
+            [[20, 30], [10, 30], [10, 20], [0, 20], [0, 10]]
+        ]
diff --git a/test/test_ami_skeleton.py b/test/test_ami_skeleton.py
index 61b5ff0..4952af4 100644
--- a/test/test_ami_skeleton.py
+++ b/test/test_ami_skeleton.py
@@ -1,18 +1,23 @@
 """Integration of image processing, binarization, skeletonization and netwprk analysis"""
+import unittest
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+import networkx as nx
+import numpy as np
 from skan.pre import threshold
+
 # library
 from skimage import io
-import matplotlib.pyplot as plt
-import numpy as np
-import networkx as nx
-from pathlib import Path
-import unittest
+
+import context
+from pyamiimage.ami_graph_all import AmiGraph
+from pyamiimage.ami_image import AmiImage
+from pyamiimage.ami_skeleton import AmiSkeleton
+from pyamiimage.ami_util import AmiUtil
+
 # local
-from ..test.resources import Resources
-from ..pyimage.ami_graph_all import AmiGraph
-from ..pyimage.ami_skeleton import AmiSkeleton
-from ..pyimage.ami_image import AmiImage
-from ..pyimage.ami_util import AmiUtil
+from resources import Resources
 
 
 class TestAmiSkeleton:
@@ -49,16 +54,20 @@ class TestAmiSkeleton:
 
     def setup_method(self):
         self.arrows_skeleton = TestAmiSkeleton.create_biosynth_arrows_skeleton()
-        self.arrows1_image = io.imread(Resources.BIOSYNTH1_ARROWS)
-        self.arrows1_skeleton = AmiImage.invert_binarize_skeletonize(io.imread(Resources.BIOSYNTH1_ARROWS))
-        self.arrows1_graph = AmiGraph.create_nx_graph_from_skeleton(self.arrows1_skeleton)
+        self.arrows1_image = io.imread(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
+        self.arrows1_skeleton = AmiImage.invert_binarize_skeletonize(
+            io.imread(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
+        )
+        self.arrows1_graph = AmiGraph.create_nx_graph_from_skeleton(
+            self.arrows1_skeleton
+        )
         # self.arrows1_graph = AmiSkeleton().create_nx_graph_via_skeleton_sknw_NX_GRAPH(Resources.BIOSYNTH1_ARROWS)
 
-
     @classmethod
-
     def create_biosynth_arrows_skeleton(cls):
-        skeleton_image = TestAmiSkeleton.create_skeleton_from_file(Resources.BIOSYNTH1_ARROWS)
+        skeleton_image = TestAmiSkeleton.create_skeleton_from_file(
+            Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
+        )
         return skeleton_image
         # @unittest.skipIf(skip_OK, "already runs")
 
@@ -81,7 +90,7 @@ def test_example_basics_biosynth1_no_text(self):
         cmap = "Greens"
         cmap = self.cmap
 
-        file = Resources.BIOSYNTH1_ARROWS
+        file = Resources.BIOSYNTH1_CROPPED_ARROWS_RAW
         assert file.exists()
         image = io.imread(file)
         # this is a gray image??
@@ -95,8 +104,6 @@ def test_example_basics_biosynth1_no_text(self):
         assert ndark == 4285
         nlight = np.sum(image > 127)
         assert nlight == 471995
-        print(f"\nnpix {npix}, nwhite {nwhite}, nblack {nblack}  nother {npix - nwhite - nblack}, ndark {ndark}, "
-              f"nlight {nlight}")
         fig, ax = plt.subplots()
         ax.set_title("greyscale")
         fig.set_title = "FIGURE"
@@ -147,7 +154,7 @@ def test_skeletonize_biosynth1_no_text(self):
             plt.show()
 
     def test_skeleton_to_graph_arrows1_WORKS(self):
-        """creates nodes and edges for already clipped """
+        """creates nodes and edges for already clipped"""
         # ami_skel = AmiSkeleton()
         #
         # skeleton_array = AmiImage.create_white_skeleton_from_file(Resources.BIOSYNTH1_ARROWS)
@@ -156,18 +163,70 @@ def test_skeleton_to_graph_arrows1_WORKS(self):
         # # build graph from skeleton
         # ami_skel.nx_graph = AmiGraph.create_nx_graph_from_skeleton(skeleton_array)
 
-
-
         AmiUtil.check_type_and_existence(self.arrows1_graph, nx.MultiGraph)
-        print(f" nx {self.arrows1_graph}, {self.arrows1_graph.nodes} {self.arrows1_graph.edges}")
-        AmiUtil.check_type_and_existence(self.arrows1_graph.nodes, nx.classes.reportviews.NodeView)
-        assert list(self.arrows1_graph.nodes) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
-                                                 19, 20, 21, 22, 23, 24, 25, 26]
-        AmiUtil.check_type_and_existence(self.arrows1_graph.edges, nx.classes.reportviews.MultiEdgeView)
-        assert list(self.arrows1_graph.edges) == [(0, 2, 0), (1, 4, 0), (2, 4, 0), (2, 3, 0), (2, 7, 0), (4, 5, 0),
-                                                     (4, 6, 0), (8, 19, 0), (9, 19, 0), (10, 12, 0), (11, 13, 0), (12, 13, 0),
-                                                     (12, 18, 0), (13, 14,  0), (13, 15, 0), (16, 18, 0), (17, 18, 0), (18, 20, 0),
-                                                     (19, 26, 0), (21, 24, 0), (22, 24, 0), (23, 24, 0), (24, 25, 0)]
+        print(
+            f" nx {self.arrows1_graph}, {self.arrows1_graph.nodes} {self.arrows1_graph.edges}"
+        )
+        AmiUtil.check_type_and_existence(
+            self.arrows1_graph.nodes, nx.classes.reportviews.NodeView
+        )
+        assert list(self.arrows1_graph.nodes) == [
+            0,
+            1,
+            2,
+            3,
+            4,
+            5,
+            6,
+            7,
+            8,
+            9,
+            10,
+            11,
+            12,
+            13,
+            14,
+            15,
+            16,
+            17,
+            18,
+            19,
+            20,
+            21,
+            22,
+            23,
+            24,
+            25,
+            26,
+        ]
+        AmiUtil.check_type_and_existence(
+            self.arrows1_graph.edges, nx.classes.reportviews.MultiEdgeView
+        )
+        assert list(self.arrows1_graph.edges) == [
+            (0, 2, 0),
+            (1, 4, 0),
+            (2, 4, 0),
+            (2, 3, 0),
+            (2, 7, 0),
+            (4, 5, 0),
+            (4, 6, 0),
+            (8, 19, 0),
+            (9, 19, 0),
+            (10, 12, 0),
+            (11, 13, 0),
+            (12, 13, 0),
+            (12, 18, 0),
+            (13, 14, 0),
+            (13, 15, 0),
+            (16, 18, 0),
+            (17, 18, 0),
+            (18, 20, 0),
+            (19, 26, 0),
+            (21, 24, 0),
+            (22, 24, 0),
+            (23, 24, 0),
+            (24, 25, 0),
+        ]
 
         if self.plot_plot:
             AmiGraph.plot_nx_graph_NX(self.arrows1_graph)
@@ -176,24 +235,31 @@ def test_skeleton_to_graph_components_with_nodes(self):
         # skeleton_array = AmiImage.create_white_skeleton_from_file(Resources.BIOSYNTH1_ARROWS)
         # Util.check_type_and_existence(skeleton_array, np.ndarray)
         # nx_graph = AmiSkeleton().create_nx_graph_via_skeleton_sknw_NX_GRAPH(Resources.BIOSYNTH1_ARROWS)
-        assert nx.algorithms.components.number_connected_components(self.arrows1_graph) == 4
-        connected_components = list(nx.algorithms.components.connected_components(self.arrows1_graph))
-        assert connected_components == [{0, 1, 2, 3, 4, 5, 6, 7},
-                                        {8, 9, 26, 19},
-                                        {10, 11, 12, 13, 14, 15, 16, 17, 18, 20},
-                                        {21, 22, 23, 24, 25}]
+        assert (
+            nx.algorithms.components.number_connected_components(self.arrows1_graph)
+            == 4
+        )
+        connected_components = list(
+            nx.algorithms.components.connected_components(self.arrows1_graph)
+        )
+        assert connected_components == [
+            {0, 1, 2, 3, 4, 5, 6, 7},
+            {8, 9, 26, 19},
+            {10, 11, 12, 13, 14, 15, 16, 17, 18, 20},
+            {21, 22, 23, 24, 25},
+        ]
         assert connected_components[0] == {0, 1, 2, 3, 4, 5, 6, 7}
         assert connected_components[1] == {8, 9, 26, 19}
 
     def test_remove_pixels_in_bounding_box_arrows1(self):
-        image = io.imread(Resources.BIOSYNTH1_ARROWS)
+        image = io.imread(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
         bbox = ((82, 102), (661, 863))
         image = AmiGraph.set_bbox_pixels_to_color(bbox, image)
         fig, ax = plt.subplots()
-        ax.imshow(image, cmap='gray')
+        ax.imshow(image, cmap="gray")
         return
 
-# the only use so far of AmiGraph
+    # the only use so far of AmiGraph
 
     def test_skeletonize(self):
         skeleton_image = self.binarize_and_skeletonize_arrows()
@@ -205,28 +271,28 @@ def test_skeletonize(self):
         assert type(skeleton_image[0][0]) is np.uint16
         assert skeleton_image[0][0] == 0
 
-# Utils
+    # Utils
 
     @classmethod
     def binarize_and_skeletonize_arrows(cls):
-        test_resources_dir = Path(Path(__file__).parent.parent, "test/resources")
-        biosynth_path_image = Path(test_resources_dir, "biosynth_path_1_cropped_text_removed.png")
-        grayscale = AmiImage.create_grayscale_from_file(biosynth_path_image)
+        grayscale = AmiImage.create_grayscale_from_file(Resources.BIOSYNTH1_CROPPED_ARROWS_RAW)
         skeleton = AmiImage.create_white_skeleton_from_image(grayscale)
         return skeleton
 
     # obsolete?
     def set_bbox_to_color(self, bbox, dd, image):
-        margined_bbox = ((bbox[0][0] - dd, bbox[0][1] + dd), (bbox[1][0] - dd, bbox[1][1] + dd))
+        margined_bbox = (
+            (bbox[0][0] - dd, bbox[0][1] + dd),
+            (bbox[1][0] - dd, bbox[1][1] + dd),
+        )
         AmiGraph.set_bbox_pixels_to_color(margined_bbox, image, color=160)
 
-
-
     @classmethod
     def create_skeleton_from_file(cls, file):
         assert file.exists()
         gray_image = AmiImage.create_grayscale_from_file(file)
         skeleton_image = AmiImage.invert_binarize_skeletonize(gray_image)
-        assert type(skeleton_image) is np.ndarray, f"skeleton type shoukd be np.ndarray, is {type(skeleton_image)}"
+        assert (
+            type(skeleton_image) is np.ndarray
+        ), f"skeleton type shoukd be np.ndarray, is {type(skeleton_image)}"
         return skeleton_image
-
diff --git a/test/test_ami_util.py b/test/test_ami_util.py
index 0e85ac6..f6dd5ee 100644
--- a/test/test_ami_util.py
+++ b/test/test_ami_util.py
@@ -1,6 +1,8 @@
 import pytest
 
-from ..pyimage.ami_util import Vector2
+import context
+from pyamiimage.ami_util import Vector2
+
 
 class TestAmiUtil:
     def test_angle(self):
diff --git a/test/test_arrow.py b/test/test_arrow.py
index df5a84a..91bee21 100644
--- a/test/test_arrow.py
+++ b/test/test_arrow.py
@@ -6,17 +6,17 @@
 import pytest
 from lxml import etree
 
-from ..pyimage.ami_arrow import AmiArrow, AmiNetwork
-from ..pyimage.ami_graph_all import AmiGraph, AmiIsland
-from ..pyimage.svg import SVGSVG, SVGArrow, SVGG, SVGRect, ns_xpath, SVG_NS
-from ..pyimage.bbox import BBox
-from ..test.resources import Resources
+import context
+from pyamiimage.ami_arrow import AmiArrow, AmiNetwork
+from pyamiimage.ami_graph_all import AmiGraph, AmiIsland
+from pyamiimage.bbox import BBox
+from pyamiimage.svg import SVG_NS, SVGG, SVGSVG, SVGArrow, SVGRect, ns_xpath
+from resources import Resources
 
 logger = logging.getLogger(__name__)
 
 
 class TestArrow:
-
     def setup_class(self):
         """
         resources are created once only in self.resources.create_ami_graph_objects()
@@ -44,16 +44,25 @@ def setup_method(self, method):
         # self.biosynth3_ami_graph = AmiGraph.create_ami_graph_from_arbitrary_image_file(Resources.BIOSYNTH3)
         self.biosynth1_ami_graph = self.resources.biosynth1_ami_graph
         self.biosynth3_ami_graph = self.resources.biosynth3_dto.ami_graph
-        self.biosynth6_compounds_ami_graph = self.resources.biosynth6_compounds_dto.ami_graph
+        self.biosynth6_compounds_ami_graph = (
+            self.resources.biosynth6_compounds_dto.ami_graph
+        )
 
-    def test_extract_single_arrow(self):
         ami_graph = self.one_head_island.ami_graph
-        assert len(self.one_head_island.node_ids) == 5, \
-            f"single arrow should have 5 nodes, found {len(self.one_head_island.node_ids)}"
+        assert (
+            len(self.one_head_island.node_ids) == 5
+        ), f"single arrow should have 5 nodes, found {len(self.one_head_island.node_ids)}"
         list1 = AmiGraph.get_node_ids_from_graph_with_degree(ami_graph.nx_graph, 1)
         assert len(list1) == 20
-        list2 = AmiGraph.get_node_ids_from_graph_with_degree(self.one_head_island.island_nx_graph, 1)
-        assert list2 == [21, 22, 23, 25], f"{__name__} ligands found {list2} expected {[21, 22, 23, 25]}"
+        list2 = AmiGraph.get_node_ids_from_graph_with_degree(
+            self.one_head_island.island_nx_graph, 1
+        )
+        assert list2 == [
+            21,
+            22,
+            23,
+            25,
+        ], f"{__name__} ligands found {list2} expected {[21, 22, 23, 25]}"
         longest_edge = ami_graph.find_longest_edge(24)
         assert longest_edge[0] == (24, 21)
         assert longest_edge[1] == pytest.approx(30.0)
@@ -62,8 +71,9 @@ def test_extract_single_arrow(self):
             print(f"{node0} - {central} - {idx} = {other_dict[idx]}")
 
     def test_double_arrow(self):
-        assert len(self.double_arrow_island.node_ids) == 8, \
-            f"double arrow should have 8 nodes, found {len(self.double_arrow_island.node_ids)}"
+        assert (
+            len(self.double_arrow_island.node_ids) == 8
+        ), f"double arrow should have 8 nodes, found {len(self.double_arrow_island.node_ids)}"
         nodes4 = self.double_arrow_island.get_node_ids_of_degree(4)
         assert nodes4 == [2, 4], f"nodes or degree 4 should be {[2, 4]}"
         assert self.double_arrow_island.get_node_ids_of_degree(3) == []
@@ -74,20 +84,31 @@ def test_branched_two_heads(self):
         one-tailed arrow that bifurcates into 2 heads
         :return:
         """
-        TestArrow.assert_arrows(self.branched_two_heads_island,
-                                {1: [10, 11, 14, 15, 16, 17, 20], 2: [], 3: [12], 4: [13, 18]})
+        TestArrow.assert_arrows(
+            self.branched_two_heads_island,
+            {1: [10, 11, 14, 15, 16, 17, 20], 2: [], 3: [12], 4: [13, 18]},
+        )
 
     def test_no_heads(self):
-        assert len(self.no_heads.node_ids) == 4, \
-            f"no heads should have 4 nodes, found {len(self.no_heads.node_ids)}"
+        assert (
+            len(self.no_heads.node_ids) == 4
+        ), f"no heads should have 4 nodes, found {len(self.no_heads.node_ids)}"
         TestArrow.assert_arrows(self.no_heads, {1: [8, 9, 26], 2: [], 3: [19], 4: []})
 
     def test_get_edges_and_lengths(self):
         node_id = 24
         nx_edges = self.arrows1_ami_graph.get_nx_edge_list_for_node(node_id)
-        assert [(24, 21, 0), (24, 22, 0), (24, 23, 0), (24, 25, 0)] == nx_edges, \
+        assert [
+            (24, 21, 0),
+            (24, 22, 0),
+            (24, 23, 0),
+            (24, 25, 0),
+        ] == nx_edges, (
             "edges should be [(24, 21), (24, 22), (24, 23), (24, 25)], found {nx_edges}"
-        edge_length_dict = self.arrows1_ami_graph.get_nx_edge_lengths_by_edge_list_for_node(node_id)
+        )
+        edge_length_dict = (
+            self.arrows1_ami_graph.get_nx_edge_lengths_by_edge_list_for_node(node_id)
+        )
         edge_lengths = [v for v in edge_length_dict.values()]
         assert pytest.approx(edge_lengths, rel=0.001) == [30.00, 8.944, 9.848, 12.041]
 
@@ -101,20 +122,27 @@ def test_get_interedge_angles(self):
             self.arrows1_ami_graph.pre_plot_nodes(plot_ids=True)
             plt.show()
 
-        assert [(24, 21, 0), (24, 22, 0), (24, 23, 0), (24, 25, 0)] == nx_edges, \
-            "edges should be [(24, 21, 0), (24, 22, 0), (24, 23, 0), (24, 25, 0)], found {nx_edges}"
+        assert [
+            (24, 21, 0),
+            (24, 22, 0),
+            (24, 23, 0),
+            (24, 25, 0),
+        ] == nx_edges, "edges should be [(24, 21, 0), (24, 22, 0), (24, 23, 0), (24, 25, 0)], found {nx_edges}"
         angles = []
 
         for edge0 in nx_edges:
             for edge1 in nx_edges:
                 # only do upper triangle
                 if (edge0 is not edge1) and edge0[1] < edge1[1]:
-                    angle = self.arrows1_ami_graph.get_interedge_tuple_angle(edge0, edge1)
+                    angle = self.arrows1_ami_graph.get_interedge_tuple_angle(
+                        edge0, edge1
+                    )
                     angles.append(angle)
         expected = [-1.107, 1.152, 3.058, 2.259, -2.117, 1.906]
 
-        assert expected == pytest.approx(angles, 0.001), \
-            f"expected {expected} found {pytest.approx(angles, 0.001)}"
+        assert expected == pytest.approx(
+            angles, 0.001
+        ), f"expected {expected} found {pytest.approx(angles, 0.001)}"
 
     def test_whole_image_biosynth3(self):
         assert self.biosynth3_ami_graph is not None
@@ -168,9 +196,14 @@ def test_biosynth1_arrows(self):
 
         ami_graph = self.biosynth1_ami_graph
 
-        TestArrow.create_and_test_arrows(ami_graph, max_dim, big_island_count=big_island_count,
-                                         expected_arrows=expected_arrows,
-                                         output_temp=output_temp, total_islands=total_islands)
+        TestArrow.create_and_test_arrows(
+            ami_graph,
+            max_dim,
+            big_island_count=big_island_count,
+            expected_arrows=expected_arrows,
+            output_temp=output_temp,
+            total_islands=total_islands,
+        )
 
     def test_biosynth3_arrows(self):
         """
@@ -178,7 +211,9 @@ def test_biosynth3_arrows(self):
         full defaults except output
         :return:
         """
-        TestArrow.create_and_test_arrows(self.biosynth3_ami_graph, 40, output_temp="biosynth3_arrows.svg")
+        TestArrow.create_and_test_arrows(
+            self.biosynth3_ami_graph, 40, output_temp="biosynth3_arrows.svg"
+        )
 
     def test_biosynth6_compounds_arrows(self):
         # TODO get interedge angles
@@ -211,9 +246,14 @@ def test_biosynth6_compounds_arrows(self):
             str(None),
             "tail 1594 - head 1702 > point 1703 barbs [1700, 1701]",
         ]
-        TestArrow.create_and_test_arrows(self.biosynth6_compounds_ami_graph, max_dim, big_island_count=big_island_count,
-                                         expected_arrows=None,
-                                         output_temp="biosynth6_compounds_arrows.svg", total_islands=total_islands)
+        TestArrow.create_and_test_arrows(
+            self.biosynth6_compounds_ami_graph,
+            max_dim,
+            big_island_count=big_island_count,
+            expected_arrows=None,
+            output_temp="biosynth6_compounds_arrows.svg",
+            total_islands=total_islands,
+        )
 
     # @unittest.skip("Not yet implemented")
     def test_several_files(self):
@@ -224,17 +264,24 @@ def test_several_files(self):
         NYI
         """
         image_dict = {}
-        image_dict["biosynth3"] = {'input': None, "ami_graph": self.biosynth3_ami_graph,
-                                   "temp_output": "biosynth3_arrows.svg"}
-        print(image_dict.keys())
+        image_dict["biosynth3"] = {
+            "input": None,
+            "ami_graph": self.biosynth3_ami_graph,
+            "temp_output": "biosynth3_arrows.svg",
+        }
+        logger.debug(image_dict.keys())
         for key in image_dict.keys():
             param_dict = image_dict[key]
-            print(param_dict)
-            TestArrow.create_and_test_arrows(param_dict["ami_graph"], 40, output_temp=param_dict["temp_output"])
+            logger.debug(param_dict)
+            TestArrow.create_and_test_arrows(
+                param_dict["ami_graph"], 40, output_temp=param_dict["temp_output"]
+            )
 
     def test_arrows_and_text_biosynth6(self):
         """simplest reaction pathway of 8 steps"""
-        self.biosynth6_compounds_ami_graph = self.resources.biosynth6_compounds_dto.ami_graph
+        self.biosynth6_compounds_ami_graph = (
+            self.resources.biosynth6_compounds_dto.ami_graph
+        )
         image = self.resources.biosynth6_compounds_dto.image
 
     def test_validate_arrows_text_biosynth1(self):
@@ -244,7 +291,7 @@ def test_validate_arrows_text_biosynth1(self):
         :return:
         """
         element = etree.parse(str(self.resources.BIOSYNTH1_ARROWS_TEXT_SVG))
-        print("FILE", self.resources.BIOSYNTH1_ARROWS_TEXT_SVG)
+        logger.debug("FILE", self.resources.BIOSYNTH1_ARROWS_TEXT_SVG)
         assert element is not None, f"{self.resources.BIOSYNTH1_ARROWS_TEXT_SVG}"
         gs = ns_xpath(element, f"{{{SVG_NS}}}g")
         assert len(gs) == 2, f"2 svg:g children (a and t)  expected"
@@ -271,8 +318,8 @@ def test_validate_arrows_text_biosynth1(self):
         </svg:g>        
         """
         # rects
-        assert arrows[0].get("role") == 'arrow', "role should be arrow"
-        assert arrows[0].get("orient") == 'up', "orient should be up"
+        assert arrows[0].get("role") == "arrow", "role should be arrow"
+        assert arrows[0].get("orient") == "up", "orient should be up"
         rect = ns_xpath(arrows[0], f"./{{{SVG_NS}}}rect[@position='core']")
         assert rect is not None, f"only one core expected"
         assert rect.get("x") == "220", f"x coord of core"
@@ -309,37 +356,6 @@ def test_validate_arrows_text_biosynth1(self):
         assert type(text0_text0) is etree._Element, f"element {text0_text0}"
         assert text0_text0.get("y") == "385", f"y"
 
-    @unittest.skip("obsolete")
-    def test_analyze_front_arrows_text_biosynth1(self):
-        """
-        analyze prepared pathway with points of up/down/right/left arrows and multiple texts
-        :return:
-        """
-        svgsvg = etree.parse(str(self.resources.BIOSYNTH1_ARROWS_TEXT_SVG))
-        position = "front"
-        self.overlap_arrows_and_text(position, svgsvg)
-        return
-
-        front_arrows = ns_xpath(svgsvg,
-                                f"{{{SVG_NS}}}g[@role='arrows']/{{{SVG_NS}}}g[@role='arrow']/{{{SVG_NS}}}rect[@position='{position}']")
-        assert len(front_arrows) == 10, f"arrows"
-        for front_arrow_elem in front_arrows:
-            front_arrow_bbox = self.get_bbox(front_arrow_elem)
-            print("bbox", front_arrow_bbox)
-        texts = ns_xpath(svgsvg, f"{{{SVG_NS}}}g[@role='texts']/{{{SVG_NS}}}g[@role='text']")
-        assert len(texts) == 28, f"texts"
-        for txt in texts:
-            text_bbox_elem = ns_xpath(txt, f"{{{SVG_NS}}}rect[@role='bbox']")[0]
-            text_bbox = self.get_bbox(text_bbox_elem)
-            text_val = ns_xpath(txt, f"{{{SVG_NS}}}text")[0].text
-            for front_arrow_elem in front_arrows:
-                front_arrow_bbox = self.get_bbox(front_arrow_elem)
-                overlap = text_bbox.intersect(front_arrow_bbox)
-                if overlap.is_valid():
-                    print("front arrow", front_arrow_bbox)
-                    print("textbox", text_bbox, text_val)
-                    print("overlap", overlap)
-
     def test_analyze_arrows_text_biosynth1(self):
         """
         analyze prepared pathway with tails of up/down/right/left arrows and multiple texts
@@ -350,34 +366,13 @@ def test_analyze_arrows_text_biosynth1(self):
         ami_network.overlap_arrows_and_text()
         ami_network.write_graph(Path(Resources.TEMP_DIR, "biosynth1_network.gpml"))
 
-    # @unittest.skip("under development")
-    # def test_raw_arrows_to_bboxes(self):
-    #     """
-    #     raw arrows in SVG resulting from pixel analysis
-    #     processed to add bounding boxes
-    #     :return:
-    #     """
-    #     element = etree.parse(str(self.resources.BIOSYNTH1_RAW_ARROWS_SVG))
-    #     assert element is not None, f"{self.resources.BIOSYNTH1_RAW_ARROWS_SVG}"
-    #     arrows = ns_xpath(element, f"{{{SVG_NS}}}g[@role='arrows']/{{{SVG_NS}}}g[@role='arrow']")
-    #     assert len(arrows) == 10, f"expected arrow count"
-    #     for arrow_svg in arrows:
-    #         svg_arrow = SVGArrow.create_from_svgg(arrow_svg)
-    #         ami_arrow = AmiArrow.create_from_svg_arrow(svg_arrow)
-    #         if ami_arrow is not None:
-    #             print("ami arrow str:", str(ami_arrow))
-    #         else:
-    #             print("cannot create AmiArrow")
-    #         print(ami_arrow.ge)
-
     @unittest.skip("Obsolete?")
     def test_write_gpml(self):
         ami_network = AmiNetwork()
         ami_network.write_graph(Path(Resources.TEMP_DIR, "test.gpml"))
 
     def test_create_overlap_boxes(self):
-        """Create front/back/side overlap boxes
-        """
+        """Create front/back/side overlap boxes"""
         svg = SVGSVG()
         arrows = [
             [[400, 300], [500, 300]],  # PLUSX horiziontal right
@@ -388,25 +383,35 @@ def test_create_overlap_boxes(self):
         expected_boxes = [
             # PLUSX right
             [
-                [[400, 500], [285, 315]], [[410, 490], [225, 285]], [[410, 490], [315, 375]], [[500, 550], [285, 315]],
-                [[350, 400], [285, 315]]
-
+                [[400, 500], [285, 315]],
+                [[410, 490], [225, 285]],
+                [[410, 490], [315, 375]],
+                [[500, 550], [285, 315]],
+                [[350, 400], [285, 315]],
             ],
             # PLUSY down
             [
-                [[285, 315], [400, 500]], [[225, 285], [410, 490]], [[315, 375], [410, 490]], [[285, 315], [500, 550]],
-                [[285, 315], [350, 500]]
-
+                [[285, 315], [400, 500]],
+                [[225, 285], [410, 490]],
+                [[315, 375], [410, 490]],
+                [[285, 315], [500, 550]],
+                [[285, 315], [350, 500]],
             ],
             # MINUSX left
             [
-                [[100, 200], [285, 315]], [[110, 190], [225, 285]], [[110, 190], [315, 375]], [[50, 100], [285, 315]],
-                [[200, 250], [285, 315]]
+                [[100, 200], [285, 315]],
+                [[110, 190], [225, 285]],
+                [[110, 190], [315, 375]],
+                [[50, 100], [285, 315]],
+                [[200, 250], [285, 315]],
             ],
-
             # MINUSY up
             [
-                [[285, 315], [100, 200]], [[225, 285], [110, 190]], [[315, 375], [110, 190]], [[285, 315], [50, 100]], [[285, 315], [200, 250]]
+                [[285, 315], [100, 200]],
+                [[225, 285], [110, 190]],
+                [[315, 375], [110, 190]],
+                [[285, 315], [50, 100]],
+                [[285, 315], [200, 250]],
             ],
         ]
 
@@ -436,23 +441,24 @@ def test_create_overlap_boxes(self):
                 bbox = BBox(xy_ranges=xy_ranges, swap_minmax=True)
                 ranges = bbox.get_ranges()
                 g = SVGG()
-                svg_rect = SVGRect(xy_ranges = ranges)
+                svg_rect = SVGRect(xy_ranges=ranges)
                 svg_rect.set_attribute("fill", "none")
                 svg_rect.set_attribute("stroke", "red")
                 svg_rect.set_attribute("opacity", "0.3")
                 g.append(svg_rect)
                 gg.append(g)
-        print("svg: ", svg.tostring(pretty_print=True))
+        logger.debug("svg: ", svg.tostring(pretty_print=True))
         path = Path(Resources.TEMP_DIR, "arrow_bboxes.svg")
         with open(path, "w") as f:
             f.write(svg.tostring(pretty_print=True))
 
         for arrow, exp_boxes in zip(arrows, expected_boxes):
-            print("a ", arrow)
+            logger.debug("a ", arrow)
             ami_arrow = AmiArrow()
             ami_arrow.svg_arrow = SVGArrow(tail_xy=arrow[0], head_xy=arrow[1])
-            box_tuple = ami_arrow.make_overlap_boxes(arrow_width=30, length=50, len_trim=10)
-            # print("BOXTUPLE", box_tuple)
+            box_tuple = ami_arrow.make_overlap_boxes(
+                arrow_width=30, length=50, len_trim=10
+            )
 
             for box, expect in zip(box_tuple, exp_boxes):
                 assert str(box) == str(expect), f"expected {expect}"
@@ -460,12 +466,21 @@ def test_create_overlap_boxes(self):
     # ------------ helpers -------------
 
     @classmethod
-    def create_and_test_arrows(cls, ami_graph, max_dim, total_islands=None, expected_arrows=None, big_island_count=None,
-                               output_temp=None):
+    def create_and_test_arrows(
+        cls,
+        ami_graph,
+        max_dim,
+        total_islands=None,
+        expected_arrows=None,
+        big_island_count=None,
+        output_temp=None,
+    ):
         islands = ami_graph.get_or_create_ami_islands()
         if total_islands:
             assert len(islands) == total_islands
-        big_islands = AmiIsland.get_islands_with_max_dimension_greater_than(max_dim, islands)
+        big_islands = AmiIsland.get_islands_with_max_dimension_greater_than(
+            max_dim, islands
+        )
         if big_island_count:
             assert len(big_islands) == big_island_count
         svg = SVGSVG()
diff --git a/test/test_bbox.py b/test/test_bbox.py
index a96d6d2..de16732 100644
--- a/test/test_bbox.py
+++ b/test/test_bbox.py
@@ -1,14 +1,24 @@
+import context
+from pyamiimage.svg import (
+    SVGG,
+    SVGSVG,
+    BBox,
+    SVGCircle,
+    SVGPath,
+    SVGRect,
+    SVGText,
+    SVGTextBox,
+    SVGTitle,
+)
 
-from ..pyimage.svg import SVGRect, SVGTitle, SVGText, SVGTextBox, SVGG, SVGSVG, SVGCircle, SVGPath, BBox
 
+class TestBBox:
 
-class TestBBox():
-
-# Bounding box Tests
+    # Bounding box Tests
 
     def test_create_empty_bbox(self):
         bbox = BBox()
-        assert bbox.xy_ranges == [[],[]]
+        assert bbox.xy_ranges == [[], []]
 
     def test_create_bbox(self):
         bbox = BBox([[100, 200], [300, 400]])
@@ -21,7 +31,7 @@ def test_create_bad_bbox(self):
             assert str(e) == "ranges must be increasing 100 !<= 50"
 
     def test_create_from_xy_w_h(self):
-        xy =[100, 200]
+        xy = [100, 200]
         width = 150
         height = 27
         bbox = BBox.create_from_xy_w_h(xy, width, height)
@@ -30,15 +40,15 @@ def test_create_from_xy_w_h(self):
 
     def test_update_bbox(self):
         bbox = BBox([[100, 200], [300, 400]])
-        bbox.set_xrange([10,20])
+        bbox.set_xrange([10, 20])
         assert bbox.xy_ranges == [[10, 20], [300, 400]]
 
     def test_update_bbox1(self):
         bbox = BBox()
-        assert bbox.xy_ranges == [[],[]]
-        bbox.set_xrange((10,20))
+        assert bbox.xy_ranges == [[], []]
+        bbox.set_xrange((10, 20))
         assert bbox.xy_ranges == [[10, 20], []]
-        bbox.set_yrange((30,40))
+        bbox.set_yrange((30, 40))
         assert bbox.xy_ranges == [[10, 20], [30, 40]]
 
     def test_get_values(self):
@@ -80,11 +90,11 @@ def test_get_unions(self):
     def test_add_points(self):
         bbox = BBox()
         assert bbox.xy_ranges == [[], []]
-        bbox.add_coordinate([1.,2.])
+        bbox.add_coordinate([1.0, 2.0])
         assert bbox.xy_ranges == [[1, 1], [2, 2]]
-        bbox.add_coordinate([3., 4.])
+        bbox.add_coordinate([3.0, 4.0])
         assert bbox.xy_ranges == [[1, 3], [2, 4]]
-        bbox.add_coordinate([5., 3.])
+        bbox.add_coordinate([5.0, 3.0])
         assert bbox.xy_ranges == [[1, 5], [2, 4]]
 
     def test_bbox_update(self):
@@ -92,21 +102,21 @@ def test_bbox_update(self):
         bbox = rect.get_or_create_bbox()
         assert bbox is None
         # assert not bbox.is_valid()
-        rect.set_xy([1,2])
+        rect.set_xy([1, 2])
         rect.set_width(5)
         rect.set_height(10)
         bbox = rect.get_or_create_bbox()
         assert bbox is not None
         assert bbox.is_valid()
-        assert bbox.xy_ranges == [[1,6], [2,12]]
+        assert bbox.xy_ranges == [[1, 6], [2, 12]]
         rect.set_width(20)
         bbox = rect.get_or_create_bbox()
         assert bbox.is_valid()
-        assert bbox.xy_ranges == [[1,21], [2,12]]
+        assert bbox.xy_ranges == [[1, 21], [2, 12]]
 
     def test_bbox_bad_values(self):
         rect = SVGRect()
-        rect.set_xy([1,2])
+        rect.set_xy([1, 2])
         rect.set_height(30)
         rect.set_width(-20)
         bbox = rect.get_or_create_bbox()
@@ -114,6 +124,4 @@ def test_bbox_bad_values(self):
         rect.set_width(20)
         bbox = rect.get_or_create_bbox()
         assert bbox.is_valid()
-        assert bbox.xy_ranges == [[1,21], [2, 32]]
-
-
+        assert bbox.xy_ranges == [[1, 21], [2, 32]]
diff --git a/test/test_colour_sep.py b/test/test_colour_sep.py
new file mode 100644
index 0000000..9f00302
--- /dev/null
+++ b/test/test_colour_sep.py
@@ -0,0 +1,307 @@
+# library imports
+import unittest
+from pathlib import Path
+from skimage import io
+
+import numpy as np
+import matplotlib.pyplot as plt
+from sklearn.cluster import KMeans
+
+import PIL
+from PIL import Image
+from sklearn.metrics import pairwise_distances_argmin
+from sklearn.utils import shuffle
+
+# local imports
+import context
+from pyamiimage import octree
+from pyamiimage._old_image_lib import Quantizer
+from pyamiimage.ami_image import AmiImage
+from pyamiimage.ami_util import AmiUtil
+
+from resources import Resources
+
+interactive = False
+#interactive = True
+
+PYAMI_DIR = Path(__file__).parent.parent
+TEST_DIR = Path(PYAMI_DIR, "test")
+PICO_DIR = Path(TEST_DIR, "alex_pico/")
+RESOURCES_DIR = Path(TEST_DIR, "resources")
+
+LONG_TEST = False
+
+"""Tests both Octree and kmeans color separation"""
+class TestOctree:
+    def setup_method(self, method=None):
+        pass
+
+    def test_reading_red_black(self):
+        """old octree"""
+        image_name = "red_black_cv.png"
+        # image_name = "purple_ocimum_basilicum.png"
+
+        path = Path(Path(__file__).parent.parent, "assets", image_name)
+        assert path.exists()
+        # img = imageio.imread(path)
+        pil_img = Image.open(path)  # PIL
+        # img = io.imread(path)
+
+        # ImageLib.image_show(img)
+        # print(img)
+
+        out_image, palette, palette_image = octree.quantize(pil_img, size=4)
+        # print(f"image {type(out_image)} ... {out_image}")
+        assert type(out_image) == PIL.Image.Image
+        assert out_image.size == (850, 641)
+        assert type(palette) == list
+        print(f"palette {len(palette)}, {palette[0]}")
+        if interactive:
+            out_image.show()
+
+    def test_81481_octree_quantize(self):
+        """old octree"""
+        size = 6
+        path = Path(PICO_DIR, "emss-81481-f001.png")
+        assert path.exists()
+        # img = imageio.imread(path)
+        img = Image.open(path)
+        pil_rgb_image, palette, palette_image = octree.quantize(img, size=size)
+        print(f"\npalette {type(palette)}  {palette}")
+        assert type(palette) is list, f"type palette {type(palette)}"
+        assert len(palette) == 36
+        assert type(pil_rgb_image) is PIL.Image.Image
+        nparray = np.asarray(pil_rgb_image)
+        assert nparray.shape == (555, 572, 3)
+        print(f"image {type(pil_rgb_image)} ... {pil_rgb_image}")
+        print(f"palette image {type(pil_rgb_image)}")
+        palette_array = np.asarray(pil_rgb_image)
+        assert palette_array.shape == (555, 572, 3)
+        path = Path(Resources.TEMP_DIR, "test1.png")
+        print(f"path {path}")
+        pil_rgb_image.save(path, "png")
+        if interactive:
+            pil_rgb_image.show()
+        pil_rgb_image.getcolors(maxcolors=256)
+
+    def test_81481_octree_new(self):
+
+        """Image.quantize(colors=256, method=None, kmeans=0, palette=None, dither=1)[source]
+        Convert the image to ‘P’ mode with the specified number of colors.
+
+        Parameters
+        colors – The desired number of colors, <= 256
+        method –
+        MEDIANCUT (median cut), MAXCOVERAGE (maximum coverage), FASTOCTREE (fast octree),
+        LIBIMAGEQUANT (libimagequant; check support using PIL.features.check_feature() with feature="libimagequant").
+        By default, MEDIANCUT will be used.
+        The exception to this is RGBA images. MEDIANCUT and MAXCOVERAGE do not support RGBA images,
+        so FASTOCTREE is used by default instead.
+        kmeans – Integer
+        palette – Quantize to the palette of given PIL.Image.Image.
+        dither – Dithering method, used when converting from mode “RGB” to “P” or from “RGB” or “L” to “1”.
+        Available methods are NONE or FLOYDSTEINBERG (default). Default: 1 (legacy setting)
+        Returns A new image"""
+        quantizer = Quantizer(input_dir=PICO_DIR, root="emss-81481-f001")
+        stream  = quantizer.extract_color_streams()
+        print(f"stream {stream}")
+
+    def test_example_several_color_streams(self):
+        """not yet useful tests"""
+        roots = [
+            "13068_2019_1355_Fig4_HTML",
+            # "fmicb-09-02460-g001",
+            # "pone.0054762.g004",
+            # "emss-81481-f001",
+        ]
+        quantizer = Quantizer(input_dir=PICO_DIR, num_colors=16)
+        for root in roots:
+            quantizer.root = root
+            stream = quantizer.extract_color_streams()
+            print(f"stream {stream}")
+
+    def test_green_battery(self):
+        streams = Quantizer(
+            input_dir=Resources.BATTERY_DIR, method="octree", root="green"
+        ).extract_color_streams()
+        print(f"streams {streams}")
+
+    def test_skimage(self):
+        # Authors: Robert Layton <robertlayton@gmail.com>
+        #          Olivier Grisel <olivier.grisel@ensta.org>
+        #          Mathieu Blondel <mathieu@mblondel.org>
+        #
+        # License: BSD 3 clause
+
+        """
+        see https://scikit-learn.org/stable/auto_examples/cluster/plot_color_quantization.html
+        """
+
+        import numpy as np
+        import matplotlib.pyplot as plt
+        from sklearn.cluster import KMeans
+        from sklearn.metrics import pairwise_distances_argmin
+        from sklearn.datasets import load_sample_image
+        from sklearn.utils import shuffle
+        from time import time
+
+        n_colors = 64
+        n_colors = 8
+
+        # Load the Summer Palace photo
+        china = load_sample_image("china.jpg")
+
+        # Convert to floats instead of the default 8 bits integer coding. Dividing by
+        # 255 is important so that plt.imshow behaves works well on float data (need to
+        # be in the range [0-1])
+        china = np.array(china, dtype=np.float64) / 255
+
+        # Load Image and transform to a 2D numpy array.
+        w, h, d = original_shape = tuple(china.shape)
+        assert d == 3
+        image_array = np.reshape(china, (w * h, d))
+
+        print("Fitting model on a small sub-sample of the data")
+        t0 = time()
+        image_array_sample = shuffle(image_array, random_state=0, n_samples=1_000)
+        kmeans = KMeans(n_clusters=n_colors, random_state=0).fit(image_array_sample)
+        print(f"done in {time() - t0:0.3f}s.")
+
+        # Get labels for all points
+        print("Predicting color indices on the full image (k-means)")
+        t0 = time()
+        labels = kmeans.predict(image_array)
+        print(f"done in {time() - t0:0.3f}s.")
+
+        codebook_random = shuffle(image_array, random_state=0, n_samples=n_colors)
+        print("Predicting color indices on the full image (random)")
+        t0 = time()
+        labels_random = pairwise_distances_argmin(codebook_random, image_array, axis=0)
+        print(f"done in {time() - t0:0.3f}s.")
+
+        def recreate_image(codebook, labels, w, h):
+            """Recreate the (compressed) image from the code book & labels"""
+            return codebook[labels].reshape(w, h, -1)
+
+        # Display all results, alongside original image
+        plt.figure(1)
+        plt.clf()
+        plt.axis("off")
+        plt.title("Original image (96,615 colors)")
+        plt.imshow(china)
+
+        plt.figure(2)
+        plt.clf()
+        plt.axis("off")
+        plt.title(f"Quantized image ({n_colors} colors, K-Means)")
+        image_q = recreate_image(kmeans.cluster_centers_, labels, w, h)
+        plt.imshow(image_q)
+
+        plt.figure(3)
+        plt.clf()
+        plt.axis("off")
+        plt.title(f"Quantized image ({n_colors} colors, Random)")
+        image_qq = recreate_image(codebook_random, labels_random, w, h)
+        plt.imshow(image_qq)
+        if interactive:
+            plt.show()
+
+    def test_skimage1(self):
+
+        n_colors = 4
+        path = Path(Resources.BATTERY_DIR, "green.png")
+        img0 = io.imread(path)
+        img = np.array(img0, dtype=np.float64) / 255
+        w, h, d = original_shape = tuple(img.shape)
+        image_array = np.reshape(img, (w * h, d))
+        image_array_sample = shuffle(image_array, random_state=0, n_samples=1_000)
+        kmeans = KMeans(n_clusters=n_colors, random_state=0).fit(image_array_sample)
+        labels = kmeans.predict(image_array)
+        codebook_random = shuffle(image_array, random_state=0, n_samples=n_colors)
+        labels_random = pairwise_distances_argmin(codebook_random, image_array, axis=0)
+
+        plt.figure(1)
+        plt.clf()
+        plt.axis("off")
+        plt.title("Original image (96,615 colors)")
+        plt.imshow(img)
+
+        plt.figure(2)
+        plt.clf()
+        plt.axis("off")
+        plt.title(f"Quantized image ({n_colors} colors, K-Means)")
+        image_q = kmeans.cluster_centers_[labels].reshape(w, h, -1)
+        plt.imshow(image_q)
+
+        plt.figure(3)
+        plt.clf()
+        plt.axis("off")
+        plt.title(f"Quantized image ({n_colors} colors, Random)")
+        image_qq = codebook_random[labels_random].reshape(w, h, -1)
+        plt.imshow(image_qq)
+        if interactive:
+            plt.show()
+
+    def test_kmeans2(self):
+        """https://stackoverflow.com/questions/48222977/python-converting-an-image-to-use-less-colors"""
+        import numpy as np
+        from skimage import io
+        from sklearn.cluster import KMeans
+
+
+        expected = [[222, 227, 219], [16, 16, 17], [95, 96, 97], [253, 149, 75], [1, 235, 29], [254, 254, 254],
+                    [20, 15, 196], [162, 187, 176], [240, 17, 23], [0, 118, 19]]
+
+        background = [255, 255, 255]
+        name = "green"
+        self._means_test_write(name, expected=expected)
+
+    @unittest.skipUnless(LONG_TEST, "takes too long")
+    def test_kmeans_long(self):
+
+        for name in [
+            "pmc8839570",
+            "MED_34909142_3",
+            "Signal_transduction_pathways_wp",
+            "red_black_cv",
+            "prisma"
+        ]:
+            print(f"======{name}======")
+            self._means_test_write(name, background=[255,255, 200], ncolors=10)
+
+    def _means_test_write(self, name, background=[255, 255, 255], expected=None, ncolors=10):
+        """skimage kmeans"""
+        path = Path(Resources.TEST_RESOURCE_DIR, name + ".png")
+        if not path.exists():
+            path = Path(Resources.TEST_RESOURCE_DIR, name + ".jpeg")
+        raw_image = io.imread(path)
+        color_delta = 20
+        labels, color_centers, quantized_images = AmiImage.kmeans(raw_image, ncolors, background)
+        print(color_centers)
+        for i, color in enumerate(color_centers):
+            if AmiUtil.is_white(color, color_delta):
+                print(f"white: {color}")
+            elif AmiUtil.is_black(color, color_delta):
+                print(f"black: {color}")
+            elif AmiUtil.is_gray(color, color_delta):
+                print(f"gray: {color}")
+                continue
+            hexs = ''.join(AmiUtil.int2hex(c)[-2:-1] for c in color)
+            if expected:
+                assert color == expected[i], f"color_centers {color}"
+            dir_path = Path(Resources.TEMP_DIR, name)
+            if not dir_path.exists():
+                dir_path.mkdir()
+            path = Path(dir_path, f"kmeans_{i}_{hexs}.png")
+            io.imsave(path, quantized_images[i])
+
+    # -------- Utility --------
+    @classmethod
+    def get_py4ami_dir(cls):
+        return Path(__file__).parent.parent
+
+
+"""
+img_bytes = bytes([R1, G1, B1, R2, G2, B2,..., Rn, Gn, Bn])
+
+im = Image.frombytes("RGB", (width, height), img_bytes)"""
diff --git a/pyimage/image_exploration.py b/test/test_image_tools.py
similarity index 62%
rename from pyimage/image_exploration.py
rename to test/test_image_tools.py
index 1380cad..8ccca56 100644
--- a/pyimage/image_exploration.py
+++ b/test/test_image_tools.py
@@ -1,19 +1,25 @@
-from skimage import data
 import os
-from skimage.filters import unsharp_mask
-import matplotlib.pyplot as plt
 from pathlib import Path
+
+import matplotlib.pyplot as plt
+import numpy as np
+from skimage.filters import unsharp_mask
+from skimage.morphology import disk  # noqa
 from skimage.morphology import (erosion, dilation, opening, closing,  # noqa
                                 white_tophat)
-from skimage.morphology import disk  # noqa
-import numpy as np
 
-from pyimage.ami_image import AmiImage
+import context
+from pyamiimage.ami_image import AmiImage
+from pyamiimage.ami_util import AmiUtil
+
+interactive = False
 
-class Exploration:
+class TestImageTools:
 
-    def sharpen_explore(self, axis=False):
-        gray = Exploration.create_gray_network_snippet("snippet_rgba.png")
+    """explores erode-dilate and similar tools"""
+
+    def test_sharpen_explore(self, axis=False):
+        gray = TestImageTools.create_gray_network_snippet("snippet_rgba.png")
 
         result_1_1 = unsharp_mask(gray, radius=1, amount=1)
         result_5_2 = unsharp_mask(gray, radius=5, amount=2)
@@ -27,11 +33,11 @@ def sharpen_explore(self, axis=False):
         ]
         ax, fig = self.create_subplots(plots, nrows=2, ncols=2, figsize=(10, 10))
 
-        Exploration.axis_layout(ax, axis, fig)
-        plt.show()
+        TestImageTools.axis_layout(ax, axis, fig)
+        if interactive:
+            plt.show()
 
-    def explore_erode_dilate(self):
-        from skimage.util import img_as_ubyte
+    def test_erode_dilate(self):
         # orig_phantom = img_as_ubyte(data.shepp_logan_phantom())
         # Exploration.make_numpy_assert(orig_phantom, shape=(400, 400), max=255, dtype=np.uint8)
         # fig, ax = plt.subplots()
@@ -47,15 +53,16 @@ def explore_erode_dilate(self):
         # plt.show()
         #
 
-        white = Exploration.create_white_network_snippet("snippet_rgba.png")
+        white = TestImageTools.create_white_network_snippet("snippet_rgba.png")
 
-        Exploration.make_numpy_assert(white, shape=(341, 796), max=1, dtype=np.bool)
+        AmiUtil.make_numpy_assert(white, shape=(341, 796), maxx=255, dtype=np.int64)
 
         footprint = disk(1)
         eroded = erosion(white, footprint)
-        Exploration.plot_comparison(white, eroded, 'erosion')
+        TestImageTools.plot_comparison(white, eroded, 'erosion')
 
-        plt.show()
+        if interactive:
+            plt.show()
 
         erode_1 = erosion(white, disk(1))
         erode_2 = erosion(white, disk(2))
@@ -73,10 +80,12 @@ def explore_erode_dilate(self):
         ]
         ax, fig = self.create_subplots(plots, nrows=3, ncols=2, figsize=(10, 10))
 
-        # Exploration.axis_layout(ax, axis, fig)
-        plt.show()
+        axis = None
+        TestImageTools.axis_layout(ax, axis, fig)
+        if interactive:
+            plt.show()
 
-# ================= resources =================
+    # ================= resources =================
     @classmethod
     def create_gray_network_snippet(cls, png):
         path = Path(Path(__file__).parent.parent, "test", "resources", png)
@@ -90,7 +99,7 @@ def create_white_network_snippet(cls, png):
         assert path.exists(), f"path {path} exists"
         white = AmiImage.create_white_binary_from_file(path)
 
-        Exploration.make_numpy_assert(white, shape=(341, 796), max=1, dtype=np.bool)
+        AmiUtil.make_numpy_assert(white, shape=(341, 796), maxx=255, dtype=np.int64)
 
         return white
 
@@ -146,46 +155,44 @@ def axis_layout(cls, ax, axis, fig):
                 a.axis('off')
         fig.tight_layout()
 
-# =========== palettes ============
+    # =========== palettes ============
 
-"""
-From https://stackoverflow.com/questions/45523205/get-rgb-colors-from-color-palette-image-and-apply-to-binary-image
+    def explore_palette(cls):
+        """
+        From https://stackoverflow.com/questions/45523205/get-rgb-colors-from-color-palette-image-and-apply-to-binary-image
 
-You can use a combination of a reshape and np.unique to extract the unique RGB values from your color palette image:
-"""
-# Load the color palette
-from skimage import io
-raise NotImplemented("image explore, needs biosynth3??")
-palette = io.imread(os.image.join(os.getcwd(), 'color_palette.png'))
+        You can use a combination of a reshape and np.unique to extract the unique RGB values from your color palette image:
+        """
+        # Load the color palette
+        raise NotImplemented("image explore, needs biosynth3??")
+        palette = io.imread(os.image.join(os.getcwd(), 'color_palette.png'))
 
-# Use `np.unique` following a reshape to get the RGB values
-palette = palette.reshape(palette.shape[0]*palette.shape[1], palette.shape[2])
-palette_colors = np.unique(palette, axis=0)
-"""
-(Note that the axis argument for np.unique was added in numpy version 1.13.0, so you may need to upgrade numpy for this to work.)
+        # Use `np.unique` following a reshape to get the RGB values
+        palette = palette.reshape(palette.shape[0] * palette.shape[1], palette.shape[2])
+        palette_colors = np.unique(palette, axis=0)
+        """
+        (Note that the axis argument for np.unique was added in numpy version 1.13.0, so you may need to upgrade numpy for this to work.)
 
-Once you have palette_colors, you can pretty much use the code you already have to save the image, except you now add the different RGB values instead of copies of ~img to your img_rgba array.
-"""
-img = None  # TODO
-for p in range(palette_colors.shape[0]):
+        Once you have palette_colors, you can pretty much use the code you already have to save the image, except you now add the different RGB values instead of copies of ~img to your img_rgba array.
+        """
+        img = None  # TODO
+        for p in range(palette_colors.shape[0]):
 
-    # Create an MxNx4 array (RGBA)
-    img_rgba = np.zeros((img.shape[0], img.shape[1], 4), dtype=np.uint8)
+            # Create an MxNx4 array (RGBA)
+            img_rgba = np.zeros((img.shape[0], img.shape[1], 4), dtype=np.uint8)
 
-    # Fill R, G and B with appropriate colors
-    for c in range(3):
-        img_rgba[:,:,c] = img.astype(np.uint8) * palette_colors[p,c]
+            # Fill R, G and B with appropriate colors
+            for c in range(3):
+                img_rgba[:, :, c] = img.astype(np.uint8) * palette_colors[p, c]
 
-    # For alpha just use the image again (makes background transparent)
-    img_rgba[:,:,3] = img.astype(np.uint8) * 255
+            # For alpha just use the image again (makes background transparent)
+            img_rgba[:, :, 3] = img.astype(np.uint8) * 255
 
-    # Save image
-    io.imsave('img_col'+str(p)+'.png', img_rgba)
+            # Save image
+            io.imsave('img_col' + str(p) + '.png', img_rgba)
 
 # (Note that you need to use np.uint8 as datatype for your image, since binary images obviously cannot represent different colors.)
-"""
 
-if __name__ == '__main__':
-    Exploration().sharpen_explore(axis=True)
-    Exploration().explore_erode_dilate()
-"""
\ No newline at end of file
+# if __name__ == '__main__':
+#     Exploration().sharpen_explore(axis=True)
+#     Exploration().explore_erode_dilate()
diff --git a/test/test_octree.py b/test/test_octree.py
deleted file mode 100644
index 9648066..0000000
--- a/test/test_octree.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# library imports
-from pathlib import Path
-import PIL
-from PIL import Image
-import numpy as np
-# local imports
-from ..pyimage import octree
-from ..pyimage.old_code.image_lib import Quantizer
-
-interactive = False
-
-PYAMI_DIR = Path(__file__).parent.parent
-TEST_DIR = Path(PYAMI_DIR, "test")
-PICO_DIR = Path(TEST_DIR, "alex_pico/")
-RESOURCES_DIR = Path(TEST_DIR, "resources")
-
-
-class TestOctree:
-
-    def setup_method(self, method=None):
-        pass
-
-    def test_red_black(self):
-        """old octree"""
-        image_name = "red_black_cv.png"
-        # image_name = "purple_ocimum_basilicum.png"
-
-        path = Path(Path(__file__).parent.parent, "assets", image_name)
-        assert path.exists()
-        # img = imageio.imread(path)
-        img = Image.open(path)
-
-        # ImageLib.image_show(img)
-        # print(img)
-
-        out_image, palette, palette_image = octree.quantize(img, size=4)
-        print(f"image {type(out_image)} ... {out_image}")
-        if interactive:
-            out_image.show()
-
-    def test_81481_diagram(self):
-        """old octree"""
-        size = 6
-        path = Path(PICO_DIR, "emss-81481-f001.png")
-        assert path.exists()
-        # img = imageio.imread(path)
-        img = Image.open(path)
-        pil_rgb_image, palette, palette_image = octree.quantize(img, size=size)
-        print(f"\npalette {type(palette)}  {palette}")
-        assert type(pil_rgb_image) is PIL.Image.Image
-        nparray = np.asarray(pil_rgb_image)
-        assert nparray.shape == (555, 572, 3)
-        print(f"image {type(pil_rgb_image)} ... {pil_rgb_image}")
-        print(f"palette image {type(pil_rgb_image)}")
-        palette_array = np.asarray(pil_rgb_image)
-        assert palette_array.shape == (555, 572, 3)
-        pil_rgb_image.save(Path(path.parent, "test1.png"), "png")
-        if interactive:
-            pil_rgb_image.show()
-        pil_rgb_image.getcolors(maxcolors=256)
-
-    def test_81481_octree_new(self):
-
-        """Image.quantize(colors=256, method=None, kmeans=0, palette=None, dither=1)[source]
-Convert the image to ‘P’ mode with the specified number of colors.
-
-Parameters
-colors – The desired number of colors, <= 256
-method –
-MEDIANCUT (median cut), MAXCOVERAGE (maximum coverage), FASTOCTREE (fast octree),
-LIBIMAGEQUANT (libimagequant; check support using PIL.features.check_feature() with feature="libimagequant").
-By default, MEDIANCUT will be used.
-The exception to this is RGBA images. MEDIANCUT and MAXCOVERAGE do not support RGBA images,
-so FASTOCTREE is used by default instead.
-kmeans – Integer
-palette – Quantize to the palette of given PIL.Image.Image.
-dither – Dithering method, used when converting from mode “RGB” to “P” or from “RGB” or “L” to “1”.
-Available methods are NONE or FLOYDSTEINBERG (default). Default: 1 (legacy setting)
-Returns A new image
-"""
-        quantizer = Quantizer(input_dir=PICO_DIR, root="emss-81481-f001")
-        quantizer.extract_color_streams()
-
-    def test_example_several_color_streams(self):
-        """not yet useful tests"""
-        roots = [
-            "13068_2019_1355_Fig4_HTML",
-            # "fmicb-09-02460-g001",
-            # "pone.0054762.g004",
-            # "emss-81481-f001",
-            ]
-        quantizer = Quantizer(input_dir=PICO_DIR, num_colors=16)
-        for root in roots:
-            quantizer.root = root
-            quantizer.extract_color_streams()
-
-    def test_green_battery(self):
-        Quantizer(input_dir=RESOURCES_DIR, method="octree",
-                  root="green").extract_color_streams()
-
-    def test_example_anuv_pathways(self):
-        """not yet useful tests"""
-        roots = [
-            "biosynth_path_1",
-            # "biosynth_path_2",
-            # "biosynth_path_3",
-            # "biosynth_path_4",
-            # "biosynth_path_5",
-            # "biosynth_path_6",
-            # "biosynth_path_7",
-            # "biosynth_path_8",
-        ]
-        for root in roots:
-            print(f"\n=====root: {root}=====")
-            Quantizer(input_dir=RESOURCES_DIR, method="octree",
-                      root=root).extract_color_streams()
-
-    # -------- Utility --------
-    @classmethod
-    def get_py4ami_dir(cls):
-        return Path(__file__).parent.parent
-
-
-"""
-img_bytes = bytes([R1, G1, B1, R2, G2, B2,..., Rn, Gn, Bn])
-
-im = Image.frombytes("RGB", (width, height), img_bytes)"""
diff --git a/test/test_svg.py b/test/test_svg.py
index 7e34300..da2b2cd 100644
--- a/test/test_svg.py
+++ b/test/test_svg.py
@@ -1,18 +1,34 @@
 import os
 from pathlib import Path
-from lxml import etree as ET
-
-
-from ..pyimage.svg import SVGRect, SVGTitle, SVGText, SVGTextBox, SVGG, SVGSVG, SVGCircle, \
-    SVGPath, BBox, SVGDefs, SVGMarker, SVGPolygon, SVGArrow
 
+from lxml import etree as ET
 
-class TestSVG():
-
+import context
+from pyamiimage.svg import (
+    SVGG,
+    SVGSVG,
+    BBox,
+    SVGArrow,
+    SVGCircle,
+    SVGDefs,
+    SVGMarker,
+    SVGPath,
+    SVGPolygon,
+    SVGRect,
+    SVGText,
+    SVGTextBox,
+    SVGTitle,
+)
+
+
+class TestSVG:
     def test_good_attribute(self):
         rect = SVGRect()
         rect.element.attrib["foo"] = "bar"
-        assert rect.tostring(pretty_print=False) == """<svg:rect xmlns:svg="http://www.w3.org/2000/svg" foo="bar"/>"""
+        assert (
+            rect.tostring(pretty_print=False)
+            == """<svg:rect xmlns:svg="http://www.w3.org/2000/svg" foo="bar"/>"""
+        )
 
     def test_bad_attribute(self):
         rect = SVGRect()
@@ -25,20 +41,29 @@ def test_bad_attribute(self):
     def test_create_empty_rect(self):
         svg_rect = SVGRect()
         assert type(svg_rect) is SVGRect
-        assert svg_rect.tostring(pretty_print=False) == """<svg:rect xmlns:svg="http://www.w3.org/2000/svg"/>"""
+        assert (
+            svg_rect.tostring(pretty_print=False)
+            == """<svg:rect xmlns:svg="http://www.w3.org/2000/svg"/>"""
+        )
 
     def test_create_empty_rect_title(self):
         rect = SVGRect()
         title = SVGTitle("title")
         rect.append(title)
-        assert rect.tostring(pretty_print=False) == """<svg:rect xmlns:svg="http://www.w3.org/2000/svg"><svg:title title="title"/></svg:rect>"""
+        assert (
+            rect.tostring(pretty_print=False)
+            == """<svg:rect xmlns:svg="http://www.w3.org/2000/svg"><svg:title title="title"/></svg:rect>"""
+        )
 
     def test_create_rect_w_h(self):
         rect = SVGRect()
         rect.set_height(50)
         rect.set_width(100)
         rect.set_xy((200, 300))
-        assert rect.tostring(pretty_print=False) == """<svg:rect xmlns:svg="http://www.w3.org/2000/svg" height="50.0" width="100.0" x="200.0" y="300.0"/>"""
+        assert (
+            rect.tostring(pretty_print=False)
+            == """<svg:rect xmlns:svg="http://www.w3.org/2000/svg" height="50.0" width="100.0" x="200.0" y="300.0"/>"""
+        )
 
     def test_create_svg_rect_w_h(self):
         svg = SVGSVG()
@@ -47,16 +72,22 @@ def test_create_svg_rect_w_h(self):
         rect.set_height(50)
         rect.set_width(100)
         rect.set_xy((200, 300))
-        assert svg.tostring(pretty_print=False) == """<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="1200.0" height="1200.0"><svg:rect height="50.0" width="100.0" x="200.0" y="300.0"/></svg:svg>"""
+        assert (
+            svg.tostring(pretty_print=False)
+            == """<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="1200.0" height="1200.0"><svg:rect height="50.0" width="100.0" x="200.0" y="300.0"/></svg:svg>"""
+        )
 
-# Circle
+    # Circle
     def test_circle(self):
         circle = SVGCircle(xy=[10, 20], rad=5)
         bbox = circle.get_or_create_bbox()
         assert bbox is not None
         assert circle.is_valid()
-        assert bbox.xy_ranges == [[5,15],[15,25]]
-        assert circle.tostring(pretty_print=False) == """<svg:circle xmlns:svg="http://www.w3.org/2000/svg" cx="10" cy="20" r="5"/>"""
+        assert bbox.xy_ranges == [[5, 15], [15, 25]]
+        assert (
+            circle.tostring(pretty_print=False)
+            == """<svg:circle xmlns:svg="http://www.w3.org/2000/svg" cx="10" cy="20" r="5"/>"""
+        )
 
     def test_write_svg(self):
         svg = SVGSVG()
@@ -89,15 +120,15 @@ def test_arrowhead(self):
         svg.append(arrow)
         # print("SVGXX\n", ET.tostring(svg.element, pretty_print=True).decode())
 
- #        print("SXX\n", ET.tostring(svg.element).decode())
- #        assert """'<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="1200.0" '
- # 'height="1200.0"><svg:defs><svg:marker id="arrowhead" markerWidth="10.0" '
- # 'markerHeight="7.0" refX="0.0" refY="3.5" orient="auto"><svg:polygon '
- # 'points="0 0, 10 3.5, 0 7" fill="none" stroke="red" '
- # 'stroke-width="1"/></svg:marker></svg:defs><svg:g><svg:line x1="150" y1="200" '
- # 'x2="50" y2="75" fill="none" stroke="red" stroke-width="1" '
- # 'marker-end="url(#arrowhead)"/></svg:g></svg:svg>'
- #  """ == ET.tostring(svg.element).decode()
+    #        print("SXX\n", ET.tostring(svg.element).decode())
+    #        assert """'<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="1200.0" '
+    # 'height="1200.0"><svg:defs><svg:marker id="arrowhead" markerWidth="10.0" '
+    # 'markerHeight="7.0" refX="0.0" refY="3.5" orient="auto"><svg:polygon '
+    # 'points="0 0, 10 3.5, 0 7" fill="none" stroke="red" '
+    # 'stroke-width="1"/></svg:marker></svg:defs><svg:g><svg:line x1="150" y1="200" '
+    # 'x2="50" y2="75" fill="none" stroke="red" stroke-width="1" '
+    # 'marker-end="url(#arrowhead)"/></svg:g></svg:svg>'
+    #  """ == ET.tostring(svg.element).decode()
 
     def test_svg_arrow_str(self):
         """
diff --git a/test/test_tesseract_hocr.py b/test/test_tesseract_hocr.py
index a06769d..6bc90b9 100644
--- a/test/test_tesseract_hocr.py
+++ b/test/test_tesseract_hocr.py
@@ -1,4 +1,4 @@
-# from ..pyimage import tesseract_hocr
+# from ..pyamiimage import tesseract_hocr
 
 from skimage import io
 from matplotlib import pyplot as plt
@@ -9,8 +9,9 @@
 from lxml import etree as ET
 import logging
 # local
-from ..pyimage.tesseract_hocr import TesseractOCR
-from ..test.resources import Resources
+import context
+from pyamiimage.tesseract_hocr import TesseractOCR
+from resources import Resources
 
 logger = logging.getLogger(__name__)
 
@@ -20,14 +21,14 @@
 (to run tesseract:
 tesseract <pathname> <output_root> hocr
 e.g.,
-tesseract test/resources/biosynth_path_1_cropped_arrows_removed.png arrows_removed hocr
+tesseract test/resources/arrows_removed.png arrows_removed hocr
 creates:
 arrows_removed.hocr (actually html file)
 we may have to manually rename these to .html
 """
 
 skip_long_tests = True
-
+interactive = False
 
 class TestTesseractHOCR:
     interactive = False
@@ -36,15 +37,15 @@ def setup_method(self, method):
         """setup any state tied to the execution of the given method in a
         class.  setup_method is invoked for every test method of a class.
         """
-        self.biosynth1 = Resources.BIOSYNTH1
+        self.biosynth1 = Resources.BIOSYNTH1_RAW
         self.biosynth1_hocr = TesseractOCR.hocr_from_image_path(self.biosynth1)
         self.biosynth1_elem = TesseractOCR.parse_hocr_string(self.biosynth1_hocr)
 
-        self.biosynth2 = Resources.BIOSYNTH2
+        self.biosynth2 = Resources.BIOSYNTH2_RAW
         self.biosynth2_hocr = TesseractOCR.hocr_from_image_path(self.biosynth2)
         self.biosynth2_elem = TesseractOCR.parse_hocr_string(self.biosynth2_hocr)
-        
-        self.biosynth3 = Resources.BIOSYNTH3
+
+        self.biosynth3 = Resources.BIOSYNTH3_RAW
         self.biosynth3_hocr = TesseractOCR.hocr_from_image_path(self.biosynth3)
         self.biosynth3_elem = TesseractOCR.parse_hocr_string(self.biosynth3_hocr)
 
@@ -115,7 +116,7 @@ def test_extract_bbox_from_hocr(self):
         """
         bbox, words = TesseractOCR.extract_bbox_from_hocr(self.biosynth3_elem)
         assert len(words) == 60
-        assert words[:3] == ['Straight', 'chain', 'ester']
+        assert words[:3] == ["Straight", "chain", "ester"]
         assert len(bbox) == 60
         assert list(bbox[0]) == [201, 45, 302, 75]
 
@@ -129,25 +130,27 @@ def test_find_phrases(self):
 
     def test_find_text_group_biosynth2(self):
         biosynth2_img = io.imread(self.biosynth2)
-        
+
         word_bboxes, words = TesseractOCR.extract_bbox_from_hocr(self.biosynth2_elem)
         raw_tesseract = TesseractOCR.draw_bbox_around_words(image=biosynth2_img, bbox_coordinates=word_bboxes)
-        
-        io.imshow(raw_tesseract)
-        io.show()
+
+        if self.interactive:
+            io.imshow(raw_tesseract)
+            io.show()
 
         phrases, phrases_bboxes = TesseractOCR.find_phrases(self.biosynth2_elem)
         phrases_tess = TesseractOCR.draw_bbox_around_words(image=biosynth2_img, bbox_coordinates=phrases_bboxes)
-        io.imshow(phrases_tess)
-        io.show()
+        if self.interactive:
+            io.imshow(phrases_tess)
+            io.show()
 
     @unittest.skip("TesseractOCR is deprecated")
     def test_find_text_group(self):
         biosynth1_img = io.imread(self.biosynth1)
-        
+
         word_bboxes, words = TesseractOCR.extract_bbox_from_hocr(self.biosynth1_elem)
         raw_tesseract = TesseractOCR.draw_bbox_around_words(image=biosynth1_img, bbox_coordinates=word_bboxes)
-        
+
         # io.imshow(raw_tesseract)
         # io.show()
 
@@ -155,26 +158,29 @@ def test_find_text_group(self):
         groups_bboxes = TesseractOCR.find_word_groups(bbox_of_phrases=phrase_bboxes)
         grouped_text = TesseractOCR.draw_bbox_around_words(image=biosynth1_img, bbox_coordinates=groups_bboxes)
 
-        io.imshow(grouped_text)
-        io.show()
+        if self.interactive:
+            io.imshow(grouped_text)
+            io.show()
         # f, ax = plt.subplots(1, 2)
         # ax[0].imshow(raw_tesseract)
         # ax[1].imshow(grouped_text)
 
         # plt.show()
 
+    @unittest.skip("split_image_into_snippets() NYI")
     def test_cropped_test_group(self):
         biosynth2_img = io.imread(self.biosynth2)
         tiles, limits = TesseractOCR.split_image_into_snippets(biosynth2_img)
         assert limits is not None
-        for tile in tiles:
-            io.imshow(tile)
-            io.show()
+        if self.interactive:
+            for tile in tiles:
+                io.imshow(tile)
+                io.show()
 
 
     @unittest.skipIf(skip_long_tests, "wikidata lookup")
     def test_phrase_wikidata_search(self):
-        path = Resources.BIOSYNTH3
+        path = Resources.BIOSYNTH3_RAW
         hocr = TesseractOCR.hocr_from_image_path(path)
         root = TesseractOCR.parse_hocr_string(hocr)
         phrases, bbox_for_phrases = TesseractOCR.find_phrases(root)
@@ -185,23 +191,21 @@ def test_phrase_wikidata_search(self):
 
     def test_output_phrases_to_file(self):
         sample_phrases = ["test phrase", "more test phrase", "one more"]
-        file = TesseractOCR.output_phrases_to_file(sample_phrases, 'test_file.txt')
+        file = TesseractOCR.output_phrases_to_file(sample_phrases, "test_file.txt")
         # phrases = []
-        with open(file, 'r') as f:
-            phrases = f.read().split('\n')
+        with open(file, "r") as f:
+            phrases = f.read().split("\n")
         phrases.pop(-1)  # remove empty string associated with last \n
         assert file.exists()
         assert phrases == sample_phrases
 
     def test_extract_bbox_from_hocr3(self):
-        test_hocr_file = Path(Path(__file__).parent, 'resources/tesseract_biosynth_path_3.hocr.html')
-        root = TesseractOCR.read_hocr_file(test_hocr_file)
+        root = TesseractOCR.read_hocr_file(Resources.BIOSYNTH3_HOCR)
         bboxes, words = TesseractOCR.extract_bbox_from_hocr(root)
         assert len(bboxes) == 60
 
     def test_extract_bboxes_from_image(self):
-        image_path = Path(Path(__file__).parent, 'resources/biosynth_path_3.png')
-        bboxes, words = TesseractOCR.extract_bbox_from_image(image_path)
+        bboxes, words = TesseractOCR.extract_bbox_from_image(Resources.BIOSYNTH3_RAW)
         assert len(bboxes) == 60
         assert str(bboxes[0]) == "[201  45 302  75]"
         assert words[0] == "Straight"
@@ -219,5 +223,6 @@ def test_envelope(self):
         full_box = TesseractOCR.envelop(bboxes)
         biosynth1_img = io.imread(self.biosynth1)
         boxed = TesseractOCR.draw_bbox_around_words(image=biosynth1_img, bbox_coordinates=[full_box])
-        io.imshow(boxed)
-        io.show()
+        if self.interactive:
+            io.imshow(boxed)
+            io.show()
diff --git a/test/test_text_and_arrows.py b/test/test_text_and_arrows.py
deleted file mode 100644
index e69de29..0000000
diff --git a/test/test_text_box.py b/test/test_text_box.py
index e0bf755..1e2d5c8 100644
--- a/test/test_text_box.py
+++ b/test/test_text_box.py
@@ -1,16 +1,19 @@
 from skimage import io
 import unittest
-from ..test.resources import Resources
-from ..pyimage.tesseract_hocr import TesseractOCR
-from ..pyimage.text_box import TextBox
+from resources import Resources
+import context
+from pyamiimage.tesseract_hocr import TesseractOCR
+from pyamiimage.text_box import TextBox
 
 class TestTextBox:
     def setup_method(self, method):
-        self.cropped1_hocr = TesseractOCR.hocr_from_image_path(Resources.BIOSYNTH1_CROPPED)
+        self.cropped1_hocr = TesseractOCR.hocr_from_image_path(
+            Resources.BIOSYNTH1_CROPPED_PNG
+        )
         self.cropped1_elem = TesseractOCR.parse_hocr_string(self.cropped1_hocr)
-        self.biosynth2_hocr = TesseractOCR.hocr_from_image_path(Resources.BIOSYNTH2)
+        self.biosynth2_hocr = TesseractOCR.hocr_from_image_path(Resources.BIOSYNTH2_RAW)
         self.biosynth2_elem = TesseractOCR.parse_hocr_string(self.biosynth2_hocr)
-        self.biosynth3_hocr = TesseractOCR.hocr_from_image_path(Resources.BIOSYNTH3)
+        self.biosynth3_hocr = TesseractOCR.hocr_from_image_path(Resources.BIOSYNTH3_RAW)
         self.biosynth3_elem = TesseractOCR.parse_hocr_string(self.biosynth3_hocr)
 
     def test_extract_phrases_boxes0(self):
@@ -39,5 +42,3 @@ def test_extract_text_path2(self):
         for text_box in text_boxes:
             # print(text_box.text, text_box.bbox)
             pass
-
-