diff --git a/connectivity.py b/connectivity.py new file mode 100644 index 0000000..cf340e1 --- /dev/null +++ b/connectivity.py @@ -0,0 +1,145 @@ +from machinevisiontoolbox import Image +import cv2 as cv +import numpy as np + +im = Image( + r""" + .......... + .......... + .......... + ....##.... + ....##.... + .......... + .......... + .......... + .......... + .......... + """, + binary=True, +) +im.showpixels() + + +class Blob: + + def __init__(self, label, moments): + self.label = label + self.contour = None + self.moments = moments + self.children = [] + self.parent = None + self.level = None + + def __str__(self): + return f"Blob {self.label}, level {self.level}, parent {self.parent.label if self.parent is not None else '-'} with children {[child.label for child in self.children]}" + + def __repr__(self): + return self.__str__() + + +# im = Image.Read("sharks.png") +im = Image.Read("multiblobs.png") + +contours, hierarchy = cv.findContours( + im.to_int(), mode=cv.RETR_TREE, method=cv.CHAIN_APPROX_NONE +) +# change contours to list of 2xN arraay +contours = [c[:, 0, :].T for c in contours] + +retval, labels = cv.connectedComponentsWithAlgorithm( + image=im.to_int(), connectivity=4, ltype=cv.CV_32S, ccltype=cv.CCL_BBDT +) +print(retval) + +blobs = [] +for label in range(retval): + m = cv.moments((labels == label).astype("uint8"), True) + print(f"Label {label} has {m['m00']} pixels") + blobs.append(Blob(label, m)) + +blobdict = {} +for i, contour in enumerate(contours): + u, v = contour[:, 0] + label = labels[v, u] + print( + f" contour #{i} with point ({u}, {v}), belongs to label {label}, length {contour.shape[1]}" + ) + blobs[label].contour = contour + blobdict[i] = label # map contour number to label + +print(retval) +print(blobdict) + +Image(labels).disp(block=True) + +hierarchy = hierarchy.squeeze() +# print(hierarchy.shape) +# print(hierarchy) + + +def levels(c, hierarchy, blobs, level): + blob = blobs[blobdict[c]] + thislevel = [] + while c != -1: + thislevel.append(c) + + if hierarchy[c, 2] != -1: + # has a child + blob.children = levels(hierarchy[c, 2], hierarchy, blobs, level + 1) + + # add reference to parent + if hierarchy[c, 3] == -1: + blob.parent = None + else: + blob.parent = blobdict[hierarchy[c, 3]] + blob.level = level + # get next at this level + c = hierarchy[c, 0] + + return [blobdict[c] for c in thislevel] + + +print(hierarchy) +topblobs = levels(0, hierarchy, blobs, 0) + +for blob in blobs: + print(blob) +print() +for blob in topblobs: + print(blob) + +# v, u = np.nonzero(labels == 1) +# m00 = len(v) +# m10 = np.sum(u) +# m01 = np.sum(v) +# m11 = np.sum(u * v) +# m20 = np.sum(u**2) +# m02 = np.sum(v**2) +# m30 = np.sum(u**3) +# u0 = u - m10 / m00 + +m = cv.moments((labels == 1).astype("uint8"), True) +print(m) + +Image(labels).disp(block=True) + +# time execution using timeit +# import timeit + +# print( +# timeit.timeit( +# "cv.findContours(im.to_int(), mode=cv.RETR_TREE, method=cv.CHAIN_APPROX_NONE)", +# globals=globals(), +# number=1000, +# ) +# ) # 0.0001 + +# print( +# timeit.timeit( +# "cv.connectedComponents(image=im.to_int(), connectivity=4, ltype=cv.CV_32S)", +# globals=globals(), +# number=1000, +# ) +# ) + +# contours 0.458 ms, connectedComponents 0.680 ms diff --git a/examples/eg1.py b/examples/eg1.py index 1a16624..a1e01ae 100644 --- a/examples/eg1.py +++ b/examples/eg1.py @@ -1,11 +1,12 @@ import code + # import machinevisiontoolbox as mvtb # from machinevisiontoolbox import Image, Blob -from machinevisiontoolbox.Image import Image +from machinevisiontoolbox import Image -# # im = Image("machinevisiontoolbox/images/flowers?.png") +# # im = Image("flowers1.png") -im = Image("flowers1.png") +im = Image.Read("flowers1.png") # im.disp() print(im) @@ -22,7 +23,7 @@ print(im.isint) im.stats() -z = im.float() ** 2 +z = im.to("float") ** 2 print(z) z.stats() z.disp() @@ -32,20 +33,13 @@ # read from web -# im = Image("http://petercorke.com/files/images/monalisa.png") -# print("monalisa:", im) -# im.disp() -# im = Image("http://petercorke.com/files/images/flowers7.png") -# print("flowers7:", im) -# im.disp() +im = Image.Read("http://petercorke.com/files/images/monalisa.png") +print("monalisa:", im) -# the images all load with 4 planes -# so they are not tagged as color, but they display as color -# some issue with imdecode() # blobs -mb = Image("multiblobs.png") +mb = Image.Read("multiblobs.png") # mb.disp() blobs = mb.blobs() @@ -71,5 +65,3 @@ # mser = mb.MSER() # mser doesn't have detectAndCompute - I think it only has detectRegions() - -code.interact(local=dict(globals(), **locals())) diff --git a/ilabel.py b/ilabel.py new file mode 100644 index 0000000..5f3b0ba --- /dev/null +++ b/ilabel.py @@ -0,0 +1,189 @@ +import numpy as np +import timeit + +# from numba import jit +# import cProfile + +# ilabel.c from MVTB for MATLAB converted to Python by CoPilot +# +# Copyright (C) 1995-2009, by Peter I. Corke + + +UNKNOWN = 0 +import time + + +def ilabel(im, connectivity=4, minsize=0): + + def merge(label1, label2): + + # merge label1 and label2 (maybe indirected) + # print(f"merge: {label1}, {label2}") + label2 = lmap[label2] + # choose which label to keep + if label1 > label2: + label1, label2 = label2, label1 + # label1 dominates + # print( + # f"merge ({row}, {col}): {label2}({blobsize[label2]}) -> {label1}({blobsize[label1]})" + # ) + # print(limage) + # print(lmap) + # print() + lmap[label2] = label1 + blobsize[label1] += blobsize[ + label2 + ] # dominant blob absorbs pixels from the other + + return label1 + + tstart = time.time() + + height, width = im.shape + + # lmap = {} # map old labels to new labels + lmap = np.zeros((20_000,), dtype="int32") + blobsize = {} # size of each blob + parent = {} # parent of each blob + color = {} # color of each blob + epoint = {} # the enclosure point of each blob, guaranteed on the boundary + + # def xxtimeit(cmd): + # row = 300 + # col = 400 + # curpix = im[row, col] + # lmap = np.zeros((20_000,), dtype="int32") + # # lmap = {} + # n = 10_000 + # print(cmd, end=": ") + # print(timeit.timeit(cmd, number=n, globals=locals()) / n * 1e6, " us") + + # xxtimeit("2+3") + # xxtimeit("im[row,col]") + # xxtimeit("lmap[curpix] = 10") + # xxtimeit("lmap[curpix]") + + # create the label image, initially all zeros (ie. no labels) + # note that performance is much worse for uint16 or int32 types + limage = np.zeros((height, width), dtype=int) + + newlabel = 0 # the next label to be assigned, incremented first + for row in range(height): + # print(row) + for col in range(width): + + ## assign a label based on already labelled neighbours to + ## west or row above that have the same color + curpix = im[row, col] + curlab = UNKNOWN + if col == 0: + # if pix is the first pixel in the row, then the label is the same as the pixel above + if curpix == im[row - 1, col]: + curlab = limage[row - 1, col] # inherit label from the north + else: + # if pix is the same as the W pixel, then the label is the same + if curpix == im[row, col - 1]: + curlab = limage[row, col - 1] # inherit label from the west + elif row > 0 and curpix == im[row - 1, col]: + curlab = lmap[limage[row - 1, col]] + # ) # inherit label from the north + # add 8-way stuff here + + if curlab == UNKNOWN: + # current label is not inherited from a neighbour, assign a new label + newlabel += 1 # assign new label + curlab = newlabel + color[curlab] = curpix # set blob color to current pixel + epoint[curlab] = None # no enclosure point yet + blobsize[curlab] = 0 # no pixels in blob yet + lmap[curlab] = curlab # map new label to itself + + # check if a blob merge is required or an enclosure has occurred + # these events can only occur on the second row or later + if row > 0: + if im[row - 1, col] == curpix: + # the current pixel is the same as the N pixel + if lmap[limage[row - 1, col]] != curlab: + # but the label is different, we have a merge + curlab = merge(curlab, limage[row - 1, col]) + + elif im[row, col - 1] == curpix and im[row - 1, col - 1] != curpix: + # the current pixel is the same as the N and W pixel, but + # different to the NW pixel, so the NW pixel represents a blob + # that has been enclosed. + + # print(f"enclosure at ({row}, {col})") + parent[limage[row - 1, col - 1]] = curlab + epoint[limage[row - 1, col - 1]] = ( + row - 1, + col - 1, + ) + elif connectivity == 8: + # for 8-way connectivity, if the pixel above and to the left is the same as the current pixel, but the label is different + + if ( + col > 0 + and im[row - 1, col - 1] == curpix + and lmap[limage[row - 1, col - 1]] != curlab + ): + # we have a merge to the NW + curlab = merge(curlab, limage[row - 1, col - 1]) + # for 8-way connectivity, if the pixel above and to the right is the same as the current pixel, but the label is different + elif ( + col < (width - 1) + and im[row - 1, col + 1] == curpix + and lmap[limage[row - 1, col + 1]] != curlab + ): + # we have a merge to the NE + curlab = merge(curlab, limage[row - 1, col + 1]) + + blobsize[curlab] += 1 # bump the blob size by 1 + limage[row, col] = curlab # stash label in label image + # prevlab = curlab + + tc = time.time() + print(f"connectivity: {tc - tstart:.2f}") + # print(tc - tstart) + + # create a mapping from unique (after redirection) labels to sequential numbers + # starting from zero + lmap2 = {} + # ulabels = set(lmap.values()) # all the label redirection targets + ulabels = list(np.unique(lmap)) + ulabels.remove(0) + for i, u in enumerate(ulabels): + lmap2[u] = i + # print(lmap2) + + # create a mapping from labels to sequential numbers starting from zero + # lmap3 = {old: lmap2[new] for (old, new) in lmap.items()} + lmap3 = {old: lmap2[lmap[old]] for old in range(1, newlabel + 1)} + # print(lmap3) + + # apply the mapping to the label image + limage2 = np.vectorize(lambda oldlabel: lmap3[oldlabel])(limage) + # print(limage2) + + # apply the mapping to the keys of the other dicts + parent = {lmap3[child]: lmap3[parent] for (child, parent) in parent.items()} + color = { + lmap3[oldlabel]: color + for (oldlabel, color) in color.items() + if oldlabel in lmap2 + } + epoint = { + lmap3[oldlabel]: edge + for (oldlabel, edge) in epoint.items() + if oldlabel in lmap2 + } + blobsize = { + lmap3[oldlabel]: size + for (oldlabel, size) in blobsize.items() + if oldlabel in lmap2 + } + + tf = time.time() + print(f"total: {tf - tstart:.2f}, tc: {tc - tstart:.2f}, tf: {tf - tc:.2f}") + print(f"newlabel: {newlabel}") + # print(tf - tstart) + return len(lmap2), limage2, parent, blobsize, epoint, color diff --git a/ilabel.pyx b/ilabel.pyx new file mode 100644 index 0000000..dc2592b --- /dev/null +++ b/ilabel.pyx @@ -0,0 +1,179 @@ +import numpy +cimport numpy + +# ilabel.c from MVTB for MATLAB converted to Python by CoPilot +# +# Copyright (C) 1995-2009, by Peter I. Corke + + +cdef unsigned int UNKNOWN = 0 +import time + + +# def ilabel(numpy.ndarray[numpy.uint8_t, ndim=2] im, connectivity=4): +def ilabel(im, int connectivity=4): + + + print("*****ilabel.pyx") + + def merge(int label1, int label2) -> int: + + # merge label1 and label2 (maybe indirected) + # print(f"merge: {label1}, {label2}") + label2 = LMAP[label2] + # choose which label to keep + if label1 > label2: + label1, label2 = label2, label1 + # label1 dominates + # print( + # f"merge ({row}, {col}): {label2}({blobsize[label2]}) -> {label1}({blobsize[label1]})" + # ) + # print(limage) + # print(lmap) + # print() + LMAP[label2] = label1 + blobsize[label1] += blobsize[ + label2 + ] # dominant blob absorbs pixels from the other + + return label1 + + tstart = time.time() + + cdef int height, width, row, col + cdef unsigned int newlabel, curlab + #cdef int curpix + + height, width = im.shape + + # LMAP = {} # map old labels to new labels + LMAP = numpy.zeros((20_000,), dtype=int) + blobsize = {} # size of each blob + parent = {} # parent of each blob + color = {} # color of each blob + epoint = {} # the enclosure point of each blob, guaranteed on the boundary + + # create the label image, initially all zeros (ie. no labels) + # note that performance is much worse for uint16 or int32 types + cdef numpy.ndarray[numpy.int_t, ndim=2] limage = numpy.zeros((height, width), dtype=int) + + newlabel = 0 # the next label to be assigned, incremented first + for row in range(height): + for col in range(width): + + ## assign a label based on already labelled neighbours to + ## west or row above that have the same color + curpix = im[row, col] + curlab = UNKNOWN + if col == 0: + # if pix is the first pixel in the row, then the label is the same as the pixel above + if curpix == im[row - 1, col]: + curlab = limage[row - 1, col] # inherit label from the north + else: + # if pix is the same as the W pixel, then the label is the same + if curpix == im[row, col - 1]: + curlab = limage[row, col - 1] # inherit label from the west + elif row > 0 and curpix == im[row - 1, col]: + curlab = LMAP[limage[row - 1, col]] + # ) # inherit label from the north + # add 8-way stuff here + + if curlab == UNKNOWN: + # current label is not inherited from a neighbour, assign a new label + newlabel += 1 # assign new label + curlab = newlabel + color[curlab] = curpix # set blob color to current pixel + epoint[curlab] = None # no enclosure point yet + blobsize[curlab] = 0 # no pixels in blob yet + LMAP[curlab] = curlab # map new label to itself + + # check if a blob merge is required or an enclosure has occurred + # these events can only occur on the second row or later + if row > 0: + if im[row - 1, col] == curpix: + # the current pixel is the same as the N pixel + if LMAP[limage[row - 1, col]] != curlab: + # but the label is different, we have a merge + curlab = merge(curlab, limage[row - 1, col]) + + elif im[row, col - 1] == curpix and im[row - 1, col - 1] != curpix: + # the current pixel is the same as the N and W pixel, but + # different to the NW pixel, so the NW pixel represents a blob + # that has been enclosed. + + # print(f"enclosure at ({row}, {col})") + parent[limage[row - 1, col - 1]] = curlab + epoint[limage[row - 1, col - 1]] = ( + row - 1, + col - 1, + ) + elif connectivity == 8: + # for 8-way connectivity, if the pixel above and to the left is the same as the current pixel, but the label is different + + if ( + col > 0 + and im[row - 1, col - 1] == curpix + and LMAP[limage[row - 1, col - 1]] != curlab + ): + # we have a merge to the NW + curlab = merge(curlab, limage[row - 1, col - 1]) + # for 8-way connectivity, if the pixel above and to the right is the same as the current pixel, but the label is different + elif ( + col < (width - 1) + and im[row - 1, col + 1] == curpix + and LMAP[limage[row - 1, col + 1]] != curlab + ): + # we have a merge to the NE + curlab = merge(curlab, limage[row - 1, col + 1]) + + blobsize[curlab] += 1 # bump the blob size by 1 + limage[row, col] = curlab # stash label in label image + # prevlab = curlab + + tc = time.time() + print(f"connectivity: {tc - tstart:.2f}") + # print(tc - tstart) + + # create a mapping from unique (after redirection) labels to sequential numbers + # starting from zero + lmap2 = {} + #ulabels = set(LMAP.values()) # all the label redirection targets + ulabels = list(numpy.unique(LMAP)) + ulabels.remove(0) + for i, u in enumerate(ulabels): + lmap2[u] = i + # print(lmap2) + + # create a mapping from labels to sequential numbers starting from zero + #lmap3 = {old: lmap2[new] for (old, new) in LMAP.items()} + lmap3 = {old: lmap2[LMAP[old]] for old in range(1, newlabel + 1)} + + # print(lmap3) + + # apply the mapping to the label image + limage2 = numpy.vectorize(lambda oldlabel: lmap3[oldlabel])(limage) + # print(limage2) + + # apply the mapping to the keys of the other dicts + parent = {lmap3[child]: lmap3[parent] for (child, parent) in parent.items()} + color = { + lmap3[oldlabel]: color + for (oldlabel, color) in color.items() + if oldlabel in lmap2 + } + epoint = { + lmap3[oldlabel]: edge + for (oldlabel, edge) in epoint.items() + if oldlabel in lmap2 + } + blobsize = { + lmap3[oldlabel]: size + for (oldlabel, size) in blobsize.items() + if oldlabel in lmap2 + } + + tf = time.time() + print(f"total: {tf - tstart:.2f}, tc: {tc - tstart:.2f}, tf: {tf - tc:.2f}") + print(f"newlabel: {newlabel}") + # print(tf - tstart) + return len(lmap2), limage2, parent, blobsize, epoint, color diff --git a/ilabeltest.py b/ilabeltest.py new file mode 100644 index 0000000..9bbfc8a --- /dev/null +++ b/ilabeltest.py @@ -0,0 +1,153 @@ +import numpy as np +from machinevisiontoolbox import Image + +# from numba import jit +import cProfile + +# ilabel.c from MVTB for MATLAB converted to Python by CoPilot +# +# Copyright (C) 1995-2009, by Peter I. Corke + +from ilabel import ilabel + +UNKNOWN = 0 +import time + + +def pprint(array, before=None, width=1): + """Pretty print a small image array + + :param array: _description_ + :type array: _type_ + :param before: _description_, defaults to None + :type before: _type_, optional + :param width: _description_, defaults to 1 + :type width: int, optional + :return: multiline string containing formatted array + :rtype: str + + .. runblock:: pycon + + >>> import numpy as np + >>> A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> print(showarray(A)) + >>> s2 = showarray(2*A, width=2) + >>> print(showarray(A, before=s2)) + + :seealso: :func:`Image.showpixels` :func:`Image` + """ + + # make the pixel value format string based on the width of the value + fmt = f" {{:{width}d}}" + + # add the header rows, which indicate the column number. 2-digit column + # numbers are shown with the digits one above the other. + s10 = " " * 5 + s1 = " " * 5 + sep = " " * 5 + s = "" + for col in range(array.shape[1]): + if col // 10 == 0: + s10 += " " * (width + 1) + else: + s10 += fmt.format(col // 10) + s1 += fmt.format(col % 10) + sep += " " * width + "-" + s = s10 + "\n" + s1 + "\n" + sep + "\n" + + # add the pixel values, row by row + for row in range(array.shape[0]): + s += f"{row:3d}: " + for col in range(array.shape[1]): + s += fmt.format(array[row, col]) + s += "\n" + + # if there is a before image, then join the two images side by side + # this assumes that the images have the same number of rows + if before is not None: + # horizontal join + return "\n".join( + [x + " |" + y[5:] for x, y in zip(before.splitlines(), s.splitlines())] + ) + else: + return s + + +# im = Image( +# r""" +# .......... +# .......... +# .......... +# ....##.... +# ....##.... +# .......... +# .......... +# .......... +# .......... +# .......... +# """, +# binary=True, +# ) +# im = Image( +# r""" +# ..#....... +# .......... +# .......... +# ...####... +# ...#..#... +# ...####... +# .......... +# .......... +# .......... +# .......... +# """, +# binary=True, +# ) +# im = Image( +# r""" +# .......... +# .......... +# .#.#.#.#.. +# .#.#.#.#.. +# .#.#.#.#.. +# .########. +# ..#.#.#.#. +# ..#.#.#.#. +# .......... +# .......... +# """, +# binary=True, +# ) +# im = Image( +# r""" +# ............ +# ............ +# .########... +# ..#......... +# ..#......... +# ............ +# ............ +# """, +# binary=True, +# ) +# z = False +# print(f"{'T' if z else 'F'}") +# fmt = "{'T' if z else 'F'}" +# fmt.format(True) +# aa = pprint(im.A) +im = Image.Read("multiblobs.png") +# time execution + +nlabels, limage, parent, blobsize, edge, color = ilabel(im.A, 4) + + +print(f"{nlabels} labels") +# print(pprint(limage, before=aa)) +# Image(limage).disp(block=True) +print(f"parents: {parent}") +print(f"blobsize: {blobsize}") +print(im.size, sum(parent.values())) +print(f"edge: {edge}") +print(f"color: {color}") + +# cProfile.run("ilabel(im.A, 4, 0)") diff --git a/machinevisiontoolbox/ImageBlobs.py b/machinevisiontoolbox/ImageBlobs.py index f8d3a24..3c3594f 100644 --- a/machinevisiontoolbox/ImageBlobs.py +++ b/machinevisiontoolbox/ImageBlobs.py @@ -25,6 +25,7 @@ rng.seed(13543) # would this be called every time at Blobs init? import matplotlib.pyplot as plt + # decorators def scalar_result(func): def innerfunc(*args): @@ -113,7 +114,7 @@ class Blobs(UserList): # lgtm[py/missing-equals] _image = [] # keep image saved for each Blobs object - def __init__(self, image=None, **kwargs): + def __init__(self, image=None, perimeter=True, background=False, **kwargs): """ Find blobs and compute their attributes @@ -172,31 +173,45 @@ def __init__(self, image=None, **kwargs): return self._image = image # keep reference to original image + image = image.mono().to_int() # convert to single channel uint8 for OpenCV - image = image.mono() - - # get all the contours - contours, hierarchy = cv.findContours( - image.to_int(), mode=cv.RETR_TREE, method=cv.CHAIN_APPROX_NONE + # compute the label image + retval, labels = cv.connectedComponents( + image=im, connectivity=4, ltype=cv.CV_32S ) - self._hierarchy_raw = hierarchy - self._contours_raw = contours + if perimeter: + # get all the contours + contours, hierarchy = cv.findContours( + image.to_int(), mode=cv.RETR_TREE, method=cv.CHAIN_APPROX_NONE + ) - # change hierarchy from a (1,M,4) to (M,4) - # the elements of each row are: - # 0: index of next contour at same level, - # 1: index of previous contour at same level, - # 2: index of first child, - # 3: index of parent - hierarchy = hierarchy[0, :, :] # drop the first singleton dimension - parents = hierarchy[:, 3] + self._hierarchy_raw = hierarchy + self._contours_raw = contours - # change contours to list of 2xN arraay - contours = [c[:, 0, :] for c in contours] + # change hierarchy from a (1,M,4) to (M,4) + # the elements of each row are: + # 0: index of next contour at same level, + # 1: index of previous contour at same level, + # 2: index of first child, + # 3: index of parent + hierarchy = hierarchy[0, :, :] # drop the first singleton dimension + parents = hierarchy[:, 3] + + # change contours to list of 2xN arrays + contours = [c[:, 0, :].T for c in contours] ## first pass: moments, children, bbox + for label in range(retval): + blob = Blob() + blob.id = label + label_mask = (labels == label).astype("uint8") + M = cv.moments(label_mask, True) + # convert dict to named tuple, easier to access using dot notation + blob.moments = _moment_tuple._make([M[field] for field in _moment_tuple._fields]) + + if perimeter: for i, (contour, hier) in enumerate(zip(contours, hierarchy)): blob = Blob() @@ -204,8 +219,8 @@ def __init__(self, image=None, **kwargs): ## bounding box: umin, vmin, width, height u1, v1, w, h = cv.boundingRect(contour) - u2 = u1 + w - v2 = v1 + h + u2 = u1 + w - 1 + v2 = v1 + h - 1 blob.bbox = np.r_[u1, u2, v1, v2] blob.touch = u1 == 0 or v1 == 0 or u2 == image.umax or v2 == image.vmax @@ -259,9 +274,7 @@ def __init__(self, image=None, **kwargs): # subtract moments of the child M = {key: M[key] - self.data[child].moments[key] for key in M} - # convert dict to named tuple, easier to access using dot notation - M = _moment_tuple._make([M[field] for field in _moment_tuple._fields]) - blob.moments = M + ## centroid blob.uc = M.m10 / M.m00 diff --git a/machinevisiontoolbox/ImageCore.py b/machinevisiontoolbox/ImageCore.py index ee8d795..8f104ad 100644 --- a/machinevisiontoolbox/ImageCore.py +++ b/machinevisiontoolbox/ImageCore.py @@ -35,7 +35,8 @@ from machinevisiontoolbox.ImageColor import ImageColorMixin from machinevisiontoolbox.ImageReshape import ImageReshapeMixin from machinevisiontoolbox.ImageWholeFeatures import ImageWholeFeaturesMixin -from machinevisiontoolbox.ImageBlobs import ImageBlobsMixin + +# from machinevisiontoolbox.ImageBlobs import ImageBlobsMixin from machinevisiontoolbox.ImageRegionFeatures import ImageRegionFeaturesMixin from machinevisiontoolbox.ImageLineFeatures import ImageLineFeaturesMixin from machinevisiontoolbox.ImagePointFeatures import ImagePointFeaturesMixin @@ -55,7 +56,7 @@ class Image( ImageSpatialMixin, ImageColorMixin, ImageReshapeMixin, - ImageBlobsMixin, + # ImageBlobsMixin, ImageWholeFeaturesMixin, ImageRegionFeaturesMixin, ImageLineFeaturesMixin, @@ -256,7 +257,6 @@ def __init__( for row in image.split("\n"): row = row.strip() if len(row) > 0: - print(row) img.append([0 if c in zeros else ord(c) for c in row]) try: image = np.array(img, dtype="uint8") diff --git a/newlabel.c b/newlabel.c new file mode 100644 index 0000000..dcb1b1c --- /dev/null +++ b/newlabel.c @@ -0,0 +1,106 @@ +/* create a Python extension to compute connectivity for a multii-leel image*/ +#include +#include +#include +#include + +#define IM(offset) (image[offset]) +#define LI(offset) (labelimage[offset]) +#define LIR(offset) (lmap[labelimage[offset]]) + +#define UNKNOWN 0 + +static unsigned int *lmap; +static unsigned int *point; +static unsigned int *label; +static unsigned int *color +static unsigned int *parent + +static inline merge(int a, int b) { + if (a < b) { + _ = a; a = b; b = _; + } +} + +// Function to compute connectivity +static PyObject *connectivity(PyObject *self, PyObject *args) { + PyArrayObject *input; + + + // Parse the input tuple + if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &input)) { + return NULL; + } + + // get pointer to the data + lmap = (unsigned int *)PyArray_DATA(input); + // get width of input image + unsigned int width = PyArray_DIM(input, 1); + // get height of input image + unsigned int height = PyArray_DIM(input, 0); + + // create a new array to store the label image + npy_intp dims[2] = {height, width}; + PyArrayObject *labelimage = (PyArrayObject *)PyArray_SimpleNew(2, dims, NPY_UINT32); + unsigned int *label = (unsigned int *)PyArray_DATA(labelimage); + + + N = -height; + NW = -height - 1; + NE = -height + 1; + W = -1; + C = 0; + maxlabel = 0; + buflen = 10000; + point = calloc(buflen, sizeof(int)); + color = calloc(buflen, sizeof(int)); + parent = calloc(buflen, sizeof(int)); + + for (row=0; row= buflen) { + // need to extend the buffers + buflen += 10000; + point = realloc(point, buflen*sizeof(int)); + color = realloc(color, buflen*sizeof(int)); + parent = realloc(parent, buflen*sizeof(int)); + } + point[curlabel] = row * height + col; // index of initial point + color[curlabel] = IM[C]; + parent[curlabel] = curlabel; // parent of a node is itself + maxlabel += 1; + } + offset = row*width + col; + if (LI(offset) != 0) { + if (row > 0) { + if (col > 0) { + if (LI(offset + NW) != 0) { + LIR(offset) = LIR(offset + NW); + } else { + LIR(offset) = LI(offset); + } + } else { + LIR(offset) = LI(offset); + } + } else { + LIR(offset) = LI(offset); + } + } + } + } + + diff --git a/ocv-test.py b/ocv-test.py new file mode 100644 index 0000000..962d52c --- /dev/null +++ b/ocv-test.py @@ -0,0 +1,303 @@ +import numpy as np +from machinevisiontoolbox import Image + + +def fmtarrays(*arrays, widths=1, arraysep=" |", labels=None): + """Pretty print small arrays + + :param array: one or more arrays to be formatted horizontally concantenated + :type array: Numpy array + :param width: number of digits for the formatted array elements, defaults to 1 + :type width: int, optional + :param arraysep: separator between arrays, defaults to " |" + :type arraysep: str, optional + :param labels: list of labels for each array, defaults to None + :type labels: list of str, optional + :return: multiline string containing formatted arrays + :rtype: str + :raises ValueError: if the arrays have different numbers of rows + + For image processing this is useful for displaying small test images. + + The arrays are formatted and concatenated horizontally with a vertical separator. + Each array has a header row that indicates the column number. Each row has a + header column that indicates the row number. + + .. runblock:: pycon + + >>> import numpy as np + >>> rng = np.random.default_rng() + >>> A = rng.integers(low=0, high=9, size=(5,5) + >>> print(fmtarrays(A)) + >>> print(fmtarrays(A, width=2)) + >>> B = rng.integers(low=0, high=9, size=(5,5) + >>> print(fmtarrays(A, B)) + >>> print(fmtarrays(A, B, labels=("A:", "B:"))) + + The number of rows in each array must be the same, but the number of columns can + vary. + + :seealso: :func:`Image.showpixels` :func:`Image` + """ + + # check that all arrays have the same number of rows + if len(set([array.shape[0] for array in arrays])) != 1: + raise ValueError("All arrays must have the same number of rows") + + if isinstance(widths, int): + widths = [widths] * len(arrays) + + # add the header rows, which indicate the column number. 2-digit column + # numbers are shown with the digits one above the other. + stitle = "" # array title row + s10 = " " * 5 # array column number, 10s digit + s1 = " " * 5 # array column number, 1s digit + divider = " " * 5 # divider between column number header and array values + s = "" + tens = False # has a tens row + for i, array in enumerate(arrays): # iterate over the input arrays + width = widths[i] + # make the pixel value format string based on the width of the value + fmt = f" {{:{width}d}}" + + # build the title row + if labels is not None: + stitle += " " * (len(s10) - len(stitle)) + labels[i] + + # build the column number header rows + for col in range(array.shape[1]): + # the 10s digits + if col // 10 == 0: + s10 += " " * (width + 1) + else: + s10 += fmt.format(col // 10) + tens = True + # the 1s digits + s1 += fmt.format(col % 10) + divider += " " * width + "-" + + s10 += " " * len(arraysep) + s1 += " " * len(arraysep) + divider += " " * len(arraysep) + + # concatenate the header rows + s = stitle + "\n" + if tens: + s += s10 + "\n" # only include if there are 10s digits + s += s1 + "\n" + divider + "\n" + + # add the element values, row by row + for row in range(array.shape[0]): + # add the row number + s += f"{row:3d}: " + + # for each array, add the elements for this row + for array, width in zip(arrays, widths): + # make the pixel value format string based on the width of the value + fmt = f" {{:{width}d}}" + for col in range(array.shape[1]): + s += fmt.format(array[row, col]) + if array is not arrays[-1]: + s += arraysep + s += "\n" + + return s + + +def fmtarray(array, width=1, label=None): + """Format a 2D array as a string with a left-hand label + + :param array: array to format + :type array: numpy array + :param width: number of digits for the formatted array elements, defaults to 1 + :type width: int, optional + :param label: labels for array, defaults to None + :type label: str, optional + :return: formatted array as a string + :rtype: str + + .. runblock:: pycon + + >>> import numpy as np + >>> rng = np.random.default_rng() + >>> A = rng.integers(low=0, high=9, size=(5,5) + >>> print(fmtarray(A, "A:")) + """ + # make the pixel value format string based on the width of the value + fmt = f" {{:{width}d}}" + s = "" + + # add the element values, row by row + labels = label.split("\n") + longest = max([len(label) for label in labels]) + for row in range(array.shape[0]): + if row < len(labels): + s += labels[row] + " " * (longest - len(labels[row])) + "| " + else: + s += " " * longest + "| " + # for each array, add the elements for this row + for col in range(array.shape[1]): + s += fmt.format(array[row, col]) + + if row < array.shape[0] - 1: + s += "\n" + + return s + + +# im = Image( +# r""" +# .......... +# .......... +# .#####.... +# ...###.... +# ...######. +# .......... +# .......... +# """, +# binary=True, +# ) +# im = Image( +# r""" +# .......... +# ....#..... +# ....#..... +# ....#..... +# .#######.. +# ....#..... +# ....#..... +# ....#..... +# .......... +# """, +# binary=True, +# ) +# im = Image( +# r""" +# ########## +# ########.# +# #######.## +# ######.### +# ##.##.#### +# ###.###### +# ####.##### +# #####.#### +# ########## + +# """, +# binary=True, +# ) +# im = Image( +# r""" +# #.#.#. +# .#.#.# +# #.#.#. +# .#.#.# +# """, +# binary=True, +# ) +im = Image( + r""" + .......... + ....##.... + ....##.... + .......... + .......... + ..######.. + .......... + + """, + binary=True, +) + +# im = Image( +# r""" +# .......... +# .......... +# ...###.... +# ...###.... +# ...###.... +# .......... +# .......... +# """, +# binary=True, +# ) +# im = Image( +# r""" +# ..#....... +# ....###... +# .#........ +# ...####... +# ...#..#... +# ...####... +# .......... +# .#......#. +# ..#....#.. +# ...#..#... +# .......... +# """, +# binary=True, +# ) +# im = Image( +# r""" +# .......... +# .......... +# .#.#.#.#.. +# .#.#.#.#.. +# .#.#.#.#.. +# .########. +# ..#.#.#.#. +# ..#.#.#.#. +# .......... +# .......... +# """, +# binary=True, +# ) +# im = Image( +# r""" +# ............ +# ............ +# .########... +# ..#......... +# ..#......... +# ............ +# ............ +# """, +# binary=True, +# ) +# z = False +# print(f"{'T' if z else 'F'}") +# fmt = "{'T' if z else 'F'}" +# fmt.format(True) +# aa = pprint(im.A) + + +import cv2 as cv + +contours, hierarchy = cv.findContours( + im.to_int(), mode=cv.RETR_EXTERNAL, method=cv.CHAIN_APPROX_NONE +) +retval, labels = cv.connectedComponentsWithAlgorithm( + im.to_int(), connectivity=8, ltype=cv.CV_32S, ccltype=cv.CCL_BBDT +) +perim = np.zeros(im.shape, dtype="uint8") +for i in range(len(contours)): + cv.drawContours(perim, contours, i, i + 1, 1) +# print(fmtarrays(im.A, labels, perim, labels=("image:", "labels:", "perim:"))) +print( + fmtarrays( + im.A, + labels, + perim, + widths=(1, 1, 1), + labels=("image:", "labels:", "contour idx+1:"), + ) +) + +for i, contour in enumerate(contours): + m = cv.moments(contour) + print( + fmtarray( + np.squeeze(contour, axis=1).T, label=f"contour {i}\nm00 = {m['m00']:.0f}" + ) + ) + print() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..942e871 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +# setup.py + +from distutils.core import setup +from Cython.Build import cythonize +import numpy + +setup( + ext_modules=cythonize("ilabel.pyx", compiler_directives={"language_level": "3"}), + include_dirs=[numpy.get_include()], +) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy