|
|
|
|
|
const statusLabel = document.getElementById('status'); |
|
const fileUpload = document.getElementById('upload'); |
|
const imageContainer = document.getElementById('container'); |
|
const example = document.getElementById('example'); |
|
const maskCanvas = document.getElementById('mask-output'); |
|
const uploadButton = document.getElementById('upload-button'); |
|
const resetButton = document.getElementById('reset-image'); |
|
const clearButton = document.getElementById('clear-points'); |
|
const cutButton = document.getElementById('cut-mask'); |
|
|
|
|
|
let lastPoints = null; |
|
let isEncoded = false; |
|
let isDecoding = false; |
|
let isMultiMaskMode = false; |
|
let modelReady = false; |
|
let imageDataURI = null; |
|
|
|
|
|
const BASE_URL = 'https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/'; |
|
const EXAMPLE_URL = BASE_URL + 'corgi.jpg'; |
|
|
|
|
|
const worker = new Worker('worker.js', { |
|
type: 'module', |
|
}); |
|
|
|
|
|
const star = new Image(); |
|
star.src = BASE_URL + 'star-icon.png'; |
|
star.className = 'icon'; |
|
|
|
const cross = new Image(); |
|
cross.src = BASE_URL + 'cross-icon.png'; |
|
cross.className = 'icon'; |
|
|
|
|
|
worker.addEventListener('message', (e) => { |
|
const { type, data } = e.data; |
|
if (type === 'ready') { |
|
modelReady = true; |
|
statusLabel.textContent = 'Ready'; |
|
|
|
} else if (type === 'decode_result') { |
|
isDecoding = false; |
|
|
|
if (!isEncoded) { |
|
return; |
|
} |
|
|
|
if (!isMultiMaskMode && lastPoints) { |
|
|
|
decode(); |
|
lastPoints = null; |
|
} |
|
|
|
const { mask, scores } = data; |
|
|
|
|
|
if (maskCanvas.width !== mask.width || maskCanvas.height !== mask.height) { |
|
maskCanvas.width = mask.width; |
|
maskCanvas.height = mask.height; |
|
} |
|
|
|
|
|
const context = maskCanvas.getContext('2d'); |
|
const imageData = context.createImageData(maskCanvas.width, maskCanvas.height); |
|
|
|
|
|
const numMasks = scores.length; |
|
let bestIndex = 0; |
|
for (let i = 1; i < numMasks; ++i) { |
|
if (scores[i] > scores[bestIndex]) { |
|
bestIndex = i; |
|
} |
|
} |
|
statusLabel.textContent = `Segment score: ${scores[bestIndex].toFixed(2)}`; |
|
|
|
|
|
const pixelData = imageData.data; |
|
for (let i = 0; i < pixelData.length; ++i) { |
|
if (mask.data[numMasks * i + bestIndex] === 1) { |
|
const offset = 4 * i; |
|
pixelData[offset] = 0; |
|
pixelData[offset + 1] = 114; |
|
pixelData[offset + 2] = 189; |
|
pixelData[offset + 3] = 255; |
|
} |
|
} |
|
|
|
|
|
context.putImageData(imageData, 0, 0); |
|
|
|
} else if (type === 'segment_result') { |
|
if (data === 'start') { |
|
statusLabel.textContent = 'Extracting image embedding...'; |
|
} else { |
|
statusLabel.textContent = 'Embedding extracted!'; |
|
isEncoded = true; |
|
} |
|
} |
|
}); |
|
|
|
function decode() { |
|
isDecoding = true; |
|
worker.postMessage({ type: 'decode', data: lastPoints }); |
|
} |
|
|
|
function clearPointsAndMask() { |
|
|
|
isMultiMaskMode = false; |
|
lastPoints = null; |
|
|
|
|
|
document.querySelectorAll('.icon').forEach(e => e.remove()); |
|
|
|
|
|
cutButton.disabled = true; |
|
|
|
|
|
maskCanvas.getContext('2d').clearRect(0, 0, maskCanvas.width, maskCanvas.height); |
|
} |
|
clearButton.addEventListener('click', clearPointsAndMask); |
|
|
|
resetButton.addEventListener('click', () => { |
|
|
|
isEncoded = false; |
|
imageDataURI = null; |
|
|
|
|
|
worker.postMessage({ type: 'reset' }); |
|
|
|
|
|
clearPointsAndMask(); |
|
|
|
|
|
cutButton.disabled = true; |
|
imageContainer.style.backgroundImage = 'none'; |
|
uploadButton.style.display = 'flex'; |
|
statusLabel.textContent = 'Ready'; |
|
}); |
|
|
|
function segment(data) { |
|
|
|
isEncoded = false; |
|
if (!modelReady) { |
|
statusLabel.textContent = 'Loading model...'; |
|
} |
|
imageDataURI = data; |
|
|
|
|
|
imageContainer.style.backgroundImage = `url(${data})`; |
|
uploadButton.style.display = 'none'; |
|
cutButton.disabled = true; |
|
|
|
|
|
worker.postMessage({ type: 'segment', data }); |
|
} |
|
|
|
|
|
fileUpload.addEventListener('change', function (e) { |
|
const file = e.target.files[0]; |
|
if (!file) { |
|
return; |
|
} |
|
|
|
const reader = new FileReader(); |
|
|
|
|
|
reader.onload = e2 => segment(e2.target.result); |
|
|
|
reader.readAsDataURL(file); |
|
}); |
|
|
|
example.addEventListener('click', (e) => { |
|
e.preventDefault(); |
|
segment(EXAMPLE_URL); |
|
}); |
|
|
|
function addIcon({ point, label }) { |
|
const icon = (label === 1 ? star : cross).cloneNode(); |
|
icon.style.left = `${point[0] * 100}%`; |
|
icon.style.top = `${point[1] * 100}%`; |
|
imageContainer.appendChild(icon); |
|
} |
|
|
|
|
|
imageContainer.addEventListener('mousedown', e => { |
|
if (e.button !== 0 && e.button !== 2) { |
|
return; |
|
} |
|
if (!isEncoded) { |
|
return; |
|
} |
|
if (!isMultiMaskMode) { |
|
lastPoints = []; |
|
isMultiMaskMode = true; |
|
cutButton.disabled = false; |
|
} |
|
|
|
const point = getPoint(e); |
|
lastPoints.push(point); |
|
|
|
|
|
addIcon(point); |
|
|
|
decode(); |
|
}); |
|
|
|
|
|
|
|
function clamp(x, min = 0, max = 1) { |
|
return Math.max(Math.min(x, max), min) |
|
} |
|
|
|
function getPoint(e) { |
|
|
|
const bb = imageContainer.getBoundingClientRect(); |
|
|
|
|
|
const mouseX = clamp((e.clientX - bb.left) / bb.width); |
|
const mouseY = clamp((e.clientY - bb.top) / bb.height); |
|
|
|
return { |
|
point: [mouseX, mouseY], |
|
label: e.button === 2 |
|
? 0 |
|
: 1, |
|
} |
|
} |
|
|
|
|
|
imageContainer.addEventListener('contextmenu', e => { |
|
e.preventDefault(); |
|
}); |
|
|
|
|
|
imageContainer.addEventListener('mousemove', e => { |
|
if (!isEncoded || isMultiMaskMode) { |
|
|
|
|
|
return; |
|
} |
|
lastPoints = [getPoint(e)]; |
|
|
|
if (!isDecoding) { |
|
decode(); |
|
} |
|
}); |
|
|
|
|
|
cutButton.addEventListener('click', () => { |
|
const [w, h] = [maskCanvas.width, maskCanvas.height]; |
|
|
|
|
|
const maskContext = maskCanvas.getContext('2d'); |
|
const maskPixelData = maskContext.getImageData(0, 0, w, h); |
|
|
|
|
|
const image = new Image(); |
|
image.crossOrigin = 'anonymous'; |
|
image.onload = async () => { |
|
|
|
const imageCanvas = new OffscreenCanvas(w, h); |
|
const imageContext = imageCanvas.getContext('2d'); |
|
imageContext.drawImage(image, 0, 0, w, h); |
|
const imagePixelData = imageContext.getImageData(0, 0, w, h); |
|
|
|
|
|
const cutCanvas = new OffscreenCanvas(w, h); |
|
const cutContext = cutCanvas.getContext('2d'); |
|
const cutPixelData = cutContext.getImageData(0, 0, w, h); |
|
|
|
|
|
for (let i = 3; i < maskPixelData.data.length; i += 4) { |
|
if (maskPixelData.data[i] > 0) { |
|
for (let j = 0; j < 4; ++j) { |
|
const offset = i - j; |
|
cutPixelData.data[offset] = imagePixelData.data[offset]; |
|
} |
|
} |
|
} |
|
cutContext.putImageData(cutPixelData, 0, 0); |
|
|
|
|
|
const link = document.createElement('a'); |
|
link.download = 'image.png'; |
|
link.href = URL.createObjectURL(await cutCanvas.convertToBlob()); |
|
link.click(); |
|
link.remove(); |
|
} |
|
image.src = imageDataURI; |
|
}); |
|
|