Bir Görüntüye Ortalamayı Zorlama


20

Standart bir gerçek renkli görüntü ve tek bir 24 bit RGB rengi (0 ile 255 arasında üç sayı) alan bir program yazın . Giriş görüntüsünü, ortalama rengi tam olarak girilen tek renk olacak şekilde değiştirin (veya aynı boyutlara sahip yeni bir görüntü çıktılayın) . Giriş görüntüsündeki pikselleri, bunu başarmak için istediğiniz şekilde değiştirebilirsiniz, ancak amaç, renk değişikliklerini görsel olarak mümkün olduğunca fark edilmez hale getirmektir .

Ortalama renk RGB görüntünün çok üç kümesidir aritmetik ortalama , her renk kanalı için bir tane olmak. Ortalama kırmızı değer, görüntüdeki tüm piksellerdeki kırmızı değerlerin toplamının, en yakın tam sayıya yuvarlanmış toplam piksel sayısına (görüntü alanı) bölünmesiyle elde edilir. Yeşil ve mavi ortalamalar aynı şekilde hesaplanır.

Bu Python 2 ( PIL ile ) komut dosyası, çoğu görüntü dosyası formatının ortalama rengini hesaplayabilir:

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)

(Orada benzer ortalama renk programlardır burada , ama mutlaka aynı hesaplamayı yapmıyoruz.)

Programınız için temel gereksinim, herhangi bir giriş görüntüsü için, karşılık gelen çıktının ortalama renginin , Python snippet'i veya eşdeğer bir kod tarafından değerlendirildiği gibi girilen renkle tam olarak eşleşmesi gerektiğidir . Çıktı görüntüsü de girdi görüntüsü ile aynı boyutlara sahip olmalıdır.

Böylece, teknik olarak tüm girdiyi belirtilen ortalama rengi renklendiren bir program gönderebilirsiniz (çünkü ortalama her zaman bu renk olurdu), ancak bu bir popülerlik yarışmasıdır - en yüksek oy sayısına sahip olmak kazanır ve bu kadar önemsiz boyun eğme size çok fazla oy vermeyecektir. İnsan vizyonundaki tuhaflıklardan yararlanmak veya görüntüyü küçültmek ve etrafına renkli bir sınır çizmek gibi yeni fikirler (umarım) size oy verecektir.

Ortalama renklerin ve görüntülerin belirli kombinasyonlarının son derece dikkat çekici renk değişiklikleri gerektirdiğini unutmayın. Örneğin, eşleşecek ortalama renk siyah olsaydı (0, 0, 0), herhangi bir giriş görüntüsünün tamamen siyah yapılması gerekir, çünkü herhangi bir pikselin sıfır olmayan değerleri varsa, ortalamayı sıfırdan farklı yaparlar ( engelleme yuvarlama hataları). Oy verirken bu tür sınırlamaları göz önünde bulundurun.

Test Görüntüleri

Bazı görüntüler ve varsayılan ortalama renkleri ile oynamak için. Tüm boyutlar için tıklayınız.

A. ortalama (127, 127, 127)

Gönderen fejesjoco 'ın tüm renkleriyle Görüntüler cevap . Bulunan orijinal üzerinde kendi blogunda .

B. ortalama (62, 71, 73)

Yokohama . Geobits tarafından sağlanmıştır .

C. ortalama (115, 112, 111)

Tokyo . Geobits tarafından sağlanmıştır .

D. ortalama (154, 151, 154)

Escher Şelalesi . Orijinal .

E. ortalama (105, 103, 102)

Shasta Dağı . Sağladıklarım.

F. ortalama (75, 91, 110)

Yıldızlı Gece

notlar

  • Programınızın kullandığı kesin girdi ve çıktı biçimleri ve görüntü dosyası türleri önemli değildir. Programınızı nasıl kullanacağınızın açık olduğundan emin olun.
  • Muhtemelen iyi bir fikirdir (ancak teknik olarak bir gereklilik değildir), bir görüntünün zaten hedef ortalama rengine sahip olması durumunda, çıktısının olduğu gibi çıkması gerekir.
  • Lütfen ortalama renk girişine (150, 100, 100) veya (75, 91, 110) olarak test görüntüleri gönderin, böylece seçmenler farklı çözümleri aynı girişleri görebilir. (Bundan daha fazla örnek göndermek iyidir, hatta teşvik edilir.)

2
Katılımcılar çözümlerinin etkinliğini göstermek için kullandıkları girdi renklerini seçebilirler mi? Bu, insanların çözümleri karşılaştırmasını zorlaştırmıyor mu? Aşırı durumda, bir kişi görüntünün ortalamasına çok benzeyen giriş renklerini seçebilir ve çözümleri çok etkili gibi görünecektir.
Reto Koradi

1
@ vihan1086 Doğru anladıysam, ortalama renk bir giriş görüntüsünde bulunmayan 24 bit RGB renk girişi olarak sağlanır.
trichoplax

3
@ Vihan1086'nın yorumunu kullanmak ve örnek renkleri giriş renklerinin kaynağı olarak kullanmak ilginç olabilir, böylece bir görüntü diğerinin ortalama renginde görüntülenir. Bu şekilde farklı cevaplar adil bir şekilde karşılaştırılabilir.
trichoplax

Ana sorun çoğu griye çok yakın bir ortalamaya sahip olmasıdır. Yıldızlı Gece muhtemelen bundan en uzak olanıdır, ancak geri kalanı oldukça düzdür.
Geobits

@RetoKoradi Umarım seçmenler bu tür şeyleri dikkate alacak kadar akıllı olacaklar, ancak hangi varsayılan ortalama renklerin kullanılacağına dair bir not ekledim.
Calvin'in Hobileri

Yanıtlar:


11

Python 2 + PIL, basit renk ölçeklendirme

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)

İşte iyi bir temel teşkil etmesi gereken saf bir yaklaşım. Her bir yinelemede, mevcut ortalamamızı istenen ortalama ile karşılaştırır ve her pikselin RGB'sini uygun oranla ölçeklendiririz. Yine de iki nedenden dolayı biraz dikkatli olmalıyız:

  • Ölçeklendirme 0 hala 0 ile sonuçlanır, bu nedenle ölçeklendirmeden önce küçük bir şey ekleriz (burada 0.01)

  • RGB değerleri 0 ile 255 arasında olduğundan, başlıklı piksellerin ölçeklendirilmesinin hiçbir şey yapmamasını sağlamak için oranı buna göre ayarlamamız gerekir.

Görüntüler PNG olarak kaydedilir, çünkü JPG olarak kaydetmek renk ortalamalarını bozar.

Örnek çıktı

(40, 40, 40)

resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin

(150, 100, 100)

resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin

(75, 91, 110), Yıldızlı Gece paleti

resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin


2
Bunun için kesinlikle kayıpsız sıkıştırmaya sahip bir görüntü formatı kullanmak istiyorsunuz. Yani JPEG iyi bir seçenek değil.
Reto Koradi

Harika görüntü meydan okuma çözümleri için her zaman Sp'ye güvenebilirsiniz.
Alex

6

C ++, gama düzeltmesi

Bu, basit bir gama düzeltmesi kullanarak görüntünün parlaklık ayarını yapar ve her bir bileşen için hedef ortalamaya uyması için gama değeri ayrı ayrı belirlenir.

Üst düzey adımlar:

  1. Her renk bileşeni için görüntüyü okuyun ve histogramı çıkarın.
  2. Her bileşen için gama değerinin ikili aramasını yapın. Ortaya çıkan histogram istenen ortama ulaşıncaya kadar gama değerleri üzerinde ikili bir arama yapılır.
  3. Görüntüyü ikinci kez okuyun ve gama düzeltmesini uygulayın.

Tüm görüntü giriş / çıkışları ASCII'de PPM dosyaları kullanır. Görüntüler GIMP kullanılarak / PNG biçimine dönüştürüldü. Kod bir Mac'te çalıştırıldı, görüntü dönüşümleri Windows'ta yapıldı.

Kod:

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

Kodun kendisi oldukça basittir. İnce ama önemli bir ayrıntı, renk değerleri [0, 255] aralığındayken, onları aralık [-1, 256] gibi gama eğrisiyle eşliyorum. Bu, ortalamanın 0 veya 255'e zorlanmasına izin verir. Aksi takdirde, 0 her zaman 0 olarak kalır ve 255, her zaman 255 kalır ve bu da hiçbir zaman ortalama 0/255'e izin vermez.

Kullanmak:

  1. Kodu, uzantılı bir dosyaya kaydedin .cpp, örn force.cpp.
  2. İle derleyin c++ -o force -O2 force.cpp.
  3. İle çalıştırın ./force input.ppm targetR targetG target >output.ppm.

40, 40, 40 için örnek çıktı

Tüm büyük örneklerin görüntülerinin, PNG olarak SE boyut sınırını aştığı için JPEG olarak dahil edildiğini unutmayın. JPEG kayıplı bir sıkıştırma formatı olduğundan, hedef ortalamaya tam olarak uymayabilir. Tam olarak eşleşen tüm dosyaların PNG sürümü var.

af1 BF1 CF1 df1 EF1 Ff1

150, 100, 100 için örnek çıktı:

AF2 bf2 cf2 df2 Ef2 FF2

75, 91, 110 için örnek çıktı:

AF3'de BF3 cf3 df3 EF3'ü FF3


Sınırı karşılamak için diğer görüntüleri küçültmek zorunda kaldım - belki denemek?
Sp3000

@ Sp3000 Tamam, şimdi tüm resimleri dahil ettim. Ayrıca şimdi küçük resimlerle. Büyükler için JPEG sürümünü kullandım. Aslında, bunlardan biri boyut sınırının altındaydı, ancak otomatik olarak JPEG'ye dönüştürülmüş gibi görünüyor. İlk ve son örnekler hala PNG'lerdir.
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)

Bu, her piksel boyunca rastgele bir sırayla yinelenir ve pikselin renginin her bir bileşeni arasındaki mesafeyi azaltır ve ( 255veya 0mevcut ortalamanın istenen ortalamadan daha az veya daha büyük olmasına bağlı olarak). Mesafe sabit bir çarpma faktörü ile azaltılır. Bu, istenen ortalama elde edilene kadar tekrarlanır. Piksel beyaz veya siyaha yakın olduğunda işlemin durmamasını sağlamak 1için renk 255(veya 0) olmadığı sürece azalma en azından her zaman olur .

Örnek çıktı

(40, 40, 40)

resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin

(150, 100, 100)

resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin

(75, 91, 110)

resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin resim açıklamasını buraya girin


1

Java

RNG tabanlı bir yaklaşım. Büyük giriş görüntüleri için biraz yavaş.

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

Testler:

(40,40,40)

resim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girin

(150.100.100)

resim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girin

(75,91,110)

resim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girinresim açıklamasını buraya girin

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.