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
```
### 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!

View File

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

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
* 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;
@ -104,13 +105,14 @@ class Terrain50Renderer {
* 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 {[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"));
}
}

View File

@ -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)
await render_manager.render_many_filename(
settings.cli.input,
settings.cli.output
);
i++;
}
l.log(`Written ${a.hicol}${i}${a.reset} items to ${a.hicol}${a.fgreen}${settings.cli.output}${a.reset}`);
}
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}`);
}
}

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."
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"