画像の平均を強制する


20

標準のトゥルーカラーイメージと単一の24ビットRGBカラー(0〜255の3つの数字)を取り込むプログラムを作成します。入力画像を修正(または同じ寸法の新しい画像を出力)して、その平均色が入力された正確な単一色になるようにします。これを実現するために、入力画像のピクセルを自由に変更できますが、目標は色の変化を視覚的にできるだけ目立たないようにすることです。

RGB画像の平均色は、実際には各カラーチャネルに1つずつ、3つの算術平均のセットです。平均の赤の値は、画像内のすべてのピクセルの赤の値を合計ピクセル数(画像領域)で割ったものを、最も近い整数に切り捨てたものです。緑と青の平均は同じ方法で計算されます。

このPython 2PILを使用)スクリプトは、ほとんどの画像ファイル形式の平均色を計算できます。

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
参加者は、ソリューションの有効性を示すために使用する入力色を選択できますか?それは人々が解決策を比較することを難しくしませんか?極端な場合、誰かが画像の平均に非常に近い入力色を選択することができ、そのソリューションは非常に効果的であるように見えます。
レトコラディ

1
@ vihan1086正しく理解できれば、平均色は入力画像からではなく、24ビットRGBカラー入力として提供されます。
trichoplax

3
@ vihan1086の解釈を使用し、サンプル画像を入力色のソースとして使用すると、ある画像が別の画像の平均色で表示されるのが面白いかもしれません。このようにして、異なる回答を公平に比較​​できます。
trichoplax

それに関する主な問題は、それらのほとんどが非常に灰色に近い平均値を持っていることです。Starry Nightはおそらくそれから最も遠いですが、残りは平均してかなり平坦です。
ジオビット

@RetoKoradi有権者はそのようなことを考慮に入れるのに十分な賢さを願っていますが、使用するデフォルトの平均色についてのメモを追加しました。
カルビンの趣味

回答:


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をスケーリングします。ただし、次の2つの理由から、少し注意する必要があります。

  • 0をスケーリングしても0になるため、スケーリングする前に小さなものを追加します(ここで0.01

  • RGB値は0〜255であるため、キャップされたピクセルをスケーリングしても何も行われないという事実を補うために、それに応じて比率を調整する必要があります。

画像はPNGとして保存されます。これは、JPGとして保存すると色の平均が台無しになるためです。

サンプル出力

(40、40、40)

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください

(150、100、100)

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください

(75、91、110)、星空の夜パレット

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください


2
このためには、非可逆圧縮を使用した画像形式を使用する必要があります。そのため、JPEGは適切なオプションではありません。
レトコラディ

クールな画像チャレンジソリューションのSpをいつでも期待できます。
アレックスA.

6

C ++、ガンマ補正

これは、単純なガンマ補正を使用して画像の明るさ調整を行います。ガンマ値は、コンポーネントごとに個別に決定され、ターゲット平均に一致します。

高レベルの手順は次のとおりです。

  1. 画像を読み取り、各色成分のヒストグラムを抽出します。
  2. 各コンポーネントのガンマ値のバイナリ検索を実行します。結果のヒストグラムが目的の平均になるまで、ガンマ値に対してバイナリ検索が実行されます。
  3. 画像をもう一度読み取り、ガンマ補正を適用します。

すべての画像入出力は、ASCIIのPPMファイルを使用します。画像は、GIMPを使用して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;
}

コード自体はかなり簡単です。微妙だが重要な詳細の1つは、色の値が[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のサンプル出力

すべての大きなサンプルの画像は、PNGとしてのSEサイズ制限を超えるため、JPEGとして含まれていることに注意してください。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 OK、すべての画像が含まれるようになりました。サムネイルも使用できるようになりました。大規模なものにはJPEGバージョンを使用することになりました。実際、そのうちの1つはサイズ制限を下回っていましたが、JPEGに自動変換されたようです。最初と最後の例はまだPNGです。
レトコラディ

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

Java

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)

ここに画像の説明を入力してくださいここに画像の説明を入力してくださいここに画像の説明を入力してくださいここに画像の説明を入力してくださいここに画像の説明を入力してくださいここに画像の説明を入力してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.