I've started working on a data annotation tool to do segmentation on images or from a video file.
I decided to use Electron because I kind of like designing GUI with HTML/CSS and that it's very easy to set up.
Basically, the application as a limited set of functionality for now :
- Select an input folder, video file or a single image;
- Select an output folder, that isn't used yet;
- Save the initial configuration so I don't have to redo it every time I reload the app;
- Load images from a folder and load them in a canvas, where we can navigate between each images with previous/next buttons
- Call a python script that parses a video and returns a path to an image file representing the current video, where we can also navigate between frames with previous/next (This might be a tad sketchy, but I don't mind as it works very well. I also don't want this reviewed, you may consider it as an external library).
This is the first part of the application, where I only deal with loading images and saving configuration, as you may see.
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval';" />
<style>
label {
display:block;
padding:5px 0px;
}
.hidden {
display:none;
}
.closed-pane {
display:none;
}
</style>
</head>
<body>
<div>
<h2>Configuration</h2>
<button class="toggle-pane configuration-pane-toggle">Toggle</button>
<div class="configuration-pane">
<ol>
<li>Select which folder or file you want to load. You may load either a folder with images, a single image or a video file that will be parsed;</li>
<li>Specify where you want the annotations to be saved. Note that if there already are annotations in this folder, they will be loaded;</li>
<li>If loading a video, specify if you want to skip some frames as the annotation may get really long;</li>
<li>If you don't want to redo this everytime you open the app, click "Save Config", a json file will be saved beside index.html</li>
<li>Click Start and get started!</li>
</ol>
<label> Input : <input type="text" class="configuration-input"/> <button class="configuration-select-input">Select</button> </label>
<label> Output : <input type="text" class="configuration-output"/> <button class="configuration-select-output">Select</button> </label>
<label> Skip n frames : <input type="number" class="configuraiton-video-skip"/> </label>
<button class="configuration-start">Start</button>
<button class="configuration-save-config">Save config</button>
</div>
</div>
<div>
<h2>Annotation</h2>
<div>
Explanations :
<ol>
<li>Press 1 to use the line annotator. You only need to click the beginning and the end of the line to create a segment. Then, use to scroll up/down to make the line bigger or smaller</li>
</ol>
</div>
<div>
<div class="navigation-pane">
<button class="navigation-save-progress">Save</button>
<button class="navigation-previous">Previous</button>
<button class="navigation-next">Next</button>
</div>
<img id="current-image" class="hidden" width="1280" height="720" src=""/>
<div class="canvas-container" style="position: relative;">
<canvas id="image-canvas" width="1280" height="720" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
<canvas id="writer-canvas" width="1280" height="720" style="position: absolute; left: 0; top: 0; z-index: 999;"></canvas>
</div>
</div>
</div>
</body>
<script src="config-io.js"></script>
</html>
JavaScript
main.js (Electron's entry point)
const { app, BrowserWindow } = require('electron');
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
});
win.setFullScreen(true);
win.loadFile('src/index.html')
win.webContents.openDevTools()
}
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
config-io.js
const fs = require('fs');
const {dialog} = require('electron').remote;
const path = require("path");
const spawn = require("child_process").spawn;
const readline = require('readline');
const constants = Object.freeze({
imageExtensions : ["png", "jpg", "jpeg"],
videoExtensions : ["mp4"]
});
function isImage(f) {
const ext = f.substring(f.lastIndexOf(".")+1);
return constants.imageExtensions.includes(ext);
}
const html = Object.freeze({
txtInput : document.getElementsByClassName("configuration-input")[0],
btnChooseInput : document.getElementsByClassName("configuration-select-input")[0],
btnChooseOutput : document.getElementsByClassName("configuration-select-output")[0],
txtOutput : document.getElementsByClassName("configuration-output")[0],
numSkipFrame : document.getElementsByClassName("configuraiton-video-skip")[0],
btnSaveConfig : document.getElementsByClassName("configuration-save-config")[0],
btnStart : document.getElementsByClassName("configuration-start")[0],
btnPrevious : document.getElementsByClassName("navigation-previous")[0],
btnNext : document.getElementsByClassName("navigation-next")[0],
imgCurrent : document.getElementById("current-image"),
cvsCurrentImg : document.getElementById("image-canvas")
});
const currentImageControler = function() {
function init() {
const file = html.txtInput.value;
const fileName = file.substring(file.lastIndexOf(path.sep)+1);
const isFile = fileName.lastIndexOf(".") != -1;
if (isFile) {
const extension = file.substring(file.lastIndexOf(".")+1);
if (constants.videoExtensions.includes(extension)) {
return fromVideo();
} else if (constants.imageExtensions.includes(extension)) {
return fromFile();
} else {
console.log("File not recognized");
return;
}
}
console.log("folder");
return fromFolder();
}
const fromVideo = function() {
const PythonFilePath = "Path to a script";
let pythonProcess;
let currentValue;
const initialize = function() {
pythonProcess = spawn('python3',[PythonFilePath, html.txtInput.value, html.txtOutput.value, html.numSkipFrame.value ?? 1]);
pythonProcess.stderr.pipe(process.stderr);
readline.createInterface({
input: pythonProcess.stdout,
terminal: false
}).on('line', function(line) {
currentValue = line;
html.imgCurrent.src = currentValue;
});
html.btnNext.onclick = next;
html.btnPrevious.onclick = previous;
document.onclose = (_) => stop().bind(this);
}
function stop() {
pythonProcess.kill();
}
function previous(e) {
pythonProcess.stdin.write("back\n");
}
function next(e) {
pythonProcess.stdin.write("next\n");
}
initialize();
};
const fromFolder = function() {
let currentIndex;
let files;
const initialize = function() {
fs.readdir(html.txtInput.value, {encoding : "utf8"}, function(err, res) {
files = res.filter(isImage).map(x => html.txtInput.value + path.sep + x);
console.log(files);
currentIndex = 0;
html.imgCurrent.src = files[currentIndex];
html.btnNext.onclick = next.bind(this);
html.btnPrevious.onclick = previous.bind(this);
});
}
function next() {
if (currentIndex >= files.length - 1) return;
currentIndex++;
update();
}
function previous() {
if (currentIndex == 0) return;
currentIndex--;
update();
}
const update = function() {
html.imgCurrent.src = files[currentIndex];
}
initialize();
};
const fromFile = function() {
let currentValue;
const initialize = function() {
currentValue = html.txtInput.value;
html.imgCurrent.src = currentValue;
}
function current() {
return currentValue;
}
initialize();
}
init();
}
window.onload = function() {
if (!fs.existsSync("config.json")) return;
const result = fs.readFileSync("config.json", {encoding: "utf-8"});
const config = JSON.parse(result);
html.txtInput.value = config.input;
html.txtOutput.value = config.output;
html.numSkipFrame.value = config.skip_n;
}
html.imgCurrent.onload = function() {
var ctx = html.cvsCurrentImg.getContext("2d");
ctx.drawImage(html.imgCurrent, 10, 10);
};
html.btnChooseInput.onclick = function() {
dialog.showOpenDialog({properties: ['openDirectory', 'openFile']}).then(function (response) {
if (response.canceled) return;
html.txtInput.value = response.filePaths[0];
});
};
html.btnChooseOutput.onclick = function() {
dialog.showOpenDialog({properties: ['openDirectory', 'createDirectory'] }).then(function (response) {
if (response.canceled) return;
html.txtOutput.value = response.filePaths[0];
});
};
html.btnStart.onclick = function() {
currentImageControler();
document.getElementsByClassName("configuration-pane")[0].classList.toggle("closed-pane");
};
html.btnSaveConfig.onclick = function() {
const config = {
input : html.txtInput.value,
output : html.txtOutput.value,
skip_n : html.numSkipFrame.value
};
const jsonConfig = JSON.stringify(config);
fs.writeFile("config.json", jsonConfig, ["utf-8"], () => alert("saved"));
};
Array.from(document.getElementsByClassName("toggle-pane")).forEach(function(element) {
element.addEventListener('click', function(e) { e.target.nextElementSibling.classList.toggle("closed-pane"); });
});
The whole thing works, but I'm pretty bad when it comes to structuring Javascript code. I come from an object oriented background, but I've been told countless times that while Javascript supports objects, it's not exactly the best paradigm to deal with what Javascript is.
So, I would like some feedback on the way my code is structured, I don't like how everything is laid out, I feel like it's messy.