Related
OpenCV / Python related:
Given a photo of a round object, how can you output that object flattened, while adjusting for surface area? Here is an example image of an input:
Soccer ball
It is similar to adjusting for camera distortion (turning a round object into flat one), but in this case the distortion comes from the object itself and not the camera.
Distorted image:
Undistorted image:
Any suggestions would help. Thank you!
Edit: The package squircle is just what I needed, thank you fmw42!
Here is a solution in Python/OpenCV. It creates transformation maps that define the equations from output back to input and applies them using cv2.remap(). The equations come from https://arxiv.org/pdf/1509.06344.pdf for the Elliptical Grid Mapping approach.
Input:
import numpy as np
import cv2
import math
# References:
# https://arxiv.org/pdf/1509.06344.pdf
# http://squircular.blogspot.com/2015/09/mapping-circle-to-square.html
# Evaluate:
# u = x*sqrt(1-y**2/2)
# v = y*sqrt(1-x**2/2)
# u,v are input circle coordinates and x,y are output square coordinates
# read input
img = cv2.imread("rings.png")
# get dimensions and center
h, w = img.shape[:2]
xcent = w / 2
ycent = h / 2
# set up the maps as float32 from output square (x,y) to input circle (u,v)
map_u = np.zeros((h, w), np.float32)
map_v = np.zeros((h, w), np.float32)
# create u and v maps where x,y is measured from the center and scaled from -1 to 1
for y in range(h):
Y = (y - ycent)/ycent
for x in range(w):
X = (x - xcent)/xcent
map_u[y, x] = xcent * X * math.sqrt(1 - 0.5*Y**2) + xcent
map_v[y, x] = ycent * Y * math.sqrt(1 - 0.5*X**2) + ycent
# do the remap
result = cv2.remap(img, map_u, map_v, cv2.INTER_LINEAR, borderMode = cv2.BORDER_REFLECT_101, borderValue=(0,0,0))
# save results
cv2.imwrite("rings_circle2square.png", result)
# display images
cv2.imshow('img', img)
cv2.imshow('result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
Result:
Here is another example:
Input:
Result:
And here is a 3rd example:
Input:
Result:
ADDITION
Here is an alternate approach based upon the Simple Stretch equations in the reference above:
import numpy as np
import cv2
import math
# References:
# https://arxiv.org/pdf/1509.06344.pdf
# Simple stretch equations
# read input
img = cv2.imread("rings.png")
#img = cv2.imread("ICM.png")
#img = cv2.imread("soccerball_small.jpg")
# get dimensions and center
h, w = img.shape[:2]
xcent = w / 2
ycent = h / 2
# set up the maps as float32 from output square (x,y) to input circle (u,v)
map_u = np.zeros((h, w), np.float32)
map_v = np.zeros((h, w), np.float32)
# create u and v maps where x,y is measured from the center and scaled from -1 to 1
# note: copysign(1,x) is signum(x) and returns 1 ,0, or -1 depending upon sign of x
for y in range(h):
Y = (y - ycent)/ycent
for x in range(w):
X = (x - xcent)/xcent
X2 = X*X
Y2 = Y*Y
XY = X*Y
R = math.sqrt(X2+Y2)
if R == 0:
map_u[y, x] = xcent
map_v[y, x] = ycent
elif X2 >= Y2:
map_u[y, x] = xcent * math.copysign(1, X) * X2/R + xcent
map_v[y, x] = ycent * math.copysign(1, X) * XY/R + ycent
else:
map_u[y, x] = xcent * math.copysign(1, Y) * XY/R + xcent
map_v[y, x] = ycent * math.copysign(1, Y) * Y2/R + ycent
# do the remap
result = cv2.remap(img, map_u, map_v, cv2.INTER_LINEAR, borderMode = cv2.BORDER_REFLECT_101, borderValue=(0,0,0))
# save results
cv2.imwrite("rings_circle2square2.png", result)
#cv2.imwrite("ICM_circle2square2.png", result)
#cv2.imwrite("soccerball_small_circle2square2.png", result)
# display images
cv2.imshow('img', img)
cv2.imshow('result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
Input:
Result:
Input:
Result:
Input:
Result:
This question already has answers here:
Segmenting License Plate Characters
(2 answers)
Closed 2 years ago.
So, I need to build a homomorphic filter, but my code seems to be wrong. I don't know if it's execution or if it's some detail I don't know about in python, but I do know that it's wrong. I'd love som insights on what I can do to improve it.
I'm using
image as input reference, because it's on the Ricardo C. Gonzales book of DIP and I know how the output should look like. I'm even using the same parameters the book used in it's filter but it isn't working.
Gonzalez's Input and output, respectively:
.
My output:
My code is as follows:
# coding: utf-8
import cv2
import numpy as np
from matplotlib import pyplot as plt
tss = cv2.imread("The_Seventh_Seal_1.jpg", 0)
mc = cv2.imread("mussels_cave_050.JPG", 0)
sh = cv2.imread("shelter_homomorphic.bmp", 0)
pet = cv2.imread("pet.png", 0)
def filtro_gaussiano_livro(img, gl, gh, inc, Dz):
im = np.copy(img)
P = im.shape[0] / 2
Q = im.shape[1] / 2
h = np.zeros(im.shape)
U, V = np.meshgrid(range(im.shape[0]), range(im.shape[1]), sparse=False, indexing='ij')
d = ((U - P) ** 2 + (V - Q) ** 2).astype(float)
d0 = Dz
c = inc
h = (gh - gl) * (1 - (np.exp(-c * (d / (d0 ** 2))))) + gl
return h
def filtro_gaussiano(img, Dz):
im = np.copy(img)
P = im.shape[0] / 2
Q = im.shape[1] / 2
h = np.zeros(im.shape)
U, V = np.meshgrid(range(im.shape[0]), range(im.shape[1]), sparse=False, indexing='ij')
d = (((U - P) ** 2) + ((V - Q) ** 2)).astype(float)
h = 1 - np.exp(-(d / (2 * (Dz ** 2))))
return h
def uint8_conv(img):
mat = np.copy(img)
for i in range(mat.shape[0]):
for j in range(mat.shape[1]):
if mat[i, j] < 0:
mat[i, j] = 0
elif mat[i, j] > 255:
mat[i, j] = 255
else:
mat[i, j] = mat[i, j]
return np.uint8(mat)
def reescalona(img, min, max):
mat = np.copy(img)
ph = cv2.add(min, (
cv2.divide((cv2.multiply((cv2.subtract(mat, np.min(mat))), (max - min))), (np.max(mat) - np.min(mat)))))
rtn = np.uint8(ph)
return rtn
def homomorfica(img, l, s):
im = np.float64(np.copy(img))
cv2.imshow("BORDER", im)
if s == 0:
f = filtro_gaussiano(im, l)
elif s == 1:
f = filtro_gaussiano_livro(im, 0.05, 3.5, 1, l)
cv2.imshow("gauss " + str(s), f)
im_log = np.log1p(im)
Im_shift = np.fft.fftshift(np.fft.fft2(im_log))
Im_fft_filt = np.multiply(f, Im_shift)
cv2.imshow("FFT Shift", uint8_conv(np.real(Im_shift)))
Im_filt = np.real(np.fft.ifft2(np.fft.ifftshift(Im_fft_filt)))
Im = np.exp(Im_filt) - 1
Im = reescalona(Im, 0, 255)
return uint8_conv(Im)
# def notch(img):
raio = 2500
i = pet
a = homomorfica(i, raio, 0)
b = homomorfica(i, raio, 1)
cv2.imshow("Imagem original", i)
cv2.imshow("Filtro homofobico comum", a)
cv2.imshow("Filtro homofobico do livro", b)
k = 0
while k != 27:
k = cv2.waitKey(0)
cv2.destroyAllWindows()
Here is one way to do homomorphic filtering in the frequency domain using Python/Numpy/OpenCV.
I believe your issue is just your filtering. I will show two different filters below that vary in radius of the circle and Gaussian filtering.
Read the input as grayscale
Take the natural log of the input
Do FFT to real/imaginary components
Shift the FFT so DC point is in the center
Create a black circular mask on a white background of small radius
Apply Gaussian blur to the mask
Shift the FFT so DC point is at the top left corner
Do IFFT and convert to a simple real image
Take the exponential of the IFFT
Stretch that to the range 0 to 255
Save the result
import numpy as np
import cv2
# read input and convert to grayscale
img = cv2.imread('person.png', cv2.IMREAD_GRAYSCALE)
hh, ww = img.shape[:2]
# take ln of image
img_log = np.log(np.float64(img), dtype=np.float64)
# do dft saving as complex output
dft = np.fft.fft2(img_log, axes=(0,1))
# apply shift of origin to center of image
dft_shift = np.fft.fftshift(dft)
# create black circle on white background for high pass filter
#radius = 3
radius = 13
mask = np.zeros_like(img, dtype=np.float64)
cy = mask.shape[0] // 2
cx = mask.shape[1] // 2
cv2.circle(mask, (cx,cy), radius, 1, -1)
mask = 1 - mask
# antialias mask via blurring
#mask = cv2.GaussianBlur(mask, (7,7), 0)
mask = cv2.GaussianBlur(mask, (47,47), 0)
# apply mask to dft_shift
dft_shift_filtered = np.multiply(dft_shift,mask)
# shift origin from center to upper left corner
back_ishift = np.fft.ifftshift(dft_shift_filtered)
# do idft saving as complex
img_back = np.fft.ifft2(back_ishift, axes=(0,1))
# combine complex real and imaginary components to form (the magnitude for) the original image again
img_back = np.abs(img_back)
# apply exp to reverse the earlier log
img_homomorphic = np.exp(img_back, dtype=np.float64)
# scale result
img_homomorphic = cv2.normalize(img_homomorphic, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)
# write result to disk
cv2.imwrite("person_dft_numpy_mask.png", (255*mask).astype(np.uint8))
cv2.imwrite("person_dft_numpy_homomorphic.png", img_homomorphic)
cv2.imshow("ORIGINAL", img)
cv2.imshow("MASK", mask)
cv2.imshow("FILTERED DFT/IFT ROUND TRIP", img_back)
cv2.imshow("HOMOMORPHIC", img_homomorphic)
cv2.waitKey(0)
cv2.destroyAllWindows()
High Pass Filter Mask and Homomorphic Result for radius=3 and blur=7:
High Pass Filter Mask and Homomorphic Result for radius=13 and blur=47:
def normalize_brightness(img: Image) -> Image:
"""
Normalize the brightness of the given Image img by:
computing the average brightness of the picture:
- this can be done by calculating the average brightness of each pixel
in img (the average brightness of each pixel is the sum of the values
of red, blue and green of the pixel, divided by 3 as a float division)
- the average brightness of the picture is then the sum of all the
pixel averages, divided by the product of the width and height of img
find the factor, let's call it x, which we can multiply the
average brightness by to get the value of 128.
multiply the colors in each pixel by this factor x
"""
img_width, img_height = img.size
pixels = img.load() # create the pixel map
h = 0
for i in range(img_width):
for j in range(img_height):
r, g, b = pixels[i, j]
avg = sum(pixels[i, j]) / 3
h += avg
total_avg = int(h / (img_width * img_height))
x = 128 // total_avg
r, g, b = pixels[i, j]
pixels[i, j] = (r * x, g * x, b * x)
return img
I am a little lost as to what I am doing wrong can someone help?
You really should avoid for loops when image processing with Python whenever possible because it is seriously slow, verbose, harder to read and more likely to contain errors. Try to use vectorised Numpy functions, or OpenCV or PIL built-in functions.
#!/usr/bin/env python3
from PIL import Image
import numpy as np
def normalize(im):
"""Normalise brightness of image"""
# Convert to Numpy array
na = np.array(im, dtype=np.float32)
# Calculate average brightness
avg = na.mean()
# Calculate factor x
x = 128 / avg
# Scale whole array as float since likely fractional
na *= x
# Convert back to PIL Image and return
return Image.fromarray(na.astype(np.uint8))
# Load image and normalize
im = Image.open('start.png').convert('RGB')
result = normalize(im)
result.save('result.png')
This code runs in around 800 microseconds on my machine whereas any version with a for loop requires around 70x longer.
Input image:
Result:
Your calculation code to get the factor seems okay, processing every pixel to get the average of sum of averages.
However, your modification code to adjust the brightness is not done within a similar loop so it will operate on one pixel, and I'm not even sure that pixel is even within the image. You should do that within a loop as well:
for i in range(img_width):
for j in range(img_height):
(r, g, b) = pixels[i, j]
pixels[i, j] = (r * x, g * x, b * x)
This should replace the third-last and second-last lines of what you have at the moment (between x = ... and return ...). So what you would end up with is:
img_width, img_height = img.size
pixels = img.load() # create the pixel map
h = 0
for i in range(img_width):
for j in range(img_height):
r, g, b = pixels[i, j]
avg = sum(pixels[i, j]) / 3
h += avg
total_avg = int(h / (img_width * img_height))
x = 128 // total_avg
# == New stuff below
for i in range(img_width):
for j in range(img_height):
(r, g, b) = pixels[i, j]
pixels[i, j] = (r * x, g * x, b * x)
# == New stuff above
return img
A few other things to look in to:
First, I'm not sure if returning img is the right thing to do here, unless pixels is a reference to (not copy of) the pixels in the image. You may want to check up on that as well.
Further, it may be possible that the value for [rgb] * x gives you something more than 255 for certain input data sets. If that's the case, you probably want to clamp them into the range 0..255 to ensure this doesn't happen. Something like (replacing the "new stuff" in the code above):
for i in range(img_width):
for j in range(img_height):
# Get original pixel.
(r, g, b) = pixels[i, j]
# Scale with upper limit.
r = min(255, r * x)
g = min(255, g * x)
b = min(255, b * x)
# Replace pixel with scaled one.
pixels[i, j] = (r, g, b)
At first, thanks to paxdiablo for sharing his answer.
I would just like to improve on the answer.
The calculation of the average can be optimized using list comprehension like:
x = 128 // (sum([sum(pixels[i, j]) / 3 for i in range(img_width) for j in range(img_height)]) / (img_width * img_height))
So my complete answer will be:
Normalize the brightness of the given Image
img_width, img_height = img.size
pixels = img.load() # create the pixel map
x = 128 // (sum([sum(pixels[i, j]) / 3 for i in range(img_width) for j in range(img_height)]) / (img_width * img_height))
for i in range(img_width):
for j in range(img_height):
r, g, b = pixels[i, j]
pixels[i, j] = [min(255, r * x), min(255, g * x), min(255, b * x)]
return img
I am pretty new to Python and want to do the following: I want to divide the following image into 8 pie segments:
I want it to look something like this (I made this in PowerPoint):
The background should be black and the edge of the figure should have an unique color as well as each pie segment.
EDIT: I have written a code that divides the whole image in 8 segments:
from PIL import Image, ImageDraw
im=Image.open('C:/Users/20191881/Documents/OGO Beeldanalyse/Python/asymmetrie/rotation.png')
fill = 255
draw = ImageDraw.Draw(im)
draw.line((0,0) + im.size, fill)
draw.line((0, im.size[1], im.size[0], 0), fill)
draw.line((0.5*im.size[0],0, 0.5*im.size[0], im.size[1]), fill)
draw.line((0, 0.5*im.size[1], im.size[0], 0.5*im.size[1]), fill)
del draw
im.show()
The output gives:
The only thing that is left to do is to find a way to make each black segment inside the border an unique color and also give all the white edge segments an unique color.
Your code divides the image in eight parts, that's correct, but with respect to the image center, you don't get eight "angular equally" pie segments like you show in your sketch.
Here would be my solution, only using Pillow and the math module:
import math
from PIL import Image, ImageDraw
def segment_color(i_color, n_colors):
r = int((192 - 64) / (n_colors - 1) * i_color + 64)
g = int((224 - 128) / (n_colors - 1) * i_color + 128)
b = 255
return (r, g, b)
# Load image; generate ImageDraw
im = Image.open('path_to/vgdrD.png').convert('RGB')
draw = ImageDraw.Draw(im)
# Number of pie segments (must be an even number)
n = 8
# Replace (all-white) edge with defined edge color
edge_color = (255, 128, 0)
pixels = im.load()
for y in range(im.height):
for x in range(im.width):
if pixels[x, y] == (255, 255, 255):
pixels[x, y] = edge_color
# Draw lines with defined line color
line_color = (0, 255, 0)
d = min(im.width, im.height) - 10
center = (int(im.width/2), int(im.height)/2)
for i in range(int(n/2)):
angle = 360 / n * i
x1 = math.cos(angle/180*math.pi) * d/2 + center[0]
y1 = math.sin(angle/180*math.pi) * d/2 + center[1]
x2 = math.cos((180+angle)/180*math.pi) * d/2 + center[0]
y2 = math.sin((180+angle)/180*math.pi) * d/2 + center[1]
draw.line([(x1, y1), (x2, y2)], line_color)
# Fill pie segments with defined segment colors
for i in range(n):
angle = 360 / n * i + 360 / n / 2
x = math.cos(angle/180*math.pi) * 20 + center[0]
y = math.sin(angle/180*math.pi) * 20 + center[1]
ImageDraw.floodfill(im, (x, y), segment_color(i, n))
im.save(str(n) + '_pie.png')
For n = 8 pie segments, the following result is produced:
The first step is to replace all white pixels in the original image with the desired edge color. Of course, the assumption here is, that there are no other (white) pixels in the image. Also, this might be better done using NumPy and vectorized code, but I wanted to keep the solution Pillow-only.
Next step is to draw the (green) lines. Here, I calculate the proper coordinates of the lines' start and end using sin and cos.
The last step is to flood fill the pie segments' area, cf. ImageDraw.floodfill. Therefore, I calculate the seed points the same way as before, but add an angular shift to hit a point exactly within the pie segment.
As you can see, n is variable in my solution (n must be even):
Of course, there are limitations regarding the angular resolution, most due to the small image.
Hope that helps!
EDIT: Here's a modified version to also allow for individually colored edges.
import math
from PIL import Image, ImageDraw
def segment_color(i_color, n_colors):
r = int((192 - 64) / (n_colors - 1) * i_color + 64)
g = int((224 - 128) / (n_colors - 1) * i_color + 128)
b = 255
return (r, g, b)
def edge_color(i_color, n_colors):
r = 255
g = 255 - int((224 - 32) / (n_colors - 1) * i_color + 32)
b = 255 - int((192 - 16) / (n_colors - 1) * i_color + 16)
return (r, g, b)
# Load image; generate ImageDraw
im = Image.open('images/vgdrD.png').convert('RGB')
draw = ImageDraw.Draw(im)
center = (int(im.width/2), int(im.height)/2)
# Number of pie segments (must be an even number)
n = 8
# Replace (all-white) edge with defined edge color
max_len = im.width + im.height
im_pix = im.load()
for i in range(n):
mask = Image.new('L', im.size, 0)
mask_draw = ImageDraw.Draw(mask)
angle = 360 / n * i
x1 = math.cos(angle/180*math.pi) * max_len + center[0]
y1 = math.sin(angle/180*math.pi) * max_len + center[1]
angle = 360 / n * (i+1)
x2 = math.cos(angle/180*math.pi) * max_len + center[0]
y2 = math.sin(angle/180*math.pi) * max_len + center[1]
mask_draw.polygon([center, (x1, y1), (x2, y2)], 255)
mask_pix = mask.load()
for y in range(im.height):
for x in range(im.width):
if (im_pix[x, y] == (255, 255, 255)) & (mask_pix[x, y] == 255):
im_pix[x, y] = edge_color(i, n)
# Draw lines with defined line color
line_color = (0, 255, 0)
d = min(im.width, im.height) - 10
for i in range(int(n/2)):
angle = 360 / n * i
x1 = math.cos(angle/180*math.pi) * d/2 + center[0]
y1 = math.sin(angle/180*math.pi) * d/2 + center[1]
x2 = math.cos((180+angle)/180*math.pi) * d/2 + center[0]
y2 = math.sin((180+angle)/180*math.pi) * d/2 + center[1]
draw.line([(x1, y1), (x2, y2)], line_color)
# Fill pie segments with defined segment colors
for i in range(n):
angle = 360 / n * i + 360 / n / 2
x = math.cos(angle/180*math.pi) * 20 + center[0]
y = math.sin(angle/180*math.pi) * 20 + center[1]
ImageDraw.floodfill(im, (x, y), segment_color(i, n))
im.save(str(n) + '_pie.png')
Binary masks for each pie segment are created, and all white pixels only within that binary mask are replaced with a defined edge color.
Using NumPy still seems favorable, but I was curious to do that in Pillow only.
Okay, so i have these annotation functions
def rotate_bound(image, angle):
# grab the dimensions of the image and then determine the
# center
(h, w) = image.shape[:2]
(cX, cY) = (w // 2, h // 2)
# grab the rotation matrix (applying the negative of the
# angle to rotate clockwise), then grab the sine and cosine
# (i.e., the rotation components of the matrix)
M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
# compute the new bounding dimensions of the image
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
# adjust the rotation matrix to take into account translation
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
# perform the actual rotation and return the image
return cv2.warpAffine(image, M, (nW, nH), borderValue=(255, 255, 255))
def blur_image(image, radius):
pil_img = Image.fromarray(image)
pil_img = pil_img.filter(ImageFilter.GaussianBlur(radius=radius))
opencv_image = np.array(pil_img)
return opencv_image
def enhance_brightness(image, value):
for x in range(image.shape[0]):
for y in range(image.shape[1]):
if image[x][y][0] < 245 and image[x][y][1] < 245 and image[x][y][2] < 245:
if image[x][y][0] + value <= 255 and image[x][y][0] + value >= 0:
image[x][y][0] += value
if image[x][y][1] + value <= 255 and image[x][y][0] + value >= 0:
image[x][y][1] += value
if image[x][y][2] + value <= 255 and image[x][y][0] + value >= 0:
image[x][y][2] += value
return image
When i try to use them, i get errors i cant fix.
Both functions rotate_bound and enhance_brightness give the same error:
'JpegImageFile' object has no attribute 'shape'.
And the blur function returns another error:
a bytes-like object is required, not 'JpegImageFile'
I would be very glad if someone gives me a hand.
You're importing the image using Pillow. Pillow has no attribute 'shape'. Instead, try
img = cv2.imread("test.jpg")
This should fix all the issues.