Last active
July 14, 2020 13:10
-
-
Save dodijk/79a437ba36f5151e4c9e558f43812b22 to your computer and use it in GitHub Desktop.
Adaptation of https://github.com/Labelbox/labelbox/tree/master/custom-interfaces/video-comparison for images.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link | |
rel="stylesheet" | |
href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" | |
/> | |
<link | |
href="https://fonts.googleapis.com/icon?family=Material+Icons" | |
rel="stylesheet" | |
/> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> | |
<script src="https://api.labelbox.com/static/labeling-api.js"></script> | |
<style> | |
.app { | |
height: 100vh; | |
} | |
.flex-column { | |
flex-direction: column; | |
} | |
.flex-grow { | |
display: flex; | |
flex-grow: 1; | |
} | |
.content { | |
margin: 0px 20px; | |
} | |
.asset { | |
max-width: 40%; | |
max-height: 40%; | |
} | |
.sidebar { | |
display: flex; | |
height: 100vh; | |
border-right: 1px solid #e8e8e8; | |
width: 300px; | |
min-width: 300px; | |
} | |
.text { | |
background-color: #ececec; | |
padding: 30px; | |
font-size: 20px; | |
overflow: auto; | |
font-style: italic; | |
} | |
.navbar { | |
padding: 10px; | |
border-bottom: 1px solid #e8e8e8; | |
} | |
.btn { | |
background-color: aqua | |
} | |
.btn:visited { | |
background-color: yellow | |
} | |
.btn:hover { | |
background-color: skyblue | |
} | |
.btn:active { | |
background-color: pink; | |
} | |
.btn:focus { | |
background-color: pink; | |
} | |
#questions { | |
padding: 10px; | |
overflow-y: auto; | |
height: 100%; | |
} | |
.label { | |
color: #717171; | |
} | |
.question { | |
margin-bottom: 15px; | |
} | |
[type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:before, [type="radio"].with-gap:checked+span:after { | |
border-color: #03a9f4 !important; | |
} | |
[type="checkbox"].filled-in:checked+span:not(.lever):after { | |
border-color: #03a9f4 !important; | |
background-color: #03a9f4 !important; | |
} | |
input.materialize-textarea:focus:not([readonly]) { | |
border-color: #03a9f4 !important; | |
} | |
</style> | |
<div class="app flex-grow"> | |
<div class="flex-column sidebar"> | |
<div class="navbar"> | |
<i | |
class="material-icons" | |
style="color: #9b9b9b; cursor: pointer;" | |
onclick="goHome()" | |
>home</i | |
> | |
</div> | |
<div id="questions"></div> | |
<div class="flex-grow"></div> | |
<div style="display: flex;"> | |
<a | |
class="waves-effect waves-light btn-large" | |
style="background-color: white; color: black; width: 100%;" | |
onclick="skip()" | |
>Overslaan</a | |
> | |
<a | |
class="waves-effect waves-light btn-large" | |
style="background-color: #03a9f4; width: 100%;" | |
onclick="submit()" | |
>Versturen</a | |
> | |
</div> | |
</div> | |
<div class="flex-grow flex-column content"> | |
<div style="display: flex; align-items: center; padding: 10px 0px"> | |
<i | |
id="back" | |
class="material-icons" | |
style="color: #9b9b9b; margin-left: -5px; opacity: 0.2;" | |
onclick="goBack()" | |
>keyboard_arrow_left</i | |
> | |
<div style="color: #717171; padding: 0px 10px;" id="externalid"> | |
Label deze plaatjes | |
</div> | |
<i | |
id="next" | |
class="material-icons" | |
style="color: #9b9b9b; margin-left: -5px; opacity: 0.2;" | |
onclick="goNext()" | |
>keyboard_arrow_right</i | |
> | |
</div> | |
<div id="asset">loading...</div> | |
</div> | |
</div> | |
<script> | |
let state = { | |
projectId: new URL(window.location.href).searchParams.get("project"), | |
currentAsset: undefined | |
}; | |
const defaultConfiguration = { | |
classifications: [ | |
{ | |
name: "model", | |
instructions: "Select the car model", | |
type: "radio", | |
options: [ | |
{ | |
value: "model_s", | |
label: "Tesla Model S" | |
}, | |
{ | |
value: "model_3", | |
label: "Tesla Model 3" | |
}, | |
{ | |
value: "model_x", | |
label: "Tesla Model X" | |
} | |
] | |
}, | |
{ | |
name: "image_problems", | |
instructions: "Select all that apply", | |
type: "checklist", | |
options: [ | |
{ | |
value: "blur", | |
label: "Blurry" | |
}, | |
{ | |
value: "saturated", | |
label: "Over Saturated" | |
}, | |
{ | |
value: "pixelated", | |
label: "Pixelated" | |
} | |
] | |
}, | |
{ | |
name: "description", | |
instructions: "Describe this image", | |
type: "text" | |
} | |
] | |
}; | |
function createOptionQuestion({ type, name, options, instructions }, answer) { | |
const createOption = ({ value, label }) => { | |
return ` | |
<p value=${value}> | |
<label> | |
${ | |
type == "radio" | |
? `<input class="with-gap" type="radio" name="group-${name}" valuetosubmit="${value}" ${ | |
answer === value ? "checked" : "" | |
} />` | |
: `<input type="checkbox" class="filled-in" valuetosubmit="${value}" ${ | |
(answer || []).indexOf(value) !== -1 ? "checked" : "" | |
} />` | |
} | |
<span>${label}</span> | |
</label> | |
</p> | |
`; | |
}; | |
return ` | |
<div class="question" id="${name}" questiontype="${type}"> | |
<div class="label">${instructions}</div> | |
<form action="#"> | |
${options.map(createOption).join("")} | |
</form> | |
</div> | |
`; | |
} | |
function createTextInput({ id, label }, answer) { | |
return ` | |
<div class="question" id="${id}" questiontype="text"> | |
<div class="input-field col s12"> | |
<div class="label">${label}</div> | |
<input class="materialize-textarea" data-length="120" value="${answer || | |
""}"></input> | |
</div> | |
</div> | |
`; | |
} | |
function createQuestion(question, answers) { | |
const optionalAnswer = (answers || {})[question.name]; | |
if (question.type === "text") { | |
return createTextInput( | |
{ | |
id: question.name, | |
label: question.instructions | |
}, | |
optionalAnswer | |
); | |
} | |
if (question.type === "radio" || question.type === "checklist") { | |
return createOptionQuestion(question, optionalAnswer); | |
} | |
console.log("Unknown question type", question); | |
} | |
let classifications = []; | |
let markQuestionsAsLoaded; | |
new Promise(resolve => { | |
markQuestionsAsLoaded = resolve; | |
}).then(() => { | |
Labelbox.currentAsset().subscribe(asset => { | |
if (asset) { | |
drawAsset(asset); | |
} | |
}); | |
}); | |
function drawQuestions(classifications, answers) { | |
if (answers && answers.chosenImageId) { | |
document.querySelector("#chose-" + answers.chosenImageId).checked = true; | |
} | |
document.querySelector("#questions").innerHTML = classifications | |
.map(classification => createQuestion(classification, answers)) | |
.join(""); | |
} | |
Labelbox.getTemplateCustomization().subscribe(customization => { | |
classifications = | |
(customization && customization.classifications) || | |
defaultConfiguration.classifications; | |
drawQuestions(classifications); | |
markQuestionsAsLoaded(); | |
}); | |
function goHome() { | |
window.location.href = | |
"https://app.labelbox.com/projects/" + state.projectId; | |
} | |
function skip() { | |
Labelbox.skip().then(() => { | |
Labelbox.fetchNextAssetToLabel(); | |
}); | |
} | |
function getChosenImageId() { | |
if (document.querySelector("#chose-" + state.leftImageId).checked) { | |
return state.leftImageId; | |
} | |
if (document.querySelector("#chose-" + state.rightImageId).checked) { | |
return state.rightImageId; | |
} | |
return; | |
} | |
function getLabel() { | |
const chosenImageId = getChosenImageId(); | |
const getAnswer = node => { | |
const key = node.getAttribute("id"); | |
const type = node.getAttribute("questiontype"); | |
if (type === "text") { | |
return { | |
[key]: node.querySelector("input").value | |
}; | |
} | |
if (type === "radio") { | |
const inputs = Array.from(node.querySelectorAll("input")); | |
const selected = inputs.find(child => child.checked); | |
return { | |
[key]: selected && selected.getAttribute("valuetosubmit") | |
}; | |
} | |
if (type === "checklist") { | |
const inputs = Array.from(node.querySelectorAll("input")); | |
const value = inputs | |
.filter(child => child.checked) | |
.map(child => child.getAttribute("valuetosubmit")); | |
return { | |
[key]: value | |
}; | |
} | |
console.log("Unable to find type for", node); | |
}; | |
const answers = Array.from( | |
document.querySelector("#questions").children | |
).map(getAnswer); | |
return Object.assign({ chosenImageId }, ...answers); | |
} | |
function submit() { | |
const label = JSON.stringify(getLabel()); | |
const jumpToNext = Boolean(!state.currentAsset.label); | |
// Progress is this asset is new | |
Labelbox.setLabelForAsset(label).then(() => { | |
if (jumpToNext) { | |
Labelbox.fetchNextAssetToLabel(); | |
} | |
}); | |
if (jumpToNext) { | |
document.querySelector("#asset").innerHTML = "loading..."; | |
} | |
} | |
function goBack() { | |
if (state.currentAsset.previous) { | |
Labelbox.setLabelAsCurrentAsset(state.currentAsset.previous); | |
} | |
} | |
function goNext() { | |
if (state.currentAsset.next) { | |
Labelbox.setLabelAsCurrentAsset(state.currentAsset.next); | |
} else { | |
Labelbox.fetchNextAssetToLabel(); | |
} | |
} | |
function getHtmlForAsset({ leftImage, rightImage }) { | |
return ` | |
<div style="display: flex; flex-direction: column;"> | |
<div style="font-size: 25px; margin: 10px;">Welke afbeelding spreekt je meer aan?</div> | |
<div style="display: flex; flex-direction: row; justify-content: space-around;"> | |
<img class="asset" src="${ | |
leftImage.url | |
}" controls style="flex: 47" id="${leftImage.id}"></img> | |
<img class="asset" src="${ | |
rightImage.url | |
}" controls style="flex: 47" id="${rightImage.id}"></img> | |
</div> | |
<div style="display: flex; flex-direction: row;"> | |
<label style="display: flex; justify-content: center; flex: 47"> | |
<input class="with-gap" type="radio" name="group-image-compare" id="chose-${ | |
leftImage.id | |
}" /> | |
<span style="font-size: 20px; margin-top: 20px;">Afbeelding A</span> | |
</label> | |
<label style="display: flex; justify-content: center; flex: 47"> | |
<input class="with-gap" type="radio" name="group-image-compare" id="chose-${ | |
rightImage.id | |
}" /> | |
<span style="font-size: 20px; margin-top: 20px;">Afbeelding B</span> | |
</label> | |
</div> | |
</div> | |
`; | |
} | |
function getImages(data) { | |
try { | |
const { | |
compare: { a, b } | |
} = JSON.parse(data); | |
if (!a.id || !a.url || !b.id || !b.url) { | |
throw new Error( | |
"both a and b must have an id that is used in the label" | |
); | |
} | |
return { | |
leftImage: a, | |
rightImage: b | |
}; | |
} catch (e) { | |
console.log(e); | |
alert( | |
"DataRow format does not match this template. Please see the chrome console for more information" | |
); | |
throw new Error( | |
`Please see the example dataset. DataRows must match this format "{\"compare\":{\"a\":{\"id\":\"your-db-id-89E30C47-5807-5622-AA3D-D390AFE53728\",\"url\":\"http://commondatastorage.googleapis.com/gtv-images-bucket/sample/BigBuckBunny.mp4\"},\"b\":{\"id\":\"your-db-id-0D10F70F-F865-A5B0-A548-7CB0176324AF\",\"url\":\"http://commondatastorage.googleapis.com/gtv-images-bucket/sample/ElephantsDream.mp4\"}}}". Not ${data}` | |
); | |
} | |
} | |
function drawAsset(asset) { | |
const backButton = document.querySelector("#back"); | |
backButton.style.opacity = asset.previous ? 1 : 0.2; | |
backButton.style.cursor = asset.previous ? "pointer" : "inherit"; | |
const hasNext = Boolean(asset.next || asset.label); | |
const nextButton = document.querySelector("#next"); | |
nextButton.style.opacity = hasNext ? 1 : 0.2; | |
nextButton.style.cursor = hasNext ? "pointer" : "inherit"; | |
const { leftImage, rightImage } = getImages(asset.data); | |
if ((state.currentAsset && state.currentAsset.data) !== asset.data) { | |
document.querySelector("#asset").innerHTML = getHtmlForAsset({ | |
leftImage, | |
rightImage | |
}); | |
} | |
if ((state.currentAsset && state.currentAsset.id) !== asset.id) { | |
if (asset.label) { | |
try { | |
const label = JSON.parse(asset.label); | |
drawQuestions(classifications, label); | |
} catch (e) { | |
console.log("failed to read label", e); | |
} | |
} else { | |
drawQuestions(classifications); | |
} | |
} | |
state = { | |
...state, | |
currentAsset: asset, | |
leftImageId: leftImage.id, | |
rightImageId: rightImage.id | |
}; | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment