The image below has a green region that I'm looking to replace with any other image. It's not necessary for its perspective to match.
I've been able to create a mask. But haven't really been successful with resizing and aligning the other image to this one with the green region. Most resources I've found online mention both images' need for the same size, but I'm only looking to resize the new image to fit inside the green rectangle instead of having two square images overlapping with one of them with a cutout.
What's a good approach here?
Here is one solution using Python OpenCV.
Read both images.
Measure and enter 4 corresponding sets of x,y control points.
Compute homography (perspective coefficients)
Warp the source image using the homography -- the background will be black
Create a binary mask from the dst image using the green color range.
Invert the mask.
Apply the inverted mask to the dst image to blacken the inside of the region of interest (where the src will go)
Add the warped src to the masked dst to form the result
src:
dst:
#!/python3.7
import cv2
import numpy as np
# Read source image.
src = cv2.imread('original.jpg')
# Four corners of source image
# Coordinates are in x,y system with x horizontal to the right and y vertical downward
# listed clockwise from top left
pts_src = np.float32([[0, 0], [325, 0], [325, 472], [0, 472]])
# Read destination image.
dst = cv2.imread('green_rect.png')
# Four corners of destination image.
pts_dst = np.float32([[111, 59], [206, 60], [216, 215], [121, 225]])
# Calculate Homography if more than 4 points
# h = forward transformation matrix
#h, status = cv2.findHomography(pts_src, pts_dst)
# Alternate if only 4 points
h = cv2.getPerspectiveTransform(pts_src,pts_dst)
# Warp source image to destination based on homography
# size argument is width x height, so have to reverse shape values
src_warped = cv2.warpPerspective(src, h, (dst.shape[1],dst.shape[0]))
# Set BGR color ranges
lowerBound = np.array([0, 255, 0]);
upperBound = np.array([0, 255, 0]);
# Compute mask (roi) from ranges in dst
mask = cv2.inRange(dst, lowerBound, upperBound);
# Dilate mask, if needed, when green border shows
kernel = np.ones((3,3),np.uint8)
mask = cv2.dilate(mask,kernel,iterations = 1)
# Invert mask
inv_mask = cv2.bitwise_not(mask)
# Mask dst with inverted mask
dst_masked = cv2.bitwise_and(dst, dst, mask=inv_mask)
# Put src_warped over dst
result = cv2.add(dst_masked, src_warped)
# Save outputs
cv2.imwrite('warped_src.jpg', src_warped)
cv2.imwrite('inverted_mask.jpg', inv_mask)
cv2.imwrite('masked_dst.jpg', dst_masked)
cv2.imwrite('perspective_composite.jpg', result)
warped_src:
inverted_mask:
masked_dst:
result:
I will leave it to the reader to filter the excess green border or edit the control points in the dst image to make the region of interest larger.
Note: if the aspect ratio of the src does not match that of the green rectangle, then the src will get distorted with this method.
Per request in comments to my previous answer doing it in perspective, here is one way to do it with a simple scale and translation affine warp.
Read both images
Measure the height of the green region and get the height of the src image
Measure the center (x,y) of the green region and get the center of the src image
Compute the affine matrix coefficients for scale and translation only (no rotation or skew)
Warp the source image using the affine matrix -- the background will be black
Create a binary mask from the warped src image making everything not black into white
Invert the mask
Apply the inverted mask to the dst image
Add the warped src over the masked dst to form the result
src:
dst:
#!/python3.7
import cv2
import numpy as np
# Read source image.
src = cv2.imread('original.jpg')
h_src, w_src = src.shape[:2]
# Read destination image.
dst = cv2.imread('green_rect.png')
h_dst, w_dst = dst.shape[:2]
# compute scale from height of src and height of green region
h_green=170
scale = h_green/h_src
# compute offsets from center of scaled src and center of green
x_src = (scale)*w_src/2
y_src = (scale)*h_src/2
x_green = 165
y_green = 140
xoff = (x_green - x_src)
yoff = (y_green - y_src)
# build affine matrix for scale and translate only
affine_matrix = np.float32([ [scale,0,xoff], [0,scale,yoff] ])
# do affine warp
# add 1 to src to ensure no pure black
src_warped = cv2.warpAffine(src+1, affine_matrix, (w_dst, h_dst), cv2.INTER_AREA)
# Compute mask (roi) in warped src
_, mask = cv2.threshold(src_warped,1,255,cv2.THRESH_BINARY)
# Invert single channel of mask
inv_mask = cv2.bitwise_not(mask[:,:,0])
# Mask dst with inverted mask
dst_masked = cv2.bitwise_and(dst, dst, mask=inv_mask)
# Put warped src over masked dst
result = cv2.add(dst_masked,src_warped)
# Save outputs
cv2.imwrite('warped_src.jpg', src_warped)
cv2.imwrite('masked_src.jpg', mask)
cv2.imwrite('affine_composite.jpg', result)
warped_src:
inverted mask:
masked dst
result:
Related
I'm new to opencv and I m trying to remove all these diagonal parallel lines that are noise in my image.
I have tried using HoughLinesP after some erosion/dilatation but the result is poo (and keeping only the one with a near 135 degree angle).
img = cv2.imread('images/dungeon.jpg')
ret,img = cv2.threshold(img,180,255,0)
element = cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5))
eroded = cv2.erode(img,element)
dilate = cv2.dilate(eroded, element)
skeleton = cv2.subtract(img, dilate)
gray = cv2.cvtColor(skeleton,cv2.COLOR_BGR2GRAY)
minLineLength = 10
lines = cv2.HoughLinesP(gray, 1, np.pi/180, 1, 10, 0.5)
for line in lines:
for x1,y1,x2,y2 in line:
angle = math.atan2(y2-y1,x2-x1)
if (angle > -0.1 and angle < 0.1):
cv2.line(img,(x1,y1),(x2,y2),(0,255,0),1)
cv2.imshow("result", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
My thinking here was to detect these lines in order to remove them afterwards but I m not even sure that's the good way to do this.
I guess you are trying to get the contours of the walls, right? Here’s a possible path to the solution using mainly spatial filtering. You will still need to clean the results to get where you want. The idea is to try and compute a mask of the parallel lines (high-frequency noise) of the image and calculate the difference between the (binary) input and this mask. These are the steps:
Convert the input image to grayscale
Apply Gaussian Blur to get rid of the high-frequency noise you are trying to eliminate
Get a binary image of the blurred image
Apply area filters to get rid of everything that is not noise, to get a noise mask
Compute the difference between the original binary mask and the noise mask
Clean up the difference image
Compute contours on this image
Let’s see the code:
import cv2
import numpy as np
# Set image path
path = "C://opencvImages//"
fileName = "map.png"
# Read Input image
inputImage = cv2.imread(path+fileName)
# Convert BGR to grayscale:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Apply Gaussian Blur:
blurredImage = cv2.GaussianBlur(grayscaleImage, (3, 3), cv2.BORDER_DEFAULT)
# Threshold via Otsu:
_, binaryImage = cv2.threshold(blurredImage, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# Save a copy of the binary mask
binaryCopy = cv2.cvtColor(binaryImage, cv2.COLOR_GRAY2BGR)
This is the output:
Up until now you get this binary mask. The process so far has smoothed the noise and is creating thick black blobs where the noise is located. Again, the idea is to generate a noise mask that can be subtracted to this image.
Let’s apply an area filter and try to remove the big white blobs, which are NOT the noise we are interested to preserve. I’ll define the function towards the end, for now I just want to present the general idea:
# Set the minimum pixels for the area filter:
minArea = 50000
# Perform an area filter on the binary blobs:
filteredImage = areaFilter(minArea, binaryImage)
The filter will suppress every white blob that is above the minimum threshold. The value is big because in this particular case we are interested in preserving only the black blobs. This is the result:
We have a pretty solid mask. Let’s subtract this from the original binary mask we created earlier:
# Get the difference between the binary image and the mask:
imgDifference = binaryImage - filteredImage
This is what we get:
The difference image has some small noise. Let’s apply the area filter again to get rid of it. This time with a more traditional threshold value:
# Set the minimum pixels for the area filter:
minArea = 20
# Perform an area filter on the binary blobs:
filteredImage = areaFilter(minArea, imgDifference)
Cool. This is the final mask:
Just for completeness. Let’s compute contours on this input, which is very straightforward:
# Find the big contours/blobs on the filtered image:
contours, hierarchy = cv2.findContours(filteredImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# Draw the contours on the mask image:
cv2.drawContours(binaryCopy, contours, -1, (0, 255, 0), 3)
Let’s see the result:
As you see it is not perfect. However, there’s still some room for improvement, perhaps you can polish a little bit more this idea to get a potential solution. Here's the definition and implementation of the areaFilter function:
def areaFilter(minArea, inputImage):
# Perform an area filter on the binary blobs:
componentsNumber, labeledImage, componentStats, componentCentroids = \
cv2.connectedComponentsWithStats(inputImage, connectivity=4)
# 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')
return filteredImage
I am new to opencv.
My Idea is: I have a picture, and defined 4 points (pixels?) e.g. 0x0,0x100,100x0,100x00
What would be best approach to probe each of those BUT, creating square around them.
so e.g. for 0x0 (well not the best example as it can't go around), so let's say 50x50 point and create some kind of mask around that pixel let's say 10x10 pixels square width and height, and then get average RGB of that square, and then do it for all points.
So far I can only probe single points for RGB, but don't have an idea how to approach masking.
I have a feeling like openCV could have some easy solution for that, but all I am finding is super overcomplicated (imho) code that I don't really understand.
If you have an irregular region, then make a mask for it. You can compute the mean of region corresponding to the mask in Python/OpenCV as follows:
Input:
Mask:
import cv2
# load image
img = cv2.imread('zelda1.jpg')
# load mask as grayscale
mask = cv2.imread('zelda1_mask.png', 0)
# get mean of pixels corresponding to mask
mean = cv2.mean(img, mask=mask)
# print mean of each channel including alpha; alpha=0 is opaque
print(mean)
# mask region on input
region = img.copy()
img_masked = cv2.bitwise_and(img, img, mask=mask)
# Save result
cv2.imwrite('zelda1_region2.jpg', img_masked)
# Display input
cv2.imshow('input', img)
cv2.imshow('mask', mask)
cv2.imshow('input masked', img_masked)
cv2.waitKey(0)
cv2.destroyAllWindows()
Region of image where mean is computed:
Mean:
(50.23702664796634, 32.84151472650771, 198.3702664796634, 0.0)
Here is one way to do that in Python/OpenCV using Numpy slicing to get a square region about any give point.
Input:
import cv2
# load image
img = cv2.imread('zelda1.jpg')
# Define point
x = 90
y = 200
# Define region size
rr = 10
# crop image +-20 pixels
crop = img[y-rr:y+rr, x-rr:x+rr]
# compute mean
mean = cv2.mean(crop)
# print mean of each channel including alpha; alpha=0 is opaque
print(mean)
# draw region on input
region = img.copy()
cv2.rectangle(region, (x-rr,y-rr), (x+rr,y+rr), (255,255,255), 1)
# Save result
cv2.imwrite('zelda1_region.jpg', region)
# Display input
cv2.imshow('input', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Region:
Mean of region for each channel:
(53.6175, 35.9, 205.2375, 0.0)
I have an image that only contains a tiled shape in it with everywhere else black. However, this tiled pattern can be shifted/offset anywhere in the image particularly over the image borders. Knowing that this shape can be fit inside the image after offsetting it and leaving the borders black, how can I calculate how many pixels in x and y coordinates it needs to get offset for that to happen in an optimized way?
Input image
Desired output after offset/shiftimg
My thought was getting connected components in the image, check which labels are on the border, calculate the longest distance between each axis shapes that are on the border and offsetting in the axis' with those values. It can work but I feel like there should be smarter ways.
So here is the details of what I put in my comment for doing that with Python/OpenCV/Numpy. Is this what you want?
Read the input
Convert to gray
Threshold to binary
Count the number of white pixels in each column and store in array
Find the first and last black (zero count) element in the array
Get the center x values
Crop the image into left and right parts at the center x
Stack them together horizontally in the opposite order
Save the result
Input:
import cv2
import numpy as np
# read image
img = cv2.imread('black_white.jpg')
hh, ww = img.shape[:2]
# convert to gray
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# threshold
thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)[1]
# count number of white pixels in columns as new array
count = np.count_nonzero(thresh, axis=0)
# get first and last x coordinate where black (count==0)
first_black = np.where(count==0)[0][0]
last_black = np.where(count==0)[0][-1]
# compute x center
black_center = (first_black + last_black) // 2
print(black_center)
# crop into two parts
left = img[0:hh, 0:black_center]
right = img[0:hh, black_center:ww]
# combine them horizontally after swapping
result = np.hstack([right, left])
# write result to disk
cv2.imwrite("black_white_rolled.jpg", result)
# display it
cv2.imshow("RESULT", result)
cv2.waitKey(0)
I am trying to write an algorithm to systematically determine how many different "curves" are in an image. Example Image. I'm specifically interested in the white lines here, so I've used a color threshold to mask the rest of the image and only get the white pixels. These lines represent a path run by a player (wide receivers in the NFL), so I'm interested in the x and y coordinates that the path represents - and each "curve" represents a different path that the player took (or "route"). All curves should start on or behind the blue line.
However, while I can get just the white pixels, I can't figure out how to systematically identify the separate curves. In this example image, there are 8 white curves (or routes) present. I've identified those curves in this image. I tried edge detection, and then using scipy ndimage to get the number of connected components, but because the curves overlap it counts them as connected and only gives me 3 labeled components for this image as opposed to eight. Here's what the edge detection output looks like. Is there a better way to go about this? Here is my sample code.
import cv2
from skimage.morphology import skeletonize
import numpy as np
from scipy import ndimage
#Read in image
image = cv2.imread('example_image.jpeg')
#Color boundary to get white pixels
lower_white = np.array([230, 230, 230])
upper_white = np.array([255, 255, 255])
#mask image for white pixels
mask = cv2.inRange(image, lower_white, upper_white)
c_pixels = cv2.bitwise_and(image, image, mask=mask)
#make pixels from 0 to 1 form to use in skeletonize
c_pixels = c_pixels.clip(0,1)
ske_c = skeletonize(c_pixels[:,:,1]).astype(np.uint8)
#Edge Detection
inputImage =ske_c*255
edges = cv2.Canny(inputImage,100,200,apertureSize = 7)
#Show edges
cv2.imshow('edges', edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
#Find number of components
# smooth the image (to remove small objects); set the threshold
edgesf = ndimage.gaussian_filter(edges, 1)
T = 50 # set threshold by hand to avoid installing `mahotas` or
# `scipy.stsci.image` dependencies that have threshold() functions
# find connected components
labeled, nr_objects = ndimage.label(edgesf > T) # `dna[:,:,0]>T` for red-dot case
print("Number of objects is %d " % nr_objects)
I am new to openCV and I was wondering if there is a way to remove the periodic stripes in the lower half of this image.
I looked at this post but couldn't quite understand what was going on: Removing periodic noise from an image using the Fourier Transform
Here is how to mitigate (reduce, but not totally eliminate) the lines using Fourier Transform and notch filtering processing with Python/OpenCV/Numpy. Since the horizontal lines in the input are very close, there will be horizontal linear structures spaced far apart in the Fourier Transform spectrum. So what I did was:
Read the input
Pad with the mean value to powers of 2 size (to try to mitigate any ringing from the discontinuity with the padding)
Do the DFT
Compute the spectrum image from the magnitude
Threshold the image and draw a black horizontal line through the center to blank out the bright DC component
Find where the bright spots (lines) show.
Get the coordinates of the bright spots and draw white horizontal lines on the thresholded image to form a mask
Apply the mask to the magnitude image
Do the IDFT
Crop back to the size and normalize to the same dynamic range as the original image
Input:
import numpy as np
import cv2
import math
# read input as grayscale
img = cv2.imread('pattern_lines.png', 0)
hh, ww = img.shape
# get min and max and mean values of img
img_min = np.amin(img)
img_max = np.amax(img)
img_mean = int(np.mean(img))
# pad the image to dimension a power of 2
hhh = math.ceil(math.log2(hh))
hhh = int(math.pow(2,hhh))
www = math.ceil(math.log2(ww))
www = int(math.pow(2,www))
imgp = np.full((hhh,www), img_mean, dtype=np.uint8)
imgp[0:hh, 0:ww] = img
# convert image to floats and do dft saving as complex output
dft = cv2.dft(np.float32(imgp), flags = cv2.DFT_COMPLEX_OUTPUT)
# apply shift of origin from upper left corner to center of image
dft_shift = np.fft.fftshift(dft)
# extract magnitude and phase images
mag, phase = cv2.cartToPolar(dft_shift[:,:,0], dft_shift[:,:,1])
# get spectrum
spec = np.log(mag) / 20
min, max = np.amin(spec, (0,1)), np.amax(spec, (0,1))
# threshold the spectrum to find bright spots
thresh = (255*spec).astype(np.uint8)
thresh = cv2.threshold(thresh, 155, 255, cv2.THRESH_BINARY)[1]
# cover the center rows of thresh with black
yc = hhh // 2
cv2.line(thresh, (0,yc), (www-1,yc), 0, 5)
# get the y coordinates of the bright spots
points = np.column_stack(np.nonzero(thresh))
print(points)
# create mask from spectrum drawing horizontal lines at bright spots
mask = thresh.copy()
for p in points:
y = p[0]
cv2.line(mask, (0,y), (www-1,y), 255, 5)
# apply mask to magnitude such that magnitude is made black where mask is white
mag[mask!=0] = 0
# convert new magnitude and old phase into cartesian real and imaginary components
real, imag = cv2.polarToCart(mag, phase)
# combine cartesian components into one complex image
back = cv2.merge([real, imag])
# shift origin from center to upper left corner
back_ishift = np.fft.ifftshift(back)
# do idft saving as complex output
img_back = cv2.idft(back_ishift)
# combine complex components into original image again
img_back = cv2.magnitude(img_back[:,:,0], img_back[:,:,1])
# crop to original size
img_back = img_back[0:hh, 0:ww]
# re-normalize to 8-bits in range of original
min, max = np.amin(img_back, (0,1)), np.amax(img_back, (0,1))
notched = cv2.normalize(img_back, None, alpha=img_min, beta=img_max, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)
cv2.imshow("ORIGINAL", img)
cv2.imshow("PADDED", imgp)
cv2.imshow("MAG", mag)
cv2.imshow("PHASE", phase)
cv2.imshow("SPECTRUM", spec)
cv2.imshow("THRESH", thresh)
cv2.imshow("MASK", mask)
cv2.imshow("NOTCHED", notched)
cv2.waitKey(0)
cv2.destroyAllWindows()
# write result to disk
cv2.imwrite("pattern_lines_spectrum.png", (255*spec).clip(0,255).astype(np.uint8))
cv2.imwrite("pattern_lines_thresh.png", thresh)
cv2.imwrite("pattern_lines_mask.png", mask)
cv2.imwrite("pattern_lines_notched.png", notched)
Spectrum (note the bright spots in the middle at y=64 and 192):
Threshold Image:
Bright Spot Locations:
[[ 0 1023]
[ 0 1024]
[ 0 1025]
[ 1 1024]
[ 64 1024]
[ 65 1024]
[ 191 1024]
[ 192 1024]
[ 255 1024]]
Mask:
Result: