Forcer une moyenne sur une image

20

Écrivez un programme qui prend une image truecolor standard et une seule couleur RVB 24 bits (trois chiffres de 0 à 255). Modifiez l'image d'entrée (ou sortez une nouvelle image avec les mêmes dimensions) de sorte que sa couleur moyenne soit exactement la couleur unique qui a été entrée. Vous pouvez modifier les pixels de l'image d'entrée de la manière que vous souhaitez pour y parvenir, mais l'objectif est de rendre les changements de couleur aussi visuellement imperceptibles que possible .

La couleur moyenne d'une image RVB est en réalité un ensemble de trois moyennes arithmétiques , une pour chaque canal de couleur. La valeur rouge moyenne est la somme des valeurs rouges sur tous les pixels de l'image divisée par le nombre total de pixels (la zone d'image), arrondie à l'entier le plus proche. Les moyennes verte et bleue sont calculées de la même manière.

Ce script Python 2 (avec PIL ) peut calculer la couleur moyenne de la plupart des formats de fichier image:

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)

(Il existe ici des programmes de couleurs moyennes similaires , mais ils ne font pas nécessairement exactement le même calcul.)

La principale exigence de votre programme est que pour toute image d'entrée, la couleur moyenne de sa sortie correspondante doit correspondre exactement à la couleur qui a été entrée - comme jugé par l'extrait de code Python ou un code équivalent. L'image de sortie doit également avoir exactement les mêmes dimensions que l'image d'entrée.

Donc, vous pouvez techniquement soumettre un programme qui colore simplement l'entrée entière de la couleur moyenne spécifiée (parce que la moyenne serait toujours cette couleur), mais c'est un concours de popularité - la soumission avec le plus grand nombre de votes gagnera , et c'est si trivial la soumission ne vous apportera pas beaucoup de votes positifs. De nouvelles idées comme tirer parti des caprices de la vision humaine, ou réduire l'image et dessiner une bordure colorée autour d'elle (espérons-le) vous feront voter.

Notez que certaines combinaisons de couleurs et d'images moyennes nécessitent des changements de couleur extrêmement perceptibles. Par exemple, si la couleur moyenne à associer était noire (0, 0, 0), toute image d'entrée devrait être complètement noire, car si des pixels avaient des valeurs non nulles, ils rendraient également la moyenne non nulle ( sauf erreur d’arrondi). Gardez ces limites à l'esprit lors du vote.

Images de test

Certaines images et leurs couleurs moyennes par défaut pour jouer avec. Cliquez pour les tailles complètes.

A. moyenne (127, 127, 127)

Des images de fejesjoco avec toutes les couleurs répondent . Trouvé original sur son blog .

B. moyenne (62, 71, 73)

Yokohama . Fourni par Geobits .

C. moyenne (115, 112, 111)

Tokyo . Fourni par Geobits .

D. moyenne (154, 151, 154)

Cascade d'Escher . Original .

E. moyenne (105, 103, 102)

Mont Shasta . Fourni par moi.

F. moyen (75, 91, 110)

La nuit étoilée

Remarques

  • Les formats d'entrée et de sortie exacts et les types de fichiers image utilisés par votre programme importent peu. Assurez-vous simplement qu'il est clair comment utiliser votre programme.
  • C'est probablement une bonne idée (mais ce n'est pas une exigence technique) que si une image a déjà la couleur moyenne cible, elle doit être sortie telle quelle.
  • Veuillez publier des images de test avec la couleur d'entrée moyenne (150, 100, 100) ou (75, 91, 110), afin que les votants puissent voir les mêmes entrées dans différentes solutions. (Publier plus d'exemples que cela est bien, même encouragé.)
Loisirs de Calvin
la source
2
Les participants peuvent choisir les couleurs d'entrée qu'ils utilisent pour démontrer l'efficacité de leur solution? Cela ne rend-il pas difficile pour les gens de comparer les solutions? Dans le cas extrême, quelqu'un pourrait choisir des couleurs d'entrée très similaires à la moyenne de l'image, et il semblerait que leur solution soit très efficace.
Reto Koradi
1
@ vihan1086 Si j'ai bien compris, la couleur moyenne est fournie en tant qu'entrée couleur RVB 24 bits, introuvable dans une image d'entrée.
trichoplax
3
Il pourrait être intéressant d'utiliser l'interprétation de @ vihan1086 et d'utiliser les exemples d'images comme source de couleurs d'entrée afin qu'une image soit affichée dans la couleur moyenne d'une autre. De cette façon, différentes réponses peuvent être comparées équitablement.
trichoplax
Le principal problème est que la plupart d'entre eux ont une moyenne très proche du gris. La nuit étoilée est probablement la plus éloignée de cela, mais le reste est assez plat.
Geobits
@RetoKoradi Espérons que les électeurs seront assez intelligents pour prendre en compte de telles choses, bien que j'aie ajouté une note sur les couleurs moyennes par défaut à utiliser.
Calvin's Hobbies

Réponses:

11

Python 2 + PIL, mise à l'échelle simple des couleurs

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)

Voici une approche naïve qui devrait servir de bonne base de référence. À chaque itération, nous comparons notre moyenne actuelle avec la moyenne souhaitée et mettons à l'échelle le RVB de chaque pixel selon le rapport correspondant. Nous devons cependant être un peu prudents, pour deux raisons:

  • La mise à l'échelle de 0 donne toujours 0, donc avant de mettre à l'échelle, nous ajoutons quelque chose de petit (ici 0.01)

  • Les valeurs RVB sont comprises entre 0 et 255, nous devons donc ajuster le rapport en conséquence pour compenser le fait que la mise à l'échelle des pixels plafonnés ne fait rien.

Les images sont enregistrées au format PNG, car l'enregistrement au format JPG semble perturber les moyennes des couleurs.

Exemple de sortie

(40, 40, 40)

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

(150, 100, 100)

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

(75, 91, 110), palette Nuit étoilée

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

Sp3000
la source
2
Vous voulez certainement utiliser un format d'image avec une compression sans perte pour cela. JPEG n'est donc pas une bonne option.
Reto Koradi
Vous pouvez toujours compter sur Sp pour des solutions de challenge d'images sympas.
Alex A.
6

C ++, correction gamma

Cela permet d'ajuster la luminosité de l'image à l'aide d'une simple correction gamma, la valeur gamma étant déterminée séparément pour chaque composant pour correspondre à la moyenne cible.

Les étapes de haut niveau sont les suivantes:

  1. Lisez l'image et extrayez l'histogramme pour chaque composante de couleur.
  2. Effectuez une recherche binaire de la valeur gamma pour chaque composant. Une recherche binaire est effectuée sur les valeurs gamma, jusqu'à ce que l'histogramme résultant ait la moyenne souhaitée.
  3. Lisez l'image une deuxième fois et appliquez la correction gamma.

Toutes les entrées / sorties d'images utilisent des fichiers PPM en ASCII. Les images ont été converties de / vers PNG à l'aide de GIMP. Le code a été exécuté sur un Mac, les conversions d'images ont été effectuées sur Windows.

Code:

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

Le code lui-même est assez simple. Un détail subtil mais important est que, alors que les valeurs de couleur sont dans la plage [0, 255], je les mappe sur la courbe gamma comme si la plage était [-1, 256]. Cela permet à la moyenne d'être forcée à 0 ou 255. Sinon, 0 resterait toujours 0 et 255 resterait toujours 255, ce qui pourrait ne jamais permettre une moyenne de 0/255.

Utiliser:

  1. Enregistrez le code dans un fichier avec une extension .cpp, par exemple force.cpp.
  2. Compilez avec c++ -o force -O2 force.cpp.
  3. Courez avec ./force input.ppm targetR targetG target >output.ppm.

Exemple de sortie pour 40, 40, 40

Notez que les images de tous les échantillons plus grands sont incluses en JPEG car elles dépassent la limite de taille SE en PNG. Étant donné que JPEG est un format de compression avec perte, ils peuvent ne pas correspondre exactement à la moyenne cible. J'ai la version PNG de tous les fichiers, qui correspond exactement.

Af1 Bf1 Cf1 Df1 Ef1 Ff1

Exemple de sortie pour 150, 100, 100:

Af2 Bf2 Cf2 Df2 Ef2 Ff2

Exemple de sortie pour 75, 91, 110:

Af3 Bf3 Cf3 Df3 Ef3 Ff3

Reto Koradi
la source
J'ai dû réduire les autres images pour respecter la limite - peut-être essayer cela?
Sp3000
@ Sp3000 Ok, toutes les images sont incluses maintenant. Maintenant aussi avec des vignettes. J'ai fini par utiliser la version JPEG pour les grandes. En fait, l'un d'eux était en dessous de la taille limite, mais il semble qu'il ait été automatiquement converti en JPEG. Les premier et dernier exemples sont toujours des fichiers 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)

Cela parcourt chaque pixel dans un ordre aléatoire et réduit la distance entre chaque composante de la couleur du pixel et 255ou 0(selon que la moyenne actuelle est inférieure ou supérieure à la moyenne souhaitée). La distance est réduite d'un facteur multiplicatif fixe. Ceci est répété jusqu'à ce que la moyenne souhaitée soit obtenue. La réduction est toujours au moins 1, sauf si la couleur est 255(ou 0), pour garantir que le traitement ne se bloque pas une fois que le pixel est proche du blanc ou du noir.

Exemple de sortie

(40, 40, 40)

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

(150, 100, 100)

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

(75, 91, 110)

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

es1024
la source
1

Java

Une approche basée sur RNG. Un peu lent pour les grandes images d'entrée.

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

Tests:

(40,40,40)

entrez la description de l'image icientrez la description de l'image icientrez la description de l'image icientrez la description de l'image icientrez la description de l'image icientrez la description de l'image ici

(150 100 100)

entrez la description de l'image icientrez la description de l'image icientrez la description de l'image icientrez la description de l'image icientrez la description de l'image icientrez la description de l'image ici

(75,91 110)

entrez la description de l'image icientrez la description de l'image icientrez la description de l'image icientrez la description de l'image icientrez la description de l'image icientrez la description de l'image ici

SuperJedi224
la source