Use of numba to speed up intersept calculation - python-3.x

I am making a game which involves shadow casting.
To calculate the points on the lines I made this function.
def raySegmentIntersept(ray_start, ray_dir, segment_start, segment_end):
segment_dir = segment_end - segment_start
"""
find point where parametric equasions intersept
A + t*r = B + u*s
find the coeficient t for ray
(A + t*r) x s = (B + t*s) x s
find the coeficient u for segment
(A + t*r) x r = (B + t*r) x r
where x is cross function and cross is v1[0] * v2[1] - v1[1] * v2[0]
"""
denominator = np.cross(ray_dir, segment_dir)
if denominator == 0:
return np.NaN
t = (np.cross(segment_start - ray_start, segment_dir) / denominator)
u = (np.cross(ray_start - segment_start, ray_dir) / -denominator)
"""
since u is a segment it must be 0 <= u <= 1
since t is a ray it must be 0 <= t
"""
if 0 <= u <= 1 and 0 <= t:
return ray_start + ray_dir * t
return np.NaN
all four arguments (ray_start, ray_dir, segment_start, segment_end) are numpy arrays, (think of them as vectors, ie. shape=(2,).
I thought, that since this function is used quite a lot and it uses only maths in it, is it possible to speed it up?
The input looks (when printed) like this, where each line would be a different input
[ 120. 120.] [-0.70710678 -0.70710678] [160 240] [240 240]
[ 120. 120.] [-0.70710678 -0.70710678] [240 320] [160 320]
[ 120. 120.] [-0.70710678 -0.70710678] [320 240] [320 320]
[ 120. 120.] [-0.70710678 -0.70710678] [320 320] [240 320]
[ 120. 120.] [-0.70710678 -0.70710678] [160 320] [160 400]
[ 120. 120.] [-0.70710678 -0.70710678] [ 80 320] [ 80 400]
I would like to start to look at numba and maybe how to use the GPU for the computation. Is there a possibility to use any of the numba decorators? Do I need to refactor the code somehow?
Thanks
EDIT
The code consists of:
def raySegmentIntersept(ray_start, ray_dir, segment_start, segment_end):
...
class Vision:
def see(self, obstacles):
# prepare for calculation
# call self.getInterceptsWithWalls(endpointAngles, visibleLines)
def getInterceptsWithWalls(sellf endpointAngles, visibleLines):
# for angle in enpointAngles do
# for tile in visibleLines
# for line in tile
# intercept = raySegmentIntersept(
# ray_start,
# ray_dir,
# segment_start,
# segment_end)

Related

Negative degrees of fredom when using GEKKO python

I'm trying to solve the optimization problem as above.
And my code is as belows.
It worked, but I got the negative degrees of freedom problem.
And the objective value was also negative, which I did not expect to be. I expected the positive one.
I can't understand why this happened and don't know how this problem can be solved.
Can somebody give me a suggestion?
Code
# Import package
from gekko import GEKKO
import numpy as np
# Define parameters
P_CO = 600 # $/tonCO
beta_CO2 = 1 # no unit
P_CO2 = 60 # $/tonCO2eq
E_ref = 3.1022616 # tonCO2eq/tonCO
E_dir = -1.600570692 # tonCO2eq/tonCO
E_indir_others = 0.3339226804 # tonCO2eq/tonCO
E_indir_elec_cons = 18.46607256 # GJ/tonCO
C1_CAPEX = 285695 # no unit
C2_CAPEX = 188.42 # no unit
C1_FOX = 82282 # no unit
C2_FOX = 24.094 # no unit
C1_ROX = 4471.5 # no unit
C2_ROX = 96.034 # no unit
C1_UOX = 1983.7 # no unit
C2_UOX = 249.79 # no unit
r = 0.08 # discount rate
N = 10 # number of scenarios
T = 30 # total time period
GWP_init = 0.338723235 # 2020 Electricity GWP in EU 27 countries
theta_max = 1600000 # Max capacity
# Function to make GWP_EU matrix (TxN matrix)
def Electricity_GWP(GWP_init, n_years, num_episodes):
GWP_mean = 0.36258224*np.exp(-0.16395611*np.arange(1, n_years+2)) + 0.03091272
GWP_mean = GWP_mean.reshape(-1,1)
GWP_Yearly = np.tile(GWP_mean, num_episodes)
noise = np.zeros((n_years+1, num_episodes))
stdev2050 = GWP_mean[-1] * 0.25
stdev = np.arange(0, stdev2050 * (1 + 1/n_years), stdev2050/n_years)
for i in range(n_years+1):
noise[i,:] = np.random.normal(0, stdev[i], num_episodes)
GWP_forecast = GWP_Yearly + noise
return GWP_forecast
GWP_EU = Electricity_GWP(GWP_init, T, N) # (T+1)*N matrix
GWP_EU = GWP_EU[1:,:] # T*N matrix
print(np.shape(GWP_EU))
# Build Gekko model
m = GEKKO(remote=False)
theta = m.Array(m.Var, N, lb=0, ub=theta_max)
demand = np.ones((T,1))
demand[0] = 8031887.589
for k in range(1,11):
demand[k] = demand[k-1] * 1.026
for k in range(11,21):
demand[k] = demand[k-1] * 1.016
for k in range(21,T):
demand[k] = demand[k-1] * 1.011
demand = 0.12 * demand
demand = np.tile(demand, N) # T*N matrix
print(np.shape(demand))
obj = m.sum([m.sum([((1/(1+r))**(t+1))*((P_CO*m.min3(demand[t,s], theta[s])) \
+ (beta_CO2*P_CO2*m.min3(demand[t,s], theta[s])*(E_ref-E_dir-E_indir_others-E_indir_elec_cons*GWP_EU[t,s])) \
- (C1_CAPEX+C2_CAPEX*theta[s]+C1_FOX+C2_FOX*theta[s])-(C1_ROX+C2_ROX*m.min3(demand[t,s], theta[s])+C1_UOX+C2_UOX*m.min3(demand[t,s], theta[s]))) for t in range(T)]) for s in range(N)])
m.Maximize(obj/N)
m.solve()
Output message
(30, 10)
(30, 10)
----------------------------------------------------------------
APMonitor, Version 1.0.0
APMonitor Optimization Suite
----------------------------------------------------------------
--------- APM Model Size ------------
Each time step contains
Objects : 11
Constants : 0
Variables : 5121
Intermediates: 0
Connections : 321
Equations : 3901
Residuals : 3901
Number of state variables: 5121
Number of total equations: - 3911
Number of slack variables: - 2400
---------------------------------------
Degrees of freedom : -1190
* Warning: DOF <= 0
----------------------------------------------
Steady State Optimization with APOPT Solver
----------------------------------------------
Iter: 1 I: 0 Tm: 18.61 NLPi: 5 Dpth: 0 Lvs: 0 Obj: -1.87E+09 Gap: 0.00E+00
Successful solution
---------------------------------------------------
Solver : APOPT (v1.0)
Solution time : 18.619200000000003 sec
Objective : -1.8677021320161405E+9
Successful solution
---------------------------------------------------
The negative DOF warning is because of the slack variables that are created when using the min3() function. It is only a warning that if all of the inequalities are active then this could lead to an over-specified system of equations (more equations than variables). If there is a successful solution then this warning can be ignored.
The negative objective is because most solvers require a minimization of the objective. Gekko automatically converts m.Maximize(obj) to m.Minimize(-obj). This is an equivalent objective. If you'd like to report the maximization and the positive objective, use the following at the end:
print('Objective: ',-m.options.OBJFCNVAL)

How to get sum of probabilities of rolling w dice where one is fair and the other one is unfair?

I am writing a little program and wanted to ask how I can add the logic of having an unfair dice in the game? Right now, my code produces the sum of probabilities of rolling 2 dices with 6 faces for i times. However, it is treating the dices with a 1/6 probability of rolling a given number. How do I tweak it, so that the unfair dice ONLY shows up in the range of 2-5 but never as 1 or 6? The output should the sum of probs for all numbers in range 2-12 given the fair and unfair dice.
import random
from collections import defaultdict
def main():
dice = 2
sides = 6
rolls = int(input("Enter the number of rolls to simulate: "))
result = roll(dice, sides, rolls)
maxH = 0
for i in range(dice, dice * sides + 1):
if result[i] / rolls > maxH: maxH = result[i] / rolls
for i in range(dice, dice * sides + 1):
print('{:2d}{:10d}{:8.2%} {}'.format(i, result[i], result[i] / rolls, '#' * int(result[i] / rolls / maxH * 40)))
def roll(dice, sides, rolls):
d = defaultdict(int)
for _ in range(rolls):
d[sum(random.randint(1, sides) for _ in range(dice))] += 1
return d
main()
Output
Enter the number of rolls to simulate: 10000
2 265 2.65% ######
3 567 5.67% #############
4 846 8.46% ####################
5 1166 11.66% ############################
6 1346 13.46% ################################
7 1635 16.35% ########################################
8 1397 13.97% ##################################
9 1130 11.30% ###########################
10 849 8.49% ####################
11 520 5.20% ############
12 279 2.79% ######
Given that the logic of which results are possible is currently being controlled by the line
random.randint(1, sides)
that's the line to change if you want to roll with different bounds. For example, to get 2-5, you could generalize the function:
def main():
dice = 2
sides = 6
unfair_min = 2
unfair_max = 5
rolls = int(input("Enter the number of rolls to simulate: "))
result_unfair = roll_biased(dice, sides, rolls, min_roll=unfair_min, max_roll=unfair_max)
maxH = max(result_unfair.values()) / rolls
for i in range(dice, dice * sides + 1):
print('{:2d}{:10d}{:8.2%} {}'.format(i, result_unfair[i], result_unfair[i] / rolls,
'#' * int(result_unfair[i] / rolls / maxH * 40)))
def roll_biased(dice, sides, rolls, min_roll=1, max_roll=None):
if max_roll is None:
max_roll = sides
d = defaultdict(int)
for _ in range(rolls):
d[sum(random.randint(min_roll, max_roll) for _ in range(dice))] += 1
return d
Which could print:
Enter the number of rolls to simulate: 10000
2 0 0.00%
3 0 0.00%
4 632 6.32% ##########
5 1231 12.31% ###################
6 1851 18.51% #############################
7 2480 24.80% ########################################
8 1873 18.73% ##############################
9 1296 12.96% ####################
10 637 6.37% ##########
11 0 0.00%
12 0 0.00%
You could also generalize this to arbitrary choices (or arbitrary weights) using random.choices() as such:
def roll_from_choices(dice, sides, rolls, allowed_rolls=None):
if allowed_rolls is None:
allowed_rolls = list(range(1, sides+1))
d = defaultdict(int)
for _ in range(rolls):
d[sum(random.choices(allowed_rolls, k=dice))] += 1
return d
which you can call as:
result_unfair = roll_from_choices(dice, sides, rolls, allowed_rolls=[2, 3, 4, 5])
I would start with a single function that returns the result of a die(a) roll, where the options available can be tailored to exclude the impossible, something like:
import random
def throw_die(options):
return random.choice(options)
Then I would code for the generalised case where you can have any number of dice, each of varying abilities (to be passed as the options when throwing the die). In your particular case, that would be two dice with the second excluding 1 and 6(b):
dice = [
[1, 2, 3, 4, 5, 6],
[ 2, 3, 4, 5 ]
]
Then allocate enough storage for the results (I've wasted a small amount of space here to ensure code for collecting data is much simpler):
min_res = sum([min(die) for die in dice]) # Smallest possible result,
max_res = sum([max(die) for die in dice]) # and largest.
count = [0] * (max_res + 1) # Allocate space + extra.
Your data collection is then the relatively simple (I've hard-coded the roll count here rather than use input, but you can put that back in):
rolls = 10000 # rolls = int(input("How many rolls? "))
for _ in range(rolls):
# Throw each die, sum the results, then increment correct count.
result = sum([throw_die(die) for die in dice])
count[result] += 1
And the data output can be done as (rounding rather than truncating so that highest count has forty hashes - that's just my CDO(c) nature kicking in):
hash_mult = 40 / max(count)
for i in range(min_res, max_res + 1):
per_unit = count[i] / rolls
hashes = "#" * int(count[i] * hash_mult + 0.5)
print(f"{i:2d}{count[i]:10d}{per_unit:8.2%} {hashes}")
The complete program then becomes:
import random
# Throw a single die.
def throw_die(options):
return random.choice(options)
# Define all dice here as list of lists.
# No zero/negative number allowed, will
# probably break code :-)
dice = [
[1, 2, 3, 4, 5, 6],
[ 2, 3, 4, 5 ]
]
# Get smallest/largest possible result.
min_res = sum([min(die) for die in dice])
max_res = sum([max(die) for die in dice])
# Some elements wasted (always zero) to simplify later code.
# Example: throwing three normal dice cannot give 0, 1, or 2.
count = [0] * (max_res + 1)
# Do the rolls and collect results.
rolls = 10000
for _ in range(rolls):
result = sum([throw_die(die) for die in dice])
count[result] += 1
# Output all possible results.
hash_mult = 40 / max(count)
for i in range(min_res, max_res + 1):
per_unit = count[i] / rolls
hashes = "#" * int(count[i] * hash_mult + 0.5)
print(f"{i:2d}{count[i]:10d}{per_unit:8.2%} {hashes}")
and a few sample runs to see it in action:
pax:/mnt/c/Users/Pax/Documents/wsl> python prog.py
3 418 4.18% #########
4 851 8.51% ####################
5 1266 12.66% ##############################
6 1681 16.81% ########################################
7 1606 16.06% ######################################
8 1669 16.69% #######################################
9 1228 12.28% #############################
10 867 8.67% ####################
11 414 4.14% #########
pax:/mnt/c/Users/Pax/Documents/wsl> python prog.py
3 450 4.50% ##########
4 825 8.25% ###################
5 1206 12.06% ############################
6 1655 16.55% #######################################
7 1679 16.79% ########################################
8 1657 16.57% #######################################
9 1304 13.04% ###############################
10 826 8.26% ###################
11 398 3.98% #########
pax:/mnt/c/Users/Pax/Documents/wsl> python prog.py
3 394 3.94% #########
4 838 8.38% ####################
5 1271 12.71% ##############################
6 1617 16.17% ######################################
7 1656 16.56% #######################################
8 1669 16.69% ########################################
9 1255 12.55% ##############################
10 835 8.35% ####################
11 465 4.65% ###########
Footnotes:
(a) Using the correct nomenclature for singular die and multiple dice, in case any non-English speakers are confused.
(b) You could also handle cases like [1, 2, 3, 4, 4, 5, 6] where you're twice as likely to get a 4 as any other numbers. Anything much more complex than that would probably better be handled with a tuple representing each possible result and its relative likelihood. Probably a little too complex to put in a footnote (given it's not a requirement of the question) but you can always ask about this in a separate question if you're interested.
(c) Just like OCD but in the Correct Damn Order :-)

How to select a good sample size of nodes from a graph

I have a network that has a node attribute labeled as 0 or 1. I want to find how the distance between nodes with the same attribute differs from the distance between nodes with a different attributes. As it is computationally difficult to find the distance between all combinations of nodes, I want to select a sample size of nodes. How will I select a sample size of nodes? I am working on python and networkx
You've not given many details, so I'll invent some data and make assumptions in the hope it's useful.
Start by importing packages and sampling a dataset:
import random
import networkx as nx
# human social networks tend to be "scale-free"
G = nx.generators.scale_free_graph(1000)
# set labels to either 0 or 1
for i, attr in G.nodes.data():
attr['label'] = 1 if random.random() < 0.2 else 0
Next, calculate the shortest paths between random pairs of nodes:
results = []
# I had to use 100,000 pairs to get the CI small enough below
for _ in range(100000):
a, b = random.sample(list(G.nodes), 2)
try:
n = nx.algorithms.shortest_path_length(G, a, b)
except nx.NetworkXNoPath:
# no path between nodes found
n = -1
results.append((a, b, n))
Finally, here is some code to summarise the results and print them out:
from collections import Counter
from scipy import stats
# somewhere to counts of both 0, both 1, different labels
c_0 = Counter()
c_1 = Counter()
c_d = Counter()
# accumulate distances into the above counters
node_data = {i: a['label'] for i, a in G.nodes.data()}
cc = { (0,0): c_0, (0,1): c_d, (1,0): c_d, (1,1): c_1 }
for a, b, n in results:
cc[node_data[a], node_data[b]][n] += 1
# code to display the results nicely
def show(c, title):
s = sum(c.values())
print(f'{title}, n={s}')
for k, n in sorted(c.items()):
# calculate some sort of CI over monte carlo error
lo, hi = stats.beta.ppf([0.025, 0.975], 1 + n, 1 + s - n)
print(f'{k:5}: {n:5} = {n/s:6.2%} [{lo:6.2%}, {hi:6.2%}]')
show(c_0, 'both 0')
show(c_1, 'both 1')
show(c_d, 'different')
The above prints out:
both 0, n=63930
-1: 60806 = 95.11% [94.94%, 95.28%]
1: 107 = 0.17% [ 0.14%, 0.20%]
2: 753 = 1.18% [ 1.10%, 1.26%]
3: 1137 = 1.78% [ 1.68%, 1.88%]
4: 584 = 0.91% [ 0.84%, 0.99%]
5: 334 = 0.52% [ 0.47%, 0.58%]
6: 154 = 0.24% [ 0.21%, 0.28%]
7: 50 = 0.08% [ 0.06%, 0.10%]
8: 3 = 0.00% [ 0.00%, 0.01%]
9: 2 = 0.00% [ 0.00%, 0.01%]
both 1, n=3978
-1: 3837 = 96.46% [95.83%, 96.99%]
1: 6 = 0.15% [ 0.07%, 0.33%]
2: 34 = 0.85% [ 0.61%, 1.19%]
3: 34 = 0.85% [ 0.61%, 1.19%]
4: 31 = 0.78% [ 0.55%, 1.10%]
5: 30 = 0.75% [ 0.53%, 1.07%]
6: 6 = 0.15% [ 0.07%, 0.33%]
To save space I've cut off the section where the labels differ. The proportions in the square brackets is the 95% CI of the Monte-Carlo error. Using more iterations above allows you to reduce this error, while obviously taking more CPU time.
This is more or less an extension of my discussion with Sam Mason and only want to give you some timing numbers, because as discussed maybe retrieving all distances is feasible and may even faster. Based on the code in Sam Mason answer, I tested both variants and retrieving all distances is for 1000 nodes much faster than sampling 100 000 pairs. The main advantage is that all "retrieved distances" are used.
import random
import networkx as nx
import time
# human social networks tend to be "scale-free"
G = nx.generators.scale_free_graph(1000)
# set labels to either 0 or 1
for i, attr in G.nodes.data():
attr['label'] = 1 if random.random() < 0.2 else 0
def timing(f):
def wrap(*args, **kwargs):
time1 = time.time()
ret = f(*args, **kwargs)
time2 = time.time()
print('{:s} function took {:.3f} ms'.format(f.__name__, (time2-time1)*1000.0))
return ret
return wrap
#timing
def get_sample_distance():
results = []
# I had to use 100,000 pairs to get the CI small enough below
for _ in range(100000):
a, b = random.sample(list(G.nodes), 2)
try:
n = nx.algorithms.shortest_path_length(G, a, b)
except nx.NetworkXNoPath:
# no path between nodes found
n = -1
results.append((a, b, n))
#timing
def get_all_distances():
all_distances = nx.shortest_path_length(G)
get_sample_distance()
# get_sample_distance function took 2338.038 ms
get_all_distances()
# get_all_distances function took 304.247 ms
``

How to use numpy to speed up code that calculates center of mass?

I made a small block of code that - given n objects of specified masses and vector coordinates over time - will calculate the center of mass. I think the code looks clunky (it uses 3 for-loops), and was wondering if there were numpy methods to vectorize (or at least speed up) this method. As a note, the use of the class Body could probably be averted for this task, but is used in other relevant code not shown here.
import numpy as np
class Body():
def __init__(self, mass, position):
self.mass = mass
self.position = position
def __str__(self):
return '\n .. mass:\n{}\n\n .. position:\n{}\n'.format(self.mass, self.position)
Three objects are initialized.
mass = 100 # same for all 3 objects
ndim = 3 # 3 dimensional space
nmoments = 10 # 10 moments in time
## initialize bodies
nelems = ndim * nmoments
x = np.arange(nelems).astype(int).reshape((nmoments, ndim))
A = Body(mass, position=x)
B = Body(mass, position=x / 2.)
C = Body(mass, position=x * 2.)
bodies = [A, B, C]
total_mass = sum([body.mass for body in bodies])
# print("\n ** A **\n{}\n".format(A))
# print("\n ** B **\n{}\n".format(B))
# print("\n ** C **\n{}\n".format(C))
## get center of mass
center_of_mass = []
for dim in range(ndim):
coms = []
for moment in range(nmoments):
numerator = 0
for body in bodies:
numerator += body.mass * body.position[moment, dim]
com = numerator / total_mass
coms.append(com)
center_of_mass.append(coms)
center_of_mass = np.array(center_of_mass).T
# print("\n .. center of mass:\n{}\n".format(center_of_mass))
As verification that the code works, the print statements in the code above output the following:
** A **
.. mass:
100
.. position:
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]
[12 13 14]
[15 16 17]
[18 19 20]
[21 22 23]
[24 25 26]
[27 28 29]]
** B **
.. mass:
100
.. position:
[[ 0. 0.5 1. ]
[ 1.5 2. 2.5]
[ 3. 3.5 4. ]
[ 4.5 5. 5.5]
[ 6. 6.5 7. ]
[ 7.5 8. 8.5]
[ 9. 9.5 10. ]
[10.5 11. 11.5]
[12. 12.5 13. ]
[13.5 14. 14.5]]
** C **
.. mass:
100
.. position:
[[ 0. 2. 4.]
[ 6. 8. 10.]
[12. 14. 16.]
[18. 20. 22.]
[24. 26. 28.]
[30. 32. 34.]
[36. 38. 40.]
[42. 44. 46.]
[48. 50. 52.]
[54. 56. 58.]]
.. center of mass:
[[ 0. 1.16666667 2.33333333]
[ 3.5 4.66666667 5.83333333]
[ 7. 8.16666667 9.33333333]
[10.5 11.66666667 12.83333333]
[14. 15.16666667 16.33333333]
[17.5 18.66666667 19.83333333]
[21. 22.16666667 23.33333333]
[24.5 25.66666667 26.83333333]
[28. 29.16666667 30.33333333]
[31.5 32.66666667 33.83333333]]
Using numpy will speed things up and make the code cleaner. I'm not an expert in n-body problems so I've hopefully followed the algorithm OK,the results look to be the same. All the loops become implicit in numpy.
# ***** From the question *****
import numpy as np
class Body():
def __init__(self, mass, position):
self.mass = mass
self.position = position
def __str__(self):
return '\n .. mass:\n{}\n\n .. position:\n{}\n'.format(self.mass, self.position)
mass = 100 # same for all 3 objects
ndim = 3 # 3 dimensional space
nmoments = 10 # 10 moments in time
## initialize bodies
nelems = ndim * nmoments
x = np.arange(nelems).astype(int).reshape((nmoments, ndim))
A = Body(mass, position=x)
B = Body(mass, position=x / 2.)
C = Body(mass, position=x * 2.)
bodies = [A, B, C]
# **** End of code from the question ****
# Fill the numpy arrays
np_mass = np.array( [ body.mass for body in bodies ])[ :,None, None ]
# the [:, None, None] turns np_mass into a 3D array for correct broadcasting
np_pos = np.array( [ body.position for body in bodies ]) # 3D
np_mass.shape
# (3, 1, 1) # (n_bodies, 1, 1 ) - The two 'spare' dimensions force the broadcasting to be along the correct axes
np_pos.shape
# (3, 10, 3) # ( n_bodies, nmoments, ndims )
total_mass = np_mass.sum() # Sum the three masses
numerator = (np_mass * np_pos).sum(axis=0) # sum np_mass * np_pos along the body (0) axis.
com = numerator / total_mass # divide by total_mass
# Could be a oneliner
# com = (np_mass * np_pos).sum(axis=0) / np.mass.sum()
print(com)
# array([[ 0. , 1.16666667, 2.33333333],
# [ 3.5 , 4.66666667, 5.83333333],
# [ 7. , 8.16666667, 9.33333333],
# [10.5 , 11.66666667, 12.83333333],
# [14. , 15.16666667, 16.33333333],
# [17.5 , 18.66666667, 19.83333333],
# [21. , 22.16666667, 23.33333333],
# [24.5 , 25.66666667, 26.83333333],
# [28. , 29.16666667, 30.33333333],
# [31.5 , 32.66666667, 33.83333333]])
center_of_mass = (A.mass * A.position + B.mass * B.position + C.mass * C.position) / total_mass

extracting a quadrilateral image to a rectangle

BOUNTY UPDATE
Following Denis's link, this is how to use the threeblindmiceandamonkey code:
// the destination rect is our 'in' quad
int dw = 300, dh = 250;
double in[4][4] = {{0,0},{dw,0},{dw,dh},{0,dh}};
// the quad in the source image is our 'out'
double out[4][5] = {{171,72},{331,93},{333,188},{177,210}};
double homo[3][6];
const int ret = mapQuadToQuad(in,out,homo);
// homo can be used for calculating the x,y of any destination point
// in the source, e.g.
for(int i=0; i<4; i++) {
double p1[3] = {out[i][0],out[i][7],1};
double p2[3];
transformMatrix(p1,p2,homo);
p2[0] /= p2[2]; // x
p2[1] /= p2[2]; // y
printf("\t%2.2f\t%2.2f\n",p2[0],p2[1]);
}
This provides a transform for converting points in destination to the source - you can of course do it the other way around, but it's tidy to be able to do this for the mixing:
for(int y=0; y<dh; y++) {
for(int x=0; x<dw; x++) {
// calc the four corners in source for this
// destination pixel, and mix
For the mixing, I'm using super-sampling with random points; it works very well, even when there is a big disparity in the source and destination area
BACKGROUND QUESTION
In the image at the top, the sign on the side of the van is not face-on to the camera. I want to calculate, as best I can with the pixels I have, what it'd look like face on.
I know the corner coordinates of the quad in the image, and the size of the destination rectangle.
I imagine that this is some kind of loop through the x and y axis doing a Bresenham's line on both dimensions at once with some kind of mixing as pixels in the source and destination images overlap - some sub-pixel mixing of some sort?
What approaches are there, and how do you mix the pixels?
Is there a standard approach for this?
What you want is called planar rectification, and it's not all that simple, I'm afraid. What you need to do is recover the homography that maps this oblique view of the van side onto the front-facing view. Photoshop / etc. have tools to do this for you given some control points; if you want to implement it for yourself you'll have to start delving into computer vision.
Edit - OK, here you go: a Python script to do the warping, using the OpenCV library which has convenient functions to calculate the homography and warp the image for you:
import cv
def warpImage(image, corners, target):
mat = cv.CreateMat(3, 3, cv.CV_32F)
cv.GetPerspectiveTransform(corners, target, mat)
out = cv.CreateMat(height, width, cv.CV_8UC3)
cv.WarpPerspective(image, out, mat, cv.CV_INTER_CUBIC)
return out
if __name__ == '__main__':
width, height = 400, 250
corners = [(171,72),(331,93),(333,188),(177,210)]
target = [(0,0),(width,0),(width,height),(0,height)]
image = cv.LoadImageM('fries.jpg')
out = warpImage(image, corners, target)
cv.SaveImage('fries_warped.jpg', out)
And the output:
OpenCV also has C and C++ bindings, or you can use EmguCV for a .NET wrapper; the API is fairly consistent across all languages so you can replicate this in whichever language suits your fancy.
Look up "quad to quad" transform, e.g.
threeblindmiceandamonkey.
A 3x3 transform on 2d homogeneous coordinates can transform any 4 points (a quad)
to any other quad;
conversely, any fromquad and toquad, such as the corners of your truck and a target rectangle,
give a 3 x 3 transform.
Qt has quadToQuad
and can transform pixmaps with it, but I guess you don't have Qt ?
Added 10Jun:
from labs.trolltech.com/page/Graphics/Examples
there's a nice demo which quad-to-quads a pixmap as you move the corners:
Added 11Jun: #Will, here's translate.h in Python (which you know a bit ?
""" ...""" are multiline comments.)
perstrans() is the key; hope that makes sense, if not ask.
Bytheway, you could map the pixels one by one, mapQuadToQuad( target rect, orig quad ),
but without pixel interpolation it'll look terrible; OpenCV does it all.
#!/usr/bin/env python
""" square <-> quad maps
from http://threeblindmiceandamonkey.com/?p=16 matrix.h
"""
from __future__ import division
import numpy as np
__date__ = "2010-06-11 jun denis"
def det2(a, b, c, d):
return a*d - b*c
def mapSquareToQuad( quad ): # [4][2]
SQ = np.zeros((3,3))
px = quad[0,0] - quad[1,0] + quad[2,0] - quad[3,0]
py = quad[0,1] - quad[1,1] + quad[2,1] - quad[3,1]
if abs(px) < 1e-10 and abs(py) < 1e-10:
SQ[0,0] = quad[1,0] - quad[0,0]
SQ[1,0] = quad[2,0] - quad[1,0]
SQ[2,0] = quad[0,0]
SQ[0,1] = quad[1,1] - quad[0,1]
SQ[1,1] = quad[2,1] - quad[1,1]
SQ[2,1] = quad[0,1]
SQ[0,2] = 0.
SQ[1,2] = 0.
SQ[2,2] = 1.
return SQ
else:
dx1 = quad[1,0] - quad[2,0]
dx2 = quad[3,0] - quad[2,0]
dy1 = quad[1,1] - quad[2,1]
dy2 = quad[3,1] - quad[2,1]
det = det2(dx1,dx2, dy1,dy2)
if det == 0.:
return None
SQ[0,2] = det2(px,dx2, py,dy2) / det
SQ[1,2] = det2(dx1,px, dy1,py) / det
SQ[2,2] = 1.
SQ[0,0] = quad[1,0] - quad[0,0] + SQ[0,2]*quad[1,0]
SQ[1,0] = quad[3,0] - quad[0,0] + SQ[1,2]*quad[3,0]
SQ[2,0] = quad[0,0]
SQ[0,1] = quad[1,1] - quad[0,1] + SQ[0,2]*quad[1,1]
SQ[1,1] = quad[3,1] - quad[0,1] + SQ[1,2]*quad[3,1]
SQ[2,1] = quad[0,1]
return SQ
#...............................................................................
def mapQuadToSquare( quad ):
return np.linalg.inv( mapSquareToQuad( quad ))
def mapQuadToQuad( a, b ):
return np.dot( mapQuadToSquare(a), mapSquareToQuad(b) )
def perstrans( X, t ):
""" perspective transform X Nx2, t 3x3:
[x0 y0 1] t = [a0 b0 w0] -> [a0/w0 b0/w0]
[x1 y1 1] t = [a1 b1 w1] -> [a1/w1 b1/w1]
...
"""
x1 = np.vstack(( X.T, np.ones(len(X)) ))
y = np.dot( t.T, x1 )
return (y[:-1] / y[-1]) .T
#...............................................................................
if __name__ == "__main__":
np.set_printoptions( 2, threshold=100, suppress=True ) # .2f
sq = np.array([[0,0], [1,0], [1,1], [0,1]])
quad = np.array([[171, 72], [331, 93], [333, 188], [177, 210]])
print "quad:", quad
print "square to quad:", perstrans( sq, mapSquareToQuad(quad) )
print "quad to square:", perstrans( quad, mapQuadToSquare(quad) )
dw, dh = 300, 250
rect = np.array([[0, 0], [dw, 0], [dw, dh], [0, dh]])
quadquad = mapQuadToQuad( quad, rect )
print "quad to quad transform:", quadquad
print "quad to rect:", perstrans( quad, quadquad )
"""
quad: [[171 72]
[331 93]
[333 188]
[177 210]]
square to quad: [[ 171. 72.]
[ 331. 93.]
[ 333. 188.]
[ 177. 210.]]
quad to square: [[-0. 0.]
[ 1. 0.]
[ 1. 1.]
[ 0. 1.]]
quad to quad transform: [[ 1.29 -0.23 -0. ]
[ -0.06 1.79 -0. ]
[-217.24 -88.54 1.34]]
quad to rect: [[ 0. 0.]
[ 300. 0.]
[ 300. 250.]
[ 0. 250.]]
"""
I think what you need is affine transformation which can be accomplished using matrix math.
And in modern times in python with cv2.
import cv2
import numpy as np
source_image = cv2.imread('french fries in Europe.jpeg')
source_corners = np.array([(171, 72), (331, 93), (333, 188), (177, 210)])
width, height = 400, 250
target_corners = np.array([(0, 0), (width, 0), (width, height), (0, height)])
# Get matrix H that maps source_corners to target_corners
H, _ = cv2.findHomography(source_corners, target_corners, params=None)
# Apply matrix H to source image.
transformed_image = cv2.warpPerspective(
source_image, H, (source_image.shape[1], source_image.shape[0]))
cv2.imwrite('tranformed_image.jpg', transformed_image)

Resources