diff --git a/.gitignore b/.gitignore index b6e4761..d3223aa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,6 @@ __pycache__/ *.py[cod] *$py.class -# C extensions -*.so - # Distribution / packaging .Python build/ @@ -127,3 +124,13 @@ dmypy.json # Pyre type checker .pyre/ + +# Vizdoom +_vizdoom* + +# A2L +data/* + +# Misc +.vscode* + diff --git a/README.md b/README.md index c1fea3d..21459e1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,130 @@ # A2L - Active Affordance Learning -Code coming soon, estimated release in March 2020. -Please feel free to reach out via email if you have any questions in the meantime. \ No newline at end of file +#### Published at ICLR 2020 [[OpenReview]](https://openreview.net/forum?id=BJgMFxrYPB) [[Video]](https://iclr.cc/virtual/poster_BJgMFxrYPB.html) [[PDF]](https://arxiv.org/pdf/2001.02364.pdf) + + + +This repo provides a reference implementation for active affordance learning, which can be employed to improve autonomous navigation performance in hazardous environments (demonstrated here using the VizDoom simulator). The repo also contains a variety of convenient utilities that can be re-used in other VizDoom-based projects to improve quality of life. + +## Setup + +This code has been tested on Ubuntu 16.04. + +### Requirements + +1. python >= 3.5 +2. keras >= 2.2.0 +3. [opencv-python](https://pypi.org/project/opencv-python/) >= 3.4.0 + +### Installing Dependencies + +1. Install [VizDoom](https://github.com/mwydmuch/ViZDoom) simulator into local Python environment. + + ``` bash + # Install ZDoom dependencies + sudo apt-get install build-essential zlib1g-dev libsdl2-dev libjpeg-dev \ + nasm tar libbz2-dev libgtk2.0-dev cmake git libfluidsynth-dev libgme-dev \ + libopenal-dev timidity libwildmidi-dev unzip + + # Install Boost libraries + sudo apt-get install libboost-all-dev + + # Install Python 3 dependencies + sudo apt-get install python3-dev python3-pip + pip3 install numpy + ``` + +2. Install Keras-based [segmentation-models](https://github.com/qubvel/segmentation_models) library. + + ``` bash + pip3 install segmentation-models + ``` + +### Downloading Demo Data + +In order to download the demo data (which contains train/test maps, pre-computed train beacons, and a pre-trained A2L model), follow the steps below: + +1. Download the demo data into the root of the repo and uncompress using the following commands: + + ``` bash + wget https://www.dropbox.com/s/0hn71njit81xiy7/demo_data.tar.gz + tar -xvzf demo_data.tar.gz + ``` + +2. Check that the directory structure looks like the following: + + ```bash + ├── data + │ ├── beacons + │ ├── configs + │ ├── experiments + │ ├── maps + │ ├── samples + │ └── models + ``` + +## Usage + +Each executable script included in this repo is prefixed with *run* in its file name. For a detailed description of all possible configuration arguments, please run with the ```-h``` flag. + +### Generating Partially-Labeled Self-Supervised Samples + +The following sequence of commands are used to generate a configurable number of partially-labeled examples of navigability in a self-supervised manner. This should work with any set of maps that are compatible with VizDoom. + +1. Generate a set of beacons for each map - which describe valid spawn points from which the agent can start a sampling episode. + + ``` bash + python preprocess/run_beacon_generation.py --wad-dir ../data/maps/train/ --save-dir ../data/beacons/train/ + ``` + +2. Generate a configurable number of self-supervised examples per map. + + ``` bash + python train/run_data_sampling.py --wad-dir ../data/maps/train/ --beacon-dir ../data/beacons/train/ --save-dir ../data/samples/train/ --samples-per-map 500 + ``` + +### Training Affordance Segmentation Models + +The following command is used to train a ResNet-18-based UNet segmentation model to predict pixel-wise navigability. + +1. Train UNet-based segmentation model. + + ``` bash + python train/run_train_model.py --data-dir ../data/samples/train/ --save-dir ../data/models/ --epochs 50 --batch-size 40 + ``` + +### Training Segmentation Models with Active Affordance Learning + +The following command is used to train a ResNet-18-based UNet segmentation model using active affordance learning. The script alternates between data generation and model training, using trained seed models to actively seek out difficult examples. + +1. Train UNet-based segmentation model using active affordance learning. + + ``` bash + python train/run_train_A2L.py --wad-dir ../data/maps/train --beacon-dir ../data/beacons/train --save-dir ../data/models/active --active-iterations 5 --samples-per-map 500 --epochs 50 --batch-size 40 + ``` + +### Evaluating Navigation Performance + +The following command is used to evaluate the performance of an affordance-based agent in a goal-directed navigation task. It can also be used to evaluate a geometry-based agent by dropping the ```--model-path``` argument. + +1. Run navigation experiments specified using JSON file. + ``` bash + python eval/run_eval_navigation.py --wad-dir ../data/maps/test --model-path ../data/models/seg_model.h5 --experiment-path ../data/experiments/navigation/demo.json --iterations 5 + ``` + +## Citing + +If you've found this code to be useful, please consider citing our paper! + +``` latex +@inproceedings{qi2020learning, + title={Learning to Move with Affordance Maps}, + author={Qi, William and Mullapudi, Ravi Teja and Gupta, Saurabh and Ramanan, Deva}, + booktitle={International Conference on Learning Representations (ICLR)}, + year={2020} +} +``` + +## Questions + +If you have additional questions/concerns, please feel free to reach out to wq@cs.cmu.edu. diff --git a/docs/img/architecture.png b/docs/img/architecture.png new file mode 100644 index 0000000..9f74c0a Binary files /dev/null and b/docs/img/architecture.png differ diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/__init__.py b/src/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/astar.so b/src/common/astar.so new file mode 100755 index 0000000..eb20948 Binary files /dev/null and b/src/common/astar.so differ diff --git a/src/common/custom_game.py b/src/common/custom_game.py new file mode 100644 index 0000000..dc6517a --- /dev/null +++ b/src/common/custom_game.py @@ -0,0 +1,48 @@ +import numpy as np +import vizdoom as vzd + + +class CustomGame(vzd.DoomGame): + def __init__(self, localization_noise=0, pose_noise=0): + super().__init__() + self.localization_noise_sd = localization_noise + self.pose_noise_sd = pose_noise + self.last_true_location = None + self.last_noisy_location = None + + def new_episode(self): + super().new_episode() + self.last_true_location = None + self.last_noisy_location = None + + def get_agent_location(self): + # Get true location of agent + true_x = self.get_game_variable(vzd.GameVariable.POSITION_X) + true_y = self.get_game_variable(vzd.GameVariable.POSITION_Y) + true_angle = self.get_game_variable(vzd.GameVariable.ANGLE) + + # Return true location if first time called + if self.last_true_location is None: + self.last_true_location = (true_x, true_y, true_angle) + self.last_noisy_location = (true_x, true_y, true_angle) + return true_x, true_y, true_angle + + # Get change in location and angle + (last_true_x, last_true_y, last_true_angle) = self.last_true_location + diff_x = true_x - last_true_x + diff_y = true_y - last_true_y + diff_angle = true_angle - last_true_angle + + # Generate localization noise for agent position + noise_localization = np.random.normal(1, self.localization_noise_sd) + noise_pose = np.random.normal(1, self.pose_noise_sd) + + # Simulate agent position obtained by simulated noisy sensor + (last_x, last_y, last_angle) = self.last_noisy_location + agent_x = last_x + (noise_localization * diff_x) + agent_y = last_y + (noise_localization * diff_y) + agent_angle = last_angle + (noise_pose * diff_angle) + + self.last_true_location = (true_x, true_y, true_angle) + self.last_noisy_location = (agent_x, agent_y, agent_angle) + return agent_x, agent_y, agent_angle diff --git a/src/common/pyastar.py b/src/common/pyastar.py new file mode 100644 index 0000000..70abd01 --- /dev/null +++ b/src/common/pyastar.py @@ -0,0 +1,60 @@ +import ctypes +import numpy as np + +import inspect +from os.path import abspath, dirname, join + +fname = abspath(inspect.getfile(inspect.currentframe())) +lib = ctypes.cdll.LoadLibrary(join(dirname(fname), 'astar.so')) + +astar = lib.astar +ndmat_f_type = np.ctypeslib.ndpointer( + dtype=np.float32, ndim=1, flags='C_CONTIGUOUS') +ndmat_i_type = np.ctypeslib.ndpointer( + dtype=np.int32, ndim=1, flags='C_CONTIGUOUS') +astar.restype = ctypes.c_bool +astar.argtypes = [ndmat_f_type, ctypes.c_int, ctypes.c_int, + ctypes.c_int, ctypes.c_int, ctypes.c_bool, + ndmat_i_type] + + +def astar_path(weights, start, goal, allow_diagonal=False): + # For the heuristic to be valid, each move must cost at least 1. + if weights.min(axis=None) < 1.: + raise ValueError('Minimum cost to move must be 1, but got %f' % ( + weights.min(axis=None))) + # Ensure start is within bounds. + if (start[0] < 0 or start[0] >= weights.shape[0] or + start[1] < 0 or start[1] >= weights.shape[1]): + raise ValueError('Start of (%d, %d) lies outside grid.' % (start)) + # Ensure goal is within bounds. + if (goal[0] < 0 or goal[0] >= weights.shape[0] or + goal[1] < 0 or goal[1] >= weights.shape[1]): + raise ValueError('Goal of (%d, %d) lies outside grid.' % (goal)) + + height, width = weights.shape + start_idx = np.ravel_multi_index(start, (height, width)) + goal_idx = np.ravel_multi_index(goal, (height, width)) + + # The C++ code writes the solution to the paths array + paths = np.full(height * width, -1, dtype=np.int32) + success = astar( + weights.flatten(), height, width, start_idx, goal_idx, allow_diagonal, + paths # output parameter + ) + if not success: + return np.array([]) + + coordinates = [] + path_idx = goal_idx + while path_idx != start_idx: + pi, pj = np.unravel_index(path_idx, (height, width)) + coordinates.append((pi, pj)) + + path_idx = paths[path_idx] + + if coordinates: + coordinates.append(np.unravel_index(start_idx, (height, width))) + return np.vstack(coordinates[::-1]) + else: + return np.array([]) diff --git a/src/common/util.py b/src/common/util.py new file mode 100644 index 0000000..ca606e2 --- /dev/null +++ b/src/common/util.py @@ -0,0 +1,260 @@ +import cv2 +import math +import numpy as np +import os +import pickle +import vizdoom as vzd + +from os import listdir +from os.path import isfile, join +from vizdoom import ScreenResolution + +from common.custom_game import CustomGame + +''' +Collection of utility functions that are employed throughout the A2L repo. +''' + + +def get_sorted_wad_ids(wad_dir): + ''' + Get sorted list of WAD ids contained within specified directory. + ''' + all_files = [f for f in listdir(wad_dir) if isfile(join(wad_dir, f))] + wad_files = [f for f in all_files if f.endswith('wad')] + wad_ids = [int(f.split('.wad')[0]) for f in wad_files] + wad_ids.sort() + + return wad_ids + + +def setup_game(wad_dir, wad_id, visible=False, localization_noise=0, pose_noise=0): + ''' + Set up custom VizDoom game with specified configuration. + ''' + # Set up VizDoom Game + game = CustomGame(localization_noise, pose_noise) + game.load_config('../data/configs/delta.cfg') + game.set_screen_resolution(ScreenResolution.RES_320X240) + game.set_render_weapon(False) + game.set_window_visible(visible) + + game.set_button_max_value(vzd.Button.TURN_LEFT_RIGHT_DELTA, 10) + + # Load generated map from WAD + wad_path = os.path.join(wad_dir, '{}.wad'.format(wad_id)) # NOQA + game.set_doom_scenario_path(wad_path) + game.init() + return game + + +def compute_map_state(state, height=240, width=320, map_size=65, map_scale=8, + fov=90.0): + ''' + Get local geometric map from current game state. + ''' + # Extract agent state from game + depth_buffer = state.depth_buffer + + # Initialize maps + map_size = map_size - 1 + canvas_size = 2*map_size + 1 + vis_map = np.zeros((canvas_size, canvas_size, 3), dtype=np.uint8) + simple_map = np.zeros((canvas_size, canvas_size), dtype=np.uint8) + + # Ray cast from eye line to project depth map into 2D ray points + offset = 225 + game_unit = 100.0/14 + ray_cast = (depth_buffer[height//2] * game_unit)/float(map_scale) + ray_points = [(map_size, map_size)] + for i in range(10, canvas_size-10): + d = ray_cast[int(round(float(width)/canvas_size * i - 1))] + theta = (float(i)/canvas_size * fov) + + ray_y = int(round(d * math.sin(math.radians(offset - theta)))) + map_size + ray_x = int(round(d * math.cos(math.radians(offset - theta)))) + map_size + + _, _, p = cv2.clipLine((0, 0, canvas_size, canvas_size), + (map_size, map_size), (ray_y, ray_x)) + ray_points.append(p) + + # Label known free space on 2D map with value 2 + cv2.fillPoly(vis_map, np.array([ray_points], dtype=np.int32), (255, 255, 255)) # NOQA + cv2.fillPoly(simple_map, np.array([ray_points], dtype=np.int32), 2) + + # Label known obstacles on 2D map with value 1 + for point in ray_points[1:]: + if point[1] > 0: + cond1 = point[0] + 1 < canvas_size + cond2 = point[1] + 1 < canvas_size + cond3 = point[0] - 1 > 0 + cond4 = point[1] - 1 > 0 + + simple_map[point[1], point[0]] = 1 + if cond1: + simple_map[point[1], point[0] + 1] = 1 + if cond2: + simple_map[point[1] + 1, point[0]] = 1 + if cond3: + simple_map[point[1], point[0] - 1] = 1 + if cond4: + simple_map[point[1] - 1, point[0]] = 1 + if cond1 and cond2: + simple_map[point[1] + 1, point[0] + 1] = 1 + if cond3 and cond4: + simple_map[point[1] - 1, point[0] - 1] = 1 + if cond2 and cond3: + simple_map[point[1] + 1, point[0] - 1] = 1 + if cond1 and cond4: + simple_map[point[1] - 1, point[0] + 1] = 1 + + # Crop generated maps + vis_map = vis_map[:map_size + 1, map_size - 32:map_size + 32 + 1, :] + simple_map = simple_map[:map_size + 1, map_size - 32:map_size + 32 + 1] + # plt.imshow(simple_map) + # plt.show() + return vis_map, simple_map + + +def compute_map(game, height=240, width=320, map_size=65, map_scale=8, + fov=90.0): + ''' + (Convenience Function) Get local geometric map from current game. + ''' + state = game.get_state() + vis_map, simple_map = compute_map_state(state, height, width, map_size, map_scale, fov) + return vis_map, simple_map + + +def get_valid_locations(beacon_dir, wad_id): + ''' + Get list of valid spawn locations (in open space) from directory of saved beacons. + ''' + node_path = os.path.join(beacon_dir, '{}.pkl'.format(wad_id)) + with open(node_path, 'rb') as handle: + nodes = pickle.load(handle) + return list(nodes.keys()) + + +def abs_to_map_pos(game, abs_pos, map_size=64, map_scale=8): + ''' + Convert absolute coordinate to local map coordinate. + ''' + player_x, player_y, player_angle = game.get_agent_location() + rel_x = -abs_pos[0] + player_x + rel_y = -abs_pos[1] + player_y + + rotated_x = math.cos(math.radians(-player_angle)) * rel_x - math.sin(math.radians(-player_angle)) * rel_y # NOQA + rotated_y = math.sin(math.radians(-player_angle)) * rel_x + math.cos(math.radians(-player_angle)) * rel_y # NOQA + rotated_x = int(round(rotated_x/map_scale + map_size)) + rotated_y = int(round(rotated_y/map_scale + map_size)) - 32 + return (rotated_x, rotated_y) + + +def abs_to_global_map_pos(game, abs_pos, origin, global_map_size=1024, map_scale=8): + ''' + Convert absolute coordinate to global map coordinate. + ''' + global_x = int(round((origin[1] - abs_pos[1])/map_scale + global_map_size/2)) + global_y = int(round((abs_pos[0] - origin[0])/map_scale + global_map_size/2)) + return (global_x, global_y) + + +def global_map_to_abs_pos(game, map_pos, origin, global_map_size=1024, map_scale=8): + ''' + Convert global map coordinate to absolute coordinate. + ''' + abs_x = int(round((map_pos[1] - global_map_size/2) * map_scale + origin[0])) + abs_y = int(round(((map_pos[0] - global_map_size/2) * map_scale - origin[1]) * -1)) + return(abs_x, abs_y) + + +def map_to_abs_pos(game, map_pos, map_size=64, map_scale=8): + ''' + Convert local map coordinate to absolute coordinate. + ''' + player_x, player_y, player_angle = game.get_agent_location() + + rotated_x = (map_pos[0] - map_size) * map_scale + rotated_y = (map_pos[1] + 32 - map_size) * map_scale + rel_x = math.cos(math.radians(player_angle)) * rotated_x - \ + math.sin(math.radians(player_angle)) * rotated_y + rel_y = math.sin(math.radians(player_angle)) * rotated_x + \ + math.cos(math.radians(player_angle)) * rotated_y + abs_pos_x = int(np.around(player_x - rel_x)) + abs_pos_y = int(np.around(player_y - rel_y)) + return (abs_pos_x, abs_pos_y) + + +def dist_to(game, point): + ''' + Compute current distance from agent to specified point. + ''' + player_x, player_y, _ = game.get_agent_location() + + diff = np.zeros(2) + diff[0] = np.abs(point[0] - player_x) + diff[1] = np.abs(point[1] - player_y) + dist = np.linalg.norm(diff) + + return dist + + +def check_monster_collision(game): + ''' + Check if agent is in collision state with monster. + ''' + state = game.get_state() + + if state is None: + return False + + player_x = game.get_game_variable(vzd.GameVariable.POSITION_X) + player_y = game.get_game_variable(vzd.GameVariable.POSITION_Y) + for l in state.labels: + if l.object_name == 'Zombieman': + mon_rel_x = l.object_position_x - player_x + mon_rel_y = l.object_position_y - player_y + if np.linalg.norm([mon_rel_x, mon_rel_y]) < 50: + return True + return False + + +def get_angle_diff(player_angle, sample_angle): + ''' + Compute smallest difference between two specified angles. + ''' + angle_diff = sample_angle - player_angle + if angle_diff < 0: + counter_rotate = abs(angle_diff) > abs(angle_diff + 360) + smallest_diff = angle_diff + 360 if counter_rotate else angle_diff + else: + counter_rotate = abs(angle_diff) > abs(angle_diff - 360) + smallest_diff = angle_diff - 360 if counter_rotate else angle_diff + return smallest_diff + + +def check_damage(game): + ''' + Check if agent has taken damage in last timestep. + Health is reset to max after every check to make sure agent doesn't die. + ''' + took_damage = False + health = game.get_game_variable(vzd.GameVariable.HEALTH) + + if health < 85: + took_damage = True + game.send_game_command("give health") + return took_damage + + +def setup_trial(game, start_pos): + ''' + Project segmented first-person view into top-down 2D space. + ''' + game.new_episode() + game.send_game_command('warp {} {}'.format(start_pos[0], start_pos[1])) + + # Idle for several actions to ensure warp completes + for i in range(2): + game.make_action([0, 0]) diff --git a/src/common/visible.npy b/src/common/visible.npy new file mode 100644 index 0000000..da2a7ea Binary files /dev/null and b/src/common/visible.npy differ diff --git a/src/eval/__init__.py b/src/eval/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/eval/run_eval_navigation.py b/src/eval/run_eval_navigation.py new file mode 100644 index 0000000..01a8229 --- /dev/null +++ b/src/eval/run_eval_navigation.py @@ -0,0 +1,108 @@ +import argparse +import json +import keras.backend as K +import os + +from collections import defaultdict +from keras.utils import multi_gpu_model +from os.path import join +from segmentation_models import Unet + +import common.util as util +from modules.navigation import navigate + +''' +This script is used to evaluate navigation performance using a geometric baseline or +affordance-based approach. Experiments are specified using JSON files and use of +affordance maps is toggled via the --model-path option. Results are dumped to disk +in JSON format. + +Configuration is performed via command line arguments specified below. +''' + + +def parse_arguments(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Paths + parser.add_argument('--wad-dir', type=str, default='../data/maps/test', + help='Path to dir containing map files') + parser.add_argument('--model-path', type=str, + help='Path to model if affordance-based navigation is employed') + parser.add_argument('--save-dir', type=str, + help='Path to dir where results and vids should be saved') + parser.add_argument('--experiment-path', type=str, + default='../data/experiments/navigation/demo.json', + help='Path to file containing experimental setup') + + # Navigation Options + parser.add_argument('--iterations', type=int, default=5, + help='Number of times to repeat navigation experiment') + parser.add_argument('--localization-noise', type=float, default=0.0) + parser.add_argument('--max-steps', type=int, default=1000, + help='Max number of steps for navigation trial') + + # Config Options + parser.add_argument('--multi-gpu-model', action='store_true', + help='Specifies whether multi-GPU Keras model should be used') + parser.add_argument('--save-vid', action='store_true', + help='Specifies whether videos should be saved from every run') + + # Visualization Options + parser.add_argument('--viz-output', action='store_true', + help='Specifies whether output should be shown for debugging') + + args = parser.parse_args() + return args + + +def run_navigation_trials(args): + # Load model if specified + if args.model_path: + K.set_learning_phase(1) + model = Unet('resnet18', input_shape=(256, 320, 4), activation='sigmoid', classes=1, + encoder_weights=None) + if args.multi_gpu_model: + model = multi_gpu_model(model) + + model.load_weights(args.model_path) + else: + model = None + + # Create save directories for results and vids + if args.save_dir: + os.makedirs(args.save_dir, exist_ok=True) + if args.save_vid: + vid_dir = join(args.save_dir, 'vids') + os.makedirs(vid_dir, exist_ok=True) + + # Read saved experimental setup from disk + with open(args.experiment_path, 'r') as fp: + experiment_dict = json.load(fp) + + # Execute experiments for all maps + result_dict = defaultdict(list) + for wad_id, experiments in sorted(experiment_dict.items()): + print('INFO: Testing on map {}'.format(wad_id)) + game = util.setup_game(args.wad_dir, wad_id, visible=args.viz_output, + localization_noise=args.localization_noise) + + # Execute individual experiments within map + for exp_idx, experiment in enumerate(experiments): + for i in range(args.iterations): + util.setup_trial(game, experiment['start']) + vid_path = join(vid_dir, '{}_{}_{}.mp4'.format(wad_id, exp_idx, i)) if args.save_vid else None # NOQA + result, full_path = navigate(game, args.max_steps, experiment['goal'], model, vid_path) + result_dict[wad_id].append(result) + print('INFO: Trial complete {}'.format(result)) + + # Save results from experiment + if args.save_dir: + result_path = join(args.save_dir, 'results.json') + with open(result_path, 'w') as fp: + json.dump(result_dict, fp, sort_keys=True, indent=4) + + +if __name__ == "__main__": + args = parse_arguments() + run_navigation_trials(args) diff --git a/src/eval/run_human_navigation.py b/src/eval/run_human_navigation.py new file mode 100644 index 0000000..70c9c67 --- /dev/null +++ b/src/eval/run_human_navigation.py @@ -0,0 +1,178 @@ +import argparse +import cv2 +import json +import math +import numpy as np +import os +import time +import vizdoom as vzd + +from collections import defaultdict +from os.path import join + +import common.util as util +from common.custom_game import CustomGame + + +''' +This script is used to evaluate human navigation performance. Participants are asked to navigate +to a relative goal shown in an ego-centric map. Experiments are specified and results are dumped +to disk in JSON format. + +Configuration is performed via command line arguments specified below. +''' + + +def parse_arguments(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Paths + parser.add_argument('--wad-dir', type=str, default='../data/maps/test', + help='Path to dir containing map files') + parser.add_argument('--save-dir', type=str, + help='Path to dir where results and vids should be saved') + parser.add_argument('--experiment-path', type=str, + default='../data/experiments/navigation/demo.json', + help='Path to file containing experimental setup') + + # Navigation Options + parser.add_argument('--max-steps', type=int, default=1000, + help='Max number of steps for navigation trial') + + # Config Options + parser.add_argument('--save-vid', action='store_true', + help='Specifies whether videos should be saved from every run') + + args = parser.parse_args() + return args + + +def setup_player_game(wad_dir, wad_id): + # Set up VizDoom Game + game = CustomGame() + game.load_config('../data/configs/default.cfg') + game.set_screen_resolution(vzd.ScreenResolution.RES_640X480) + game.set_render_weapon(False) + game.set_window_visible(True) + game.add_game_args("+freelook 1") + game.set_mode(vzd.Mode.SPECTATOR) + + # Load generated map from WAD + wad_path = join(wad_dir, '{}.wad'.format(wad_id)) # NOQA + game.set_doom_scenario_path(wad_path) + game.init() + return game + + +def build_goal_map(game, final_goal): + # Build visualization for map of relative goal position + goal_map = np.zeros((101, 101, 3), np.uint8) + cv2.circle(goal_map, (50, 50), 1, (0, 255, 0), -1) + + player_x, player_y, player_angle = game.get_agent_location() + rel_x = -final_goal[0] + player_x + rel_y = -final_goal[1] + player_y + + rotated_x = math.cos(math.radians(-player_angle)) * rel_x - math.sin(math.radians(-player_angle)) * rel_y # NOQA + rotated_y = math.sin(math.radians(-player_angle)) * rel_x + math.cos(math.radians(-player_angle)) * rel_y # NOQA + rotated_x = int(round(rotated_x/25) + 50) + rotated_y = int(round(rotated_y/25) + 50) + rotated_x = min(100, max(0, rotated_x)) + rotated_y = min(100, max(0, rotated_y)) + cv2.circle(goal_map, (rotated_y, rotated_x), 2, (0, 0, 255), -1) + + return goal_map + + +def human_navigate(game, max_steps, final_goal, vid_path=None): + # Set up video writer + vid_out = None + if vid_path is not None: + vid_out = cv2.VideoWriter(vid_path, + cv2.VideoWriter_fourcc(*'mp4v'), + vzd.DEFAULT_TICRATE // 2, (640, 480)) + cv2.namedWindow('Map', cv2.WINDOW_NORMAL) + cv2.resizeWindow('Map', 500, 500) + + # Initialize variables + total_damage = 0 + total_steps = 0 + + while not game.is_episode_finished() and total_steps < args.max_steps: + # Get current state and buffer + state = game.get_state() + rgb_frame = np.rollaxis(state.screen_buffer, 0, 3) + + # Show goal relative to agent + goal_map = build_goal_map(game, final_goal) + cv2.imshow('Map', goal_map) + cv2.waitKey(1) + + # Advance action + game.advance_action() + + # Allow human time before episode starts + if total_steps == 0: + time.sleep(2) + + # Check for damage + cur_damage = 100 - game.get_game_variable(vzd.HEALTH) + if cur_damage > 15: + total_damage += 20 + print('Total Damage: {}'.format(total_damage)) + if util.check_monster_collision(game): + total_damage += 4 + print('Total Damage: {}'.format(total_damage)) + game.send_game_command("give health") + + # Check if goal is reached + dist_to_goal = util.dist_to(game, final_goal) + if dist_to_goal < 40: + print('Goal reached') + return {'success': 1, 'steps': total_steps, 'damage': total_damage} + + # Write to video if specified + if vid_out: + vis_buffer = np.zeros((480, 640, 3), dtype=np.uint8) + rgb_frame = cv2.cvtColor(rgb_frame, cv2.COLOR_BGR2RGB) + vis_buffer[:480, :640, :] = rgb_frame + vid_out.write(vis_buffer) + + total_steps += 1 + + return {'success': 0, 'steps': total_steps, 'damage': total_damage} + + +def run_navigation_trials(args): + # Create save directories for results and vids + if args.save_dir: + os.makedirs(args.save_dir, exist_ok=True) + if args.save_vid: + vid_dir = join(args.save_dir, 'vids') + os.makedirs(vid_dir, exist_ok=True) + + # Read saved experimental setup from disk + with open(args.experiment_path, 'r') as fp: + experiment_dict = json.load(fp) + + # Execute experiments for specified number of times + result_dict = defaultdict(list) + for wad_id, experiments in sorted(experiment_dict.items()): + game = setup_player_game(args.wad_dir, wad_id) + for idx, experiment in enumerate(experiments): + vid_path = join(vid_dir, '{}_{}.mp4'.format(wad_id, idx)) if args.save_vid else None + util.setup_trial(game, experiment['start']) + result = human_navigate(game, args.max_steps, experiment['goal'], vid_path) + result_dict[wad_id].append(result) + game.close() + + # Save results from experiment + if args.save_dir: + result_path = join(args.save_dir, 'results.json') + with open(result_path, 'w') as fp: + json.dump(result_dict, fp, sort_keys=True, indent=4) + + +if __name__ == "__main__": + args = parse_arguments() + run_navigation_trials(args) diff --git a/src/modules/__init__.py b/src/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/affordance.py b/src/modules/affordance.py new file mode 100644 index 0000000..a9b3a9a --- /dev/null +++ b/src/modules/affordance.py @@ -0,0 +1,113 @@ +import cv2 +import math +import numpy as np + + +def segment_view(model, rgbd_frame, threshold=0.5): + ''' + Use trained segmentation model to predict pixel-wise navigability. + ''' + input = np.zeros((1, 256, 320, 4)) + input[0, 8:-8, :, :] = rgbd_frame + + conf_map = model.predict(input)[0, 8:-8, :, :] + conf_map[:120, :, :] = 0 + + seg_map = (conf_map > threshold).astype(np.uint8) + return seg_map, conf_map + + +def project_seg_map(rgbd_frame, seg_map): + ''' + Project segmented first-person view of navigability into top-down 2D space. + ''' + width = 320.0 + map_size = 64 + canvas_size = 2*map_size + 1 + map_scale = 8 + offset = 315 + fov = 90.0 + game_unit = 100.0/14 + + # Mask seg map edges + masked_seg_map = np.zeros_like(seg_map) + masked_seg_map[:, 80:240, 0] = seg_map[:, 80:240, 0] + seg_map = masked_seg_map + + proj_map = np.zeros((canvas_size, canvas_size), dtype=np.uint8) + mon_pixels = np.nonzero(seg_map) + for i in range(len(mon_pixels[0])): + map_x = mon_pixels[1][i] + map_y = mon_pixels[0][i] + + map_depth = rgbd_frame[map_y, map_x, -1] + d = (map_depth * game_unit) / float(map_scale) + theta = ((float(map_x) / width) * fov) + ray_y = int(round(d * math.sin(math.radians(offset - theta)))) + ray_x = -int(round(d * math.cos(math.radians(offset - theta)))) + proj_y = ray_y + map_size + proj_x = ray_x + map_size + if proj_y >= 0 and proj_x >= 0 and proj_y < canvas_size and proj_x < canvas_size: + cv2.circle(proj_map, (proj_x, proj_y), 1, 1, thickness=-1) + + proj_map = proj_map[:map_size + 1, map_size - 32:map_size + 32 + 1] + return proj_map + + +def project_conf_map(rgbd_frame, conf_map, log_scale=True): + ''' + Project segmented first-person view of confidence values into top-down 2D space. + ''' + # Set hyper-parameters + width = 320.0 + map_size = 64 + canvas_size = 2*map_size + 1 + map_scale = 8 + offset = 315 + fov = 90.0 + game_unit = 100.0/14 + + # Mask confidence map edges + masked_conf_map = np.zeros_like(conf_map) + masked_conf_map[:, 80:240, 0] = conf_map[:, 80:240, 0] + conf_map = masked_conf_map + + # Initialize projected map + proj_map = np.zeros((canvas_size, canvas_size)) + count_map = np.zeros((canvas_size, canvas_size)) + proj_pixels = np.where(conf_map > 1e-8) + + # Project non-zero points into 2D space + for i in range(len(proj_pixels[0])): + map_x = proj_pixels[1][i] + map_y = proj_pixels[0][i] + + map_depth = rgbd_frame[map_y, map_x, -1] + d = (map_depth * game_unit) / float(map_scale) + theta = ((float(map_x) / width) * fov) + ray_y = int(round(d * math.sin(math.radians(offset - theta)))) + ray_x = -int(round(d * math.cos(math.radians(offset - theta)))) + proj_y = ray_y + map_size + proj_x = ray_x + map_size + + # Ignore invalid projected coordinates + if proj_y < 0 or proj_x < 0: + continue + + if log_scale: + proj_map[proj_y, proj_x] += np.log10(conf_map[map_y, map_x]) + else: + proj_map[proj_y, proj_x] += conf_map[map_y, map_x] + count_map[proj_y, proj_x] += 1 + + # Build map of valid cells for which we have affordance information + valid_map = np.copy(count_map)[:map_size + 1, map_size - 32:map_size + 32 + 1] + valid_map[np.where(valid_map > 1)] = 1 + + # Normalize projection values in cells with multiple counts + count_map[np.where(count_map == 0)] = 1 + proj_map = proj_map / count_map + proj_map = proj_map[:map_size + 1, map_size - 32:map_size + 32 + 1] + + # proj_map = gaussian_filter(proj_map, sigma=0.5) + return proj_map, valid_map diff --git a/src/modules/locomotion.py b/src/modules/locomotion.py new file mode 100644 index 0000000..b0bf90b --- /dev/null +++ b/src/modules/locomotion.py @@ -0,0 +1,44 @@ +import math +import random +import vizdoom as vzd + +import common.util as util + + +def navigate_beeline(game, goal): + ''' + Executes action to move agent towards goal, following beeline navigation policy. + ''' + player_x, player_y, player_angle = game.get_agent_location() + + goal_angle_rad = math.atan2(goal[1] - player_y, goal[0] - player_x) + goal_angle = math.degrees(goal_angle_rad) + if goal_angle < 0: + goal_angle = 360 + goal_angle + + if abs(goal_angle - player_angle) > 15: + angle_diff = goal_angle - player_angle + if angle_diff > 180: + angle_diff -= 360 + elif angle_diff <= -180: + angle_diff += 360 + action = [-angle_diff, 0] + else: + action = [0, 15] + + game.make_action(action) + + +def rotate_random_angle(game): + ''' + Executes actions to rotate agent to random angle + ''' + sample_angle = random.randint(0, 359) + player_angle = game.get_game_variable(vzd.GameVariable.ANGLE) + smallest_diff = util.get_angle_diff(player_angle, sample_angle) + + # Rotate agent to random angle + while abs(smallest_diff) > 5: + game.make_action([-smallest_diff, 0]) + player_angle = game.get_game_variable(vzd.GameVariable.ANGLE) + smallest_diff = util.get_angle_diff(player_angle, sample_angle) diff --git a/src/modules/navigation.py b/src/modules/navigation.py new file mode 100644 index 0000000..48b9878 --- /dev/null +++ b/src/modules/navigation.py @@ -0,0 +1,223 @@ +import cv2 +import numpy as np +import vizdoom as vzd + +import common.util as util +import modules.affordance as affordance +import modules.locomotion as locomotion +import modules.planning as planning + + +def visualize(game, merged_map, cur_goal, abs_path, show=False): + ''' + Visualize local scene representation. + ''' + map_rgb = cv2.cvtColor(merged_map * 127, cv2.COLOR_GRAY2RGB) + + if abs_path: + for waypoint in abs_path: + map_point = util.abs_to_map_pos(game, waypoint) + cv2.circle(map_rgb, (map_point[1], map_point[0]), 0, (255, 0, 0), thickness=-1) + + if cur_goal: + map_goal = util.abs_to_map_pos(game, cur_goal) + cv2.circle(map_rgb, (map_goal[1], map_goal[0]), 2, (0, 0, 255), thickness=-1) + + map_rgb = cv2.resize(map_rgb, dsize=(240, 240)) + + if show: + cv2.imshow('Projected Map', map_rgb) + cv2.waitKey(0) + + return map_rgb + + +def visualize_global(game, global_map, global_map_size, origin, cur_goal, abs_path): + ''' + Visualize global scene representation. + ''' + map_rgb = cv2.cvtColor(global_map.astype(np.uint8) * 127, cv2.COLOR_GRAY2RGB) + + if abs_path: + for waypoint in abs_path: + map_point = util.abs_to_global_map_pos(game, waypoint, origin, global_map_size) + cv2.circle(map_rgb, (map_point[1], map_point[0]), 2, (255, 0, 0), thickness=-1) + + # Draw current position + cur_pos = (game.get_game_variable(vzd.POSITION_X), game.get_game_variable(vzd.POSITION_Y)) + map_cur = util.abs_to_global_map_pos(game, cur_pos, origin, global_map_size) + cv2.circle(map_rgb, (map_cur[1], map_cur[0]), 3, (0, 255, 0), thickness=-1) + + # Draw goal + map_goal = util.abs_to_global_map_pos(game, cur_goal, origin, global_map_size) + cv2.circle(map_rgb, (map_goal[1], map_goal[0]), 3, (0, 0, 255), thickness=-1) + return map_rgb + + +def plan_path_global(game, global_map, cur_goal, origin, global_map_size, model=None): + ''' + Plan path towards goal in global coordinate space. + ''' + cur_x, cur_y, _ = game.get_agent_location() + cur_abs_pos = (cur_x, cur_y) + cur_map_pos = util.abs_to_global_map_pos(game, cur_abs_pos, origin, global_map_size) + map_goal = util.abs_to_global_map_pos(game, cur_goal, origin, global_map_size) + + # Compute absolute and map path + if model: + path = planning.shortest_path_affordance(global_map, cur_map_pos, map_goal) + else: + path = planning.shortest_path_geometric(global_map, cur_map_pos, map_goal) + + abs_path = [] + for cell in path: + abs_pos = util.global_map_to_abs_pos(game, cell, origin, global_map_size) + abs_path.append(abs_pos) + + return path, abs_path + + +def merge_maps(proj_map, fs_map, threshold): + ''' + Merge projected navigability map with geometric map to form affordance map. + ''' + # Initialize local cost map + local_cost_map = np.zeros_like(proj_map) + + # Set costs for navigation planning + local_cost_map[np.where((proj_map >= threshold) & (fs_map == 2))] = 10 # High Cost + local_cost_map[np.where((proj_map < threshold) & (fs_map == 2))] = 1 # Low Cost + + # Set walls to be non-navigable cost + local_cost_map[np.where(fs_map == 1)] = 999 + + return local_cost_map + + +def navigate(game, max_steps, final_goal, model=None, vid_path=None, + localization_noise=0.0): + ''' + Use geometric or affordance-based maps to navigate agent towards final goal. + ''' + # Set up video writer + vid_out = None + if vid_path is not None: + vid_out = cv2.VideoWriter(vid_path, + cv2.VideoWriter_fourcc(*'mp4v'), + vzd.DEFAULT_TICRATE // 2, (1120, 480)) + + # Initialize variables + abs_path = [] + path_idx = 0 + global_map_size = 1024 + global_cost_map = np.zeros((global_map_size, global_map_size)) + origin_x, origin_y, _ = game.get_agent_location() + origin = (origin_x, origin_y) + total_damage = 0 + total_steps = 0 + replan = False + full_path = [] + + for i in range(max_steps): + # Get current state and buffer + state = game.get_state() + if state is None: + break + + rgb_frame = np.rollaxis(state.screen_buffer, 0, 3) + depth_frame = np.expand_dims(state.depth_buffer, axis=2) + rgbd_frame = np.concatenate((rgb_frame, depth_frame), axis=2) + + # Compute geometric free space map + _, fs_map = util.compute_map(game) + fs_map[:, :16] = 0 + fs_map[:, 48:] = 0 + local_cost_map = fs_map + + # Compute and merge projected segmentation map if model specified + if model: + threshold = 0.4 + seg_map, conf_map = affordance.segment_view(model, rgbd_frame, threshold) + proj_conf_map, valid_map = affordance.project_conf_map(rgbd_frame, conf_map, False) + + local_cost_map = merge_maps(proj_conf_map, fs_map, threshold) + + # Update global map used for planning + update_cells = np.where(local_cost_map > 0) + for cell in zip(update_cells[0], update_cells[1]): + abs_pos = util.map_to_abs_pos(game, cell) + global_pos = util.abs_to_global_map_pos(game, abs_pos, origin, global_map_size) + cur_val = global_cost_map[global_pos[0], global_pos[1]] + new_val = local_cost_map[cell[0], cell[1]] + + if model: + global_cost_map[global_pos[0], global_pos[1]] = (cur_val + new_val) / 2 + else: + global_cost_map[global_pos[0], global_pos[1]] = new_val + + # Replan path to specified goal + if (i % 10 == 0) or replan: + path, abs_path = plan_path_global(game, global_cost_map, final_goal, origin, + global_map_size, model) + replan = False + + # Visualize merged map, goal, and planned path + if len(abs_path) > 0: + path_idx = min(len(abs_path) - 1, 3) + global_map_rgb = visualize_global(game, global_cost_map, global_map_size, origin, + final_goal, abs_path[path_idx:]) + locomotion.navigate_beeline(game, abs_path[path_idx]) + else: + global_map_rgb = visualize_global(game, global_cost_map, global_map_size, origin, + final_goal, None) + game.make_action([10, 0]) + replan = True + + # Add current location to path + cur_x, cur_y, _ = game.get_agent_location() + full_path.append((round(cur_x), round(cur_y))) + + # Select next goal if reached + dist_to_goal = util.dist_to(game, final_goal) + if dist_to_goal < 30: + print('Goal reached') + return {'success': 1, 'steps': total_steps, 'damage': total_damage}, full_path + + # Check for damage + cur_damage = 100 - game.get_game_variable(vzd.HEALTH) + if cur_damage > 15: + total_damage += 20 + print('Total Damage: {}'.format(total_damage)) + if util.check_monster_collision(game): + total_damage += 4 + print('Total Damage: {}'.format(total_damage)) + game.send_game_command("give health") + + # Write to video if specified + if vid_out: + vis_buffer = np.zeros((480, 1120, 3), dtype=np.uint8) + rgb_frame = cv2.cvtColor(rgb_frame, cv2.COLOR_BGR2RGB) + vis_buffer[:240, :320, :] = rgb_frame + + if model: + rgb_frame_alpha = 0.7 * rgb_frame + seg_map_colour = cv2.applyColorMap(seg_map * 255, cv2.COLORMAP_JET) + seg_map_alpha = 0.3 * seg_map_colour + seg_map_combined = rgb_frame_alpha + seg_map_alpha + vis_buffer[240:, :320, :] = seg_map_combined + + conf_map_int = np.rint(conf_map * 255).astype(np.uint8) + conf_map_colour = cv2.applyColorMap(conf_map_int, cv2.COLORMAP_JET) + conf_map_alpha = 0.3 * conf_map_colour + conf_map_combined = rgb_frame_alpha + conf_map_alpha + vis_buffer[240:, 320:640, :] = conf_map_combined + + vis_buffer[:, 640:, :] = global_map_rgb[512 - 240: 512 + 240, 512 - 240: 512 + 240] + vid_out.write(vis_buffer) + + total_steps = i + 1 + + # Terminate game and write video to disk + if vid_out: + vid_out.release() + return {'success': 0, 'steps': 1000, 'damage': total_damage}, full_path diff --git a/src/modules/planning.py b/src/modules/planning.py new file mode 100644 index 0000000..85c04fb --- /dev/null +++ b/src/modules/planning.py @@ -0,0 +1,54 @@ +import numpy as np + +import common.pyastar as pyastar + + +def shortest_path_geometric(map, start, goal): + ''' + Find shortest path to goal using geometric navigation cost map. + ''' + # Check if start and goal locations are valid + map[start[0], start[1]] = 2 + map[start[0], start[1] - 1] = 2 + map[start[0], start[1] + 1] = 2 + map[start[0] + 1, start[1]] = 2 + map[start[0] + 1, start[1] - 1] = 2 + map[start[0] + 1, start[1] + 1] = 2 + map[start[0] - 1, start[1]] = 2 + map[start[0] - 1, start[1] - 1] = 2 + map[start[0] - 1, start[1] + 1] = 2 + if map[goal[0], goal[1]] == 1: + return [] + + # Find shortest path using A star + map = np.copy(map).astype(np.float32) + map[np.where(map == 0)] = 3 + map[np.where(map == 1)] = 999 + map[np.where(map == 2)] = 1 + + path = pyastar.astar_path(map, start, goal, allow_diagonal=True) + return path + + +def shortest_path_affordance(map, start, goal): + ''' + Find shortest path to goal using affordance-based navigation cost map. + ''' + # Check if start and goal locations are valid + map[start[0], start[1]] = 1 + map[start[0], start[1] - 1] = 1 + map[start[0], start[1] + 1] = 1 + map[start[0] + 1, start[1]] = 1 + map[start[0] + 1, start[1] - 1] = 1 + map[start[0] + 1, start[1] + 1] = 1 + map[start[0] - 1, start[1]] = 1 + map[start[0] - 1, start[1] - 1] = 1 + map[start[0] - 1, start[1] + 1] = 1 + + # Find shortest path using A star + map = np.copy(map).astype(np.float32) + map = np.around(map) + map[np.where(map == 0)] = 20 # Set unknown cells to higher cost + + path = pyastar.astar_path(map, start, goal, allow_diagonal=True) + return path diff --git a/src/preprocess/__init__.py b/src/preprocess/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/preprocess/random_explore.py b/src/preprocess/random_explore.py new file mode 100644 index 0000000..b4f1003 --- /dev/null +++ b/src/preprocess/random_explore.py @@ -0,0 +1,216 @@ +import cv2 +import math +import numpy as np +import random +import vizdoom as vzd + +from os.path import join + + +def navigate_beeline(simple_map): + actions = [0, 0, 0] + + # Spin around if no beacon is present in map + no_beacon = np.sum(simple_map == 4) == 0 + if no_beacon: + actions[1] = 1.0 + return actions + + # Take action to move towards goal if present + center = simple_map.shape[0] / 2.0 + beacon_locs = np.where(simple_map == 4) + min_dist = 9999 + + for l in zip(beacon_locs[0], beacon_locs[1]): + xdiff = l[0] - center + ydiff = l[1] - center + d = abs(xdiff) + abs(ydiff) + if (d < min_dist): + min_dist = d + actions[2] = 1.0 + if (abs(ydiff) > 3): + if (ydiff < 0): + actions[0] = 1.0 + else: + actions[1] = 1.0 + + return actions + + +def update_map(state, height=240, width=320, + map_size=256, map_scale=3, fov=90.0, + beacon_scale=50, pick_new_goal=False, + only_visible_beacons=True, curr_goal=None, + explored_goals={}, nodes={}, edges={}): + # Extract agent state from game + depth_buffer = state.depth_buffer + player_x = state.game_variables[0] + player_y = state.game_variables[1] + player_angle = state.game_variables[2] + + # Initialize maps + canvas_size = 2*map_size + 1 + simple_map = np.zeros((canvas_size, canvas_size), dtype=np.uint8) + + # Compute upper left and upper right extreme coordinates + r = canvas_size + offset = 225 + + y1 = int(r * math.cos(math.radians(offset + player_angle))) + x1 = int(r * math.sin(math.radians(offset + player_angle))) + + y2 = int(r * math.cos(math.radians(offset + player_angle - fov))) + x2 = int(r * math.sin(math.radians(offset + player_angle - fov))) + + # Draw FOV boundaries + _, p1, p2 = cv2.clipLine((0, 0, canvas_size, canvas_size), + (map_size, map_size), + (map_size + x1, map_size + y1)) + _, p3, p4 = cv2.clipLine((0, 0, canvas_size, canvas_size), + (map_size, map_size), + (map_size + x2, map_size + y2)) + + # Ray cast from eye line to project depth map into 2D ray points + game_unit = 100.0/14 + ray_cast = (depth_buffer[height//2] * game_unit)/float(map_scale) + + ray_points = [(map_size, map_size)] + for i in range(10, canvas_size-10): + d = ray_cast[int(float(width)/canvas_size * i - 1)] + theta = (float(i)/canvas_size * fov) + + ray_y = int(d * math.sin(math.radians(offset - theta))) + map_size + ray_x = int(d * math.cos(math.radians(offset - theta))) + map_size + + _, _, p = cv2.clipLine((0, 0, canvas_size, canvas_size), + (map_size, map_size), + (ray_y, ray_x)) + ray_points.append(p) + + # Fill free space on 2D map with colour + cv2.fillPoly(simple_map, np.array([ray_points], dtype=np.int32), (1, 1, 1)) + + quantized_x = int(player_x/beacon_scale) * beacon_scale + quantized_y = int(player_y/beacon_scale) * beacon_scale + beacon_radius = 10 + + # Get beacons within range of current agent position + beacons = [] + for bnx in range(-beacon_radius, beacon_radius+1): + for bny in range(-beacon_radius, beacon_radius+1): + beacon_x = quantized_x + bnx * beacon_scale + beacon_y = quantized_y + bny * beacon_scale + beacons.append((beacon_x, beacon_y)) + + # Compute set of visible beacons and draw onto the map + visible_beacons_world = [] + for b in beacons: + # Insert nodes and edges for beacons + if b not in nodes: + nodes[b] = True + + neighbors = [(b[0], b[1] - beacon_scale), + (b[0], b[1] + beacon_scale), + (b[0] - beacon_scale, b[1]), + (b[0] + beacon_scale, b[1]), + (b[0] - beacon_scale, b[1] - beacon_scale), + (b[0] + beacon_scale, b[1] + beacon_scale), + (b[0] - beacon_scale, b[1] + beacon_scale), + (b[0] + beacon_scale, b[1] - beacon_scale)] + + for n in neighbors: + if n in visible_beacons_world: + if (b, n) not in edges: + edges[(b, n)] = True + edges[(n, b)] = True + + # Draw beacons into map + object_relative_x = -b[0] + player_x + object_relative_y = -b[1] + player_y + + rotated_x = math.cos(math.radians(-player_angle)) * object_relative_x - math.sin(math.radians(-player_angle)) * object_relative_y # NOQA + rotated_y = math.sin(math.radians(-player_angle)) * object_relative_x + math.cos(math.radians(-player_angle)) * object_relative_y # NOQA + + rotated_x = int(rotated_x/map_scale + map_size) + rotated_y = int(rotated_y/map_scale + map_size) + + if (rotated_x >= 0 and rotated_x < canvas_size and + rotated_y >= 0 and rotated_y < canvas_size): + object_id = 3 + show = True + if simple_map[rotated_x, rotated_y] == 0: + show = (only_visible_beacons is not True) + else: + visible_beacons_world.append((b[0], b[1])) + + if show: + simple_map[rotated_x, rotated_y] = object_id + + # Pick new goal from unexplored visible beacons if required + if pick_new_goal: + unexplored_beacons = [] + for b in visible_beacons_world: + if b not in explored_goals: + unexplored_beacons.append(b) + + if len(unexplored_beacons) > 0: + beacon_idx = random.randint(0, len(unexplored_beacons)-1) + curr_goal = unexplored_beacons[beacon_idx] + explored_goals[curr_goal] = True + else: + curr_goal = None + + # Draw current goal location on map + if curr_goal is not None: + object_relative_x = -curr_goal[0] + player_x + object_relative_y = -curr_goal[1] + player_y + + rotated_x = math.cos(math.radians(-player_angle)) * object_relative_x - math.sin(math.radians(-player_angle)) * object_relative_y # NOQA + rotated_y = math.sin(math.radians(-player_angle)) * object_relative_x + math.cos(math.radians(-player_angle)) * object_relative_y # NOQA + + rotated_x = int(rotated_x/map_scale + map_size) + rotated_y = int(rotated_y/map_scale + map_size) + + if (rotated_x >= 0 and rotated_x < canvas_size and + rotated_y >= 0 and rotated_y < canvas_size): + object_id = 4 + if simple_map[rotated_x, rotated_y] > 0: + simple_map[rotated_x, rotated_y] = object_id + + return simple_map, curr_goal + + +def explore_map_random_policy(config, wad_dir, wad_id, nodes={}, edges={}, explored_goals={}): + wad_path = join(wad_dir, '{}.wad'.format(wad_id)) + + # Set up Doom enviroinment + game = vzd.DoomGame() + game.load_config(config) + game.set_doom_scenario_path(wad_path) + game.set_window_visible(False) + game.init() + game.send_game_command("iddqd") + game.new_episode() + + # Play until the game (episode) is over + step = 0 + curr_goal = None + explored_goals = {} + + while not game.is_episode_finished(): + # Pick new local goal in visibility every 50 steps + pick_new_goal = True if step % 50 == 0 else False + + # Update map from current POV + state = game.get_state() + simple_map, curr_goal = update_map(state, pick_new_goal=pick_new_goal, + curr_goal=curr_goal, explored_goals=explored_goals, + nodes=nodes, edges=edges) + + # Take action towards current goal + action = navigate_beeline(simple_map) + game.make_action(action) + step = step + 1 + game.close() + + return nodes, edges, explored_goals diff --git a/src/preprocess/run_beacon_generation.py b/src/preprocess/run_beacon_generation.py new file mode 100644 index 0000000..45c88f7 --- /dev/null +++ b/src/preprocess/run_beacon_generation.py @@ -0,0 +1,79 @@ +import argparse +import pickle as pkl +import time + +import common.util as util +from preprocess.random_explore import explore_map_random_policy + + +''' +This script is used to generate beacons located in freespace from any compatible Doom map. + +The locations associated with these beacons can be used as agent spawn locations for +self-supervised sampling. + +Configuration is performed via command line arguments specified below. +''' + + +def parse_arguments(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Paths + parser.add_argument('--wad-dir', type=str, default='../data/maps/test', + help='Path to dir containing map files') + parser.add_argument('--save-dir', type=str, required=True, + help='Path to save PKLs with generated beacons') + + # Beacon Options + parser.add_argument('--iters', type=int, default=5, + help='Number of iterations to explore for, accumulating beacons from each') + + args = parser.parse_args() + return args + + +def filter_graph(nodes, edges): + nodes_with_edges = {} + for e in edges: + nodes_with_edges[e[0]] = True + nodes_with_edges[e[1]] = True + + for n in list(nodes.keys()): + if n not in nodes_with_edges: + nodes.pop(n) + + +def generate_beacons(args): + default_cfg_path = '../data/configs/default.cfg' + wad_ids = util.get_sorted_wad_ids(args.wad_dir) + + # Generate beacons for each map in dir + for idx, wad_id in enumerate(wad_ids): + start = time.time() + + # Explore map with random policy and build beacon connectivity graph + nodes = {} + edges = {} + for i in range(args.iters): + explore_map_random_policy(default_cfg_path, args.wad_dir, wad_id, + nodes=nodes, edges=edges) + print('{} nodes accumulated...'.format(len(nodes))) + filter_graph(nodes, edges) + + # Dump accumulated beacon nodes to disk as PKL + print('{} nodes after filtering...'.format(len(nodes))) + with open('./{}.pkl'.format(wad_id), 'wb') as handle: + pkl.dump(nodes, handle, protocol=pkl.HIGHEST_PROTOCOL) + + # Report statistics for current map + end = time.time() + elapsed_time = end - start + print('Finished exploring map {} for {} iterations in {}s'.format( + idx, args.iters, elapsed_time + )) + + +if __name__ == "__main__": + args = parse_arguments() + generate_beacons(args) diff --git a/src/train/__init__.py b/src/train/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/train/data_loader.py b/src/train/data_loader.py new file mode 100644 index 0000000..0a2dc49 --- /dev/null +++ b/src/train/data_loader.py @@ -0,0 +1,73 @@ +import keras +import numpy as np + + +class DataGenerator(keras.utils.Sequence): + 'Generates data for Keras' + def __init__(self, data_file_x, data_file_y, data_file_weights=None, batch_size=32, + in_shape=(256, 320, 4), out_shape=(256, 320, 1), shuffle=True): + 'Initialization' + self.data_x = np.load(data_file_x, mmap_mode='r') + self.data_y = np.load(data_file_y, mmap_mode='r') + self.data_weights = None + if data_file_weights is not None: + self.data_weights = np.load(data_file_weights, mmap_mode='r') + + self.batch_size = batch_size + self.list_IDs = np.arange(self.data_x.shape[0]) + self.in_shape = in_shape + self.out_shape = out_shape + self.shuffle = shuffle + self.on_epoch_end() + + def __len__(self): + 'Denotes the number of batches per epoch' + return int(np.floor(len(self.list_IDs) / self.batch_size)) + + def __getitem__(self, index): + 'Generate one batch of data' + # Generate indexes of the batch + indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size] + + # Find list of IDs + list_IDs_temp = [self.list_IDs[k] for k in indexes] + + # Generate data + X, y, weights = self.__data_generation(list_IDs_temp) + + # Pad input to UNet shape + padded_X = np.zeros((self.batch_size, 256, 320, 4)) + padded_X[:, 8:-8, :, :] = X + + # Add weights to labels if necessary + if self.data_weights is not None: + padded_y = np.zeros((self.batch_size, 256, 320, 2)) + padded_y[:, 8:-8, :, 1] = weights[:, :, :, 0] + else: + padded_y = np.zeros((self.batch_size, 256, 320, 1)) + + padded_y[:, 8:-8, :, 0] = y[:, :, :, 0] + padded_y[:, 8:-8, :, 0] = padded_y[:, 8:-8, :, 0] - 1 + return padded_X, padded_y + + def on_epoch_end(self): + 'Updates indexes after each epoch' + self.indexes = np.arange(len(self.list_IDs)) + if self.shuffle: + np.random.shuffle(self.indexes) + + def __data_generation(self, list_IDs_temp): + 'Generates data containing batch_size samples' # X : (n_samples, *dim, n_channels) + # Initialization + X = np.empty((self.batch_size, 240, 320, 4)) + y = np.empty((self.batch_size, 240, 320, 1)) + weights = np.empty((self.batch_size, 240, 320, 1)) + + # Generate data + for i, ID in enumerate(list_IDs_temp): + X[i] = self.data_x[ID] + y[i] = self.data_y[ID] + if self.data_weights is not None: + weights[i] = self.data_weights[ID] + + return X, y, weights diff --git a/src/train/run_data_sampling.py b/src/train/run_data_sampling.py new file mode 100644 index 0000000..d9fadca --- /dev/null +++ b/src/train/run_data_sampling.py @@ -0,0 +1,140 @@ +import argparse +import json +import keras.backend as K +import matplotlib.pyplot as plt +import numpy as np + +from keras.utils import multi_gpu_model +from os.path import join +from sample import sample +from segmentation_models import Unet +from tqdm import tqdm + +import common.util as util + +''' +This script is used to generate self-supervised partial labels for hazard segmentation. + +Configuration is performed via command line arguments specified below. +''' + + +def parse_arguments(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Paths + parser.add_argument('--wad-dir', type=str, default='../data/maps/test', + help='Path to dir containing map files') + parser.add_argument('--beacon-dir', type=str, required=True, + help='Path to dir containing beacons specifying valid spawn positions \ + for each map') + parser.add_argument('--model-path', type=str, + help='Path to seed model if active sampling is being employed') + parser.add_argument('--multi-gpu-model', action='store_true', + help='Specifies whether multi-GPU model is being used') + parser.add_argument('--save-dir', type=str, required=True, + help='Path to dir where partially-labeled samples should be saved') + + # Sampling Options + parser.add_argument('--samples-per-map', type=int, default=500, + help='Number of iterations to explore for') + parser.add_argument('--max-sample-ratio', type=float, default=2.0, + help='Maximum ratio of number of attempted samples to specified \ + samples per map') + parser.add_argument('--freespace-only', action='store_true', + help='Specifies if sample goals should be selected only from freespace') + + # Visualization Options + parser.add_argument('--viz-output', action='store_true', + help='Specifies whether visualizations should be shown for debugging') + + args = parser.parse_args() + return args + + +def load_model(args): + # Load trained seed segmentation model to use with active sampling + K.set_learning_phase(0) + model = Unet('resnet18', input_shape=(256, 320, 4), activation='sigmoid', classes=1, + encoder_weights=None) + + # Convert to multi-GPU model if necessary + if args.multi_gpu_model: + model = multi_gpu_model(model) + + model.load_weights(args.model_path) + return model + + +def generate_data(args): + ''' + Sample up to max_sample times from each map, saving results to disk in NPY format + ''' + wad_ids = util.get_sorted_wad_ids(args.wad_dir) + max_samples = args.samples_per_map * args.max_sample_ratio + + # Load seed model if specified + model = None + if args.model_path: + model = load_model(args) + + # Sample from each map up to limit + data_x = [] + data_y = [] + data_weights = [] + maps_sample_log = {} + map_progress = tqdm(desc='Map Progress', total=len(wad_ids)) + + for wad_id in wad_ids: + sample_progress = tqdm(desc='Sample Progress', total=args.samples_per_map) + valid_xy = util.get_valid_locations(args.beacon_dir, wad_id) + game = util.setup_game(args.wad_dir, wad_id, visible=args.viz_output) + + cur_sample = 0 + times_sampled = 0 + sample_log = [0, 0, 0, 0] + while cur_sample < args.samples_per_map and times_sampled < max_samples: + status, rgbd_frames, seg_maps, weight_maps = sample(args, game, valid_xy, model) + times_sampled += 1 + + # Check if sample status indicates success + if status >= 0: + for (rgbd_frame, seg_map, weight_map) in zip(rgbd_frames, seg_maps, weight_maps): + # Check that labels in frame are not too sparse + if np.count_nonzero(seg_map) >= 1000: + data_x.append(rgbd_frame) + data_y.append(seg_map[:, :, np.newaxis]) + data_weights.append(weight_map[:, :, np.newaxis]) + + if args.viz_output: + plt.imshow(rgbd_frame[:, :, :3]) + plt.imshow(seg_map, alpha=0.6) + plt.show() + + sample_log[status] += 1 + cur_sample += 1 + sample_progress.update(1) + + # Log number of samples collected + maps_sample_log[wad_id] = sample_log + sample_progress.close() + map_progress.update(1) + print('INFO: Sampled map {} for {} times resulting in {} samples'.format(wad_id, times_sampled, cur_sample)) # NOQA + print('INFO: Sample Distribution: {} success || {} environmental damage || {} monster damage || {} obstacle'.format(sample_log[0], sample_log[1], sample_log[2], sample_log[3])) # NOQA + + # Save data to disk as NPY + data_x = np.asarray(data_x, dtype=np.uint8) + data_y = np.asarray(data_y, dtype=np.uint8) + data_weights = np.asarray(data_weights, dtype=np.uint8) + np.save(join(args.save_dir, 'x.npy'), data_x) + np.save(join(args.save_dir, 'y.npy'), data_y) + np.save(join(args.save_dir, 'weights.npy'), data_weights) + with open(join(args.save_dir, 'sample_stats.json'), 'w') as fp: + json.dump(maps_sample_log, fp, indent=4) + print('Done: Generated {} samples'.format(data_x.shape[0])) + map_progress.close() + + +if __name__ == "__main__": + args = parse_arguments() + generate_data(args) diff --git a/src/train/run_train_A2L.py b/src/train/run_train_A2L.py new file mode 100644 index 0000000..92f7599 --- /dev/null +++ b/src/train/run_train_A2L.py @@ -0,0 +1,160 @@ +import argparse +import copy +import numpy as np +import os + +from os.path import join + +from run_data_sampling import generate_data +from run_train_model import train_model + +''' +This script is used to train a ResNet-18-based UNet segmentation model using active learning. +A small dataset is first collected and used to train a seed model, which is employed to actively +generate additional examples that maximize model uncertainty. The next iterations of this model +are then trained using a blended dataset consisting of exanokes collected in all active iterations. + +Configuration is performed via command line arguments specified below. +''' + + +def parse_arguments(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Paths + parser.add_argument('--wad-dir', type=str, default='../data/maps/test', + help='Path to dir containing map files') + parser.add_argument('--beacon-dir', type=str, required=True, + help='Path to dir containing beacons specifying valid spawn positions \ + for each map') + parser.add_argument('--multi-gpu-model', action='store_true', + help='Specifies whether multi-GPU model should be used') + parser.add_argument('--save-dir', type=str, required=True, + help='Path to root dir where collected samples and intermediate models \ + should be saved') + + # Sampling Options + parser.add_argument('--active-iterations', type=int, default=5, + help='Number of active learning iterations to run') + parser.add_argument('--samples-per-map', type=int, default=500, + help='Total number of samples to collect per map') + parser.add_argument('--max-sample-ratio', type=float, default=2.0, + help='Ratio of maximum number of times to sample a map') + parser.add_argument('--freespace-only', action='store_true', + help='Specifies if sample goals should be selected only from freespace') + + # Training Options + parser.add_argument('--batch-size', type=int, default=40, + help='Batch size used for training') + parser.add_argument('--epochs', type=int, default=50, + help='Number of epochs to train for in first iteration') + + args = parser.parse_args() + return args + + +def active_train_loop(args): + ''' + Run active sampling + training loop active_iterations times. + ''' + for idx in range(args.active_iterations): + active_iteration(args, idx) + + +def active_iteration(args, idx): + ''' + Collect samples using seed model and traing next iteration of active learning model. + ''' + # Create dir for first iteration samples/model + cur_sample_dir = join(args.save_dir, str(idx), 'samples') + os.makedirs(cur_sample_dir, exist_ok=True) + + # Collect samples from environments (using previously trained model if not first iteration) + sample_args = copy.deepcopy(args) + sample_args.samples_per_map = int(args.samples_per_map / args.active_iterations) + sample_args.save_dir = cur_sample_dir + sample_args.viz_output = False + if idx > 0: + prev_model_path = join(args.save_dir, str(idx - 1), 'seg_model.h5') + sample_args.model_path = prev_model_path + else: + sample_args.model_path = None + + print('INFO: Collecting samples for iteration {}'.format(idx)) + generate_data(sample_args) + print('INFO: Done collecting samples for iteration {}'.format(idx)) + + # Build blended dataset from current and previous iterations + if idx > 0: + print('INFO: Building blended dataset for iteration {}'.format(idx)) + build_blended_dataset(args, idx) + print('INFO: Done building blended dataset for iteration {}'.format(idx)) + + # Train model using saved data from current iteration + # (init weights from previous model if not first iteration) + train_args = copy.deepcopy(args) + train_args.save_dir = join(args.save_dir, str(idx)) + train_args.use_weights = True + if idx > 0: + prev_model_path = join(args.save_dir, str(idx - 1), 'seg_model.h5') + blended_data_path = join(args.save_dir, str(idx), 'blended_samples') + train_args.saved_model_path = prev_model_path + train_args.data_dir = blended_data_path + else: + train_args.saved_model_path = None + train_args.data_dir = cur_sample_dir + + print('INFO: Training model for iteration {}'.format(idx)) + train_model(train_args) + print('INFO: Done training model for iteration {}'.format(idx)) + + +def build_blended_dataset(args, idx): + ''' + Blend samples collected in current iteration of active loop with previously-collected samples. + ''' + blended_dir = join(args.save_dir, str(idx), 'blended_samples') + os.makedirs(blended_dir, exist_ok=True) + + # Load memmaps for current samples + cur_sample_dir = join(args.save_dir, str(idx), 'samples') + cur_x = np.load(join(cur_sample_dir, 'x.npy'), mmap_mode='r') + cur_y = np.load(join(cur_sample_dir, 'y.npy'), mmap_mode='r') + cur_weights = np.load(join(cur_sample_dir, 'weights.npy'), mmap_mode='r') + + # Load memmaps for previous samples + prev_sample_dir = join(args.save_dir, str(idx - 1), 'samples') + prev_x = np.load(join(prev_sample_dir, 'x.npy'), mmap_mode='r') + prev_y = np.load(join(prev_sample_dir, 'y.npy'), mmap_mode='r') + prev_weights = np.load(join(prev_sample_dir, 'weights.npy'), mmap_mode='r') + + # Build blended dataset + num_new_samples = cur_x.shape[0] + num_old_samples = min(prev_x.shape[0], num_new_samples) + rand_old_idxs = np.random.choice(prev_x.shape[0], size=num_old_samples, replace=False) + + # Blend X data + rand_old_x = prev_x[rand_old_idxs, :, :, :] + blended_x = np.concatenate((cur_x, rand_old_x), axis=0) + np.save(join(blended_dir, 'x.npy'), blended_x) + del rand_old_x + del blended_x + + # Blend y data + rand_old_y = prev_y[rand_old_idxs, :, :, :] + blended_y = np.concatenate((cur_y, rand_old_y), axis=0) + np.save(join(blended_dir, 'y.npy'), blended_y) + del rand_old_y + del blended_y + + # Blend weight data + rand_old_weights = prev_weights[rand_old_idxs, :, :, :] + blended_weights = np.concatenate((cur_weights, rand_old_weights), axis=0) + np.save(join(blended_dir, 'weights.npy'), blended_weights) + del rand_old_weights + del blended_weights + + +if __name__ == "__main__": + args = parse_arguments() + active_train_loop(args) diff --git a/src/train/run_train_model.py b/src/train/run_train_model.py new file mode 100644 index 0000000..229095c --- /dev/null +++ b/src/train/run_train_model.py @@ -0,0 +1,232 @@ +import argparse +import io +import keras.optimizers +import numpy as np +import os +import tensorflow as tf + +from keras import backend as K +from keras.utils import multi_gpu_model +from PIL import Image +from segmentation_models import Unet + +from train.data_loader import DataGenerator + +''' +This script is used to train a ResNet-18-based UNet segmentation model using partially labelled +data obtained from the sampling script. Use of a distance-weighted loss can be enabled using the +--use-weights argument. + +Configuration is performed via command line arguments specified below. +''' + + +def parse_arguments(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Paths + parser.add_argument('--data-dir', type=str, required=True, + help='Path to dir containing saved samples') + parser.add_argument('--save-dir', type=str, required=True, default='.', + help='Path to dir where trained model should be saved') + parser.add_argument('--saved-model-path', type=str, + help='Path to saved model from which to load weights') + + # Training Options + parser.add_argument('--epochs', type=int, default=50, + help='Number of epochs to train for') + parser.add_argument('--batch-size', type=int, default=40, + help='Batch size used for training') + parser.add_argument('--use-weights', action='store_true', + help='Specifies if saved weights should be used to compute loss/acc') + + args = parser.parse_args() + + return args + + +def train_model(args): + # Avoid Tensorflow eats up GPU memory + config = tf.ConfigProto() + config.gpu_options.allow_growth = True + sess = tf.Session(config=config) + K.set_session(sess) + + # Set file paths + args.log_path = os.path.join(args.save_dir, 'logs') + args.model_path = os.path.join(args.save_dir, 'seg_model.h5') + args.data_path_x = os.path.join(args.data_dir, 'x.npy') + args.data_path_y = os.path.join(args.data_dir, 'y.npy') + args.data_path_weights = None + if args.use_weights: + args.data_path_weights = os.path.join(args.data_dir, 'weights.npy') + + # Set up training data generator + training_generator = DataGenerator(args.data_path_x, args.data_path_y, args.data_path_weights, + batch_size=args.batch_size, in_shape=(256, 320, 4), + out_shape=(256, 320, 1)) + + # Set up model + model = Unet('resnet18', input_shape=(256, 320, 4), activation='sigmoid', classes=1, + encoder_weights=None) + adam = keras.optimizers.Adam(lr=1e-4, beta_1=0.9, beta_2=0.999, epsilon=1e-08, + decay=0.0) + callbacks_list = [TensorBoardImage(args.data_path_x, args.log_path), + keras.callbacks.TensorBoard(log_dir=args.log_path, + update_freq=1000, + write_graph=True), + keras.callbacks.ModelCheckpoint(args.model_path, + save_weights_only=True)] + + # Run model on multiple GPUs if available + try: + model = multi_gpu_model(model) + print("Training model on multiple GPUs") + except ValueError: + print("Training model on single GPU") + + # Load weights if specified + if args.saved_model_path is not None: + model.load_weights(args.saved_model_path) + + # Set loss + loss = mask_loss + acc = mask_acc + if args.use_weights: + loss = mask_loss_weighted + acc = mask_acc_weighted + + # Compile model and start training + model.compile(loss=[loss], optimizer=adam, + metrics=[acc]) + model.fit_generator(generator=training_generator, + epochs=args.epochs, + use_multiprocessing=True, + workers=8, + callbacks=callbacks_list) + + return model + + +def mask_loss(y_true, y_pred): + weights = tf.less(y_true, 0) + mask = tf.where(weights, tf.zeros_like(y_true), tf.ones_like(y_true)) + num_nonzero_weights = tf.cast(tf.count_nonzero(mask, axis=[1, 2, 3]) + 1, tf.float32) + + bce = K.binary_crossentropy(y_true, y_pred) + masked_bce = tf.multiply(bce, mask) + masked_bce = K.sum(masked_bce, axis=[1, 2, 3]) / num_nonzero_weights + return K.mean(masked_bce) + + +def mask_loss_weighted(y_true, y_pred): + mask = tf.cast(y_true[:, :, :, -1:], tf.float32) / 4 + y_true = y_true[:, :, :, :1] + num_nonzero_weights = tf.cast(tf.count_nonzero(mask, axis=[1, 2, 3]) + 1, tf.float32) + + bce = K.binary_crossentropy(y_true, y_pred) + masked_bce = tf.multiply(bce, mask) + masked_bce = K.sum(masked_bce, axis=[1, 2, 3]) / num_nonzero_weights + return K.mean(masked_bce) + + +def multiway_mask_loss(y_true, y_pred): + # Mask out labels below 0 when computing loss + weights = tf.less(K.sum(y_true, axis=3), 0) + mask = tf.where(weights, + tf.zeros_like(weights, dtype=tf.float32), + tf.ones_like(weights, dtype=tf.float32)) + num_nonzero_weights = tf.cast(tf.count_nonzero(mask, axis=[1, 2]) + 1, tf.float32) + + # Set labels below 0 to 0 to prevent errors in cross-entropy loss computation + invalids = tf.less(y_true, 0) + modified_y_true = tf.where(invalids, tf.zeros_like(y_true), y_true) + + # Compute categorical cross-entropy loss + cce = K.sparse_categorical_crossentropy(modified_y_true, y_pred) + masked_cce = tf.multiply(cce, mask) + masked_cce = K.sum(masked_cce, axis=[1, 2]) / num_nonzero_weights + return K.mean(masked_cce) + + +def mask_acc(y_true, y_pred): + weights = tf.less(y_true, 0) + mask = tf.where(weights, tf.zeros_like(y_true), tf.ones_like(y_true)) + num_nonzero_weights = tf.cast(tf.count_nonzero(mask, axis=[1, 2, 3]), tf.float32) + + equal = tf.cast(K.equal(y_true, K.round(y_pred)), tf.float32) + ksum = K.sum(equal, axis=[1, 2, 3]) / num_nonzero_weights + return K.mean(ksum, axis=-1) + + +def mask_acc_weighted(y_true, y_pred): + mask = tf.cast(y_true[:, :, :, -1:], tf.float32) / 4 + y_true = y_true[:, :, :, :1] + + equal = tf.cast(K.equal(y_true, K.round(y_pred)), tf.float32) + weighted_equal = tf.multiply(equal, mask) + weighted_sum = K.sum(weighted_equal, axis=[1, 2, 3]) / K.sum(mask, axis=[1, 2, 3]) + return K.mean(weighted_sum, axis=-1) + + +def multiway_mask_acc(y_true, y_pred): + # Mask out labels below 0 when computing acc + weights = tf.less(K.sum(y_true, axis=3), 0) + mask = tf.where(weights, + tf.zeros_like(weights, dtype=tf.float32), + tf.ones_like(weights, dtype=tf.float32)) + num_nonzero_weights = tf.cast(tf.count_nonzero(mask, axis=[1, 2]) + 1, tf.float32) + + # Set labels below 0 to 0 to prevent errors in acc computation + invalids = tf.less(y_true, 0) + modified_y_true = tf.where(invalids, tf.zeros_like(y_true), y_true) + + # Compute accuracy + equal = K.equal(tf.cast(K.sum(y_true, axis=3), tf.int64), tf.argmax(y_pred, axis=3)) + equal = tf.cast(equal, tf.float32) + equal = tf.multiply(equal, mask) + ksum = K.sum(equal, axis=[1, 2]) / num_nonzero_weights + return K.mean(ksum, axis=-1) + + +class TensorBoardImage(keras.callbacks.Callback): + def __init__(self, data_path_x, log_path): + super().__init__() + self.data_path_x = data_path_x + self.log_path = log_path + + def on_epoch_end(self, epoch, logs={}): + summary = tf.Summary(value=[tf.Summary.Value(tag='train/0', + image=self._make_image(self.model, 0)), + tf.Summary.Value(tag='train/10', + image=self._make_image(self.model, 10)), + tf.Summary.Value(tag='train/25', + image=self._make_image(self.model, 25)), + tf.Summary.Value(tag='train/50', + image=self._make_image(self.model, 50))]) + writer = tf.summary.FileWriter(self.log_path) + writer.add_summary(summary, epoch) + writer.close() + + def _make_image(self, model, idx): + height, width, channel = (256, 320, 1) + x = np.zeros((1, 256, 320, 4)) + x[0, 8:-8, :, :] = np.load(self.data_path_x, mmap_mode='r')[idx] + pred_y = model.predict(x) + pred_y = np.rint(pred_y)[0, :, :, 0].astype(np.uint8) + img = Image.fromarray(pred_y * 255, mode='L') + + output = io.BytesIO() + img.save(output, format='PNG') + img_string = output.getvalue() + output.close() + + return tf.Summary.Image(height=height, + width=width, + colorspace=channel, + encoded_image_string=img_string) + + +if __name__ == "__main__": + args = parse_arguments() + train_model(args) diff --git a/src/train/sample.py b/src/train/sample.py new file mode 100644 index 0000000..e7053ba --- /dev/null +++ b/src/train/sample.py @@ -0,0 +1,433 @@ +import cv2 +import math +import matplotlib.pyplot as plt +import numpy as np +import random +import vizdoom as vzd + +import common.util as util +import modules.affordance as affordance +import modules.locomotion as locomotion +import modules.planning as planning + + +def pick_goal(game, fs_map, explored_goals, beacon_radius=12, + beacon_scale=40, map_scale=8, map_size=64): + ''' + Pick random point in agent's FOV to set as sampling goal. + ''' + # Recreate full map from partial free space map + canvas_size = 2*map_size + 1 + full_map = np.zeros((canvas_size, canvas_size), dtype=np.uint8) + full_map[:map_size + 1, map_size - 32:map_size + 32 + 1] = fs_map + + # Get current player position and angle + player_x, player_y, player_angle = game.get_agent_location() + quantized_x = int(round(player_x/beacon_scale)) * beacon_scale + quantized_y = int(round(player_y/beacon_scale)) * beacon_scale + + # Get beacons within range of current agent position + beacons = [] + for bnx in range(-beacon_radius, beacon_radius+1): + for bny in range(-beacon_radius, beacon_radius+1): + beacon_x = quantized_x + bnx * beacon_scale + beacon_y = quantized_y + bny * beacon_scale + beacons.append((beacon_x, beacon_y)) + + # Compute set of visible beacons + visible_beacons_world = [] + for b in beacons: + object_relative_x = -b[0] + player_x + object_relative_y = -b[1] + player_y + rotated_x = math.cos(math.radians(-player_angle)) * object_relative_x - math.sin(math.radians(-player_angle)) * object_relative_y # NOQA + rotated_y = math.sin(math.radians(-player_angle)) * object_relative_x + math.cos(math.radians(-player_angle)) * object_relative_y # NOQA + rotated_x = int(round(rotated_x/map_scale + map_size)) + rotated_y = int(round(rotated_y/map_scale + map_size)) + + if (rotated_x >= 0 and rotated_x < canvas_size and + rotated_y >= 0 and rotated_y < canvas_size): + if full_map[rotated_x, rotated_y] == 0: + continue + object_id = 3 + visible_beacons_world.append((b[0], b[1])) + full_map[rotated_x, rotated_y] = object_id + + # Pick new goal from unexplored visible beacons if required + unexplored_beacons = [] + for b in visible_beacons_world: + if b not in explored_goals: + unexplored_beacons.append(b) + + if len(unexplored_beacons) > 0: + beacon_idx = random.randint(0, len(unexplored_beacons)-1) + curr_goal = unexplored_beacons[beacon_idx] + explored_goals[curr_goal] = True + else: + curr_goal = None + + # Compute relative goal + rel_goal = None + if curr_goal is not None: + diff_x = curr_goal[0] - player_x + diff_y = curr_goal[1] - player_y + rel_goal = np.array([diff_x, diff_y]) + + return curr_goal, rel_goal + + +def get_view_xy(state): + ''' + Get absolute XY coordinates corresponding to each pixel in agent FOV + ''' + # Set hyper-parameters + width = 320.0 + fov = 110.0 + game_unit = 100.0/14 + + # Get depth buffer and position from game state + depth_buffer = state.depth_buffer + player_x = state.game_variables[0] + player_y = state.game_variables[1] + player_angle = state.game_variables[2] + + # Build map of angles + angles = np.zeros((240, 320)) + for i in range(depth_buffer.shape[1]): + angles[:, 319 - i] = i*(fov/width) + (player_angle - fov/2) + + # Build map of absolute coordinates + d = depth_buffer * game_unit + ray_y = player_y + (d * np.sin(np.deg2rad(angles))) + ray_x = player_x + (d * np.cos(np.deg2rad(angles))) + ray_xy = np.dstack((ray_x, ray_y)) + + return ray_xy + + +def label_seg_map(state, pos, radius, colour, seg_map=None, weight_map=None, debug=False): + ''' + Paint labels onto scene observations obtained from the agent. + ''' + # Initialize seg map and get position from state + player_x = state.game_variables[0] + player_y = state.game_variables[1] + dist = np.linalg.norm([player_x - pos[0], player_y - pos[1]]) + if seg_map is None: + seg_map = np.zeros((240, 320), dtype=np.uint8) + weight_map = np.zeros((240, 320), dtype=np.uint8) + + # Compute map of relative coordinates to pos + view_xy = get_view_xy(state) + rel_view_xy = np.copy(view_xy) + rel_view_xy[:, :, 0] = rel_view_xy[:, :, 0] - pos[0] + rel_view_xy[:, :, 1] = rel_view_xy[:, :, 1] - pos[1] + view_dist = np.linalg.norm(rel_view_xy, axis=2) + + # Label points with Euclidean distance less than radius from pos + depth_frame = state.depth_buffer + weight_map = np.copy(weight_map) + if colour == 1: + max_height = int(max((200 - dist*0.3), 100)) + view_dist[:max_height, :] = 9999 + label_pts_max = np.where((view_dist < radius*0.50) & (depth_frame > 5)) + label_pts_med = np.where((view_dist >= radius*0.50) & (view_dist < radius*0.75) & (depth_frame > 5)) + label_pts_min = np.where((view_dist >= radius*0.75) & (view_dist <= radius) & (depth_frame > 5)) + else: + max_height = int(max((150 - dist*0.6), 20)) + view_dist[:max_height, :] = 9999 + label_pts_max = np.where((view_dist < radius*0.50)) + label_pts_med = np.where((view_dist >= radius*0.50) & (view_dist < radius*0.75)) + label_pts_min = np.where((view_dist >= radius*0.75) & (view_dist <= radius)) + + for i in range(len(label_pts_min[0])): + cv2.circle(seg_map, (label_pts_min[1][i], label_pts_min[0][i]), 1, colour, + thickness=-1) + cv2.circle(weight_map, (label_pts_min[1][i], label_pts_min[0][i]), 1, 2, + thickness=-1) + for i in range(len(label_pts_med[0])): + cv2.circle(seg_map, (label_pts_med[1][i], label_pts_med[0][i]), 1, colour, + thickness=-1) + cv2.circle(weight_map, (label_pts_med[1][i], label_pts_med[0][i]), 1, 3, + thickness=-1) + for i in range(len(label_pts_max[0])): + cv2.circle(seg_map, (label_pts_max[1][i], label_pts_max[0][i]), 1, colour, + thickness=-1) + cv2.circle(weight_map, (label_pts_max[1][i], label_pts_max[0][i]), 1, 4, + thickness=-1) + + # Visualize intermediate output if debug mode enabled + if debug: + vis_xy = np.zeros((240, 320, 3)) + vis_xy[:, :, :2] = view_xy + _, axarr = plt.subplots(2, 2) + axarr[0][0].imshow(vis_xy[:, :, 0]) + axarr[0][1].imshow(vis_xy[:, :, 1]) + axarr[1][0].imshow(view_dist) + axarr[1][1].imshow(seg_map) + plt.show() + + return seg_map, weight_map + + +def check_valid_position(game): + ''' + Checks if the agent is in a valid position to sample from. + ''' + # Check if agent is capable of moving (not stuck in wall) + pre_x = game.get_game_variable(vzd.GameVariable.POSITION_X) + pre_y = game.get_game_variable(vzd.GameVariable.POSITION_Y) + game.make_action([0, 10]) + dx = game.get_game_variable(vzd.GameVariable.POSITION_X) - pre_x + dy = game.get_game_variable(vzd.GameVariable.POSITION_Y) - pre_y + norm = np.linalg.norm([dx, dy]) + if norm < 0.1 or norm > 10: + return False + + # Check if agent was spawned on ground level + cur_z = game.get_game_variable(vzd.GameVariable.POSITION_Z) + if cur_z > 20: + return False + + return True + + +def max_uncertainty_path(game, model, fs_map, cur_goal, global_map_size=1024): + ''' + Plan path to sampling goal that maximizes model uncertainty. + ''' + # Compute global cost map using model + global_cost_map, origin = compute_global_cost_map(game, model, fs_map) + + # Compute path to goal that maximizes uncertainty + _, abs_path = plan_sample_path(game, global_cost_map, cur_goal, origin, global_map_size) + return abs_path + + +def compute_global_cost_map(game, model, fs_map, global_map_size=1024): + ''' + Compute global cost map to use for maximum uncertainty planning. + ''' + # Get RGBD frame from game + state = game.get_state() + rgb_frame = np.rollaxis(state.screen_buffer, 0, 3) + depth_frame = np.expand_dims(state.depth_buffer, axis=2) + rgbd_frame = np.concatenate((rgb_frame, depth_frame), axis=2) + + # Compute seg map + _, conf_map = affordance.segment_view(model, rgbd_frame) + proj_map, valid_map = affordance.project_conf_map(rgbd_frame, conf_map) + + # Set costs for navigation planning + cost_map = np.copy(proj_map) + cost_map[:, :] = 10 + cost_map[np.where(fs_map == 2)] = 3 + cost_map[np.where((proj_map <= -1) & (proj_map >= -5))] = 1 + cost_map[np.where(fs_map == 1)] = 100 + for x in [31, 32, 33]: + for i in range(3): + if valid_map[65 - (i + 1), x]: + cost_map[65 - (i + 1):, x] = 1 + break + + # Set origin as current position + cur_x, cur_y, _ = game.get_agent_location() + origin = (cur_x, cur_y) + + # Initialize global map with values from local map + global_cost_map = np.ones((global_map_size, global_map_size)) * 10 + global_cost_map[512-65:512, 512-33:512+32] = cost_map + + return global_cost_map, origin + + +def plan_sample_path(game, global_map, cur_goal, origin, global_map_size=1024): + ''' + Plan shortest path to sample goal. + ''' + cur_x, cur_y, _ = game.get_agent_location() + cur_abs_pos = (cur_x, cur_y) + cur_map_pos = util.abs_to_global_map_pos(game, cur_abs_pos, origin, global_map_size) + map_goal = util.abs_to_global_map_pos(game, cur_goal, origin, global_map_size) + + # Compute absolute and map path + abs_path = [] + path = planning.shortest_path_geometric(global_map, cur_map_pos, map_goal) + for cell in path: + abs_pos = util.global_map_to_abs_pos(game, cell, origin, global_map_size) + abs_path.append(abs_pos) + + return path, abs_path + + +def max_uncertainty_locomotion(game, global_path): + ''' + Navigate towards sequence of waypoints that maximizes model uncertainty. + ''' + # Select goal from global path + cur_goal = global_path[0] + + # Use beeline locomotion to navigate towards goal + locomotion.navigate_beeline(game, cur_goal) + + # Check if intermediate goal has been reached + player_x, player_y, _ = game.get_agent_location() + if math.sqrt(abs(player_x - cur_goal[0])**2 + abs(player_y - cur_goal[1])**2) < 10: + del global_path[0] + + +def filter_states(states, rgbd_frames): + states_x = [state.game_variables[0] for state in states] + states_y = [state.game_variables[1] for state in states] + + good_idxs = [0] + for i in range(1, len(states)): + xy_diff = np.array(states_x[i] - states_x[i - 1], states_y[i] - states_y[i - 1]) + diff_norm = np.linalg.norm(xy_diff) + if diff_norm > 10: + good_idxs.append(i) + + states = [states[idx] for idx in good_idxs] + rgbd_frames = [rgbd_frames[idx] for idx in good_idxs] + return states, rgbd_frames + + +def sample(args, game, valid_xy, model=None): + ''' + Initialize and conduct one full sampling episode. + ''' + # Start new episode and warp to random goal + game.new_episode() + game.send_game_command('iddqd') + random_xy = random.choice(valid_xy) + game.send_game_command('warp {} {}'.format(random_xy[0], random_xy[1])) + + # Rotate to random angle and check if position is valid + locomotion.rotate_random_angle(game) + valid_sample = check_valid_position(game) + if not valid_sample: + return -1, None, None, None + + # Compute and select goal in visibility + if args.freespace_only: + _, fs_map = util.compute_map(game) + else: + fs_map = np.load('./common/visible.npy') + cur_goal, _ = pick_goal(game, fs_map, {}) + if cur_goal is None: + return -1, None, None, None + + # Compute affordance map if model specified + if model is not None: + global_path = max_uncertainty_path(game, model, fs_map, cur_goal) + if len(global_path) == 0: + return -1, None, None, None + max_steps = max(200, len(global_path) * 50) + else: + max_steps = 200 + + # Build RGBD input + label_states = [] + rgbd_frames = [] + original_state = game.get_state() + label_states.append(original_state) + rgb_frame = np.rollaxis(original_state.screen_buffer, 0, 3) + depth_frame = np.expand_dims(original_state.depth_buffer, axis=2) + rgbd_frame = np.concatenate((rgb_frame, depth_frame), axis=2) + rgbd_frames.append(rgbd_frame) + + # Attempt to navigate towards goal using beeline policy + steps_taken = 0 + waypoints = [] + game.send_game_command('iddqd') + prev_health = game.get_game_variable(vzd.GameVariable.HEALTH) + + while util.dist_to(game, cur_goal) > 30 and steps_taken < max_steps: + # Take action + if model is not None: + max_uncertainty_locomotion(game, global_path) + else: + locomotion.navigate_beeline(game, cur_goal) + + # Check if episode is terminated + if game.get_state() is None: + return -1, None, None, None + + cur_x, cur_y, _ = game.get_agent_location() + cur_health = game.get_game_variable(vzd.GameVariable.HEALTH) + health_diff = prev_health - cur_health + + # Record waypoints + if (steps_taken % 5) == 0: + if len(waypoints) == 0 or np.linalg.norm([waypoints[-1][0] - cur_x, + waypoints[-1][1] - cur_y]) > 5: + waypoints.append((cur_x, cur_y)) + if len(waypoints) % 4 == 0: + cur_state = game.get_state() + label_states.append(cur_state) + cur_rgb_frame = np.rollaxis(cur_state.screen_buffer, 0, 3) + cur_depth_frame = np.expand_dims(cur_state.depth_buffer, axis=2) + cur_rgbd_frame = np.concatenate((cur_rgb_frame, cur_depth_frame), axis=2) + rgbd_frames.append(cur_rgbd_frame) + + # Check if episode should terminate from damage + if health_diff > 15: + # STATUS: Took large amount of damage (probably environmental hazard) + seg_maps = [None for label_state in label_states] + weight_maps = [np.zeros((240, 320), dtype=np.uint8) for label_state in label_states] + for idx, label_state in enumerate(label_states): + for waypoint in waypoints[:-5]: + seg_maps[idx], weight_map = label_seg_map(label_state, waypoint, 25, 1, seg_maps[idx], weight_maps[idx]) + weight_maps[idx] = np.maximum(weight_map, weight_maps[idx]) + for waypoint in waypoints[-5:]: + seg_maps[idx], weight_map = label_seg_map(label_state, waypoint, 50, 2, seg_maps[idx], weight_maps[idx]) + weight_maps[idx] = np.maximum(weight_map, weight_maps[idx]) + seg_maps[idx], weight_maps[idx] = label_seg_map(label_state, (cur_x, cur_y), 50, 2, seg_maps[idx], weight_maps[idx]) + weight_maps[idx] = np.maximum(weight_map, weight_maps[idx]) + return 1, rgbd_frames, seg_maps, weight_maps + elif util.check_monster_collision(game): + # STATUS: Collided with dynamic hazard (probably monster) + seg_maps = [None for label_state in label_states] + weight_maps = [np.zeros((240, 320), dtype=np.uint8) for label_state in label_states] + for idx, label_state in enumerate(label_states): + for waypoint in waypoints: + seg_maps[idx], weight_map = label_seg_map(label_state, waypoint, 25, 1, seg_maps[idx], weight_maps[idx]) + weight_maps[idx] = np.maximum(weight_map, weight_maps[idx]) + seg_maps[idx], weight_map = label_seg_map(label_state, (cur_x, cur_y), 40, 2, seg_maps[idx], weight_maps[idx]) + weight_maps[idx] = np.maximum(weight_map, weight_maps[idx]) + return 2, rgbd_frames, seg_maps, weight_maps + + prev_health = cur_health + steps_taken += 1 + + # Invalid sample if episode terminates after 0 steps + if steps_taken == 0: + return -1, None, None, None + + # Successful sample if goal reached under max_steps + if steps_taken < max_steps: + # STATUS: Sample success + seg_maps = [None for label_state in label_states] + weight_maps = [np.zeros((240, 320), dtype=np.uint8) for label_state in label_states] + for idx, label_state in enumerate(label_states): + for waypoint in waypoints: + seg_maps[idx], weight_map = label_seg_map(label_state, waypoint, 25, 1, seg_maps[idx], weight_maps[idx]) + weight_maps[idx] = np.maximum(weight_map, weight_maps[idx]) + + return 0, rgbd_frames, seg_maps, weight_maps + # Failed sample if more than 200 steps passed without reaching goal + else: + # STATUS: Sample failed to reach goal + # Get rid of label frames that don't go anywhere + label_states, rgbd_frames = filter_states(label_states, rgbd_frames) + + rgbd_frames = rgbd_frames + seg_maps = [None for label_state in label_states] + weight_maps = [np.zeros((240, 320), dtype=np.uint8) for label_state in label_states] + for idx, label_state in enumerate(label_states): + for waypoint in waypoints: + seg_maps[idx], weight_map = label_seg_map(label_state, waypoint, 25, 1, seg_maps[idx], weight_maps[idx]) + weight_maps[idx] = np.maximum(weight_map, weight_maps[idx]) + seg_maps[idx], weight_map = label_seg_map(label_state, (cur_x, cur_y), 25, 2, seg_maps[idx], weight_maps[idx]) + weight_maps[idx] = np.maximum(weight_map, weight_maps[idx]) + + return 3, rgbd_frames, seg_maps, weight_maps diff --git a/utils/run_get_automap.py b/utils/run_get_automap.py new file mode 100644 index 0000000..e8385a9 --- /dev/null +++ b/utils/run_get_automap.py @@ -0,0 +1,70 @@ +import argparse +import matplotlib.pyplot as plt +import numpy as np +import os +import vizdoom as vzd + +from src.common.custom_game import CustomGame + +''' +This script is used to extract a birds-eye map view around the spawn point within a specified map. + +Configuration is performed via command line arguments specified below. +''' + + +def parse_arguments(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Paths + parser.add_argument('--wad-dir', type=str, default='./data/maps/test', + help='Path to dir containing maps') + parser.add_argument('--wad-id', type=int, required=True, + help='WAD ID of map to generate automap for') + parser.add_argument('--out-dir', type=str, default='.', + help='Path to dir where output automap should be saved') + + args = parser.parse_args() + return args + + +def setup_game(wad_dir, wad_id): + # Set up VizDoom Game + game = CustomGame() + game.load_config('../data/configs/human.cfg') + game.set_screen_resolution(vzd.ScreenResolution.RES_320X240) + game.set_render_weapon(False) + game.set_window_visible(False) + game.set_automap_mode(vzd.AutomapMode.WHOLE) + game.set_automap_rotate(True) + game.add_game_args("+am_followplayer 1") + game.add_game_args("+viz_am_scale 0.8") + game.add_game_args("+am_backcolor 888888") + game.add_game_args("+freelook 1") + game.set_mode(vzd.Mode.SPECTATOR) + + # Load generated map from WAD + wad_path = os.path.join(wad_dir, '{}.wad'.format(wad_id)) # NOQA + game.set_doom_scenario_path(wad_path) + game.init() + game.send_game_command("iddqd") + return game + + +def save_automap(wad_dir, wad_id, out_dir): + # Initialize game + game = setup_game(wad_dir, wad_id) + + # Get automap + state = game.get_state() + automap = np.rollaxis(state.automap_buffer, 0, 3) + plt.imshow(automap) + + # Save automap + out_path = os.path.join(out_dir, 'automap_{}.png'.format(wad_id)) + plt.imsave(out_path, automap) + + +if __name__ == "__main__": + args = parse_arguments() + save_automap(args.wad_dir, args.wad_id, args.out_dir) diff --git a/utils/run_human_play.py b/utils/run_human_play.py new file mode 100644 index 0000000..a73d34f --- /dev/null +++ b/utils/run_human_play.py @@ -0,0 +1,72 @@ +import argparse +import os +import vizdoom as vzd + +from src.common.custom_game import CustomGame # NOQA + +''' +This script is used to explore VizDoom maps as an invincible human player. + +Configuration is performed via command line arguments specified below. +''' + + +def parse_arguments(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Paths + parser.add_argument('--wad-dir', type=str, default='./data/maps/test', + help='Path to dir containing maps') + parser.add_argument('--cfg-path', type=str, default='./data/configs/default.cfg', + help='Path to VizDoom config file in player mode') + parser.add_argument('--wad-id', type=int, required=True, + help='WAD ID of map to play') + + args = parser.parse_args() + return args + + +def setup_player_game(wad_dir, cfg_path, wad_id): + # Set up VizDoom Game + game = CustomGame() + game.load_config(cfg_path) + game.set_screen_resolution(vzd.ScreenResolution.RES_320X240) + game.set_render_weapon(False) + game.set_window_visible(True) + game.add_game_args("+freelook 1") + game.set_mode(vzd.Mode.SPECTATOR) + + # Load generated map from WAD + wad_path = os.path.join(wad_dir, '{}.wad'.format(wad_id)) # NOQA + game.set_doom_scenario_path(wad_path) + game.init() + game.send_game_command("iddqd") + return game + + +def play_map(wad_dir, cfg_path, wad_id): + # Initialize game + game = setup_player_game(wad_dir, cfg_path, wad_id) + + # Initialize variables + total_steps = 0 + + # Play game + while not game.is_episode_finished(): + # Advance action + game.advance_action() + + # Log current status + if total_steps % 10 == 0: + cur_pos = (game.get_game_variable(vzd.POSITION_X), + game.get_game_variable(vzd.POSITION_Y)) + print("Step #" + str(total_steps)) + print("Position:", cur_pos) + print("=====================") + + total_steps += 1 + + +if __name__ == "__main__": + args = parse_arguments() + play_map(args.wad_dir, args.cfg_path, args.wad_id) diff --git a/utils/run_multi_to_single_gpu.py b/utils/run_multi_to_single_gpu.py new file mode 100644 index 0000000..a91609b --- /dev/null +++ b/utils/run_multi_to_single_gpu.py @@ -0,0 +1,46 @@ +import argparse +import keras.backend as K + +from keras.utils import multi_gpu_model +from segmentation_models import Unet + +''' +This script is used to convert segmentation models trained on multiple GPUs to a format compatible +with single GPU machines. + +Configuration is performed via command line arguments specified below. +''' + + +def parse_arguments(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Paths + parser.add_argument('--model-path', type=str, required=True, + help='Path to multi-GPU model') + parser.add_argument('--out-path', type=str, + help='Output path for converted single-GPU model') + + args = parser.parse_args() + return args + + +def convert_model(args): + if args.out_path is None: + args.out_path = args.model_path.split('.h5')[0] + '_single.h5' + + # Load multi-GPU model weights + K.set_learning_phase(1) + model = Unet('resnet18', input_shape=(256, 320, 4), activation='sigmoid', classes=1, + encoder_weights=None) + model = multi_gpu_model(model) + model.load_weights(args.model_path) + + # Set weights in single-GPU model and save + single_model = model.layers[-2] + single_model.save(args.out_path) + + +if __name__ == "__main__": + args = parse_arguments() + convert_model(args)