diff --git a/README.md b/README.md index cd49b96..4c80a8d 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ classification. The example above could be written as: This will bring up a web server with a UI for assigning one of those two labels to each image on your local file system. The results will go in `output.csv`. +Alternatively, you can also pass in a text file containing the paths to images. +In this case, the images can be URLs, rather than paths to local files. + For more details, run `classify-images --help`. ## Tips & Tricks diff --git a/classify-images.ts b/classify-images.ts index 8406d08..49ae04b 100644 --- a/classify-images.ts +++ b/classify-images.ts @@ -32,26 +32,28 @@ interface CLIArgs { max_width?: number; } -const program = new Command() as (Command & CLIArgs); +const program = new Command(); program .version('2.1.1') - .usage('[options] /path/to/images/*.jpg') + .usage('[options] /path/to/images/*.jpg | images.txt') .option('-o, --output ', 'Path to output CSV file (default output.csv)', 'output.csv') .option('-l, --labels ', 'Comma-separated list of choices of labels', list, ['Yes', 'No']) .option('-w, --max_width ', 'Make the images this width when displaying in-browser', parseInt) - .parse(process.argv) + .parse() if (program.args.length == 0) { console.error('You must specify at least one image file!\n'); program.help(); // exits } +const options = program.opts(); +console.log(options.labels); -if (fs.existsSync(program.output)) { +if (fs.existsSync(options.output)) { console.warn(dedent` - Output file ${program.output} already exists. + 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`); @@ -60,16 +62,22 @@ if (fs.existsSync(program.output)) { const csvInfo = temp.openSync({suffix: '.csv'}); const templateInfo = temp.openSync({suffix: '.html'}); -fs.writeSync(csvInfo.fd, 'path\n' + program.args.join('\n') + '\n'); +const 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 { + fs.writeSync(csvInfo.fd, 'path\n' + images.join('\n') + '\n'); +} fs.closeSync(csvInfo.fd); // Add keyboard shortcuts. 1=first button, etc. -const buttonsHtml = (program.labels as string[]).map((label, idx) => { +const buttonsHtml = options.labels.map((label, idx) => { const buttonText = `${label} (${1 + idx})`; return ``; }).join(' '); -const widthHtml = program.max_width ? ` width="${program.max_width}"` : ''; +const widthHtml = options.max_width ? ` width="${options.max_width}"` : ''; const undoHtml = dedent`
@@ -86,6 +94,6 @@ html += dedent` fs.writeSync(templateInfo.fd, html); fs.closeSync(templateInfo.fd); -const args = ['localturk', '--static-dir', '.', templateInfo.path, csvInfo.path, program.output]; +const args = ['localturk', '--static-dir', '.', templateInfo.path, csvInfo.path, options.output]; console.log('Running ', args.join(' ')); child_process.spawn(args[0], args.slice(1), {stdio: 'inherit'}); diff --git a/localturk.ts b/localturk.ts index 498fca7..17d72f0 100644 --- a/localturk.ts +++ b/localturk.ts @@ -41,7 +41,7 @@ interface CLIArgs { writeTemplate: boolean; } -const program = new Command() as (Command & CLIArgs); +const program = new Command(); program .version('2.1.1') @@ -54,9 +54,12 @@ program 'Serve images in random order, rather than sequentially. This is useful for ' + 'generating valid subsamples or for minimizing collisions during group localturking.') .option('-w, --write-template', 'Generate a stub template file based on the input CSV.') - .parse(process.argv); + .parse(); -const {args, randomOrder, writeTemplate} = program; +const options = program.opts(); + +const {args} = program; +const {randomOrder, writeTemplate} = options; if (!((3 === args.length && !writeTemplate) || (1 === args.length && writeTemplate))) { program.help(); @@ -68,10 +71,10 @@ if (writeTemplate) { } const [templateFile, tasksFile, outputsFile] = args; -const port = program.port || 4321; +const port = options.port || 4321; // --static-dir is particularly useful for classify-images, where the template file is in a // temporary directory but the image files could be anywhere. -const staticDir = program['staticDir'] || path.dirname(templateFile); +const staticDir = options['staticDir'] || path.dirname(templateFile); type Task = {[key: string]: string}; let flash = ''; // this is used to show warnings in the web UI. @@ -85,7 +88,7 @@ async function renderTemplate({task, numCompleted, numTotal}: TaskStats) { // Note: these two fields are not available in mechanical turk. fullDict['ALL_JSON'] = utils.htmlEntities(JSON.stringify(task, null, 2)); fullDict['ALL_JSON_RAW'] = JSON.stringify(task); - for (var [k, v] of Object.entries(program.var)) { + for (var [k, v] of Object.entries(options.var)) { fullDict[k] = utils.htmlEntities(v as string); } const userHtml = utils.renderTemplate(template, fullDict); diff --git a/sample/image-urls.txt b/sample/image-urls.txt new file mode 100644 index 0000000..1948b10 --- /dev/null +++ b/sample/image-urls.txt @@ -0,0 +1,3 @@ +https://upload.wikimedia.org/wikipedia/commons/thumb/8/85/Tour_Eiffel_Wikimedia_Commons_%28cropped%29.jpg/432px-Tour_Eiffel_Wikimedia_Commons_%28cropped%29.jpg +https://upload.wikimedia.org/wikipedia/commons/d/d6/Liberty_and_Ellis_Island.jpg +https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Shea_Stadium_%282009%29.jpg/1024px-Shea_Stadium_%282009%29.jpg