
TRADITIONAL MAT5 FORMAT: (5 files that end with *C.bmp,
*I.bmp, *X.byt, *Y.byt, *Z.byt) This is a format we use to represent our data.
We made it up but we know other 3-D folk have used almost the same thing. The
reason we use it is that we haven’t really found a better one that is an
accepted standard. We have the capability to convert it to STL, IGES, ASCII,
Wavefront, VRML and GPD + UV formats.
Mat5 consists of 5 matrices, hence the name. The first
matrix is the color image, sometimes called the texture map in BMP format. So
for a mat5 set named lgh, the first matrix is lghC.bmp (suffix is C.bmp) which
is viewable by any image viewer. Also note that since it is a BMP file, the
number of columns must be an integer multiple of 4. The row, column size of the
“color” image sets the size of the remaining 4 image matrices. The
second matrix is the “Indicator” or “quality” matrix.
The most common format we use for this is also BMP and so we would have
lghI.bmp (suffix is I.bmp). This Indicator matrix is black and white in value
and can also be stored as a headerless file with each pixel being one unsigned
char but will have the suffix “I.byt” or lghI.byt. Since we use the
I.bmp so much there is a chance that some of our software does not recognize
the I.byt but if you find that is the case, let us know and we will fix it.
This would save you some memory. The indicator matrix can be edited to exclude
pixels. If a pixel in the indicator matrix is 0 then that pixel is considered nonvalid.
You can also threshold the indicator to exclude pixels that are dark in color.
Never the less, everything to do with pixel selection revolves around the
indictor matrix. The remaining three matrices are X, Y, and Z. They contain no
header information and use one float value for each element. The X, Y and Z
matrices contain the X, Y and Z coordinates, respectively. Their suffixes are
X.byt, Y.byt and Z.byt, respectively. In this example they would be lghX.byt,
lghY.byt and lghZ.byt. The reason mat5 is so advantageous is that the indice
position of the elements corresponds to the relative spatial position of the
3-D points. So if you rotate, scale and/or translate the coordinates X,Y,Z you
will still know the relative positions of the points with respect to each
other. This cannot be over stressed and it make triangulation and merging of
data much easier. The format GPD + UV has this feature but by separating the
coordinates from the indices, it uses up more space for storage.
// MAT5 BSD LICENSE
//Copyright (c) 2009, University of Kentucky, Lexington
//All rights reserved.
//Redistribution and use in source and binary forms, with
or without modification, are permitted provided that the following conditions
are met:
//1. Redistributions of source code must retain the above
copyright notice, this list of conditions and the following disclaimer.
//2. Redistributions in binary form must reproduce the
above copyright notice, this list of conditions and the following disclaimer
// in
the documentation and/or other materials provided with the distribution.
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
//BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
//IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
//OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
//DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
//OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.


(Left) Annabell_C.bmp and (right) Annabell_I.bmp. The
X.byt, Y.byt and Z.byt are not shown.
We have a PreMat5 format consisting of a A.byt, G.byt and
either, or both XP.byt and YP.byt. A.byt is a 4x4 transformation matrix in
floating point, the G.byt is the calibration grid data with the first two bytes
being a short integer number representing the number of calibration points, and
the point format being floating point values for Xw, Yw, Zw, Xc, Yc, Xp and Yp. The
XP.byt and/or YP.byt matrices are the “phase” of the projected patterns.
From these, the X.byt, Y.byt and Z.byt can be generated for multiple scanners.
Other mat5 matrices that we sometimes use but generally don’t support are
P.byt and O.byt. The P.byt matrix is the “phase” of the surface
which has not yet been converted to X,Y,Z and is context dependent on the set
up of the scanner. The O.byt with stands for original texture image. We use
this to store the raw texture image prior to Bayer filtering or other color
mapping algorithms.
Samples of mat5 formatted data: weisu_mat5.zip,
fingerprint0.jpg,
finger0_crop.ZIP
NEW F FORMAT MAT5: We imbedded our 5 matrices into a
single 32 bit BMP formatted file that ends with *F.bmp. Try downloading the
bitmap below and view in our new GL3Dview.exe
program.
Copyright (c) 2009, University of Kentucky, Lexington
All rights reserved.
Redistribution and use in source and binary forms, with
or without modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above
copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the
above copyright notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS
PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// BITMAP STRUCTURES
typedef struct
{
unsigned short ImageFileType; // Image file type
always 4d42h ("BMP")
unsigned long FileSize; // Physical file size in bytes
unsigned short reserved1; // always 0
unsigned short reserved2; // always 0
unsigned long ImageDataOffset; // Start of image data offset in bytes
} BMPHEADER;
struct BMPINFOHEADER
{
unsigned short HeaderSize; // size of this header
unsigned short Spacer;
unsigned long ImageWidth; // image width in pixels
unsigned long ImageHeight; // image height in pixels
unsigned short
NumberOfImagePlanes; // always 1
unsigned short BitsPerPixel; // 1 4 8 or 24
unsigned long
CompressionMethod; // 0(uncompressed) 1(8 bit RLE) or 2(4bit RLE)
unsigned long SizeOfBitmap; // in bytes
unsigned long HorzResolution; // pixels per meter
unsigned long VertResolution; // pixels per meter
unsigned long NumColorsUsed; // 0 implies max posible size
unsigned long
NumSignificantColors; // 0 implies all colors significant
};
/*. BMP pixel structure .*/
typedef struct
{
unsigned char b; /*. 1 byte .*/
unsigned char g; /*. 1 byte .*/
unsigned char r; /*. 1 byte .*/
} BMPPIXEL;
/*. 32 bit BMP pixel structure .*/
typedef struct
{
unsigned char b; /*. 1 byte .*/
unsigned char g; /*. 1 byte .*/
unsigned char r; /*. 1 byte .*/
unsigned char blank; /*. 1 byte .*/
} BMPPIXEL32;
/*. MAT5 pixel structure .*/
typedef struct
{
BMPPIXEL
C; /*. 3 byte .*/
unsigned char I; /*. 1 byte .*/
float X; /*. 4 bytes .*/
float Y; /*. 4 bytes .*/
float Z; /*. 4 bytes .*/
} MAT5PIXEL;
/*****************************************************/
// Hassebrook copyright 2009, University of Kentucky copyright 2009
// initialized 8-19-09
// returns mat5 knowing that CIXYZ exists
short fileCIXYZ2mat5(char
*mat5prefix,BMPPIXEL *bmpimageC,BMPPIXEL *bmpimageI,float *fltimageX,float *fltimageY,float *fltimageZ,unsigned long Nx,unsigned long My)
{
char filename[512];
short iresult;
// input C
strcpy_ansi(filename,mat5prefix);
strcat_ansi(filename,"C.bmp");
iresult=mat5bmpin(bmpimageC,filename,Nx,My);
// input I
strcpy_ansi(filename,mat5prefix);
strcat_ansi(filename,"I.bmp");
iresult=mat5bmpin(bmpimageI,filename,Nx,My);
// input X,Y,Z
strcpy_ansi(filename,mat5prefix);
strcat_ansi(filename,"X.byt");
iresult=mat5fltfileio(0,filename,fltimageX,Nx,My);
strcpy_ansi(filename,mat5prefix);
strcat_ansi(filename,"Y.byt");
iresult=mat5fltfileio(0,filename,fltimageY,Nx,My);
strcpy_ansi(filename,mat5prefix);
strcat_ansi(filename,"Z.byt");
iresult=mat5fltfileio(0,filename,fltimageZ,Nx,My);
return(iresult);
}
/*******************************************************/
/*******************************************************/
short mat5bmpin(BMPPIXEL *bmpimage,char *bmpfilein,unsigned long Nx,unsigned long My)
{
BMPPIXEL bmppixel,bmpblack,bmpwhite,bmptable[256];
BMPHEADER bmphead;
BMPINFOHEADER bmpinfohead;
FILE *fp;
short dtype;
unsigned long index,indexbit;
unsigned long
j,m,n,m1,n1,Ntable;
unsigned long
mirror,ipad,pad2,n1bit,n4bit;
unsigned char
btemp,bitvalues[8],highnibble,lownibble;
bmpblack.r=bmpblack.g=bmpblack.b=0;
bmpwhite.r=bmpwhite.g=bmpwhite.b=255;
// input BMP header
errno_t err;
err=fopen_s(&fp,bmpfilein,"rb");
if(err!=0) return(0);
fseek(fp,0,SEEK_SET);
/* read the header */
fread(&bmphead.ImageFileType,sizeof(bmphead.ImageFileType),1,fp);
fread(&bmphead.FileSize,sizeof(bmphead.FileSize),1,fp);
fread(&bmphead.reserved1,sizeof(bmphead.reserved1),1,fp);
fread(&bmphead.reserved2,sizeof(bmphead.reserved2),1,fp);
fread(&bmphead.ImageDataOffset,sizeof(bmphead.ImageDataOffset),1,fp);
/* read the info header */
fread(&bmpinfohead,sizeof(bmpinfohead),1,fp);
// verify BMP
if(bmphead.ImageFileType!=19778)
{
MessageBeep((WORD)-1);
//MessageBox("INPUT BMP file is not
19778","Sorry.",MB_OK+MB_ICONSTOP);
return(-1);
}
/* input header*/
Nx=n1=bmpinfohead.ImageWidth;
My=m1=bmpinfohead.ImageHeight;
if((n1<1)||(m1<1)) {fclose(fp);return(0);}
if((bmpinfohead.BitsPerPixel!=24)&&(bmpinfohead.BitsPerPixel!=8)&&(bmpinfohead.BitsPerPixel!=4)&&(bmpinfohead.BitsPerPixel!=1))
{
fclose(fp);
MessageBeep((WORD)-1);
//sprintf(stemp,"INPUT BMP file has %d bits per
pixel",bmpinfohead.BitsPerPixel);
//MessageBox(stemp,"Sorry, this version only does 24 bits
only.",MB_OK+MB_ICONSTOP);
return(-2);
}
if(bmpinfohead.CompressionMethod!=0)
{
fclose(fp);
MessageBeep((WORD)-1);
//MessageBox("INPUT BMP file is compressed","Sorry,maybe
next version.",MB_OK+MB_ICONSTOP);
return(-3);
}
// input color table
fseek(fp,54,SEEK_SET);
Ntable=0;
Ntable=(bmphead.ImageDataOffset-54)/4;
if(Ntable>256) Ntable=256;
for(n=0;n<Ntable;n++)
{
fread((BMPPIXEL
*)&bmptable[n],sizeof(BMPPIXEL),1,fp);
fread((char *)&btemp,sizeof(char),1,fp);
}
fseek(fp,bmphead.ImageDataOffset,SEEK_SET);
// end of color table input
if(bmpinfohead.BitsPerPixel==24)
{
dtype=30;
pad2=4-(n1*bmpinfohead.BitsPerPixel/8)%4; // bytes to pad each
scan line so that it is a multiple of 4
if(pad2==4) pad2=0;
}
if(bmpinfohead.BitsPerPixel==8)
{
dtype=20;
pad2=4-(n1*bmpinfohead.BitsPerPixel/8)%4; // bytes to pad each
scan line so that it is a multiple of 4
if(pad2==4) pad2=0;
}
if(bmpinfohead.BitsPerPixel==1)
{
dtype=21;
n1bit=n1/8;n1bit*=8;
if(n1bit==n1) n1bit=n1/8; //full byte fill
else n1bit=(n1/8) + 1; // partial byte fill
pad2=4-(n1bit)%4; // bytes to pad each
scan line so that it is a multiple of 4
if(pad2==4) pad2=0;
}
if(bmpinfohead.BitsPerPixel==4)
{
dtype=24;
n4bit=n1/2;n4bit*=2;
if(n4bit==n1) n4bit=n1/2; //full byte fill
else n4bit=(n1/2) + 1; // partial byte fill
pad2=4-(n4bit)%4; // bytes to pad each scan
line so that it is a multiple of 4
if(pad2==4) pad2=0;
}
// load the file into the array
if(dtype==21) // 1 bit
{
for(m=0;m<m1;m++)
{
indexbit=0;
for(n=0;n<n1bit;n++)
{
mirror=(m1-1)-m;
fread((char *)&btemp,sizeof(char),1,fp);
// decompose bits
mat5byte2bit(&bitvalues[0],btemp);
for(j=0;j<8;j++)
{
if(indexbit<n1)
{
// store in output array
bmppixel=bmptable[(short)bitvalues[7-j]];
index=n1
* mirror + indexbit;
bmpimage[index]=bmppixel;
++indexbit;
}
else goto SKIPBITS;
}
SKIPBITS:;
if(n==(n1bit-1))
{
for(ipad=0;ipad<pad2;ipad++)
fread((char *)&btemp,sizeof(char),1,fp);
}
}
// n
} // m
}
if(dtype==24) // 4 bit
{
for(m=0;m<m1;m++)
{
indexbit=0;
for(n=0;n<n4bit;n++)
{
mirror=(m1-1)-m;
fread((char *)&btemp,sizeof(char),1,fp);
// decompose nibbles
mat5byte2nibble(&highnibble,&lownibble,btemp);
// store highbyte
if(indexbit<n1)
{
// convert to rgb
bmppixel=bmptable[(short)highnibble];
index=n1
* mirror + indexbit;
bmpimage[index]=bmppixel;
++indexbit;
}
// store lowbyte
if(indexbit<n1)
{
// convert to rgb
bmppixel=bmptable[(short)lownibble];
index=n1
* mirror + indexbit;
bmpimage[index]=bmppixel;
++indexbit;
}
// store in output array
if(n==(n4bit-1))
{
for(ipad=0;ipad<pad2;ipad++)
fread((char *)&btemp,sizeof(char),1,fp);
}
}
// n
} // m
}
if(dtype==20) // 8 bit
{
for(m=0;m<m1;m++)
for(n=0;n<n1;n++)
{
mirror=(m1-1)-m;
index=n1
* mirror + n;
fread((char *)&btemp,sizeof(char),1,fp);
// store in output array
bmppixel=bmptable[(short)btemp];
bmpimage[index]=bmppixel;
if(n==(n1-1))
{
for(ipad=0;ipad<pad2;ipad++)
fread((char *)&btemp,sizeof(char),1,fp);
}
}
// m,n
}
if(dtype==30) // 24 bit
{
for(m=0;m<m1;m++)
for(n=0;n<n1;n++)
{
mirror=(m1-1)-m;
index=n1
* mirror +n;
fread((BMPPIXEL
*)&bmppixel,sizeof(BMPPIXEL),1,fp);
// store in output array, swap r ang b
bmpimage[index]=bmppixel;
if(n==(n1-1))
{
for(ipad=0;ipad<pad2;ipad++)
fread((char *)&btemp,sizeof(char),1,fp);
}
}
// m,n
}
fclose(fp);
return(1);
}
short mat5fltfileio(short iotype,char *filename,float *fltimage,unsigned long Nx,unsigned long My)
{
FILE
*fpflt;
unsigned long Nindex;
errno_t
err;
Nindex=Nx*My;
if(iotype==0) //read in file
{
// open file
err=fopen_s(&fpflt,filename,"rb");
if(err!=0) return(0);
fread((float *)fltimage,1,Nindex*sizeof(float),fpflt);
fclose(fpflt);
return(1);
}
if(iotype==1)
{
// open file
err=fopen_s(&fpflt,filename,"wb");
if(err!=0) return(0);
fwrite((float *)fltimage,Nindex*sizeof(float),1,fpflt);
fclose(fpflt);
return(1);
}
return(-1);
}
/*****************************************************/
//Copyright (c) 2009, University of Kentucky, Lexington
//All rights reserved.
//Redistribution and use in source and binary forms, with
or without modification, are permitted provided that the following conditions
are met:
//1. Redistributions of source code must retain the above
copyright notice, this list of conditions and the following disclaimer.
//2. Redistributions in binary form must reproduce the
above copyright notice, this list of conditions and the following disclaimer
// in
the documentation and/or other materials provided with the distribution.
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
//BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
//IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
//OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
//DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
//OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
// Hassebrook copyright 2009, University of Kentucky copyright 2009
// initialized 8-19-09
// returns mat5 knowing that F exists
short fileF2mat5(char
*mat5prefix,BMPPIXEL *bmpimageC,BMPPIXEL *bmpimageI,float *fltimageX,float *fltimageY,float *fltimageZ,unsigned long Nx,unsigned long My)
{
char filename[512],btemp;
BMPPIXEL
black;
BMPPIXEL32
bmppixel32;
BMPHEADER
bmphead;
BMPINFOHEADER
bmpinfohead;
FILE
*fp;
unsigned long
M1,N1,ipad,pad2,m,n,m1,index;
float fltvalue;
black.r=0;black.g=0;black.b=0;
strcpy_ansi(filename,mat5prefix);
strcat_ansi(filename,"F.bmp");
// input BMP header
errno_t
err;
err=fopen_s(&fp,filename,"rb");
if(err!=0) return(0);
fseek(fp,0,SEEK_SET);
/* read the header */
fread(&bmphead.ImageFileType,sizeof(bmphead.ImageFileType),1,fp);
fread(&bmphead.FileSize,sizeof(bmphead.FileSize),1,fp);
fread(&bmphead.reserved1,sizeof(bmphead.reserved1),1,fp);
fread(&bmphead.reserved2,sizeof(bmphead.reserved2),1,fp);
fread(&bmphead.ImageDataOffset,sizeof(bmphead.ImageDataOffset),1,fp);
/* read the info header */
fread(&bmpinfohead,sizeof(bmpinfohead),1,fp);
// verify BMP
if(bmphead.ImageFileType!=19778)
{
MessageBeep((WORD)-1);
//MessageBox("INPUT BMP file is not
19778","Sorry.",MB_OK+MB_ICONSTOP);
return(-1);
}
/* input header*/
N1=bmpinfohead.ImageWidth;
M1=bmpinfohead.ImageHeight;
if((N1<1)||(M1<1)||(M1/4!=My)||(N1!=Nx)) {fclose(fp);return(0);}
if(bmpinfohead.BitsPerPixel!=32)
{
fclose(fp);
MessageBeep((WORD)-1);
//sprintf(stemp,"INPUT BMP file has %d bits per
pixel",bmpinfohead.BitsPerPixel);
//MessageBox(stemp,"Sorry, this version only does 24 bits
only.",MB_OK+MB_ICONSTOP);
return(-2);
}
if(bmpinfohead.CompressionMethod!=0)
{
fclose(fp);
MessageBeep((WORD)-1);
//MessageBox("INPUT BMP file is compressed","Sorry,maybe
next version.",MB_OK+MB_ICONSTOP);
return(-3);
}
// goto data
fseek(fp,bmphead.ImageDataOffset,SEEK_SET);
// end of color table input
if(bmpinfohead.BitsPerPixel==32)
{
pad2=4-(Nx*bmpinfohead.BitsPerPixel/8)%4; // bytes to pad each
scan line so that it is a multiple of 4
if(pad2==4) pad2=0;
}
// input Z matrix
for(m=0;m<My;m++)
for(n=0;n<Nx;n++)
{
m1=My-1-m;
index=m1*Nx+n;
fread((float *)&fltvalue,sizeof(float),1,fp);
fltimageZ[index]=fltvalue;
if(n==(Nx-1))
{
for(ipad=0;ipad<pad2;ipad++) fread((char *)&btemp,sizeof(char),1,fp);
}
}
// input Y matrix
for(m=0;m<My;m++)
for(n=0;n<Nx;n++)
{
m1=My-1-m;
index=m1*Nx+n;
fread((float *)&fltvalue,sizeof(float),1,fp);
fltimageY[index]=fltvalue;
if(n==(Nx-1))
{
for(ipad=0;ipad<pad2;ipad++) fread((char *)&btemp,sizeof(char),1,fp);
}
}
// input X matrix
for(m=0;m<My;m++)
for(n=0;n<Nx;n++)
{
m1=My-1-m;
index=m1*Nx+n;
fread((float *)&fltvalue,sizeof(float),1,fp);
fltimageX[index]=fltvalue;
if(n==(Nx-1))
{
for(ipad=0;ipad<pad2;ipad++) fread((char *)&btemp,sizeof(char),1,fp);
}
}
// store C and I image pixels
for(m=0;m<My;m++)
for(n=0;n<Nx;n++)
{
m1=My-1-m;
index=m1*Nx+n;
fread((BMPPIXEL32
*)&bmppixel32,sizeof(BMPPIXEL32),1,fp);
bmpimageC[index].r=bmppixel32.r;
bmpimageC[index].g=bmppixel32.g;
bmpimageC[index].b=bmppixel32.b;
bmpimageI[index].r=bmppixel32.blank;
bmpimageI[index].g=bmppixel32.blank;
bmpimageI[index].b=bmppixel32.blank;
if(n==(Nx-1))
{
btemp=0;
for(ipad=0;ipad<pad2;ipad++) fread((char *)&btemp,sizeof(char),1,fp);
}
}
fclose(fp);
return(0);
}