def speed(self, last_session=False, file_name=None): """Render speed track image.""" track_data = self.data.get_speed() if track_data is None: Message('No tracking data found.') return None top_resolution, (min_value, max_value), tracks = track_data output_resolution, upscale_resolution = calculate_resolution( tracks.keys(), top_resolution) upscaled_arrays = upscale_arrays_to_resolution(tracks, upscale_resolution) colour_range = self._get_colour_range(min_value, max_value, 'GenerateSpeed') image_output = arrays_to_colour(colour_range, upscaled_arrays) image_output = image_output.resize(output_resolution, Image.ANTIALIAS) if file_name is None: file_name = self.name.generate('Speed', reload=True) if self.save: create_folder(file_name) Message('Saving image to "{}"...'.format(file_name)) image_output.save(file_name) Message('Finished saving.')
def arrays_to_heatmap(numpy_arrays, gaussian_size, clip): """Convert list of arrays into a heatmap. The stages and values are chosen with trial and error, so this function is still open to improvement. """ #Add all arrays together Message('Merging arrays...') merged_arrays = numpy.merge(numpy_arrays, 'add', 'float64') #Set to constant values Message('Flattening values...') flattened = numpy.remap_to_range(merged_arrays) #Blur the array if gaussian_size: Message('Applying gaussian blur...') heatmap = blur(flattened, gaussian_size) else: heatmap = flattened Message('Finding range limits...') min_value = numpy.min(heatmap) #Lower the maximum value a little all_values = numpy.sort(heatmap.ravel(), unique=True) max_value = all_values[round_int(all_values.size * clip)] return ((min_value, max_value), heatmap)
def keyboard(self, last_session=False, file_name=None): """Render keyboard image.""" kb = DrawKeyboard(self.profile, self.data, last_session=last_session) image_output = kb.draw_image() if file_name is None: file_name = self.name.generate('Keyboard', reload=True) if self.save: create_folder(file_name) Message('Saving image to "{}"...'.format(file_name)) image_output.save(file_name) Message('Finished saving.')
def get_resolution(): """Get the resolution of the main monitor. Returns: (x, y) resolution as a tuple. """ Message('Unable to read resolution, set to default of 1920x1080.') return (1920, 1080)
def calculate(self): Message(self.string['coordinates']) (width, height), coordinate_dict = self.grid.generate_coordinates(self.keys) return { 'Width': width, 'Height': height, 'Coordinates': coordinate_dict }
def clicks(self, last_session=False, file_name=None, _double_click=False): """Render heatmap of clicks.""" top_resolution, (min_value, max_value), clicks = self.data.get_clicks( session=last_session, double_click=_double_click) output_resolution, upscale_resolution = calculate_resolution( clicks.keys(), top_resolution) lmb = CONFIG['GenerateHeatmap']['_MouseButtonLeft'] mmb = CONFIG['GenerateHeatmap']['_MouseButtonMiddle'] rmb = CONFIG['GenerateHeatmap']['_MouseButtonRight'] skip = [] if lmb or mmb or rmb: if not lmb: skip.append(0) if not mmb: skip.append(1) if not rmb: skip.append(2) upscaled_arrays = upscale_arrays_to_resolution(clicks, upscale_resolution, skip=skip) (min_value, max_value), heatmap = arrays_to_heatmap( upscaled_arrays, gaussian_size=gaussian_size(upscale_resolution[0], upscale_resolution[1]), clip=1 - CONFIG['Advanced']['HeatmapRangeClipping']) colour_range = self._get_colour_range(min_value, max_value, 'GenerateHeatmap') image_output = Image.fromarray(colour_range.convert_to_rgb(heatmap)) image_output = image_output.resize(output_resolution, Image.ANTIALIAS) if file_name is None: file_name = self.name.generate('Clicks', reload=True) if self.save: create_folder(file_name) Message('Saving image to "{}"...'.format(file_name)) image_output.save(file_name) Message('Finished saving.')
def upscale_arrays_to_resolution(arrays, target_resolution, skip=[]): """Upscale a dict of arrays to a certain resolution. The dictionary key must be a resolution, and the values can either be an array or list of arrays. Use skip to ignore array indexes in the list. """ if isinstance(skip, int): skip = [skip] skip = set(skip) #Count number of arrays num_arrays = 0 for resolution, array_list in get_items(arrays): if isinstance(array_list, (list, tuple)): array_len = len(array_list) num_arrays += array_len - len( [i for i in range(array_len) if i in skip]) elif 0 not in skip: num_arrays += 1 #Upscale each array Message('Upscaling arrays to {}x{}...'.format(target_resolution[0], target_resolution[1])) processed = 0 output = [] for resolution, array_list in get_items(arrays): if not isinstance(array_list, (list, tuple)): array_list = [array_list] for i, array in enumerate(array_list): if i in skip: continue processed += 1 Message('Processing array for {}x{} ({}/{})'.format( resolution[0], resolution[1], processed, num_arrays)) zoom_factor = (target_resolution[1] / resolution[1], target_resolution[0] / resolution[0]) upscaled = upscale(array, zoom_factor) output.append(upscaled) return output
def __init__(self, profile_name, data=None, last_session=False): all_strings = Language().get_strings() self._string = all_strings['string'] self.string = all_strings['string']['keyboard'] self.keys = all_strings['keyboard']['key'] #self.word = all_strings['word'] self.name = profile_name self.last_session = last_session Message(self._string['profile']['load']) self.reload(data)
def get_output(self): allowed_levels = range(CONFIG['Advanced']['MessageLevel'], 3) output = [ capitalize(u' | '.join(self.message_queue[i])) for i in allowed_levels ][::-1] message = u' | '.join(i for i in output if i) for msg in self._debug: Message(msg) self.reset() return message
def convert_to_rgb(self, array): """Convert an array into an RGB numpy array.""" message = 'Converting {} points to RGB values... (this may take a few seconds)' try: Message(message.format(array.size)) except AttributeError: array = numpy.array(array) Message(message.format(array.size)) new = numpy.round(numpy.divide(array - self.min, self._step_size), 0, 'int64') start_colour = self.cache[0] end_colour = self.cache[-1] colour_array = [[ self.cache[item] if 0 <= item <= self.steps else start_colour if item < 0 else end_colour for item in sublst ] for sublst in new.tolist()] return numpy.array(colour_array, dtype='uint8')
def keyboard(self, image_name): Message('Generating CSV from keyboard...') result = ['Key,Count,Time'] for key in self.data['Keys']['All']['Pressed']: result.append('{},{},{}'.format( key, self.data['Keys']['All']['Pressed'][key], self.data['Keys']['All']['Held'][key])) file_name = image_name.generate('csv-keyboard', reload=True) create_folder(file_name) with open(file_name, 'w') as f: f.write('\n'.join(result))
def fix_poe_mine_build(profile_name, numpad_key): try: _num = 'NUM{}'.format(int(numpad_key)) except ValueError: return False data = LoadData(profile_name, _reset_sessions=False) #Make sure record exists, quick delete if issue is obvious (0 press >0 held) try: if not data['Keys']['All']['Pressed'][_num]: raise KeyError except KeyError: try: del data['Keys']['All']['Held'][_num] except KeyError: pass else: #Count the other numpad items total = {'pressed': 0, 'held': 0, 'count': 0} for i in range(1, 10): if i == numpad_key: continue num = 'NUM{}'.format(i) try: total['pressed'] += data['Keys']['All']['Pressed'][num] total['held'] += data['Keys']['All']['Held'][num] except KeyError: pass else: total['count'] += 1 #Get the average time the numpad is pressed for try: average_press_time = total['held'] / total['pressed'] except ZeroDivisionError: average_press_time = 0 Message('Unable to get an average as no other keypad data exists.') result = input( 'Do you want to delete the numpad key instead (y/n)? ') if not is_yes(result): return False #Assign to numpad key new_held_time = round_int(data['Keys']['All']['Pressed'][_num] * average_press_time) data['Keys']['All']['Held'][_num] = new_held_time return save_data(profile_name, data)
def tracks(self, image_name): Message('Generating CSV from tracks...') for resolution in self.data['Maps']['Tracks']: CONFIG['GenerateImages']['_TempResolutionX'], CONFIG[ 'GenerateImages']['_TempResolutionY'] = resolution result = self._generate(resolution, self.data['Maps']['Tracks'][resolution]) if result is not None: file_name = image_name.generate('csv-tracks', reload=True) create_folder(file_name) with open(file_name, 'w') as f: f.write(result)
def _create_grid(self): Message(self.string['layout']) grid = KeyboardGrid(self.key_counts, _new_row=False) layout = Language().get_keyboard_layout() for row in layout: grid.new_row() for name, width, height in row: if name == '__STATS__': hide_border = True custom_colour = False else: hide_border = False custom_colour = None grid.add_key(name, width, height, hide_border=hide_border, custom_colour=custom_colour) return grid
def handle_error(trace=None, log=True): """Any errors are sent to here.""" if trace is not None: output = [ 'Mouse Tracks {} ({}) | Python {} | {}'.format( VERSION, FILE_VERSION, PYTHON_VERSION, OPERATING_SYSTEM) ] output.append('') output.append(trace) output = '\n'.join(output) if log: file_name = format_file_path('{}\\error.txt'.format(DEFAULT_PATH)) with open(file_name, 'w') as f: f.write(output) Message(trace) string = Language().get_strings() input(string['string']['exit']) sys.exit(0)
def clicks(self, image_name): mouse_buttons = ['LMB', 'MMB', 'RMB'] Message('Generating CSV from clicks...') for i, mouse_button in enumerate(self.data['Maps']['Click']['Single']): CONFIG['GenerateHeatmap']['_MouseButtonLeft'] = i == 0 CONFIG['GenerateHeatmap']['_MouseButtonMiddle'] = i == 1 CONFIG['GenerateHeatmap']['_MouseButtonRight'] = i == 2 for resolution, array in get_items( self.data['Maps']['Click']['Single'][mouse_button]): CONFIG['GenerateImages']['_TempResolutionX'], CONFIG[ 'GenerateImages']['_TempResolutionY'] = resolution result = self._generate(resolution, array) if result is not None: file_name = image_name.generate('csv-clicks', reload=True) create_folder(file_name) with open(file_name, 'w') as f: f.write(result)
average_press_time = 0 Message('Unable to get an average as no other keypad data exists.') result = input( 'Do you want to delete the numpad key instead (y/n)? ') if not is_yes(result): return False #Assign to numpad key new_held_time = round_int(data['Keys']['All']['Pressed'][_num] * average_press_time) data['Keys']['All']['Held'][_num] = new_held_time return save_data(profile_name, data) Message('This is a fix for the numlock mine trick in Path of Exile.') Message( 'The trick fools the computer into thinking the key is held down, but it doesn\'t release when the game is quit.' ) Message( 'This fix replaces the \'held down\' time of the key with an average from the other numpad keys.' ) Message( 'The issue is now fixed for other profiles, as the held down keys will be ignored after a profile switch.' ) profile_name = input('Type the name of the profile to fix: ') numpad_key = input('Type which numpad key is being used for mines: ') Message('Please wait while the fix is completed...') if fix_poe_mine_build(profile_name, numpad_key): Message('Finished updating profile.') else:
from __future__ import absolute_import from core.compatibility import Message from core.os import get_key_press from core.track import RefreshRateLimiter while True: with RefreshRateLimiter(10): keys = [] for i in range(256): if get_key_press(i): keys.append(i) if keys: Message(keys)
def draw_image(self, file_path=None, font='arial.ttf'): data = self.calculate() #Create image object image = Image.new('RGB', (data['Width'], data['Height'])) background = tuple(list(data['Coordinates']['Background'][:3]) + [0]) image.paste(data['Coordinates']['Background'], (0, 0, data['Width'], data['Height'])) pixels = image.load() #Add drop shadow shadow = (64, 64, 64) if (DROP_SHADOW_X or DROP_SHADOW_Y ) and data['Coordinates']['Background'][:3] == (255, 255, 255): Message(self.string['draw']['shadow']) shadow_colour = tuple( int(pow(i + 30, 0.9625)) for i in data['Coordinates']['Shadow']) for colour in data['Coordinates']['Fill']: for x, y in data['Coordinates']['Fill'][colour]: pixels[DROP_SHADOW_X + x, DROP_SHADOW_Y + y] = shadow #Fill colours Message(self.string['draw']['keys']) for colour in data['Coordinates']['Fill']: for x, y in data['Coordinates']['Fill'][colour]: pixels[x, y] = colour #Draw border Message(self.string['draw']['outline']) border = tuple(255 - i for i in data['Coordinates']['Background']) for x, y in data['Coordinates']['Outline']: pixels[x, y] = border #Draw text Message(self.string['draw']['text']) draw = ImageDraw.Draw(image) font_key = ImageFont.truetype(font, size=FONT_SIZE_MAIN) font_amount = ImageFont.truetype(font, size=FONT_SIZE_STATS) #Generate stats stats = [ self.string['stats']['time'].format( T=ticks_to_seconds(self.ticks, 60)) ] total_presses = format_amount(sum(self.key_counts['Pressed'].values()), 'press', max_length=25, decimal_units=False) stats.append(self.string['stats']['count'].format(T=total_presses)) if CONFIG['GenerateKeyboard']['DataSet'].lower() == 'time': stats.append(self.string['stats']['colour']['time']) elif CONFIG['GenerateKeyboard']['DataSet'].lower() == 'count': stats.append(self.string['stats']['colour']['count']) stats_text = ['{}:'.format(self.name), '\n'.join(stats)] #Write text to image for values in data['Coordinates']['Text']: x, y = values['Offset'] text = values['KeyName'] text_colour = values['Colour'] #Override for stats text if text == '__STATS__': draw.text((x, y), stats_text[0], font=font_key, fill=text_colour) y += (FONT_SIZE_MAIN + FONT_LINE_SPACING) draw.text((x, y), stats_text[1], font=font_amount, fill=text_colour) continue height_multiplier = max(0, values['Dimensions'][1] - 1) x += FONT_OFFSET_X if not height_multiplier: y += FONT_OFFSET_Y y += (KEY_SIZE - FONT_SIZE_MAIN + FONT_OFFSET_Y) * height_multiplier #Ensure each key is at least at a constant height if '\n' not in text: text += '\n' draw.text((x, y), text, font=font_key, fill=text_colour) #Correctly place count at bottom of key if height_multiplier: y = values['Offset'][1] + ( KEY_SIZE + KEY_PADDING) * height_multiplier + FONT_OFFSET_Y y += (FONT_SIZE_MAIN + FONT_LINE_SPACING) * (1 + text.count('\n')) #Here either do count or percent, but not both as it won't fit output_type = 'press' #press or time max_width = int(10 * values['Dimensions'][0] - 3) text = format_amount(values['Counts'][output_type], output_type, max_length=max_width, min_length=max_width - 1, decimal_units=False) draw.text((x, y), 'x{}'.format(text), font=font_amount, fill=text_colour) if file_path: Message(self._string['image']['save']['start']) image.save(file_path, CONFIG['GenerateImages']['FileType']) Message(self._string['image']['save']['end']) return image
def user_generate(): """Ask for options and generate an image. This seriously needs rewriting. Idea: List of profiles (choose page/type id/type name), shows the file size and last modified date of each profile. (Load profile) Say some stats about the profile Ask for mouse tracks, clicks and key presses For each of those, ask for colour profile and say the file location Ask to open folder (will require image path rewrite for a base path) Loop back to start if required """ all_strings = Language().get_strings() _string = all_strings['string'] string = all_strings['string']['image'] word = all_strings['word'] Message(string['profile']['list'].format(L=word['list'])) profile = input() if profile.lower() == word['list'].lower(): #Read the data folder and format names all_files = list_data_files() if not all_files: Message(string['profile']['empty']) Message(all_strings['exit']) input() sys.exit() programs = {format_name(DEFAULT_NAME): DEFAULT_NAME} app_list = AppList() for program_name in app_list.names: programs[format_name(program_name)] = program_name page = 1 limit = 15 maximum = len(all_files) total_pages = round_up(maximum / limit) sort_date = string['page']['sort']['date'] sort_name = string['page']['sort']['name'] change_sort = [sort_name, 2] #Ask for user input while True: offset = (page - 1) * limit results = all_files[offset:offset + limit] for i, r in enumerate(results): try: program_name = programs[r] except KeyError: program_name = r Message('{}: {}'.format(i + offset + 1, program_name)) Message(string['page']['current'].format(C=page, T=total_pages, P=word['page'])) Message(change_sort[0].format( S='{} {}'.format(word['sort'], change_sort[1]))) Message(string['profile']['number']['input']) profile = input() last_page = page #Change page if profile.lower().startswith('{P} '.format(P=word['page'])): try: page = int(profile.split()[1]) if not 0 < page <= total_pages: raise ValueError except IndexError: Message(string['page']['invalid']) except ValueError: if page > total_pages: page = total_pages else: page = 1 #Shortcut to change page elif profile.endswith('>'): if page < total_pages: page += 1 elif profile.startswith('<'): if page > 1: page -= 1 #Change sorting of profile list elif (profile.lower().startswith('{} '.format(word['sort'])) or profile.lower() == word['sort']): try: sort_level = int(profile.split()[1]) except ValueError: sort_level = 0 except IndexError: sort_level = 1 try: sort_reverse = int(profile.split()[2]) except (ValueError, IndexError): sort_reverse = 0 if sort_level == 1: all_files = list_data_files() change_sort = [sort_name, 2] elif sort_level == 2: all_files = sorted(list_data_files()) change_sort = [sort_date, 1] if sort_reverse: all_files = all_files[::-1] #Select profile else: try: num = int(profile) - 1 if not 0 <= num <= maximum: raise IndexError profile_name = all_files[num] try: profile = programs[all_files[num]] except KeyError: profile = all_files[num] break except ValueError: break except IndexError: Message(string['profile']['number']['nomatch']) try: profile = programs[profile] except KeyError: pass #Load functions Message(_string['import']) from core.image import RenderImage Message(_string['profile']['load'].format(P=profile)) try: r = RenderImage(profile) except ValueError: Message('Error: Selected profile is empty or doesn\'t exist.') return #Check if profile is running try: current_profile = format_name(RunningApplications().check()[0]) except TypeError: pass else: selected_profile = format_name(profile) if current_profile == selected_profile: Message(string['profile']['running']['warning']) save_time = ticks_to_seconds(CONFIG['Save']['Frequency'], 1) metadata = load_data(profile, _metadata_only=True) if metadata['Modified'] is None: Message(string['save']['wait']) Message(string['save']['frequency'].format(T=save_time)) Message(_string['exit']) input() sys.exit() else: last_save_time = time.time() - metadata['Modified'] next_save_time = CONFIG['Save']['Frequency'] - last_save_time last_save = ticks_to_seconds(last_save_time, 1, allow_decimals=False) next_save = ticks_to_seconds(next_save_time, 1, allow_decimals=False) Message(string['save']['next'].format(T1=last_save, T2=next_save)) generate_tracks = False generate_speed = False generate_heatmap = False generate_keyboard = False generate_csv = False default_options = [True, False, True, True, False] kb_string = string['name']['keyboard'] kph = r.keys_per_hour() if kph < 10: default_options[2] = False kb_string = '{} ({})'.format( kb_string, string['name']['low']['keyboard']).format(C=round(kph, 2)) Message(string['option']['generate']) default_option_text = ' '.join( str(i + 1) for i, v in enumerate(default_options) if v) Message(string['option']['select'].format(V=default_option_text)) Message('1: {}'.format(string['name']['track'])) Message('2: {}'.format(string['name']['speed'])) Message('3: {}'.format(string['name']['click'])) Message('4: {}'.format(kb_string)) Message('5: {}'.format(string['name']['csv'])) selection = list(map(int, input().split())) result = value_select(selection, default_options, start=1) if result[0]: generate_tracks = True if result[1]: generate_speed = True if result[2]: generate_heatmap = True Message('Which mouse buttons should be included in the heatmap?.') default_options = [ CONFIG['GenerateHeatmap']['_MouseButtonLeft'], CONFIG['GenerateHeatmap']['_MouseButtonMiddle'], CONFIG['GenerateHeatmap']['_MouseButtonRight'] ] default_option_text = ' '.join( str(i + 1) for i, v in enumerate(default_options) if v) Message(string['option']['select'].format(V=default_option_text)) Message('1: {}'.format(word['mousebutton']['left'])) Message('2: {}'.format(word['mousebutton']['middle'])) Message('3: {}'.format(word['mousebutton']['right'])) selection = list(map(int, input().split())) heatmap_buttons = value_select(selection, default_options, start=1) CONFIG['GenerateHeatmap']['_MouseButtonLeft'] = heatmap_buttons[0] CONFIG['GenerateHeatmap']['_MouseButtonMiddle'] = heatmap_buttons[1] CONFIG['GenerateHeatmap']['_MouseButtonRight'] = heatmap_buttons[2] if not any(heatmap_buttons): generate_heatmap = False if result[3]: generate_keyboard = True if result[4]: generate_csv = True if any((generate_tracks, generate_speed, generate_heatmap, generate_keyboard, generate_csv)): last_session_start = r.data['Ticks']['Session']['Total'] last_session_end = r.data['Ticks']['Total'] all_time = ticks_to_seconds(last_session_end, UPDATES_PER_SECOND) last_session_time = ticks_to_seconds( last_session_end - last_session_start, UPDATES_PER_SECOND) csv_only = generate_csv and not any( (generate_tracks, generate_heatmap, generate_keyboard)) if not last_session_time or last_session_time == all_time or csv_only: last_session = False else: while True: Message(string['option']['session']['select']) Message('1: {} [{}]'.format( string['option']['session']['all'].format(T=all_time), word['default'])) Message('2: {}'.format( string['option']['session']['last'].format( T=last_session_time))) selection = list(map(int, input().split())) result = value_select(selection, [True, False], start=1) if result[0] and result[1]: Message(string['option']['error']['single']) elif result[1]: last_session = True break else: last_session = False break #Generate if generate_tracks: r.tracks(last_session) if generate_speed: r.speed(last_session) if generate_heatmap: r.clicks(last_session) if generate_keyboard: r.keyboard(last_session) if generate_csv: r.csv() if CONFIG['GenerateImages']['OpenOnFinish']: Message(string['option']['open']) open_folder(r.name.generate()) else: Message(string['option']['error']['nothing'])
from __future__ import absolute_import from core.compatibility import Message from core.os import WindowFocus from core.track import RefreshRateLimiter while True: with RefreshRateLimiter(10): Message(WindowFocus())