对图像强制平均


20

编写一个程序,以获取标准的真彩色图像和单一的24位RGB颜色(0到255之间的三个数字)。修改输入图像(或输出具有相同尺寸的新图像),使其平均颜色恰好是输入的单色。您可以按照自己喜欢的任何方式修改输入图像中的像素,但目的是使颜色变化在视觉上尽可能不明显

RGB图像的平均颜色实际上是一组三个算术平均值的集合,每个颜色通道对应一个。平均红色值是图像中所有像素上的红色值之和除以像素总数(图像区域),四舍五入到最接近的整数。绿色和蓝色平均值的计算方法相同。

这个Python 2(带有PIL)脚本可以计算大多数图像文件格式的平均颜色:

from PIL import Image
print 'Enter image file'
im = Image.open(raw_input()).convert('RGB')
pixels = im.load()
avg = [0, 0, 0]
for x in range(im.size[0]):
    for y in range(im.size[1]):
        for i in range(3):
            avg[i] += pixels[x, y][i]
print 'The average color is', tuple(c // (im.size[0] * im.size[1]) for c in avg)

(类似的还有平均颜色方案在这里,但他们不一定做同样的计算。)

程序的主要要求是,对于任何输入图像,其相应输出的平均颜色必须与输入的颜色完全匹配-由Python代码段或某些等效代码判断。输出图像还必须具有与输入图像完全相同的尺寸。

因此,从技术上讲,您可以提交一个程序,该程序仅对整个输入进行着色以指定的平均颜色(因为平均值始终是该颜色),但这是一场人气竞赛 -投票数最高的提交将获胜,这很简单提交不会给您带来很多赞成。利用人类视觉中的怪癖,缩小图像并在其周围绘制彩色边框等新颖的想法(希望)能使您获得投票。

请注意,平均颜色和图像的某些组合需要非常明显的颜色变化。例如,如果要匹配的平均颜色为黑色(0,0,0),则任何输入图像都必须设为全黑,因为如果任何像素的值都为非零,那么它们也将使平均值也为非零(排除舍入误差)。投票时请牢记这些限制。

测试影像

某些图像及其默认的平均颜色可供选择。点击查看原图。

A.平均(127,127,127)

fejesjoco各种颜色图片回答。在他的博客上找到了原件

B.平均(62,71,73)

横滨。由Geobits提供。

C.平均值(115、112、111)

东京。由Geobits提供。

D.平均(154,151,154)

埃舍尔瀑布原来的

E.平均值(105、103、102)

沙斯塔山。由我提供。

F.平均(75,91,110)

星夜

笔记

  • 程序使用的确切输入和输出格式以及图像文件类型无关紧要。只要确保清楚如何使用您的程序即可。
  • 如果图像已经具有目标平均颜色,则应该按原样输出,这可能是一个好主意(但从技术上讲不是要求)。
  • 请发布平均色彩输入为(150、100、100)或(75、91、110)的测试图像,以便投票者可以在不同解决方案中看到相同的输入。(发布更多的示例比这还好,甚至可以鼓励。)

2
参与者可以选择他们用来证明其解决方案有效性的输入颜色?这是否使人们难以比较解决方案?在极端情况下,有人会选择与图像平均值非常相似的输入颜色,并且看起来他们的解决方案非常有效。
Reto Koradi 2015年

1
@ vihan1086如果我正确理解,则将平均颜色作为24位RGB颜色输入提供,而从输入图像中找不到。
trichoplax

3
使用@ vihan1086的解释,并使用示例图像作为输入颜色的来源,因此一个图像以另一图像的平均颜色显示可能会很有趣。这样,可以公平地比较不同的答案。
trichoplax

这样做的主要问题是,大多数人的平均水平非常接近灰色。繁星之夜可能是离这一点最远的地方,但其余的平均表现平平。
Geobits

@RetoKoradi希望选民能够足够聪明地考虑到此类问题,尽管我已经添加了关于要使用的默认平均颜色的注释。
加尔文的爱好

Answers:


11

Python 2 + PIL,简单的颜色缩放

from PIL import Image
import math

INFILE = "street.jpg"
OUTFILE = "output.png"
AVERAGE = (150, 100, 100)

im = Image.open(INFILE)
im = im.convert("RGB")
width, height = prev_size = im.size
pixels = {(x, y): list(im.getpixel((x, y)))
          for x in range(width) for y in range(height)}

def get_avg():
    total_rgb = [0, 0, 0]

    for x in range(width):
        for y in range(height):
            for i in range(3):
                total_rgb[i] += int(pixels[x, y][i])

    return [float(x)/(width*height) for x in total_rgb]

curr_avg = get_avg()

while tuple(int(x) for x in curr_avg) != AVERAGE:
    print curr_avg   
    non_capped = [0, 0, 0]
    total_rgb = [0, 0, 0]

    for x in range(width):
        for y in range(height):
            for i in range(3):
                if curr_avg[i] < AVERAGE[i] and pixels[x, y][i] < 255:
                    non_capped[i] += 1
                    total_rgb[i] += int(pixels[x, y][i])

                elif curr_avg[i] > AVERAGE[i] and pixels[x, y][i] > 0:
                    non_capped[i] += 1
                    total_rgb[i] += int(pixels[x, y][i])

    ratios = [1 if z == 0 else
              x/(y/float(z))
              for x,y,z in zip(AVERAGE, total_rgb, non_capped)]

    for x in range(width):
        for y in range(height):
            col = []

            for i in range(3):
                new_col = (pixels[x, y][i] + 0.01) * ratios[i]
                col.append(min(255, max(0, new_col)))

            pixels[x, y] = tuple(col)

    curr_avg = get_avg()

print curr_avg

for pixel in pixels:
    im.putpixel(pixel, tuple(int(x) for x in pixels[pixel]))

im.save(OUTFILE)

这是一种幼稚的方法,应该作为一个良好的基准。在每次迭代中,我们将当前的平均值与所需的平均值进行比较,并根据比率缩放每个像素的RGB。但是,我们必须谨慎一些,原因有两个:

  • 缩放0仍会得出0,因此在缩放之前,我们要添加一些小数值(此处0.01

  • RGB值在0到255之间,因此我们需要相应地调整比率,以弥补缩放有上限的像素没有任何作用。

图像另存为PNG,因为另存为JPG似乎会弄乱颜色平均值。

样品输出

(40,40,40)

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明

(150,100,100)

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明

(75、91、110),“星夜”调色板

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明


2
您肯定要为此使用具有无损压缩的图像格式。因此,JPEG不是一个好的选择。
Reto Koradi 2015年

您始终可以依靠Sp获得出色的图像挑战解决方案。
Alex A.

6

C ++,伽玛校正

这将使用简单的伽玛校正对图像进行亮度调节,其中伽玛值将针对每个分量分别确定,以匹配目标平均值。

较高级别的步骤是:

  1. 读取图像并提取每个颜色分量的直方图。
  2. 对每个组件的gamma值执行二进制搜索。对伽马值执行二进制搜索,直到所得直方图具有所需的平均值。
  3. 再次读取图像,然后应用伽玛校正。

所有图像输入/输出均使用ASCII格式的PPM文件。使用GIMP将图像从PNG转换为PNG。该代码在Mac上运行,图像转换在Windows上完成。

码:

#include <cmath>
#include <string>
#include <vector>
#include <sstream>
#include <fstream>
#include <iostream>

static inline int mapVal(int val, float gamma)
{
    float relVal = (val + 1.0f) / 257.0f;
    float newRelVal = powf(relVal, gamma);

    int newVal = static_cast<int>(newRelVal * 257.0f - 0.5f);
    if (newVal < 0)
    {
        newVal = 0;
    }
    else if (newVal > 255)
    {
        newVal = 255;
    }

    return newVal;
}

struct Histogram
{
    Histogram();

    bool read(const std::string fileName);
    int getAvg(int colIdx) const;
    void adjust(const Histogram& origHist, int colIdx, float gamma);

    int pixCount;
    std::vector<int> freqA[3];
};

Histogram::Histogram()
  : pixCount(0)
{
    for (int iCol = 0; iCol < 3; ++iCol)
    {
        freqA[iCol].resize(256, 0);
    }
}

bool Histogram::read(const std::string fileName)
{
    for (int iCol = 0; iCol < 3; ++iCol)
    {
        freqA[iCol].assign(256, 0);
    }

    std::ifstream inStrm(fileName);

    std::string format;
    inStrm >> format;
    if (format != "P3")
    {
        std::cerr << "invalid PPM header" << std::endl;
        return false;
    }

    int w = 0, h = 0;
    inStrm >> w >> h;
    if (w <= 0 || h <= 0)
    {
        std::cerr << "invalid size" << std::endl;
        return false;
    }

    int maxVal = 0;
    inStrm >> maxVal;
    if (maxVal != 255)
    {
        std::cerr << "invalid max value (255 expected)" << std::endl;
        return false;
    }

    pixCount = w * h;

    int sumR = 0, sumG = 0, sumB = 0;
    for (int iPix = 0; iPix < pixCount; ++iPix)
    {
        int r = 0, g = 0, b = 0;
        inStrm >> r >> g >> b;
        ++freqA[0][r];
        ++freqA[1][g];
        ++freqA[2][b];
    }

    return true;
}

int Histogram::getAvg(int colIdx) const
{
    int avg = 0;
    for (int val = 0; val < 256; ++val)
    {
        avg += freqA[colIdx][val] * val;
    }

    return avg / pixCount;
}

void Histogram::adjust(const Histogram& origHist, int colIdx, float gamma)
{
    freqA[colIdx].assign(256, 0);

    for (int val = 0; val < 256; ++val)
    {
        int newVal = mapVal(val, gamma);
        freqA[colIdx][newVal] += origHist.freqA[colIdx][val];
    }
}

void mapImage(const std::string fileName, float gammaA[])
{
    std::ifstream inStrm(fileName);

    std::string format;
    inStrm >> format;

    int w = 0, h = 0;
    inStrm >> w >> h;

    int maxVal = 0;
    inStrm >> maxVal;

    std::cout << "P3" << std::endl;
    std::cout << w << " " << h << std::endl;
    std::cout << "255" << std::endl;

    int nPix = w * h;

    for (int iPix = 0; iPix < nPix; ++iPix)
    {
        int inRgb[3] = {0};
        inStrm >> inRgb[0] >> inRgb[1] >> inRgb[2];

        int outRgb[3] = {0};
        for (int iCol = 0; iCol < 3; ++iCol)
        {
            outRgb[iCol] = mapVal(inRgb[iCol], gammaA[iCol]);
        }

        std::cout << outRgb[0] << " " << outRgb[1] << " "
                  << outRgb[2] << std::endl;
    }
}

int main(int argc, char* argv[])
{
    if (argc < 5)
    {
        std::cerr << "usage: " << argv[0]
                  << " ppmFileName targetR targetG targetB"
                  << std::endl;
        return 1;
    }

    std::string inFileName = argv[1];

    int targAvg[3] = {0};
    std::istringstream strmR(argv[2]);
    strmR >> targAvg[0];
    std::istringstream strmG(argv[3]);
    strmG >> targAvg[1];
    std::istringstream strmB(argv[4]);
    strmB >> targAvg[2];

    Histogram origHist;
    if (!origHist.read(inFileName))
    {
        return 1;
    }

    Histogram newHist(origHist);
    float gammaA[3] = {0.0f};

    for (int iCol = 0; iCol < 3; ++iCol)
    {
        float minGamma = 0.0f;
        float maxGamma = 1.0f;
        for (;;)
        {
            newHist.adjust(origHist, iCol, maxGamma);
            int avg = newHist.getAvg(iCol);
            if (avg <= targAvg[iCol])
            {
                break;
            }
            maxGamma *= 2.0f;
        }

        for (;;)
        {
            float midGamma = 0.5f * (minGamma + maxGamma);

            newHist.adjust(origHist, iCol, midGamma);
            int avg = newHist.getAvg(iCol);
            if (avg < targAvg[iCol])
            {
                maxGamma = midGamma;
            }
            else if (avg > targAvg[iCol])
            {
                minGamma = midGamma;
            }
            else
            {
                gammaA[iCol] = midGamma;
                break;
            }
        }
    }

    mapImage(inFileName, gammaA);

    return 0;
}

代码本身非常简单。一个细微但重要的细节是,尽管颜色值在[0,255]范围内,但我将它们映射到伽玛曲线,就好像该范围是[-1,256]。这允许将平均值强制为0或255。否则,0将始终保持为0,而255将始终保持为255,这可能永远不允许平均值为0/255。

使用方法:

  1. 在扩展名的文件保存代码.cpp,例如force.cpp
  2. 用编译c++ -o force -O2 force.cpp
  3. 用运行./force input.ppm targetR targetG target >output.ppm

40、40、40的样本输出

请注意,所有较大样本的图像都包含为JPEG,因为它们超出了SE大小限制(为PNG)。由于JPEG是有损压缩格式,因此它们可能与目标平均值不完全匹配。我具有所有文件的PNG版本,完全匹配。

Af1 Bf1 Cf1 Df1 Ef1 Ff1

150、100、100的样本输出:

Af2 Bf2 Cf2 Df2 Ef2 Ff2

75、91、110的样本输出:

Af3 Bf3 Cf3 Df3 Ef3 Ff3


我不得不缩小其他图像以达到极限-也许可以尝试一下?
Sp3000

@ Sp3000好,现在包含了所有图像。现在也有缩略图。我最终将JPEG版本用于大版本。实际上,其中一个小于大小限制,但看起来它已自动转换为JPEG。第一个和最后一个示例仍然是PNG。
Reto Koradi

2

Python 2 + PIL

from PIL import Image
import random
import math

SOURCE = 'input.png'
OUTPUT = 'output.png'
AVERAGE = [150, 100, 100]

im = Image.open(SOURCE).convert('RGB')
pixels = im.load()
w = im.size[0]
h = im.size[1]
npixels = w * h

maxdiff = 0.1

# for consistent results...
random.seed(42)
order = range(npixels)
random.shuffle(order)

def calc_sum(pixels, w, h):
    sums = [0, 0, 0]
    for x in range(w):
        for y in range(h):
            for i in range(3):
                sums[i] += pixels[x, y][i]
    return sums

def get_coordinates(index, w):
    return tuple([index % w, index // w])

desired_sums = [AVERAGE[0] * npixels, AVERAGE[1] * npixels, AVERAGE[2] * npixels]

sums = calc_sum(pixels, w, h)
for i in range(3):
    while sums[i] != desired_sums[i]:
        for j in range(npixels):
            if sums[i] == desired_sums[i]:
                break
            elif sums[i] < desired_sums[i]:
                coord = get_coordinates(order[j], w)
                pixel = list(pixels[coord])
                delta = int(maxdiff * (255 - pixel[i]))
                if delta == 0 and pixel[i] != 255:
                    delta = 1
                delta = min(delta, desired_sums[i] - sums[i])

                sums[i] += delta
                pixel[i] += delta
                pixels[coord] = tuple(pixel)
            else:
                coord = get_coordinates(order[j], w)
                pixel = list(pixels[coord])
                delta = int(maxdiff * pixel[i])
                if delta == 0 and pixel[i] != 0:
                    delta = 1
                delta = min(delta, sums[i] - desired_sums[i])

                sums[i] -= delta
                pixel[i] -= delta
                pixels[coord] = tuple(pixel)

# output image
for x in range(w):
    for y in range(h):
        im.putpixel(tuple([x, y]), pixels[tuple([x, y])])

im.save(OUTPUT)

这以随机顺序遍历每个像素,并减小了像素颜色和/ 255或每个像素之间的距离0(取决于当前平均值是小于还是大于所需平均值)。距离减少了一个固定的乘数。重复进行直到获得期望的平均值。1除非颜色为255(或0),否则缩小至少应始终为,以确保一旦像素接近白色或黑色,处理就不会停止。

样品输出

(40,40,40)

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明

(150,100,100)

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明

(75,91,110)

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明 在此处输入图片说明


1

爪哇

基于RNG的方法。对于大型输入图像,速度有点慢。

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.*;

import javax.imageio.ImageIO;


public class Averager {
    static Random r;
    static long sigmaR=0,sigmaG=0,sigmaB=0;
    static int w,h;
    static int rbar,gbar,bbar;
    static BufferedImage i;
    private static File file;
    static void upRed(){
        int x=r.nextInt(w);
        int y=r.nextInt(h);
        Color c=new Color(i.getRGB(x, y));
        if(c.getRed()==255)return;
        sigmaR++;
        c=new Color(c.getRed()+1,c.getGreen(),c.getBlue());
        i.setRGB(x, y,c.getRGB());
    }
    static void downRed(){
        int x=r.nextInt(w);
        int y=r.nextInt(h);
        Color c=new Color(i.getRGB(x, y));
        if(c.getRed()==0)return;
        sigmaR--;
        c=new Color(c.getRed()-1,c.getGreen(),c.getBlue());
        i.setRGB(x, y,c.getRGB());
    }
    static void upGreen(){
        int x=r.nextInt(w);
        int y=r.nextInt(h);
        Color c=new Color(i.getRGB(x, y));
        if(c.getGreen()==255)return;
        sigmaG++;
        c=new Color(c.getRed(),c.getGreen()+1,c.getBlue());
        i.setRGB(x, y,c.getRGB());
    }
    static void downGreen(){
        int x=r.nextInt(w);
        int y=r.nextInt(h);
        Color c=new Color(i.getRGB(x, y));
        if(c.getGreen()==0)return;
        sigmaG--;
        c=new Color(c.getRed(),c.getGreen()-1,c.getBlue());
        i.setRGB(x,y,c.getRGB());
    }
    static void upBlue(){
        int x=r.nextInt(w);
        int y=r.nextInt(h);
        Color c=new Color(i.getRGB(x, y));
        if(c.getBlue()==255)return;
        sigmaB++;
        c=new Color(c.getRed(),c.getGreen(),c.getBlue()+1);
        i.setRGB(x, y,c.getRGB());
    }
    static void downBlue(){
        int x=r.nextInt(w);
        int y=r.nextInt(h);
        Color c=new Color(i.getRGB(x, y));
        if(c.getBlue()==0)return;
        sigmaB--;
        c=new Color(c.getRed(),c.getGreen(),c.getBlue()-1);
        i.setRGB(x,y,c.getRGB());
    }
    public static void main(String[]a) throws Exception{
        Scanner in=new Scanner(System.in);
        i=ImageIO.read(file=new File(in.nextLine()));
        rbar=in.nextInt();
        gbar=in.nextInt();
        bbar=in.nextInt();
        w=i.getWidth();
        h=i.getHeight();
        final int npix=w*h;
        r=new Random(npix*(long)i.hashCode());
        for(int x=0;x<w;x++){
            for(int y=0;y<h;y++){
                Color c=new Color(i.getRGB(x, y));
                sigmaR+=c.getRed();
                sigmaG+=c.getGreen();
                sigmaB+=c.getBlue();
            }
        }
        while(sigmaR/npix<rbar){
            upRed();
        }
        while(sigmaR/npix>rbar){
            downRed();
        }
        while(sigmaG/npix<gbar){
            upGreen();
        }
        while(sigmaG/npix>gbar){
            downGreen();
        }
        while(sigmaB/npix<bbar){
            upBlue();
        }
        while(sigmaB/npix>bbar){
            downBlue();
        }
        String k=file.getName().split("\\.")[0];
        ImageIO.write(i,"png",new File(k="out_"+k+".png"));
    }
}

测试:

(40,40,40)

在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明

(150,100,100)

在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明

(75,91,110)

在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.