Python program to detect intersection of one-dimensional line segments - python

I have line segments of four colors—pink, green, orange, red—as in the figure below.
As an example, the first pink segment has start and end position (5258,5422).
The coordinates are stored in this dictionary of tuples:
mycoord = { 'pink' :[(5258,5422), (5479,5864)],
'green' :[(5425,5450)],
'orange':[(5266,5770)],
'red' :[(5258,5864)] }
What I want to do is to get all the possible intersections' start and end values as shown in this figure:
Thus, the desired answer would be:
sect1/pink-red : 5258,5266
sect2/pink-orange-red : 5266,5422
sect3/orange-red : 5422,5425
sect4/green-orange-red: 5425,5450
sect5/orange-red : 5450,5479
sect6/pink-orange-red : 5479,5770
sect7/pink-red : 5770,5864
Note that I want to preserve the color indicator for each intersection (e.g., pink-red). How can I achieve this with Python?

I suggest that you proceed as follows.
Sort the endpoints, remembering each one's color and whether it's a left (opening) or right (closing) endpoint.
Iterate over the endpoints, keeping track of open spans with a hash that maps each color to the number of open spans of that color. Increment when you open a span of a given color, decrement when you close a span. Remove colors when their count reaches zero. For each distinct endpoint, put the colors of all open spans at that point into a set.
Iterate over consecutive pairs of distinct endpoints. These form the left and right endpoints of the spans that interest you. For each endpoint, you know the colors that are active at that point. The set of colors that are active during the span is the set intersection of the colors that are active at the left end and the colors that are active at the right end.
Note: If the intersection of colors between two endpoints is empty, you've found a gap between spans, so you know that it should be skipped. You might also like to skip spans with only one color. The implementation below does not. You can easily change it to skip single-color spans by modifying this line:
if len(colors) > 0:
so that it reads:
if len(colors) > 1:
If you're interested in seeing the gaps between spans, you can change the threshold to -1 or remove the condition altogether.
Implementation:
mycoord = { 'pink' :[(5258,5422), (5479,5864)],
'green' :[(5425,5450)],
'orange':[(5266,5770)],
'red' :[(5258,5864)] }
# Sort the endpoints. Remember their color and whether they open or close.
points = []
for color, spans in mycoord.items():
for open, close in spans:
points.append((open, 'open', color))
points.append((close, 'close', color))
points.sort()
# Iterate over the endpoints. Keep track of open spans. Mark intersections.
active_spans = {}
intersections = []
for point, kind, color in points:
if len(intersections) != 0 and intersections[-1][0] == point:
intersections[-1][1].add(color)
else:
color_set = set([color] + list(active_spans.keys()))
intersections.append((point, color_set))
if kind == 'close':
active_spans[color] -= 1
if active_spans[color] == 0:
del active_spans[color]
else:
active_spans[color] = active_spans.setdefault(color, 0) + 1
# Iterate over consecutive pairs of unique intersections. Intersect the color sets.
tab_width = sum(map(len, mycoord)) + len(mycoord)
count = 0
for i in range(1, len(intersections)):
a, b = intersections[i - 1], intersections[i]
colors = sorted(a[1] & b[1])
if len(colors) > 0:
count += 1
print('sect{0}/{1:<{2}}: {3},{4}'.format(count, '-'.join(colors), tab_width,
a[0], b[0]))
Result:
sect1/pink-red : 5258,5266
sect2/orange-pink-red : 5266,5422
sect3/orange-red : 5422,5425
sect4/green-orange-red : 5425,5450
sect5/orange-red : 5450,5479
sect6/orange-pink-red : 5479,5770
sect7/pink-red : 5770,5864

Using the brace open/close idea of Michael Laszlo above:
>>> mycoord = { 'pink' :[(5258,5422), (5479,5864)],
'green' :[(5425,5450)],
'orange':[(5266,5770)],
'red' :[(5258,5864)] }
>>> labeled_values=[]
# make tuples of (value, brace status (open/close), color)
>>> for color,color_ranges in mycoord.items():
for color_range in color_ranges:
labeled_values.append((color_range[0],True,color))
labeled_values.append((color_range[1],False,color))
# labeled_values are now like (5258, True, 'pink'), (5422, False, 'pink') ...
>>> sects = []
# traverse the sorted values and maintain a color-set
>>> color_set_so_far=set()
>>> range_start = -1
>>> for value,range_open,color in sorted(labeled_values):
if not range_open or range_start != value:
sects.append(("-".join(color_set_so_far), range_start, value))
if range_open:
color_set_so_far.add(color)
else:
color_set_so_far.remove(color)
range_start = value
>>> sects = [s for s in sects if s[0] and s[1]!=s[2]] # filter out empty ranges
>>> sects
# [('pink-red', 5258, 5266), ('pink-orange-red', 5266, 5422), ('orange-red', 5422, 5425), ('orange-green-red', 5425, 5450), ('orange-red', 5450, 5479), ('pink-orange-red', 5479, 5770), ('pink-red', 5770, 5864)]

Related

Remove list in lists that satisfied the condition

I'm trying to make a quick OCR for specific use, I know should've just write a preprocessor for normal OCR and that would been faster but this idea came up to me first and I figure I should just try it anyway haha. This program would take a picture on a region of screen and identify the number within it, as of right now, it's only 0 and 1 but I've been working on it and stuck with some problems. Here is my code
while True:
if keyboard.is_pressed('`'):
Firstlist = list(pyautogui.locateAllOnScreen(image[0], confidence = 0.95,region=( 1570 , 990 , 230 , 70 )))
print(len(Firstlist))
Firstlist1 = list(pyautogui.locateAllOnScreen(image1, confidence = 0.95,region=( 1570 , 990 , 230 , 70 ))) + Firstlist
print(len(Firstlist1))
print(Firstlist)
if len(Firstlist) > 0:
print(Firstlist[0][0])
#compare all first instance of that number and eliminate all duplicated with in a different of 5 x pixel
break
Which would identify some predetermined set of number like this on screen and right now, it would give me a set of coordinate for number zero on screen, here is the result, please ignore other parts, it's just me playing around. Problem with this is pyautogui.locateAllOnScreen would sometimes generate duplicate value of the same picture within the coordinate ranging from approx 0-5 pixels if not set the confidence level right.
Example:
Value supposed to be [ (1655,1024,20,26),(1675,1024,20,26) ] but will yield a third value like [ (1655,1024,20,26), (1658,1024,20,26), (1675,1024,20,26) ].
And that's why I'm trying to make a correction for this. Is there anyway to identified if that x value of second duplicate coordinate is within a range of 0-5 pixels to the first coordinate and just delete it, moving the rest up the ladder so that the number would come up right and in order? Thank you!
Note: I'm still working on learning the list removal process by myself, and read the removing list with lambda to me is like gibberish. Please forgive me if something is wrong. Have a good day y'all!
You can try this.
if len(Firstlist) > 2:
elems = [f[0] for f in Firstlist] # create a list of just first index
i = 0
while i < len(elems) - 1: # iterate through the list with i
j = i + 1
while j < len(elems): # iterate through the rest of the list with j
if abs(elems[i] - elems[j]) <= 5: # if item at index i is within 5 pixels of item at index j
del elems[j] # delete item j and continue
else:
j += 1 # otherwise move to next item
i += 1 # Move to next i item
list1 = [ (1655,1024,20,26), (1658,1024,20,26), (1675,1024,20,26) ]
x = [list1[0]] + [x for x in list1 if abs(list1[0][0] - x[0]) > 5]
print(x)
Output:
[(1655, 1024, 20, 26), (1675, 1024, 20, 26)]

Create "synthetic points"

I need to create inside a python routine, something that I am calling "synthetic points".
I have a series of data which vary between -1 and 1, however, when I put this data on a chart, they form a trapezoidal chart.
What I would like to do is create points where the same x-axis value, could take two y-axis values, and then, this will create a chart with
straight lines making a "rectangular chart"
An example the format data that I have:
0;-1
1;-1
2;-1
3;-1
4;-1
5;-1
6;-1
7;1
8;1
9;1
10;1
11;1
12;1
13;1
14;1
15;1
16;-1
17;-1
18;-1
19;-1
20;-1
For example, in this case, I would need the data assume the following format:
0;-1
1;-1
2;-1
3;-1
4;-1
5;-1
6;-1
6;1 (point 6 with two values)
7;1
8;1
9;1
10;1
11;1
12;1
13;1
14;1
15;1
15;-1 (point 15 with two values)
16;-1
17;-1
18;-1
19;-1
20;-1
So what you need to do is, always when I had a value change, this will create a new point. This makes the graph, rectangular, as the only possible values for the y variable are -1 and 1.
The code I need to enter is below. What was done next was just to put the input data in this format of -1 and 1.
arq = open('vazdif.out', 'rt')
list = []
i = 0
for row in arq:
field = row.split(';')
vaz = float(field[2])
if vaz < 0:
list.append("-1")
elif vaz > 0:
list.append("1")
n = len(list)
fou = open('res_id.out','wt')
for i in range(n):
fou.write('{};{}\n'.format(i,list[i]))
fou.close
Thank you for your help
P.s. English is not my first language, forgive my mistakes on write or on the code.
I added a new value prev_value, if the previous value is of the opposite sign (multiply with the current value < 0), it adds an extra index to the list.
I think the field[1] and field[2] are probably wrong, but I'll trust your code works so far. Similar with fou, I would replace with with open ...
arq = open('vazdif.out', 'rt')
list = []
i = 0
prev_value = 0
for row in arq:
field = row.split(';')
xxx = int(field[1])
vaz = float(field[2])
if vaz * prev_value < 0:
list.append([list[-1][0], - list[-1][1]])
if vaz < 0:
list.append([xxx, -1])
else:
list.append([xxx, 1])
prev_val = vaz
fou = open('res_id.out','wt')
for i in list:
fou.write(f'{i[0]};{i[1]}\n')
fou.close

How to sort a collection based on another collection in Python?

I'm very new to Python and I need help with the following problem. This definition that I have posted below will take in a collection of tuples that represent rectangles Ex. (width,height).
In the method, I first sort the rectangle collection so that the elements with the largest width are at the beginning of the list, I then use this collection later to assign UPPER_LEFT_X and UPPER_LEFT_Y coordinates for each rectangle.
My question is, how do I take this collection of coordinates and sort it in a way that it is in the exact same order as the original_rectangle_collection? The way that the Driver works, is that the determined coordinates have to be given in the same order as the original rectangle collection. Thanks! Let me know if more explanation is required.
def find_best_coordinates(rectangles):
placement = []
upper_left_x = 0
upper_left_y = 0
loop_count = 0
original_rectangle_collection = rectangles
#sort the rectangles according to width.
rectangles.sort(key=operator.itemgetter(0))
#reverse the list so the tallest rectanlgle is placed first
rectangles.reverse()
#set the max width to the tallest rectangle's width
max_width = rectangles[0][0]
#print("Max width",max_width)
#loop through and place the rectangles
for rectangle in rectangles:
if loop_count == 0:
max_width = rectangle[0]
height = rectangle[1]
coordinate = (upper_left_x, upper_left_y)
placement.insert(0, coordinate)
upper_left_y = upper_left_y - height - 990
loop_count = loop_count + 1
if loop_count == 50:
upper_left_x = upper_left_x + max_width + 990
# print("x = ", upper_left_x)
loop_count = 0
#print("y = ", upper_left_y)
upper_left_y = 0
#reverse the list before it gets returned
placement.reverse()
return placement
You could do it this way, by always passing the original index along with the rectangle, and keeping it with the generated coordinate.
# Store indexes
rdicts = [{'index': i, 'rectangle': r} for i, r in enumerate(rectangles)]
# Sort by width
rdicts.sort(key=lambda d: d['rectangle'][0], reverse=True)
placement = []
for rdict in rdicts:
rectangle = rdict['rectangle']
... # do the rest of your algorithm
# Add the calculated coordinate, along with the original index
placement.append({'index': rdict['index'], 'coordinate': coordinate})
# Sort by the original index
placement.sort(key=lambda p: p['index'])
# Return the coordinates without the indexes.
ordered_placement = [p['coordinate'] for p in placement]
You can go a little faster by just sorting the indexes:
processing_order = sorted(
range(len(rectangles)),
key=lambda i: rectangles[i][0]
)
for rect_i in processing_order:
r = rectangles[rect_i]
# ... blah blah with r as current rectangle
# Done processing, output them in original order:
for r in rectangles:
# Still in order!

How to reduce a collection of ranges to a minimal set of ranges [duplicate]

This question already has answers here:
Union of multiple ranges
(5 answers)
Closed 7 years ago.
I'm trying to remove overlapping values from a collection of ranges.
The ranges are represented by a string like this:
499-505 100-115 80-119 113-140 500-550
I want the above to be reduced to two ranges: 80-140 499-550. That covers all the values without overlap.
Currently I have the following code.
cr = "100-115 115-119 113-125 80-114 180-185 500-550 109-120 95-114 200-250".split(" ")
ar = []
br = []
for i in cr:
(left,right) = i.split("-")
ar.append(left);
br.append(right);
inc = 0
for f in br:
i = int(f)
vac = []
jnc = 0
for g in ar:
j = int(g)
if(i >= j):
vac.append(j)
del br[jnc]
jnc += jnc
print vac
inc += inc
I split the array by - and store the range limits in ar and br. I iterate over these limits pairwise and if the i is at least as great as the j, I want to delete the element. But the program doesn't work. I expect it to produce this result: 80-125 500-550 200-250 180-185
For a quick and short solution,
from operator import itemgetter
from itertools import groupby
cr = "499-505 100-115 80-119 113-140 500-550".split(" ")
fullNumbers = []
for i in cr:
a = int(i.split("-")[0])
b = int(i.split("-")[1])
fullNumbers+=range(a,b+1)
# Remove duplicates and sort it
fullNumbers = sorted(list(set(fullNumbers)))
# Taken From http://stackoverflow.com/questions/2154249
def convertToRanges(data):
result = []
for k, g in groupby(enumerate(data), lambda (i,x):i-x):
group = map(itemgetter(1), g)
result.append(str(group[0])+"-"+str(group[-1]))
return result
print convertToRanges(fullNumbers)
#Output: ['80-140', '499-550']
For the given set in your program, output is ['80-125', '180-185', '200-250', '500-550']
Main Possible drawback of this solution: This may not be scalable!
Let me offer another solution that doesn't take time linearly proportional to the sum of the range sizes. Its running time is linearly proportional to the number of ranges.
def reduce(range_text):
parts = range_text.split()
if parts == []:
return ''
ranges = [ tuple(map(int, part.split('-'))) for part in parts ]
ranges.sort()
new_ranges = []
left, right = ranges[0]
for range in ranges[1:]:
next_left, next_right = range
if right + 1 < next_left: # Is the next range to the right?
new_ranges.append((left, right)) # Close the current range.
left, right = range # Start a new range.
else:
right = max(right, next_right) # Extend the current range.
new_ranges.append((left, right)) # Close the last range.
return ' '.join([ '-'.join(map(str, range)) for range in new_ranges ]
This function works by sorting the ranges, then looking at them in order and merging consecutive ranges that intersect.
Examples:
print(reduce('499-505 100-115 80-119 113-140 500-550'))
# => 80-140 499-550
print(reduce('100-115 115-119 113-125 80-114 180-185 500-550 109-120 95-114 200-250'))
# => 80-125 180-185 200-250 500-550

Python 3.3.2 - 'Grouping' System with Characters

I have a fun little problem.
I need to count the amount of 'groups' of characters in a file. Say the file is...
..##.#..#
##..####.
.........
###.###..
##...#...
The code will then count the amount of groups of #'s. For example, the above would be 3. It includes diagonals. Here is my code so far:
build = []
height = 0
with open('file.txt') as i:
build.append(i)
height += 1
length = len(build[0])
dirs = {'up':(-1, 0), 'down':(1, 0), 'left':(0, -1), 'right':(0, 1), 'upleft':(-1, -1), 'upright':(-1, 1), 'downleft':(1, -1), 'downright':(1, 1)}
def find_patches(grid, length):
queue = []
queue.append((0, 0))
patches = 0
while queue:
current = queue.pop(0)
line, cell = path[-1]
if ## This is where I am at. I was making a pathfinding system.
Here’s a naive solution I came up with. Originally I just wanted to loop through all the elements once an check for each, if I can put it into an existing group. That didn’t work however as some groups are only combined later (e.g. the first # in the second row would not belong to the big group until the second # in that row is processed). So I started working on a merge algorithm and then figured I could just do that from the beginning.
So how this works now is that I put every # into its own group. Then I keep looking at combinations of two groups and check if they are close enough to each other that they belong to the same group. If that’s the case, I merge them and restart the check. If I completely looked at all possible combinations and could not merge any more, I know that I’m done.
from itertools import combinations, product
def canMerge (g, h):
for i, j in g:
for x, y in h:
if abs(i - x) <= 1 and abs(j - y) <= 1:
return True
return False
def findGroups (field):
# initialize one-element groups
groups = [[(i, j)] for i, j in product(range(len(field)), range(len(field[0]))) if field[i][j] == '#']
# keep joining until no more joins can be executed
merged = True
while merged:
merged = False
for g, h in combinations(groups, 2):
if canMerge(g, h):
g.extend(h)
groups.remove(h)
merged = True
break
return groups
# intialize field
field = '''\
..##.#..#
##..####.
.........
###.###..
##...#...'''.splitlines()
groups = findGroups(field)
print(len(groups)) # 3
I'm not exactly sure what your code is trying to do. Your with statement opens a file, but all you do is append the file object to a list before the with ends and it gets closed (without its contents ever being read). I suspect his is not what you intend, but I'm not sure what you were aiming for.
If I understand your problem correctly, you are trying to count the connected components of a graph. In this case, the graph's vertices are the '#' characters, and the edges are wherever such characters are adjacent to each other in any direction (horizontally, vertically or diagonally).
There are pretty simple algorithms for solving that problem. One is to use a disjoint set data structure (also known as a "union-find" structure, since union and find are the two operations it supports) to connect groups of '#' characters together as they're read in from the file.
Here's a fairly minimal disjoint set I wrote to answer another question a while ago:
class UnionFind:
def __init__(self):
self.rank = {}
self.parent = {}
def find(self, element):
if element not in self.parent: # leader elements are not in `parent` dict
return element
leader = self.find(self.parent[element]) # search recursively
self.parent[element] = leader # compress path by saving leader as parent
return leader
def union(self, leader1, leader2):
rank1 = self.rank.get(leader1,1)
rank2 = self.rank.get(leader2,1)
if rank1 > rank2: # union by rank
self.parent[leader2] = leader1
elif rank2 > rank1:
self.parent[leader1] = leader2
else: # ranks are equal
self.parent[leader2] = leader1 # favor leader1 arbitrarily
self.rank[leader1] = rank1+1 # increment rank
And here's how you can use it for your problem, using x, y tuples for the nodes:
nodes = set()
groups = UnionFind()
with open('file.txt') as f:
for y, line in enumerate(f): # iterate over lines
for x, char in enumerate(line): # and characters within a line
if char == '#':
nodes.add((x, y)) # maintain a set of node coordinates
# check for neighbors that have already been read
neighbors = [(x-1, y-1), # up-left
(x, y-1), # up
(x+1, y-1), # up-right
(x-1, y)] # left
for neighbor in neighbors:
if neighbor in nodes:
my_group = groups.find((x, y))
neighbor_group = groups.find(neighbor)
if my_group != neighbor_group:
groups.union(my_group, neighbor_group)
# finally, count the number of unique groups
number_of_groups = len(set(groups.find(n) for n in nodes))

Categories