C#中的JPEG伪影去除

5
我正在为一个属于母组织的俱乐部建立网站。我正在下载(盗链;)母组织个人资料页面上放置的图片以显示在我的页面上。但是他们的网站有一个漂亮的白色背景,而我的网站有一个漂亮的灰色渐变背景。这不很匹配。所以我的想法是在将它们保存到我的服务器之前编辑这些图片。
我正在使用GDI+来增强我的图像,当我使用Bitmap的MakeTransparent方法时,它确实起作用,并且确实执行了它应该执行的操作,但我仍然在整个图片上看到这些白色JPEG伪影。这些伪影使得图片质量很差,我最好不要使图片透明,只是保留白色,但这在我的网站上真的很丑陋。当然,我总是可以添加一个漂亮的边框和一个白色背景,但我更愿意将背景改为透明。
所以我想知道是否可以在C#中删除一些简单的JPEG伪影?有人做过吗?
感谢您的时间。
示例图片:
转换后的图像:

那是真的...而且它的运行效果非常好...对于JPEG遗憾的是它已经乱了 :)..张贴了一张图片! - Arcturus
我看不见!抱歉,它没有显示出来。 - MusiGenesis
我也是...我无法让图片正常工作 :P - Arcturus
你能展示一下它在你的渐变上是什么样子吗?你发布的图片看起来对我来说很好(我假设你想让它看起来像一个戴着咖啡杯头盔的星球大战士兵)。 - MusiGenesis
没有梯度,但你会明白的。 - Arcturus
显示剩余4条评论
8个回答

5

嗯,我尝试了一些远非完美的东西,但我想它可能对其他人有用。

我已经完成了:

alt text

遇到的问题:阴影距离“灰白色”足够远,难以自动转换,即使你这样做,阴影仍然会出现在图像本身上。 顶部反光...中心物体的光斑,也比抗锯齿位更接近灰白色。 图像中有三到七个白点,没有连接到任何主要角落; 最后边缘仍然有一点白色(可以通过调整代码来消除它,但是不能在不拆下反光顶部的一部分的情况下去除它)。

C# 低效的代码:

    static void Main()
    {
        Bitmap bmp=new Bitmap("test.jpg");

        int width = bmp.Width;
        int height = bmp.Height;
        Dictionary<Point, int> currentLayer = new Dictionary<Point, int>();
        currentLayer[new Point(0, 0)] = 0;
        currentLayer[new Point(width - 1, height - 1)] = 0;
        while (currentLayer.Count != 0)
        {
            foreach (Point p in currentLayer.Keys)
                bmp.SetPixel(p.X, p.Y, Color.Black);
            Dictionary<Point, int> newLayer = new Dictionary<Point, int>();
            foreach (Point p in currentLayer.Keys)
                foreach (Point p1 in Neighbors(p, width, height))
                    if (Distance(bmp.GetPixel(p1.X, p1.Y), Color.White) < 40)
                        newLayer[p1] = 0;
            currentLayer = newLayer;
        }

        bmp.Save("test2.jpg");
    }

    static int Distance(Color c1, Color c2)
    {
        int dr = Math.Abs(c1.R - c2.R);
        int dg = Math.Abs(c1.G - c2.G);
        int db = Math.Abs(c1.B - c2.B);
        return Math.Max(Math.Max(dr, dg), db);
    }

    static List<Point> Neighbors(Point p, int maxX, int maxY)
    {
        List<Point> points=new List<Point>();
        if (p.X + 1 < maxX) points.Add(new Point(p.X + 1, p.Y));
        if (p.X - 1 >= 0) points.Add(new Point(p.X - 1, p.Y));
        if (p.Y + 1 < maxY) points.Add(new Point(p.X, p.Y + 1));
        if (p.Y - 1 >= 0) points.Add(new Point(p.X, p.Y - 1));
        return points;
    }

代码的工作原理是从两个点开始;将它们设置为黑色,然后检查是否有任何临近的白色像素;如果有,则将它们添加到一个列表中,然后执行。最终,它会用完所有的白色像素。
作为替代方案,您可能希望考虑重新设计网站,使用白色背景。

虽然不是完美的,但你的回答得到了最多的赞,所以我接受了你的回答;我放弃了对这个图像解码的追求。我在白色图像周围加了一个边框,就像一幅画一样。感谢你的帮助!! :)请参见http://www.501st.nl/Members.aspx ;) - Arcturus

1

另一种基于评论对话框的方法:

static void Main()
{
    Bitmap mask = new Bitmap(@"mask.bmp");
    Bitmap bmp=new Bitmap(@"test.jpg");
    int width = bmp.Width;
    int height = bmp.Height;

    for(int x=0; x<width; x++)
        for (int y = 0; y < height; y++)
            if (mask.GetPixel(x, y).R < 250)
                bmp.SetPixel(x,y,mask.GetPixel(x,y));
    bmp.Save(@"test3.jpg");
}

给定掩码:

alt text

你会得到结果:

alt text

使用Paint.NET稍微清理了口罩的边框,并禁用了抗锯齿。如果您可以辨别出使用了哪个边框,这仅适用于该边框...但它确实变得很好...除了绿色部分...


是的,我尝试了我的第二种方法,但当我将绿色变成透明时,它又出问题了;)..(请参见绿色边框)..问题可能是我的网站背景..我们在背景中使用渐变,而不需要精确放置图片,透明背景是正确的选择..啊好吧..我们有另一个想法。我们让用户上传自己的动作图片,添加一个漂亮的冲锋队员图片框架,然后让它成为个人资料图片。这与此问题无关,因此避免它也会解决它,谢谢你的帮助..非常感谢。 - Arcturus
我认为你可以修改遮罩图像,使绿色边框在手动修饰时变为透明;然后只需使用红色区域与原始图像合并,结果是边框上的透明区域和中心的合并区域。不过,无论哪种方式,都很有趣。 - CoderTao

1

循环遍历图像中的每个像素,如果R、G和B大于230,则用所需的颜色(或透明)替换颜色。甚至可以根据旧颜色与“真正”白色相差多远来加权新颜色。

如果实际图像也是白色,则可能会出现问题,否则您将得到一个灰色的风暴兵 :)


1
这可以改为更通用的解决方案,而不会通过执行伪图遍历使风暴突击队员变灰。从左上角白色像素开始,然后向左/下/上/右前往每个与“足够接近”条件匹配的像素。基本上是一种填充算法。如果实际图像边缘有混合(白色和棕色混合看起来很奇怪),这仍然可能会引起麻烦。 - CoderTao
我喜欢你的回答,它干净简洁。今天下午和女友一起吃晚餐时,我也想到了这个解决方案,为Stormtrooper添加了一个“无需更改”的区域/块。 - Arcturus
好的,我也尝试了你的解决方案...但是边缘处仍然有白色边框...该死,这个白色太丑了;)...不过还是谢谢你花时间帮忙! - Arcturus

1

你不可能用任何类似于100%准确度的东西自动完成此操作。

原因是您只有彩色信息,您知道图像中的一些像素正在尝试与其混合。仅有图像中的一些像素实际上会使用接近此值或相同值的颜色进行背景阴影处理,而其他像素将使用(在这种情况下为白色),因为实际上表示的对象是白色的(该帝国风暴军的精度真是够了)。

检测哪些是哪些的复杂机器学习是一个有趣的问题领域,可能会成为您的有趣项目,但肯定不会为您的直接问题提供快速解决方案。

您还面临另一个问题,即使您可以可靠地检测出图像中那些试图混合到背景中的区域,如果颜色不相容,则在将它们‘取消混合’并重新混合到新背景颜色中时会出现问题。在这种情况下,您的灰色可能有效,因为它是一种类似于白色的广谱颜色。

您想要使用的技术如下:

使用泛洪填充算法,从图像边缘向内选择所有距离已知背景颜色x%(1)以内的像素。 对于这些像素,将它们的alpha通道设置为与原始颜色匹配程度的比例值,并消除与之相关的色彩偏差。 因此,如果背景是RGB值a、b、c,像素是a+5、b、c-7,则结果为RGBA 5,0,0,((a+b+c-7)/(a+b+c)*256)(1)。 将这个alpha混合图像与新背景颜色的矩形合成。 将结果渲染为没有alpha通道的新图像。
但是,这仍然会存在一些问题,例如对象的颜色接近任一背景颜色时: * 对于原始图像,可能正在使用阴影来暗示对象的存在,因此泛洪填充将“侵入”图像内部。 * 对于后者,生成的图像将失去对象的定义,没有微妙的阴影、高光或明显的线条来指示对象的结束和背景的结束。

这只是一个非常粗略的第一次近似,但可能涵盖您目标的合理百分比。那些具有完全封闭透明孔洞的图片(例如您示例中外拱顶上的间隙)不太可能以自动方式良好地工作,因为算法将无法区分白色孔洞和白色风暴兵。
您可能希望使您的算法突出显示它计划重新混合的图片区域,并允许简单选择要包含/排除的区域(如果您想要花哨一些,可以使用Pain.Net的魔术棒选择工具作为示例来进行简单的像素选择,以减少前期工作量)。


  1. x的值将是您调整的内容 - 可能是基于图像的某些方面(例如接近背景颜色的图像比例),您可以自动进行微调。
  2. 请注意,此公式假定接近白色,对于接近黑色的情况,您需要进行反转。

0

感谢大家的回答,都是很好的建议。

这是我昨天想出来的:

public const int PIXEL_REGION = 60;
public const int TRANSPARENT_DISTANCE = 60;
public static readonly Color TRANSPARENT_COLOR = Color.White;

private System.Drawing.Image ProcessImage(System.Drawing.Image image)
{
    Bitmap result = new Bitmap(image.Width, image.Height);

    Bitmap workImage = new Bitmap(image);

    for (int x = 0; x < image.Width; x++)
    {
        for (int y = 0; y < image.Height; y++)
        {
            Color color = workImage.GetPixel(x, y);

            if (x < PIXEL_REGION || x > image.Width - PIXEL_REGION || y < PIXEL_REGION || y > image.Height - PIXEL_REGION)
            {
                double distance = CalculateColourDistance(TRANSPARENT_COLOR, color);

                if(distance < TRANSPARENT_DISTANCE)
                {
                    result.SetPixel(x, y, Color.Transparent);
                    continue;
                }
            }

            result.SetPixel(x, y, color);
        }
    }

    return result;
}

private double CalculateColourDistance(Color c1, Color c2)
{
    int a = c2.A - c1.A;
    int r = c2.R - c1.R;
    int g = c2.G - c1.G;
    int b = c2.B - c1.B;

    return Math.Sqrt(Math.Pow(a, 2) + Math.Pow(r, 2) + Math.Pow(g, 2) + Math.Pow(b, 2));
}

虽然这不是书中最漂亮的代码,但它所做的事情是计算像素区域中每个像素与Color.White之间的距离,并且当距离小于预定义距离时,将其设为透明色。

这段代码产生以下结果:

TB-5404 transformed v 2.0

不是很糟糕..但还是有点烂 :)

我还有一个绝招,如果成功了,我也会与你们分享...


0

你还需要处理一些类似于米白色的颜色。可能在最初制作图像并设置背景颜色时,进行了一些反锯齿处理,然后保存为jpg格式时,并不是所有颜色都能完美保留。因此,如果你想将某个特定颜色变成透明,它并不能获取该颜色的所有阴影,这会留下许多伪像。你需要使用一些方法,按照接近关键颜色的程度来进行透明度处理。这可能更容易通过Photoshop等软件批量处理,但我不知道你是否需要实时处理。


嗯,那可能会改变的,你知道的。有时候拥有一些动态的东西会很好,以处理未来成员的所有图像。 - Arcturus
在查看图片后,如果您的源可以提供非损失格式(如位图),那么您可能不会遇到目前的问题。看起来浅灰色是由于jpg压缩引起的。也许他们可以在服务器上同时托管jpg和bmp文件,使用jpg文件在网站上,将bmp文件保留在同一位置。这样,当您拉取图像时,可以替换扩展名,拉取bmp文件并更改颜色。 - AaronLS
是的,我可以尝试一下,但我不认为他们有原始源代码,因为这是许多制作这些框架的人提交的图片集合。不过还是谢谢你的建议。 - Arcturus

0

在编程方面,没有(远非简单的)方法来处理这个问题。图像边缘周围的白色伪影区域是几乎为白色但又不完全是白色的像素导致的,因此它们无法呈现透明效果。口罩/咖啡杯上还有一些纯白色斑点,因此它们变得透明并呈现灰色。

你最好联系原网站的管理员,看看他们能否以保留原始图层的格式将原始图像发送给你,希望是Photoshop或其他格式。然后,您可以以保持原始透明度(PNG或类似格式)或使用背景渐变重新生成图像(由于您不知道图片将呈现在渐变中的哪里,所以这将非常困难)。

如您所建议的那样,在图片周围加上某种边框。


嗯,是的...总有新成员加入,我真的想一劳永逸地解决这个问题...不过还是谢谢你的时间...非常感激...也许我最终会采用边框解决方案... - Arcturus

0

这是又一次失败的尝试 ;)

我有一个想法.. 电影使用绿幕,为什么不为我的图像使用绿色叠加层.. 所以这就是叠加层:

Green overlay with transparent inner thing

结果如下:

Green stuff all over the place

所以我学到了有关PNG压缩的另一个宝贵经验.. 我也尝试了bmp,但不知怎么回事,总是出现绿色边框.. 我可以尝试擦除它,但也许我只需将其留在白色背景上,放置一些愚蠢的边框并完成它...

嗯,好吧.. ;)


稍微不同的问题,奇怪的边框是恒定的吗?(也就是说,大多数/所有图像是否具有相同的边框,并在内部提供用户提供的图像?) - CoderTao
不,有不同种类的边框...例如:将自行车侦察兵图像(上方)与冲锋队员边框进行比较:http://images.501stforums.com/memberdata/54/04/tk5404_full.jpg - Arcturus
好的,思路(虽然现在不太有用)是你手动清理边框(看起来你正在使用绿色方法进行清理),然后标记一个内部区域为“要合并的区域”(绿色、品红色、荧光粉色等任何可行的颜色),然后将中心区域与原始区域合并。你有手动清理的边框和根据定义清理的中心区域。只有当你知道边框时才有效。 - CoderTao
覆盖图像的中心是透明的,虽然它与原始图像融合得很好,但是显然我的覆盖并不完美。 - Arcturus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接