Matricks weblog. JPEG Recover 2
(engelska versionen kommer när jag bytt weblog mjukvara)
JPEG Recover 2
Okej, förra gången snackade jag om ett litet äventyr in i JPEG världen och jag tänker göra det igen. En annan person på detta forum ville ha hjälp med en bild som var ännu mer förstörd än den förra. Bilden är helt grå men han kunde se en liten thumbnail av den i explorer. Han blev tipsad av en person att kontakta mig och fråga mig om hjälp p.g.a mitt arbete på den förra bilden. Jag tänke att jag göra samma sak som förut och vara klar med det hela men denna gång blev det ännu mer intressant.
Orginal bilden är en 2593x1952 JPEG tagen med en HP PhotoSmart R707 och har storleken 171654 bytes. Bilden kan ni tanka här.
Jag körde JPEG programmet jag skrev förra gången på bilden och jag fick lite skum output ifrån den. Jag rätta till buggarna i programmet och under mina undersökningar på denna bild så uppdaterade jag programmet också. Här kommer koden
#include <stdio.h>
#include <stdlib.h>
enum
{
TEM = 0x01, SOF = 0xc0, DHT = 0xc4, JPGA = 0xc8,
DAC = 0xcc, RST = 0xd0, SOI = 0xd8, EOI = 0xd9,
SOS = 0xda, DQT = 0xdb, DNL = 0xdc, DRI = 0xdd,
DHP = 0xde, EXP = 0xdf, APP0 = 0xe0, APP1 = 0xe1,
APP2 = 0xe2, APP3 = 0xe3, APP4 = 0xe4, APP5 = 0xe5,
APP6 = 0xe6, APP7 = 0xe7, JPG = 0xf0, COM = 0xfe,
};
int main()
{
const char *aMarkerNameLUT[256] = {0};
aMarkerNameLUT[TEM] = "TEM ";
aMarkerNameLUT[SOF] = "SOF "; // start of frame
aMarkerNameLUT[DHT] = "DHT "; // define huffman table
aMarkerNameLUT[JPGA] = "JPGA";
aMarkerNameLUT[DAC] = "DAC ";
aMarkerNameLUT[RST] = "RST ";
aMarkerNameLUT[SOI] = "SOI "; // start of image
aMarkerNameLUT[EOI] = "EOI "; // end of image
aMarkerNameLUT[SOS] = "SOS "; // start of scans
aMarkerNameLUT[DQT] = "DQT "; // define quantization table
aMarkerNameLUT[DNL] = "DNL ";
aMarkerNameLUT[DRI] = "DRI ";
aMarkerNameLUT[DHP] = "DHP ";
aMarkerNameLUT[EXP] = "EXP ";
aMarkerNameLUT[APP0] = "APP0";
aMarkerNameLUT[APP1] = "APP1";
aMarkerNameLUT[APP2] = "APP2";
aMarkerNameLUT[APP3] = "APP3";
aMarkerNameLUT[APP4] = "APP4";
aMarkerNameLUT[APP5] = "APP5";
aMarkerNameLUT[APP6] = "APP6";
aMarkerNameLUT[APP7] = "APP7";
aMarkerNameLUT[JPG] = "JPG ";
aMarkerNameLUT[COM] = "COM "; // comment
FILE *file = fopen("Bild_1-pfx.jpg", "rb");
int lastoffset = -1;
while(!feof(file))
{
int c = fgetc(file);
if(c == 0xff)
{
int t = fgetc(file);
if(t != 0xff && t != 0x00) // not valid markers
{
int o = ftell(file)-2;
if(lastoffset != -1)
printf("%8x %8d\n", o-lastoffset, o-lastoffset);
printf("%8x %8d %x %s", o, o, t, aMarkerNameLUT[t]);
lastoffset = o;
}
if(t >= APP0 && t <= APP7)
{
int a = fgetc(file);
int b = fgetc(file);
fseek(file, ((a<<8)|b)-2, SEEK_CUR);
}
}
}
printf("%8x %8d\n", ftell(file)-lastoffset, ftell(file)-lastoffset);
fclose(file);
}
Och här är den output den genererade ifrån orginal JPEG filen.
offset size
hex dec type hex dec
0 0 d8 SOI 2 2
2 2 e1 APP1 4386 17286
4388 17288 e4 APP4 10000 65536
14388 82824 e4 APP4 10000 65536
24388 148360 e4 APP4 5860 22624
29be8 170984 db DQT c7 199
29caf 171183 c4 DHT 21 33
29cd0 171216 c4 DHT b7 183
29d87 171399 c4 DHT 21 33
29da8 171432 c4 DHT b7 183
29e5f 171615 c0 SOF 13 19
29e72 171634 dd DRI 6 6
29e78 171640 da SOS e 14
Vi har det normala APP1 chunket med EXIF informationen i. Jag pillade ut thumbnailen på samma sätt som förut och här är resultatet.

Som ni ser på outputen ifrån jpegdump programmet så kommer SOS chunket som innehåller själva JPEG datan bara några bytes innan filens slut vilket betyder att hires informationen är helt väck. Men det finns något mycket mer intressant där och det är dom där tre stora APP4 chunksen. Jag hoppades på någon sort av okomprimmerad thumbnail som var gjord för att snabbt browsa bilderna på kameran. Jag kollade upp specificationerna för kameran. Den har en 1.5” display som klarar nästan 120000 pixlar. Detta ger oss ett format på någonting i stilen med 320x320. Detta betyder att displayen på kameran kan visa mycket mer än den där lilla 160x120 vilket skulle motivera en mer högupplöst thumbnail.
Jag petade ut dom tre APP4 chunksen till varsin fil som jag döpte till app4_1, app4_2 och app4_3. Jag började att analysera dom genom att kasta in dom i en hex editor. Här är dom 64 första bytsen i chunksen.
app4_1:
ff e4 ff fe 48 33 58 30 00 00 00 04 00 00 01 40
00 00 00 f0 00 00 00 01 00 00 00 01 00 00 00 03
82 ad 7f ae 81 af 7f b0 81 b2 7e b3 82 b3 7e b4
82 b4 7d b4 80 b6 7f b6 80 b7 7f b6 81 b7 7e b8
81 b9 7f ba 80 b9 7c ba 82 ba 7c bb 81 bb 7d bb
82 bb 7e bc 83 bb 7f bb 82 bd 7d bd 82 bd 7f bd
82 bd 7e be 82 be 7d bf 81 be 7f be 82 bf 7d be
82 bf 7f c0 81 c0 7d c0 80 c0 7e c0 7f c0 7d c0
app4_2:
ff e4 ff fe 48 33 58 30 00 00 00 04 00 00 01 40
00 00 00 f0 00 00 00 01 00 00 00 02 00 00 00 03
84 08 81 11 84 14 85 14 7f 16 86 0d 7d 09 86 09
82 0a 85 11 84 49 88 75 77 7e 98 82 77 85 9b 8c
76 94 9c 94 77 8c 9c 8c 77 93 9d 98 77 99 a0 94
75 92 9e 8b 75 82 9d 7b 76 76 a0 74 75 72 9e 71
75 6f 9f 70 76 71 9d 76 77 7d 9b 82 77 85 9c 87
77 88 9c 89 77 88 9b 85 75 83 9a 81 78 80 97 81
app4_3:
ff e4 58 5e 48 33 58 30 00 00 00 04 00 00 01 40
00 00 00 f0 00 00 00 01 00 00 00 03 00 00 00 03
80 2f 81 2d 7f 2b 82 2b 7d 24 84 18 7e 15 85 15
7a 19 85 1f 7e 27 83 2f 80 2f 84 30 7d 31 84 32
82 32 85 32 7f 33 85 33 7d 36 83 38 7f 39 85 38
7f 38 84 3c 7e 3e 82 40 7f 40 87 40 7e 3f 84 40
7f 3c 84 36 7e 36 83 35 7e 3a 84 3b 7f 3c 83 3b
7e 3e 85 40 7f 40 85 3f 80 3c 84 3b 7f 3c 84 3f
Vi ser snabbt ett mönster. Varje chunk har en header på 32 bytes och datan ser ut att sitta ihop i grupper om två byte. Om vi kollar närmre på headern ser vi att dom två första bytesen är JPEG markern för ett APP4 chunk följt av 2 bytes som säger storleken på chunket. Efter detta kommer 4 bytes med ascii världet 'H3X0'. Jag googlade efter detta men hittande inget av substans. Bara någon sida som jämförde vad kamror lägger i sina JPEG filer. APP4 H3X0 var dokumenterat som okänt.
Efter det så har vi sex stycken 32bits integers i big-endian format med lite värden. Jag satte upp dom i en tabell och jämförde dom och här är vad jag kom fram till.
app4_1 app4_2 app4_3 comment
00000004 00000004 00000004 ??
00000140 00000140 00000140 width (320 decimal)
000000f0 000000f0 000000f0 height (240 decimal)
00000001 00000001 00000001 ??
00000001 00000002 00000003 sequence id
00000003 00000003 00000003 number of chunks
Så det såg ut som dom sparade en bild på 320x240 i dessa tre chunks. Dom kan inte spara bilden i ett APP4 chunk eftersom max storleken är 65536, så det verkade som dom splittade upp det till 3 med ett sekvens id. Jag började räkna lite på saken. Den totala datamängden utan headers är (65536-32)*3+22624-32=153600. Antalet pixlar i en 320x240 bild är 320x240=76800. Men en snabb koll ser vi att data storleken är exakt dubbelt så stor som antalet pixlar. Detta indikerar på en 320x240 16bits bild.
Jag strippade bort alla headers med 'dd' och slog ihop allt med 'cat' (För er windows användare, dd är ett unix kommando som låter dig manipulera rena data filer)
$ dd if=app4_1 of=app4_1_stripped bs=1 skip=32
$ dd if=app4_2 of=app4_2_stripped bs=1 skip=32
$ dd if=app4_3 of=app4_3_stripped bs=1 skip=32
$ cat app4_1_stripped app4_2_stripped app4_3_stripped > data
Så nu hade jag en fil som hette data med ett innehåll som tror sig vara rå-datan för en 320x240 16bits bild. För att visa denna data så måste jag få in den i en bildvisare på något sätt. Jag hade ingen bildvisare som klarade att visa raw data så jag skrev ett litet program som genererar tga headers. Här kommer sourcen för 'maketgaheader' programmet.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum
{
TYPE_NODATA=0,
TYPE_COLORMAPPED=1,
TYPE_TRUECOLOR=2,
TYPE_BW=3,
TYPE_RLEBIT=8,
};
#define bail(check,reason) if(check) { printf("%s: %s\n", argv[0], reason); return -1; }
int main(int argc, char **argv)
{
int width = -1;
int height = -1;
int pixeldepth = -1;
int imagetype = -1; //TYPE_RGBA;
char *filename = 0x0;
if(argc <= 1)
{
printf("USAGE: %s w=X h=X pd=X t=truecolor|bw filename\n", argv[0]);
return -1;
}
for(int i = 1; i < argc; i++)
{
if(argv[i][0] == 'w' && argv[i][1] == '=')
width = atoi(argv[i]+2);
else if(argv[i][0] == 'h' && argv[i][1] == '=')
height = atoi(argv[i]+2);
else if(argv[i][0] == 'p' && argv[i][1] == 'd' && argv[i][2] == '=')
pixeldepth = atoi(argv[i]+3);
else if(argv[i][0] == 't' && argv[i][1] == '=')
{
if(strcmp(argv[i]+2, "truecolor") == 0)
imagetype = TYPE_TRUECOLOR;
else if(strcmp(argv[i]+2, "bw") == 0)
imagetype = TYPE_BW;
else
{
printf("%s: invalid image type\n", argv[0]);
return -1;
}
}
else
filename = argv[i];
}
bail(width < 0 || width > 0xffff, "invalid width or none specified (-w=x)");
bail(height < 0 || height > 0xffff, "invalid height or none specified (-h=x)");
bail(pixeldepth < 0, "invalid pixel depth or none specified (-pd=x)");
bail(filename == 0, "no filename specified");
FILE *output = fopen(filename, "wb");
if(!output)
{
printf("%s: failed to open '%s' for writing\n", argv[0], filename);
return -1;
}
printf("w=%d h=%d pd=%d\n", width, height, pixeldepth);
fputc(0, output); // idlen
fputc(0, output); // colormaptype
fputc(imagetype, output); // imagetype
fputc(0, output); fputc(0, output); // colormap first entry index
fputc(0, output); fputc(0, output); // colormap length
fputc(0, output); // colormap entry size
fputc(0, output); fputc(0, output); // pos x
fputc(0, output); fputc(0, output); // pos y
fputc(width&0xff, output); fputc(width>>8, output); // width
fputc(height&0xff, output); fputc(height>>8, output); // height
fputc(pixeldepth, output); // pixel depth
fputc(0, output); // image descriptor
fclose(output);
return 0;
}
Jag skapade en header för rå-datan och slog ihop allt till en tga bild.
$ ./maketgaheader w=320 h=240 pd=16 t=truecolor tgah_320x240x16
$ cat tgah_320x240x16 data > out_320x240x16.tga
Här är resultatet.

Man kan se att det är bilden men alla färger är helt fel. Jag tänkte att jag byteswappar datan innan jag slår ihop det till en tgabild ifall det är någon endianness problem. Så jag skrev ett litet program som byteswappade 16 integers helt enkelt. Här är koden för 'byteswap16' programmet.
#include <stdio.h>
int main()
{
int c;
while(!feof(stdin))
{
c = fgetc(stdin);
fputc(fgetc(stdin), stdout);
fputc(c, stdout);
}
return 0;
}
Jag körde det på datan och gjorde en ny tga bild.
$ cat data | ./byteswap16 > data_byteswapped
$ cat tgah_320x240x16 data_byteswapped > out_320x240x16_byteswapped.tga

GAH! Samma röra. Datan kan vara sparad i något annat 16bits format istället för det vanliga 565 formatet som TGA använder. Det kan vara 1555 men jag beslöt för att inte utreda det. Istället så tänkte jag kolla på datan som en ren svartvit bild och se vad det gav mig. Jag genererade en ny tga header och tga fil för det. Jag dubblade bredden så jag skulle få se allt. Här är kommandona och bilden.
$ ./maketgaheader w=640 h=240 pd=8 t=bw tgah_640x240x8
$ cat tgah_640x240x8 data > out_640x240x8.tga

Och en liten mer inzoomad.

När jag studera bilden inzoomad så fick jag en ide. Datan såg ju ut att vara grupperad i två bytes. Det verkade som den sparade ljusstyrkan för varje pixel i en byte och något annat i den andra. Min ide var att dom komprimerade datan genom att transformera bilden till ett annat color space som kallas YCbCr eller YUV. Detta color space separerar ljusstyrkan och färgen. Detta är en klar fördel när man skall komprimera bilder eftersom själva ljusstrykan är mycket viktigare än färgen.
Du kan testa detta ifall du vill. Ta en ganska stor blir, duplicera den och skala ner till typ 10-15%. Ta den andra kopian och gör den svartvit. Skala upp den med färg igen och lägg ihop dom. Dom det är gjort korrekt så kommer resultatet vara mycket bra. Mycket simpel komprimering.
Vidare tänkte jag att data var packad Cb Y Cr Y, dvs varje pixel har egen ljusstryka men delar på färg informationen. Jag skrev två små program som separerar dessa genom att hoppa över varannan byte. Här kommer koden för programmen 'byteskip01' och 'byteskip10'.
#include <stdio.h>
int main()
{
int c;
while(!feof(stdin))
{
/* byt dessa för att få byteskip10 */
fgetc(stdin);
fputc(fgetc(stdin), stdout);
}
return 0;
}
Och kommandona för att skapa två tga bilder ifrån dom två olika kanalerna.
$ ./maketgaheader w=320 h=240 pd=8 t=bw tgah_320x240x8
$ cat data | ./byteskip01 > data01
$ cat data | ./byteskip10 > data10
$ cat tgah_320x240x8 data01 > data01_320x240x8.tga
$ cat tgah_320x240x8 data10 > data10_320x240x8.tga
data01:

data10:

Men se där. Vi fick ut en fin svartvit bild och en annan liten skum bild. Detta verifierar min Cb Y Cr Y teori. Om du har sett Cb och Cr kanalerna separerade ifrån Y förut så känner man igen mönstret. Du kan se detta när en film som använder en kodec som kör YCbCr har blivigt dålig. Ända som återstår är att skriva ett program som konverterar deras CbYCrY format till vanligt RGB format. Här kommer koden för 'cbycry_to_rgb' programmet som gör just detta.
#include <stdio.h>
#define clamp(x) if(x < 0) x = 0; if(x > 255) x = 255;
void ycbcr_to_rgb(int y, int cb, int cr, int *r, int *g, int *b)
{
*r = (int)(y + 1.402f *(cr-128.0f));
*g = (int)(y - 0.34414f*(cb-128.0f) - 0.71414f*(cr-128.0f));
*b = (int)(y + 1.772f*(cb-128.0f));
clamp(*r); clamp(*g); clamp(*b);
}
int main()
{
int cb, cr, y0, y1, r, g, b;
while(!feof(stdin))
{
cr = fgetc(stdin);
y0 = fgetc(stdin);
cb = fgetc(stdin);
y1 = fgetc(stdin);
ycbcr_to_rgb(y0,cb,cr,&r,&g,&b);
fputc(r, stdout);
fputc(g, stdout);
fputc(b, stdout);
ycbcr_to_rgb(y1,cb,cr,&r,&g,&b);
fputc(r, stdout);
fputc(g, stdout);
fputc(b, stdout);
}
return 0;
}
Och kommandona.
$ ./maketgaheader w=320 h=240 pd=24 t=truecolor tgah_320x240x24
$ cat data | ./cbycry_to_rgb > data_rgb
$ cat tgah_320x240x24 data_rgb > rgb_320x240x24.tga
And the resulting image.

Ahh, där kom den! En bild med fyra gånger mer pixlar än den där crappiga EXIF thumbnailen. Denna har dessutom mindre kompression. Ända problemet är att denna har en date-stamp på sig men det kan man lätt fåbort med Photoshop, GIMP eller något liknande genom att anväda data ifrån EXIF thumbnailen. Så ytterligare engång har jag jag lyckats få ut mer information än den vanliga bildvisaren klarar av. Detta resultat blev faktiskt bättre än det förra pga den vendor specifika thumbnailen.
Tills nästa gång. Chaio!
Teeworlds - För dig som gillar gulliga saker med stora vapen.