Как мне написать 1bpp tiff с помощью libtiff на iOS?

Я пытаюсь написать UIImage как tiff, используя libtiff. Проблема в том, что хотя я пишу это как 1 бит на пиксель, файлы по-прежнему выходят в диапазоне 2-5 МБ, когда я ожидаю что-то вроде 100 КБ или меньше.

Вот что у меня есть.

- (void) convertUIImage:(UIImage *)uiImage toTiff:(NSString *)file withThreshold:(float)threshold {

    TIFF *tiff;
    if ((tiff = TIFFOpen([file UTF8String], "w")) == NULL) {
        [[[UIAlertView alloc] initWithTitle:@"Error" message:[NSString stringWithFormat:@"Unable to write to file %@.", file] delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
        return;
    }

    CGImageRef image = [uiImage CGImage];

    CGDataProviderRef provider = CGImageGetDataProvider(image);
    CFDataRef pixelData = CGDataProviderCopyData(provider);
    unsigned char *buffer = (unsigned char *)CFDataGetBytePtr(pixelData);

    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image);
    CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(image);
    size_t compBits = CGImageGetBitsPerComponent(image);
    size_t pixelBits = CGImageGetBitsPerPixel(image);
    size_t width = CGImageGetWidth(image);
    size_t height = CGImageGetHeight(image);
    NSLog(@"bitmapInfo=%d, alphaInfo=%d, pixelBits=%lu, compBits=%lu, width=%lu, height=%lu", bitmapInfo, alphaInfo, pixelBits, compBits, width, height);


    TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, width);
    TIFFSetField(tiff, TIFFTAG_IMAGELENGTH, height);
    TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 1);
    TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, 1);
    TIFFSetField(tiff, TIFFTAG_ROWSPERSTRIP, 1);

    TIFFSetField(tiff, TIFFTAG_FAXMODE, FAXMODE_CLASSF);
    TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_CCITTFAX4);
    TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
    TIFFSetField(tiff, TIFFTAG_FILLORDER, FILLORDER_MSB2LSB);
    TIFFSetField(tiff, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);

    TIFFSetField(tiff, TIFFTAG_XRESOLUTION, 200.0);
    TIFFSetField(tiff, TIFFTAG_YRESOLUTION, 200.0);
    TIFFSetField(tiff, TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH);

    unsigned char red, green, blue, gray, bite;
    unsigned char *line = (unsigned char *)_TIFFmalloc(width/8);
    unsigned long pos;
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            pos = y * width * 4 + x * 4; // multiplying by four because each pixel is represented by four bytes
            red = buffer[ pos ];
            green = buffer[ pos + 1 ];
            blue = buffer[ pos + 2 ];
            gray = .3 * red + .59 * green + .11 * blue; // http://answers.yahoo.com/question/index?qid=20100608031814AAeBHPU


            bite = line[x / 8];
            bite = bite << 1;
            if (gray > threshold) bite = bite | 1;
//            NSLog(@"y=%d, x=%d, byte=%d, red=%d, green=%d, blue=%d, gray=%d, before=%@, after=%@", y, x, x/8, red, green, blue, gray, [self bitStringForChar:line[x / 8]], [self bitStringForChar:bite]);
            line[x / 8] = bite;
        }
        TIFFWriteEncodedStrip(tiff, y, line, width);
    }

    // Close the file and free buffer
    TIFFClose(tiff);
    if (line) _TIFFfree(line);
    if (pixelData) CFRelease(pixelData);

}

Первая строка NSLog говорит:

bitmapInfo=5, alphaInfo=5, pixelBits=32, compBits=8, width=3264, height=2448

У меня также есть версия этого проекта, в которой вместо этого используется GPUImage. При этом я могу получить то же изображение примерно до 130 КБ в виде 8-битного PNG. Если я отправлю этот PNG на сайт оптимизатора PNG, они могут уменьшить его примерно до 25 КБ. Если кто-то может показать мне, как написать 1-битный PNG, сгенерированный из моих фильтров GPUImage, я воздержусь от tiff.

Спасибо!


person bmauter    schedule 20.02.2014    source источник


Ответы (2)


Мне нужно сгенерировать изображение TIFF на iPhone и отправить его на удаленный сервер, который ожидает файлы TIFF. Я не могу использовать принятый ответ, который преобразуется в 1bpp PNG, и я работаю над решением для преобразования в формат TIFF, 1bpp CCITT Group 4 с использованием libTIFF.

После отладки метода я нашел, где находятся ошибки, и наконец получил правильное решение.

Следующий блок кода является решением. Прочитайте после кода, чтобы найти объяснение ошибок в методе OP.

- (void) convertUIImage:(UIImage *)uiImage toTiff:(NSString *)file withThreshold:(float)threshold {

    CGImageRef srcCGImage = [uiImage CGImage];
    CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(srcCGImage));
    unsigned char *pixelDataPtr = (unsigned char *)CFDataGetBytePtr(pixelData);

    TIFF *tiff;
    if ((tiff = TIFFOpen([file UTF8String], "w")) == NULL) {
        [[[UIAlertView alloc] initWithTitle:@"Error" message:[NSString stringWithFormat:@"Unable to write to file %@.", file] delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
        return;
    }

    size_t width = CGImageGetWidth(srcCGImage);
    size_t height = CGImageGetHeight(srcCGImage);

    TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, width);
    TIFFSetField(tiff, TIFFTAG_IMAGELENGTH, height);
    TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 1);
    TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, 1);
    TIFFSetField(tiff, TIFFTAG_ROWSPERSTRIP, 1);

    TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_CCITTFAX4);
    TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);
    TIFFSetField(tiff, TIFFTAG_FILLORDER, FILLORDER_MSB2LSB);
    TIFFSetField(tiff, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);

    TIFFSetField(tiff, TIFFTAG_XRESOLUTION, 200.0);
    TIFFSetField(tiff, TIFFTAG_YRESOLUTION, 200.0);
    TIFFSetField(tiff, TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH);

    unsigned char *ptr = pixelDataPtr; // initialize pointer to the first byte of the image buffer 
    unsigned char red, green, blue, gray, eightPixels;
    tmsize_t bytesPerStrip = ceil(width/8.0);
    unsigned char *strip = (unsigned char *)_TIFFmalloc(bytesPerStrip);

    for (int y=0; y<height; y++) {
        for (int x=0; x<width; x++) {
            red = *ptr++; green = *ptr++; blue = *ptr++;
            ptr++; // discard fourth byte by advancing the pointer 1 more byte
            gray = .3 * red + .59 * green + .11 * blue; // http://answers.yahoo.com/question/index?qid=20100608031814AAeBHPU
            eightPixels = strip[x/8];
            eightPixels = eightPixels << 1;
            if (gray < threshold) eightPixels = eightPixels | 1; // black=1 in tiff image without TIFFTAG_PHOTOMETRIC header
            strip[x/8] = eightPixels;
        }
        TIFFWriteEncodedStrip(tiff, y, strip, bytesPerStrip);
    }

    TIFFClose(tiff);
    if (strip) _TIFFfree(strip);
    if (pixelData) CFRelease(pixelData);
}

Вот ошибки и объяснение того, что не так.

1) выделение памяти под одну строку развертки мало на 1 байт, если ширина изображения не кратна 8.

unsigned char *line = (unsigned char *)_TIFFmalloc(width/8);

следует заменить на

tmsize_t bytesPerStrip = ceil(width/8.0); unsigned char *line = (unsigned char *)_TIFFmalloc(bytesPerStrip);

Объяснение в том, что мы должны взять потолок деления на 8, чтобы получить количество байтов для полосы. Например, полосе из 83 пикселей нужно 11 байт, а не 10, иначе мы можем потерять 3 последних пикселя. Также обратите внимание, что мы должны разделить на 8,0, чтобы получить число с плавающей запятой и передать его функции ceil. При целочисленном делении в C теряется десятичная часть и округляется до нуля, что в нашем случае неверно.

2) последний аргумент, переданный в функцию TIFFWriteEncodedStrip, неверен. Мы не можем передать количество пикселей в полосе, мы должны передать количество байтов на полосу.

Итак, замените:

TIFFWriteEncodedStrip(tiff, y, line, width);

by

TIFFWriteEncodedStrip(tiff, y, line, bytesPerStrip);

3) Последняя ошибка, которую трудно обнаружить, связана с соглашением о том, представляет ли бит со значением 0 белый или черный цвет в двухтональном изображении. Благодаря заголовку TIFF TIFFTAG_PHOTOMETRIC мы можем смело указать это. Однако я обнаружил, что некоторые старые программы игнорируют этот заголовок. Что происходит, если заголовок отсутствует или игнорируется, так это то, что бит 0 интерпретируется как white, а бит 1 интерпретируется как black.

По этой причине я рекомендую заменить строку

TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);

by

TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);

а затем инвертировать пороговое сравнение, заменить строку

if (gray > threshold) bite = bite | 1;

by

if (gray < threshold) bite = bite | 1;

В моем методе я использую арифметику C-указателя вместо индекса для доступа к растровому изображению в памяти.

Наконец, пара улучшений:

а) определить кодировку исходного UIImage (RGBA, ABGR и т. д.) и получить правильные значения RGB для каждого пикселя

б) алгоритм преобразования изображения в градациях серого в двухтональное изображение можно улучшить, используя алгоритм адаптивного порога вместо чисто бинарного условного выражения.

person Juan Catalan    schedule 17.05.2015
comment
Вау, спасибо. Я собираюсь попробовать это завтра. В настоящее время я использую фильтр адаптивного порога GPUImage. Он работает нормально, за исключением того, что сплошные черные области изображения становятся белыми. Я свяжусь с вами на TIFF написать. - person bmauter; 18.05.2015
comment
@bmauter Правда в том, что я делаю преобразование в двухтональный с помощью индивидуального алгоритма адаптивного порога. Я использую OpenCV для управления изображением. Когда у меня есть двухцветное изображение, я использую преобразование tiff перед загрузкой изображения на сервер. Ключевым здесь является преобразование в двухтональный, и без алгоритма адаптивного порога результаты могут быть плохими. Сообщите мне, сработал ли у вас алгоритм TIFF. - person Juan Catalan; 18.05.2015
comment
@bmauter Вы пробовали предложенное мной решение TIFF? - person Juan Catalan; 01.06.2015
comment
Я пробовал. Хотя мы собираемся придерживаться того, что у нас есть, я думаю, что ваш ответ — лучший ответ на этот вопрос. Я приму это. Спасибо! - person bmauter; 01.06.2015
comment
В моем случае я не мог изменить TIFF на PNG, потому что изображение отправляется в некоторые системы OCR, которые принимают только TIFF. Но если у меня возникнут подобные потребности в будущем, я обязательно попробую ваше решение png. - person Juan Catalan; 01.06.2015
comment
Я очень ценю ответ ОП на его собственный вопрос, однако это должен быть выбранный ответ. В одном из моих проектов мне нужно было конвертировать 1-битный TIFF, а не PNG, и этот пост спас мне жизнь! - person yf526; 04.06.2015
comment
Хорошее решение! Но можно ли это сделать без файла, просто вернуть NSData? - person AlexZd; 20.04.2016

В итоге я остановился на GPUImage и libpng. Если кто-то хочет знать, как написать png в iOS за пределами UIPNGRepresentation, вот:

- (void) writeUIImage:(UIImage *)uiImage toPNG:(NSString *)file {
    FILE *fp = fopen([file UTF8String], "wb");
    if (!fp) return [self reportError:[NSString stringWithFormat:@"Unable to open file %@", file]];

    CGImageRef image = [uiImage CGImage];

    CGDataProviderRef provider = CGImageGetDataProvider(image);
    CFDataRef pixelData = CGDataProviderCopyData(provider);
    unsigned char *buffer = (unsigned char *)CFDataGetBytePtr(pixelData);

    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image);
    CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(image);
    size_t compBits = CGImageGetBitsPerComponent(image);
    size_t pixelBits = CGImageGetBitsPerPixel(image);
    size_t width = CGImageGetWidth(image);
    size_t height = CGImageGetHeight(image);
    NSLog(@"bitmapInfo=%d, alphaInfo=%d, pixelBits=%lu, compBits=%lu, width=%lu, height=%lu", bitmapInfo, alphaInfo, pixelBits, compBits, width, height);

    png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png_ptr) [self reportError:@"Unable to create write struct."];

    png_infop info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr) {
        png_destroy_write_struct(&png_ptr, (png_infopp)NULL);
        return [self reportError:@"Unable to create info struct."];
    }

    if (setjmp(png_jmpbuf(png_ptr))) {
        png_destroy_write_struct(&png_ptr, &info_ptr);
        fclose(fp);
        return [self reportError:@"Got error callback."];
    }

    png_init_io(png_ptr, fp);
    png_set_IHDR(png_ptr, info_ptr, (png_uint_32)width, (png_uint_32)height, 1, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
    png_write_info(png_ptr, info_ptr);

    png_set_packing(png_ptr);

    png_bytep line = (png_bytep)png_malloc(png_ptr, width);
    unsigned long pos;
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            pos = y * width * 4 + x * 4; // multiplying by four because each pixel is represented by four bytes
            line[x] = buffer[ pos ]; // just use the first byte (red) since r=g=b in grayscale
        }
        png_write_row(png_ptr, line);
    }

    png_write_end(png_ptr, info_ptr);

    png_destroy_write_struct(&png_ptr, &info_ptr);
    if (pixelData) CFRelease(pixelData);

    fclose(fp);
}

Почему вы хотите это сделать? UIPNGRepresentation — это RGBA с 8 битами на компонент. Это 32 бита на пиксель. Так как я хотел монохромное изображение 1728x2304, мне нужен только 1 бит на пиксель, и в итоге я получаю изображения размером до 40k. Тот же образ с UIPNGRepresentation весит 130k. К счастью, сжатие очень помогает этой 32-битной версии, но изменение битовой глубины на 1 действительно приводит к очень маленьким размерам файлов.

person bmauter    schedule 21.02.2014
comment
Кстати, разные изображения могут использовать разные кодировки байтов. Я имею дело только с изображениями, снятыми камерой устройства, поэтому биты всегда были RGBA (8 бит на канал). Обратите внимание, что я использовал первый байт (красный) и игнорировал остальные три. Если ваше изображение закодировано в ARGB, и вы читаете только первый байт, вы будете получать только значения альфа-канала. Скорее всего, они могут быть только белыми или только черными. Значение alphaInfo говорит вам, какую кодировку ожидать. Подробности смотрите в CGImage.h. - person bmauter; 11.04.2014