当我着色时,绘制算法在边缘留下白色像素

我正在创建一个绘图应用程序。 当我选择一种非常深的颜色并在pandas脸上画画时,边缘会留白。 我希望我的代码为边界内的整个区域着色。 这是LINK 。 这些是我在javascript中使用的function。 我怎样才能让它们变得更好?

function matchOutlineColor(r, g, b, a) { return (r + g + b = 50); }; function matchStartColor(pixelPos, startR, startG, startB) { var r = outlineLayerData.data[pixelPos], g = outlineLayerData.data[pixelPos + 1], b = outlineLayerData.data[pixelPos + 2], a = outlineLayerData.data[pixelPos + 3]; // If current pixel of the outline image is black if (matchOutlineColor(r, g, b, a)) { return false; } r = colorLayerData.data[pixelPos]; g = colorLayerData.data[pixelPos + 1]; b = colorLayerData.data[pixelPos + 2]; // If the current pixel matches the clicked color if (r === startR && g === startG && b === startB) { return true; } // If current pixel matches the new color if (r === curColor.r && g === curColor.g && b === curColor.b) { return false; } return true; }; function colorPixel(pixelPos, r, g, b, a) { colorLayerData.data[pixelPos] = r; colorLayerData.data[pixelPos + 1] = g; colorLayerData.data[pixelPos + 2] = b; colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255; }; function floodFill(startX, startY, startR, startG, startB) { document.getElementById('color-lib-1').style.display = "none"; document.getElementById('color-lib-2').style.display = "none"; document.getElementById('color-lib-3').style.display = "none"; document.getElementById('color-lib-4').style.display = "none"; document.getElementById('color-lib-5').style.display = "none"; change = false; var newPos, x, y, pixelPos, reachLeft, reachRight, drawingBoundLeft = drawingAreaX, drawingBoundTop = drawingAreaY, drawingBoundRight = drawingAreaX + drawingAreaWidth - 1, drawingBoundBottom = drawingAreaY + drawingAreaHeight - 1, pixelStack = [ [startX, startY] ]; while (pixelStack.length) { newPos = pixelStack.pop(); x = newPos[0]; y = newPos[1]; // Get current pixel position pixelPos = (y * canvasWidth + x) * 4; // Go up as long as the color matches and are inside the canvas while (y >= drawingBoundTop && matchStartColor(pixelPos, startR, startG, startB)) { y -= 1; pixelPos -= canvasWidth * 4; } pixelPos += canvasWidth * 4; y += 1; reachLeft = false; reachRight = false; // Go down as long as the color matches and in inside the canvas while (y  drawingBoundLeft) { if (matchStartColor(pixelPos - 4, startR, startG, startB)) { if (!reachLeft) { // Add pixel to stack pixelStack.push([x - 1, y]); reachLeft = true; } } else if (reachLeft) { reachLeft = false; } } if (x < drawingBoundRight) { if (matchStartColor(pixelPos + 4, startR, startG, startB)) { if (!reachRight) { // Add pixel to stack pixelStack.push([x + 1, y]); reachRight = true; } } else if (reachRight) { reachRight = false; } } pixelPos += canvasWidth * 4; } } }; // Start painting with paint bucket tool starting from pixel specified by startX and startY function paintAt(startX, startY) { var pixelPos = (startY * canvasWidth + startX) * 4, r = colorLayerData.data[pixelPos], g = colorLayerData.data[pixelPos + 1], b = colorLayerData.data[pixelPos + 2], a = colorLayerData.data[pixelPos + 3]; if (r === curColor.r && g === curColor.g && b === curColor.b) { // Return because trying to fill with the same color return; } if (matchOutlineColor(r, g, b, a)) { // Return because clicked outline return; } floodFill(startX, startY, r, g, b); redraw(); }; 

您正在为抖动/平滑/抗锯齿/损耗压缩图像使用精确颜色填充。 这会使像素颜色略有不同(未着色(边框)),从而产生伪影。 有两种简单的方法可以解决它:

  1. 匹配关闭颜色而不是精确

    如果你现在有2种RGB颜色(r0,g0,b0)(r1,g1,b1) (如果我看到它的话)你的检查是这样的:

     if ((r0!=r1)&&(g0!=g1)&&(b0!=b1)) colors_does_not_match; 

    你应该这样做:

     if (((r0-r1)*(r0-r1))+((g0-g1)*(g0-g1))+((b0-b1)*(b0-b1))>treshold) colors_does_not_match; 

    其中, treshold是最大允许距离^ 2,用于填充。 如果使用的阈值太低,则会保留工件。 如果使用太高的阈值,那么填充物也会流到附近的相似颜色……

    如果你想拥有更具视觉效果的东西,那么你可以比较HSV空间中的颜色 。

  2. 使用封闭边框填充

    它与泛色填充基本相同,但您重新着色所有不包含边框颜色的像素,而不是仅包含起始颜色的像素。 这将填充该区域,但边框中将存在伪影(将有点微弱),但不像您当前的方法视觉上不愉快。

  3. 我有时会使用颜色均匀填充

    这个适用于编辑由于光线或其他颜色阴影的照片。 所以我填充颜色就像填充洪水一样,但是如果颜色距离起始位置的距离不是很大,那么就会考虑颜色匹配,而不是同时使用小阈值的最后填充位置。 这种方式我通常通过重新着色到普通颜色来修复拼接照片中的天空文物,并使用一些喷涂工具渐变图案模糊区域…

[Edit1] C ++示例

它使用VCL封装的GDI位图,因此重写对您的样式的图像访问。 你也可以忽略窗口的东西(我留下它只是为了展示如何使用它……)。 我实现了方法#1,#2,所以选择你想要的方法。 你的边界不是很尖锐,因此阈值非常大( 223^2 ),使得#2选项不稳定。 static变量只是为了缓解堆栈的垃圾,所以如果你想multithreading,你需要删除它…

 //--------------------------------------------------------------------------- // variables //--------------------------------------------------------------------------- Graphics::TBitmap *bmp=NULL; // your image union color { DWORD dd; BYTE db[4]; }; // direct RGB channel access DWORD **pixel=NULL; // direct 32bit pixel access int xs=0,ys=0; // image resolution int mx=0,my=0; // mouse position int treshold=50000; // color match treshold //--------------------------------------------------------------------------- // Flood fill //--------------------------------------------------------------------------- DWORD _floodfill_col_fill; // temp storage to ease up recursion heap/stack trashing DWORD _floodfill_col_start; // temp storage to ease up recursion heap/stack trashing void _floodfill(int x,int y) // recursive subfunction do not call this directly { // variables static color c; static int r,g,b; // color match c.dd=pixel[y][x]&0x00FFFFFF; // mask out alpha channel just to be sure if (_floodfill_col_fill==c.dd) return; // ignore already filled parts (exact match) r=c.db[0]; // r0,g0,b0 g=c.db[1]; b=c.db[2]; c.dd=_floodfill_col_start; r-=c.db[0]; r*=r; // (r0-r1)^2,(g0-g1)^2,(b0-b1)^2 g-=c.db[1]; g*=g; b-=c.db[2]; b*=b; if (r+g+b>treshold) return; // recolor pixel[y][x]=_floodfill_col_fill; // 4 neighboars recursion if (x> 0) _floodfill(x-1,y); if (x 0) _floodfill(x,y-1); if (y 0) _borderfill(x-1,y); if (x 0) _borderfill(x,y-1); if (yLoadFromFile("test.bmp"); // cropped part of your image bmp->HandleType=bmDIB; // allow direct pixel access bmp->PixelFormat=pf32bit; // set 32bit pixels for easy addressing xs=bmp->Width; // get the image size ys=bmp->Height; ClientWidth=xs; // just resize my App window to match image size ClientHeight=ys; // direct pixel access pixel = new DWORD*[ys]; // buffer for pointers to all lines of image for (int y=0;yScanLine[y]; } //--------------------------------------------------------------------------- void __fastcall TForm1::FormDestroy(TObject *Sender) { // [exit event] delete bmp; delete pixel; } //--------------------------------------------------------------------------- void __fastcall TForm1::FormPaint(TObject *Sender) { // [on paint event] Canvas->Draw(0,0,bmp); } //--------------------------------------------------------------------------- void __fastcall TForm1::FormClick(TObject *Sender) { // [on mouse click event] floodfill(mx,my,0x00FF0000); // borderfill(mx,my,0x00FF0000,0x00000000); Paint(); // shedule repaint event } //--------------------------------------------------------------------------- void __fastcall TForm1::FormMouseMove(TObject *Sender, TShiftState Shift, int X,int Y) { // [on mouse move event] mx=X; my=Y; // store actual mouse position } //--------------------------------------------------------------------------- 

并且gfx结果:

例

[Edit2]迭代分段泛洪填充

标准递归填充填充的问题在于它具有潜在的危险性并且经常导致堆叠溢出更大或更复杂的填充(例如尝试使用许多螺钉的螺旋……)更不用说它通常很慢。 泛洪填充所需的内存量大约为填充像素数的倍数(因此,请使用您使用的所有变量和操作数,并乘以像素大小的图像大小,很快就会出现大量数字)。

为避免这种情况,您可以使用线段而不是像素来限制递归层。 这将加速一切并限制内存使用量。 此方法也可以迭代完成,在这种情况下更好,所以算法是这样的:

  1. 有一个分段列表seg[]

    我选择了水平线,所以每个段都有x0,x1,y和一些标志变量供以后使用。 从开始,列表为空。

  2. 从填充起始位置x,y添加新段

    因此,只需扫描x,y之前和之后的所有像素x,y以覆盖左侧x0和右侧x1所有后续起始颜色( col0 ),并将新段添加到列表中,标记为done=0

  3. 使用done=0循环所有段

    设置其done=1并扫描与其相邻的所有像素x=y={y-1,y+1}如果找到尝试添加新段(就像在#2中 )但在添加之前测试所有其他重叠段。 如果找到任何重叠段,则用新找到的段放大,并设置其done=0否则添加新段。 这样,段数不会太高。

  4. 循环#3,同时进行任何更改

    为了加快这个速度,您可以使用索引表idx[][]来保存特定y坐标处的段的所有索引。 我设置/使用它以便segment[idx[y][?]].y=y

  5. 循环所有段并重新着色像素

这里是我的C ++代码:

 //--------------------------------------------------------------------------- // variables //--------------------------------------------------------------------------- Graphics::TBitmap *bmp=NULL; // your image union color { DWORD dd; BYTE db[4]; }; // direct RGB channel access DWORD **pixel=NULL; // direct 32bit pixel access int xs=0,ys=0; // image resolution int mx=0,my=0; // mouse position int treshold=50000; // color match treshold //--------------------------------------------------------------------------- // Color compare with treshold //--------------------------------------------------------------------------- bool color_equal(DWORD c0,DWORD c1) { static color c; static int r,g,b; c.dd=c0&0x00FFFFFF; // mask out alpha channel just to be sure r=c.db[0]; // r0,g0,b0 g=c.db[1]; b=c.db[2]; c.dd=c1&0x00FFFFFF; r-=c.db[0]; r*=r; // (r0-r1)^2,(g0-g1)^2,(b0-b1)^2 g-=c.db[1]; g*=g; b-=c.db[2]; b*=b; return (r+g+b<=treshold); } //--------------------------------------------------------------------------- bool color_nonequal(DWORD c0,DWORD c1) { static color c; static int r,g,b; c.dd=c0&0x00FFFFFF; // mask out alpha channel just to be sure r=c.db[0]; // r0,g0,b0 g=c.db[1]; b=c.db[2]; c.dd=c1&0x00FFFFFF; r-=c.db[0]; r*=r; // (r0-r1)^2,(g0-g1)^2,(b0-b1)^2 g-=c.db[1]; g*=g; b-=c.db[2]; b*=b; return (r+g+b>treshold); } //--------------------------------------------------------------------------- // Flood fill segmented by lines //--------------------------------------------------------------------------- struct _segment { int x0,x1,y; int done; _segment(){}; _segment(_segment& a){ *this=a; }; ~_segment(){}; _segment* operator = (const _segment *a) { *this=*a; return this; }; /*_segment* operator = (const _segment &a) { ...copy... return this; };*/ }; void floodfill_segmented(int x,int y,DWORD col) // This is the main call for flood fill (x,y) start position and col is fill color { // init variables int i,j,k,e,ee,x0,x1; _segment s,*p; List<_segment> seg; // H-line segments List< List > idx; // index table seg[idx[y]].y=y to speed up searches DWORD col0=pixel[y][x]&0x00FFFFFF; DWORD col1=col &0x00FFFFFF; // sanity check if (color_equal(col0,col1)) return; // prepare segment table and macros seg.allocate(ys<<3); seg.num=0; idx.allocate(ys ); idx.num=ys; for (i=0;i=0;x--) if (color_equal(col0,pixel[y][x])) s.x0=x; else break; \ for (x=s.x1;xx0>=s.x0)&&(p->x0<=s.x1)) ee=1; \ if ((p->x1>=s.x0)&&(p->x1<=s.x1)) ee=1; \ if ((p->x0<=s.x0)&&(p->x1>=s.x0)) ee=1; \ if ((p->x0<=s.x1)&&(p->x1>=s.x1)) ee=1; \ if (ee) \ { \ if (p->x0>s.x0) { p->done=0; p->x0=s.x0; } \ if (p->x1done=0; p->x1=s.x1; } \ s=*p; \ break; \ } \ } \ if (ee) ee=p->done; else { idx.dat[sy].add(seg.num); seg.add(s); } \ } // first segment; _seg_add; for (e=1;e;) { // add new adjacent segments for (e=0,p=seg.dat,i=0;idone) { p->done=1; y=p->y-1; if (y>=0) for (x=p->x0;x<=p->x1;x++) if (color_equal(col0,pixel[y][x])) { _seg_add; e|=!ee; x=s.x1; p=seg.dat+i; } y=p->y+1; if (yx0;x<=p->x1;x++) if (color_equal(col0,pixel[y][x])) { _seg_add; e|=!ee; x=s.x1; p=seg.dat+i; } } } #undef seg_add // recolor for (p=seg.dat,i=0;ix0,y=p->y;x<=p->x1;x++) pixel[y][x]=col; } //--------------------------------------------------------------------------- 

color_equal只是将颜色与阈值进行比较(并且它应该是宏而不是函数以获得更快的速度)。

我也使用我的动态列表模板,所以:

  • List xxx;double xxx[];相同double xxx[];
  • xxx.add(5);5添加到列表的末尾
  • xxx[7]访问数组元素(安全)
  • xxx.dat[7]访问数组元素(不安全但快速直接访问)
  • xxx.num是数组的实际使用大小
  • xxx.reset()清除数组并设置xxx.num = 0
  • xxx.allocate(100)预分配100项目的空间

这里可以比较一下:

例

左边是源图像720x720像素螺旋,右边是从左下方填充后的结果。使用40 ms代码而不使用idx[]并使用数组范围检查需要大约440 ms 。 如果我使用标准的泛洪填充,它会在几秒钟后因堆栈溢出而崩溃。 另一方面,对于较小的填充,标准递归填充通常比此更快。