image: Refactor everything that isn't cli handling into manager class

The cli handling belongs in image/index.mjs, but everything that isn't 
cli handling belongs in a middleware class kinda thing. In this case, 
we've created a RenderManager class.
This commit is contained in:
Starbeamrainbowlabs 2021-01-18 19:14:03 +00:00
parent d94bed2f1b
commit 6ec176d895
Signed by: sbrl
GPG Key ID: 1BE5172E637709C2
6 changed files with 152 additions and 40 deletions

View File

@ -31,6 +31,34 @@ If you installed it locally, you'll need to do this:
path/to/node_modules/.bin/terrain50 --help 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 ## Read-world use
- I'm using it for the main Node.js application for my PhD in Computer Science! - I'm using it for the main Node.js application for my PhD in Computer Science!

View File

@ -12,4 +12,23 @@ function percentage(count_so_far, total, range=100) {
return (count_so_far/total)*range; 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 };

View File

@ -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;

View File

@ -24,9 +24,10 @@ class Terrain50Renderer {
* You probably want the .render() method, which returns a buffer * You probably want the .render() method, which returns a buffer
* containing a png-encoded image. * containing a png-encoded image.
* @param {Terrain50} terrain The Terrain50 object instance to render. * @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. * @return {ArrayBuffer} A canvas with the image rendered on it.
*/ */
async do_render(terrain) { async do_render(terrain, classes = null) {
let colour_domain = null; let colour_domain = null;
if(this.colour_domain === "auto") { if(this.colour_domain === "auto") {
let min = terrain.min_value, max = terrain.max_value; let min = terrain.min_value, max = terrain.max_value;
@ -103,14 +104,15 @@ class Terrain50Renderer {
* Renders the given Terrain50 object to an image. * Renders the given Terrain50 object to an image.
* Returns a buffer containing a PNG-encoded image, which is ready to be * Returns a buffer containing a PNG-encoded image, which is ready to be
* written to disk for example. * 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. * @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), let width = Math.floor(terrain.meta.ncols / this.scale_factor),
height = Math.floor(terrain.meta.nrows / 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")); return Buffer.from(encode(result, [ width, height ], "png"));
} }
} }

View File

@ -6,10 +6,9 @@ import path from 'path';
import l from '../../Helpers/Log.mjs'; import l from '../../Helpers/Log.mjs';
import a from '../../Helpers/Ansi.mjs'; import a from '../../Helpers/Ansi.mjs';
import Terrain50 from 'terrain50'; import RenderManager from './RenderManager.mjs';
import Terrain50Renderer from './Terrain50Renderer.mjs';
import { write_safe, end_safe } from '../../Helpers/StreamHelpers.mjs'; import bounds2classes from '../../Helpers/MathsHelpers.mjs';
export default async function(settings) { export default async function(settings) {
if(typeof settings.cli.input !== "string") { if(typeof settings.cli.input !== "string") {
@ -26,42 +25,34 @@ export default async function(settings) {
process.exit(1); 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(settings.cli.stream) {
if(!fs.existsSync(settings.cli.output)) await render_manager.render_many_filename(
await fs.promises.mkdir(settings.cli.output, { recursive: true, mode: 0o755 }); settings.cli.input,
settings.cli.output
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}`);
} }
else { else {
let terrain50 = Terrain50.Parse( await render_manager.render_one_filename(
await fs.promises.readFile(settings.cli.input, "utf-8") 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}`);
} }
} }

View File

@ -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." 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 default_value = false
type = "boolean" 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"