получить границы сложного объекта на изображении с белым фоном (js)



Каков хороший способ получения границ для изображения (не самого изображения, а скорее цветных пикселей)? Я использую javascript, поэтому старайтесь держать Algo в пределах этой области, если сможете.



Например, как бы я получил список / два списка всех точек x и y, где для точек, которые существуют на границе этого (группы) объекта (ов):



Введите описание изображения здесь



Обратите внимание, что внутренняя часть должна быть включена, но отсутствие цвета, который полностью включен внутренность (как отверстие) должна быть исключена.



Таким образом, результатом будут два списка, содержащие точки x и y (для пикселей), которые построят объект, подобный этому:

Введите описание изображения здесь



Ниже показано, как я "достиг" этого. Хотя он работает для всех вогнутых объектов, если вы попытаетесь использовать его для более сложных объектов с некоторыми выпуклыми сторонами, он неизбежно потерпит неудачу.



Успех с вогнутым объектом




Фрагмент:






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>





Отказ со сложным объектом







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, что внутри этого сложного выпуклого изображения происходит сбой внутри.



Вопрос



Поэтому остается вопрос: каков хороший способ создания маски, если хотите, из изображение (без учета отверстий), которое будет покрывать всю внешнюю сторону любого сложного/выпуклого объекта, где фон белый, а компоненты изображения-что угодно, кроме белого? СПАСИБО

638   1  

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

    Ничего не найдено.