Ask Your Question

Revision history [back]

OpenCV crash on corrupted tiff files

I have developed a REST service to convert tiff image to PNG format. The REST service reads the tiff file from local machine and calls the required OpenCV methods to perform the conversion and resize to the requested height and width. The high level steps are 1)Decompress tiff source as a gray scale image because most of tiffs are CCIT1.4 images 2)Scale to requested width and height 3)Encode the image into a PNG

The problem is when there are multiple REST requests coming in, the output PNG is getting corrupted. On reissuing the same request the output PNG appears fine. I believe the problem is multiple Opencv processes triggered via OpenCV JNI is causing this. Before thinking this could be a bug, is there anything basic that should be added or setup to make Opencv work in a multi-threaded environment.

Library version: 3.4.1 OS: RHEL 7.4

The code used is given below:

public class TiffToPNGConverter {

    private static final String PNG_EXTENSION = ".png";
    private static final String ENCODE_CONVERSION_ERROR_MSG = "Opencv failed to encode image";
    private static final String CREATE_IMAGE_ERROR_MSG = "Opencv failed to create scaled image";
    private static final String DECODE_CONVERSION_ERROR_MSG = "Opencv in-memory image decode failed";

    private static final int IMAGE_ROW = 1;
    private static final int DEFAULT_IMAGE_ROW = 0;
    private static final int DEFAULT_IMAGE_COLUMN = 0;
    private static final int DEFAULT_SCALE_FACTOR_XAXIS = 0;
    private static final int DEFAULT_SCALE_FACTOR_YAXIS = 0;
    /*
     * For gray scale text images, not seeing any difference in image size over compression values 1-9 inclusive.
     */
    private static final int[] PNG_COMPRESSIONS_PARAMS = { CV_IMWRITE_PNG_COMPRESSION, 1, 0 };

    static {
        try {
            System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        } catch (final UnsatisfiedLinkError e) {
            log.error("Unable to load Native Opencv library {}", Core.NATIVE_LIBRARY_NAME, e);
        }
    }

    /**
     * Just this method to utilize opencv java library and perform the required conversion
     *
     * @param sourceTiffData
     *            source tiff image as raw bytes
     * @param pngWidth
     *            requested conversion width
     * @return scaled png format
     */
    public byte[] run(byte[] sourceTiffData, int pngWidth) {
        log.debug("Requested png width {}", pngWidth);

        // step 1: Decompress tiff source as a gray scale image because most of tiffs are CCIT1.4 images
        Mat decompressedSource = decompressTiff(sourceTiffData);
        log.debug("decompression complete");

        // step 2: Scale to requested png width and height
        Mat scaledImage = scaleImage(decompressedSource, pngWidth);
        log.debug("Image scaling complete");

        // step 3: Encode the image into a PNG
        return encodeImageToPNG(scaledImage);
    }

    /**
     * Decompress tiff
     *
     * @param sourceTiffData
     *            source tiff image as raw bytes
     * @return decompressed tiff as opencv image object
     */
    private Mat decompressTiff(byte[] sourceTiffData) {
        // There is a constructor to add the byte[] directly but that is failing, probably because it requires row and
        // column information which is not available by default
        Mat decompressedSourceHolder = new Mat(IMAGE_ROW, sourceTiffData.length, CV_8UC1);
        decompressedSourceHolder.put(DEFAULT_IMAGE_ROW, DEFAULT_IMAGE_COLUMN, sourceTiffData);

        Mat decompressedSource = Imgcodecs.imdecode(decompressedSourceHolder, CV_LOAD_IMAGE_GRAYSCALE);
        decompressedSourceHolder.release();
        decompressedSourceHolder = null;

        if (decompressedSource == null) {
            throw new TiffToPNGConversionException(DECODE_CONVERSION_ERROR_MSG);
        }
        return decompressedSource;
    }

    /**
     * Scale image to width from request
     *
     * @param decompressedSource
     *            decompressed tiff image
     * @param pngWidth
     *            scale to image size requested
     * @return scaled image
     */
    @SuppressWarnings("squid:S2583")
    // Opencv could potentially return a nul Mat object if type of file has some issues
    private Mat scaleImage(Mat decompressedSource, int pngWidth) {
        // Compute height without skewing the aspect ratio between source and destination image
        if (decompressedSource.width() != pngWidth) {
            int pngHeight = (pngWidth * decompressedSource.height()) / decompressedSource.width();
            Size expectedPngSize = new Size(pngWidth, pngHeight);
            Mat scaledImage = new Mat(expectedPngSize, decompressedSource.type());

            if (scaledImage == null) {
                decompressedSource.release();
                decompressedSource = null;
                throw new TiffToPNGConversionException(CREATE_IMAGE_ERROR_MSG);
            }

            // Generally, it is best to enlarge images using over a 4x4 or 8x8 pixel neighbor, but with text images,
            // it doesn't seem to add much to beyond simple biliner interpolation. This may change for drawings or images
            // but that will have to be better tested.
            int interpolation = decompressedSource.width() > pngWidth ? INTER_AREA : INTER_LINEAR;
            // actual image resize happens here
            Imgproc.resize(decompressedSource, scaledImage, scaledImage.size(), DEFAULT_SCALE_FACTOR_XAXIS,
                    DEFAULT_SCALE_FACTOR_YAXIS, interpolation);
            decompressedSource.release();
            decompressedSource = null;
            return scaledImage;
        }
        return decompressedSource;
    }

    /**
     * Convert scaled image to PNG
     *
     * @param scaledImage
     *            scaled image
     * @return sclaed PNG
     */
    private byte[] encodeImageToPNG(Mat scaledImage) {
        // compress png
        MatOfByte pngCompressedOutput = new MatOfByte();
        boolean compressionResult = Imgcodecs.imencode(PNG_EXTENSION, scaledImage, pngCompressedOutput,
                new MatOfInt(PNG_COMPRESSIONS_PARAMS));
        if (!compressionResult) {
            scaledImage.release();
            scaledImage = null;
            throw new TiffToPNGConversionException(ENCODE_CONVERSION_ERROR_MSG);
        }
        log.debug("png conversion complete");
        scaledImage.release();
        scaledImage = null;
        return pngCompressedOutput.toArray();
    }
}