Skip to content

Commit 249d45d

Browse files
committed
Initial code commit
1 parent f14ead0 commit 249d45d

File tree

1 file changed

+360
-0
lines changed

1 file changed

+360
-0
lines changed

parse.py

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
import sys, itertools, requests, json, os.path, hashlib, time, cv2, numpy as np
2+
3+
api_cache_folder = 'cache'
4+
api_url = 'http://schoolido.lu/api/cards/?page_size=100'
5+
round_card_images_folder = 'round_card_images'
6+
all_patterns_file = 'cache/all_patterns.png'
7+
8+
circle_size = 128
9+
10+
rarity_top = 2
11+
rarity_height = 22
12+
rarity_left = 2
13+
rarity_width = 22
14+
15+
pattern_top = 16
16+
pattern_height = 80
17+
pattern_left = 16
18+
pattern_width = 80
19+
pattern_ratio = 0.5
20+
21+
match_top = 12
22+
match_height = 88
23+
match_left = 12
24+
match_width = 88
25+
26+
27+
def api_get(url):
28+
"""
29+
Make a request against the schoolido.lu api. The results are cached
30+
:param url: url
31+
:return: result data
32+
"""
33+
cache_file = "%s/%s.json" % (api_cache_folder, hashlib.md5(url).hexdigest())
34+
if not os.path.isfile(cache_file):
35+
if not os.path.isdir(api_cache_folder):
36+
os.mkdir(api_cache_folder)
37+
api_result = []
38+
while url is not None:
39+
sys.stderr.write('Fetching %s\n' % url)
40+
r = requests.get(url)
41+
if r.status_code != 200:
42+
raise Exception(r.status_code)
43+
json_data = r.json()
44+
url = json_data['next']
45+
api_result += json_data['results']
46+
with open(cache_file, 'w') as f:
47+
json.dump(api_result, f, indent=4, sort_keys=True)
48+
49+
if os.path.isfile(all_patterns_file):
50+
os.unlink(all_patterns_file)
51+
52+
with open(cache_file) as f:
53+
result = json.load(f)
54+
return result
55+
56+
57+
def get_card_images(card):
58+
"""
59+
Returns a list of a card's round images including the remote and local (if cached) paths, and whether it
60+
belongs to the idolized version of the card
61+
:param card: card data from schoolido.lu api
62+
:return: list of (url, local_path, idolized) tuples
63+
"""
64+
urls = [(card['round_card_image'], False), (card['round_card_idolized_image'], True)]
65+
result = []
66+
for url, idolized in urls:
67+
if url is None:
68+
continue
69+
if url[0:2] == '//':
70+
url = 'http:%s' % url
71+
local_path = "%s/%s" % (round_card_images_folder, os.path.basename(url).split('?')[0])
72+
result.append((url, local_path, idolized))
73+
return result
74+
75+
76+
def fetch_round_card(card):
77+
"""
78+
Downloads a card's round images (if they don't exist).
79+
There is a 100ms sleep time after each download to give the host some rest.
80+
:param card: card data from schoolido.lu api
81+
"""
82+
if not os.path.isdir(round_card_images_folder):
83+
os.mkdir(round_card_images_folder)
84+
85+
for url, dest, idolized in get_card_images(card):
86+
if not os.path.isfile(dest):
87+
sys.stderr.write('Fetching %s\n' % url)
88+
r = requests.get(url)
89+
if r.status_code != 200:
90+
raise Exception(r.status_code)
91+
with open(dest, 'wb') as f:
92+
f.write(r.content)
93+
time.sleep(0.1)
94+
95+
96+
def get_card_group(card, idolized):
97+
"""
98+
Returns an identifier for a card's group, to help separate them by rarities
99+
:param card: card data from schoolido.lu api
100+
:param idolized: True if idolized version
101+
:return: card group name
102+
"""
103+
return "%s-%s%d" % (card['rarity'], card['attribute'], 1 if idolized else 0)
104+
105+
106+
def make_rarity_patterns(cards):
107+
"""
108+
Make and return patterns for each card rarity
109+
:param cards: list of card data from schoolido.lu ap
110+
:return: list of (group identifier, pattern, idolized) tuples
111+
"""
112+
did = {}
113+
patterns = []
114+
115+
for card in cards:
116+
images = get_card_images(card)
117+
for index, (url, local_path, idolized) in enumerate(images):
118+
group = get_card_group(card, idolized)
119+
if group in did:
120+
continue
121+
did[group] = 1
122+
im = cv2.imread(local_path)
123+
im_cropped = im[rarity_top:rarity_top + rarity_height, rarity_left:rarity_left + rarity_width]
124+
patterns.append((group, im_cropped, idolized))
125+
return patterns
126+
127+
128+
def get_pattern_coordinates(index, idolized):
129+
"""
130+
Returns coordinates for extracting the card's pattern from the all_patterns image
131+
:param index: card index inside the cards obtained from the api
132+
:param idolized: True if idolized version
133+
:return: coordinates to be used as im_patterns[coordinates]
134+
"""
135+
real_index = (index * 2 + 1) if idolized else (index * 2)
136+
y1 = int(pattern_height * pattern_ratio * real_index)
137+
y2 = int(pattern_height * pattern_ratio * (real_index + 1))
138+
x1 = 0
139+
x2 = int(pattern_width * pattern_ratio)
140+
return np.index_exp[y1:y2, x1:x2]
141+
142+
143+
def generate_all_patterns(cards):
144+
"""
145+
Generates an all_patterns image
146+
:param cards: list of card data from schoolido.lu ap
147+
"""
148+
if not os.path.isdir(os.path.dirname(all_patterns_file)):
149+
os.mkdir(os.path.dirname(all_patterns_file))
150+
coordinates = get_pattern_coordinates(len(cards), True)
151+
im_patterns_height, im_patterns_width = [c.stop for c in coordinates]
152+
im_patterns = np.ones((im_patterns_height, im_patterns_width, 3), np.uint8)
153+
154+
for index, card in enumerate(cards):
155+
for url, local_path, idolized in get_card_images(card):
156+
if not os.path.exists(local_path):
157+
fetch_round_card(card)
158+
im = cv2.imread(local_path)
159+
im_cropped = im[pattern_top:pattern_top + pattern_height, pattern_left:pattern_left + pattern_width]
160+
im_cropped = cv2.resize(im_cropped, (0, 0), fx=pattern_ratio, fy=pattern_ratio)
161+
coordinates = get_pattern_coordinates(index, idolized)
162+
im_patterns[coordinates] = im_cropped
163+
cv2.imwrite(all_patterns_file, im_patterns)
164+
165+
166+
def make_card_patterns(cards):
167+
"""
168+
Return patterns for each round card image
169+
:param cards: list of card data from schoolido.lu ap
170+
:return: a map, with group identifiers as keys and a list of (pattern, card data, idolized) as values
171+
"""
172+
if not os.path.isfile(all_patterns_file):
173+
generate_all_patterns(cards)
174+
175+
im_patterns = cv2.imread(all_patterns_file)
176+
result = {}
177+
for index, card in enumerate(cards):
178+
for url, local_path, idolized in get_card_images(card):
179+
coordinates = get_pattern_coordinates(index, idolized)
180+
im_cropped = im_patterns[coordinates]
181+
182+
groups = [get_card_group(card, idolized)]
183+
# Include non-idolized patterns in both groups, since an idolized card can show the unidolized version
184+
if not idolized:
185+
groups.append(get_card_group(card, True))
186+
for group in groups:
187+
if group not in result:
188+
result[group] = []
189+
result[group].append((im_cropped, card, idolized))
190+
return result
191+
192+
193+
def vertical_split(im):
194+
"""
195+
Split an image vertically. This works for member list as well as (well, most times) scouting screenshots
196+
:param im: screenshot data
197+
:return: the ratio for resizing the screenshot, a list of (start, stop) chunks to use when looping
198+
"""
199+
im_gray = cv2.cvtColor(im, cv2.COLOR_RGB2GRAY)
200+
thresh = 223
201+
202+
im_bw = cv2.threshold(im_gray, thresh, 255, cv2.THRESH_BINARY)[1]
203+
rows = [y for y in xrange(len(im_bw)) if
204+
max([len(list(g)) for e, g in itertools.groupby(im_bw[y]) if e == 255] or [0]) > len(im[0]) / 2]
205+
206+
row_groups = [(min(arr), max(arr)) for arr in np.split(rows, np.where(np.diff(rows) != 1)[0] + 1)]
207+
row_sizes = [(b[0] - a[1] - 1) for a, b in zip(row_groups, row_groups[1:]) if (b[0] - a[1] - 1) > 100]
208+
row_positions = [(a[1], b[0]) for a, b in zip(row_groups, row_groups[1:]) if (b[0] - a[1] - 1) > 100]
209+
210+
ratio = 1.0 * circle_size / np.median(row_sizes)
211+
return ratio, row_positions
212+
213+
214+
def horizontal_split(im):
215+
"""
216+
Horizontally split a row into smaller chunks which could contain rounded card images
217+
:param im: a screenshot's row obtained after using vertical_split's result
218+
:return: a list of (start, stop) chunks to use when looping
219+
"""
220+
im_gray = cv2.cvtColor(im, cv2.COLOR_RGB2GRAY)
221+
thresh = 223
222+
223+
im_bw = cv2.threshold(im_gray, thresh, 255, cv2.THRESH_BINARY)[1]
224+
im_width = len(im[0])
225+
226+
cols = [y for y in xrange(len(im_bw[0])) if 0 not in np.transpose(im_bw)[y]]
227+
228+
col_groups = [(min(arr), max(arr)) for arr in np.split(cols, np.where(np.diff(cols) != 1)[0] + 1)]
229+
col_positions = [(b, b + circle_size) for a, b in col_groups] + [(a - circle_size, a) for a, b in col_groups]
230+
unique_col_positions = []
231+
for col_position in col_positions:
232+
if col_position[0] < 0 or col_position[1] >= im_width:
233+
continue
234+
if any([abs(col_position[0] - col_position2[0]) <= 2 and abs(col_position[1] - col_position2[1]) <= 2 for
235+
col_position2 in unique_col_positions]):
236+
break
237+
unique_col_positions.append(col_position)
238+
239+
return unique_col_positions
240+
241+
242+
def get_matching_cards(im_match, possible_cards):
243+
"""
244+
Tries to match a region against a list of possible card patterns
245+
:param im_match: region data
246+
:param possible_cards: list of (card_pattern, card data, idolized) tuples
247+
:return: the matching cards, as (card data, idolized) tuples
248+
"""
249+
im_match = cv2.resize(im_match, (0, 0), fx=pattern_ratio, fy=pattern_ratio)
250+
matches = []
251+
threshold = 0.8
252+
for card_pattern, card, idolized in possible_cards:
253+
if len(im_match) < len(card_pattern) or len(im_match[0]) < len(card_pattern[0]):
254+
break
255+
res = cv2.matchTemplate(im_match, card_pattern, cv2.TM_CCOEFF_NORMED)
256+
loc = np.where(res >= threshold)
257+
card_pattern_matches = zip(*loc[::-1])
258+
if card_pattern_matches:
259+
matches.append((card, idolized))
260+
return matches
261+
262+
263+
def search_row(im, rarity_patterns, card_patterns):
264+
"""
265+
Searches a row for round card pattern matches
266+
:param im: the image's row data
267+
:param rarity_patterns: result from make_rarity_patterns
268+
:param card_patterns: result from make_make_card_patterns
269+
:return: list of match information
270+
"""
271+
found_cards = []
272+
273+
im_width = len(im[0])
274+
im_height = len(im)
275+
276+
col_positions = horizontal_split(im)
277+
278+
for group_min_x, group_max_x in col_positions:
279+
im4 = im[:, group_min_x:group_max_x]
280+
281+
for pattern_group, pattern, pattern_idolized in rarity_patterns:
282+
im3 = im4[0:rarity_top + rarity_height + 2, 0:rarity_left + rarity_width + 2]
283+
res = cv2.matchTemplate(im3, pattern, cv2.TM_CCOEFF_NORMED)
284+
285+
threshold = 0.7
286+
loc = np.where(res >= threshold)
287+
rarity_pattern_matches = zip(*loc[::-1])
288+
289+
for index, pt in enumerate(rarity_pattern_matches):
290+
distances = [abs(pt[0] - pt2[0]) + abs(pt[1] - pt2[1]) for pt2 in rarity_pattern_matches[:index]]
291+
if distances and min(distances) < 100:
292+
continue
293+
294+
match_x = pt[0] - rarity_left
295+
match_y = pt[1] - rarity_top
296+
relative_x = 1.0 * (match_x + group_min_x) / im_width
297+
relative_y = 1.0 * match_y / im_height
298+
299+
im_match = im4[pt[1] + match_top:pt[1] + match_top + match_height,
300+
pt[0] + match_left:pt[0] + match_left + match_width]
301+
for matching_card, matching_idolized in get_matching_cards(im_match, card_patterns[pattern_group]):
302+
found_cards.append({
303+
'card': matching_card,
304+
'idolized': matching_idolized,
305+
'relative_x': relative_x,
306+
'relative_y': relative_y,
307+
})
308+
return found_cards
309+
310+
311+
def main():
312+
"""
313+
Reads a screenshot and tries to find round card image matches, using data and images from schoolido.lu
314+
The screenshot's path is read from argv[1]
315+
The result is written as json to stdout
316+
Extra info is written to stderr
317+
Note: Cache is stored inside cache/ and round_card_images/
318+
Delete the cache/ folder to force downloading of card data
319+
"""
320+
os.chdir(os.path.dirname(os.path.realpath(__file__)))
321+
start = time.time()
322+
cards = api_get(api_url)
323+
sys.stderr.write("%f elapsed after api_get\n" % (time.time() - start))
324+
325+
card_patterns = make_card_patterns(cards)
326+
sys.stderr.write("%f elapsed after card_patterns\n" % (time.time() - start))
327+
328+
rarity_patterns = make_rarity_patterns(cards)
329+
sys.stderr.write("%f elapsed after rarity_patterns\n" % (time.time() - start))
330+
331+
im_original = cv2.imread(sys.argv[1])
332+
sys.stderr.write("%f elapsed after imread\n" % (time.time() - start))
333+
334+
ratio, row_positions = vertical_split(im_original)
335+
sys.stderr.write("%f elapsed after vertical_split\n" % (time.time() - start))
336+
337+
original_width = len(im_original[0])
338+
original_height = len(im_original)
339+
resized_width = original_width * ratio
340+
resized_height = original_height * ratio
341+
342+
found_cards = []
343+
for group_min_y, group_max_y in row_positions:
344+
im = cv2.resize(im_original[group_min_y:group_max_y, :], (0, 0), fx=ratio, fy=ratio)
345+
group_min_relative_y = 1.0 * group_min_y / original_height
346+
group_relative_h = 1.0 * (group_max_y - group_min_y) / original_height
347+
for found_card in search_row(im, rarity_patterns, card_patterns):
348+
found_card['relative_y'] = group_min_relative_y + group_relative_h * found_card['relative_y']
349+
found_cards.append(found_card)
350+
351+
for found_card in found_cards:
352+
found_card['relative_w'] = 1.0 * circle_size / resized_width
353+
found_card['relative_h'] = 1.0 * circle_size / resized_height
354+
355+
sys.stderr.write("%f elapsed before dumping\n" % (time.time() - start))
356+
sys.stdout.write(json.dumps(found_cards, indent=4, sort_keys=True))
357+
358+
359+
if __name__ == '__main__':
360+
main()

0 commit comments

Comments
 (0)