well 2 images sounds reasonable as most retro HW used 50 or 60 Hz refresh rate and switching 2 frames will give 25 or 30 Hz which is still high enough for human sight.
Alternating 2 images bmp0,bmp1 with the same display time for both will blend together to their average:
bmp = (bmp0 + bmp1)/2
Now let bmp0 be a pal0 palette truncated image bmp then:
bmp0 = trunc(bmp,pal0)
bmp1 = trunc(2*bmp - bmp0,pal1)
so both images use just colors from their palette but their average is more closer to original image then each of them...
Here simple C++/VC example using color quantization described in link at the bottom of this answer:
//$$---- 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;
}
//---------------------------------------------------------------------------
Just ognore the VCL stuff. Function compute will compute the bmp0,bmp1 and their corresponding palettes pal0,pal1. The number of colors is configurable by constant colors.
Here output for 64 colors palette:

original image:

The difference is 4 times exaggerated. Also the 2 used palettes are rendered there the top one is for bmp0 and the other is for bmp1.
Also to avoid flickering you should keep the difference between bmp0 and bmp1 as small as possible.