Forçar uma média em uma imagem

20

Escreva um programa que capte uma imagem truecolor padrão e uma única cor RGB de 24 bits (três números de 0 a 255). Modifique a imagem de entrada (ou produza uma nova imagem com as mesmas dimensões) para que sua cor média seja exatamente a única cor que foi inserida. Você pode modificar os pixels na imagem de entrada da maneira que desejar, mas o objetivo é fazer com que as alterações de cores sejam visualmente imperceptíveis quanto possível .

A cor média de uma imagem RGB é realmente um conjunto de três médias aritméticas , uma para cada canal de cores. O valor médio vermelho é a soma dos valores vermelhos em todos os pixels da imagem divididos pelo número total de pixels (a área da imagem), arredondados para o número inteiro mais próximo. As médias verde e azul são calculadas da mesma maneira.

Este Python 2 script (com PIL ) pode calcular a cor média da maioria dos formatos de arquivo de imagem:

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)

(Existem programas de cores médios semelhantes aqui , mas eles não fazem exatamente o mesmo cálculo.)

O principal requisito para o seu programa é que, para qualquer imagem de entrada, a cor média da saída correspondente deva corresponder exatamente à cor que foi inserida - conforme julgado pelo snippet do Python ou algum código equivalente. A imagem de saída também deve ter exatamente as mesmas dimensões da imagem de entrada.

Assim, você pode enviar tecnicamente um programa que simplesmente colore toda a entrada com a cor média especificada (porque a média sempre seria essa cor), mas esse é um concurso de popularidade - a submissão com o maior número de votos vencerá , e é tão trivial a submissão não recebe muitos votos positivos. Novas idéias, como tirar proveito das peculiaridades da visão humana, ou diminuir a imagem e desenhar uma borda colorida em torno dela (espero), você recebe votos.

Observe que certas combinações de cores e imagens comuns exigem alterações de cor extremamente visíveis. Por exemplo, se a cor média a combinar fosse preta (0, 0, 0), qualquer imagem de entrada precisaria ser totalmente preta, pois se algum pixel tivesse valores diferentes de zero, também tornaria a média diferente de zero ( exceto erros de arredondamento). Lembre-se de tais limitações ao votar.

Imagens de teste

Algumas imagens e suas cores médias padrão para brincar. Clique para ver os tamanhos completos.

A. média (127, 127, 127)

De fejesjoco 's Imagens com todas as cores responder . Original encontrado em seu blog .

B. média (62, 71, 73)

Yokohama . Fornecido por Geobits .

C. média (115, 112, 111)

Tóquio . Fornecido por Geobits .

D. média (154, 151, 154)

Cachoeira de Escher . Original .

E. média (105, 103, 102)

Monte Shasta . Fornecido por mim.

F. média (75, 91, 110)

A noite estrelada

Notas

  • Os formatos exatos de entrada e saída e os tipos de arquivo de imagem usados ​​pelo seu programa não importam muito. Apenas verifique se está claro como usar seu programa.
  • Provavelmente, é uma boa ideia (mas não tecnicamente um requisito) que, se uma imagem já tiver a cor média da meta, ela deve ser impressa como está.
  • Poste imagens de teste com a entrada de cores média como (150, 100, 100) ou (75, 91, 110), para que os eleitores possam ver as mesmas entradas em diferentes soluções. (Publicar mais exemplos do que este é bom, até incentivado.)
Passatempos de Calvin
fonte
2
Os participantes escolhem as cores de entrada que usam para demonstrar a eficácia de sua solução? Isso não torna difícil para as pessoas comparar as soluções? No caso extremo, alguém poderia escolher cores de entrada muito semelhantes à média da imagem e pareceria que sua solução é muito eficaz.
Reto Koradi
1
@ vihan1086 Se entendi corretamente, a cor média é fornecida como uma entrada de cores RGB de 24 bits, não encontrada em uma imagem de entrada.
Trichoplax
3
Pode ser interessante usar a interpretação de @ vihan1086 e usar as imagens de exemplo como fonte das cores de entrada, para que uma imagem seja exibida na cor média de outra. Dessa forma, diferentes respostas podem ser comparadas de maneira justa.
31515 trichoplax
O principal problema disso é que a maioria deles tem uma média muito próxima do cinza. Noite estrelada é provavelmente a mais distante disso, mas o resto fica na média.
Geobits
@RetoKoradi Espero que os eleitores sejam inteligentes o suficiente para levar essas coisas em consideração, embora eu tenha acrescentado uma nota sobre quais cores médias padrão usar.
24715 Calvin's Hobbies

Respostas:

11

Python 2 + PIL, escala de cores simples

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)

Aqui está uma abordagem ingênua que deve servir como uma boa linha de base. A cada iteração, comparamos nossa média atual com a média desejada e escalamos o RGB de cada pixel pela proporção correspondente. Temos que ter um pouco de cuidado, porém, por duas razões:

  • Escalar 0 ainda resulta em 0; portanto, antes de escalar, adicionamos algo pequeno (aqui 0.01)

  • Os valores RGB estão entre 0 e 255, portanto, precisamos ajustar a proporção para compensar o fato de que a escala de pixels limitados não faz nada.

As imagens são salvas como PNG porque salvar como JPG parece prejudicar as médias de cores.

Saída de amostra

(40, 40, 40)

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

(150, 100, 100)

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

(75, 91, 110), paleta Noite estrelada

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

Sp3000
fonte
2
Você definitivamente deseja usar um formato de imagem com compactação sem perdas para isso. Portanto, o JPEG não é uma boa opção.
Reto Koradi 23/07/2015
Você sempre pode contar com o Sp para soluções interessantes de desafio de imagem.
23415 Alex A.
6

C ++, correção de gama

Isso faz um ajuste de brilho da imagem usando uma correção gama simples, com o valor gama determinado separadamente para cada componente para corresponder à média alvo.

As etapas de alto nível são:

  1. Leia a imagem e extraia o histograma para cada componente de cor.
  2. Execute uma pesquisa binária do valor gama para cada componente. Uma pesquisa binária é realizada nos valores gama, até que o histograma resultante tenha a média desejada.
  3. Leia a imagem uma segunda vez e aplique a correção gama.

Toda entrada / saída de imagem usa arquivos PPM em ASCII. As imagens foram convertidas de / para PNG usando o GIMP. O código foi executado em um Mac, as conversões de imagem foram feitas no Windows.

Código:

#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;
}

O código em si é bastante direto. Um detalhe sutil, mas importante, é que, enquanto os valores das cores estão no intervalo [0, 255], mapeio-os para a curva gama como se o intervalo fosse [-1, 256]. Isso permite que a média seja forçada a 0 ou 255. Caso contrário, 0 permanecerá sempre 0 e 255 permanecerá sempre 255, o que talvez nunca permita uma média de 0/255.

Usar:

  1. Salve o código em um arquivo com extensão .cpp, por exemplo force.cpp.
  2. Compile com c++ -o force -O2 force.cpp.
  3. Corra com ./force input.ppm targetR targetG target >output.ppm.

Saída de amostra para 40, 40, 40

Observe que as imagens para todas as amostras maiores são incluídas como JPEGs, pois elas excedem o limite de tamanho SE como PNGs. Como o JPEG é um formato de compactação com perdas, eles podem não corresponder exatamente à média de destino. Eu tenho a versão PNG de todos os arquivos, que corresponde exatamente.

Af1 Bf1 Cf1 Df1 Ef1 Ff1

Saída de amostra para 150, 100, 100:

Af2 Bf2 Cf2 Df2 Ef2 Ff2

Saída de amostra para 75, 91, 110:

Af3 Bf3 Cf3 Df3 Ef3 Ff3

Reto Koradi
fonte
Eu tive que diminuir as outras imagens para atingir o limite - talvez tente isso?
Sp3000 27/07/2015
@ Sp3000 Ok, tenho todas as imagens incluídas agora. Também com miniaturas agora. Acabei usando a versão JPEG para as grandes. Na verdade, um deles estava abaixo do limite de tamanho, mas parece que foi convertido automaticamente em JPEG. O primeiro e o último exemplo ainda são PNGs.
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)

Isso percorre cada pixel em uma ordem aleatória e reduz a distância entre cada componente da cor do pixel e 255ou 0(dependendo se a média atual é menor ou maior que a média desejada). A distância é reduzida por um fator multiplicativo fixo. Isso é repetido até que a média desejada seja obtida. A redução é sempre pelo menos 1, a menos que a cor seja 255(ou 0), para garantir que o processamento não seja interrompido quando o pixel estiver perto de branco ou preto.

Saída de amostra

(40, 40, 40)

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

(150, 100, 100)

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

(75, 91, 110)

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

es1024
fonte
1

Java

Uma abordagem baseada em RNG. Um pouco lento para imagens de entrada grandes.

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"));
    }
}

Testes:

(40,40,40)

insira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aqui

(150.100.100)

insira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aqui

(75,91.110)

insira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aquiinsira a descrição da imagem aqui

SuperJedi224
fonte