Forzar un promedio en una imagen


20

Escriba un programa que tome una imagen truecolor estándar y un solo color RGB de 24 bits (tres números del 0 al 255). Modifique la imagen de entrada (o envíe una nueva imagen con las mismas dimensiones) de modo que su color promedio sea ​​exactamente el único color que se ingresó. Puede modificar los píxeles de la imagen de entrada de la forma que desee para lograrlo, pero el objetivo es hacer que los cambios de color sean lo más visualmente imperceptibles posible .

El color promedio de una imagen RGB es realmente un conjunto de tres medios aritméticos , uno para cada canal de color. El valor rojo promedio es la suma de los valores rojos en todos los píxeles de la imagen divididos por el número total de píxeles (el área de la imagen), redondeados al número entero más cercano. Los promedios verde y azul se calculan de la misma manera.

Este script Python 2 (con PIL ) puede calcular el color promedio de la mayoría de los formatos de archivo de imagen:

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)

(Hay programas de color promedio similares aquí , pero no necesariamente hacen exactamente el mismo cálculo).

El requisito principal para su programa es que para cualquier imagen de entrada, el color promedio de su salida correspondiente debe coincidir exactamente con el color que se ingresó, según el fragmento de Python o algún código equivalente. La imagen de salida también debe tener exactamente las mismas dimensiones que la imagen de entrada.

Por lo tanto, técnicamente podría enviar un programa que simplemente colorea la entrada completa con el color promedio especificado (porque el promedio siempre sería ese color), pero este es un concurso de popularidad : la presentación con el mayor número de votos ganará , y será tan trivial la sumisión no le dará muchos votos a favor. Ideas novedosas como aprovechar las peculiaridades de la visión humana, o reducir la imagen y dibujar un borde de color a su alrededor (con suerte) le dará votos.

Tenga en cuenta que ciertas combinaciones de colores e imágenes promedio requieren cambios de color extremadamente notables. Por ejemplo, si el color promedio para igualar fuera negro (0, 0, 0), cualquier imagen de entrada necesitaría hacerse completamente negra porque si los píxeles tuvieran valores distintos de cero, también harían que el promedio no sea cero ( salvo errores de redondeo). Tenga en cuenta tales limitaciones al votar.

Imágenes de prueba

Algunas imágenes y sus colores promedio predeterminados para jugar. Haga clic para ver los tamaños completos.

A. promedio (127, 127, 127)

De las imágenes de fejesjoco con respuesta de todos los colores . Encontrado original en su blog .

B. promedio (62, 71, 73)

Yokohama . Proporcionado por Geobits .

C. promedio (115, 112, 111)

Tokio . Proporcionado por Geobits .

D. promedio (154, 151, 154)

La cascada de Escher . Original .

E. promedio (105, 103, 102)

Monte Shasta . Proporcionado por mí

F. promedio (75, 91, 110)

La noche estrellada

Notas

  • Los formatos exactos de entrada y salida y los tipos de archivos de imagen que usa su programa no importan demasiado. Solo asegúrese de que esté claro cómo usar su programa.
  • Probablemente sea una buena idea (pero técnicamente no es un requisito) que si una imagen ya tiene el color promedio objetivo, debería salir como está.
  • Publique imágenes de prueba con la entrada de color promedio como (150, 100, 100) o (75, 91, 110), para que los votantes puedan ver las mismas entradas en diferentes soluciones. (Publicar más ejemplos que esto está bien, incluso alentado).

2
Los participantes pueden elegir los colores de entrada que usan para demostrar la efectividad de su solución. ¿Eso no dificulta que la gente compare las soluciones? En el caso extremo, alguien podría elegir colores de entrada que sean muy similares al promedio de la imagen, y parecería que su solución es muy efectiva.
Reto Koradi

1
@ vihan1086 Si he entendido correctamente, el color promedio se proporciona como una entrada de color RGB de 24 bits, que no se encuentra en una imagen de entrada.
trichoplax

3
Puede ser interesante usar la interpretación de @ vihan1086 y usar las imágenes de ejemplo como fuente de colores de entrada para que una imagen se muestre en el color promedio de otra. De esta manera, las diferentes respuestas se pueden comparar de manera justa.
trichoplax

El principal problema con eso es que la mayoría de ellos tienen un promedio muy cercano al gris. Starry Night es probablemente el más alejado de eso, pero el resto promedia bastante rotundamente.
Geobits

@RetoKoradi Con suerte, los votantes serán lo suficientemente inteligentes como para tener en cuenta estas cosas, aunque he agregado una nota sobre qué colores promedio predeterminados usar.
Calvin's Hobbies

Respuestas:


11

Python 2 + PIL, escala de color simple

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)

Aquí hay un enfoque ingenuo que debería servir como una buena línea de base. En cada iteración, comparamos nuestro promedio actual con el promedio deseado, y escalamos el RGB de cada píxel en la proporción correspondiente. Sin embargo, debemos tener un poco de cuidado por dos razones:

  • Escalar 0 todavía da como resultado 0, así que antes de escalar agregamos algo pequeño (aquí 0.01)

  • Los valores RGB están entre 0 y 255, por lo que debemos ajustar la relación en consecuencia para compensar el hecho de que el escalado de píxeles limitados no hace nada.

Las imágenes se guardan como PNG porque guardar como JPG parece estropear los promedios de color.

Salida de muestra

(40, 40, 40)

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

(150, 100, 100)

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

(75, 91, 110), paleta Starry Night

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí


2
Definitivamente desea utilizar un formato de imagen con compresión sin pérdidas para esto. Entonces JPEG no es una buena opción.
Reto Koradi

Siempre puede contar con Sp para obtener soluciones geniales de desafío de imagen.
Alex A.

6

C ++, corrección gamma

Esto hace un ajuste de brillo de la imagen usando una corrección gamma simple, con el valor gamma determinado por separado para cada componente para que coincida con el promedio objetivo.

Los pasos de alto nivel son:

  1. Lea la imagen y extraiga el histograma para cada componente de color.
  2. Realice una búsqueda binaria del valor gamma para cada componente. Se realiza una búsqueda binaria en los valores gamma, hasta que el histograma resultante tenga el promedio deseado.
  3. Lea la imagen por segunda vez y aplique la corrección gamma.

Todas las entradas / salidas de imágenes utilizan archivos PPM en ASCII. Las imágenes se convirtieron de / a PNG usando GIMP. El código se ejecutó en una Mac, las conversiones de imágenes se realizaron en 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;
}

El código en sí es bastante sencillo. Un detalle sutil pero importante es que, mientras los valores de color están en el rango [0, 255], los mapeo a la curva gamma como si el rango fuera [-1, 256]. Esto permite que el promedio se fuerce a 0 o 255. De lo contrario, 0 siempre sería 0 y 255 siempre sería 255, lo que podría nunca permitir un promedio de 0/255.

Usar:

  1. Guarde el código en un archivo con extensión .cpp, por ejemplo force.cpp.
  2. Compilar con c++ -o force -O2 force.cpp.
  3. Corre con ./force input.ppm targetR targetG target >output.ppm.

Salida de muestra para 40, 40, 40

Tenga en cuenta que las imágenes para todas las muestras más grandes se incluyen como JPEG, ya que exceden el límite de tamaño SE como PNG. Dado que JPEG es un formato de compresión con pérdida, es posible que no coincidan exactamente con el promedio objetivo. Tengo la versión PNG de todos los archivos, que coincide exactamente.

Af1 Bf1 Cf1 Df1 Ef1 Ff1

Salida de muestra para 150, 100, 100:

Af2 Bf2 Cf2 Df2 Ef2 Ff2

Salida de muestra para 75, 91, 110:

Af3 Bf3 Cf3 Df3 Ef3 Ff3


Tuve que reducir las otras imágenes para alcanzar el límite, ¿tal vez intentar eso?
Sp3000

@ Sp3000 Ok, tengo todas las imágenes incluidas ahora. También con miniaturas ahora. Terminé usando la versión JPEG para los grandes. En realidad, uno de ellos estaba por debajo del límite de tamaño, pero parece que se convirtió automáticamente a JPEG. El primer y el último ejemplo siguen siendo 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)

Esto itera a través de cada píxel en un orden aleatorio y reduce la distancia entre cada componente del color del píxel 255o 0(dependiendo de si el promedio actual es menor o mayor que el promedio deseado). La distancia se reduce por un factor multiplicativo fijo. Esto se repite hasta obtener el promedio deseado. La reducción es siempre al menos 1, a menos que el color sea 255(o 0), para garantizar que el procesamiento no se detenga una vez que el píxel esté cerca del blanco o el negro.

Salida de muestra

(40, 40, 40)

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

(150, 100, 100)

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

(75, 91, 110)

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí


1

Java

Un enfoque basado en RNG. Un poco lento para grandes imágenes de entrada.

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

Pruebas:

(40,40,40)

ingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquí

(150,100,100)

ingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquí

(75,91,110)

ingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquíingrese la descripción de la imagen aquí

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.