Minimax (with alpha beta pruning): when to store the solution? - minimax

I have implemented a version of the minimax algorithm with alpha/beta pruning, for a connect four game. When using minimax, I would like to save the best AI column to play for in a dictionary called solution (so that, I can also save other information if needed).
So my pseudo code looks like this:
function alphabeta(node, depth, α, β, maximizingPlayer, solution) is
if depth = 0 or node is a terminal node then
return the heuristic value of node
value = None
if maximizingPlayer then # AI is playing
value := −∞
for each child of node do
value_new := alphabeta(child, depth − 1, α, β, FALSE)
if value_new > value then
value = value_new
solution.update({'best_column': child_node_move})
if value ≥ β then
break (* β cutoff *)
α := max(α, value)
else
value := +∞
for each child of node do
value := min(value, alphabeta(child, depth − 1, α, β, TRUE))
if value ≤ α then
break (* α cutoff *)
β := min(β, value)
return value
where child_node_move is a move being tested from a new child node (e.g. each child node is a connect four state, with a move being tested for best move).
Now, I have noticed that the algorithm doesn't quite work when I store values in solution and I don't filter on the depth. Indeed, the algorithm will work at depth 1 but not when I increase the depth parameter.
The only way I managed the algorithm to work, is by modifying the update of solution only when the depth is at 0. For example, If the initial depth is four, I replace part of the above code with:
if value_new > value then
value = value_new
if depth == 4: # only store for the root level => this works
solution.update({'best_column': child_node_move})
So my question is two-fold:
Am I supposed to store the solution only at the root level? If so, why? I don't really understand why this is working actually, and not the other way around.
Should I also store solutions when the MIN player is playing? To me it makes no sense. Also, what is the MIN player minimizing? His own score, or the MAX score?
Edit: here is my code
def minimax(self, board: np.ndarray, depth: int, alpha: float, beta: float, is_max_player: bool, solution) -> float:
assert alpha < beta
sy, sx = board.shape
# When the depth limit of the search is exceeded,
# score the node as if it were a leaf
# The heuristic value is a score measuring the favorability of the node for the maximizing player.
if depth == 0 or self.is_last_move(board) or self.is_winning_move(board):
return self.score(board)
if is_max_player:
value = -np.inf
for k in range(0, sx):
if 0 in board[:, k]:
b = ConnectN.play(board, k, True)
value_new = self.minimax(b, depth - 1, alpha, beta, (not is_max_player), solution)
if value_new > value: # maximize value
value = value_new
solution.update({'col': k, 'depth': depth, 'score': value,'is_max_player': is_max_player})
if value >= beta: # beta pruning
break
alpha = max(alpha, value) # no fail-soft
else:
value = np.inf
for k in range(0, sx):
if 0 in board[:, k]:
b = ConnectN.play(board, k, False)
value_new = self.minimax(b, depth - 1, alpha, beta, (not is_max_player), solution)
if value_new < value: # minimize value
value = value_new
# solution.update({'col': k, 'depth': depth, 'score': value,'is_max_player': is_max_player})
if value <= alpha: # alpha pruning
break
beta = min(beta, value) # no fail-soft
return value
board is a numpy.ndarray with shape (sy=6, sx=7). Function is called like this:
solution = {}
score = self.minimax(c4, 4, alpha=-np.inf, beta=+np.inf, is_max_player=True, solution=solution)
best_move = solution['col']
The score function is pretty basic (pseudo code):
initial score = 0
for every sets of four slots (horizontal, vertical, both diagonals)
do
if AI connects four chessmen: score = Infinity
elif AI connects three chessmen and one empty slot: score += 300
elif AI connects two chessmen and two empty slots: score += 200
if HUMAN connects three chessmen and one empty slot: score -=500
if HUMAN connects four chessmen: score = -Infinity
return score

Related

Detect rings/circuits of connected voxels

I have a skeletonized voxel structure that looks like this:
The actual structure is significantly larger than this exampleIs there any way to find the closed rings in the structure?
I tried converting it to a graph and using graph based approaches but they all have the problem that a graph has no spatial information of node position and hence a graph can have multiple rings that are homologous.
It is not possible to find all the rings and then filter out the ones of interest since the graph is just too large. The size of the rings varies significantly.
Thanks for your help and contribution!
Any language approaches and pseudo-code are welcomed though I work mostly in Python and Matlab.
EDIT:
No the graph is not planar.
The problem with the Graph cycle base is the same as with other simple graph based approaches. The graph lacks any spatial information and different spatial configurations can have the same cycle base, hence the cycle base does not necessarily correspond to the cycles or holes in the graph.
Here is the adjacency matrix in sparse format:
NodeID1 NodeID2 Weight
Pastebin with adjacency matrix
And here are the corresponding X,Y,Z coordinates for the Nodes of the graph:
X Y Z
Pastebin with node coordinates
(The actual structure is significantly larger than this example)
First I reduce the size of the problem considerably by contracting neighbouring nodes of degree 2 into hypernodes: each simple chain in the graph is substituted with a single node.
Then I find the cycle basis, for which the maximum cost of the cycles in the basis set is minimal.
For the central part of the network, the solution can easily be plotted as it is planar:
For some reason, I fail to correctly identify the cycle basis but I think the following should definitely get you started and maybe somebody else can chime in.
Recover data from posted image (as OP wouldn't provide some real data)
import numpy as np
import matplotlib.pyplot as plt
from skimage.morphology import medial_axis, binary_closing
from matplotlib.patches import Path, PathPatch
import itertools
import networkx as nx
img = plt.imread("tissue_skeleton_crop.jpg")
# plt.hist(np.mean(img, axis=-1).ravel(), bins=255) # find a good cutoff
bw = np.mean(img, axis=-1) < 200
# plt.imshow(bw, cmap='gray')
closed = binary_closing(bw, selem=np.ones((50,50))) # connect disconnected segments
# plt.imshow(closed, cmap='gray')
skeleton = medial_axis(closed)
fig, ax = plt.subplots(1,1)
ax.imshow(skeleton, cmap='gray')
ax.set_xticks([])
ax.set_yticks([])
def img_to_graph(binary_img, allowed_steps):
"""
Arguments:
----------
binary_img -- 2D boolean array marking the position of nodes
allowed_steps -- list of allowed steps; e.g. [(0, 1), (1, 1)] signifies that
from node with position (i, j) nodes at position (i, j+1)
and (i+1, j+1) are accessible,
Returns:
--------
g -- networkx.Graph() instance
pos_to_idx -- dict mapping (i, j) position to node idx (for testing if path exists)
idx_to_pos -- dict mapping node idx to (i, j) position (for plotting)
"""
# map array indices to node indices and vice versa
node_idx = range(np.sum(binary_img))
node_pos = zip(*np.where(np.rot90(binary_img, 3)))
pos_to_idx = dict(zip(node_pos, node_idx))
# create graph
g = nx.Graph()
for (i, j) in node_pos:
for (delta_i, delta_j) in allowed_steps: # try to step in all allowed directions
if (i+delta_i, j+delta_j) in pos_to_idx: # i.e. target node also exists
g.add_edge(pos_to_idx[(i,j)], pos_to_idx[(i+delta_i, j+delta_j)])
idx_to_pos = dict(zip(node_idx, node_pos))
return g, idx_to_pos, pos_to_idx
allowed_steps = set(itertools.product((-1, 0, 1), repeat=2)) - set([(0,0)])
g, idx_to_pos, pos_to_idx = img_to_graph(skeleton, allowed_steps)
fig, ax = plt.subplots(1,1)
nx.draw(g, pos=idx_to_pos, node_size=1, ax=ax)
NB: These are not red lines, these are lots of red dots corresponding to nodes in the graph.
Contract Graph
def contract(g):
"""
Contract chains of neighbouring vertices with degree 2 into one hypernode.
Arguments:
----------
g -- networkx.Graph or networkx.DiGraph instance
Returns:
--------
h -- networkx.Graph or networkx.DiGraph instance
the contracted graph
hypernode_to_nodes -- dict: int hypernode -> [v1, v2, ..., vn]
dictionary mapping hypernodes to nodes
"""
# create subgraph of all nodes with degree 2
is_chain = [node for node, degree in g.degree() if degree == 2]
chains = g.subgraph(is_chain)
# contract connected components (which should be chains of variable length) into single node
components = list(nx.components.connected_component_subgraphs(chains))
hypernode = g.number_of_nodes()
hypernodes = []
hyperedges = []
hypernode_to_nodes = dict()
false_alarms = []
for component in components:
if component.number_of_nodes() > 1:
hypernodes.append(hypernode)
vs = [node for node in component.nodes()]
hypernode_to_nodes[hypernode] = vs
# create new edges from the neighbours of the chain ends to the hypernode
component_edges = [e for e in component.edges()]
for v, w in [e for e in g.edges(vs) if not ((e in component_edges) or (e[::-1] in component_edges))]:
if v in component:
hyperedges.append([hypernode, w])
else:
hyperedges.append([v, hypernode])
hypernode += 1
else: # nothing to collapse as there is only a single node in component:
false_alarms.extend([node for node in component.nodes()])
# initialise new graph with all other nodes
not_chain = [node for node in g.nodes() if not node in is_chain]
h = g.subgraph(not_chain + false_alarms)
h.add_nodes_from(hypernodes)
h.add_edges_from(hyperedges)
return h, hypernode_to_nodes
h, hypernode_to_nodes = contract(g)
# set position of hypernode to position of centre of chain
for hypernode, nodes in hypernode_to_nodes.items():
chain = g.subgraph(nodes)
first, last = [node for node, degree in chain.degree() if degree==1]
path = nx.shortest_path(chain, first, last)
centre = path[len(path)/2]
idx_to_pos[hypernode] = idx_to_pos[centre]
fig, ax = plt.subplots(1,1)
nx.draw(h, pos=idx_to_pos, node_size=20, ax=ax)
Find cycle basis
cycle_basis = nx.cycle_basis(h)
fig, ax = plt.subplots(1,1)
nx.draw(h, pos=idx_to_pos, node_size=10, ax=ax)
for cycle in cycle_basis:
vertices = [idx_to_pos[idx] for idx in cycle]
path = Path(vertices)
ax.add_artist(PathPatch(path, facecolor=np.random.rand(3)))
TODO:
Find the correct cycle basis (I might be confused what the cycle basis is or networkx might have a bug).
EDIT
Holy crap, this was a tour-de-force. I should have never delved into this rabbit hole.
So the idea is now that we want to find the cycle basis for which the maximum cost for the cycles in the basis is minimal. We set the cost of a cycle to its length in edges, but one could imagine other cost functions. To do so, we find an initial cycle basis, and then we combine cycles in the basis until we find the set of cycles with the desired property.
def find_holes(graph, cost_function):
"""
Find the cycle basis, that minimises the maximum individual cost of the cycles in the basis set.
"""
# get cycle basis
cycles = nx.cycle_basis(graph)
# find new basis set that minimises maximum cost
old_basis = set()
new_basis = set(frozenset(cycle) for cycle in cycles) # only frozensets are hashable
while new_basis != old_basis:
old_basis = new_basis
for cycle_a, cycle_b in itertools.combinations(old_basis, 2):
if len(frozenset.union(cycle_a, cycle_b)) >= 2: # maybe should check if they share an edge instead
cycle_c = _symmetric_difference(graph, cycle_a, cycle_b)
new_basis = new_basis.union([cycle_c])
new_basis = _select_cycles(new_basis, cost_function)
ordered_cycles = [order_nodes_in_cycle(graph, nodes) for nodes in new_basis]
return ordered_cycles
def _symmetric_difference(graph, cycle_a, cycle_b):
# get edges
edges_a = list(graph.subgraph(cycle_a).edges())
edges_b = list(graph.subgraph(cycle_b).edges())
# also get reverse edges as graph undirected
edges_a += [e[::-1] for e in edges_a]
edges_b += [e[::-1] for e in edges_b]
# find edges that are in either but not in both
edges_c = set(edges_a) ^ set(edges_b)
cycle_c = frozenset(nx.Graph(list(edges_c)).nodes())
return cycle_c
def _select_cycles(cycles, cost_function):
"""
Select cover of nodes with cycles that minimises the maximum cost
associated with all cycles in the cover.
"""
cycles = list(cycles)
costs = [cost_function(cycle) for cycle in cycles]
order = np.argsort(costs)
nodes = frozenset.union(*cycles)
covered = set()
basis = []
# greedy; start with lowest cost
for ii in order:
cycle = cycles[ii]
if cycle <= covered:
pass
else:
basis.append(cycle)
covered |= cycle
if covered == nodes:
break
return set(basis)
def _get_cost(cycle, hypernode_to_nodes):
cost = 0
for node in cycle:
if node in hypernode_to_nodes:
cost += len(hypernode_to_nodes[node])
else:
cost += 1
return cost
def _order_nodes_in_cycle(graph, nodes):
order, = nx.cycle_basis(graph.subgraph(nodes))
return order
holes = find_holes(h, cost_function=partial(_get_cost, hypernode_to_nodes=hypernode_to_nodes))
fig, ax = plt.subplots(1,1)
nx.draw(h, pos=idx_to_pos, node_size=10, ax=ax)
for ii, hole in enumerate(holes):
if (len(hole) > 3):
vertices = np.array([idx_to_pos[idx] for idx in hole])
path = Path(vertices)
ax.add_artist(PathPatch(path, facecolor=np.random.rand(3)))
xmin, ymin = np.min(vertices, axis=0)
xmax, ymax = np.max(vertices, axis=0)
x = xmin + (xmax-xmin) / 2.
y = ymin + (ymax-ymin) / 2.
# ax.text(x, y, str(ii))

finding shortest path given distance transform image

I am given a distance transform (below) and I need to write a program that finds the shortest path going from point A(140,200) to point B(725,1095) while making sure I am at least ten pixels away from any obstacle
distance_transform_given
(the above image is the distance transform of map)
This is what I have done so far:
I started off at the initial point and evaluated the grayscale intensity of every point around it. ( 8 neighboring points that is)
Then I moved to the point with the highest grayscale intensity of the 8 neighboring points.
Then I repeated this process but I get random turns and not the shortest path.
please do help me out
code of what I have done so far :
def find_max_neigh_location(np,img):
maxi = 0
x0=0
y0=0
for i in range(len(np)):
if img[np[i][0]][np[i][1]][0] >maxi:
maxi = img[np[i][0]][np[i][1]][0]
x0 = np[i][0]
y0 = np[i][1]
return x0,y0
-----------------------------------------------------------------
def check_if_extremes(x,y):
if(x==1099 and y==1174):return 1
elif(y==1174 and x!=1099):return 2
elif(x==1099 and y!=1174):return 3
else:return 0
--------------------------------------------------------
def find_highest_neighbour(img,x,y,visted_points):
val = check_if_extremes(x,y)
if val==1:
neigh_points = [(x-1,y),(x-1,y-1),(x,y-1)]
np = list(set(neigh_points)-set(visited_points))
x0,y0 = find_max_neigh_location(np,img)
elif val==2:
neigh_points = [(x-1,y),(x-1,y-1),(x,y-1),(x+1,y-1),(x+1,y)]
np = list(set(neigh_points)-set(visited_points))
x0,y0 = find_max_neigh_location(np,img)
elif val==3:
neigh_points = [(x-1,y),(x-1,y-1),(x,y-1),(x,y+1),(x-1,y+1)]
np = list(set(neigh_points)-set(visited_points))
x0,y0 = find_max_neigh_location(np,img)
elif val==0:
neigh_points = [(x-1,y),(x-1,y-1),(x,y-1),(x,y+1),(x+1,y),(x+1,y+1),(x,y+1),(x-1,y+1)]
np = list(set(neigh_points)-set(visited_points))
x0,y0 = find_max_neigh_location(np,img)
for pt in neigh_points:
visited_points.append(pt)
return x0,y0,visited_points
---------------------------------------------------------
def check_if_neighbour_is_final_pt(img,x,y):
l = [(x-1,y), (x+1,y),(x,y-1),(x,y+1),(x-1,y-1),(x+1,y+1),(x-1,y+1),(x+1,y-1)]
if (725,1095) in l:
return True
else:
return False
--------------------------------------------------------------
x=140
y=200
pos=[]
count = 0
visited_points = [(x,y)]
keyword = True
while keyword:
val = check_if_neighbour_is_final_pt(img,x,y)
if val == True:
keyword = False
if val == False:
count=count+1
x,y,visited_points = find_highest_neighbour(img,x,y,visited_points)
img[x][y] = [255,0,0]
cv2.imwrite("img\distance_transform_result__"+str(count)+".png",img)
As you did not comment your code at all I won't read through your code.
I'll stick to what you described as your approach.
The fact that you start at point A and move to the brightest point A's neigbourhood shows that you don't know what distance transform does or what it is you see in your distance map... Never start coding if you don't know what you're dealing with.
Distance transform transforms a binary image into an image where each pixel's value is the minimum distance of the input image's foreground pixel to the background.
Dark pixels mean close to background (obstacles in your problem) and bright pixels are further away.
So moving to the brightest pixel nearby will only lead you away from the obstacles but never to your target point.
First restriction:
Never get closer to an obstacle than 10 pixels!
This means, every pixel that is closer to the obstacle (darker than 10) cannot be part of your path. So apply a global threshold of 10 to your distance map.
Now every white pixel can be used for your path to B.
The rest ist an optimization problem. There is plenty of literature on shortest path algorithms online. I'll leave that up to you...

How to filter given width of lines in a image?

I need to filter given width of lines in a image.
I am coding a program which will detect lines of road image. And I found something like that but can't understand logic of it. My function has to do that:
I will send image and width of line in terms of pixel size(e.g 30 pixel width), the function will filter just these lines in image.
I found that code:
void filterWidth(Mat image, int tau) // tau=width of line I want to filter
int aux = 0;
for (int j = 0; j < quad.rows; ++j)
{
unsigned char *ptRowSrc = quad.ptr<uchar>(j);
unsigned char *ptRowDst = quadDst.ptr<uchar>(j);
for (int i = tau; i < quad.cols - tau; ++i)
{
if (ptRowSrc[i] != 0)
{
aux = 2 * ptRowSrc[i];
aux += -ptRowSrc[i - tau];
aux += -ptRowSrc[i + tau];
aux += -abs((int)(ptRowSrc[i - tau] - ptRowSrc[i + tau]));
aux = (aux < 0) ? (0) : (aux);
aux = (aux > 255) ? (255) : (aux);
ptRowDst[i] = (unsigned char)aux;
}
}
}
What is the mathematical explanation of that code? And how does that work?
Read up about convolution filters. This code is a particular case of a 1 dimensional convolution filter (it only convolves with other pixels on the currently processed line).
The value of aux is started with 2 * the current pixel value, then pixels on either side of it at distance tau are being subtracted from that value. Next the absolute difference of those two pixels is also subtracted from it. Finally it is capped to the range 0...255 before being stored in the output image.
If you have an image:
0011100
This convolution will cause the centre 1 to gain the value:
2 * 1
- 0
- 0
- abs(0 - 0)
= 2
The first '1' will become:
2 * 1
- 0
- 1
- abs(0 - 1)
= 0
And so will the third '1' (it's a mirror image).
And of course the 0 values will always stay zero or become negative, which will be capped back to 0.
This is a rather weird filter. It takes the pixel values three by three on the same line, with a tau spacing. Let these values by Vl, V and Vr.
The filter computes - Vl + 2 V - Vr, which can be seen as a second derivative, and deducts |Vl - Vr|, which can be seen as a first derivative (also called gradient). The second derivative gives a maximum response in case of a maximum configuration (Vl < V > Vr); the first derivative gives a minimum response in case of a symmetric configuration (Vl = Vr).
So the global filter will give a maximum response for a symmetric maximum (like with a light road on a dark background, vertical, with a width less than 2.tau).
By rearranging the terms, you can see that the filter also yields the smallest of the left and right gradients, V - Vm and V - Vp (clamped to zero).

Unit Testing probability

I have a method that creates a 2 different instances (M, N) in a given x of times (math.random * x) the method will create object M and the rest of times object N.
I have written unit-tests with mocking the random number so I can assure that the method behaves as expected. However I am not sure on how to (and if) to test that the probability is accurate, for example if x = 0.1 I expect 1 out of 10 cases to return instance M.
How do I test this functionality?
Split the test. The first test should allow you to define what the random number generator returns (I assume you already have that). This part of the test just satisfies the "do I get the expected result if the random number generator would return some value".
The second test should just run the random number generator using some statistical analysis function (like counting how often it returns each value).
I suggest to wrap the real generator with a wrapper that returns "create M" and "create N" (or possibly just 0 and 1). That way, you can separate implementation from the place where it's used (the code which creates the two different instance shouldn't need to know how the generator is initialized or how you turn the real result into "create X".
I'll do this in the form of Python.
First describe your functionality:
def binomial_process(x):
'''
given a probability, x, return M with that probability,
else return N with probability 1-x
maybe: return random.random() > x
'''
Then test for this functionality:
import random
def binom(x):
return random.random() > x
Then write your test functions, first a setup function to put together your data from an expensive process:
def setUp(x, n):
counter = dict()
for _ in range(n):
result = binom(x)
counter[result] = counter.get(result, 0) + 1
return counter
Then the actual test:
import scipy.stats
trials = 1000000
def test_binomial_process():
ps = (.01, .1, .33, .5, .66, .9, .99)
x_01 = setUp(.01, trials)
x_1 = setUp(.1, trials)
x_33 = setUp(.1, trials)
x_5 = setUp(.5, trials)
x_66 = setUp(.9, trials)
x_9 = setUp(.9, trials)
x_99 = setUp(.99, trials)
x_01_result = scipy.stats.binom_test(x_01.get(True, 0), trials, .01)
x_1_result = scipy.stats.binom_test(x_1.get(True, 0), trials, .1)
x_33_result = scipy.stats.binom_test(x_33.get(True, 0), trials, .33)
x_5_result = scipy.stats.binom_test(x_5.get(True, 0), trials)
x_66_result = scipy.stats.binom_test(x_66.get(True, 0), trials, .66)
x_9_result = scipy.stats.binom_test(x_9.get(True, 0), trials, .9)
x_99_result = scipy.stats.binom_test(x_99.get(True, 0), trials, .99)
setups = (x_01, x_1, x_33, x_5, x_66, x_9, x_99)
results = (x_01_result, x_1_result, x_33_result, x_5_result,
x_66_result, x_9_result, x_99_result)
print 'can reject the hypothesis that the following tests are NOT the'
print 'results of a binomial process (with their given respective'
print 'probabilities) with probability < .01, {0} trials each'.format(trials)
for p, setup, result in zip(ps, setups, results):
print 'p = {0}'.format(p), setup, result, 'reject null' if result < .01 else 'fail to reject'
Then write your function (ok, we already did):
def binom(x):
return random.random() > x
And run your tests:
test_binomial_process()
Which on last output gives me:
can reject the hypothesis that the following tests are NOT the
results of a binomial process (with their given respective
probabilities) with probability < .01, 1000000 trials each
p = 0.01 {False: 10084, True: 989916} 4.94065645841e-324 reject null
p = 0.1 {False: 100524, True: 899476} 1.48219693752e-323 reject null
p = 0.33 {False: 100633, True: 899367} 2.96439387505e-323 reject null
p = 0.5 {False: 500369, True: 499631} 0.461122365668 fail to reject
p = 0.66 {False: 900144, True: 99856} 2.96439387505e-323 reject null
p = 0.9 {False: 899988, True: 100012} 1.48219693752e-323 reject null
p = 0.99 {False: 989950, True: 10050} 4.94065645841e-324 reject null
Why do we fail to reject on p=0.5? Let's look at the help on scipy.stats.binom_test:
Help on function binom_test in module scipy.stats.morestats:
binom_test(x, n=None, p=0.5, alternative='two-sided')
Perform a test that the probability of success is p.
This is an exact, two-sided test of the null hypothesis
that the probability of success in a Bernoulli experiment
is `p`.
Parameters
----------
x : integer or array_like
the number of successes, or if x has length 2, it is the
number of successes and the number of failures.
n : integer
the number of trials. This is ignored if x gives both the
number of successes and failures
p : float, optional
The hypothesized probability of success. 0 <= p <= 1. The
default value is p = 0.5
alternative : {'two-sided', 'greater', 'less'}, optional
Indicates the alternative hypothesis. The default value is
'two-sided'.
So .5 is the default null hypothesis for test, and it makes sense not to reject the null hypothesis in this case.

Building tree/graph from image points

I start to describe my problem with this picture:
In the picture, we can see some points (black dots). What I want to do is to first store all the points and then find the node points and the tip points (red dots).
What is more, I need to check if these red points can be connected by straight lines (along the black points) to find angles between the red lines.
I don't know if I explained it clearly enough, but what I figured is that I should implement a tree/graph and than use some path finding to check if the red points are connected?
Basically, I started with something like:
class Point {
public:
int x;
int y;
vector<Point> neighbors;
Point(void);
Point(int x, int y);
}
vector<Point> allPoints;
Where I store all the points in allPoints vector. Than for each Point, I check all his neighbors ([x+1,y], [x-1,y], [x+1,y+1], [x-1, y+1], ...) and store them in neighbors vector for that Point.
Then, by the size of the neighbors vector, I determine if the Point is a node (3 or more neighbors), a tip (1 neighbor), or just some basic point (2 neighbors).
And here comes the part, where I have no idea how to implement some path finding (to check whether there is a way for example from a tip point to the closest node point).
What is more, I have no idea if my "tree" representation is good (probably is not). So if anyone would help me to achieve what I want, it would be great.
P.S. I'm writing in C++ (and OpenCV) and VS2010.
Edit:
This is how it looks like in a real program (red lines are drown by me in paint, but this is what i want to achieve):
I'm not sure if that post should be an answer or edit, because i have no idea if anyone would notice that I added something, co i decided to post it as an answer. Sorry if its wrong.
About a problem. I did some codding, I belive I know how to do what I wanted, but another problem came out ;d.
Well lets start with picture:
To see where is a problem, you have to zoom above picture, about 400%, and take a look inside green rectangles. Image "skeleton" are my basic lines, and image "temp", are my output tips and nodes. As you can see, tips are fine (violet rectangles) (points with 1 neighbor) but unfortunately there are lines where pixel have 3 or more neighbors and they are not nodes (right green rectangle on "skeleton" is the line which have no nodes.. and green rectangle on "temp" are my false nodes .. marked because of the specific pixels positions).When You super zoom it, You are going to notice that I marked pixels with colors to make it more clear.
So the problem is that sometimes both nodes and "branches" have more than 2 neighbors which leads me to a problem "how to find a difference between them". All i need are nodes (small green rectangle on "skeleton" image - when you zoom you will see that there are 2 pixels that could be a node, but this is not important as long as they are so close to each other).
Any help?
If you need code, just tell and I will edit & paste it.
Edit:
I did something!
I found a way to filter redundant pixels from the lines. But that made some of my skeleton lines to disconnect, and that is not good, because some nodes are considered as "tips" now.
Just look at the picture:
Small dots are "good" nodes, but dots inside red rectangles, are nodes which got disconnected (zoom in to see that), and are now considered as tips.
How did I filtered the pixels? Here is the code:
void SKEL::clearPixels(cv::Mat& input)
{
uchar* data = (uchar *)input.data;
for (int i = 1 ; i < input.rows-1 ; i++)
{
for (int j = 1 ; j < input.cols-1 ; j++)
{
if (input.at<uchar>(i,j) == 255) //if its part of skeleton
{
if (input.at<uchar>(i-1,j+1) == 255)
{
if (input.at<uchar>(i,j+1) == 255) data[i*input.step+(j+1)*input.channels()] = 0;
if (input.at<uchar>(i-1,j) == 255) data[(i-1)*input.step+(j)*input.channels()] = 0;
}
if (input.at<uchar>(i+1,j+1) == 255)
{
if (input.at<uchar>(i,j+1) == 255) data[i*input.step+(j+1)*input.channels()] = 0;
if (input.at<uchar>(i+1,j) == 255) data[(i+1)*input.step+(j)*input.channels()] = 0;
}
if (input.at<uchar>(i-1,j-1) == 255)
{
if (input.at<uchar>(i,j-1) == 255) data[i*input.step+(j-1)*input.channels()] = 0;
if (input.at<uchar>(i-1,j) == 255) data[(i-1)*input.step+(j)*input.channels()] = 0;
}
if (input.at<uchar>(i+1,j-1) == 255)
{
if (input.at<uchar>(i,j-1) == 255) data[i*input.step+(j-1)*input.channels()] = 0;
if (input.at<uchar>(i+1,j) == 255) data[(i+1)*input.step+(j)*input.channels()] = 0;
}
}
}
}
}
For each pixel I checked if it has (x+1,y-1) (x+1,y+1) (x-1,y+1) (x-1, y-1) neighbor and if it does , i checked if there are neighbors just next to that neighbor and removed them. It was the only idea that I had , and its quite slow, but for now , nothing better comes to my mind.
So now, my main problem is. How to reconnect broken nodes ? ..
This is late but I got something working and put it in a github repo: pixeltree (it's too big to just paste the whole thing here). I work in python rather than c++ but I hope the idea will help if you or someone else needs it. Disclaimer - there is probably some accepted method of doing this but I didn't find it.
Approach
Separate the image into as many regions as you want (e.g. black/white)
For each region, choose any pixel
Build a full tree from that pixel that covers every pixel in the region
Cut back the non-meaningful branches of the tree until it is a minimal tree
Details
The hard part is the last step of cutting back the tree. This is the approach I used:
For each region again, choose a root at a "distant" pixel (technically, one end of the longest path in the graph).
For each leaf in that tree, perform a breadth-first-search until encountering another branch. Eliminate the branch of this leaf if its height is less than the encountered pixel.
Examples
Embarrassingly large function that does most of the work:
def treeify(image, target_families=None):
# family map is used everywhere to identify pixels, regions, etc.
family_map = _make_family_map(image)
target_families = target_families or np.unique(family_map)
trees = list()
rows, cols = family_map.shape
remaining_points = set((r, c) for r in range(rows) for c in range(cols)
if family_map[r, c] in target_families)
# the "tree" here is actually just a non-cyclic undirected graph which could
# be rooted anywhere. The basic graph is done with each pixel pointing to some neighbors (edges)
edges = np.empty_like(family_map, dtype=object)
edges.flat = [set() for _ in edges.flat]
# continue until all regions within the graph are handled
while remaining_points:
# grow a tree from any remaining point until complete
start_p = remaining_points.pop()
family = family_map[start_p]
tree = {'family': family,
'any_point': start_p}
trees.append(tree)
q = [(None, start_p)]
while q:
# pushright + popleft --> breadth first expansion
# random within left part of q - roughly BFS with less pattern
source, p = q.pop(random.randrange(0, max(1, len(q)//2)))
try:
remaining_points.remove(p)
except KeyError:
pass # tree start point is always already gone
# send qualifying neighbors for expansion
q_points = tuple(qp for sp, qp in q)
expansion_points = [n for n in _neighbors(p, 'all', family_map)
if all((n != source,
n in remaining_points,
n not in q_points,
family_map[n] == family))]
expansion_pairs = [(p, n) for n in expansion_points]
q.extend(expansion_pairs)
# document all edges for this point
if source is None:
all_connections = list(expansion_points)
else:
all_connections = expansion_points + [source]
edges[p].update(all_connections)
# prune all but "best" branches within each area
for tree in trees:
family = tree['family']
# root graph at one end of the longest path in the graph
distant_point = _most_distant_node(tree['any_point'], edges)
# for choosing best paths: document the height of every pixel
heights = _heights(distant_point, edges)
remaining_leaves = set(_leaves(distant_point, edges))
# repeatedly look for a leaf and decide to keep it or prune its branch
# stop when no leaves are pruned
while remaining_leaves:
leaf = remaining_leaves.pop()
# identify any degenerate path to next branching pixel
# this path is ignored when testing for nearby branches
ignore = set(_identify_degenerate_branch(leaf, edges))
# BFS expansion to find nearby other branches
expansion_q = deque()
expansion_q.append(leaf)
while expansion_q:
p = expansion_q.popleft() # pushright + popleft for BFS
ignore.add(p)
# decide what to do with each neighbor
for n in _neighbors(p, 'sides', family_map):
if n in ignore:
continue # already decided to ignore this point
elif n in expansion_q:
continue # already slated for expansion testing
elif family_map[n] != family:
ignore.add(n) # ignore other families
continue
elif len(edges[n]) == 0:
expansion_q.append(n) # expand into empty spaces
elif _disqualified(leaf, n, edges, heights):
_prune_branch_of_leaf(leaf, edges, heights)
expansion_q.clear() # this leaf is done. stop looking
break
else:
expansion_q.append(n)
return trees, edges