diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0c05e086c2d8ce056c0cccd9e841ae067b933fca..ef3b984099eeedcc3ce0d98c94e5266bd3239099 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -114,6 +114,7 @@ endif()
 
 SET(CMAKE_USE_RELATIVE_PATHS ON)
 
+add_subdirectory(common/cpp)
 add_subdirectory(renderer)
 add_subdirectory(net)
 add_subdirectory(vision)
diff --git a/common/config/config.json b/common/config/config.json
index bafcaf0b4fedcfaa3aa154fe2afd9ac0ee0be026..80d6589a63910e414ff02d2db35e46291eb69130 100644
--- a/common/config/config.json
+++ b/common/config/config.json
@@ -1,66 +1,68 @@
 {
-	"middlebury": {
-		"dataset": "",
-		"threshold": 10.0,
-		"scale": 0.25
-	},
-	"source": {
-		"flip": false,
-		"nostereo": false,
-		"scale": 1.0,
-		"flip_vert": false
-	},
-	"calibrate": false,
-	"calibration": {
-		"board_size": [9,6],
-		"square_size": 50,
-		"frame_delay": 1.0,
-		"num_frames": 35,
-		"assume_zero_tangential_distortion": true,
-		"fix_aspect_ratio": true,
-		"fix_principal_point_at_center": true,
-		"use_fisheye_model": false,
-		"fix_k1": false,
-		"fix_k2": false,
-		"fix_k3": false,
-		"fix_k4": true,
-		"fix_k5": true,
-		"save": true,
-		"use_intrinsics": true,
-		"use_extrinsics": true,
-		"flip_vertical": false
-	},
-	"camera": {
-		"name": "Panasonic Lumix DMC-FZ300",
-		"focal_length": 25,
-		"sensor_width": 6.17,
-		"base_line": 0.1
-	},
-	"disparity": {
-		"algorithm": "rtcensus",
-		"use_cuda": true,
-		"minimum": 0,
-		"maximum": 208,
-		"tau": 0.0,
-		"gamma": 0.0,
-		"window_size": 5,
-		"sigma": 1.5,
-		"lambda": 8000.0
-	},
-	"display": {
-		"flip_vert": false,
-		"disparity": false,
-		"points": true,
-		"depth": false,
-		"left": false,
-		"right": false
-	},
-	"net": {
-		"listen": "tcp://*:9001",
-		"peers": []
-	},
-	"stream": {
-		"name": "dummy"
+	"vision": {
+		"middlebury": {
+			"dataset": "",
+			"threshold": 10.0,
+			"scale": 0.25
+		},
+		"source": {
+			"flip": false,
+			"nostereo": false,
+			"scale": 1.0,
+			"flip_vert": false
+		},
+		"calibrate": false,
+		"calibration": {
+			"board_size": [9,6],
+			"square_size": 50,
+			"frame_delay": 1.0,
+			"num_frames": 35,
+			"assume_zero_tangential_distortion": true,
+			"fix_aspect_ratio": true,
+			"fix_principal_point_at_center": true,
+			"use_fisheye_model": false,
+			"fix_k1": false,
+			"fix_k2": false,
+			"fix_k3": false,
+			"fix_k4": true,
+			"fix_k5": true,
+			"save": true,
+			"use_intrinsics": true,
+			"use_extrinsics": true,
+			"flip_vertical": false
+		},
+		"camera": {
+			"name": "Panasonic Lumix DMC-FZ300",
+			"focal_length": 25,
+			"sensor_width": 6.17,
+			"base_line": 0.1
+		},
+		"disparity": {
+			"algorithm": "rtcensus",
+			"use_cuda": true,
+			"minimum": 0,
+			"maximum": 208,
+			"tau": 0.0,
+			"gamma": 0.0,
+			"window_size": 5,
+			"sigma": 1.5,
+			"lambda": 8000.0
+		},
+		"display": {
+			"flip_vert": false,
+			"disparity": false,
+			"points": true,
+			"depth": false,
+			"left": false,
+			"right": false
+		},
+		"net": {
+			"listen": "tcp://*:9001",
+			"peers": []
+		},
+		"stream": {
+			"name": "dummy"
+		}
 	},
 	"representation": {
 		"net": {
diff --git a/common/cpp/CMakeLists.txt b/common/cpp/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2a9be0f3b3f32963d088d6af06809db57281d3b7
--- /dev/null
+++ b/common/cpp/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(COMMONSRC
+	src/config.cpp
+	src/configuration.cpp
+)
+
+add_library(ftlcommon ${COMMONSRC})
+
+target_include_directories(ftlcommon PUBLIC
+	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+	$<INSTALL_INTERFACE:include>
+	PRIVATE src)
+target_link_libraries(ftlcommon glog::glog)
+
diff --git a/common/cpp/include/ftl/configuration.hpp b/common/cpp/include/ftl/configuration.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..df32e22947fceddde0391f79bd7cbccf482cac18
--- /dev/null
+++ b/common/cpp/include/ftl/configuration.hpp
@@ -0,0 +1,17 @@
+#ifndef _FTL_COMMON_CONFIGURATION_HPP_
+#define _FTL_COMMON_CONFIGURATION_HPP_
+
+#include <nlohmann/json.hpp>
+#include <string>
+#include <vector>
+
+namespace ftl {
+
+extern nlohmann::json config;
+
+std::vector<std::string> configure(int argc, char **argv, const std::string &app);
+
+};
+
+#endif  // _FTL_COMMON_CONFIGURATION_HPP_
+
diff --git a/common/cpp/src/configuration.cpp b/common/cpp/src/configuration.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ccae3c756db6a2b7d4f976de497c8f6f05d8679e
--- /dev/null
+++ b/common/cpp/src/configuration.cpp
@@ -0,0 +1,114 @@
+#include <glog/logging.h>
+#include <ftl/config.h>
+
+#include <nlohmann/json.hpp>
+#include <ftl/configuration.hpp>
+
+#include <fstream>
+#include <string>
+#include <map>
+#include <iostream>
+
+using nlohmann::json;
+using std::ifstream;
+using std::string;
+using std::map;
+using std::vector;
+
+// Store loaded configuration
+namespace ftl {
+json config;
+};
+
+using ftl::config;
+
+/**
+ * Find and load a JSON configuration file
+ */
+static bool findConfiguration(const string &file, const vector<string> &paths,
+		const std::string &app) {
+	ifstream i;
+	
+	if (file != "") i.open(file);
+	if (!i.is_open()) i.open("./config.json");
+	if (!i.is_open()) i.open(FTL_LOCAL_CONFIG_ROOT "/config.json");
+	if (!i.is_open()) i.open(FTL_GLOBAL_CONFIG_ROOT "/config.json");
+	if (!i.is_open()) return false;
+	i >> config;
+	config = config[app];
+	return true;
+}
+
+/**
+ * Generate a map from command line option to value
+ */
+static map<string, string> read_options(char ***argv, int *argc) {
+	map<string, string> opts;
+
+	while (*argc > 0) {
+		string cmd((*argv)[0]);
+		if (cmd[0] != '-') break;
+
+		size_t p;
+		if ((p = cmd.find("=")) == string::npos) {
+			opts[cmd.substr(2)] = "true";
+		} else {
+			opts[cmd.substr(2, p-2)] = cmd.substr(p+1);
+		}
+
+		(*argc)--;
+		(*argv)++;
+	}
+
+	return opts;
+}
+
+/**
+ * Put command line options into json config. If config element does not exist
+ * or is of a different type then report an error.
+ */
+static void process_options(const map<string, string> &opts) {
+	for (auto opt : opts) {
+		if (opt.first == "config") continue;
+
+		if (opt.first == "version") {
+			std::cout << "FTL Vision Node - v" << FTL_VERSION << std::endl;
+			std::cout << FTL_VERSION_LONG << std::endl;
+			exit(0);
+		}
+
+		try {
+			auto ptr = json::json_pointer("/"+opt.first);
+			// TODO(nick) Allow strings without quotes
+			auto v = json::parse(opt.second);
+			if (v.type() != config.at(ptr).type()) {
+				LOG(ERROR) << "Incorrect type for argument " << opt.first;
+				continue;
+			}
+			config.at(ptr) = v;
+		} catch(...) {
+			LOG(ERROR) << "Unrecognised option: " << opt.first;
+		}
+	}
+}
+
+vector<string> ftl::configure(int argc, char **argv, const std::string &app) {
+	argc--;
+	argv++;
+
+	// Process Arguments
+	auto options = read_options(&argv, &argc);
+	
+	vector<string> paths;
+	while (argc-- > 0) {
+		paths.push_back(argv[0]);
+	}
+	
+	if (!findConfiguration(options["config"], paths, app)) {
+		LOG(FATAL) << "Could not find any configuration!";
+	}
+	process_options(options);
+
+	return paths;
+}
+
diff --git a/vision/CMakeLists.txt b/vision/CMakeLists.txt
index 47a39a6e4764d466876874994a8ae1d098e3df6c..a0cc3f6b9f57fb1d43d5c56ed035c17b11b0da44 100644
--- a/vision/CMakeLists.txt
+++ b/vision/CMakeLists.txt
@@ -5,7 +5,6 @@ include_directories(${PROJECT_SOURCE_DIR}/vision/include)
 add_subdirectory(lib)
 
 set(CVNODESRC
-	../common/cpp/src/config.cpp
 	src/main.cpp
 	src/calibrate.cpp
 	src/local.cpp
@@ -38,6 +37,7 @@ if (CUDA_FOUND)
 endif (CUDA_FOUND)
 
 add_executable(ftl-vision ${CVNODESRC})
+add_dependencies(ftl-vision ftlcommon)
 add_dependencies(ftl-vision ftlnet)
 add_dependencies(ftl-vision ftlrender)
 add_dependencies(ftl-vision libelas)
@@ -47,6 +47,6 @@ set_property(TARGET ftl-vision PROPERTY CUDA_SEPARABLE_COMPILATION ON)
 endif()
 
 #target_include_directories(cv-node PUBLIC ${PROJECT_SOURCE_DIR}/include)
-target_link_libraries(ftl-vision ftlrender Threads::Threads ZLIB::ZLIB libelas ${OpenCV_LIBS} ${LIBSGM_LIBRARIES} ${CUDA_LIBRARIES} glog::glog ftlnet)
+target_link_libraries(ftl-vision ftlcommon ftlrender Threads::Threads ZLIB::ZLIB libelas ${OpenCV_LIBS} ${LIBSGM_LIBRARIES} ${CUDA_LIBRARIES} glog::glog ftlnet)
 
 
diff --git a/vision/src/main.cpp b/vision/src/main.cpp
index cc25a334be8d49afa01ab37d179b69791dcc87a3..27a071b08006e0618b4e717b2de6617f1ea69707 100644
--- a/vision/src/main.cpp
+++ b/vision/src/main.cpp
@@ -5,7 +5,7 @@
  */
 
 #include <glog/logging.h>
-#include <ftl/config.h>
+#include <ftl/configuration.hpp>
 #include <ctpl_stl.h>
 
 #include <string>
@@ -53,78 +53,8 @@ using std::mutex;
 using std::unique_lock;
 using cv::Mat;
 using json = nlohmann::json;
-using std::ifstream;
+using ftl::config;
 
-// Store loaded configuration
-static json config;
-
-/**
- * Find and load a JSON configuration file
- */
-static bool findConfiguration(const string &file) {
-	ifstream i;
-	
-	if (file != "") i.open(file);
-	if (!i.is_open()) i.open("./config.json");
-	if (!i.is_open()) i.open(FTL_LOCAL_CONFIG_ROOT "/config.json");
-	if (!i.is_open()) i.open(FTL_GLOBAL_CONFIG_ROOT "/config.json");
-	if (!i.is_open()) return false;
-	i >> config;
-	return true;
-}
-
-/**
- * Generate a map from command line option to value
- */
-map<string, string> read_options(char ***argv, int *argc) {
-	map<string, string> opts;
-
-	while (*argc > 0) {
-		string cmd((*argv)[0]);
-		if (cmd[0] != '-') break;
-
-		size_t p;
-		if ((p = cmd.find("=")) == string::npos) {
-			opts[cmd.substr(2)] = "true";
-		} else {
-			opts[cmd.substr(2, p-2)] = cmd.substr(p+1);
-		}
-
-		(*argc)--;
-		(*argv)++;
-	}
-
-	return opts;
-}
-
-/**
- * Put command line options into json config. If config element does not exist
- * or is of a different type then report an error.
- */
-static void process_options(const map<string, string> &opts) {
-	for (auto opt : opts) {
-		if (opt.first == "config") continue;
-
-		if (opt.first == "version") {
-			std::cout << "FTL Vision Node - v" << FTL_VERSION << std::endl;
-			std::cout << FTL_VERSION_LONG << std::endl;
-			exit(0);
-		}
-
-		try {
-			auto ptr = json::json_pointer("/"+opt.first);
-			// TODO(nick) Allow strings without quotes
-			auto v = json::parse(opt.second);
-			if (v.type() != config.at(ptr).type()) {
-				LOG(ERROR) << "Incorrect type for argument " << opt.first;
-				continue;
-			}
-			config.at(ptr) = v;
-		} catch(...) {
-			LOG(ERROR) << "Unrecognised option: " << opt.first;
-		}
-	}
-}
 
 static void run(const string &file) {
 	ctpl::thread_pool pool(2);
@@ -230,19 +160,11 @@ static void run(const string &file) {
 }
 
 int main(int argc, char **argv) {
-	argc--;
-	argv++;
-
-	// Process Arguments
-	auto options = read_options(&argv, &argc);
-	if (!findConfiguration(options["config"])) {
-		LOG(FATAL) << "Could not find any configuration!";
-	}
-	process_options(options);
+	auto paths = ftl::configure(argc, argv, "vision");
 
 	// Choose normal or middlebury modes
 	if (config["middlebury"]["dataset"] == "") {
-		run((argc > 0) ? argv[0] : "");
+		run((paths.size() > 0) ? paths[0] : "");
 	} else {
 		ftl::middlebury::test(config);
 	}