I have a dicom image but the image is padded. I have code to remove the padding from the image so that only the scan is left but I have to open the image using ImageJ and manually find min and max values for the x and y axis for where the image starts and ends. The scan has a gray value range of -3000 to 2000. The padded area has a value of 0. Is there a way to find these min and max values without having do it manually?
Original Image:
Desired Image:
Below a Python script using SimpleITK that crops out the background.
The basic idea is that it creates a mask image of pixels that are not the background value. Then it uses SimpleITK's LabelShapeStatisticsImageFilter to find the bounding box for the non-zero pixels in that mask image.
import SimpleITK as sitk
img = sitk.ReadImage("padded-image.png")
# Grey background in this example
bg_value = 161
# Create a mask image that is just non-background pixels
fg_mask = (img != bg_value)
# Compute shape statistics on the mask
lsif = sitk.LabelShapeStatisticsImageFilter()
lsif.Execute(fg_mask)
# Get the bounds of the mask.
# Bounds are given as [Xstart, Ystart, Xwidth, Ywidth]
bounds = lsif.GetBoundingBox(1)
print(bounds)
Xmin_crop = bounds[0]
Ymin_crop = bounds[1]
Xmax_crop = img.GetWidth() - (bounds[0]+bounds[2])
Ymax_crop = img.GetHeight() - (bounds[1]+bounds[3])
# Crop parameters are how much to crop off each side
cropped_img = sitk.Crop(img, [Xmin_crop, Ymin_crop], [Xmax_crop, Ymax_crop])
sitk.Show(cropped_img)
sitk.WriteImage(cropped_img, "cropped-image.png")
Because I used your 8-bit PNG image, the background value is set to 161. If you use your original 16-bit DICOM CT, you'd use a background value of 0. SimpleITK can read DICOM, along with a number of other image formats.
For more info about the LabelShapeStatisticsImageFilter class, here's the documentation: https://simpleitk.org/doxygen/latest/html/classitk_1_1simple_1_1LabelShapeStatisticsImageFilter.html#details
Here is an alternate way in Python/OpenCV using color thresholding and contours to find the bounding box.
Input:
import cv2
import numpy as np
# read image
img = cv2.imread('scan.png')
# threshold on gray color (161,161,161)
lower = (161,161,161)
upper = (161,161,161)
thresh = cv2.inRange(img, lower, upper)
# invert threshold image so border is black and center box is white
thresh = 255 - thresh
# get external contours (presumably just one)
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
cntr = contours[0]
x,y,w,h = cv2.boundingRect(cntr)
# crop to bounding rectangle
crop = img[y:y+h, x:x+w]
# save cropped image
cv2.imwrite('scan_thresh.png',thresh)
cv2.imwrite('scan_crop.png',crop)
cv2.imshow("THRESH", thresh)
cv2.imshow("CROP", crop)
cv2.waitKey(0)
cv2.destroyAllWindows()
Thresholded image:
Cropped Result:
Without having to resort to something as complex (and large/slow to import) as SITK or CV with complicated image analysis - you can do it easily just using numpy.
That's going to be a lot faster and reliable IMHO:
# if a is your image:
same_cols = np.all(a == a[0, :], axis=0)
same_cols_index = np.where(same_cols==False)[0]
C0,C1 = same_cols_index[0], same_cols_index[-1] + 1
same_rows = np.all(a == a[:, 0], axis=1)
same_rows_index = np.where(same_rows==False)[0]
R0,R1 = same_rows_index[0], same_rows_index[-1] + 1
print('rows', R0, R1)
print('cols', C0, C1)
a_snipped = a[R0:R1, C0:C1]
The logic here is
Find all rows and columns that have all values the same as the first row or column. You could replace that to be all rows/cols with value == 0 if you want
Get the indexes of the rows/columns from (1) where they are not all the same (ie == False)
Get the first and last index where they aren't all the same
Use the row/column first and last indicies to get the corresponding slice of your array (note you need to add 1 to the last index to include it in the slice)
Example
# make a sample image
a = np.zeros((512,512), dtype=np.int32)
r0, r1 = 53, 421
c0, c1 = 43, 470
rnd = np.random.randint(-3000, 2000, (r1-r0, c1-c0))
a[r0:r1, c0:c1] = rnd
plt.imshow(a, cmap='gray', vmin=-50, vmax=50)
same_cols = np.all(a == a[0, :], axis=0)
same_cols_index = np.where(same_cols==False)[0]
C0,C1 = same_cols_index[0], same_cols_index[-1] + 1
same_rows = np.all(a == a[:, 0], axis=1)
same_rows_index = np.where(same_rows==False)[0]
R0,R1 = same_rows_index[0], same_rows_index[-1] + 1
print('rows', R0, R1)
print('cols', C0, C1)
a_snipped = a[R0:R1, C0:C1]
plt.imshow(a_snipped, cmap='gray', vmin=-3000, vmax=2000)
rows 53 421
cols 43 470
Related
I have many x-ray scans and need to crop the scanned object from its background noise.
The files are in .png format and I am planning to use OpenCV Python for this task. I have seen some works with FindContours() but unsure that thresholding will work for this case.
Before Image:
After/Cropped Image:
Any suggested solution/code is appreciated.
Here is one way to do that in Python/OpenCV. It assumes you have the same excess border in all your images so that one can sort contours by area and skip the largest contour to get the second largest one.
Input:
import cv2
import numpy as np
# load image
img = cv2.imread("table_xray.jpg")
hh, ww = img.shape[:2]
# convert to gray
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# median filter
filt = cv2.medianBlur(gray, 15)
# threshold the filtered image and invert
thresh = cv2.threshold(filt, 64, 255, cv2.THRESH_BINARY)[1]
thresh = 255 - thresh
# find contours and store index with area in list
cntrs_info = []
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
index=0
for cntr in contours:
area = cv2.contourArea(cntr)
print(index, area)
cntrs_info.append((index,area))
index = index + 1
# sort contours by area
def takeSecond(elem):
return elem[1]
cntrs_info.sort(key=takeSecond, reverse=True)
# get bounding box of second largest contour skipping large border
index_second = cntrs_info[1][0]
x,y,w,h = cv2.boundingRect(contours[index_second])
print(index_second,x,y,w,h)
# crop input image
results = img[y:y+h,x:x+w]
# write result to disk
cv2.imwrite("table_xray_thresholded.png", thresh)
cv2.imwrite("table_xray_extracted.png", results)
cv2.imshow("THRESH", thresh)
cv2.imshow("RESULTS", results)
cv2.waitKey(0)
cv2.destroyAllWindows()
Filtered and Thresholded Image:
Cropped Result:
This is another possible solution. It uses the K-Channel of your input image, once converted to the CMYK color-space. The K (or Key) channel has most of the information of the black color, so it should be useful for segmenting the input image. After that, you can apply a heavy morphological chain to produce a good mask of the object. After that, cropping the object is very straightforward. Let's see the code:
# Imports
import cv2
import numpy as np
# Read image
imagePath = "D://opencvImages//"
inputImage = cv2.imread(imagePath+"jU6QA.jpg")
# Convert to float and divide by 255:
imgFloat = inputImage.astype(np.float) / 255.
# Calculate channel K:
kChannel = 1 - np.max(imgFloat, axis=2)
# Convert back to uint 8:
kChannel = (255*kChannel).astype(np.uint8)
The first bit of the program converts your image to the CMYK color-space and extracts the K channel. OpenCV has no direct conversion to this color-space, so a manual conversion is necessary. We need to be careful with the data types because there are float operations involved. The resulting image is this:
Pixels with black information are assigned an intensity close to 255. Now, let's threshold this image to get a binary mask. The threshold level is fixed:
# Threshold the image with a fixed thresh level
thresholdLevel = 200
_, binaryImage = cv2.threshold(kChannel, thresholdLevel, 255, cv2.THRESH_BINARY)
This produces the following binary image:
Alright. We need to isolate the object, however we have both the lines of the background and the "frame" around the image. Let's get rid of the lines first. We will apply a morphological Erosion. Then, we will remove the frame Flood-Filling with black color at two locations: upper left and bottom right of the image. After that, we will apply a Dilation to restore the object's original size. I wrapped these OpenCV functions inside custom functions that save me the typing of a couple of lines - These helper functions are presented at the end of the post. This is the approach:
# Perform Small Erosion:
binaryImage = morphoOperation(binaryImage, 3, 5, "Erode")
# Flood-Fill at two locations: Top left corner and bottom right:
(imageHeight, imageWidth) = binaryImage.shape[:2]
floodPositions = [(0, 0),(imageWidth-1, imageHeight-1)]
binaryImage = floodFill(binaryImage, floodPositions, 0)
# Perform Small Dilate:
binaryImage = morphoOperation(binaryImage, 3, 5, "Dilate")
This is the result:
Nice. We can improve the mask by applying a second morphological chain, this time with more iterations. Let's apply a Dilation to try and join the "holes" of the object, followed with a Erosion to, once again, restore the object's original size:
# Perform Big Dilate:
binaryImage = morphoOperation(binaryImage, 3, 10, "Dilate")
# Perform Big Erode:
binaryImage = morphoOperation(binaryImage, 3, 10, "Erode")
This yields the following result:
The gaps inside the object have been filled. Now, let's retrieve the contours on this mask to find the object's contour. I've additionally included an area filter. The mask is pretty clean by this point, so maybe this filter is not too necessary. Once the contour is located, we can crop the object from the original image:
# Find the contours on the binary image:
contours, hierarchy = cv2.findContours(binaryImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# BGR image for drawing results:
binaryBGR = cv2.cvtColor(binaryImage, cv2.COLOR_GRAY2BGR)
# Look for the outer bounding boxes (no children):
for _, c in enumerate(contours):
# Get blob area:
currentArea = cv2.contourArea(c)
# Set a min area value:
minArea = 10000
if minArea < currentArea:
# Get the contour's bounding rectangle:
boundRect = cv2.boundingRect(c)
# Get the dimensions of the bounding rect:
rectX = boundRect[0]
rectY = boundRect[1]
rectWidth = boundRect[2]
rectHeight = boundRect[3]
# Set bounding rect:
color = (0, 255, 0)
cv2.rectangle( binaryBGR, (int(rectX), int(rectY)),
(int(rectX + rectWidth), int(rectY + rectHeight)), color, 5 )
cv2.imshow("Rects", binaryBGR)
# Crop original input:
currentCrop = inputImage[rectY:rectY + rectHeight, rectX:rectX + rectWidth]
cv2.imshow("Cropped", currentCrop)
cv2.waitKey(0)
The last step produces the following two images. The first is the object enclosed by a rectangle, the second one is the actual crop:
I also tested the algorithm with your second image, these are the final results:
Wow. Somebody brought a gun to the airport? That's not OK. These are the helper functions used earlier. This first function performs the morphological operations:
def morphoOperation(binaryImage, kernelSize, opIterations, opString):
# Get the structuring element:
morphKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
# Perform Operation:
if opString == "Dilate":
op = cv2.MORPH_DILATE
else:
if opString == "Erode":
op = cv2.MORPH_ERODE
outImage = cv2.morphologyEx(binaryImage, op, morphKernel, None, None, opIterations,
cv2.BORDER_REFLECT101)
return outImage
The second function performs Flood-Filling given a list of seed-points:
def floodFill(binaryImage, positions, color):
# Loop thru the positions list of tuples:
for p in range(len(positions)):
currentSeed = positions[p]
x = int(currentSeed[0])
y = int(currentSeed[1])
# Apply flood-fill:
cv2.floodFill(binaryImage, mask=None, seedPoint=(x, y), newVal=(color))
return binaryImage
I'm trying to identify a list of objects that appeared newly in a photo. The plan is to get multiple cropped images from the original image and feed them to a neural network for object detection. Right now, I'm having trouble in extracting objects that appeared in a frame.
import cv2 as cv
import matplotlib.pyplot as plt
def mdisp(image):
plt.imshow(image)
plt.show()
im1 = cv.imread('images/litter-before.jpg')
mdisp(im1)
print(im1.shape)
im2 = cv.imread('images/litter-after.jpg')
mdisp(im2)
print(im2.shape)
backsub1=cv.createBackgroundSubtractorMOG2()
backsub2=cv.createBackgroundSubtractorKNN()
fgmask = backsub1.apply(im1)
fgmask = backsub1.apply(im2)
print(fgmask.shape)
mdisp(fgmask)
new_image = im2 * (fgmask[:,:,None].astype(im2.dtype))
mdisp(new_image)
Ideally, I would like to get a cropped picture of the item within red circle. How can I do it with OpenCv
Here's an approach, subtracting the two frames directly. The idea is that you first convert your images to grayscale, then blur a little bit to ignore the noise. Subtract the two frames, threshold the difference and look for the largest blob that is above a certain area threshold value.
Let's see:
import cv2
import numpy as np
# image path
path = "C:/opencvImages/"
fileName01 = "01.jpg"
fileName02 = "02.jpg"
# Read the2 images in default mode:
image01 = cv2.imread(path + fileName01)
image02 = cv2.imread(path + fileName02)
# Store a copy of the last frame for results drawing:
inputCopy = image02.copy()
# Convert RGB images to grayscale:
grayscaleImage01 = cv2.cvtColor(image01, cv2.COLOR_BGR2GRAY)
grayscaleImage02 = cv2.cvtColor(image02, cv2.COLOR_BGR2GRAY)
# Convert RGB images to grayscale:
filterSize = 5
imageMedian01 = cv2.medianBlur(grayscaleImage01, filterSize)
imageMedian02 = cv2.medianBlur(grayscaleImage02, filterSize)
Now you have the grayscale, blurred frames. Next, we need to calculate the difference between these frames. I don't wanna loose data, so I have to be careful with the data type here. Remember that these are grayscale, uint8 matrices, but the difference could potentially yield negative values. Let's convert the matrices to floats, take the difference, and convert this matrix to uint8:
# uint8 to float32 conversion:
imageMedian01 = imageMedian01.astype('float32')
imageMedian02 = imageMedian02.astype('float32')
# Take the difference and convert back to uint8
imageDifference = np.clip(imageMedian01 - imageMedian02, 0, 255)
imageDifference = imageDifference.astype('uint8')
This gives you the frames difference:
Let's threshold this to get a binary image. I'm using a threshold value of 127, as it is a the center of the 8-bit range:
threshValue = 127
_, binaryImage = cv2.threshold(imageDifference, threshValue, 255, cv2.THRESH_BINARY)
This is the binary image:
We are looking for the biggest blob here, let's find blob/contours and filter the small ones. Let's set a minimum area of 10 pixels:
# Perform an area filter on the binary blobs:
componentsNumber, labeledImage, componentStats, componentCentroids = \
cv2.connectedComponentsWithStats(binaryImage, connectivity=4)
# Set the minimum pixels for the area filter:
minArea = 10
# Get the indices/labels of the remaining components based on the area stat
# (skip the background component at index 0)
remainingComponentLabels = [i for i in range(1, componentsNumber) if componentStats[i][4] >= minArea]
# Filter the labeled pixels based on the remaining labels,
# assign pixel intensity to 255 (uint8) for the remaining pixels
filteredImage = np.where(np.isin(labeledImage, remainingComponentLabels) == True, 255, 0).astype('uint8')
# Find the big contours/blobs on the filtered image:
contours, hierarchy = cv2.findContours(filteredImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
contours_poly = [None] * len(contours)
boundRect = []
# Alright, just look for the outer bounding boxes:
for i, c in enumerate(contours):
if hierarchy[0][i][3] == -1:
contours_poly[i] = cv2.approxPolyDP(c, 3, True)
boundRect.append(cv2.boundingRect(contours_poly[i]))
# Draw the bounding boxes on the (copied) input image:
for i in range(len(boundRect)):
print(boundRect[i])
color = (0, 255, 0)
cv2.rectangle(inputCopy, (int(boundRect[i][0]), int(boundRect[i][1])), \
(int(boundRect[i][0] + boundRect[i][2]), int(boundRect[i][1] + boundRect[i][3])), color, 1)
Check out the results:
I'm doing a project with depth image. But I have problems with noise and failed pixel reading with my depth camera. There are some spots and contours (especially edges) that have zero value. How to just ignore this zero value and blend it with surrounding values?
I have tried dilation and erosion (morph image processing), but I still can't get the right combination. It indeed removed some of the noise, but I just need to get rid of zeros at all points
Image Example:
The zero value is the darkest blue (I'm using colormap)
To illustrate what I want to do, please refer to this poor paint drawing:
I want to get rid the black spot (for example black value is 0 or certain value), and blend it with its surround.
Yes, I'm able to localized the spot using np.where or the similar function, but I have no idea how to blend it. Maybe a filter to be applied? I need to do this in a stream, so I need a fairly fast process, maybe 10-20 fps will do. Thank you in advance!
Update :
Is there a way other than inpaint? I've looked for various inpaints, but I don't need as sophisticated as impainting. I just need to blend it with simple line, curve, or shape and 1D. I think inpaint is an overkill. Besides, I need them to be fast enough to be used for video stream 10-20 fps, or even better.
Here is one way to do that in Python/OpenCV.
Use median filtering to fill the holes.
Read the input
Convert to gray
Threshold to make a mask (spots are black)
Invert the mask (spots are white)
Find the largest spot contour perimeter from the inverted mask and use half of that value as a median filter size
Apply median filtering to the image
Apply the mask to the input
Apply the inverse mask to the median filtered image
Add the two together to form the result
Save the results
Input:
import cv2
import numpy as np
import math
# read image
img = cv2.imread('spots.png')
# convert to gray
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# threshold
mask = cv2.threshold(gray,0,255,cv2.THRESH_BINARY)[1]
# erode mask to make black regions slightly larger
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
mask = cv2.morphologyEx(mask, cv2.MORPH_ERODE, kernel)
# make mask 3 channel
mask = cv2.merge([mask,mask,mask])
# invert mask
mask_inv = 255 - mask
# get area of largest contour
contours = cv2.findContours(mask_inv[:,:,0], cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
contours = contours[0] if len(contours) == 2 else contours[1]
perimeter_max = 0
for c in contours:
perimeter = cv2.arcLength(c, True)
if perimeter > perimeter_max:
perimeter_max = perimeter
# approx radius from largest area
radius = int(perimeter_max/2) + 1
if radius % 2 == 0:
radius = radius + 1
print(radius)
# median filter input image
median = cv2.medianBlur(img, radius)
# apply mask to image
img_masked = cv2.bitwise_and(img, mask)
# apply inverse mask to median
median_masked = cv2.bitwise_and(median, mask_inv)
# add together
result = cv2.add(img_masked,median_masked)
# save results
cv2.imwrite('spots_mask.png', mask)
cv2.imwrite('spots_mask_inv.png', mask_inv)
cv2.imwrite('spots_median.png', median)
cv2.imwrite('spots_masked.png', img_masked)
cv2.imwrite('spots_median_masked.png', median_masked)
cv2.imwrite('spots_removed.png', result)
cv2.imshow('mask', mask)
cv2.imshow('mask_inv', mask_inv )
cv2.imshow('median', median)
cv2.imshow('img_masked', img_masked)
cv2.imshow('median_masked', median_masked)
cv2.imshow('result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
Threshold image as mask:
Inverted mask:
Median filtered image:
Masked image:
Masked median filtered image:
Result:
Perhaps using a NaN-adjusted Gaussian filter is good and fast enough? When you consider your zeros/black spots as NaNs, this approach also works for larger black areas.
# import modules
import matplotlib.pyplot as plt
import numpy as np
import skimage
import skimage.filters
# set seed
np.random.seed(42)
# create dummy image
# (smooth for more realisitc appearance)
size = 50
img = np.random.rand(size, size)
img = skimage.filters.gaussian(img, sigma=5)
# create dummy missing/NaN spots
mask = np.random.rand(size, size) < 0.02
img[mask] = np.nan
# define and apply NaN-adjusted Gaussian filter
# (https://stackoverflow.com/a/36307291/5350621)
def nangaussian(U, sigma=1, truncate=4.0):
V = U.copy()
V[np.isnan(U)] = 0
VV = skimage.filters.gaussian(V, sigma=sigma, truncate=truncate)
W = 0*U.copy()+1
W[np.isnan(U)] = 0
WW = skimage.filters.gaussian(W, sigma=sigma, truncate=truncate)
return VV/WW
smooth = nangaussian(img, sigma=1, truncate=4.0)
# do not smooth full image but only copy smoothed NaN spots
fill = img.copy()
fill[mask] = smooth[mask]
# plot results
vmin, vmax = np.nanmin(img), np.nanmax(img)
aspect = 'auto'
plt.subplot(121)
plt.title('original image (white = NaN)')
plt.imshow(img, aspect=aspect, vmin=vmin, vmax=vmax)
plt.axis('off')
plt.subplot(122)
plt.title('filled image')
plt.imshow(fill, aspect=aspect, vmin=vmin, vmax=vmax)
plt.axis('off')
I want to extract one or more areas of an image which are marked on the image with an arbitrary shape (the shape edges have always the same color - red). The marked areas all have about the same size (not excactly) and I would like to have the cut out rectangles to have the same result size.
Can you give me a hint how to do this? I guess one could use opencv for this task in python, but I'm not familiar with it, so thanks for your help!
edit: added an example image, the goal would be to extract the red areas by rectangles of the same size.
Here is one way to do that in Python/OpenCV.
Read the gray input with red shapes drawn
Threshold on the red color of the shapes
Apply morphology close to ensure the shapes are continous outlines with no gaps
Get the external contours and their bounding boxes
Compute the centers of each bounding box and save in list and also the maximum width and height of all the bounding boxes.
For each center and the maximum width and height, crop the input image and save
Input:
import cv2
import numpy as np
# read image
img = cv2.imread('monet_shapes.png')
# threshold on red regions
lowerBound = np.array([0, 0, 150]);
upperBound = np.array([100, 100, 255]);
thresh = cv2.inRange(img, lowerBound, upperBound);
# apply morphology to ensure regions are continuous outlines and no gaps
kernel = np.ones((9,9), np.uint8)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
# get external contours
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
wmax = 0
hmax = 0
# get bounding boxes and max width and max height from all boxes and centers
centers = []
for cntr in contours:
# get bounding boxes
x,y,w,h = cv2.boundingRect(cntr)
cx = x + w // 2
cy = y + h // 2
cent = [cx,cy]
centers.append(cent)
if w > wmax:
wmax = w
if h > hmax:
hmax = h
print(wmax,hmax)
# show thresh and result
cv2.imshow("thresh", thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()
# save threshold
cv2.imwrite("monet_shapes_thresh.png",thresh)
# crop bounding boxes of size maxw, maxh about centers and save
i = 1
for cent in centers:
cx = cent[0]
cy = cent[1]
box = img[cy-hmax//2:cy+hmax//2, cx-wmax//2:cx+wmax//2]
cv2.imwrite("blackbox_result_{0}.png".format(i),box)
i = i + 1
Threshold image:
Resulting 5 cropped regions:
I would like to crop the images like the one below using python's OpenCV library. The area of interest is inside the squiggly lines on the top and bottom, and the lines on the side. The problem is that every image is slightly different. This means that I need some automated way of cropping for the area of interest. I guess the top and the sides would be easy since you could just crop it by 10 pixels or so. But how can I crop out the bottom half of the image where the line is not straight? I have included this example image. The image that follows highlights in pink the area of the image that I am interested in keeping.
Here is one way using Python/OpenCV.
Read input
Get center point (assume it is inside the desired region)
Convert image to grayscale
Floodfill the gray image and set background to black
Get the largest contour and its bounding box
Draw the largest contour as filled on black background as mask
Apply the mask to the input image
Crop the masked input image
Input:
import cv2
import numpy as np
# load image and get dimensions
img = cv2.imread("odd_region.png")
hh, ww, cc = img.shape
# compute center of image (as integer)
wc = ww//2
hc = hh//2
# create grayscale copy of input as basis of mask
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# create zeros mask 2 pixels larger in each dimension
zeros = np.zeros([hh + 2, ww + 2], np.uint8)
# do floodfill at center of image as seed point
ffimg = cv2.floodFill(gray, zeros, (wc,hc), (255), (0), (0), flags=8)[1]
# set rest of ffimg to black
ffimg[ffimg!=255] = 0
# get contours, find largest and its bounding box
contours = cv2.findContours(ffimg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
area_thresh = 0
for cntr in contours:
area = cv2.contourArea(cntr)
if area > area_thresh:
area = area_thresh
outer_contour = cntr
x,y,w,h = cv2.boundingRect(outer_contour)
# draw the filled contour on a black image
mask = np.full([hh,ww,cc], (0,0,0), np.uint8)
cv2.drawContours(mask,[outer_contour],0,(255,255,255),thickness=cv2.FILLED)
# mask the input
masked_img = img.copy()
masked_img[mask == 0] = 0
#masked_img[mask != 0] = img[mask != 0]
# crop the bounding box region of the masked img
result = masked_img[y:y+h, x:x+w]
# draw the contour outline on a copy of result
result_outline = result.copy()
cv2.drawContours(result_outline,[outer_contour],0,(0,0,255),thickness=1,offset=(-x,-y))
# display it
cv2.imshow("img", img)
cv2.imshow("ffimg", ffimg)
cv2.imshow("mask", mask)
cv2.imshow("masked_img", masked_img)
cv2.imshow("result", result)
cv2.imshow("result_outline", result_outline)
cv2.waitKey(0)
cv2.destroyAllWindows()
# write result to disk
cv2.imwrite("odd_region_cropped.png", result)
cv2.imwrite("odd_region_cropped_outline.png", result_outline)
Result:
Result With Contour Drawn: