]> www.dolda2000.com Git - kaka/cakelight.git/commitdiff
Add very special handling for the MJPG video format
authorTomas Wenström <tomas.wenstrom@gmail.com>
Tue, 14 Jul 2020 15:33:33 +0000 (17:33 +0200)
committerTomas Wenström <tomas.wenstrom@gmail.com>
Tue, 14 Jul 2020 15:33:33 +0000 (17:33 +0200)
config.properties.template
src/kaka/cakelight/Configuration.java
src/kaka/cakelight/FrameGrabber.java
src/kaka/cakelight/VideoFrame.java

index 317ead11631f774d876112544d0a8484ecbc5d36..ff75679c0e39a20769f0b76070e471a1ccf02b26 100644 (file)
@@ -1,11 +1,15 @@
 jdk.home=/usr/lib/jvm/java-8-openjdk-amd64
 
 # Video info can be found with 'v4l2-ctl --get-fmt-video'
-# Supported formats: UYVY, YUYV, YVYU
+# Supported formats: UYVY, YUYV, YVYU, MJPG
+# When using MJPG, stream data via a named pipe and point video.device at the pipe:
+#   mkfifo cakelight-video-stream
+#   v4l2-ctl --stream-mmap --stream-to cakelight-video-stream
 video.format=UYVY
 video.width=720
 video.height=480
 video.bpp=2
+video.device=auto
 
 video.crop.left=27
 video.crop.right=29
index 99756580d2b345a9bd80428936a5425ff138db52..755586fd986905024a5ab5a11bce50743b6ec634 100644 (file)
@@ -53,6 +53,7 @@ public class Configuration {
         public int height;
         public int bpp;
         public int format;
+        public boolean mjpg;
         public double saturation;
         public String device;
         public boolean deviceIsAutomatic;
@@ -72,6 +73,9 @@ public class Configuration {
                 case "YVYU":
                     format = Imgproc.COLOR_YUV2BGR_YVYU;
                     break;
+                case "MJPG":
+                    format = 0;
+                    mjpg = true;
                 default:
                     format = Imgproc.COLOR_YUV2BGR_UYVY;
             }
index e654c9183075f51198340ea0184cf79210915c28..5354ea69a8ab1a13dbb5bbf132fc491beee55b4b 100644 (file)
@@ -1,5 +1,7 @@
 package kaka.cakelight;
 
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
 import java.io.*;
 import java.util.Optional;
 
@@ -10,6 +12,7 @@ public class FrameGrabber implements Closeable {
     private File file;
     private int bytesPerFrame;
     private InputStream fileStream;
+    private final ByteArrayOutputStream bufferedBytes = new ByteArrayOutputStream();
 
     private FrameGrabber() {
     }
@@ -39,11 +42,26 @@ public class FrameGrabber implements Closeable {
      */
     public Optional<VideoFrame> grabFrame() {
         try {
-            byte[] data = new byte[bytesPerFrame];
-            int count = fileStream.read(data);
-            if (count != bytesPerFrame) {
-                log("Expected to read " + bytesPerFrame + " bytes per frame but read " + count);
+            byte[] data;
+            if (config.video.mjpg) {
+                byte[] jpgData = readStreamingJpgData();
+                if (jpgData == null) {
+                    return Optional.empty();
+                }
+                saveTemporaryJpgFile(jpgData);
+                byte[] bmpData = convertJpgFileToByteArray();
+                if (bmpData == null) {
+                    return Optional.empty();
+                }
+                data = bmpData;
+            } else {
+                data = new byte[bytesPerFrame];
+                int count = fileStream.read(data);
+                if (count != bytesPerFrame) {
+                    log("Expected to read " + bytesPerFrame + " bytes per frame but read " + count);
+                }
             }
+
             return Optional.of(VideoFrame.of(data, config));
         } catch (IOException e) {
             e.printStackTrace();
@@ -52,6 +70,62 @@ public class FrameGrabber implements Closeable {
         return Optional.empty();
     }
 
+    private byte[] readStreamingJpgData() throws IOException {
+        byte[] data;
+        byte[] batch = new byte[1024];
+        boolean lastByteIsXX = false;
+        loop:
+        while (true) {
+            int batchCount = fileStream.read(batch);
+            if (batchCount == -1) {
+                return null;
+            }
+            if (lastByteIsXX) {
+                if (batch[0] == (byte) 0xd8) {
+                    data = bufferedBytes.toByteArray();
+                    bufferedBytes.reset();
+                    bufferedBytes.write(0xff);
+                    bufferedBytes.write(batch, 0, batchCount);
+                    break;
+                }
+                bufferedBytes.write(0xff);
+            }
+            for (int i = 0; i < batchCount - 1; i++) {
+                if (batch[i] == (byte) 0xff && batch[i + 1] == (byte) 0xd8) { // start of jpeg
+                    if (i > 0) {
+                        bufferedBytes.write(batch, 0, i);
+                    }
+                    data = bufferedBytes.toByteArray();
+                    bufferedBytes.reset();
+                    bufferedBytes.write(batch, i, batchCount - i);
+                    break loop;
+                }
+            }
+            lastByteIsXX = batch[batchCount - 1] == (byte) 0xff;
+            bufferedBytes.write(batch, 0, batchCount - (lastByteIsXX ? 1 : 0));
+        }
+        return data;
+    }
+
+    private void saveTemporaryJpgFile(byte[] data) throws IOException {
+        try (FileOutputStream fos = new FileOutputStream("/tmp/cakelight-video-stream.jpg")) {
+            fos.write(data);
+        }
+    }
+
+    private byte[] convertJpgFileToByteArray() throws IOException {
+        BufferedImage image = ImageIO.read(new File("/tmp/cakelight-video-stream.jpg"));
+        if (image != null) { // will almost always be null the first time
+            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+                ImageIO.write(image, "bmp", baos);
+                baos.flush();
+                return baos.toByteArray();
+            }
+        } else {
+            return null;
+        }
+    }
+
     @Override
     public void close() throws IOException {
         fileStream.close();
index a3353a5b9ce68c8768a7d8e74b621c8df90134ba..fd055a99c5278f31648e46f1e717aff04c3e09a7 100644 (file)
@@ -1,5 +1,6 @@
 package kaka.cakelight;
 
+import org.opencv.core.Core;
 import org.opencv.core.CvType;
 import org.opencv.core.Mat;
 import org.opencv.core.Size;
@@ -23,10 +24,37 @@ public class VideoFrame {
     public static VideoFrame of(byte[] data, Configuration config) {
         VideoFrame frame = new VideoFrame(data);
         frame.config = config;
-        frame.convert();
+        if (config.video.mjpg) {
+            frame.convertJpg();
+        } else {
+            frame.convert();
+        }
         return frame;
     }
 
+    private void convertJpg() {
+        Mat src = new Mat(config.video.height, config.video.width, CvType.CV_8UC3); // 8-bit, unsigned, 3 channels
+        src.put(0, 0, data);
+//        save(src, "/home/kaka/test-src.data");
+
+        converted = new Mat();
+        Imgproc.cvtColor(src, converted, Imgproc.COLOR_BGR2RGB);
+
+        Core.flip(converted, converted, 0); // up-side down
+//        save(converted, "/home/kaka/test-converted.data");
+        int mysteriousPixelShift = 18;
+        converted = converted.submat( // crop mysterious pixel shift
+                0,
+                converted.rows(),
+                mysteriousPixelShift,
+                converted.cols() - mysteriousPixelShift
+        );
+//        save(converted, "/home/kaka/test-croppedAgain.data");
+        model4(converted, Imgproc.INTER_AREA);
+        src.release();
+        converted.release();
+    }
+
     private void convert() {
         /* TODO: how to do this?
         1) Resize to an image with the size of the number of leds and use config to define how many pixels deep into the screen to use.