Related
I'm attempting to extend the 'tail' of an arrow. So far I've been able to draw a line through the center of the arrow, but this line extends 'both' ways, rather than in just one direction. The script below shows my progress. Ideally I would be able to extend the tail of the arrow regardless of the orientation of the arrow image. Any suggestions on how to accomplish this. Image examples below, L:R start, progress, goal.
# import image and grayscale
image = cv2.imread("image path")
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.imshow("original",image)
# inverts black and white
gray = 255 - image
cv2.imshow("Inverted", gray)
# Extend the borders for the line
extended = cv2.copyMakeBorder(gray, 20, 20, 10, 10, cv2.BORDER_CONSTANT)
cv2.imshow("extended borders", extended)
# contour finding
contours, hierarchy = cv2.findContours(extended, 1, 2)
cont = contours[0]
rows,cols = extended.shape[:2]
[vx,vy,x,y] = cv2.fitLine(cont, cv2.DIST_L2,0,0.01,0.01)
leftish = int((-x*vy/vx) + y)
rightish = int(((cols-x)*vy/vx)+y)
line = cv2.line(extended,(cols-1,rightish),(0,leftish),(255,255,255), 6)
cv2.imshow("drawn line", line)
"Moments" can be strange things. They're building blocks and show up most often in statistics.
It helps to have a little background in statistics, and see the application of those calculations to image data, which can be considered a set of points. If you've ever calculated the weighted average or "centroid" of something, you'll recognize some of the sums that show up in "moments".
Higher order moments can be building blocks to higher statistical measures such as covariance and skewness.
Using covariance, you can calculate the major axis of your set of points, or your arrow in this case.
Using skewness, you can figure out which side of a distribution is heavier than the other... i.e. which side is the arrow's tip and which is its tail.
This should give you a very precise angle. The scale/radius however is best estimated using other ways. You'll notice that the radius estimated from the area of the arrow fluctuates a little. You could find the points belonging to the arrow that are furthest away from the center, and take that as a somewhat stable length.
Here's a longish program that implements the two ideas above and shows the direction of an arrow:
#!/usr/bin/env python3
import os
import sys
import numpy as np
import cv2 as cv
# utilities to convert between 2D vectors and complex numbers
# complex numbers are handy for rotating stuff
def to_complex(vec):
assert vec.shape[-1] == 2
if vec.dtype == np.float32:
return vec.view(np.complex64)
elif vec.dtype == np.float64:
return vec.view(np.complex128)
else:
assert False, vec.dtype
def from_complex(cplx):
if cplx.dtype == np.complex64:
return cplx.view(np.float32)
elif cplx.dtype == np.complex128:
return cplx.view(np.float64)
else:
assert False, cplx.dtype
# utilities for drawing with fractional bits of position
# just to make a pretty picture
def iround(val):
return int(round(val))
def ipt(vec, shift=0):
if isinstance(vec, (int, float)):
return iround(vec * 2**shift)
elif isinstance(vec, (tuple, list, np.ndarray)):
return tuple(iround(el * 2**shift) for el in vec)
else:
assert False, type(vec)
# utilities for affine transformation
# just to make a pretty picture
def rotate(degrees=0):
# we want positive rotation
# meaning move +x towards +y
# getRotationMatrix2D does it differently
result = np.eye(3).astype(np.float32)
result[0:2, 0:3] = cv.getRotationMatrix2D(center=(0,0), angle=-degrees, scale=1.0)
return result
def translate(dx=0, dy=0):
result = np.eye(3).astype(np.float32)
result[0:2,2] = [dx, dy]
return result
# main logic
def calculate_direction(im):
# using "nonzero" (default behavior) is a little noisy
mask = (im >= 128)
m = cv.moments(mask.astype(np.uint8), binaryImage=True)
# easier access... see below for details
m00 = m['m00']
m10 = m['m10']
m01 = m['m01']
mu00 = m00
mu20 = m['mu20']
mu11 = m['mu11']
mu02 = m['mu02']
nu30 = m['nu30']
nu03 = m['nu03']
# that's just the centroid
cx = m10 / m00
cy = m01 / m00
centroid = np.array([cx, cy]) # as a vector
# and that's the size in pixels:
size = m00
# and that's an approximate "radius", if it were a circle which it isn't
radius = (size / np.pi) ** 0.5
# (since the "size" in pixels can fluctuate due to resampling, so will the "radius")
# wikipedia helpfully mentions "image orientation" as an example:
# https://en.wikipedia.org/wiki/Image_moment#Examples_2
# we'll use that for the major axis
mup20 = mu20 / mu00
mup02 = mu02 / mu00
mup11 = mu11 / mu00
theta = 0.5 * np.arctan2(2 * mup11, mup20 - mup02)
#print(f"angle: {theta / np.pi * 180:+6.1f} degrees")
# we only have the axis, not yet the direction
# we will assess "skewness" now
# https://en.wikipedia.org/wiki/Skewness#Definition
# note how "positive" skewness appears in a distribution:
# it points away from the heavy side, towards the light side
# fortunately, cv.moments() also calculates those "standardized moments"
# https://en.wikipedia.org/wiki/Standardized_moment#Standard_normalization
skew = np.array([nu30, nu03])
#print("skew:", skew)
# we'll have to *rotate* that so it *roughly* lies along the x axis
# then assess which end is the heavy/light end
# then use that information to maybe flip the axis,
# so it points in the direction of the arrow
skew_complex = to_complex(skew) # reinterpret two reals as one complex number
rotated_skew_complex = skew_complex * np.exp(1j * -theta) # rotation
rotated_skew = from_complex(rotated_skew_complex)
#print("rotated skew:", rotated_skew)
if rotated_skew[0] > 0: # pointing towards tail
theta = (theta + np.pi) % (2*np.pi) # flip direction 180 degrees
else: # pointing towards head
pass
print(f"angle: {theta / np.pi * 180:+6.1f} degrees")
# construct a vector that points like the arrow in the picture
direction = np.exp([1j * theta])
direction = from_complex(direction)
return (radius, centroid, direction)
def draw_a_picture(im, radius, centroid, direction):
height, width = im.shape[:2]
# take the source at half brightness
canvas = cv.cvtColor(im // 2, cv.COLOR_GRAY2BGR)
shift = 4 # prettier drawing
cv.circle(canvas,
center=ipt(centroid, shift),
radius=ipt(radius, shift),
thickness=iround(radius * 0.1),
color=(0,0,255),
lineType=cv.LINE_AA,
shift=shift)
# (-direction) meaning point the *opposite* of the arrow's direction, i.e. towards tail
cv.line(canvas,
pt1=ipt(centroid + direction * radius * -3.0, shift),
pt2=ipt(centroid + direction * radius * +3.0, shift),
thickness=iround(radius * 0.05),
color=(0,255,255),
lineType=cv.LINE_AA,
shift=shift)
cv.line(canvas,
pt1=ipt(centroid + (-direction) * radius * 3.5, shift),
pt2=ipt(centroid + (-direction) * radius * 4.5, shift),
thickness=iround(radius * 0.15),
color=(0,255,255),
lineType=cv.LINE_AA,
shift=shift)
return canvas
if __name__ == '__main__':
imfile = sys.argv[1] if len(sys.argv) >= 2 else "p7cmR.png"
src = cv.imread(imfile, cv.IMREAD_GRAYSCALE)
src = 255 - src # invert (white arrow on black background)
height, width = src.shape[:2]
diagonal = np.hypot(height, width)
outsize = int(np.ceil(diagonal * 1.3)) # fudge factor
cv.namedWindow("arrow", cv.WINDOW_NORMAL)
cv.resizeWindow("arrow", 5*outsize, 5*outsize)
angle = 0 # degrees
increment = +1
do_spin = True
while True:
print(f"{angle:+.0f} degrees")
M = translate(dx=+outsize/2, dy=+outsize/2) # rotate(degrees=angle) # translate(dx=-width/2, dy=-height/2)
im = cv.warpAffine(src, M=M[:2], dsize=(outsize, outsize), flags=cv.INTER_CUBIC, borderMode=cv.BORDER_REPLICATE)
# resampling introduces blur... except when it's an even number like 0 degrees, 90 degrees, ...
# so at even rotations, things will jump a little.
# this rotation is only for demo purposes
(radius, centroid, direction) = calculate_direction(im)
canvas = draw_a_picture(im, radius, centroid, direction)
cv.imshow("arrow", canvas)
if do_spin:
angle = (angle + increment) % 360
print()
key = cv.waitKeyEx(30 if do_spin else -1)
if key == -1:
continue
elif key in (0x0D, 0x20): # ENTER (CR), SPACE
do_spin = not do_spin # toggle spinning
elif key == 27: # ESC
break # end program
elif key == 0x250000: # VK_LEFT
increment = -abs(increment)
angle += increment
elif key == 0x270000: # VK_RIGHT
increment = +abs(increment)
angle += increment
else:
print(f"key 0x{key:02x}")
cv.destroyAllWindows()
I'm trying to use otsu's method in order to separate the background from the foreground of this picture. I'm doing this by breaking the picture into it's colour components and then finding the threshold values for the green and red components. I'll later decide if something is foreground by seeing if it has a high enough green value, but low enough red value. The problem is that I keep getting thresholds of around 245 which I feel like is not what they should be if you look at my histogram.
I feel like the thresholds should be before the 2 green peak at around 150?
Thank-you
def otsu_helper(counts):
'''
Does the arithmetic of the otsu method
:param counts: An array recording how many pixels are in each bin
:return: A threshold found by minimizing the weighted sum of group variances σ^(2)w(t)
'''
# variables for otsu's method
m1t = 0 # mean prob for first group
q1t = 0 # prob that a given greylevel < t
s21t = 0 # variace for the group of values less that t
tot = 0 #keep track of the total amount of pixels captured to bins so far
pix = sum(counts) #The total number of pixels
print(counts)
group1 = np.zeros(shape=(256,2))
#Calculate the mean probability and variances for group1 for all values of t
for i in range(0,256):
j = i+1
bin = counts\[i\]
#print(i,":",bin)
tot += bin
pi = bin/pix
q1t += tot/pix
if (q1t != 0):
m1t += (j * pi )/q1t
s21t += ( (j - m1t)**2 * pi )/q1t
group1\[i\] = \[q1t,s21t\] # Store the probability and variance for later use in finding the sum of group variances
else:
group1\[i\] = \[None,None\]
tot = 0
q2t = 0 # prob that a given greylevel > t
m2t = 0 # mean prob for second group
s22t = 0 # variace for the group of values greater than t
group2 = np.zeros(shape=(256,2))
group2\[255\] = \[None,None\] #At threshold 256, there are no values greater than t
# Calculate the mean probability and variances for group2 for all values of t
for i in range(254, -1, -1):
j = i+1
bin = counts\[j\] #Because all the values from t+1 to G are summed
tot += bin
pi = (bin / pix)
q2t += tot / pix
if (q2t != 0):
m2t += (j*pi)/q2t
s22t += ((j - m2t)**2 * pi)/q2t
group2\[i\] = \[q2t, s22t\]
else:
group2\[i\] = \[None, None\]
s2w = 999999999999 # Weight sum of group variances
t = 0 #The optimal threshold value
#Calculate the weighted sum of group variances for all values of t and save the minimum
for i in range(0,256):
if (not np.isnan(group1\[i,1\]) and not np.isnan(group2\[i:1\])):
s2wt = group1\[i,0\]*group1\[i,1\] + group2\[i,0\]*group2\[i,1\]
print(i,":","{0:.{1}e}".format(s2wt, 1))
if s2w > s2wt:
s2w = s2wt
t = i
return t
def otsu(I):
'''
Finds the optimal threshold of an image
:param I: Input image to find the threshold
:return:A tuple of values that minimizes weighted sum of group variances σ^(2)w(t) for the red and green channel
'''
#Isolating the colour channels and placing them in bins
red = 256 * I\[:, :, 0\] # Zero out contribution from green
green = 256 * I\[:, :, 1\]
blue = 256 * I\[:, :, 2\]
#red, green, blue = np.moveaxis(I, -1, 0)
bins = np.array(range(0,257))
g_counts,pixels = np.histogram(green,bins)
r_counts,pixels = np.histogram(red, bins)
b_counts,pixels = np.histogram(blue,bins)
print('gt = otsu_helper(g_counts)')
gt = otsu_helper(g_counts)
print('rt = otsu_helper(r_counts)')
rt = otsu_helper(r_counts)
# return (gt,rt)
# Creating a histogram of the green colour channel
pixels = pixels\[:-1\]
plt.bar(pixels, g_counts, align='center')
plt.xlim(-1, 256)
plt.show()
return (gt,rt)
I'm running quite a complex code so I won't bother with details as I've had it working before but now im getting this error.
Particle is a 3D tuple filled with 0 or 255, and I am using the scipy centre of mass function and then trying to turn the value into its closest integer (as I'm dealing with arrays). The error is found with on the last line... can anyone explain why this might be??
2nd line fills Particle
3rd line deletes any surrounding particles with a different label (This is in a for loop for all labels)
Particle = []
Particle = big_labelled_stack[x_start+20:x_stop+20,y_start+20:y_stop+20,z_start+20:z_stop+20]
Particle = np.where(Particle == i ,255,0)
CoM = scipy.ndimage.measurements.center_of_mass(Particle)
CoM = [ (int(round(x)) for x in CoM ]
Thanks in advance. If you need more code just ask but I dont think it will help you and its very messy.
################## MORE CODE
border = 30
[labelled_stack,no_of_label] = label(labelled,structure_array,output_type)
# RE-LABEL particles now no. of seeds has been reduced! LAST LABELLING
#Increase size of stack by increasing borders and equal them to 0; to allow us to cut out particles into cube shape which else might lye outside the border
h,w,l = labelled.shape
big_labelled_stack = np.zeros(shape=(h+60,w+60,l+60),dtype=np.uint32)
# Creates an empty border around labelled_stack full of zeros of size border
if (no_of_label > 0): #Small sample may return no particles.. so this stage not neccesary
info = np.zeros(shape=(no_of_label,19)) #Creates array to store coordinates of particles
for i in np.arange(1,no_of_label,1):
coordinates = find_objects(labelled_stack == i)[0] #Find coordinates of label i.
x_start = int(coordinates[0].start)
x_stop = int(coordinates[0].stop)
y_start = int(coordinates[1].start)
y_stop = int(coordinates[1].stop)
z_start = int(coordinates[2].start)
z_stop = int(coordinates[2].stop)
dx = (x_stop - x_start)
dy = (y_stop - y_start)
dz = (z_stop - z_start)
Particle = np.zeros(shape=(dy,dx,dz),dtype = np.uint16)
Particle = big_labelled_stack[x_start+30:x_start+dx+30,y_start+30:y_start+dy+30,z_start+30:z_start+dz+30]
Particle = np.where(Particle == i ,255,0)
big_labelled_stack[border:h+border,border:w+border,border:l+border] = labelled_stack
big_labelled_stack = np.where(big_labelled_stack == i , 255,0)
CoM_big_stack = scipy.ndimage.measurements.center_of_mass(big_labelled_stack)
C = np.asarray(CoM_big_stack) - border
if dx > dy:
b = dx
else: #Finds the largest of delta_x,y,z and saves as b, so that we create 'Cubic_Particle' of size 2bx2bx2b (cubic box)
b = dy
if dz > b:
b = dz
CoM = scipy.ndimage.measurements.center_of_mass(Particle)
CoM = [ (int(round(x))) for x in CoM ]
Cubic_Particle = np.zeros(shape=(2*b,2*b,2*b))
Cubic_Particle[(b-CoM[0]):(b+dx-CoM[0]),(b-CoM[1]):(b+dy-CoM[1]),(b-CoM[2]):(b+dz-CoM[2])] = Particle
volume = Cubic_Particle.size # Gives volume of the box in voxels
info[i-1,:] = [C[0],C[1],C[2],i,C[0]-b,C[1]-b,C[2]-b,C[0]+b,C[1]+b,C[2]+b,volume,0,0,0,0,0,0,0,0] # Fills an array with label.No., size of box, and co-ords
else:
print('No particles found, try increasing the sample size')
info = []
Ok, so I have a stack full of labelled particles, there are two things I am trying to do, first find the centre of masses of each particle with respect ot the labelled_stack which is what CoM_big_labelled_stack (and C) does. and stores the co-ords in a list (tuple) called info. I am also trying to create a cubic box around the particle, with its centre of mass as the centre (which is relating to the CoM variable), so first I use the find objects function in scipy to find a particle, i then use these coordinates to create a non-cubic box around the particle, and find its centre of mass.I then find the longest dimension of the box and call it b, creating a cubic box of size 2b and filling it with particle in the right position.
Sorry this code is a mess, I am very new to Python
I have a program which gets the contours of an image. I want to break these contours into sub-contours based on relative straightness. If the straightness of an extra pixel is less than a threshold value times the previous straightness, the contour should be split. When I put an image of a square drawn in paint, the program output should be 4 sub-contours. However, for some reason I get 3 sub-contours with 2 of them including 2-3 points and one including most of the original contour. Note, the 0.85 in my code is a threshold derived from a research paper. dPreious is the pixel distance of the edge(Not Including the Current Pixel). v is the euclidean distance between end-points.
def split(contour):
new = [];
toAdd = [];
sCurrent = 0.0
sPrevious = 0.0
v = 0.0
dPrevious = 1
for points in range(0,len(contour)):
currentX = float(contour[points][0])
currentY = float(contour[points][1])
if points != 0:
v = math.sqrt(math.pow(float(toAdd[0][0])-currentX,2) + math.pow(float(toAdd[0][1])-currentY,2))
dPrevious = len(toAdd)
sCurrent = v/dPrevious
if sCurrent < 0.85*sPrevious:
new.append(toAdd)
toAdd = []
toAdd.append(contour[points])
sPrevious = sCurrent
new.append(toAdd)
return new
If you use OpenCV for finding the contours you can use the functionality of the cv2.approxPolyDP() function to do the job for you:
cv2.approxPolyDP. You might have to experiment with the epsilon parameter to have succes with it.
I'm new to the coding world, so take it easy on me here. I'm trying to create a numeric simulations of a square pan being heated at exactly one point i.e. with a wire. Unfortunately, my functions keep failing my doc.tests and I don't understand why. Any help would be much appreciated
import sys
sys.path.append('/home/courses/python')
from logic import *
from numpy import *
def create_plate(size, plate_temperature):
plate_temperatures = empty([size, size])
plate_temperatures[:,:] = plate_temperature
return plate_temperatures
def simulate_plate(plate_temperatures,hot_x,hot_y, hot_temp,air_temp, htc,hcc,ahtc):
""" Simulate one time step on the plate, given array of temperatures, etc. """
precondition()
plate_temperatures[hot_x,hot_y] = hot_temp
#number rows = len(input)
#number cols = len(input[0])
# First, calculate heat flow _into_ every 1cmx1cm square
heat_in = zeros_like(plate_temperatures)
# The "length" of an array is the number of rows; the length of a row is the number of columns.
#because the plate is always a square precondition range(len(plate_temperatures)) ==range(len(plate_temperatures[row_place]))
for row_place in range(len(plate_temperatures)): # is this accurate?
for column_place in range(len(plate_temperatures[row_place])):
plate_temperatures[hot_x,hot_y] = hot_temp
faces = 2 # Number of faces exposed to the air, top and bottom of plate
if(column_place!=0): #square is not on left edge of plate
from_left = (plate_temperatures[row_place,column_place-1] - plate_temperatures[row_place,column_place]) * htc # heat from left
else:#square is on left edge; nothing to the left of it
from_left = 0
if(column_place<(len(plate_temperatures[row_place])-1)): #square is not on the right edge of plate
from_right = (plate_temperatures[row_place,column_place+1] - plate_temperatures[row_place,column_place]) * htc # heat from right
else: #square is on the right edge; nothing to the right of it
from_right = 0
if row_place !=0: #square is not on the top edge of plate
from_above =(plate_temperatures[row_place+1,column_place] - plate_temperatures[row_place,column_place]) * htc # heat from above
else: # square is on the top edge of plate
from_above = 0
if row_place< (len(plate_temperatures)-1):#square is not on the bottom edge of the plate
from_below = (plate_temperatures[row_place-1,column_place] - plate_temperatures[row_place,column_place]) * htc # heat from below
else:#square is on the bottom edge of the plate
from_below = 0
from_air = (air_temp - plate_temperatures[row_place,column_place]) * faces * ahtc # heat from air
heat_in[row_place,column_place] = from_left + from_right + from_above + from_below + from_air
# Now, add the heat we found above to the old temperatures
plate_temperatures[row_place, column_place] += (heat_in[row_place,column_place] * hcc)
# restore the temperature of the heated spot
plate_temperatures[hot_x,hot_y] = hot_temp
def simulate_plate_N_steps(plate_temperatures,hot_x,hot_y, hot_temp,air_temp, htc,hcc,ahtc, N):
""" Just call the one-time-step function N times... """
all_averages = [] # log of temperatures at end of time steps
for t in range(N):
simulate_plate(plate_temperatures,hot_x,hot_y, hot_temp,air_temp, htc,hcc,ahtc)
all_averages.append(average_temp(plate_temperatures))
print "Completed", N, "simulated time steps; end-step average temps were:", all_averages
# copied from http://docs.python.org/lib/module-doctest.html
def _test():
import doctest
result = doctest.testmod()
if result[0] == 0:
print "Wahoo! Passed all", result[1], __file__.split('/')[-1], "tests!"
else:
print "Rats!"
if __name__ == "__main__": _test()