3

I'm trying to make gif animation from jpegs I get from video camera. But this process is unreal long. I used two different libraries. First is written with native C++ code and second is Java's one.

I compress frames as I can, but even this cannot reduce generating time.

Native library takes about 80-100 seconds and Java's takes about 40-60 seconds (I don't know how java is 2 times faster, but logs show me this result) for 5 seconds video with 16 fps (80 frames per gif).

I changed a bit C++ algorithm according to this, because I got same problem (tried both versions with changing a piece of code and changing whole learn() function).

Here you can see piece of logs:

It's last three frames in native implementation:

D/TimeUtils: Adding frame executed in 949ms
D/TimeUtils: Adding frame executed in 976ms
D/TimeUtils: Adding frame executed in 1028ms
D/TimeUtils: Creating gif with native library executed in 82553ms

It's last three frames in Java's version:

D/TimeUtils: Adding frame executed in 541ms
D/TimeUtils: Adding frame executed in 513ms
D/TimeUtils: Adding frame executed in 521ms
D/TimeUtils: Creating gif with nbadal's library executed in 44811ms

Maybe some other useful logs:

D/CameraActivity: Duration of the captured video is 5000ms
V/CameraActivity: Dimensions are 288w x 288h
D/CameraActivity: Final bitmaps count: 80

TimeUtils.java contains static methods to check how long method executes.

NativeGifConverter.java (only converting function):

@Override public void createGifFile(String path, List<String> bitmapPaths) {

    Bitmap bitmap = BitmapUtils.retrieve(bitmapPaths.get(0));

    if (init(path, bitmap.getWidth(), bitmap.getHeight(), mNumColors, mQuality, mFrameDelay) != 0) {
      Timber.e("Gifflen init failed");
      return;
    }

    bitmap.recycle();

    for (String bitmapPath : bitmapPaths) {

      bitmap = howLong("Retrieving bitmap", () -> BitmapUtils.retrieve(bitmapPath));

      final int width = bitmap.getWidth();
      final int height = bitmap.getHeight();
      final int[] pixels = new int[width * height];
      final Bitmap finalBitmap = bitmap; // for counting time
      howLongVoid("Retrieving pixels", () -> finalBitmap.getPixels(pixels, 0, width, 0, 0, width, height));
      howLongVoid("Adding frame", () -> addFrame(pixels));

      bitmap.recycle();
    }
    bitmap = null;
    close();
  }

NbadalGifConverter.java (only converting function):

  @Override public void createGifFile(String path, List<String> bitmapsNames) {

    final ByteArrayOutputStream bos = new ByteArrayOutputStream();

    final AnimatedGifEncoder encoder = new AnimatedGifEncoder();
    encoder.setDelay(mDelay);
    encoder.setQuality(mQuality);
    encoder.start(bos);

    for (String bitmapName : bitmapsNames) {
      final Bitmap bitmap = howLong("Retrieving bitmap", () -> BitmapUtils.retrieve(bitmapName));
      howLongVoid("Adding frame", () -> encoder.addFrame(bitmap));
    }

    encoder.finish();
    FileUtils.store(bos.toByteArray(), path.substring(0, path.lastIndexOf('.')) + ".gif");
  }

I'm open to show you another related pieces of code. I would greatly appreciate any help.

[UPDATE]

Logs of the retrieving bitmaps:

D/TimeUtils: Retrieving bitmap executed in 3ms
D/TimeUtils: Retrieving bitmap executed in 3ms
D/TimeUtils: Retrieving bitmap executed in 4ms
Community
  • 1
  • 1
Anton Shkurenko
  • 4,301
  • 5
  • 29
  • 64
  • sorry not a JAVA coder as you see in my answers I am C++ oriented. Anyway: how big are the images you are converting. The longest operation is not the LZW compresion itself but the color quantization from true-color JPG to up to 256 colors in GIF (or more with some advanced things). To speedup you can try: 1. convert JPG to 256 color image (BMP for example) prior to conversion with some fast method and use that as GIF encoding input that might speed up considerably. If you have option to use global GIF palette only you can try to create some covering the base colors you need ... – Spektre Nov 27 '15 at 08:50
  • that will be even faster but can distort the quality of image. This is best suited for dithering capable encoders. As I have no experience with libs you are using can not help more ... The last option of speding up is to limit dictionary to less then 4096 entries but have no clue if doable in your libs. the bigger the dictionary the longer the compresion ... – Spektre Nov 27 '15 at 08:52
  • @Spektre they are pretty little, I compressed pics to little dimensions, logs show 288x288, and I use RGB_565. I will follow your advices, thanks! – Anton Shkurenko Nov 27 '15 at 08:55
  • @Spektre here is C++ algorithm for calling as native from java: http://jiggawatt.org/badc0de/android/#gifflen – Anton Shkurenko Nov 27 '15 at 08:55
  • on a standard PC I encode those almost in real-time (but I use multi threading) so either you got too small horsepower or the implementations are not optimized. for example see this [GIF on the bottom of answer](http://stackoverflow.com/a/30750626/2521214) it is captured in RT while drawing by hand/mouse in my SVG editor ... so you have something to compare to – Spektre Nov 27 '15 at 09:02
  • Encoding is on the phone, so it can be truth. I would use multithreading, but I don't know a lot about gif format, I thought, I have to add frames sequentially – Anton Shkurenko Nov 27 '15 at 09:14
  • yes ... there are 2 ways: 1. each CPU encodes to its own file and at the end are those combined. 2. each CPU encodes its frame and waits for its order to store it to the single file... The second is a bit slower but do not need to combine N-files to single at the end ... (I am using option #2) this has any relevance only if you got more CPU/Cores than just 1 – Spektre Nov 27 '15 at 09:18
  • How to concat gifs? I didn't find any answer. Anyway I see, that I will use 2nd option, phones haven't that power. – Anton Shkurenko Nov 27 '15 at 09:33
  • you just copy the encoded frames ... there is no checksum or order info in GIF you just copy them ... for that you need where frame starts and ends and exactly thats about this [Interlace GIF](http://stackoverflow.com/a/33604815/2521214) question. you just handle GIF as file and copy BYTES ... to target file. The speed bust between #1 and #2 is just few percent for similar images like in videoclip ... so if this is too much you can still use #2 like me. But do not know if your lib is thread safe ... – Spektre Nov 27 '15 at 09:38
  • Hm, great thanks! How can I ever thank you? – Anton Shkurenko Nov 27 '15 at 10:00
  • 2
    by helping others ... preserving knowledge is important these days – Spektre Nov 27 '15 at 10:48
  • @Spektre huh, finally I posted the answer, thank you! – Anton Shkurenko Dec 16 '15 at 16:23

1 Answers1

4

Firstly I have to thank to the @Spektre for this answer: Effective gif/image color quantization?

My colleague and I just translated it from the C++ to the Java. It shows good results in 4x less time. I'll try to improve it, but this is already much better result, than AnimatedGifEncoder.java (I used before)

Here is the code:

public static final int MAX_COLOR_COUNT = 65536;

/**
 * @param pixels rgb 888
 * @param palette int[256]
 * @return indices of colors in palette
 */
private int[][][] createPalette(int[] pixels, int[] palette) {

  final int[] histogram = new int[MAX_COLOR_COUNT]; // pixel count histogram
  final int[] indices = new int[MAX_COLOR_COUNT]; // here index is color value

  for (int i = 0; i < MAX_COLOR_COUNT; i++) {
    indices[i] = i;    
  }

  // creating histogram
  for (int color : pixels) {
    //                   0001 1111             0111 1110 0000         1111 1000 0000 0000
    color = ((color >> 3) & 0x1F) | ((color >> 5) & 0x7E0) | ((color >> 8) & 0xF800);
    if (histogram[color] < Integer.MAX_VALUE) { // picture must be really big
      histogram[color]++;
    }
  }

  // removing zeros
  int j = 0;
  for (int i = 0; i < MAX_COLOR_COUNT; i++) {
    histogram[j] = histogram[i];
    indices[j] = indices[i];
    if (histogram[j] != 0) {
      j++;
    }
  }
  final int histograms = j;

  // bubble sort
  for (int i = 1; i != 0; ) {
    i = 0;
    for (int x = 0, y = 1; y < histograms; x++, y++) {
      if (histogram[x] < histogram[y]) {
        i = histogram[x];
        histogram[x] = histogram[y];
        histogram[y] = i;
        i = indices[x];
        indices[x] = indices[y];
        indices[y] = i;
        i = 1;
      }
    }
  }

  final int[][][] colorMap = new int[32][64][32];

  int colorTableIndex = 0, x = 0;
  for (; x < histograms; x++) { // main colors
    final int color = indices[x];
    // 1f (16) = 0001 1111 (2)
    // 3f (16) = 0011 1111 (2)
    // (1111 1)(111 111)(1 1111)
    final int b = color & 0x1f;
    final int g = (color >> 5) & 0x3f;
    final int r = (color >> 11) & 0x1f;

    // skip if similar color already in palette[]
    int a = 0, i = 0;
    for (; i < colorTableIndex; i++) {
      final byte tempB = (byte) ((palette[i] >> 3) & 0x1f);
      final byte tempG = (byte) ((palette[i] >> 10) & 0x3f);
      final byte tempR = (byte) ((palette[i] >> 19) & 0x1f);

      // if difference between two colors is pretty small
      // taxicab distance
      int difference = tempB - b;
      if (difference < 0) {
        difference = -difference;
      }
      a = difference;
      difference = tempG - g;
      if (difference < 0) {
        difference = -difference;
      }
      a += difference;
      difference = tempR - r;
      if (difference < 0) {
        difference = -difference;
      }
      a += difference;
      if (a <= 2) { // smaller than 16/8
        a = 1;
        break;
      }
      a = 0;
    }

    if (a != 0) {
      colorMap[r][g][b] = i; // map to existing color
    } else {
      colorMap[r][g][b] = colorTableIndex; // map to new index

      // 1111 1000 1111 1100 1111 1000
      palette[colorTableIndex] = b << 3 | (g << 10) | (r << 19); // fill this index with new color
      colorTableIndex++;
      if (colorTableIndex >= 256/*palette.length*/) {
        x++;
        break;
      }
    }
  }   // colorTableIndex = new color table size

  for (; x < histograms; x++) { // minor colors

    final int color = indices[x];

    final int b = color & 0x1f;
    final int g = (color >> 5) & 0x3f;
    final int r = (color >> 11) & 0x1f;

    // find closest color
    int minDistance = -1;
    int colorIndex = 0;
    for (int a, i = 0; i < colorTableIndex; i++) {
      final byte tempB = (byte) ((palette[i] >> 3) & 0x1f);
      final byte tempG = (byte) ((palette[i] >> 10) & 0x3f);
      final byte tempR = (byte) ((palette[i] >> 19) & 0x1f);

      int difference = tempB - b;
      if (difference < 0) {
        difference = -difference;
      }
      a = difference;
      difference = tempG - g;
      if (difference < 0) {
        difference = -difference;
      }
      a += difference;
      difference = tempR - r;
      if (difference < 0) {
        difference = -difference;
      }
      a += difference;
      if ((minDistance < 0) || (minDistance > a)) {
        minDistance = a;
        colorIndex = i;
      }
    }
    colorMap[r][g][b] = colorIndex;
  }

  return colorMap;
}

private byte[] map(int[] pixels, int[][][] colorMap) {
  final int pixelsLength = pixels.length;

  final byte[] mapped = new byte[pixelsLength];
  for (int i = 0; i < pixelsLength; i++) {
    final int color =
        ((pixels[i] >> 3) & 0x1F) | ((pixels[i] >> 5) & 0x7E0) | ((pixels[i] >> 8) & 0xF800);

    final int b = color & 0x1f;
    final int g = (color >> 5) & 0x3f;
    final int r = (color >> 11) & 0x1f;

    mapped[i] = (byte) colorMap[r][g][b];
  }
  return mapped;
}
Community
  • 1
  • 1
Anton Shkurenko
  • 4,301
  • 5
  • 29
  • 64
  • Hi Anton, same concern here :) How had you placed the code in AnimatedGifEncoder ?? – Paresh P. Jul 14 '16 at 11:07
  • 3
    Here is full source of my code there: https://gist.github.com/tonyshkurenko/80dfafe9604c12661f57, just remove @DebugLog from Hugo, some logs from Timber, my TimeUtils and also it doesn't contain some attributes, that AnimatedGifEncoder does, but if you will compare sources, they are easy to be added :) – Anton Shkurenko Jul 14 '16 at 11:10
  • Hi, @AntonShkurenko I do not know how to thank you, colleague! I`ve upvoted most of your answers on StackOverflow! Thanks! – Zikkoua Apr 08 '17 at 18:58
  • @Zikkoua huh, thanks :) But StackOverflow has defence algorithm, it took away almost all :) Glad, that I helped you! – Anton Shkurenko Apr 09 '17 at 09:57