用于像素混合的调色板量化

Posted

技术标签:

【中文标题】用于像素混合的调色板量化【英文标题】:Palette quantization for pixel mixing 【发布时间】:2021-02-26 04:39:56 【问题描述】:

对于使用硬件调色板的图形模式,可以通过在两个图像之间快速切换来实现更多颜色的错觉(我不知道这有什么通用名称)。

有什么算法可以从普通的全彩色图像中计算出最佳(或半最佳)调色板?两个目标图像要么共享相同的调色板,要么拥有自己的调色板。两种情况的算法都很有趣(如果它们根本不同的话)。

举个例子:假设我有一个随机的全彩色 PNG 图像,其中包含数千种颜色(每个通道 8 位,完全不透明),并且想要创建两个 GIF 图像(非动画),每个图像有 256 种颜色。假设我每帧(以 60Hz 的帧速率)在这些 GIF 图像之间切换,因此结果显示为两个图像之间的混合。问题是:如何计算最佳调色板在两个 GIF 文件之间共享?或者两个不同的调色板,每个文件一个? (这两种情况都很有趣)。

更新:

只是为了创建一个我想做的例子,我通过使用随机突变进化出合适的调色板来强制结果。为了便于在 2D 中可视化,我使用了去除了蓝色通道的图像。原来的样子是这样的。

颜色分布如下所示。除了一些使用高达 90 次的深(几乎是黑色)灰色和一些使用约 30 次的亮黄色之外,使用的颜色之间的使用分布非常均匀。

这是我想出的 16 色调色板。我不知道它离最佳调色板有多远,但它似乎很接近。

混合来自两个图像的颜色会产生更大的颜色集。多大取决于颜色之间的允许距离(距离越大,闪烁越明显)。最大距离为 100。

这些混合颜色很好地覆盖了色彩空间,如图所示,它显示了从每种原始颜色到最近(混合)调色板颜色的距离。一些边缘颜色的距离可达 40。但几乎所有颜色的距离都不到该距离的一半。

这是转换两个图像以使用此调色板的结果。首先是两张 16 色图像,然后是混合结果(理想情况下看起来完全没有任何可见闪烁)。

发现差异并不容易(当然取决于您拥有的显示器)。所以这里有两张只是差异的图像。左边的值与实际差异成正比,右边的值夸大了它们,以便在出现最大差异的区域变得更加直观。 (显然,与在 3D 色彩空间中操作相比,会有更大的差异。)

我了解(但假设的)结果也可以通过应用抖动来改进。而且我知道颜色之间距离的视觉感知在整个色彩空间中并不是恒定的。更不用说那些依赖硬件的小东西了。但首先要做的就是……

【问题讨论】:

@Fabel 请参阅Effective gif/image color quantization?,它可以为您提供调色板...在调色板之间切换(利用精灵硬件)类似于dithering,但不是将丢失的颜色渗入相邻像素,而是将它们复制到不同的像素在同一个地方切换的精灵/图像......这个想法是交替像素之间的相对视觉差异不大。所有这些都是理论,因为我没有这样的硬件,也没有我更习惯于像 ZX 这样的软件渲染的架构 也许发布一个示例输入图像和目标平台硬件的细节是个好主意......比如位深度、调色板限制、切换速度、最大/首选重叠/交替精灵位置,切换速度......每个精灵是否都有自己的调色板?你是想均匀地交替精灵还是想要像 PWM 一样通过调制每个精灵的显示时间来调整输出......以及你的时间测量的粒度或切换可以用...... 也许考虑在您的问题中添加image-processing 标签以吸引一些***贡献者。 @Spektre 我用更详细和更直观的示例更新了这个问题。如您所见,调色板中不存在最常用的颜色(对应于代码中直方图的高点)。我怀疑最适合我的用例的调色板可能非常好,然后也可以与抖动一起使用。但是我不希望有一个非常快的算法来解决这个问题(但至少比随机突变的进化要快很多)。 @Spektre 我有兴趣将它用于不同常见尺寸的调色板,至少 8、16、32 和 256 种颜色(每个颜色通道 3 到 8 位)。我意识到如果图像有自己的调色板或共享,这是两种完全不同的情况。而且我对这两种不同的情况都感兴趣(也许我应该为每种情况写一个单独的问题)。 【参考方案1】:

2 张图像听起来很合理,因为大多数复古硬件使用 50 或 60 Hz 刷新率,切换 2 帧将提供 25 或 30 Hz,这对于人类视觉来说仍然足够高。

交替显示两张图片bmp0,bmp1,两者的显示时间相同,它们将融合到它们的平均值:

bmp = (bmp0 + bmp1)/2

现在让bmp0 成为pal0 调色板截断图像bmp 然后:

bmp0 = trunc(bmp,pal0)
bmp1 = trunc(2*bmp - bmp0,pal1)

所以两张图片都只使用调色板中的颜色,但它们的平均值更接近原始图像,然后它们中的每一个......

这里是简单的 C++/VC 示例,使用此答案底部链接中描述的颜色量化:

//$$---- Form CPP ----
//---------------------------------------------------------------------------
#include <vcl.h>
#include <math.h>
#include <jpeg.hpp>
#pragma hdrstop
#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMain *Main;
//---------------------------------------------------------------------------
Graphics::TBitmap *bmp0,*bmp1,*bmp,*bmpd;
//---------------------------------------------------------------------------
//--- Palette ---------------------------------------------------------------
//---------------------------------------------------------------------------
const int _pals=64+0*8192;                          // max 8K colors in palette
DWORD pal[_pals];                               // palette 0x00RRGGBB
int pals=0;                                     // colors inside palette
const int rgb_bpc=5;                            // bits per channel (after truncation)
const int rgb_sh=8-rgb_bpc;                     // bits to drop (truncation)
const int rgb_n=1<<rgb_bpc;                     // colors per channel (after truncation)
int   rgb[rgb_n][rgb_n][rgb_n];                 // recolor table
void  pal_clear();                              // clear palette to empty
void  pal_dither(int n);                        // add up to n colors for dithering to pal[pals]
void  pal_major(int n,Graphics::TBitmap *bmp);  // add up to n major colors from bmp to pal[pals]
void  pal_compute_recolor();                    // compure recolor rgb[n][n][n] array from pal[pals] where n is power of 2, and compute sh (bits to drop from 8bit channel)
void  rgb2chn(int &r,int &g,int &b,DWORD c);    // rgb color to r,g,b
DWORD chn2rgb(int r,int g,int b);               // r,g,b to rgb color
int   chn2pal(int r,int g,int b);               // r,g,b to palette index
int   rgb2pal(DWORD c);                         // rgb color to palette index
DWORD pal2rgb(int ix);                          // palette index to rgb color
void pal_render(Graphics::TBitmap *bmp,int y0); // render palette at bmp,y0
//---------------------------------------------------------------------------
void pal_clear()
    
    pals=0;
    
//---------------------------------------------------------------------------
void pal_VGA(int n)
    
    const DWORD pal_VGA256[256]=
               // 0x00RRGGBB
        0x00000000,0x000000A8,0x0000A800,0x0000A8A8,0x00A80000,0x00A800A8,0x00A85400,0x00A8A8A8,
        0x00545454,0x005454FC,0x0054FC54,0x0054FCFC,0x00FC5454,0x00FC54FC,0x00FCFC54,0x00FCFCFC,
        0x00000000,0x00101010,0x00202020,0x00343434,0x00444444,0x00545454,0x00646464,0x00747474,
        0x00888888,0x00989898,0x00A8A8A8,0x00B8B8B8,0x00C8C8C8,0x00DCDCDC,0x00ECECEC,0x00FCFCFC,
        0x000000FC,0x004000FC,0x008000FC,0x00BC00FC,0x00FC00FC,0x00FC00BC,0x00FC0080,0x00FC0040,
        0x00FC0000,0x00FC4000,0x00FC8000,0x00FCBC00,0x00FCFC00,0x00BCFC00,0x0080FC00,0x0040FC00,
        0x0000FC00,0x0000FC40,0x0000FC80,0x0000FCBC,0x0000FCFC,0x0000BCFC,0x000080FC,0x000040FC,
        0x008080FC,0x009C80FC,0x00BC80FC,0x00DC80FC,0x00FC80FC,0x00FC80DC,0x00FC80BC,0x00FC809C,
        0x00FC8080,0x00FC9C80,0x00FCBC80,0x00FCDC80,0x00FCFC80,0x00DCFC80,0x00BCFC80,0x009CFC80,
        0x0080FC80,0x0080FC9C,0x0080FCBC,0x0080FCDC,0x0080FCFC,0x0080DCFC,0x0080BCFC,0x00809CFC,
        0x00B8B8FC,0x00C8B8FC,0x00DCB8FC,0x00ECB8FC,0x00FCB8FC,0x00FCB8EC,0x00FCB8DC,0x00FCB8C8,
        0x00FCB8B8,0x00FCC8B8,0x00FCDCB8,0x00FCECB8,0x00FCFCB8,0x00ECFCB8,0x00DCFCB8,0x00C8FCB8,
        0x00B8FCB8,0x00B8FCC8,0x00B8FCDC,0x00B8FCEC,0x00B8FCFC,0x00B8ECFC,0x00B8DCFC,0x00B8C8FC,
        0x00000070,0x001C0070,0x00380070,0x00540070,0x00700070,0x00700054,0x00700038,0x0070001C,
        0x00700000,0x00701C00,0x00703800,0x00705400,0x00707000,0x00547000,0x00387000,0x001C7000,
        0x00007000,0x0000701C,0x00007038,0x00007054,0x00007070,0x00005470,0x00003870,0x00001C70,
        0x00383870,0x00443870,0x00543870,0x00603870,0x00703870,0x00703860,0x00703854,0x00703844,
        0x00703838,0x00704438,0x00705438,0x00706038,0x00707038,0x00607038,0x00547038,0x00447038,
        0x00387038,0x00387044,0x00387054,0x00387060,0x00387070,0x00386070,0x00385470,0x00384470,
        0x00505070,0x00585070,0x00605070,0x00685070,0x00705070,0x00705068,0x00705060,0x00705058,
        0x00705050,0x00705850,0x00706050,0x00706850,0x00707050,0x00687050,0x00607050,0x00587050,
        0x00507050,0x00507058,0x00507060,0x00507068,0x00507070,0x00506870,0x00506070,0x00505870,
        0x00000040,0x00100040,0x00200040,0x00300040,0x00400040,0x00400030,0x00400020,0x00400010,
        0x00400000,0x00401000,0x00402000,0x00403000,0x00404000,0x00304000,0x00204000,0x00104000,
        0x00004000,0x00004010,0x00004020,0x00004030,0x00004040,0x00003040,0x00002040,0x00001040,
        0x00202040,0x00282040,0x00302040,0x00382040,0x00402040,0x00402038,0x00402030,0x00402028,
        0x00402020,0x00402820,0x00403020,0x00403820,0x00404020,0x00384020,0x00304020,0x00284020,
        0x00204020,0x00204028,0x00204030,0x00204038,0x00204040,0x00203840,0x00203040,0x00202840,
        0x002C2C40,0x00302C40,0x00342C40,0x003C2C40,0x00402C40,0x00402C3C,0x00402C34,0x00402C30,
        0x00402C2C,0x0040302C,0x0040342C,0x00403C2C,0x0040402C,0x003C402C,0x0034402C,0x0030402C,
        0x002C402C,0x002C4030,0x002C4034,0x002C403C,0x002C4040,0x002C3C40,0x002C3440,0x002C3040,
        0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,
        ;
    for (int i=0;(i<n)&&(i<256)&&(pals<_pals);i++,pals++) pal[pals]=pal_VGA256[i];
    
//---------------------------------------------------------------------------
void pal_dither(int n)
    
    
//---------------------------------------------------------------------------
void pal_major(int n,Graphics::TBitmap *bmp)    // only for rgb_bits=5 !!!
    
    union  DWORD dd; BYTE db[4];  c0,c1;
    int i,x,y,xs,ys,a,aa,hists;
    DWORD *p,cc,r,g,b;
    DWORD his[32768];
    DWORD idx[32768];
    // init
    xs=bmp->Width;
    ys=bmp->Height;
    n+=pals;
    // 15bit histogram
    for (x=0;x<32768;x++)  his[x]=0; idx[x]=x; 
    for (                           y=0;y<ys;y++)
     for (p=(DWORD*)bmp->ScanLine[y],x=0;x<xs;x++)
        
        cc=p[x];
        cc=((cc>>3)&0x1F)|((cc>>6)&0x3E0)|((cc>>9)&0x7C00);
        if (his[cc]<0xFFFFFFFF) his[cc]++;
        
    // remove zeroes
     for (x=0,y=0;y<32768;y++)
        
        his[x]=his[y];
        idx[x]=idx[y];
        if (his[x]) x++;
         hists=x;
    // sort by hist
    for (i=1;i;)
     for (i=0,x=0,y=1;y<hists;x++,y++)
      if (his[x]<his[y])
        
        i=his[x]; his[x]=his[y]; his[y]=i;
        i=idx[x]; idx[x]=idx[y]; idx[y]=i; i=1;
        
    // set pal color palete
    for (x=0;x<hists;x++) // main colors
        
        cc=idx[x];
        b= cc     &31;
        g=(cc>> 5)&31;
        r=(cc>>10)&31;
        c0.db[0]=b;
        c0.db[1]=g;
        c0.db[2]=r;
        c0.dd=(c0.dd<<3)&0x00F8F8F8;
        // skip if similar color already in pal[]
        for (a=0,i=0;i<pals;i++)
            
            c1.dd=pal[i];
            aa=int(BYTE(c1.db[0]))-int(BYTE(c0.db[0])); if (aa<=0) aa=-aa; a =aa;
            aa=int(BYTE(c1.db[1]))-int(BYTE(c0.db[1])); if (aa<=0) aa=-aa; a+=aa;
            aa=int(BYTE(c1.db[2]))-int(BYTE(c0.db[2])); if (aa<=0) aa=-aa; a+=aa;
            if (a<=16)  a=1; break;  a=0; // *** treshold ***
            
        if (!a)
            
            pal[pals]=c0.dd; pals++;
            if (pals>=n)  x++; break; 
            
        
    
//---------------------------------------------------------------------------
void  pal_compute_recolor()
    
    int i,j,x,y,c,r,g,b,rr,gg,bb;
    // test all truncated rgb colors
    for (r=0;r<rgb_n;r++)
     for (g=0;g<rgb_n;g++)
      for (b=0;b<rgb_n;b++)
        
        // find closest match in pal[m]
        for (j=-1,x=1000000,i=0;i<pals;i++)
            
            c=pal[i];
            bb= c     &255; bb-=b<<rgb_sh; bb*=bb;
            gg=(c>> 8)&255; gg-=g<<rgb_sh; gg*=gg;
            rr=(c>>16)&255; rr-=r<<rgb_sh; rr*=rr;
            y=rr+gg+bb;
            if (x>y) x=y; j=i; 
            
        // store it as recolor value
        rgb[r][g][b]=j;
        
    
//---------------------------------------------------------------------------
void rgb2chn(int &r,int &g,int &b,DWORD c)
    
    b= c     &255;
    g=(c>> 8)&255;
    r=(c>>16)&255;
    
//---------------------------------------------------------------------------
DWORD chn2rgb(int r,int g,int b)
    
    return b+(g<<8)+(r<<16);
    
//---------------------------------------------------------------------------
int chn2pal(int r,int g,int b)
    
    return rgb[r>>rgb_sh][g>>rgb_sh][b>>rgb_sh];
    
//---------------------------------------------------------------------------
int rgb2pal(DWORD c)
    
    int r,g,b;
    b= c     &255;
    g=(c>> 8)&255;
    r=(c>>16)&255;
    return rgb[r>>rgb_sh][g>>rgb_sh][b>>rgb_sh];
    
//---------------------------------------------------------------------------
DWORD pal2rgb(int ix)
    
    return pal[ix];
    
//---------------------------------------------------------------------------
void pal_render(Graphics::TBitmap *bmp,int y0)
    
    int xs,ys,x,y,i,j,c,*p;
    xs=bmp->Width;
    ys=bmp->Height;
    for (c=y0,i=0;(i<pals)&&(c+8<ys);c+=10,i=j)
     for (y=c;y<c+8;y++)
      for (p=(int*)bmpd->ScanLine[y],j=i,x=0;(x<xs)&&(j<pals);x++)
        p[x]=pal[j]; if ((x&7)==7) x++; j++; if (x+8>xs) break; 
    
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
void compute() // bmp -> bmp0+bmp1 using pal[]
    
    const int colors=64;
    int i,j,r,g,b,rr,gg,bb,x,y,xs,ys,c;
    int *p,*p0,*p1,*pd;
    int pal0[colors],pal1[colors];
    // allow direct pixel access and resize to coomon size
    xs=bmp->Width;
    ys=bmp->Height;
    bmp->HandleType=bmDIB;
    bmp->PixelFormat=pf32bit;
    bmp0->HandleType=bmDIB;
    bmp0->PixelFormat=pf32bit;
    bmp0->SetSize(xs,ys);
    bmp1->HandleType=bmDIB;
    bmp1->PixelFormat=pf32bit;
    bmp1->SetSize(xs,ys);
    bmpd->PixelFormat=pf32bit;
    bmpd->SetSize(xs,ys);
    // compute palette for bmp0
    pal_clear();
    pal_major(colors,bmp);
    pal_compute_recolor();
    for (i=0;i<colors;i++) pal0[i]=pal[i];  // store palette for later
    // recolor bmp0,bmp1
    for (y=0;y<ys;y++)
        
        p =(int*)bmp ->ScanLine[y];
        p0=(int*)bmp0->ScanLine[y];
        p1=(int*)bmp1->ScanLine[y];
        for (x=0;x<xs;x++)
            
            // i = recolor(p)   // bmp0
            rgb2chn(r,g,b,p[x]); i=chn2pal(r,g,b);
            // p1 = (2*p-p0)    // bmp1
            rgb2chn(rr,gg,bb,pal[i]);
            bb=b+b-bb; if (bb>255) bb=255; if (bb<0) bb=0;
            gg=g+g-gg; if (gg>255) gg=255; if (gg<0) gg=0;
            rr=r+r-rr; if (rr>255) rr=255; if (rr<0) rr=0;
            // copy pixels to bmps
            p0[x]=pal[i];                   // quantized
            p1[x]=chn2rgb(rr,gg,bb);        // true color for now
            
        

    // compute palette for bmp1
    pal_clear();
    pal_major(colors,bmp1);
    pal_compute_recolor();
    for (i=0;i<colors;i++) pal1[i]=pal[i];  // store palette for later
    // recolor bmp1
    for (y=0;y<ys;y++)
        
        p1=(int*)bmp1->ScanLine[y];
        for (x=0;x<xs;x++) p1[x]=pal[rgb2pal(p1[x])]; // quantized
        

    // Blend and difference for debug
    for (y=0;y<ys;y++)
        
        p =(int*)bmp ->ScanLine[y];
        p0=(int*)bmp0->ScanLine[y];
        p1=(int*)bmp1->ScanLine[y];
        pd=(int*)bmpd->ScanLine[y];
        for (x=0;x<xs;x++)
            
            // get r,g,b
            rgb2chn(r ,g ,b ,p0[x]);
            rgb2chn(rr,gg,bb,p1[x]);
            // blend
            r=(r+rr)>>1;
            g=(g+gg)>>1;
            b=(b+bb)>>1;
            // diff
            rgb2chn(rr,gg,bb,p[x]);
            i=2;    // scale
            rr=abs(r-rr)<<i; if (rr>255) rr=255;
            gg=abs(g-gg)<<i; if (gg>255) gg=255;
            bb=abs(b-bb)<<i; if (bb>255) bb=255;
            // copy pixels
            p[x]=chn2rgb(r,g,b);
            pd[x]=chn2rgb(rr,gg,bb);
            
        
    // render palettes
    for (i=0;i<colors;i++) pal[i]=pal0[i]; pal_render(bmpd,0);
    for (i=0;i<colors;i++) pal[i]=pal1[i]; pal_render(bmpd,100*colors/xs+10);


    bmp ->SaveToFile("out_blend.bmp");
    bmp0->SaveToFile("out_bmp0.bmp");
    bmp1->SaveToFile("out_bmp1.bmp");
    bmpd->SaveToFile("out_diff.bmp");
    
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner)
    
    bmp=new Graphics::TBitmap;
    bmp0=new Graphics::TBitmap;
    bmp1=new Graphics::TBitmap;
    bmpd=new Graphics::TBitmap;
    // load jpg into bmp
    TJPEGImage *jpg = new TJPEGImage();
    jpg->LoadFromFile("in.jpg");
    bmp->Assign(jpg);
    delete jpg;
    // resize window
    ClientWidth=bmp->Width<<2;
    ClientHeight=bmp->Height;
    // compute
    compute();
    
//---------------------------------------------------------------------------
void __fastcall TMain::FormDestroy(TObject *Sender)
    
    delete bmp;
    delete bmp0;
    delete bmp1;
    delete bmpd;
    
//---------------------------------------------------------------------------
void __fastcall TMain::tim_redrawTimer(TObject *Sender)
    
/*
    // alternatin images
    static int cnt=0;
    cnt=(cnt+1)&1;
    if (cnt==0) Canvas->Draw(0,0,bmp0);
    if (cnt==1) Canvas->Draw(0,0,bmp1);
    Canvas->Draw(bmp->Width,0,bmpd);
*/
    // debug view of all images
    int x=0;
    Canvas->Draw(x,0,bmp ); x+=bmp ->Width;
    Canvas->Draw(x,0,bmp0); x+=bmp0->Width;
    Canvas->Draw(x,0,bmp1); x+=bmp1->Width;
    Canvas->Draw(x,0,bmpd); x+=bmpd->Width;

    
//---------------------------------------------------------------------------

忽略 VCL 的东西。函数compute 将计算bmp0,bmp1 及其对应的调色板pal0,pal1。颜色数量可通过常量colors配置。

这里输出 64 色调色板:

原图:

差异被夸大了 4 倍。 2 个使用的调色板也在那里渲染,顶部的一个用于bmp0,另一个用于bmp1

Effective gif/image color quantization? dithering

此外,为了避免闪烁,您应该尽可能减小 bmp0bmp1 之间的差异。

【讨论】:

感谢您的努力,但您是如何优化图像的调色板的呢?我的问题特别是关于如何“计算最佳(或半最佳)调色板”。我用来创建这两个图像的方法是创建一个包含颜色对(所有颜色相距不远)和它们混合的颜色值的数组,作为虚拟调色板,然后重新映射到该数组。不如您的解决方案快,但我认为更精确。 哦,刚刚意识到您使用了标准的 VGA 调色板(没有正确阅读 cmets 就直接获取代码,抱歉)。然而,我寻求的答案是如何“自定义计算调色板的一部分以包含更接近的颜色”。 @Fabel 我所做的是一种非常柔和的抖动形式,标准的 VGA 调色板(前 16 和 256 色)是专门为抖动而设计的......来自 GIF 量化的算法会给你图像中最接近的重要颜色(多少是​​恒定的)所以我会将这两种颜色结合起来......就像使用 VGA 的前 16 种颜色,然后从量化中添加 16 种颜色...... 我不明白。您似乎详细描述了减少颜色的基本技术(包括抖动),但仅暗示了实际的调色板优化。那些“来自量化的 16 种 [颜色]”,它们是否构成了(更多)优化的调色板?如果是这样,那么将结果图像与原始图像进行比较的过程会产生用于改进调色板的数据,并且迭代它可能会进一步改进调色板?如果这就是您所说的,请您详细说明那部分。 @Fabel 我重新编辑了我的问题...已经对代码进行了一些修改并进行了一些更改...现在结果要好得多,代价是使用了 2 次量化(现在是包含在代码中)它与量化链接中的代码基本相同(也描述了该过程),但您可以使用任何量化方法。

以上是关于用于像素混合的调色板量化的主要内容,如果未能解决你的问题,请参考以下文章

图像的准确颜色量化以最小化调色板

用黑色像素填充 256 调色板的空白区域

使用黑色像素填充256色调色板的空白区域

使用 PHP GD 库从 BMP 获取像素的调色板

如何将调色板 (n=5) 与第二个调色板 (n=4) 加上矩阵中的亮度变量 (n=4) 最佳混合(总共 5*4*4=80 种颜色)

Codecs系列HEVC-SCC:调色板PM模式分析