I have a large binary image (4k x 7k pix) from which I want to extract the entire yellow portion as a single rectangle. I tried binary erosion to even out features insi
As the image you provided includes distracting axes, and is the wrong colour and too small, I created as realistic a version as I could with ImageMagick like this in Terminal:
convert bbox.png -alpha off -crop 120x215+40+13 -colorspace gray -normalize -threshold 50% -scale 4200x7200\! bbox.png
The full-size version is 4200x7200.
I then wrote a numpy
-based version of bbox
as follows
#!/usr/local/bin/python3
import numpy as np
from PIL import Image
def bbox(image):
"""Find bounding box of image"""
# Project all columns into row same width as image
proj=np.any(image,axis=0)
# Find first non-zero value from Left
L=np.argmax(proj)
# And right
R=image.shape[1]-np.argmax(np.flipud(proj))-1
# Project all rows into column same height as image
proj=np.any(image,axis=1)
# Find first non-zero value from Top
T=np.argmax(proj)
# And Bottom
B=image.shape[0]-np.argmax(np.flipud(proj))-1
return T,L,B,R
image=np.array(Image.open("a.png").convert("L"))
print(bbox(image))
That runs in 5.3ms on my Mac. Just for fun, I threaded it and ran the horizontal projection and vertical projection on separate parallel threads and it came down to 3.6ms with the same results.
#!/usr/local/bin/python3
import numpy as np
from PIL import Image
import threading
import queue
def DoOneDim(image,axis,q):
"""Find bounding box of image"""
proj=np.any(image,axis=axis)
# Find first non-zero value
A=np.argmax(proj)
# And and last
B=image.shape[1-axis]-np.argmax(np.flipud(proj))-1
q.put({axis:(A,B)})
def bboxTh(image):
"""Threaded version of bbox() that does vertical and horizontal on their own theads"""
q = queue.Queue()
Hthread=threading.Thread(target=DoOneDim, args=(image,0,q))
Vthread=threading.Thread(target=DoOneDim, args=(image,1,q))
Hthread.start()
Vthread.start()
Hthread.join()
Vthread.join()
results=dict()
while not q.empty():
results.update(q.get())
return results
image=np.array(Image.open("a.png").convert("L"))
print(bboxTh(image))
The identified box looks likes this:
Because you are looking for a single bounding box, don't use regionprops
or any per-object function. This also makes it so that you don't need to try to make a single object out of all the yellow dots.
The simplest solution here is to walk over the image, and for each pixel, determine if it is "yellow enough" (whatever that means for your application). If so, add the pixel's coordinates to the running bounding box calculation.
The bounding box calculation is quite simple:
top_left = [1e9, 1e9]
bottom_right = [0, 0]
for ...:
# within your loop over the pixels, [x, y] are the current coordinates
top_left = [min(top_left[0], x), min(top_left[1], y)];
bottom_right = [max(bottom_right[0], x), max(bottom_right[1], y)];
There might be a way with skimage to do this without loops, but I don't know that package at all.