diff --git a/ybat.css b/ybat.css index 0125390..ab8d47d 100644 --- a/ybat.css +++ b/ybat.css @@ -11,9 +11,10 @@ hr { } .container { - display: block; + display: flex; + align-items: flex-start; position: absolute; - height: auto; + padding: 1rem 0; bottom: 0; top: 0; left: 0; @@ -36,8 +37,18 @@ hr { } .right { + width: calc(40% - 10px); + border-left: 1px solid gray; + display: block; + margin: 10px 0 10px calc(30% + 10px); + padding-left: 10px; +} + +.rightBboxes { + width: calc(30% - 10px); border-left: 1px solid gray; - margin: 10px 10px 10px calc(30% + 10px); + display: block; + margin: 10px 0 10px 10px; padding-left: 10px; } @@ -50,7 +61,7 @@ hr { width: 130px; } -#imageList, #classList, #description { +#imageList, #classList, #description, #currentBboxesList { width: 100%; margin-top: 10px; margin-bottom: 10px; diff --git a/ybat.html b/ybat.html index b9a6d05..23621f6 100644 --- a/ybat.html +++ b/ybat.html @@ -11,6 +11,11 @@ +
@@ -25,20 +30,26 @@ - +

- +

+ +
+ + +
+ @@ -80,8 +91,12 @@ + +
+ + +
\ No newline at end of file diff --git a/ybat.js b/ybat.js index 5a47816..61c0d17 100644 --- a/ybat.js +++ b/ybat.js @@ -5,6 +5,11 @@ // Parameters const saveInterval = 60 // Bbox recovery save in seconds const fontBaseSize = 30 // Text size in pixels + + // For internal use + + + const fontColor = "#001f3f" // Base font color const borderColor = "#001f3f" // Base bbox border color const backgroundColor = "rgba(0, 116, 217, 0.2)" // Base bbox fill color @@ -29,11 +34,14 @@ let classes = {} let bboxes = {} + let hiddenCanvas = {} + const extensions = ["jpg", "jpeg", "png", "JPG", "JPEG", "PNG"] let currentImage = null let currentClass = null let currentBbox = null + let currentBboxes = null let imageListIndex = 0 let classListIndex = 0 @@ -44,6 +52,9 @@ let screenX = 0 let screenY = 0 + // Lock/Unlock Bboxes + let lockBboxes = true + // Mouse container const mouse = { x: 0, @@ -85,6 +96,11 @@ alert("Restore function is not supported. If you need it, use Chrome or Firefox instead.") } + // Prevent accidental reloading + window.onbeforeunload = function(){ + return 'Are you sure you want to leave the page?'; + }; + // Start everything document.onreadystatechange = () => { if (document.readyState === "complete") { @@ -94,11 +110,13 @@ listenImageSelect() listenClassLoad() listenClassSelect() + listenBboxesSelect() listenBboxLoad() listenBboxSave() listenBboxVocSave() listenBboxCocoSave() listenBboxRestore() + listenLockBboxes() listenKeyboard() listenImageSearch() listenImageCrop() @@ -107,7 +125,6 @@ const listenCanvas = () => { canvas = new Canvas("canvas", document.getElementById("right").clientWidth, window.innerHeight - 20) - canvas.on("draw", (context) => { if (currentImage !== null) { drawImage(context) @@ -137,9 +154,22 @@ } const drawNewBbox = (context) => { - if (mouse.buttonL === true && currentClass !== null && currentBbox === null) { - const width = (mouse.realX - mouse.startRealX) - const height = (mouse.realY - mouse.startRealY) + if (mouse.buttonL === true && currentClass !== null && currentBbox === null && mouse.startRealX >= 0 && mouse.startRealY >= 0 && mouse.startRealX <= currentImage.width && mouse.startRealY <= currentImage.height) { + if (mouse.realX > currentImage.width) { + var width = (currentImage.width - mouse.startRealX) + } else if (mouse.realX < 0) { + var width = - mouse.startRealX + } else { + var width = (mouse.realX - mouse.startRealX) + } + + if (mouse.realY > currentImage.height) { + var height = (currentImage.height - mouse.startRealY) + } else if (mouse.realY < 0) { + var height = - mouse.startRealY + } else { + var height = (mouse.realY - mouse.startRealY) + } setBBoxStyles(context, true) context.strokeRect(zoomX(mouse.startRealX), zoomY(mouse.startRealY), zoom(width), zoom(height)) @@ -152,7 +182,7 @@ } const drawExistingBboxes = (context) => { - const currentBboxes = bboxes[currentImage.name] + currentBboxes = bboxes[currentImage.name] for (let className in currentBboxes) { currentBboxes[className].forEach(bbox => { @@ -282,39 +312,67 @@ } } else if (event.type === "mouseup" || event.type === "mouseout") { if (mouse.buttonL === true && currentImage !== null && currentClass !== null) { - const movedWidth = Math.max((mouse.startRealX - mouse.realX), (mouse.realX - mouse.startRealX)) - const movedHeight = Math.max((mouse.startRealY - mouse.realY), (mouse.realY - mouse.startRealY)) - - if (movedWidth > minBBoxWidth && movedHeight > minBBoxHeight) { // Only add if bbox is big enough - if (currentBbox === null) { // And only when no other bbox is selected - storeNewBbox(movedWidth, movedHeight) - } else { // Bbox was moved or resized - update original data - updateBboxAfterTransform() + if (mouse.startRealX >= 0 && mouse.startRealY >= 0 && mouse.startRealX <= currentImage.width && mouse.startRealY <= currentImage.height) { + if (mouse.realX > currentImage.width) { + var width = (currentImage.width - mouse.startRealX) + } else if (mouse.realX < 0) { + var width = - mouse.startRealX + } else { + var width = (mouse.realX - mouse.startRealX) + } + + if (mouse.realY > currentImage.height) { + var height = (currentImage.height - mouse.startRealY) + } else if (mouse.realY < 0) { + var height = - mouse.startRealY + } else { + var height = (mouse.realY - mouse.startRealY) } - } else { // (un)Mark a bbox - setBboxMarkedState() + + const movedWidth = Math.abs(width) + const movedHeight = Math.abs(height) + + if (movedWidth > minBBoxWidth && movedHeight > minBBoxHeight) { // Only add if bbox is big enough + if (currentBbox === null) { // And only when no other bbox is selected + storeNewBbox(movedWidth, movedHeight) + setCurrentBboxesList(currentImage.name) + } else { // Bbox was moved or resized - update original data + updateBboxAfterTransform() + + } + } else { // (un)Mark a bbox + setBboxMarkedState() + setCurrentBboxesList(currentImage.name) - if (currentBbox !== null) { // Bbox was moved or resized - update original data - updateBboxAfterTransform() + if (currentBbox !== null) { // Bbox was moved or resized - update original data + updateBboxAfterTransform() + } } } + setBboxMarkedState() + + if (currentBbox !== null) { // Bbox was moved or resized - update original data + updateBboxAfterTransform() + } } mouse.buttonR = false mouse.buttonL = false } - moveBbox() - resizeBbox() - changeCursorByLocation() + if (!lockBboxes) { + moveBbox() + resizeBbox() + } + changeCursorByLocation() panImage(xx, yy) } const storeNewBbox = (movedWidth, movedHeight) => { const bbox = { - x: Math.min(mouse.startRealX, mouse.realX), - y: Math.min(mouse.startRealY, mouse.realY), + x: Math.max(Math.min(mouse.startRealX, mouse.realX), 0), + y: Math.max(Math.min(mouse.startRealY, mouse.realY), 0), width: movedWidth, height: movedHeight, marked: true, @@ -364,11 +422,20 @@ currentBbox.originalWidth = currentBbox.bbox.width currentBbox.originalHeight = currentBbox.bbox.height currentBbox.moving = false + // Interactive labeling of existing bboxes + if (classList.length > 1) { + classList.options[classListIndex].selected = false + + classListIndex = classes[currentBbox.bbox.class] + + classList.options[classListIndex].selected = true + classList.selectedIndex = classListIndex + } } const setBboxMarkedState = () => { if (currentBbox === null || (currentBbox.moving === false && currentBbox.resizing === null)) { - const currentBboxes = bboxes[currentImage.name] + currentBboxes = bboxes[currentImage.name] let wasInside = false let smallestBbox = Number.MAX_SAFE_INTEGER @@ -424,14 +491,23 @@ } if (currentBbox.moving === true) { - currentBbox.bbox.x = currentBbox.originalX + (mouse.realX - mouse.startRealX) - currentBbox.bbox.y = currentBbox.originalY + (mouse.realY - mouse.startRealY) + const newXcand = currentBbox.originalX + (mouse.realX - mouse.startRealX) + const newYcand = currentBbox.originalY + (mouse.realY - mouse.startRealY) + if ( + (newXcand > 0) && + (newYcand > 0) && + currentBbox.bbox.width + newXcand <= currentImage.width && + currentBbox.bbox.height + newYcand <= currentImage.height + ) { + currentBbox.bbox.x = newXcand + currentBbox.bbox.y = newYcand + } } } } const resizeBbox = () => { - if (mouse.buttonL === true && currentBbox !== null) { + if (mouse.buttonL === true && currentBbox !== null && mouse.realX >= 0 && mouse.realX <= currentImage.width && mouse.realY >= 0 && mouse.realY <= currentImage.height) { const topLeftX = currentBbox.bbox.x const topLeftY = currentBbox.bbox.y const bottomLeftX = currentBbox.bbox.x @@ -486,7 +562,7 @@ const changeCursorByLocation = () => { if (currentImage !== null) { - const currentBboxes = bboxes[currentImage.name] + currentBboxes = bboxes[currentImage.name] for (let className in currentBboxes) { for (let i = 0; i < currentBboxes[className].length; i++) { @@ -716,6 +792,8 @@ imageListIndex = imageList.selectedIndex setCurrentImage(images[imageList.options[imageListIndex].innerHTML]) + const newImageName = images[imageList.options[imageListIndex].innerHTML].meta.name + setCurrentBboxesList(newImageName) }) } @@ -755,6 +833,7 @@ option.value = i option.innerHTML = rows[i] + option.label = String(i) + ': ' + rows[i] if (i === 0) { option.selected = true @@ -787,12 +866,93 @@ currentClass = null } + const setCurrentBboxesList = (imageName) => { + document.getElementById("currentBboxesList").innerHTML = "" + const currentBboxesList = document.getElementById("currentBboxesList") + + currentBboxes = bboxes[imageName] + + if (typeof currentBboxes !== 'undefined') { + for (let className in classes) { + if (typeof currentBboxes[className] !== "undefined") { + for (let i = 0; i < currentBboxes[className].length; i += 1){ + const option = document.createElement("option") + + option.innerHTML = className + option.label = className + ' n°' + String(i) + + if (currentBbox !== null && option.label == currentBbox.bbox.class + ' n°' + String(currentBbox.index)) { + option.selected = true + } + + currentBboxesList.appendChild(option) + } + } + } + } + } + + const listenBboxesSelect = () => { + const currentBboxesList = document.getElementById("currentBboxesList") + + currentBboxesList.addEventListener("click", () => { + if (typeof currentBboxesList.options[currentBboxesList.selectedIndex] !== 'undefined'){ + const label = currentBboxesList.options[currentBboxesList.selectedIndex].label + for (let className in classes) { + if (typeof currentBboxes[className] !== 'undefined') { + for (let i = 0; i < currentBboxes[className].length; i += 1){ + const bbox = currentBboxes[className][i] + + if (label == className + ' n°' + String(i)) { + if (currentBbox !== null) { + currentBbox.bbox.marked = false + } + currentBbox = { + bbox: bbox, + index: i, + originalX: bbox.x, + originalY: bbox.y, + originalWidth: bbox.width, + originalHeight: bbox.height, + moving: false, + resizing: null + } + currentBbox.bbox.marked = true + } + } + } + } + } + }) + } + const setCurrentClass = () => { const classList = document.getElementById("classList") currentClass = classList.options[classList.selectedIndex].text if (currentBbox !== null) { + currentBboxes = bboxes[currentImage.name] + var temp = currentBboxes[currentBbox.bbox.class][currentBbox.index] + + bboxes[currentImage.name][currentBbox.bbox.class].splice(currentBbox.index, 1); + bboxes[currentImage.name][currentBbox.bbox.class].filter(item => item !== undefined) + + const newClass = currentClass + temp.class = newClass + + if (typeof bboxes[currentImage.name][newClass] === "undefined") { + bboxes[currentImage.name][newClass] = Array(0) + } + + bboxes[currentImage.name][newClass].push(temp) + bboxes[currentImage.name][newClass].filter(item => item !== undefined) + + currentBbox.bbox = temp + currentBbox.index = bboxes[currentImage.name][newClass].length - 1 + + setCurrentBboxesList(currentImage.name) + currentBbox.bbox.marked = false // We unmark via reference currentBbox = null // and the we delete } @@ -801,10 +961,12 @@ const listenClassSelect = () => { const classList = document.getElementById("classList") - classList.addEventListener("change", () => { - classListIndex = classList.selectedIndex + classList.addEventListener("click", () => { + if (currentClass != null) { + classListIndex = classList.selectedIndex - setCurrentClass() + setCurrentClass() + } }) } @@ -850,12 +1012,17 @@ reader.readAsArrayBuffer(event.target.files[i]) } } + + setCurrentBboxesList(currentImage.name) } }) } const resetBboxes = () => { + document.getElementById("currentBboxesList").innerHTML = "" + bboxes = {} + currentBbox = null } const storeBbox = (filename, text) => { @@ -880,33 +1047,35 @@ if (extension === "txt") { const rows = text.split(/[\r\n]+/) - for (let i = 0; i < rows.length; i++) { - const cols = rows[i].split(" ") - - cols[0] = parseInt(cols[0]) - - for (let className in classes) { - if (classes[className] === cols[0]) { - if (typeof bbox[className] === "undefined") { - bbox[className] = [] + if (rows.length > 0) { + for (let i = 0; i < rows.length; i++) { + const cols = rows[i].split(" ") + + cols[0] = parseInt(cols[0]) + + for (let className in classes) { + if (classes[className] === cols[0]) { + if (typeof bbox[className] === "undefined") { + bbox[className] = [] + } + + // Reverse engineer actual position and dimensions from yolo format + const width = cols[3] * image.width + const x = cols[1] * image.width - width * 0.5 + const height = cols[4] * image.height + const y = cols[2] * image.height - height * 0.5 + + bbox[className].push({ + x: Math.floor(x), + y: Math.floor(y), + width: Math.floor(width), + height: Math.floor(height), + marked: false, + class: className + }) + + break } - - // Reverse engineer actual position and dimensions from yolo format - const width = cols[3] * image.width - const x = cols[1] * image.width - width * 0.5 - const height = cols[4] * image.height - const y = cols[2] * image.height - height * 0.5 - - bbox[className].push({ - x: Math.floor(x), - y: Math.floor(y), - width: Math.floor(width), - height: Math.floor(height), - marked: false, - class: className - }) - - break } } } @@ -915,33 +1084,48 @@ const xmlDoc = parser.parseFromString(text, "text/xml") const objects = xmlDoc.getElementsByTagName("object") - - for (let i = 0; i < objects.length; i++) { - const objectName = objects[i].getElementsByTagName("name")[0].childNodes[0].nodeValue - - for (let className in classes) { - if (className === objectName) { - if (typeof bbox[className] === "undefined") { - bbox[className] = [] + if (rows.length > 0) { + const currentBboxesList = document.getElementById("currentBboxesList") + for (let i = 0; i < objects.length; i++) { + const objectName = objects[i].getElementsByTagName("name")[0].childNodes[0].nodeValue + + for (let className in classes) { + if (className === objectName) { + if (typeof bbox[className] === "undefined") { + bbox[className] = [] + } + + const bndBox = objects[i].getElementsByTagName("bndbox")[0] + + const bndBoxX = bndBox.getElementsByTagName("xmin")[0].childNodes[0].nodeValue + const bndBoxY = bndBox.getElementsByTagName("ymin")[0].childNodes[0].nodeValue + const bndBoxMaxX = bndBox.getElementsByTagName("xmax")[0].childNodes[0].nodeValue + const bndBoxMaxY = bndBox.getElementsByTagName("ymax")[0].childNodes[0].nodeValue + + bbox[className].push({ + x: parseInt(bndBoxX), + y: parseInt(bndBoxY), + width: parseInt(bndBoxMaxX) - parseInt(bndBoxX), + height: parseInt(bndBoxMaxY) - parseInt(bndBoxY), + marked: false, + class: className + }) + + const option = document.createElement("option") + + const indx = bboxes[currentImage.name][className].length - 1 + option.innerHTML = className + option.label = className + ' n°' + String(indx) + option.value = {className, indx} + + if (i === 0) { + option.selected = false + } + + currentBboxesList.appendChild(option) + + break } - - const bndBox = objects[i].getElementsByTagName("bndbox")[0] - - const bndBoxX = bndBox.getElementsByTagName("xmin")[0].childNodes[0].nodeValue - const bndBoxY = bndBox.getElementsByTagName("ymin")[0].childNodes[0].nodeValue - const bndBoxMaxX = bndBox.getElementsByTagName("xmax")[0].childNodes[0].nodeValue - const bndBoxMaxY = bndBox.getElementsByTagName("ymax")[0].childNodes[0].nodeValue - - bbox[className].push({ - x: parseInt(bndBoxX), - y: parseInt(bndBoxY), - width: parseInt(bndBoxMaxX) - parseInt(bndBoxX), - height: parseInt(bndBoxMaxY) - parseInt(bndBoxY), - marked: false, - class: className - }) - - break } } } @@ -951,6 +1135,8 @@ } else { const json = JSON.parse(text) + const currentBboxesList = document.getElementById("currentBboxesList") + for (let i = 0; i < json.annotations.length; i++) { let imageName = null let categoryName = null @@ -1001,6 +1187,19 @@ class: className }) + const option = document.createElement("option") + + const indx = bboxes[currentImage.name][className].length - 1 + option.innerHTML = className + option.label = className + ' n°' + String(indx) + option.value = {className, indx} + + if (i === 0) { + option.selected = false + } + + currentBboxesList.appendChild(option) + break } } @@ -1148,12 +1347,12 @@ for (let i = 0; i < bboxes[imageName][className].length; i++) { const bbox = bboxes[imageName][className][i] - const segmentation = [ + const segmentation = [[ bbox.x, bbox.y, bbox.x, bbox.y + bbox.height, bbox.x + bbox.width, bbox.y + bbox.height, bbox.x + bbox.width, bbox.y - ] + ]] result.annotations.push({ segmentation: segmentation, @@ -1188,6 +1387,16 @@ }) } + const listenLockBboxes = () => { + document.getElementById("lockBboxes").addEventListener("change", (event) => { + if (event.currentTarget.checked) { + lockBboxes = true; + } else { + lockBboxes = false; + } + }) + } + const listenKeyboard = () => { const imageList = document.getElementById("imageList") const classList = document.getElementById("classList") @@ -1200,9 +1409,10 @@ bboxes[currentImage.name][currentBbox.bbox.class].splice(currentBbox.index, 1) currentBbox = null + setCurrentBboxesList(currentImage.name) + document.body.style.cursor = "default" } - event.preventDefault() } @@ -1223,6 +1433,8 @@ document.body.style.cursor = "default" } + const newImageName = images[imageList.options[imageListIndex].innerHTML].meta.name + setCurrentBboxesList(newImageName) event.preventDefault() } @@ -1244,6 +1456,8 @@ document.body.style.cursor = "default" } + const newImageName = images[imageList.options[imageListIndex].innerHTML].meta.name + setCurrentBboxesList(newImageName) event.preventDefault() } @@ -1285,6 +1499,58 @@ event.preventDefault() } + + var class_keys = Array(10).fill().map((d, i) => i + 48) + class_keys = class_keys.map(String) + + var keyMap = {} + var i = 0 + for (const classe_key in class_keys) { + keyMap[classe_key] = i + i+=1 + } + + if ((event.shiftKey || event.key in class_keys) && currentBbox !== null) { + if (event.code.startsWith('Digit') && keyMap[event.key] in Object.values(classes)) { + currentBboxes = bboxes[currentImage.name] + var temp = currentBboxes[currentBbox.bbox.class][currentBbox.index] + + bboxes[currentImage.name][currentBbox.bbox.class].splice(currentBbox.index, 1); + bboxes[currentImage.name][currentBbox.bbox.class].filter(item => item !== undefined) + + const newClassInd = keyMap[event.key] + const newClass = Object.keys(classes).find(k => classes[k] === newClassInd); + temp.class = newClass + + if (typeof bboxes[currentImage.name][newClass] === "undefined") { + bboxes[currentImage.name][newClass] = Array(0) + } + + bboxes[currentImage.name][newClass].push(temp) + bboxes[currentImage.name][newClass].filter(item => item !== undefined) + + currentBbox.bbox = temp + currentBbox.index = bboxes[currentImage.name][newClass].length - 1 + + if (classList.length > 1) { + classList.options[classListIndex].selected = false + + classListIndex = classes[currentBbox.bbox.class] + + classList.options[classListIndex].selected = true + classList.selectedIndex = classListIndex + } + + currentClass = currentBbox.bbox.class + + setCurrentBboxesList(currentImage.name) + + currentBbox.bbox.marked = false + currentBbox = null + + } + } + }) } @@ -1314,7 +1580,7 @@ document.getElementById("imageList").selectedIndex = images[imageName].index setCurrentImage(images[imageName]) - + setCurrentBboxesList(imageName) break } }