Subtracting a line from an image using OpenCV - python-3.x

So being new to OpenCV, I'm trying to detect a part of the image ( the thick line between the "PSSU" and "356750 / 22G1" characters ) and then subtract it from the original image. I want to have a clean final image that I can then put through OCR.
I've managed to detect the line ( red highlights ).
The code for line detection is shown below :
import cv2
import numpy as np
# Reading the required image in
# which operations are to be done.
# Make sure that the image is in the same
# directory in which this python program is
img = cv2.imread('c:\\ml\\test.jpg')
# Convert the img to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply edge detection method on the image
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
# This returns an array of r and theta values
# 4th value ( 400 ) is threshold of how thick the line is thats to be detected. Higher value = thicker line to be detected.
lines = cv2.HoughLines(edges, 1, np.pi/180, 400)
# The below for loop runs till r and theta values
# are in the range of the 2d array
for r_theta in lines:
arr = np.array(r_theta[0], dtype=np.float64)
r, theta = arr
# Stores the value of cos(theta) in a
a = np.cos(theta)
# Stores the value of sin(theta) in b
b = np.sin(theta)
# x0 stores the value rcos(theta)
x0 = a*r
# y0 stores the value rsin(theta)
y0 = b*r
# x1 stores the rounded off value of (rcos(theta)-1000sin(theta))
x1 = int(x0 + 1000*(-b))
# y1 stores the rounded off value of (rsin(theta)+1000cos(theta))
y1 = int(y0 + 1000*(a))
# x2 stores the rounded off value of (rcos(theta)+1000sin(theta))
x2 = int(x0 - 1000*(-b))
# y2 stores the rounded off value of (rsin(theta)-1000cos(theta))
y2 = int(y0 - 1000*(a))
# cv2.line draws a line in img from the point(x1,y1) to (x2,y2).
# (0,0,255) denotes the colour of the line to be
# drawn. In this case, it is red.
cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
# All the changes made in the input image are finally
# written on a new image houghlines.jpg
cv2.imwrite('linesDetected.jpg', img)
So how do I now subtract the line ( red highlights ) from the original image?
Thank you.

Having (x1, y1) and (x2, y2) you can slice the image in two parts like:
img_left = img[0:x1, 0:y1]
img_right = img[0:x2, 0:y2]
And then join them back:
final_img = np.concatenate((img_left, img_right), axis=1)

Related

matplotlib draw a contour line on a colorbar plot

I used below code to generate the colorbar plot of an image:
plt.imshow(distance)
cb = plt.colorbar()
plt.savefig(generate_filename("test_images.png"))
cb.remove()
The image looks likes this:
I want to draw a single contour line on this image where the signed distance value is equal to 0. I checked the doc of pyplot.contour but it needs a X and Y vector that represents the coordinates and a Z that represents heights. Is there a method to generate X, Y, and Z? Or is there a better function to achieve this? Thanks!
If you leave out X and Y, by default, plt.contour uses the array indices (in this case the range 0-1023 in both x and y).
To only draw a contour line at a given level, you can use levels=[0]. The colors= parameter can fix one or more colors. Optionally, you can draw a line on the colorbar to indicate the value of the level.
import matplotlib.pyplot as plt
import numpy as np
from scipy import ndimage # to smooth a test image
# create a test image with similar properties as the given one
np.random.seed(20221230)
distance = np.pad(np.random.randn(1001, 1001), (11, 11), constant_values=-0.02)
distance = ndimage.filters.gaussian_filter(distance, 100)
distance -= distance.min()
distance = distance / distance.max() * 0.78 - 0.73
plt.imshow(distance)
cbar = plt.colorbar()
level = 0
color = 'red'
plt.contour(distance, levels=[level], colors=color)
cbar.ax.axhline(level, color=color) # show the level on the colorbar
plt.show()
Reference: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.contour.html
You can accomplish this by setting the [levels] parameter in contour([X, Y,] Z, [levels], **kwargs).
You can draw contour lines at the specified levels by giving an array that is in increasing order.
import matplotlib.pyplot as plt
import numpy as np
x = y = np.arange(-3.0, 3.0, 0.02)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-X ** 2 - Y ** 2)
Z2 = np.exp(-(X - 1) ** 2 - (Y - 1) ** 2)
Z3 = np.exp(-(X + 1) ** 2 - (Y + 1) ** 2)
Z = (Z1 - Z2 - Z3) * 2
fig, ax = plt.subplots()
im = ax.imshow(Z, interpolation='gaussian',
origin='lower', extent=[-4, 4, -4, 4],
vmax=abs(Z).max(), vmin=-abs(Z).max())
plt.colorbar(im)
CS = ax.contour(X, Y, Z, levels=[0.9], colors='black')
ax.clabel(CS, fmt='%1.1f', fontsize=12)
plt.show()
Result (levels=[0.9]):

How to calculate the common volume/intersection between 2, 2D kde plots in python?

I have 2 sets of datapoints:
import random
import pandas as pd
A = pd.DataFrame({'x':[random.uniform(0, 1) for i in range(0,100)], 'y':[random.uniform(0, 1) for i in range(0,100)]})
B = pd.DataFrame({'x':[random.uniform(0, 1) for i in range(0,100)], 'y':[random.uniform(0, 1) for i in range(0,100)]})
For each one of these dataset I can produce the jointplot like this:
import seaborn as sns
sns.jointplot(x=A["x"], y=A["y"], kind='kde')
sns.jointplot(x=B["x"], y=B["y"], kind='kde')
Is there a way to calculate the "common area" between these 2 joint plots ?
By common area, I mean, if you put one joint plot "inside" the other, what is the total area of intersection. So if you imagine these 2 joint plots as mountains, and you put one mountain inside the other, how much does one fall inside the other ?
EDIT
To make my question more clear:
import matplotlib.pyplot as plt
import scipy.stats as st
def plot_2d_kde(df):
# Extract x and y
x = df['x']
y = df['y']
# Define the borders
deltaX = (max(x) - min(x))/10
deltaY = (max(y) - min(y))/10
xmin = min(x) - deltaX
xmax = max(x) + deltaX
ymin = min(y) - deltaY
ymax = max(y) + deltaY
# Create meshgrid
xx, yy = np.mgrid[xmin:xmax:100j, ymin:ymax:100j]
# We will fit a gaussian kernel using the scipy’s gaussian_kde method
positions = np.vstack([xx.ravel(), yy.ravel()])
values = np.vstack([x, y])
kernel = st.gaussian_kde(values)
f = np.reshape(kernel(positions).T, xx.shape)
fig = plt.figure(figsize=(13, 7))
ax = plt.axes(projection='3d')
surf = ax.plot_surface(xx, yy, f, rstride=1, cstride=1, cmap='coolwarm', edgecolor='none')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('PDF')
ax.set_title('Surface plot of Gaussian 2D KDE')
fig.colorbar(surf, shrink=0.5, aspect=5) # add color bar indicating the PDF
ax.view_init(60, 35)
I am interested in finding the interection/common volume (just the number) of these 2 kde plots:
plot_2d_kde(A)
plot_2d_kde(B)
Credits: The code for the kde plots is from here
I believe this is what you're looking for. I'm basically calculating the space (integration) of the intersection (overlay) of the two KDE distributions.
A = pd.DataFrame({'x':[random.uniform(0, 1) for i in range(0,100)], 'y':[random.uniform(0, 1) for i in range(0,100)]})
B = pd.DataFrame({'x':[random.uniform(0, 1) for i in range(0,100)], 'y':[random.uniform(0, 1) for i in range(0,100)]})
# KDE fro both A and B
kde_a = scipy.stats.gaussian_kde([A.x, A.y])
kde_b = scipy.stats.gaussian_kde([B.x, B.y])
min_x = min(A.x.min(), B.x.min())
min_y = min(A.y.min(), B.y.min())
max_x = max(A.x.max(), B.x.max())
max_y = max(A.y.max(), B.y.max())
print(f"x is from {min_x} to {max_x}")
print(f"y is from {min_y} to {max_y}")
x = [a[0] for a in itertools.product(np.arange(min_x, max_x, 0.01), np.arange(min_y, max_y, 0.01))]
y = [a[1] for a in itertools.product(np.arange(min_x, max_x, 0.01), np.arange(min_y, max_y, 0.01))]
# sample across 100x100 points.
a_dist = kde_a([x, y])
b_dist = kde_b([x, y])
print(a_dist.sum() / len(x)) # intergral of A
print(b_dist.sum() / len(x)) # intergral of B
print(np.minimum(a_dist, b_dist).sum() / len(x)) # intergral of the intersection between A and B
The following code compares calculating the volume of the intersection either via scipy's dblquad or via taking the average value over a grid.
Remarks:
For the 2D case (and with only 100 sample points), it seems the delta's need to be quite larger than 10%. The code below uses 25%. With a delta of 10%, the calculated values for f1 and f2 are about 0.90, while in theory they should be 1.0. With a delta of 25%, these values are around 0.994.
To approximate the volume the simple way, the average needs to be multiplied by the area (here (xmax - xmin)*(ymax - ymin)). Also, the more grid points are considered, the better the approximation. The code below uses 1000x1000 grid points.
Scipy has some special functions to calculate the integral, such as scipy.integrate.dblquad. This is much slower than the 'simple' method, but a bit more precise. The default precision didn't work, so the code below reduces that precision considerably. (dblquad outputs two numbers: the approximate integral and an indication of the error. To only get the integral, dblquad()[0] is used in the code.)
The same approach can be used for more dimensions. For the 'simple' method, create a more dimensional grid (xx, yy, zz = np.mgrid[xmin:xmax:100j, ymin:ymax:100j, zmin:zmax:100j]). Note that a subdivision by 1000 in each dimension would create a grid that's too large to work with.
When using scipy.integrate, dblquad needs to be replaced by tplquad for 3 dimensions or nquad for N dimensions. This probably will also be rather slow, so the accuracy needs to be reduced further.
import numpy as np
import pandas as pd
import scipy.stats as st
from scipy.integrate import dblquad
df1 = pd.DataFrame({'x':np.random.uniform(0, 1, 100), 'y':np.random.uniform(0, 1, 100)})
df2 = pd.DataFrame({'x':np.random.uniform(0, 1, 100), 'y':np.random.uniform(0, 1, 100)})
# Extract x and y
x1 = df1['x']
y1 = df1['y']
x2 = df2['x']
y2 = df2['y']
# Define the borders
deltaX = (np.max([x1, x2]) - np.min([x1, x2])) / 4
deltaY = (np.max([y1, y2]) - np.min([y1, y2])) / 4
xmin = np.min([x1, x2]) - deltaX
xmax = np.max([x1, x2]) + deltaX
ymin = np.min([y1, y2]) - deltaY
ymax = np.max([y1, y2]) + deltaY
# fit a gaussian kernel using scipy’s gaussian_kde method
kernel1 = st.gaussian_kde(np.vstack([x1, y1]))
kernel2 = st.gaussian_kde(np.vstack([x2, y2]))
print('volumes via scipy`s dblquad (volume):')
print(' volume_f1 =', dblquad(lambda y, x: kernel1((x, y)), xmin, xmax, ymin, ymax, epsabs=1e-4, epsrel=1e-4)[0])
print(' volume_f2 =', dblquad(lambda y, x: kernel2((x, y)), xmin, xmax, ymin, ymax, epsabs=1e-4, epsrel=1e-4)[0])
print(' volume_intersection =',
dblquad(lambda y, x: np.minimum(kernel1((x, y)), kernel2((x, y))), xmin, xmax, ymin, ymax, epsabs=1e-4, epsrel=1e-4)[0])
Alternatively, one can calculate the mean value over a grid of points, and multiply the result by the area of the grid. Note that np.mgrid is much faster than creating a list via itertools.
# Create meshgrid
xx, yy = np.mgrid[xmin:xmax:1000j, ymin:ymax:1000j]
positions = np.vstack([xx.ravel(), yy.ravel()])
f1 = np.reshape(kernel1(positions).T, xx.shape)
f2 = np.reshape(kernel2(positions).T, xx.shape)
intersection = np.minimum(f1, f2)
print('volumes via the mean value multiplied by the area:')
print(' volume_f1 =', np.sum(f1) / f1.size * ((xmax - xmin)*(ymax - ymin)))
print(' volume_f2 =', np.sum(f2) / f2.size * ((xmax - xmin)*(ymax - ymin)))
print(' volume_intersection =', np.sum(intersection) / intersection.size * ((xmax - xmin)*(ymax - ymin)))
Example output:
volumes via scipy`s dblquad (volume):
volume_f1 = 0.9946974276169385
volume_f2 = 0.9928998852123891
volume_intersection = 0.9046421634401607
volumes via the mean value multiplied by the area:
volume_f1 = 0.9927873844924111
volume_f2 = 0.9910132867915901
volume_intersection = 0.9028999384136771

How to convert a python palette to an array of colors of a given size?

It needs to fill the space between the 10 graphs that take place above each other with the function
plt.fill_between(x, y1, y2, color= 'here_is_color', alpha=0.5)
The colors should change, for example, from red to blue from top to bottom. To do this, I think you can convert a given palette to a color array and use it in a loop until the space between the graphs is filled.
I share the solution I found:)
col = cm.get_cmap('RdBu', size)
num=1.0
Next, change the num counter in the loop as needed, the function will look like
plt.fill_between(x, y1, y2, color=col(num))
You can use the color palettes defined for matplit-lib:
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
Get the colors and sort them by hsv (or supply the names in the order you want - see matplotlib.org named_colors.html example
def get_hsv_colors():
# https://matplotlib.org/3.1.0/gallery/color/named_colors.html
by_hsv = sorted((tuple(mcolors.rgb_to_hsv(mcolors.to_rgb(color))), name)
for name, color in mcolors.CSS4_COLORS.items())
return [name for _, name in by_hsv]
Create some demo data for stacked y's:
def get_y_data():
y1,y2,y3,y4,y5,y6 = np.random.rand(6,20)
y2 += y1 + 0.01
y3 += y2 + 0.01
y4 += y3 + 0.01
y5 += y4 + 0.01
y6 += y5 + 0.01
return y1,y2,y3,y4,y5,y6
Plug it together:
# get the color-names from above or supply your own names:
cols_to_use = get_hsv_colors()[20::5] # skip the greys, only take every 5th name
# zipp the data with the corresponding color
matched_y_data = list( zip((y1,y2,y3,y4,y5,y6), cols_to_use))
# plot data with lines
for (y,c) in matched_y_data:
plt.plot(x, y, color=c)
plt.show()
# plot the same using fill_between
fig, ax1 = plt.subplots(1, 1, sharex=True, figsize=(6, 6))
# base line y -> 0:
ax1.fill_between(x, matched_y_data[0][0], 0, color=matched_y_data[0][1])
# intermediate lines y_n -> y_n+1:
for (idx, (y_data, col)) in enumerate(matched_y_data[:-1]):
ax1.fill_between(x, y_data, matched_y_data[idx+1][0], color=col)
# last line
mm = max( matched_y_data[-1][0] ) + 0.1
ax1.fill_between(x, matched_y_data[-1][0], mm, color=matched_y_data[-1][1])
ax1.set_xlabel('x')
fig.tight_layout()
plt.show()
to get
and
You can get your colorscheme by carefully choosing your color names:
# zipp the data with the corresponding color
matched_y_data = list(
zip((y1,y2,y3,y4,y5,y6),
"royalblue cornflowerblue lightsteelblue mistyrose lightsalmon tomato".split()))

How to use `extent` in matplotlib ax.imshow() without changing the positions of the overlayed ax.text() handles?

I am trying to annotate a heatmap. The matplotlib docs present an example, which suggests creating a helper function to format the annotations. I feel there must be a simpler way to do what I want. I can annotate inside the boxes of the heatmap, but these texts change position when editing the extent of the heatmap. My question is how to use extent in ax.imshow(...) while also using ax.text(...) to annotate the correct positions. Below is an example:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize
def get_manhattan_distance_matrix(coordinates):
shape = (coordinates.shape[0], 1, coordinates.shape[1])
ct = coordinates.reshape(shape)
displacement = coordinates - ct
return np.sum(np.abs(displacement), axis=-1)
x = np.arange(11)[::-1]
y = x.copy()
coordinates = np.array([x, y]).T
distance_matrix = get_manhattan_distance_matrix(coordinates)
# print("\n .. {} COORDINATES:\n{}\n".format(coordinates.shape, coordinates))
# print("\n .. {} DISTANCE MATRIX:\n{}\n".format(distance_matrix.shape, distance_matrix))
norm = Normalize(vmin=np.min(distance_matrix), vmax=np.max(distance_matrix))
This is where to modify the value of extent.
extent = (np.min(x), np.max(x), np.min(y), np.max(y))
# extent = None
According to the matplotlib docs, the default extent is None.
fig, ax = plt.subplots()
handle = ax.imshow(distance_matrix, cmap='plasma', norm=norm, interpolation='nearest', origin='upper', extent=extent)
kws = dict(ha='center', va='center', color='gray', weight='semibold', fontsize=5)
for i in range(len(distance_matrix)):
for j in range(len(distance_matrix[i])):
if i == j:
ax.text(j, i, '', **kws)
else:
ax.text(j, i, distance_matrix[i, j], **kws)
plt.show()
plt.close(fig)
One can generate two figures by modifying extent - simply uncomment the commented line and comment the uncommented line. The two figures are below:
One can see that by setting extent, the pixel locations change, which in turn changes the positions of the ax.text(...) handles. Is there a simple solution to fix this - that is, set an arbitrary extent and still have the text handles centered in each box?
When extent=None, the effective extent is from -0.5 to 10.5 in both x and y. So the centers lie on the integer positions. Setting the extent from 0 to 10 doesn't align with the pixels. You'd have to multiply by 10/11 to get them right.
The best approach would be to set extent = (np.min(x)-0.5, np.max(x)+0.5, np.min(y)-0.5, np.max(y)+0.5) to get the centers back at integer positions.
Also note that default an image is displayed starting from the top, and that the y-axis is reversed. If you change the extent, to get the image upright, you need ax.imshow(..., origin='lower'). (The 0,0 pixel should be the blue one in the example plot.)
To put a text in the center of a pixel, you can add 0.5 to the horizontal index, divide by the width in pixels and multiply by the difference of the x-axis. And the similar calculation for the y-axis. To get better readability, the text color can be made dependent on the pixel color.
# ...
extent = (np.min(x), np.max(x), np.min(y), np.max(y))
x0, x1, y0, y1 = extent
fig, ax = plt.subplots()
handle = ax.imshow(distance_matrix, cmap='plasma', norm=norm, interpolation='nearest', origin='lower', extent=extent)
kws = dict(ha='center', va='center', weight='semibold', fontsize=5)
height = len(distance_matrix)
width = len(distance_matrix[0])
for i in range(height):
for j in range(width):
if i != j:
val = distance_matrix[i, j]
ax.text(x0 + (j + 0.5) / width * (x1 - x0), y0 + (i + 0.5) / height * (y1 - y0),
f'{val}\n{i},{j}', color='white' if norm(val) < 0.6 else 'black', **kws)
plt.show()

Inverted pixel coordinates of segmentation mask

The following is an image of a segmentation mask (it appears yellow). Overlaid onto this mask are the pixels/coordinates of the very same segmentation mask (appears blue).
My question is: why are these pixels/coordinates inverted, transparent and split at the diagonal? Why are they not plotted as a complete "fill", such as the mask itself?
My goal is for these coordinates to appear in "normal" (x,y) linear order. Code:
from matplotlib import patches
import numpy as np
# create mask
mask = np.zeros((350, 525), dtype=np.uint8)
# populate region of mask
mask[2:222,42:521] = 1
# get coordinates of populated region
y, x = np.where(mask == 1)
pts = np.column_stack([x, y])
# define figure, axes, title
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.set_title('Segmentation mask pixel coordinates')
# show mask
plt.imshow(mask, interpolation='none')
# add mask points
poly = patches.Polygon(pts)
ax.add_patch(poly)
plt.show()
In your example len(pts) gives 105380 because pts contains all the points of the mask in row-based order. So poly has a snake-like shape with length=105380 and width=1. The snake starts in the upper left corner and ends in the lower right - that's why you have diagonal line.
To correct the plot you may do the following modification:
# borders
(x1, y1), (x2, y2) = pts.min(axis=0), pts.max(axis=0)
# corners
pts_for_poly = list(zip((x1, x2, x2, x1), (y1, y1, y2, y2)))
# rectangle polygon
poly = patches.Polygon(pts_for_poly)
I hope now it looks kinda like expected or close to that.

Resources