From: Tomas Wenström Date: Sun, 22 Nov 2020 18:48:58 +0000 (+0100) Subject: Merge branch 'master' of ssh://dolda2000.com/srv/git/r/kaka/cakelight X-Git-Url: http://dolda2000.com/gitweb/?a=commitdiff_plain;h=8d0b33bf3dbf21aab3f94ac48db8da4fcee5d0cc;hp=0c8d61b6afd2f094eadae092ed9a5c6f6adc0760;p=kaka%2Fcakelight.git Merge branch 'master' of ssh://dolda2000.com/srv/git/r/kaka/cakelight --- diff --git a/build.xml b/build.xml index 67547df..c9dc10e 100644 --- a/build.xml +++ b/build.xml @@ -54,9 +54,9 @@ - - - + + + @@ -77,9 +77,9 @@ - - - + + + diff --git a/config.properties.template b/config.properties.template index 532e6a0..ff75679 100644 --- a/config.properties.template +++ b/config.properties.template @@ -1,11 +1,15 @@ -jdk.home.1.8=/usr/lib/jvm/java-8-openjdk-amd64 +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 diff --git a/src/kaka/cakelight/Color.java b/src/kaka/cakelight/Color.java index 0332371..5ea98cd 100644 --- a/src/kaka/cakelight/Color.java +++ b/src/kaka/cakelight/Color.java @@ -73,12 +73,16 @@ public class Color { s = delta / max; // Hue - if (r == max) { - h = (g - b) / delta; // between yellow & magenta - } else if (g == max) { - h = 2 + (b - r) / delta; // between cyan & yellow + if (delta > 0) { + if (r == max) { + h = (g - b) / delta; // between yellow & magenta + } else if (g == max) { + h = 2 + (b - r) / delta; // between cyan & yellow + } else { + h = 4 + (r - g) / delta; // between magenta & cyan + } } else { - h = 4 + (r - g) / delta; // between magenta & cyan + h = 0; } h /= 6.0; diff --git a/src/kaka/cakelight/Configuration.java b/src/kaka/cakelight/Configuration.java index a24f411..755586f 100644 --- a/src/kaka/cakelight/Configuration.java +++ b/src/kaka/cakelight/Configuration.java @@ -53,7 +53,10 @@ public class Configuration { public int height; public int bpp; public int format; + public boolean mjpg; public double saturation; + public String device; + public boolean deviceIsAutomatic; public CropConfiguration crop; public ListConfiguration list; @@ -61,6 +64,8 @@ public class Configuration { width = Integer.parseInt(get(prop, "video.width", "720")); height = Integer.parseInt(get(prop, "video.height", "576")); bpp = Integer.parseInt(get(prop, "video.bpp", "2")); + device = get(prop, "video.device", "auto"); + deviceIsAutomatic = "auto".equals(device); switch (get(prop, "video.format", "").toUpperCase()) { case "YUYV": format = Imgproc.COLOR_YUV2BGR_YUYV; @@ -68,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; } diff --git a/src/kaka/cakelight/FrameGrabber.java b/src/kaka/cakelight/FrameGrabber.java index e654c91..5354ea6 100644 --- a/src/kaka/cakelight/FrameGrabber.java +++ b/src/kaka/cakelight/FrameGrabber.java @@ -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 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(); diff --git a/src/kaka/cakelight/VideoFrame.java b/src/kaka/cakelight/VideoFrame.java index a3353a5..fd055a9 100644 --- a/src/kaka/cakelight/VideoFrame.java +++ b/src/kaka/cakelight/VideoFrame.java @@ -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. diff --git a/src/kaka/cakelight/mode/VideoMode.java b/src/kaka/cakelight/mode/VideoMode.java index 41b599a..a8397dc 100644 --- a/src/kaka/cakelight/mode/VideoMode.java +++ b/src/kaka/cakelight/mode/VideoMode.java @@ -25,7 +25,12 @@ public class VideoMode extends Mode { @Override public void enter(Configuration config) { this.config = config; - deviceListener.startListening(); + if (config.video.deviceIsAutomatic) { + deviceListener.startListening(); + } else { + File videoDevice = new File(config.video.device); + startGrabberThread(videoDevice); + } } @Override @@ -44,7 +49,9 @@ public class VideoMode extends Mode { @Override public void exit() { grabberThread.interrupt(); - deviceListener.stopListening(); + if (config.video.deviceIsAutomatic) { + deviceListener.stopListening(); + } } private void startGrabberThread(File videoDevice) {