【问题标题】:How do I write a 1bpp tiff with libtiff on iOS?如何在 iOS 上使用 libtiff 编写 1bpp tiff?
【发布时间】:2014-02-20 18:09:56
【问题描述】:

我正在尝试使用 libtiff 将 UIImage 写为 tiff。问题是,即使我将其写入为每像素 1 位,但当我预计文件大小接近 100k 或更少时,文件仍然在 2-5MB 范围内。

这就是我所拥有的。

- (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 代替。有了它,我可以将相同的图像缩小到大约 130k 作为 8 位 PNG。如果我将该 PNG 发送到 PNG 优化器站点,他们可以将其降低到大约 25k。如果有人可以告诉我如何编写从我的 GPUImage 过滤器生成的 1 位 PNG,我将放弃 tiff。

谢谢!

【问题讨论】:

    标签: ios uiimage png gpuimage libtiff


    【解决方案1】:

    我需要在 iPhone 中生成 TIFF 图像并将其发送到需要 TIFF 文件的远程服务器。我无法使用转换为 1bpp PNG 的公认答案,并且我一直在研究使用 libTIFF 转换为 TIFF、1bpp CCITT Group 4 格式的解决方案。

    在调试方法后,我发现了错误所在,我终于得到了正确的解决方案。

    以下代码块是解决方案。阅读代码后可以找到对 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)如果图像的宽度不是8的倍数,则为一条扫描线分配的内存短1个字节。

    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);

    通过

    TIFFWriteEncodedStrip(tiff, y, line, bytesPerStrip);

    3) 最后一个难以检测的错误与关于位为 0 的位表示双色调图像中的白色还是黑色的约定有关。感谢 TIFF 标头TIFFTAG_PHOTOMETRIC,我们可以安全地指出这一点。但是我发现一些较旧的软件会忽略此标头。如果标头不存在或被忽略,会发生什么情况:0 位被解释为white1 位被解释为black

    因此我建议更换该行

    TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);

    TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);

    然后倒置阈值比较,换行

    if (gray &gt; threshold) bite = bite | 1;

    if (gray &lt; threshold) bite = bite | 1;

    在我的方法中,我使用 C 指针算法而不是索引来访问内存中的位图。

    最后,一些改进:

    a) 检测原始 UIImage 的编码(RGBA、ABGR 等)并为每个像素获取正确的 RGB 值

    b) 可以通过使用自适应阈值算法而不是纯二进制条件来改进从灰度图像转换为双色调图像的算法。

    【讨论】:

    • 哇,谢谢。我明天要试试这个。我目前正在使用 GPUImage 的自适应阈值过滤器。它工作正常,只是图像的纯黑色区域变为白色。我会在 TIFF 写作时回复你。
    • @bmauter 事实上,我使用自定义的自适应阈值算法将其转换为双色调。我使用 OpenCV 来操作图像。获得双色调图像后,我会在将图像上传到服务器之前使用 tiff 转换。这里的关键是转换为双色调,如果没有自适应阈值算法,结果可能很差。让我知道 TIFF 算法是否适合您。
    • @bmauter 你试过我提出的 TIFF 解决方案了吗?
    • 我试过了。虽然我们将坚持我们所拥有的,但我认为您的回答是这个问题的最佳答案。我会接受的。谢谢!
    • 就我而言,我无法将 TIFF 更改为 PNG,因为图像被发送到某些仅接受 TIFF 的 OCR 系统。但如果我将来有类似的需求,我肯定会尝试你的 png 解决方案。
    【解决方案2】:

    我最终选择了 GPUImage 和 libpng。如果有人想知道如何在 UIPNGRepresentation 之外的 iOS 中编写 png,请参考:

    - (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 确实可以将其缩小到非常小的文件大小。

    【讨论】:

    • 顺便说一下,不同的图像可以使用不同的字节编码。我只处理设备相机拍摄的图像,所以位总是 RGBA(每通道 8 位)。请注意,我使用的是第一个字节(红色)而忽略了其他三个。如果您的图像以 ARGB 编码,并且您只读取第一个字节,那么您只会获得 alpha 通道值。它们可能只有白色或只有黑色。 alphaInfo 值告诉您预期的编码。详情请查看 CGImage.h。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多