/* * Copyright 2015 The Etc2Comp Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifdef _WIN32 #define _CRT_SECURE_NO_WARNINGS (1) #endif #include "EtcConfig.h" #include "EtcComparison.h" #include "EtcAnalysis.h" #include "EtcTool.h" #include "Etc.h" #include "EtcFile.h" #include "EtcMath.h" #include "EtcImage.h" #include "EtcBlock4x4.h" #include "EtcBlock4x4Encoding_ETC1.h" #include "EtcBlock4x4Encoding_RGB8.h" #include "EtcBlock4x4Encoding_R11.h" #include "EtcBlock4x4Encoding_RG11.h" #include "lodepng.h" #include #include #include #include namespace Etc { const float Comparison::ERROR_EPSILON = 0.000001f; // ---------------------------------------------------------------------------------------------------- // Comparison::Comparison(Analysis *a_panalysisParent, unsigned int a_uiIndex, const char *a_pstrFilename, int a_iPixelX, int a_iPixelY) { m_panalysisParent = a_panalysisParent; m_uiIndex = a_uiIndex; m_pstrFilename = new char[strlen(a_pstrFilename)+1]; strcpy(m_pstrFilename, a_pstrFilename); SetName(); m_pstrOutputFolder = new char[256]; sprintf(m_pstrOutputFolder, "%s%cComparison_%s", m_panalysisParent->GetOutputFolder(), ETC_PATH_SLASH, m_pstrName); CreateNewDir(m_pstrOutputFolder); // read etc file Etc::File etcfile(a_pstrFilename, Etc::File::Format::INFER_FROM_FILE_EXTENSION); etcfile.UseSingleBlock(a_iPixelX, a_iPixelY); // construct image with encoding bits m_pimage = new Image(etcfile.GetImageFormat(), etcfile.GetSourceWidth(), etcfile.GetSourceHeight(), etcfile.GetEncodingBits(), etcfile.GetEncodingBitsBytes(), a_panalysisParent->GetImage(), a_panalysisParent->GetImage()->GetErrorMetric()); if (m_pimage->GetExtendedWidth() != a_panalysisParent->GetImage()->GetExtendedWidth() || m_pimage->GetExtendedHeight() != a_panalysisParent->GetImage()->GetExtendedHeight()) { printf("Error: comparison image (%s) has different width or height\n", m_pstrFilename); exit(1); } // scale = 1 Analysis::DrawImage(m_pimage, m_pstrOutputFolder, false); // scale = 2, with modes Analysis::DrawImage(m_pimage, m_pstrOutputFolder, true); DrawImageComparison(m_panalysisParent->GetImage()); WriteLogFile(); } // ---------------------------------------------------------------------------------------------------- // void Comparison::DrawImageComparison(Image *a_pimageUnderTest) { assert(m_pimage->GetExtendedWidth() == a_pimageUnderTest->GetExtendedWidth() && m_pimage->GetExtendedHeight() == a_pimageUnderTest->GetExtendedHeight()); unsigned int uiPngWidth = 2 * m_pimage->GetExtendedWidth(); unsigned int uiPngHeight = 2 * m_pimage->GetExtendedHeight(); unsigned char* paucPngPixels = new unsigned char[uiPngWidth * uiPngHeight * 4]; assert(paucPngPixels); ColorR8G8B8A8 *pargba8PngPixels = (ColorR8G8B8A8 *)paucPngPixels; for (unsigned int uiBlock = 0; uiBlock < m_pimage->GetNumberOfBlocks(); uiBlock++) { Block4x4 *pblock = &m_pimage->GetBlocks()[uiBlock]; Block4x4 *pblockUnderTest = &a_pimageUnderTest->GetBlocks()[uiBlock]; if (a_pimageUnderTest->GetFormat() == Image::Format::R11 || a_pimageUnderTest->GetFormat() == Image::Format::SIGNED_R11) { Analysis::DrawBlockPixels(pblock, pargba8PngPixels, uiPngWidth, true, 2); } else { Analysis::DrawBlockPixels(pblock, pargba8PngPixels, uiPngWidth, false, 2); } float fErrorBlockReference = pblock->GetError(); float fErrorBlockUnderTest = pblockUnderTest->GetError(); DrawBlockComparison2x(pblockUnderTest, pargba8PngPixels, uiPngWidth, fErrorBlockUnderTest, fErrorBlockReference); } char strFilename[200]; sprintf(strFilename, "%s%cComparison.png", m_pstrOutputFolder, ETC_PATH_SLASH); unsigned iResult = lodepng_encode32_file(strFilename, paucPngPixels, uiPngWidth, uiPngHeight); if (iResult != 0) { printf("Error couldn't write modes image (%s)\n", strFilename); } } // ---------------------------------------------------------------------------------------------------- // void Comparison::DrawBlockComparison2x(Block4x4 *a_pblockUnderTest, ColorR8G8B8A8 *a_pargba8Output, unsigned int a_uiOutputWidth, float a_fErrorBlockUnderTest, float a_fErrorBlockReference) { static const unsigned int SCALE = 2; typedef struct { int iH; int iV; } PixelCoord; static const PixelCoord s_apixelcoordDifference0[] = { { 0,0 },{ 1,0 },{ 2,0 },{ 3,0 },{ 4,0 },{ 5,0 },{ 6,0 },{ 7,0 }, { 0,1 },{ 0,2 },{ 0,3 },{ 0,4 },{ 0,5 },{ 0,6 },{ 0,7 }, { 1,7 },{ 2,7 },{ 3,7 },{ 4,7 },{ 5,7 },{ 6,7 },{ 7,7 }, { 7,1 },{ 7,2 },{ 7,3 },{ 7,4 },{ 7,5 },{ 7,6 },{ -1,-1 } }; static const PixelCoord s_apixelcoordDifference5[] = { { 3,3 }, { 0,0 },{ 1,0 },{ 2,0 },{ 3,0 },{ 4,0 },{ 5,0 },{ 6,0 },{ 7,0 }, { 0,1 },{ 0,2 },{ 0,3 },{ 0,4 },{ 0,5 },{ 0,6 },{ 0,7 }, { 1,7 },{ 2,7 },{ 3,7 },{ 4,7 },{ 5,7 },{ 6,7 },{ 7,7 }, { 7,1 },{ 7,2 },{ 7,3 },{ 7,4 },{ 7,5 },{ 7,6 },{ -1,-1 } }; static const PixelCoord s_apixelcoordDifference10[] = { { 3,3 },{ 3,4 },{ 4,3 },{ 4,4 }, { 0,0 },{ 1,0 },{ 2,0 },{ 3,0 },{ 4,0 },{ 5,0 },{ 6,0 },{ 7,0 }, { 0,1 },{ 0,2 },{ 0,3 },{ 0,4 },{ 0,5 },{ 0,6 },{ 0,7 }, { 1,7 },{ 2,7 },{ 3,7 },{ 4,7 },{ 5,7 },{ 6,7 },{ 7,7 }, { 7,1 },{ 7,2 },{ 7,3 },{ 7,4 },{ 7,5 },{ 7,6 },{ -1,-1 } }; static const PixelCoord s_apixelcoordDifference20[] = { { 2,2 },{ 2,3 },{ 3,2 },{ 3,3 },{ 4,4 },{ 4,5 },{ 5,4 },{ 5,5 }, { 0,0 },{ 1,0 },{ 2,0 },{ 3,0 },{ 4,0 },{ 5,0 },{ 6,0 },{ 7,0 }, { 0,1 },{ 0,2 },{ 0,3 },{ 0,4 },{ 0,5 },{ 0,6 },{ 0,7 }, { 1,7 },{ 2,7 },{ 3,7 },{ 4,7 },{ 5,7 },{ 6,7 },{ 7,7 }, { 7,1 },{ 7,2 },{ 7,3 },{ 7,4 },{ 7,5 },{ 7,6 },{ -1,-1 } }; static const PixelCoord s_apixelcoordCorners[] = { { 0,0 },{ 7,0 },{ 0,7 },{ 7,7 },{ -1,-1 } }; // output pixel coord of upper left corner of block unsigned int uiBlockH = SCALE * a_pblockUnderTest->GetSourceH(); unsigned int uiBlockV = SCALE * a_pblockUnderTest->GetSourceV(); ColorR8G8B8A8 *pargba8Block = &a_pargba8Output[uiBlockV*a_uiOutputWidth + uiBlockH]; float fRelativeError = 0.0f; if (a_fErrorBlockUnderTest != a_fErrorBlockReference) { fRelativeError = fabs((a_fErrorBlockUnderTest - a_fErrorBlockReference) / (a_fErrorBlockUnderTest + a_fErrorBlockReference)); if (fabsf(a_fErrorBlockUnderTest - a_fErrorBlockReference) < ERROR_EPSILON) { fRelativeError = 0.0f; } } ColorR8G8B8A8 rgb8Draw; rgb8Draw.ucA = 255; // equal if (fRelativeError == 0.0f) { rgb8Draw.ucR = 128; rgb8Draw.ucG = 128; rgb8Draw.ucB = 128; } // better tthan reference else if (a_fErrorBlockUnderTest < a_fErrorBlockReference) { rgb8Draw.ucR = 0; rgb8Draw.ucG = 255; rgb8Draw.ucB = 0; } // worse than reference else if (a_fErrorBlockUnderTest > a_fErrorBlockReference) { rgb8Draw.ucR = 255; rgb8Draw.ucG = 0; rgb8Draw.ucB = 0; } const PixelCoord *papixelcoordDraw = s_apixelcoordCorners; // if 20% worse if (fRelativeError >= 1.44f) { papixelcoordDraw = s_apixelcoordDifference20; } // if 10% worse else if (fRelativeError >= 1.21f) { papixelcoordDraw = s_apixelcoordDifference10; } // if 5% worse else if (fRelativeError >= 1.1025f) { papixelcoordDraw = s_apixelcoordDifference5; } else if (fRelativeError > 0.0f) { papixelcoordDraw = s_apixelcoordDifference0; } // outline for (const PixelCoord *pcoord = papixelcoordDraw; pcoord->iH >= 0; pcoord++) { ColorR8G8B8A8 *prgba8 = &pargba8Block[pcoord->iV*a_uiOutputWidth + pcoord->iH]; *prgba8 = rgb8Draw; } } // ---------------------------------------------------------------------------------------------------- // void Comparison::SetName(void) { // alloc memory for name unsigned int uiNameBytes = (unsigned int)strlen(m_pstrFilename) + 1; if (uiNameBytes < 4) { uiNameBytes = 4; } m_pstrName = new char[uiNameBytes]; m_pstrName[0] = 0; // first, try to find folder name of comparison image { char *pstr = new char[strlen(m_pstrFilename) + 1]; strcpy(pstr, m_pstrFilename); int iLastSlash; int iPenultimateSlash; // find last slash for (iLastSlash = (int)strlen(pstr) - 1; iLastSlash >= 0; iLastSlash--) { if (pstr[iLastSlash] == ETC_PATH_SLASH) { break; } } // find penultimate slash for (iPenultimateSlash = iLastSlash - 1; iPenultimateSlash >= 0; iPenultimateSlash--) { if (pstr[iPenultimateSlash] == ETC_PATH_SLASH) { break; } } if (iLastSlash > 0) { pstr[iLastSlash] = 0; strcpy(m_pstrName, &pstr[iPenultimateSlash] + 1); assert(strlen(m_pstrName) > 0); } delete[] pstr; } // otherwise use index as name if (m_pstrName[0] == 0) { sprintf(m_pstrName, "%u", m_uiIndex); } } // ---------------------------------------------------------------------------------------------------- // void Comparison::WriteLogFile(void) { char strPath[200]; sprintf(strPath, "%s%cComparison.txt", m_pstrOutputFolder, ETC_PATH_SLASH); FILE *pfile = fopen(strPath, "wt"); if (pfile == nullptr) { printf("Error couldn't open comparison log file (%s)\n", strPath); exit(1); } float fImageError = m_panalysisParent->GetImage()->GetError(); unsigned int uiImagePixels = m_panalysisParent->GetImage()->GetSourceWidth() * m_panalysisParent->GetImage()->GetSourceHeight(); if (m_pimage->GetFormat() == Image::Format::R11 || m_pimage->GetFormat() == Image::Format::SIGNED_R11) { fprintf(pfile, "PSNR(r) = %.4f\n", Analysis::ConvertErrorToPSNR(fImageError, 1 * uiImagePixels)); fprintf(pfile, "\n"); } else if (m_pimage->GetFormat() == Image::Format::RG11 || m_pimage->GetFormat() == Image::Format::SIGNED_RG11) { fprintf(pfile, "PSNR(rg) = %.4f\n", Analysis::ConvertErrorToPSNR(fImageError, 2 * uiImagePixels)); fprintf(pfile, "\n"); } else { fprintf(pfile, "PSNR(rgb) = %.4f\n", Analysis::ConvertErrorToPSNR(fImageError, 3 * uiImagePixels)); fprintf(pfile, "PSNR(rgba) = %.4f\n", Analysis::ConvertErrorToPSNR(fImageError, 4 * uiImagePixels)); fprintf(pfile, "\n"); } float fReferenceImageError = m_pimage->GetError(); fprintf(pfile, "reference image %s\n", m_pstrFilename); if (m_pimage->GetFormat() == Image::Format::R11 || m_pimage->GetFormat() == Image::Format::SIGNED_R11) { fprintf(pfile, "reference PSNR(r) = %.4f\n", Analysis::ConvertErrorToPSNR(fReferenceImageError, 1 * uiImagePixels)); } else if (m_pimage->GetFormat() == Image::Format::RG11 || m_pimage->GetFormat() == Image::Format::SIGNED_RG11) { fprintf(pfile, "reference PSNR(rg) = %.4f\n", Analysis::ConvertErrorToPSNR(fReferenceImageError, 2 * uiImagePixels)); } else { fprintf(pfile, "reference PSNR(rgb) = %.4f\n", Analysis::ConvertErrorToPSNR(fReferenceImageError, 3 * uiImagePixels)); fprintf(pfile, "reference PSNR(rgba) = %.4f\n", Analysis::ConvertErrorToPSNR(fReferenceImageError, 4 * uiImagePixels)); } WriteBetterOrWorseBlocks(pfile, false); WriteBetterOrWorseBlocks(pfile, true); fclose(pfile); } // ---------------------------------------------------------------------------------------------------- // void Comparison::WriteBetterOrWorseBlocks(FILE *a_pfile, bool a_boolWorse) { fprintf(a_pfile, "\n"); if (a_boolWorse) { fprintf(a_pfile, "Worse Blocks\n"); } else { fprintf(a_pfile, "Better Blocks\n"); } for (unsigned int uiBlock = 0; uiBlock < m_pimage->GetNumberOfBlocks(); uiBlock++) { Block4x4 *pblockReference = &m_pimage->GetBlocks()[uiBlock]; Block4x4 *pblockUnderTest = &m_panalysisParent->GetImage()->GetBlocks()[uiBlock]; float fErrorBlockReference = pblockReference->GetError(); float fErrorBlockUnderTest = pblockUnderTest->GetError(); if (fabsf(fErrorBlockUnderTest - fErrorBlockReference) >= ERROR_EPSILON) { if ((a_boolWorse && (fErrorBlockUnderTest > fErrorBlockReference)) || (!a_boolWorse && (fErrorBlockUnderTest < fErrorBlockReference))) { fprintf(a_pfile, "HV=%u,%u\n", pblockUnderTest->GetSourceH(), pblockUnderTest->GetSourceV()); WriteBlockInfo(a_pfile, pblockUnderTest, "bsi"); WriteBlockInfo(a_pfile, pblockReference, "ref"); } } } } // ---------------------------------------------------------------------------------------------------- // void Comparison::WriteBlockInfo(FILE *a_pfile, Block4x4 *a_pblock, const char *a_pstrEncoder) { Block4x4Encoding *pencoding = a_pblock->GetEncoding(); Block4x4Encoding_R11 *pencoding_r11 = static_cast(pencoding); Block4x4Encoding_RG11 *pencoding_rg11 = static_cast(pencoding); Block4x4Encoding_RGB8 *pencoding_rgb8 = static_cast(pencoding); fprintf(a_pfile, " %s:", a_pstrEncoder); float fPSNR = 0.0f; if (m_pimage->GetFormat() == Image::Format::R11 || m_pimage->GetFormat() == Image::Format::SIGNED_R11) { fPSNR = Analysis::ConvertErrorToPSNR(a_pblock->GetError(), 1 * 16); } else if (m_pimage->GetFormat() == Image::Format::RG11 || m_pimage->GetFormat() == Image::Format::SIGNED_RG11) { fPSNR = Analysis::ConvertErrorToPSNR(a_pblock->GetError(), 2 * 16); } else { fPSNR = Analysis::ConvertErrorToPSNR(a_pblock->GetError(), 3 * 16); } if (a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_RG11) { fprintf(a_pfile, "psnr=%6.3f\n", fPSNR); } else { fprintf(a_pfile, "psnr=%6.3f ", fPSNR); } bool boolWriteSelectors = false; if (m_pimage->GetFormat() == Image::Format::RGBA8) { fprintf(a_pfile, "RGBA8 "); fprintf(a_pfile, "alpha base(%4.0f) ", pencoding_r11->GetRedBase()); fprintf(a_pfile, "alpha multiplier(%3.0f) ", pencoding_r11->GetRedMultiplier()); fprintf(a_pfile, "alpha table index(%2d) ", pencoding_r11->GetRedTableIndex()); } if (a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_ETC1) { fprintf(a_pfile, "ETC1(%c,%c) ", a_pblock->IsDifferential() ? 'D' : 'I', a_pblock->GetFlip() ? 'V' : 'H' ); if (a_pblock->IsDifferential()) { fprintf(a_pfile, "color1(%2d,%2d,%2d) ", pencoding_rgb8->GetColor1().IntRed(31.0f), pencoding_rgb8->GetColor1().IntGreen(31.0f), pencoding_rgb8->GetColor1().IntBlue(31.0f) ); fprintf(a_pfile, "color2(%2d,%2d,%2d) ", pencoding_rgb8->GetColor2().IntRed(31.0f), pencoding_rgb8->GetColor2().IntGreen(31.0f), pencoding_rgb8->GetColor2().IntBlue(31.0f)); } else { fprintf(a_pfile, "color1(%2d,%2d,%2d) ", pencoding_rgb8->GetColor1().IntRed(15.0f), pencoding_rgb8->GetColor1().IntGreen(15.0f), pencoding_rgb8->GetColor1().IntBlue(15.0f)); fprintf(a_pfile, "color2(%2d,%2d,%2d) ", pencoding_rgb8->GetColor2().IntRed(15.0f), pencoding_rgb8->GetColor2().IntGreen(15.0f), pencoding_rgb8->GetColor2().IntBlue(15.0f)); } fprintf(a_pfile, "cw1(%d) cw2(%d) ", pencoding_rgb8->GetCW1(), pencoding_rgb8->GetCW2()); boolWriteSelectors = true; } else if (a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_T || a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_H) { fprintf(a_pfile, "%-9s ", a_pblock->GetEncodingModeName()); fprintf(a_pfile, "color1(%2d,%2d,%2d) ", pencoding_rgb8->GetColor1().IntRed(15.0f), pencoding_rgb8->GetColor1().IntGreen(15.0f), pencoding_rgb8->GetColor1().IntBlue(15.0f)); fprintf(a_pfile, "color2(%2d,%2d,%2d) ", pencoding_rgb8->GetColor2().IntRed(15.0f), pencoding_rgb8->GetColor2().IntGreen(15.0f), pencoding_rgb8->GetColor2().IntBlue(15.0f)); fprintf(a_pfile, "cw1(%d) ", pencoding_rgb8->GetCW1() ); boolWriteSelectors = true; } else if (a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_PLANAR) { fprintf(a_pfile, "%-9s ", a_pblock->GetEncodingModeName()); fprintf(a_pfile, "color1(%2d,%2d,%2d) ", pencoding_rgb8->GetColor1().IntRed(63.0f), pencoding_rgb8->GetColor1().IntGreen(127.0f), pencoding_rgb8->GetColor1().IntBlue(63.0f)); fprintf(a_pfile, "color2(%2d,%2d,%2d) ", pencoding_rgb8->GetColor2().IntRed(63.0f), pencoding_rgb8->GetColor2().IntGreen(127.0f), pencoding_rgb8->GetColor2().IntBlue(63.0f)); fprintf(a_pfile, "color3(%2d,%2d,%2d) ", pencoding_rgb8->GetColor3().IntRed(63.0f), pencoding_rgb8->GetColor3().IntGreen(127.0f), pencoding_rgb8->GetColor3().IntBlue(63.0f)); } else if (a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_R11) { fprintf(a_pfile, "R11 "); fprintf(a_pfile, "base(%4.0f) ", pencoding_r11->GetRedBase()); fprintf(a_pfile, "multiplier(%3.0f) ", pencoding_r11->GetRedMultiplier()); fprintf(a_pfile, "table index(%2d) ", pencoding_r11->GetRedTableIndex()); boolWriteSelectors = true; } else if (a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_RG11) { fprintf(a_pfile, "RG11 image: %s\n", a_pstrEncoder); fprintf(a_pfile, "red base(%4.0f)\n", pencoding_rg11->GetRedBase()); fprintf(a_pfile, "grn base(%4.0f)\n\n", pencoding_rg11->GetGrnBase()); fprintf(a_pfile, "red multiplier(%3.0f)\n", pencoding_rg11->GetRedMultiplier()); fprintf(a_pfile, "grn multiplier(%3.0f)\n\n", pencoding_rg11->GetGrnMultiplier()); fprintf(a_pfile, "red table index(%2d)\n", pencoding_rg11->GetRedTableIndex()); fprintf(a_pfile, "grn table index(%2d)\n\n", pencoding_rg11->GetGrnTableIndex()); boolWriteSelectors = true; } else { assert(0); } if (boolWriteSelectors) { size_t selectorStringSize = Block4x4Encoding::PIXELS*4; char *redSelectors = new char[selectorStringSize]; char *grnSelectors = new char[selectorStringSize]; memset(&redSelectors[0], 0, selectorStringSize); memset(&grnSelectors[0], 0, selectorStringSize); if (a_pblock->GetEncodingMode() != Block4x4Encoding::MODE_RG11) { fprintf(a_pfile, "selectors("); } for (unsigned int uiPixel = 0; uiPixel < Block4x4Encoding::PIXELS; uiPixel++) { if (a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_R11) { fprintf(a_pfile, "%u", pencoding_r11->GetRedSelectors()[uiPixel]); } else if (a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_RG11) { char temp[2]; sprintf(temp,"%d",pencoding_rg11->GetRedSelectors()[uiPixel]); strcat(redSelectors,temp); sprintf(temp, "%d", pencoding_rg11->GetGrnSelectors()[uiPixel]); strcat(grnSelectors, temp); strcat(redSelectors, ",\0"); strcat(grnSelectors, ",\0"); fprintf(a_pfile, "pixel[%d]:(%f,%f,%f)\n", uiPixel, pencoding->GetDecodedColors()[uiPixel].fR, pencoding->GetDecodedColors()[uiPixel].fG, pencoding->GetDecodedColors()[uiPixel].fB); } else { fprintf(a_pfile, "%u", pencoding_rgb8->GetSelectors()[uiPixel]); } } if (a_pblock->GetEncodingMode() == Block4x4Encoding::MODE_RG11) { redSelectors[selectorStringSize - 1] = '\0'; grnSelectors[selectorStringSize - 1] = '\0'; fprintf(a_pfile, "selectors(red: %s)\n", redSelectors); fprintf(a_pfile, "selectors(grn: %s)\n,", grnSelectors); delete[] redSelectors; redSelectors = NULL; delete[] grnSelectors; grnSelectors = NULL; } else { fprintf(a_pfile, ")"); } } fprintf(a_pfile, "\n"); } // ---------------------------------------------------------------------------------------------------- // }