diff --git a/README.md b/README.md
index 7226430..97f6b2a 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,34 @@ If you installed it locally, you'll need to do this:
 path/to/node_modules/.bin/terrain50 --help
 ```
 
+### Environment Variables
+Additionally, a number of environment variables are supported.
+
+Variable    | Purpose
+------------|-----------------------------
+`NO_COLOR`  | Disables ANSI escape codes in output (i.e. coloured output). Not recommended unless you have a reason.
+`QUIET`     | Suppress all output except for warnings and errors (not fully supported everywhere yet)
+
+
+## Notes
+
+### `image` subcommand: `--boundaries` argument
+This argument's purpose is the divide the incoming data into categories so that an AI can be potentially trained on the data (e.g. water depth data, as I'm using). It takes a comma separated list of values like this:
+
+```
+0.1,0.5,1,5
+```
+
+...and turns it into a number of bins like so:
+
+ - -Infinity ≤ value < 0.1
+ - 0.1 ≤ value < 0.5
+ - 0.5 ≤ value < 1
+ - 1 ≤ value < 5
+ - 5 ≤ value < Infinity
+
+Each bin is assigned a colour. Then, for each value in the input, it draws the colour that's assigned to the bin that the value fits into.
+
 
 ## Read-world use
  - I'm using it for the main Node.js application for my PhD in Computer Science!
diff --git a/src/Helpers/MathsHelpers.mjs b/src/Helpers/MathsHelpers.mjs
index 8b9dd1d..1e21a52 100644
--- a/src/Helpers/MathsHelpers.mjs
+++ b/src/Helpers/MathsHelpers.mjs
@@ -12,4 +12,23 @@ function percentage(count_so_far, total, range=100) {
 	return (count_so_far/total)*range;
 }
 
-export { percentage };
+/**
+ * Converts the given array of boundaries to an array of class objects.
+ * @source PhD-Code - common/ai/ModelHelper.mjs
+ * @param	{Number[]}	bounds			The bounds to convert.
+ * @return	{{min:Number,max:Number}[]}	An array of objects that representa single class each, with min / max values for each class.
+ */
+function bounds2classes(bounds) {
+	if(bounds.length < 2)
+		throw new Exception(`Error: Not enough bounds supplied (got ${bounds.length}, but expected at least 2)`);
+	
+	let result = [];
+	result.push({ min: -Infinity, max: bounds[0] });
+	for(let i = 1; i < bounds.length; i++) {
+		result.push({ min: bounds[i - 1], max: bounds[i] });
+	}
+	result.push({ min: bounds[bounds.length - 1], max: Infinity });
+	return result;
+}
+
+export { percentage, bounds2classes };
diff --git a/src/Subcommands/image/RenderManager.mjs b/src/Subcommands/image/RenderManager.mjs
new file mode 100644
index 0000000..1f67856
--- /dev/null
+++ b/src/Subcommands/image/RenderManager.mjs
@@ -0,0 +1,67 @@
+"use strict";
+
+import fs from 'fs';
+import path from 'path';
+
+import Terrain50 from 'terrain50';
+
+import l from '../../Helpers/Log.mjs';
+import a from '../../Helpers/Ansi.mjs';
+import Terrain50Renderer from './Terrain50Renderer.mjs';
+
+
+class RenderManager {
+	constructor(scale_factor = 1, in_tolerant = false, in_classes = null) {
+		this.quiet = typeof process.env.QUIET == "string" ? true : false;
+		
+		this.tolerant = in_tolerant;
+		this.classes = in_classes;
+		
+		this.renderer = new Terrain50Renderer(scale_factor);
+	}
+	
+	async render_one_filename(filename_in, filename_out) {
+		let terrain50 = Terrain50.Parse(
+			await fs.promises.readFile(filename_in, "utf-8")
+		);
+		let png_buffer = await this.renderer.render(terrain50, this.classes);
+		if(!(png_buffer instanceof Buffer))
+			throw new Error(`Error: Renderer did not return Buffer (found unexpected ${png_buffer} instead)`);
+		await fs.promises.writeFile(
+			filename_out,
+			png_buffer
+		);
+		
+		if(!this.quiet) l.log(`Written to ${a.hicol}${filename_out}${a.reset}`);
+	}
+	
+	async render_many_filename(filename_in, dir_out) {
+		let reader = process.stdin;
+		if(filename_in !== "-")
+			reader = fs.createReadStream(filename_in, "utf-8");
+		
+		await this.render_many_stream(reader, dir_out);
+	}
+	
+	async render_many_stream(stream_in, dir_out) {
+		if(!fs.existsSync(dir_out))
+			await fs.promises.mkdir(dir_out, { recursive: true, mode: 0o755 });
+		
+		let i = 0;
+		for await(let next of Terrain50.ParseStream(stream_in, this.tolerant ? /\s+/ : " ")) {
+			if(!this.quiet) process.stderr.write(`${a.fgreen}>>>>> ${a.hicol} Item ${i} ${a.reset}${a.fgreen} <<<<<${a.reset}\n`);
+			
+			await fs.promises.writeFile(
+				path.join(dir_out, `${i}.png`),
+				await renderer.render(next, this.classes)
+			);
+			
+			i++;
+		}
+		
+		if(!this.quiet) l.log(`Written ${a.hicol}${i}${a.reset} items to ${a.hicol}${a.fgreen}${dir_out}${a.reset}`);
+	}
+	
+}
+
+export default RenderManager;
diff --git a/src/Subcommands/image/Terrain50Renderer.mjs b/src/Subcommands/image/Terrain50Renderer.mjs
index 4798bb1..0b07266 100644
--- a/src/Subcommands/image/Terrain50Renderer.mjs
+++ b/src/Subcommands/image/Terrain50Renderer.mjs
@@ -24,9 +24,10 @@ class Terrain50Renderer {
 	 * You probably want the .render() method, which returns a buffer 
 	 * containing a png-encoded image.
 	 * @param	{Terrain50}	terrain	The Terrain50 object instance to render.
+	 * @param	{[number, number][]}	classes	The classes to bin the values into. If not specified, values are not binned into classes. Warning: Values *must* fit into a bin. It is recommended to use -Infinity and Infinity in the first and last bins.
 	 * @return	{ArrayBuffer}	A canvas with the image rendered on it.
 	 */
-	async do_render(terrain) {
+	async do_render(terrain, classes = null) {
 		let colour_domain = null;
 		if(this.colour_domain === "auto") {
 			let min = terrain.min_value, max = terrain.max_value;
@@ -103,14 +104,15 @@ class Terrain50Renderer {
 	 * Renders the given Terrain50 object to an image.
 	 * Returns a buffer containing a PNG-encoded image, which is ready to be
 	 * written to disk for example.
-	 * @param	{Terrain50}	terrain	The terrain object to render.
+	 * @param	{Terrain50}				terrain	The terrain object to render.
+	 * @param	{[number, number][]}	classes	The classes to bin the values into. If not specified, values are not binned into classes. Warning: Values *must* fit into a bin. It is recommended to use -Infinity and Infinity in the first and last bins.
 	 * @return	{Buffer}	The terrain object as a png, represented as a buffer.
 	 */
-	async render(terrain) {
+	async render(terrain, classes = null) {
 		let width = Math.floor(terrain.meta.ncols / this.scale_factor),
 			height = Math.floor(terrain.meta.nrows / this.scale_factor);
 		
-		let result = await this.do_render(terrain);
+		let result = await this.do_render(terrain, classes);
 		return Buffer.from(encode(result, [ width, height ], "png"));
 	}
 }
diff --git a/src/Subcommands/image/index.mjs b/src/Subcommands/image/index.mjs
index 91faf51..d9aee01 100644
--- a/src/Subcommands/image/index.mjs
+++ b/src/Subcommands/image/index.mjs
@@ -6,10 +6,9 @@ import path from 'path';
 import l from '../../Helpers/Log.mjs';
 import a from '../../Helpers/Ansi.mjs';
 
-import Terrain50 from 'terrain50';
-import Terrain50Renderer from './Terrain50Renderer.mjs';
+import RenderManager from './RenderManager.mjs';
 
-import { write_safe, end_safe } from '../../Helpers/StreamHelpers.mjs';
+import bounds2classes from '../../Helpers/MathsHelpers.mjs';
 
 export default async function(settings) {
 	if(typeof settings.cli.input !== "string") {
@@ -26,42 +25,34 @@ export default async function(settings) {
 		process.exit(1);
 	}
 	
-	let renderer = new Terrain50Renderer(settings.scale_factor | 1);
+	// Parse the bounaries out
+	if(typeof settings.cli.boundaries == "string") {
+		settings.cli.boundaries = settings.cli.boundaries
+			.split(",")
+			.map((value) => parseFloat(value.trim()))
+			.filter((x) => typeof x == "number");
+		
+		settings.classes = bounds2classes(settings.cli.boundaries);
+	}
+	else
+		settings.classes = null;
+	
+	let render_manager = new RenderManager(
+		settings.scale_factor || 1,
+		settings.cli.tolerant,
+		settings.classes
+	);
 	
 	if(settings.cli.stream) {
-		if(!fs.existsSync(settings.cli.output))
-			await fs.promises.mkdir(settings.cli.output, { recursive: true, mode: 0o755 });
-		
-		let reader = process.stdin;
-		if(settings.cli.input !== "-")
-			reader = fs.createReadStream(settings.cli.input, "utf-8");
-		
-		let i = 0;
-		for await(let next of Terrain50.ParseStream(reader, settings.cli.tolerant ? /\s+/ : " ")) {
-			process.stderr.write(`${a.fgreen}>>>>> ${a.hicol} Item ${i} ${a.reset}${a.fgreen} <<<<<${a.reset}\n`);
-			
-			await fs.promises.writeFile(
-				path.join(settings.cli.output, `${i}.png`),
-				await renderer.render(next)
-			);
-			
-			i++;
-		}
-		
-		l.log(`Written ${a.hicol}${i}${a.reset} items to ${a.hicol}${a.fgreen}${settings.cli.output}${a.reset}`);
+		await render_manager.render_many_filename(
+			settings.cli.input,
+			settings.cli.output
+		);
 	}
 	else {
-		let terrain50 = Terrain50.Parse(
-			await fs.promises.readFile(settings.cli.input, "utf-8")
+		await render_manager.render_one_filename(
+			settings.cli.input,
+			settings.cli.output
 		);
-		let png_buffer = await renderer.render(terrain50);
-		if(!(png_buffer instanceof Buffer))
-			throw new Error(`Error: Renderer did not return Buffer (found unexpected ${png_buffer} instead)`);
-		await fs.promises.writeFile(
-			settings.cli.output,
-			png_buffer
-		);
-		
-		l.log(`Written to ${a.hicol}${settings.cli.output}${a.reset}`);
 	}
 }
diff --git a/src/Subcommands/image/meta.toml b/src/Subcommands/image/meta.toml
index 2738426..896792d 100644
--- a/src/Subcommands/image/meta.toml
+++ b/src/Subcommands/image/meta.toml
@@ -17,3 +17,8 @@ name = "stream"
 description = "Treat the input as a stream of Terrain50 objects. The path specified by the --output argument will be a directory containing all the extracted objects, 1 image file per object."
 default_value = false
 type = "boolean"
+
+[[arguments]]
+name = "boundaries"
+description = "Takes a comma-separated list of numbers. If specified, values will be binned into categories that match the given boundaries. See the note in the README for more information."
+type = "string"
