-
Notifications
You must be signed in to change notification settings - Fork 33
/
classify-images.ts
144 lines (128 loc) · 4.36 KB
/
classify-images.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/usr/bin/env node
/**
* This is an optimization for a common use case of localturk: classifying
* images. When you use this script, you can skip creating a CSV file of inputs
* and an HTML template.
*
* Usage:
*
* classify-images -o labels.csv --labels Yes,No,Maybe *.jpg
*
* This will present a web UI for classifying each image and put the output in
* labels.csv.
*/
import child_process from 'child_process';
import escape from 'escape-html';
import fs from 'fs';
import {Command} from 'commander';
import {dedent} from './utils';
import path from 'path';
const temp = require('temp').track();
function list(val: string) {
return val.split(',');
}
interface CLIArgs {
port?: number;
output: string;
labels: string[];
shortcuts: string[] | null;
max_width?: number;
randomOrder?: boolean;
}
const program = new Command();
program
.version('2.2.2')
.usage('[options] /path/to/images/*.jpg | images.txt')
.option('-p, --port <n>', 'Run on this port (default 4321)', parseInt)
.option('-o, --output <file>', 'Path to output CSV file (default output.csv)', 'output.csv')
.option('-l, --labels <csv>', 'Comma-separated list of choices of labels', list, ['Yes', 'No'])
.option(
'--shortcuts <a,b,c>',
'Comma-separated list of keyboard shortcuts for labels. Default is 1, 2, etc.',
list,
null,
)
.option(
'-w, --max_width <pixels>',
'Make the images this width when displaying in-browser',
parseInt,
)
.option(
'-r, --random-order',
'Serve images in random order, rather than sequentially. This is useful for ' +
'generating valid subsamples or for minimizing collisions during group localturking.',
)
.parse();
if (program.args.length == 0) {
console.error('You must specify at least one image file!\n');
program.help(); // exits
}
const options = program.opts<CLIArgs>();
let {shortcuts} = options;
if (!shortcuts) {
shortcuts = options.labels.map((_, idx) => (idx + 1).toString());
} else if (shortcuts.length !== options.labels.length) {
console.error('Number of shortcuts must match number of labels');
process.exit(1);
}
if (fs.existsSync(options.output)) {
console.warn(dedent`
Output file ${options.output} already exists.
Its contents will be assumed to be previously-generated labels.
If you want to start from scratch, either delete this file,
rename it or specify a different output via --output`);
}
const csvInfo = temp.openSync({suffix: '.csv'});
const templateInfo = temp.openSync({suffix: '.html'});
let staticDir: string | null = null;
let images = program.args;
if (images.length === 1 && images[0].endsWith('.txt')) {
fs.writeSync(csvInfo.fd, 'path\n');
fs.writeSync(csvInfo.fd, fs.readFileSync(images[0], 'utf8'));
} else {
const anyOutsideCwd = images.some(p => path.isAbsolute(p) || p.startsWith('..'));
if (anyOutsideCwd) {
staticDir = path.dirname(images[0]);
images = images.map(p => path.relative(staticDir!, p));
} else {
staticDir = '.';
}
fs.writeSync(csvInfo.fd, 'path\n' + images.join('\n') + '\n');
}
fs.closeSync(csvInfo.fd);
// Add keyboard shortcuts. 1=first button, etc.
const buttonsHtml = options.labels
.map((label, idx) => {
const buttonText = `${label} (${shortcuts[idx]})`;
return `<button type="submit" data-key='${shortcuts[idx]}' name="label" value="${label}">${escape(buttonText)}</button>`;
})
.join(' ');
const widthHtml = options.max_width ? ` width="${options.max_width}"` : '';
const undoHtml = dedent`
</form>
<form action="/delete-last" method="POST" style="display: inline-block">
<input type="submit" id="undo-button" data-key="z" value="Undo Last (z)">
</form>`;
let html = buttonsHtml + undoHtml + '\n<p><img src="${path}" ' + widthHtml + '></p>';
html += dedent`
<style>
form { display: inline-block; }
#undo-button { margin-left: 20px; }
</style>`;
fs.writeSync(templateInfo.fd, html);
fs.closeSync(templateInfo.fd);
const opts = [];
if (staticDir) {
opts.push('--static-dir', staticDir);
}
if (options.port) {
opts.push('--port', options.port.toString());
}
if (options.randomOrder) {
opts.push('--random-order');
}
const bin = ['localturk'];
// const bin = ['yarn', 'ts-node', 'localturk.ts'];
const args = [...bin, ...opts, templateInfo.path, csvInfo.path, options.output];
console.log('Running ', args.join(' '));
child_process.spawn(args[0], args.slice(1), {stdio: 'inherit'});