How to make objects move in pygame, without making the rendering slow? - python

I am currently designing an app, using pygame in which I have a number of circles connected through lines, with numerical text written in them. These circles are green, blue and red in color, while the other things are black. Background is white. (Imagine it like a network graph)
My Objective: I am trying to get an animation running, in which the user selects the two circles (let us call them nodes) and I find out the shortest path between the sender node (green) to the receiver node (red). So in this animation, I am making another moving circle on top of the line (or edge) that connects the two adjacent nodes (these may be the intermediate nodes).
So far all good, here's the code of what I am doing:
def runPathAnimation(path, colortype):
for i in range(len(path)-1):
#Calculation of the center of the nodes
x1, y1 = (gmd[path[i]].getNodePosition())[0], (gmd[path[i]].getNodePosition())[1]
x2, y2 = (gmd[path[i+1]].getNodePosition())[0], (gmd[path[i+1]].getNodePosition())[1]
#Get the slope
m = (y1-y2)/(x1-x2) if x1 != x2 else 'undefined'
if str(m) != 'undefined':
c = y2-(m*x2)
if m > 0.5 or (m <= -1 and m >= -1.5):
for y in range(min(y1,y2),max(y1,y2)):
#using the equation of the line
x = int((y-c)/m)
#redrawEverything(path) #OPTION 1
#TRY REDRAW LINE #TODO
pyg.draw.rect(screen, (255, 255, 255), (x-10,y-10,20,20)) #OPTION 2
pyg.draw.circle(screen, colortype, (x,y), 10) #Moving circle
pyg.display.update() #Update Display
#NEED: Redraw!
#The logic repeats....
else:
for x in range(min(x1,x2),max(x1,x2)):
y = int(m*x+c)
#redrawEverything(path)
#TRY REDRAW LINE
pyg.draw.rect(screen, (255, 255, 255), (x-10,y-10,20,20))
pyg.draw.circle(screen, colortype, (x,y), 10)
pyg.display.update()
#NEED: Redraw!
else:
cy = range(min(y1,y2),max(y1,y2))
if y1 > y2:
cy = reversed(cy)
for y in cy:
#redrawEverything(path)
#TRY REDRAW LINE
pyg.draw.rect(screen, (255, 255, 255), (x1-10,y-10,20,20))
pyg.draw.circle(screen, colortype, (x1,y), 10)
pyg.display.update()
#NEED: Redraw!
My Problem: There is a lot of lag with my method of simply updating a circle with another position, without disturbing anything that it covers. I had 2 options in my mind:
OPTION 1: Update everything on the screen (of course it did not give me a good performance)
OPTION 2: Update only the portion of the screen, which is what actually used. However, even with this method, I am not able to achieve a good performance for screen updation. I would like to later add a feature to control the speed of the animation, which may have a speed faster than the maximum performance of my code right now!
As you can see, I do not have any time.sleep() as of now. I would like to increase the performance of my code and then be able to add time.sleep() for a more controlled animation. My current pygame application is already running in parallel to another process, which I implemented using multiprocessing library.
Question: How do I make it faster?
My python version: 3.7.0, pygame version: 1.9.6
PS: Sorry for the length of the question

Try using
pygame.time.Clock().tick(**)
This is a command that allows you to choose the FPS you want to run your program with, allowing you to increase your rendering speed. If you decide to use this, put an integer that represents the FPS where I wrote the asterisks.

So, I found a workaround! Basically, I am unable a make the code o any faster due to pygame's own rendering abilities, even HW mode isn't improving the speed much.
Solution (more of a workaround):
I have added a layer of waiting period in which pygame takes snapshots of the rendered screen and stores the image in a self created cache, without updating the screen. Later, I just have a smooth operable screen which can be used to see the animation.
Here's the code:
def runPathAnimation(path, colortype):
index = 0
images = []
for i in range(len(path)-1):
x1, y1 = (gmd[path[i]].getNodePosition())[0], (gmd[path[i]].getNodePosition())[1]
x2, y2 = (gmd[path[i+1]].getNodePosition())[0], (gmd[path[i+1]].getNodePosition())[1]
m = (y1-y2)/(x1-x2) if x1 != x2 else 'undefined'
cx, cy = range(min(x1,x2),max(x1,x2)), range(min(y1,y2),max(y1,y2))
if y1 > y2:
cy = reversed(cy)
if x1 > x2:
cx = reversed(cx)
if str(m) != 'undefined':
con = y2-(m*x2)
if m > 0.5 or (m <= -1 and m >= -1.5):
for y in cy:
ev = pyg.event.get()
x = int((y-con)/m)
images.append(loadpath(x,y,path,colortype,index))
index += 1
r = pyg.draw.rect(screen, colortype, (md.WIDTH_NETWORKPLOT-250,md.PLOT_AREA[1]+30,index/5,20), 2)
pyg.display.update(r)
else:
for x in cx:
ev = pyg.event.get()
y = int(m*x+con)
images.append(loadpath(x,y,path,colortype,index))
index += 1
r = pyg.draw.rect(screen, colortype, (md.WIDTH_NETWORKPLOT-250,md.PLOT_AREA[1]+30,index/5,20), 2)
pyg.display.update(r)
else:
for y in cy:
ev = pyg.event.get()
images.append(loadpath(x1,y,path,colortype,index))
index += 1
r = pyg.draw.rect(screen, colortype, (md.WIDTH_NETWORKPLOT-250,md.PLOT_AREA[1]+30,index/5,20), 2)
pyg.display.update(r)
print('Loading...'+str((i+1)/len(path)*100)+'%')
runAnimation(images)
def runAnimation(images):
animate = True
img = 0
print('Start!')
while animate:
ev = pyg.event.get()
pyg.event.pump()
keys = pyg.key.get_pressed()
if keys[pyg.K_LEFT]:
img -= 1
if img < 0:
img = 0
if keys[pyg.K_RIGHT]:
img += 1
if img >= len(images) - 2:
img = len(images) - 2
if keys[pyg.K_q]:
animate = False
screen.blit(images[img],(0,0))
pyg.display.update((0, 0, md.WIDTH_NETWORKPLOT, md.PLOT_AREA[1]))
PS: In my code, md.xxx are the dimensions for my matplotlib and pygame screen.
IMPORTANT: This is just a workaround, not a solution!!

Related

Cannot read the second arm of the Analog Clock correctly by using opencv python

Picture Link since I cannot upload it> Thank you https://github.com/HassanAdamm that I can be able to continue the further code but still cannot display the correct second hand of the Analog Clock with OpenCV. Hour and Minute Hands are successfully done with HoughLineP(). I am unable to separate the seconds hand from the image. Below is my working code and hope you guys can help me with this!
import cv2
import math
import numpy as np
import tkinter as tk
from matplotlib import pyplot as plt
from math import sqrt, acos, degrees
# Reading the input image and convert the original RGB to a grayscale image
kernel = np.ones((5, 5), np.uint8)
img1 = cv2.imread('input1.jpg')
img = cv2.imread('input1.jpg', 0)
img_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
# Appling a binary threshold to the image
ret, thresh = cv2.threshold(img_gray, 50, 255, cv2.THRESH_BINARY)
# Create mask
height, width = img.shape
mask = np.zeros((height, width), np.uint8)
edges = cv2.Canny(thresh, 100, 200)
# Circle Detection
cimg = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
circles = cv2.HoughCircles(img_gray, cv2.HOUGH_GRADIENT, 1.2, 100)
for i in circles[0,:]:
i[2] = i[2] + 4
# cv2.cicle(image, center_coordinates, radius, color, thickness)
cv2.circle(mask, (int(i[0]),int(i[1])), int(i[2]), (255,255,255), thickness = -1)
# Copy that image using that mask
masked_data = cv2.bitwise_and(img1, img1, mask = mask)
# Apply threshold
_,thresh = cv2.threshold(mask, 1, 255, cv2.THRESH_BINARY)
# Find Contour
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
x, y, w, h = cv2.boundingRect(contours[0])
# Crop masked_data
crop = masked_data[y + 30 : y + h -30, x + 30 : x + w - 30]
height, width, channel = crop.shape
blur_crop = cv2.GaussianBlur(crop, (5, 5), 0)
edges = cv2.Canny(blur_crop, 50, 150)
# Line segments
line_image = np.copy(crop) * 0
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 15, np.array([]), 100, 10)
l = []
xl1, xl2, yl1, yl2 = 0, 0, 0, 0 #long -> l
xm1, xm2, ym1, ym2 = 0, 0, 0, 0 #medium -> m
xs1, xs2, ys1, ys2 = 0, 0, 0, 0 #short -> s
# Getting the values from the line
for line in lines:
x1, y1, x2, y2 = line[0]
dx = x2 - x1
if (dx < 0):
dx = dx* (-1)
dy = y2 - y1
if (dy < 0):
dy = dy* (-1)
hypo = sqrt(dx**2 + dy**2)
l.append(hypo)
l.sort(reverse=True)
s, m, h = 0, 0, 0
for f in range(len(l)):
for line in lines:
# getting the values from the line
x1, y1, x2, y2 = line[0]
#cv2.line(crop, (x1, y1), (x2, y2), (0, 255, 0), 3)
dx = x2 - x1
if (dx < 0):
dx = dx* (-1)
dy = y2 - y1
if (dy < 0):
dy = dy* (-1)
hypo2 = sqrt(dx**2 + dy**2)
if (hypo2 == l[0]):
m = hypo2
xl1 = x1
xl2 = x2
yl1 = y1
yl2 = y2
# getting line region
cv2.line(crop, (xl1, yl1), (xl2, yl2), (255, 0, 0), 3)
if (m == l[0]):
if (hypo2 == l[f]):
if ((sqrt((xl2 - x2)**2 + (yl2 - y2)**2)) > 20):
if ((sqrt((xl1 - x1)**2 + (yl1 - y1)**2)) > 20):
xs1 = x1
xs2 = x2
ys1 = y1
ys2 = y2
# getting line region
cv2.line(crop, (xl1, yl1), (xl2, yl2), (0, 255, 0), 5)
h = 1
break
# Calculate center point
xcenter = width/2
ycenter = height/2
# Determine the cooridnates of the end point (farther from the center)
def coordinates (x1, y1, x2, y2):
a = abs(xcenter - x1)
b = abs(xcenter - x2)
if (a > b):
x_coor = x1
y_coor = y1
else:
x_coor = x2
y_coor = y2
return x_coor, y_coor
xhour, yhour = coordinates(xs1, ys1, xs2, ys2)
xmin, ymin = coordinates(xl1, yl1, xl2, yl2)
xsec, ysec = coordinates(xl1, yl1, xl2, yl2)
cv2.line(crop, (xs1, ys1), (xs2, ys2), (0, 255, 0), 5)
# Calculate the Hour, Minute, Second-hands by the law of cosines
def law_of_cosines (x, y):
l1 = sqrt(((xcenter - x)**2) + ((ycenter - y)**2))
l2 = ycenter
l3 = sqrt(((xcenter - x)**2) + ((0 - y)**2))
cos_theta = ( (l1**2) + (l2**2) - (l3**2) )/(2*l1*l2)
theta_radian = acos(cos_theta)
theta = math.degrees(theta_radian)
return theta
theta_hour = law_of_cosines(xhour, yhour)
theta_min = law_of_cosines(xmin, ymin)
theta_sec = law_of_cosines(xsec, ysec)
def right_or_not (x):
if (x > xcenter):
right = 1
else:
right = 0
return right
hour_right = right_or_not(xhour)
min_right = right_or_not(xmin)
sec_right = right_or_not(xsec)
def time_cal (x, y, z):
if (z == xhour):
if (x == 1):
a = int(y/30)
else:
a = 12 - int(y/30)
if a == 0:
a = 12
else:
if (x == 1):
a = int(y/6)
else:
a = 60 - int(y/6)
if (z == xcenter):
a = 30
return a
hour = time_cal(hour_right, theta_hour, xhour)
minute = time_cal(min_right, theta_min, xmin)
sec = time_cal(sec_right, theta_sec, xsec)
# Display window
canvas = tk.Tk()
canvas.title("Analog to Digital")
canvas.geometry("500x250")
digit = tk.Label(canvas, font = ("ds-digital", 65, "bold"), bg = "white", fg = "blue", bd = 80)
digit.grid(row = 0, column = 1)
# Display result
def display(hour, minute, sec):
value = "{0:0=2d}:{1:0=2d}:{2:0=2d}".format(hour, minute, sec)
digit.config(text=value)
print(value)
display(hour, minute, sec)
canvas.mainloop()
for line in lines:
for x1,y1,x2,y2 in line:
cv2.line(line_image, (x1,y1), (x2,y2), (255,0,0), 1)
lines_edges = cv2.addWeighted(crop, 0.8, line_image, 1, 0)
cv2.imshow('Line Image', line_image)
cv2.imshow('Crop', crop)
cv2.waitKey(0)
There are lot of possible trap in this kind of things. Because each hand generate two lines, but not exactly parallel, and some interaction may make them appear shorter, etc.
But in your case, I bet the problem is far simpler:
xhour, yhour = coordinates(xs1, ys1, xs2, ys2)
xmin, ymin = coordinates(xl1, yl1, xl2, yl2)
xsec, ysec = coordinates(xl1, yl1, xl2, yl2)
I am pretty sure, one of those should be coordinates(xm1, ym1, xm2, ym2)
Edit after your comment. So, we are in a worse place. Because what you have is a computer vision problem, not just a python problem.
And there is not clear solution for that. But a few hint of what you could do.
You could identify the center of the clock (you've already done it, to draw a circle, I think), and also use the distance to the center rather than the length of the line.
You can take advantage of that, to filter lines that don't go through the center, or almost so
Since lines are the border of the hands, and those are slightly triangle, how close they come to the center is an indication of which hand it is. The hour and minute hands lines don't cross exactly the center of the circle. The second hand lines came closer to the center.
Besides, you should expect 2 lines at least (more in reality, that's how hough works) per hand. One over the center, another under. So you can take advantage of that to enhance reliability of the angle computation (by computing the median line, that goes through the center), and the length computation. And avoid counting twice the same hand
Also, you could compute angles from all lines: if there are 3 clearly separated angles, you know that all the angles you are looking for are there. The minutes and seconds for the long hand (and you can discriminate between those because of the more triangle and thick shape our hour, and more narrow shape of seconds. Which result in bigger variability of lines direction for hours than for seconds). The hour hand for the short one.
You can also try to take advantage of the "tail" of the seconds hand, and try to check if you find some dark pixels in the opposite direction of a hand. If you don't, it is not the second hand.
You could also use morphological operators, to erode black pixels before canny/hough. So that you know that the second hand has disappeared, because it is too narrow. You'll find 2 hands from there. And then redo the job without erosion. The extra hand you find is the second hand
Of course, there is the case when some hands are superposed to deal with. If you are confident that, after trying some of the ideas, you would have found 3 hands if there were 3, then, you can trust that 2 hands are at the same position. You could also use your knowledge of previous detection (you know how the hands are supposed to move)
At last, if you are not specially wanting to use line detection only, you could also simply watch the pixels on some circles. A circle whose center is the center of the clock, and whose radius is as big as possible but not big enough to include the digits, should be crossed by two hands (hours and seconds), and it will be quite easy to spot that one (minutes) is thicker than the other (seconds). If there is only one, then you know that hours and seconds are the same. A smaller circle should be crossed by 3 hands. The extra one is hour hand. If you can't find an extra one, and have 2 hands (the same as on the bigger circle) then the hour hand is superposed with either the minute hand or the second hand. If it is the second hand, then it should get a lot thicker.

how can i move a rectangle in py game without mouse and keys?

I have code to move a rectangle in pygame from left to right, up and down.
But I want to move my rectangle around the screen that I created..
can someone help me please?
import pygame
from pygame.locals import *
pygame.init()
FPS = 70
fpsclock = pygame.time.Clock()
SIZE = (1000, 700)
form1 = pygame.display.set_mode(SIZE)
WHITE=(255, 255, 255)
x = 0
y = 0
w = 50
h = 60
direction = 'right'
while True:
for event in pygame.event.get():
if event.type == QUIT:
exit()
form1.fill(WHITE)
pygame.draw.rect(form1, (255, 0, 0), (x, y, w, h), 1)
pygame.display.update()
fpsclock.tick(FPS)
if x,y==0,0:
direction='right'
if x,y==1200-50,0:
direction='down'
if x,y==1200-50,700-60:
direction='left'
if x,y==0,1200-50:
direction='right'
So the first thing you have to look at is the spacing. Even though your code works (after proper indentation) the square goes out of bounds.
The same thing applies to y as well if the square should go up and down.
If you want the square to go around you just need to go left, right, up, or down at the correct time. So if you want to start at the left upper corner and go around you just need to check if the square is in a corner and then change the direction accordingly.
Keep in mind that going down actually increases and going up decreases y.
EDIT:
Here you can see the result of my proposed concept
EDIT 2:
I've copied your code and refactored and completed it. I tried to explain why I did what I did.
import pygame
# Only import the things you need it makes debugging and documenting
# alot easier if you know where everything is coming from
from pygame.locals import QUIT
FPS = 70
# Use variables to define width and height in case you want to
# change it later
width = 200
height = 200
# I would group variables by usage, for example, I would group width,
# height of the screen and w, h of the box so I can easily manipulate
# the numbers if want to.
w = 50
h = 60
# Using an offset variable reduces the hardcoded numbers even more
# if its 0 it will just move along the border but if it >0 it will move
# the square to the centre
offset = 20
# You can declare it as a variable if you need the SIZE tuple somewhere
# else, if you don't need it you can just set it as
# pygame.display.set_mode((width, height))
SIZE = (width, height)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
x = offset
y = offset
direction = 'right'
# This is just my preference but I like to have the variables that are
# changeable at the top for easy access. I think this way the code is
# cleaner.
pygame.init()
fpsclock = pygame.time.Clock()
form1 = pygame.display.set_mode(SIZE)
while True:
for event in pygame.event.get():
if event.type == QUIT:
exit()
form1.fill(WHITE)
# Try to avoid hardcoded numbers as much as possible, hardcoded
# numbers are hard to change later on when the code gets to certain
# size and complexity.
pygame.draw.rect(form1, RED, (x, y, w, h), 1)
pygame.display.update()
fpsclock.tick(FPS)
# Don't harcode conditions, use variables so you can easily change
# them later
if x == offset and y == offset:
direction='right'
if x == width - w - offset and y == offset:
direction='down'
if x == width - w - offset and y == height - h - offset:
direction='left'
if x == offset and y == height - h - offset:
direction='up'
if direction == 'right':
x += 5
elif direction == 'down':
#Keep in mind going down actually means to increment y
y += 5
elif direction == 'left':
x -= 5
elif direction == 'up':
y -= 5

Unable to draw lines using OpenCV

I have created a per_frame function that is fed into ImageAI's detector. I want to draw a line between centroid that meet the distance criteria (if distance >= find_dist_ratio(x1, y1)). Lines should be drawn between the centroids of all objects that meet the criteria, I tried changing it and finally got it without errors but the line does not show up in the output video. Thanks for the help!
def dist_func(counting, output_objects_array,output_objects_count):
a =[]
ret, frame = camera.read()
for d in output_objects_array:
x1 = d['box_points'][0]
y1 = d['box_points'][1]
x2 = d['box_points'][2]
y2 = d['box_points'][3]
centroid = (int((x1 + x2) / 2), int((y1 + y2) / 2))
a.append(centroid)
for i in range(len(a)):
for j in range(i+1, len(a)):
distance = euc_dist(a[i],a[j])
if distance >= find_dist_ratio(x1, y1):
print('close enough')
x, y = a[i]
X, Y = a[j]
cv2.line(frame, (x, y), (X, Y), (255, 0, 0), 5)
It may sound silly, but in your piece of code I can't see if you are really showing the frame. And if the x and y variables are correct (from lower/upper case)
See this example from the docs:
# Create a black image
img = np.zeros((512,512,3), np.uint8)
# Draw a diagonal blue line with thickness of 5 px
cv.line(img,(0,0),(511,511),(255,0,0),5)
For displaying the line drawn here you should also place (after drawing)
cv2.imshow("Line draw", img)
Drawing functions in the docs

How to create python list at every new object detected

I am working on python opencv project where I am detecting and tracking person's movement in the frame. I am drawing a line from where the person enters in the frame and keeps drawing where ever the person moves in the frame and then finally stops when the person moves out of the frame. Below is the code:
centroid_dict = dict()
centroid_list = []
object_id_list = []
"""
SOME CODE
"""
objects = tracker.update(rects)
for (objectID, bbox) in objects.items():
x1, y1, x2, y2 = bbox
x1 = int(x1)
y1 = int(y1)
x2 = int(x2)
y2 = int(y2)
cX = int((x1 + x2) / 2.0)
cY = int((y1 + y2) / 2.0)
cv2.circle(frame, (cX, cY), 4, (0, 255, 0), -1)
centroid_list.append((cX, cY))
centroid_dict[objectID] = centroid_list
if objectID not in object_id_list:
# This will run only once for the first time for every objectID
start_pt = (cX, cY)
end_pt = (cX, cY)
cv2.line(frame, start_pt, end_pt, (0, 255, 0), 2)
object_id_list.append(objectID)
first_time = False
else:
l = len(centroid_list)
for pt in range(len(centroid_dict[objectID])):
if not pt + 1 == l:
start_pt = (centroid_dict[objectID][pt][0], centroid_dict[objectID][pt][1])
end_pt = (centroid_dict[objectID][pt+1][0], centroid_dict[objectID][pt+1][1])
cv2.line(frame, start_pt, end_pt, (0, 255, 0), 2)
In the above code, I am getting objectID and the bounding box coordinates of the person from tracker.update(rects). Using the coordinates I am calculating the cX and cY which I use later to draw a line. I am appending all the cX, cY in centroid_list which I am later adding it in centroid_dict[objectID]. This is working fine when the person is tracked. After the first person, when 2nd person comes in, it draws line from where the first person moved out of the frame and the main reason is that in the code, I am not clearing the centroid_list anywhere due to which it keeps holding the first person cX and cY and the 2nd person's cX and cY also due to which the line is drawn in wrong way.
The only solution is that I need to create separate list for every objectID. So that cX and cY are not mixed. I am not able to find a suitable solution for this. Can anyone please help me here. Thanks
EDIT
I am inferencing over a test video file. Here are few of the images to understand the scenario better
In the above image, we can see from where the lady enters and where she moved out of the frame. I have also marked the start and end point in the image. Now look at the below image where the 2nd lady comes in:
In above image, you can see that the 2nd lady has just entered but still for her the starting point is where the 1st lady entered previously and all the coordinates and the line drawn is from the 1st lady's coordinates. This happened because in our code centroid_list still contains the coordinates of the first lady so it actually starting drawing the line from where the 1st lady enters and thus makes no sense. Hope this scenario is much clear now.
you do not need centroid_list you can just use centroid_dict
you have to define centroid_dict as collections.defaultdict before your for loop:
from collections import defaultdict
centroid_dict = defaultdict(list)
in your for loop:
centroid_dict[objectID].append((cX, cY))
to draw all the points just iterate on your centroid_dict.values()

Python - Find center of object in an image

I have an image file that's has a white background with a non-white object.
I want to find the center of the object using python (Pillow).
I have found a similar question in c++ but no acceptable answer - How can I find center of object?
Similar question, but with broken links in answer - What is the fastest way to find the center of an irregularly shaped polygon? (broken links in answer)
I also read this page but it doesn't give me a useful recipe - https://en.wikipedia.org/wiki/Smallest-circle_problem
Here's an example image:
Edit:
The current solution I'm using is this:
def find_center(image_file):
img = Image.open(image_file)
img_mtx = img.load()
top = bottom = 0
first_row = True
# First we find the top and bottom border of the object
for row in range(img.size[0]):
for col in range(img.size[1]):
if img_mtx[row, col][0:3] != (255, 255, 255):
bottom = row
if first_row:
top = row
first_row = False
middle_row = (top + bottom) / 2 # Calculate the middle row of the object
left = right = 0
first_col = True
# Scan through the middle row and find the left and right border
for col in range(img.size[1]):
if img_mtx[middle_row, col][0:3] != (255, 255, 255):
left = col
if first_col:
right = col
first_col = False
middle_col = (left + right) / 2 # Calculate the middle col of the object
return (middle_row, middle_col)
If you define center as Center of Mass, then it is not difficult, although the CoM can be outside of your shape. You can interpret your image as a 2D distribution, and you can find its expected value (CoM) using integration (summation).
If you have numpy it is quite simple. First create a numpy array containing 1 where your image is non-white, then to make it a probability distribution divide it by the total number of ones.
from PIL import Image
import numpy as np
im = Image.open('image.bmp')
immat = im.load()
(X, Y) = im.size
m = np.zeros((X, Y))
for x in range(X):
for y in range(Y):
m[x, y] = immat[(x, y)] != (255, 255, 255)
m = m / np.sum(np.sum(m))
From this point on it turns into basic probability theory. You find the marginal distributions, then you calculate the expected values as if it was a discrete probability distribution.
# marginal distributions
dx = np.sum(m, 1)
dy = np.sum(m, 0)
# expected values
cx = np.sum(dx * np.arange(X))
cy = np.sum(dy * np.arange(Y))
(cx, cy) is the CoM you are looking for.
Remarks:
If you do not have numpy, you can still do it. It is just a bit more tedious as you have to do the summations by loops / comprehensions.
This method can easily be extended if you want to assign a 'mass' based on color. You just have to change m[x, y] = immat[(x, y)] != (255, 255, 255) to m[x, y] = f(immat[(x, y)]) where f is an arbitary (non-negative valued) function.
If you want to avoid the double loop, you can us np.asarray(im), but then be careful with the indices
No loops:
m = np.sum(np.asarray(im), -1) < 255*3
m = m / np.sum(np.sum(m))
dx = np.sum(m, 0) # there is a 0 here instead of the 1
dy = np.sum(m, 1) # as np.asarray switches the axes, because
# in matrices the vertical axis is the main
# one, while in images the horizontal one is
# the first
I would try and find a way to draw a triangle around it, with one point of the triangle at the farthest "points" on the object, and then find the center of that triangle.

Categories