получить границы сложного объекта на изображении с белым фоном (js)
Каков хороший способ получения границ для изображения (не самого изображения, а скорее цветных пикселей)? Я использую javascript, поэтому старайтесь держать Algo в пределах этой области, если сможете.
Например, как бы я получил список / два списка всех точек x и y, где для точек, которые существуют на границе этого (группы) объекта (ов):
Обратите внимание, что внутренняя часть должна быть включена, но отсутствие цвета, который полностью включен внутренность (как отверстие) должна быть исключена.
Таким образом, результатом будут два списка, содержащие точки x и y (для пикселей), которые построят объект, подобный этому:
Ниже показано, как я "достиг" этого. Хотя он работает для всех вогнутых объектов, если вы попытаетесь использовать его для более сложных объектов с некоторыми выпуклыми сторонами, он неизбежно потерпит неудачу.
Успех с вогнутым объектом
- jsfiddle: https://jsfiddle.net/bhc4qn87/
Фрагмент:
var _PI = Math.PI, _HALF_PI = Math.PI / 2, _TWO_PI = 2 * Math.PI;
var _radius = 10, _damp = 75, _center = new THREE.Vector3(0, 0, 0);
var _phi = _PI / 2, _theta = _theta = _PI / 7;
var _sceneScreenshot = null, _dirty = true;
var _tmpCan = document.createElement("canvas"),
_tmpCtx = _tmpCan.getContext("2d");
var scene = document.getElementById("scene"),
sw = scene.width, sh = scene.height;
var _scene = new THREE.Scene();
var _renderer = new THREE.WebGLRenderer({ canvas: scene, alpha: true, antialias: true });
_renderer.setPixelRatio(window.devicePixelRatio);
_renderer.setSize(sw, sh);
var _camera = new THREE.PerspectiveCamera(35, sw / sh, .1, 1000);
_tmpCan.width = sw; _tmpCan.height = sh;
_scene.add(new THREE.HemisphereLight(0x999999, 0x555555, 1));
_scene.add(new THREE.AmbientLight(0x404040));
var _camLight = new THREE.PointLight(0xdfdfdf, 1.8, 300, 2);
_scene.add(_camLight);
var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0x2378d3, opacity: .7 } );
var cube = new THREE.Mesh( geometry, material );
_scene.add( cube );
function initialize() {
document.body.appendChild(_tmpCan);
_tmpCan.style.position = "absolute";
_tmpCan.style.left = "8px";
_tmpCan.style.top = "8px";
_tmpCan.style.pointerEvents = "none";
addListeners();
updateCamera();
animate();
}
function addListeners() {
/* mouse events */
var scene = document.getElementById("scene");
scene.oncontextmenu = function(e) {
e.preventDefault();
}
scene.onmousedown = function(e) {
e.preventDefault();
mouseTouchDown(e.pageX, e.pageY, e.button);
}
scene.ontouchstart = function(e) {
if (e.touches.length !== 1) {
return;
}
e.preventDefault();
mouseTouchDown(e.touches[0].pageX, e.touches[0].pageY, e.touches.length, true);
}
function mouseTouchDown(pageX, pageY, button, touch) {
_mouseX = pageX; _mouseY = pageY;
_button = button;
if (touch) {
document.ontouchmove = function(e) {
if (e.touches.length !== 1) {
return;
}
mouseTouchMove(e.touches[0].pageX, e.touches[0].pageY, e.touches.length, true);
}
document.ontouchend = function() {
document.ontouchmove = null;
document.ontouchend = null;
}
} else {
document.onmousemove = function(e) {
mouseTouchMove(e.pageX, e.pageY, _button);
}
document.onmouseup = function() {
document.onmousemove = null;
document.onmouseup = null;
}
}
}
function mouseTouchMove(pageX, pageY, button, touch) {
var dx = pageX - _mouseX,
dy = pageY - _mouseY;
_phi += dx / _damp;
// _theta += dy / _damp;
_phi %= _TWO_PI;
if (_phi < 0) {
_phi += _TWO_PI;
}
// var maxTheta = _HALF_PI - _HALF_PI * .8,
// minTheta = -_HALF_PI + _HALF_PI * .8;
// if (_theta > maxTheta) {
// _theta = maxTheta;
// } else if (_theta < minTheta) {
// _theta = minTheta;
// }
updateCamera();
_dirty = true;
// updateLabels();
_mouseX = pageX;
_mouseY = pageY;
}
}
function updateCamera() {
// var radius = _radius + (Math.sin(_theta % _PI)) * 10;
var radius = _radius;
var y = radius * Math.sin(_theta),
phiR = radius * Math.cos(_theta);
var z = phiR * Math.sin(_phi),
x = phiR * Math.cos(_phi);
_camera.position.set(x, y, z);
_camLight.position.set(x, y, z);
_camera.lookAt(_center);
}
function updateLabels() {
if (_sceneScreenshot === null) {
return;
}
var tmpImg = new Image();
tmpImg.onload = function() {
_tmpCtx.drawImage(tmpImg, 0, 0, sw, sh);
var imgData = _tmpCtx.getImageData(0, 0, sw, sh);
var data = imgData.data;
var firstXs = [];
var lastXs = [];
for (var y = 0; y < sh; y++) {
var firstX = -1;
var lastX = -1;
for (var x = 0; x < sw; x++) {
var i = (x + y * sw) * 4;
var sum = data[i] + data[i + 1] + data[i + 2];
if (firstX === -1) {
if (sum > 3) {
firstX = x;
}
} else {
if (sum > 3) {
lastX = x;
}
}
}
if (lastX === -1 && firstX >= 0) {
lastX = firstX;
}
firstXs.push(firstX);
lastXs.push(lastX);
}
var firstYs = [];
var lastYs = [];
for (var x = 0; x < sw; x++) {
var firstY = -1;
var lastY = -1;
for (var y = 0; y < sh; y++) {
var i = (x + y * sw) * 4;
var sum = data[i] + data[i + 1] + data[i + 2];
if (firstY === -1) {
if (sum < 759) {
firstY = y;
}
} else {
if (sum < 759) {
lastY = y;
}
}
}
if (lastY === -1 && firstY >= 0) {
lastY = firstY;
}
firstYs.push(firstY);
lastYs.push(lastY);
}
postLoad(firstXs, lastXs, firstYs, lastYs);
}
tmpImg.src = _sceneScreenshot;
function postLoad(firstXs, lastXs, firstYs, lastYs) {
_tmpCtx.clearRect(0, 0, sw, sh);
_tmpCtx.beginPath();
for (var y = 0; y < sh; y++) {
_tmpCtx.moveTo(firstXs[y], y);
_tmpCtx.lineTo(lastXs[y], y);
}
/* TODO REMOVE BELOW TODO */
_tmpCtx.strokeStyle = 'black';
console.log(_tmpCtx.globalAlpha);
_tmpCtx.stroke();
/* TODO REMOVE ABOVE TODO */
_tmpCtx.beginPath();
for (var x = 0; x < sw; x++) {
_tmpCtx.moveTo(x, firstYs[x]);
_tmpCtx.lineTo(x, lastYs[x]);
}
/* TODO REMOVE BELOW TODO */
_tmpCtx.strokeStyle = 'black';
_tmpCtx.stroke();
/* TODO REMOVE ABOVE TODO */
var imgData = _tmpCtx.getImageData(0, 0, sw, sh);
var data = imgData.data;
for (var i = 0, iLen = data.length; i < iLen; i += 4) {
if (data[i + 3] < 200) {
data[i + 3] = 0;
}
/* TODO remove v TODO */
else { data[i + 3] = 120; }
}
_tmpCtx.putImageData(imgData, 0, 0);
}
}
function animate () {
cube.rotation.x += 0.001;
cube.rotation.y += 0.001;
_renderer.render(_scene, _camera);
if (_dirty) {
_sceneScreenshot = _renderer.domElement.toDataURL();
updateLabels();
_dirty = false;
}
requestAnimationFrame( animate );
}
initialize();<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/92/three.js"></script>
<canvas id="scene" width="400" height="300"></canvas>Отказ со сложным объектом
- jsfiddle: https://jsfiddle.net/xdr9bt0w/
var _PI = Math.PI, _HALF_PI = Math.PI / 2, _TWO_PI = 2 * Math.PI;
var _radius = 10, _damp = 75, _center = new THREE.Vector3(0, 0, 0);
var _phi = _PI / 2, _theta = _theta = 0;
var _sceneScreenshot = null, _dirty = true;
var _tmpCan = document.createElement("canvas"),
_tmpCtx = _tmpCan.getContext("2d");
var scene = document.getElementById("scene"),
sw = scene.width, sh = scene.height;
var _scene = new THREE.Scene();
var _renderer = new THREE.WebGLRenderer({ canvas: scene, alpha: true, antialias: true });
_renderer.setPixelRatio(window.devicePixelRatio);
_renderer.setSize(sw, sh);
var _camera = new THREE.PerspectiveCamera(35, sw / sh, .1, 1000);
_tmpCan.width = sw; _tmpCan.height = sh;
_scene.add(new THREE.HemisphereLight(0x999999, 0x555555, 1));
_scene.add(new THREE.AmbientLight(0x404040));
var _camLight = new THREE.PointLight(0xdfdfdf, 1.8, 300, 2);
_scene.add(_camLight);
var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0x2378d3, opacity: .7 } );
var cube = new THREE.Mesh( geometry, material );
_scene.add( cube );
var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0xc36843, opacity: .7 } );
var cube2 = new THREE.Mesh( geometry, material );
cube2.position.x = -.75;
cube2.position.y = .75
_scene.add( cube2 );
var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0x43f873, opacity: .7 } );
var cube3 = new THREE.Mesh( geometry, material );
cube3.position.x = -.25;
cube3.position.y = 1.5;
_scene.add( cube3 );
var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0x253621, opacity: .7 } );
var cube4 = new THREE.Mesh( geometry, material );
cube4.position.x = 1;
cube4.position.y = .35;
_scene.add( cube4 );
function initialize() {
document.body.appendChild(_tmpCan);
_tmpCan.style.position = "absolute";
_tmpCan.style.left = "200px";
_tmpCan.style.top = "0px";
_tmpCan.style.pointerEvents = "none";
addListeners();
updateCamera();
animate();
}
function addListeners() {
/* mouse events */
var scene = document.getElementById("scene");
scene.oncontextmenu = function(e) {
e.preventDefault();
}
scene.onmousedown = function(e) {
e.preventDefault();
mouseTouchDown(e.pageX, e.pageY, e.button);
}
scene.ontouchstart = function(e) {
if (e.touches.length !== 1) {
return;
}
e.preventDefault();
mouseTouchDown(e.touches[0].pageX, e.touches[0].pageY, e.touches.length, true);
}
function mouseTouchDown(pageX, pageY, button, touch) {
_mouseX = pageX; _mouseY = pageY;
_button = button;
if (touch) {
document.ontouchmove = function(e) {
if (e.touches.length !== 1) {
return;
}
mouseTouchMove(e.touches[0].pageX, e.touches[0].pageY, e.touches.length, true);
}
document.ontouchend = function() {
document.ontouchmove = null;
document.ontouchend = null;
}
} else {
document.onmousemove = function(e) {
mouseTouchMove(e.pageX, e.pageY, _button);
}
document.onmouseup = function() {
document.onmousemove = null;
document.onmouseup = null;
}
}
}
function mouseTouchMove(pageX, pageY, button, touch) {
var dx = pageX - _mouseX,
dy = pageY - _mouseY;
_phi += dx / _damp;
// _theta += dy / _damp;
_phi %= _TWO_PI;
if (_phi < 0) {
_phi += _TWO_PI;
}
// var maxTheta = _HALF_PI - _HALF_PI * .8,
// minTheta = -_HALF_PI + _HALF_PI * .8;
// if (_theta > maxTheta) {
// _theta = maxTheta;
// } else if (_theta < minTheta) {
// _theta = minTheta;
// }
updateCamera();
_dirty = true;
// updateLabels();
_mouseX = pageX;
_mouseY = pageY;
}
}
function updateCamera() {
// var radius = _radius + (Math.sin(_theta % _PI)) * 10;
var radius = _radius;
var y = radius * Math.sin(_theta),
phiR = radius * Math.cos(_theta);
var z = phiR * Math.sin(_phi),
x = phiR * Math.cos(_phi);
_camera.position.set(x, y, z);
_camLight.position.set(x, y, z);
_camera.lookAt(_center);
}
function updateLabels() {
if (_sceneScreenshot === null) {
return;
}
var tmpImg = new Image();
tmpImg.onload = function() {
_tmpCtx.drawImage(tmpImg, 0, 0, sw, sh);
var imgData = _tmpCtx.getImageData(0, 0, sw, sh);
var data = imgData.data;
var firstXs = [];
var lastXs = [];
for (var y = 0; y < sh; y++) {
var firstX = -1;
var lastX = -1;
for (var x = 0; x < sw; x++) {
var i = (x + y * sw) * 4;
var sum = data[i] + data[i + 1] + data[i + 2];
if (firstX === -1) {
if (sum > 3) {
firstX = x;
}
} else {
if (sum > 3) {
lastX = x;
}
}
}
if (lastX === -1 && firstX >= 0) {
lastX = firstX;
}
firstXs.push(firstX);
lastXs.push(lastX);
}
var firstYs = [];
var lastYs = [];
for (var x = 0; x < sw; x++) {
var firstY = -1;
var lastY = -1;
for (var y = 0; y < sh; y++) {
var i = (x + y * sw) * 4;
var sum = data[i] + data[i + 1] + data[i + 2];
if (firstY === -1) {
if (sum > 3) {
firstY = y;
}
} else {
if (sum > 3) {
lastY = y;
}
}
}
if (lastY === -1 && firstY >= 0) {
lastY = firstY;
}
firstYs.push(firstY);
lastYs.push(lastY);
}
postLoad(firstXs, lastXs, firstYs, lastYs);
}
tmpImg.src = _sceneScreenshot;
function postLoad(firstXs, lastXs, firstYs, lastYs) {
_tmpCtx.clearRect(0, 0, sw, sh);
_tmpCtx.beginPath();
for (var y = 0; y < sh; y++) {
_tmpCtx.moveTo(firstXs[y], y);
_tmpCtx.lineTo(lastXs[y], y);
}
/* TODO REMOVE BELOW TODO */
_tmpCtx.strokeStyle = 'black';
console.log(_tmpCtx.globalAlpha);
_tmpCtx.stroke();
/* TODO REMOVE ABOVE TODO */
_tmpCtx.beginPath();
for (var x = 0; x < sw; x++) {
_tmpCtx.moveTo(x, firstYs[x]);
_tmpCtx.lineTo(x, lastYs[x]);
}
/* TODO REMOVE BELOW TODO */
_tmpCtx.strokeStyle = 'black';
_tmpCtx.stroke();
/* TODO REMOVE ABOVE TODO */
var imgData = _tmpCtx.getImageData(0, 0, sw, sh);
var data = imgData.data;
for (var i = 0, iLen = data.length; i < iLen; i += 4) {
if (data[i + 3] < 200) {
data[i + 3] = 0;
}
/* TODO remove v TODO */
else { data[i + 3] = 120; }
}
_tmpCtx.putImageData(imgData, 0, 0);
}
}
function animate () {
cube.rotation.x += 0.001;
cube.rotation.y += 0.001;
cube2.rotation.x -= 0.001;
cube2.rotation.y += 0.001;
cube3.rotation.x += 0.001;
cube3.rotation.y -= 0.001;
cube4.rotation.x -= 0.001;
cube4.rotation.y -= 0.001;
_renderer.render(_scene, _camera);
if (_dirty) {
_sceneScreenshot = _renderer.domElement.toDataURL();
updateLabels();
_dirty = false;
}
requestAnimationFrame( animate );
}
initialize();<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/92/three.js"></script>
<canvas id="scene" width="400" height="300"></canvas>Вы можете видеть с помощью вышеупомянутого jsfiddle, что внутри этого сложного выпуклого изображения происходит сбой внутри.
Вопрос
Поэтому остается вопрос: каков хороший способ создания маски, если хотите, из изображение (без учета отверстий), которое будет покрывать всю внешнюю сторону любого сложного/выпуклого объекта, где фон белый, а компоненты изображения-что угодно, кроме белого? СПАСИБО
1 ответ:
Вот решение, которое использует алгоритмflood-fill , чтобы покрыть внешние области белым, а остальные-черным.
Имейте в виду, что это очень наивная реализация, есть много оптимизаций, которые потенциально могут быть выполнены (например, путем вычисления ограничивающего прямоугольника и заполнения только внутри него, другой вариант-использовать 32-битные массивы для выполнения фактического назначения пикселей при заполнении).
Еще следует отметить, что начинка всегда начинается в верхний левый угол, если объект в данный момент покрывает этот пиксель, он не будет работать (однако вы можете выбрать другой пиксель для начала).Я удалил обработчики прикосновений и некоторые другие элементы, чтобы сократить пример. Функция
updateMask-это место, где создается маска.
function createCube(color, x, y){ const geo = new THREE.BoxBufferGeometry( 1, 1, 1 ); const mat = new THREE.MeshPhysicalMaterial( { color: color, opacity: 1 } ); const mesh = new THREE.Mesh(geo, mat); mesh.position.x = x; mesh.position.y = y; return mesh; } const c_main = document.getElementById("main"); const c_mask = document.getElementById("mask"); const ctx_mask = c_mask.getContext("2d"); ctx_mask.fillStyle = "#000"; const cw = c_main.width, ch = c_main.height; const TWO_PI = Math.PI * 2; const damp = 75, radius = 10, animspeed = 0.001; const center = new THREE.Vector3(0, 0, 0); let x1 = 0; let phi = Math.PI / 2; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(35, cw / ch, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ canvas: c_main, alpha: true, antialias: false }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(cw, ch); const camLight = new THREE.PointLight(0xdfdfdf, 1.8, 300, 2); scene.add(new THREE.HemisphereLight(0x999999, 0x555555, 1)); scene.add(new THREE.AmbientLight(0x404040)); scene.add(camLight); const cubes = []; cubes.push(createCube(0x2378d3, 0, 0)); cubes.push(createCube(0xc36843, -0.75, 0.75)); cubes.push(createCube(0x43f873, -0.25, 1.5)); cubes.push(createCube(0x253621, 1, 0.35)); scene.add(...cubes); function initialize() { c_main.addEventListener("mousedown", mouseDown, false); updateCamera(); animate(); } function updateMask(){ //First, fill the canvas with black ctx_mask.globalCompositeOperation = "source-over"; ctx_mask.fillRect(0,0, cw, ch); //Then using the composite operation "destination-in" the canvas is made transparent EXCEPT where the new image is drawn. ctx_mask.globalCompositeOperation = "destination-in"; ctx_mask.drawImage(c_main, 0, 0); //Now, use a flood fill algorithm of your choice to fill the outer transparent field with white. const idata = ctx_mask.getImageData(0,0, cw, ch); const array = idata.data; floodFill(array, 0, 0, cw, ch); ctx_mask.putImageData(idata, 0, 0); //The only transparency left are in the "holes", we make these black by using the composite operation "destination-over" to paint black behind everything. ctx_mask.globalCompositeOperation = "destination-over"; ctx_mask.fillRect(0,0, cw, ch); } function mouseDown(e){ e.preventDefault(); x1 = e.pageX; const button = e.button; document.addEventListener("mousemove", mouseMove, false); document.addEventListener("mouseup", mouseUp, false); } function mouseUp(){ document.removeEventListener("mousemove", mouseMove, false); document.removeEventListener("mouseup", mouseUp, false); } function mouseMove(e){ const x2 = e.pageX; const dx = x2 - x1; phi += dx/damp; phi %= TWO_PI; if( phi < 0 ){ phi += TWO_PI; } x1 = x2; updateCamera(); } function updateCamera() { const x = radius * Math.cos(phi); const y = 0; const z = radius * Math.sin(phi); camera.position.set(x, y, z); camera.lookAt(center); camLight.position.set(x, y, z); } function animate(){ cubes[0].rotation.x += animspeed; cubes[0].rotation.y += animspeed; cubes[1].rotation.x -= animspeed; cubes[1].rotation.y += animspeed; cubes[2].rotation.x += animspeed; cubes[2].rotation.y -= animspeed; cubes[3].rotation.x -= animspeed; cubes[3].rotation.y -= animspeed; renderer.render(scene, camera); updateMask(); requestAnimationFrame(animate); } const FILL_THRESHOLD = 254; //Quickly adapted flood fill from http://www.adammil.net/blog/v126_A_More_Efficient_Flood_Fill.html function floodStart(array, x, y, width, height){ const M = width * 4; while(true){ let ox = x, oy = y; while(y !== 0 && array[(y-1)*M + x*4 + 3] < FILL_THRESHOLD){ y--; } while(x !== 0 && array[y*M + (x-1)*4 + 3] < FILL_THRESHOLD){ x--; } if(x === ox && y === oy){ break; } } floodFill(array, x, y, width, height); } function floodFill(array, x, y, width, height){ const M = width * 4; let lastRowLength = 0; do{ let rowLength = 0, sx = x; let idx = y*M + x*4 + 3; if(lastRowLength !== 0 && array[idx] >= FILL_THRESHOLD){ do{ if(--lastRowLength === 0){ return; } } while(array[ y*M + (++x)*4 + 3]); sx = x; } else{ for(; x !== 0 && array[y*M + (x-1)*4 + 3] < FILL_THRESHOLD; rowLength++, lastRowLength++){ const idx = y*M + (--x)*4; array[idx] = 255; array[idx + 1] = 255; array[idx + 2] = 255; array[idx + 3] = 255; if( y !== 0 && array[(y-1)*M + x*4 + 3] < FILL_THRESHOLD ){ floodStart(array, x, y-1, width, height); } } } for(; sx < width && array[y*M + sx*4 + 3] < FILL_THRESHOLD; rowLength++, sx++){ const idx = y*M + sx*4; array[idx] = 255; array[idx + 1] = 255; array[idx + 2] = 255; array[idx + 3] = 255; } if(rowLength < lastRowLength){ for(let end=x+lastRowLength; ++sx < end; ){ if(array[y*M + sx*4 + 3] < FILL_THRESHOLD){ floodFill(array, sx, y, width, height); } } } else if(rowLength > lastRowLength && y !== 0){ for(let ux=x+lastRowLength; ++ux<sx; ){ if(array[(y-1)*M + ux*4 + 3] < FILL_THRESHOLD){ floodStart(array, ux, y-1, width, height); } } } lastRowLength = rowLength; } while(lastRowLength !== 0 && ++y < height); } initialize();<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/92/three.js"></script> <canvas id="main" width="300" height="200"></canvas> <canvas id="mask" width="300" height="200"></canvas>


Comments