user15727451
user15727451

Reputation: 1

How to resize an image with "varying scaling"?

I need to resize an image, but with a "varying scaling" in the y axis, after warping:

Plotted Image

Original input image

Warped output image

The image (left one) was taken at an angle, so I've used the getPerspectiveTransform and warpPerspective OpenCV functions to get the top/plan view of the image (right one).

But, now the top half of the warped image is stretched and the bottom half is squashed, and this amount of stretch/squash is varying continuously as you go down the image. So, I need to do the opposite.

For example: The zebra crossing lines in the warped image are thicker at the top of the image and thinner at the bottom. I want them to all be the same thickness and same vertical distance from each other essentially.

Badly drawn but something like this: (if we ignore the 2 people, I think this is what the final output image should be like.) predicted output image

My end goal is to measure distance between people's feet in an image (shown by green dots), but I've got that section sorted already.

By vertically scaling the warped image to make it linear, it will allow me to accurately measure the real distance in the x & y direction from a top/plan view, (i.e each pixel in the x or y direction is say 1cm in real distance)

I was thinking of multiplying each row of the image by a factor (e.g. top rows multiply by smaller number like 0.8 or 0.9, and bottom rows multiply by bigger number like 1.1 or 1.2), but I really don't know how to do that.

Code:

import cv2 as cv
from matplotlib import pyplot as plt
import numpy as np

# READ IMAGE
imgOrig = cv.imread('.jpg')

# RESIZE IMAGE
width = int(1000)
ratio = imgOrig.shape[1]/width
height = int(imgOrig.shape[0]/ratio)
dsize = (width, height)
img = cv.resize(imgOrig, dsize)

feetLocation = [[280, 500], [740, 496]]
cv.circle(img,(280, 500),5,(0,255,0),thickness= 10)
cv.circle(img,(740, 496),5,(0,255,0),thickness= 10)

# WARPING
pts1 = np.float32([[0, -0], [width, 0], [-1800, height], [width + 1800, height]])
pts2 = np.float32([[0, 0], [width, 0], [0, height], [width, height]])
M = cv.getPerspectiveTransform(pts1, pts2)
dst = cv.warpPerspective(img, M, (width, height))

#DISPLAY IMAGES
plt.subplot(121),plt.imshow(img),plt.title('Original Image')
plt.subplot(122),plt.imshow(dst),plt.title('Warped Image')
plt.show()

Upvotes: 0

Views: 578

Answers (1)

HansHirse
HansHirse

Reputation: 18895

I was working on a solution, before the several edits were applied. I focussed on the actual boxes only. If, instead, you actually need the surrounding, too, the following approach won't help you much, I'm afraid. Also, I assumed the bottom box to be fully included. So, if that one's somehow cut like presented in your new desired final output, additional work would be needed to handle that case.


From the given image, you could mask the gray-ish part around and between the single boxes using the saturation and value channels from the HSV color space:

Mask

Following, row-wise sum all pixels, apply some moving average to clean the signal, and detect the peaks in that signal:

Peaks

The bottom image border must be manually added, since there is no gray-ish border (most likely because the box is somehow cut).

Now, for each of these "peak rows", determine the first and last masked pixels, and build boxes from each two neighbouring "peak rows". Finally, for each of these boxes, apply a distinct perspective transform to a given size. If needed, stack those boxes vertically for example:

Output

That'd be the whole code:

import cv2
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import find_peaks

# Read original image
imgOrig = cv2.cvtColor(cv2.imread('DInAq.jpg'), cv2.COLOR_BGR2RGB)

# Resize image
width = int(1000)
ratio = imgOrig.shape[1] / width
height = int(imgOrig.shape[0] / ratio)
dsize = (width, height)
img = cv2.resize(imgOrig, dsize)

# Mask low saturation and medium to high value (i.e. gray-ish/white-ish colors)
img_gauss = cv2.GaussianBlur(img, (5, 5), -1)
h, s, v = cv2.split(cv2.cvtColor(img_gauss, cv2.COLOR_BGR2HSV))
mask = (s < 24) & (v > 64)

# Row-wise sum mask pixels, apply moving average filter, and find peaks
row_sum = np.sum(mask, axis=1)
row_sum = np.convolve(row_sum, np.ones(5)/5, 'same')
peaks = find_peaks(row_sum, prominence=50)[0]
peaks = np.insert(peaks, 4, img.shape[0]-1)

# Find first and last pixels per "peak row"
x1 = [np.argwhere(mask[p, :]).min() for p in peaks]
x2 = [np.argwhere(mask[p, :]).max() for p in peaks]

# Collect single boxes
boxes = []
for i in np.arange(len(peaks)-1, 0, -1):
    boxes.append([[x1[i], peaks[i]],
                  [x1[i-1], peaks[i-1]],
                  [x2[i-1], peaks[i-1]],
                  [x2[i], peaks[i]]])

# Warp each box individually to a given size
warped = []
bw, bh = [400, 400]
for box in reversed(boxes):
    pts1 = np.float32(box)
    pts2 = np.float32([[0, bh-1], [0, 0], [bw-1, 0], [bw-1, bh-1]])
    M = cv2.getPerspectiveTransform(pts1, pts2)
    warped.append(cv2.warpPerspective(img, M, (bw, bh)))

# Output
plt.figure(1)
plt.subplot(121), plt.imshow(img), plt.title('Original image')
for box in boxes:
    pts = np.array(box)
    plt.plot(pts[:, 0], pts[:, 1], 'rx')
plt.subplot(122), plt.imshow(np.vstack(warped)), plt.title('Warped image')
plt.tight_layout(), plt.show()

That's kind of an automated way to detect and extract the single boxes. For better results, you could set up a simple GUI (solely using OpenCV, for example), and let the user click on the exact corners, and build the boxes to be transformed from there.

----------------------------------------
System information
----------------------------------------
Platform:      Windows-10-10.0.16299-SP0
Python:        3.9.1
PyCharm:       2021.1
Matplotlib:    3.4.1
NumPy:         1.20.2
OpenCV:        4.5.1
SciPy:         1.6.2
----------------------------------------

Upvotes: 2

Related Questions