Hi all,
I'm using the OctoWS2811 library to send video to an LED panel (image here). I have my settings set as instructed in both the Arduino and Processing code. I have three issues, however:
1. The image is squished horizontally.
2. The image is not centered and some of it does not appear at all (see image).
3. I'm only getting 3 rows of LEDs for each pin instead of 4.
My LED matrix is made up of NeoPixels wired in a zigzag pattern (alternating direction of LEDs) starting in the top-left and ending at the bottom-left. There are 24 rows of 25 LEDs. Put another way, there are 25 LEDs horizontally per row, and 24 rows vertically.
My video file is 480 x 480.
Any suggestions of how to approach this? Code below:
ARDUINO CODE:
PROCESSING CODE:
I'm using the OctoWS2811 library to send video to an LED panel (image here). I have my settings set as instructed in both the Arduino and Processing code. I have three issues, however:
1. The image is squished horizontally.
2. The image is not centered and some of it does not appear at all (see image).
3. I'm only getting 3 rows of LEDs for each pin instead of 4.
My LED matrix is made up of NeoPixels wired in a zigzag pattern (alternating direction of LEDs) starting in the top-left and ending at the bottom-left. There are 24 rows of 25 LEDs. Put another way, there are 25 LEDs horizontally per row, and 24 rows vertically.
My video file is 480 x 480.
Any suggestions of how to approach this? Code below:
ARDUINO CODE:
Code:
/* OctoWS2811 VideoDisplay.ino - Video on LEDs, from a PC, Mac, Raspberry Pi
http://www.pjrc.com/teensy/td_libs_OctoWS2811.html
Copyright (c) 2013 Paul Stoffregen, PJRC.COM, LLC
*/
#include <OctoWS2811.h>
#define LED_WIDTH 25 // number of LEDs horizontally
#define LED_HEIGHT 32 // number of LEDs vertically (must be multiple of 8)
#define LED_LAYOUT 0 // 0 = even rows left->right, 1 = even rows right->left
#define VIDEO_XOFFSET 0
#define VIDEO_YOFFSET 0 // display entire image
#define VIDEO_WIDTH 100
#define VIDEO_HEIGHT 100
const int ledsPerStrip = LED_WIDTH * LED_HEIGHT / 8;
DMAMEM int displayMemory[ledsPerStrip*6];
int drawingMemory[ledsPerStrip*6];
elapsedMicros elapsedUsecSinceLastFrameSync = 0;
const int config = WS2811_800kHz; // color config is on the PC side
OctoWS2811 leds(ledsPerStrip, displayMemory, drawingMemory, config);
void setup() {
pinMode(12, INPUT_PULLUP); // Frame Sync
Serial.setTimeout(50);
leds.begin();
leds.show();
}
void loop() {
int startChar = Serial.read();
if (startChar == '*') {
// receive a "master" frame - we send the frame sync to other boards
// the sender is controlling the video pace. The 16 bit number is
// how far into this frame to send the sync to other boards.
unsigned int startAt = micros();
unsigned int usecUntilFrameSync = 0;
int count = Serial.readBytes((char *)&usecUntilFrameSync, 2);
if (count != 2) return;
count = Serial.readBytes((char *)drawingMemory, sizeof(drawingMemory));
if (count == sizeof(drawingMemory)) {
unsigned int endAt = micros();
unsigned int usToWaitBeforeSyncOutput = 100;
if (endAt - startAt < usecUntilFrameSync) {
usToWaitBeforeSyncOutput = usecUntilFrameSync - (endAt - startAt);
}
digitalWrite(12, HIGH);
pinMode(12, OUTPUT);
delayMicroseconds(usToWaitBeforeSyncOutput);
digitalWrite(12, LOW);
// WS2811 update begins immediately after falling edge of frame sync
digitalWrite(13, HIGH);
leds.show();
digitalWrite(13, LOW);
}
} else if (startChar == '$') {
// receive a "master" frame - we send the frame sync to other boards
// we are controlling the video pace. The 16 bit number is how long
// after the prior frame sync to wait until showing this frame
unsigned int usecUntilFrameSync = 0;
int count = Serial.readBytes((char *)&usecUntilFrameSync, 2);
if (count != 2) return;
count = Serial.readBytes((char *)drawingMemory, sizeof(drawingMemory));
if (count == sizeof(drawingMemory)) {
digitalWrite(12, HIGH);
pinMode(12, OUTPUT);
while (elapsedUsecSinceLastFrameSync < usecUntilFrameSync) /* wait */ ;
elapsedUsecSinceLastFrameSync -= usecUntilFrameSync;
digitalWrite(12, LOW);
// WS2811 update begins immediately after falling edge of frame sync
digitalWrite(13, HIGH);
leds.show();
digitalWrite(13, LOW);
}
} else if (startChar == '%') {
// receive a "slave" frame - wait to show it until the frame sync arrives
pinMode(12, INPUT_PULLUP);
unsigned int unusedField = 0;
int count = Serial.readBytes((char *)&unusedField, 2);
if (count != 2) return;
count = Serial.readBytes((char *)drawingMemory, sizeof(drawingMemory));
if (count == sizeof(drawingMemory)) {
elapsedMillis wait = 0;
while (digitalRead(12) != HIGH && wait < 30) ; // wait for sync high
while (digitalRead(12) != LOW && wait < 30) ; // wait for sync high->low
// WS2811 update begins immediately after falling edge of frame sync
if (wait < 30) {
digitalWrite(13, HIGH);
leds.show();
digitalWrite(13, LOW);
}
}
} else if (startChar == '@') {
// reset the elapsed frame time, for startup of '$' message playing
elapsedUsecSinceLastFrameSync = 0;
} else if (startChar == '?') {
// when the video application asks, give it all our info
// for easy and automatic configuration
Serial.print(LED_WIDTH);
Serial.write(',');
Serial.print(LED_HEIGHT);
Serial.write(',');
Serial.print(LED_LAYOUT);
Serial.write(',');
Serial.print(0);
Serial.write(',');
Serial.print(0);
Serial.write(',');
Serial.print(VIDEO_XOFFSET);
Serial.write(',');
Serial.print(VIDEO_YOFFSET);
Serial.write(',');
Serial.print(VIDEO_WIDTH);
Serial.write(',');
Serial.print(VIDEO_HEIGHT);
Serial.write(',');
Serial.print(0);
Serial.write(',');
Serial.print(0);
Serial.write(',');
Serial.print(0);
Serial.println();
} else if (startChar >= 0) {
// discard unknown characters
}
}
PROCESSING CODE:
Code:
/* OctoWS2811 movie2serial.pde - Transmit video data to 1 or more
Teensy 3.0 boards running OctoWS2811 VideoDisplay.ino
http://www.pjrc.com/teensy/td_libs_OctoWS2811.html
Copyright (c) 2013 Paul Stoffregen, PJRC.COM, LLC
*/
import processing.video.*;
import processing.serial.*;
import java.awt.Rectangle;
Movie myMovie;
float gamma = 1.7;
int numPorts=0; // the number of serial ports in use
int maxPorts=24; // maximum number of serial ports
Serial[] ledSerial = new Serial[maxPorts]; // each port's actual Serial port
Rectangle[] ledArea = new Rectangle[maxPorts]; // the area of the movie each port gets, in % (0-100)
boolean[] ledLayout = new boolean[maxPorts]; // layout of rows, true = even is left->right
PImage[] ledImage = new PImage[maxPorts]; // image sent to each port
int[] gammatable = new int[256];
int errorCount=0;
float framerate=0;
void setup() {
String[] list = Serial.list();
delay(20);
println("Serial Ports List:");
println(list);
serialConfigure("/dev/tty.usbmodem509331"); // change these to your port names
if (errorCount > 0) exit();
for (int i=0; i < 256; i++) {
gammatable[i] = (int)(pow((float)i / 255.0, gamma) * 255.0 + 0.5);
}
myMovie = new Movie(this, "moon_4x_480.mov");
myMovie.loop(); // start the movie :-)
size(myMovie.width, myMovie.height + 100); // create the w indow
}
// movieEvent runs for each new frame of movie data
void movieEvent(Movie m) {
// read the movie's next frame
m.read();
//if (framerate == 0) framerate = m.getSourceFrameRate();
framerate = 30.0; // TODO, how to read the frame rate???
for (int i=0; i < numPorts; i++) {
// copy a portion of the movie's image to the LED image
int xoffset = percentage(m.width, ledArea[i].x);
int yoffset = percentage(m.height, ledArea[i].y);
int xwidth = percentage(m.width, ledArea[i].width);
int yheight = percentage(m.height, ledArea[i].height);
ledImage[i].copy(m, xoffset, yoffset, xwidth, yheight,
0, 0, ledImage[i].width, ledImage[i].height);
// convert the LED image to raw data
byte[] ledData = new byte[(ledImage[i].width * ledImage[i].height * 3) + 3];
image2data(ledImage[i], ledData, ledLayout[i]);
if (i == 0) {
ledData[0] = '*'; // first Teensy is the frame sync master
int usec = (int)((1000000.0 / framerate) * 0.75);
ledData[1] = (byte)(usec); // request the frame sync pulse
ledData[2] = (byte)(usec >> 8); // at 75% of the frame time
} else {
ledData[0] = '%'; // others sync to the master board
ledData[1] = 0;
ledData[2] = 0;
}
// send the raw data to the LEDs :-)
ledSerial[i].write(ledData);
}
}
// image2data converts an image to OctoWS2811's raw data format.
// The number of vertical pixels in the image must be a multiple
// of 8. The data array must be the proper size for the image.
void image2data(PImage image, byte[] data, boolean layout) {
int offset = 3;
int x, y, xbegin, xend, xinc, mask;
int linesPerPin = image.height / 8;
println("linesPerPin = " + linesPerPin);
int pixel[] = new int[8];
for (y = 0; y < linesPerPin; y++) {
if ((y & 1) == (layout ? 0 : 1)) {
// even numbered rows are left to right
xbegin = 0;
xend = image.width;
xinc = 1;
} else {
// odd numbered rows are right to left
xbegin = image.width - 1;
xend = -1;
xinc = -1;
}
for (x = xbegin; x != xend; x += xinc) {
for (int i=0; i < 8; i++) {
// fetch 8 pixels from the image, 1 for each pin
pixel[i] = image.pixels[x + (y + linesPerPin * i) * image.width];
pixel[i] = colorWiring(pixel[i]);
}
// convert 8 pixels to 24 bytes
for (mask = 0x800000; mask != 0; mask >>= 1) {
byte b = 0;
for (int i=0; i < 8; i++) {
if ((pixel[i] & mask) != 0) b |= (1 << i);
}
data[offset++] = b;
}
}
}
}
// translate the 24 bit color from RGB to the actual
// order used by the LED wiring. GRB is the most common.
int colorWiring(int c) {
int red = (c & 0xFF0000) >> 16;
int green = (c & 0x00FF00) >> 8;
int blue = (c & 0x0000FF);
red = gammatable[red];
green = gammatable[green];
blue = gammatable[blue];
return (green << 16) | (red << 8) | (blue); // GRB - most common wiring
}
// ask a Teensy board for its LED configuration, and set up the info for it.
void serialConfigure(String portName) {
if (numPorts >= maxPorts) {
println("too many serial ports, please increase maxPorts");
errorCount++;
return;
}
try {
ledSerial[numPorts] = new Serial(this, portName);
if (ledSerial[numPorts] == null) throw new NullPointerException();
ledSerial[numPorts].write('?');
} catch (Throwable e) {
println("Serial port " + portName + " does not exist or is non-functional");
errorCount++;
return;
}
delay(50);
String line = ledSerial[numPorts].readStringUntil(10);
if (line == null) {
println("Serial port " + portName + " is not responding.");
println("Is it really a Teensy 3.0 running VideoDisplay?");
errorCount++;
return;
}
String param[] = line.split(",");
if (param.length != 12) {
println("Error: port " + portName + " did not respond to LED config query");
errorCount++;
return;
}
// only store the info and increase numPorts if Teensy responds properly
ledImage[numPorts] = new PImage(Integer.parseInt(param[0]), Integer.parseInt(param[1]), RGB);
ledArea[numPorts] = new Rectangle(Integer.parseInt(param[5]), Integer.parseInt(param[6]),
Integer.parseInt(param[7]), Integer.parseInt(param[8]));
ledLayout[numPorts] = (Integer.parseInt(param[5]) == 0);
numPorts++;
}
// draw runs every time the screen is redrawn - show the movie...
void draw() {
// show the original video
image(myMovie, 0, 80);
// then try to show what was most recently sent to the LEDs
// by displaying all the images for each port.
for (int i=0; i < numPorts; i++) {
// compute the intended size of the entire LED array
int xsize = percentageInverse(ledImage[i].width, ledArea[i].width);
int ysize = percentageInverse(ledImage[i].height, ledArea[i].height);
// computer this image's position within it
int xloc = percentage(xsize, ledArea[i].x);
int yloc = percentage(ysize, ledArea[i].y);
// show what should appear on the LEDs
image(ledImage[i], 240 - xsize / 2 + xloc, 10 + yloc);
}
}
// respond to mouse clicks as pause/play
boolean isPlaying = true;
void mousePressed() {
if (isPlaying) {
myMovie.pause();
isPlaying = false;
} else {
myMovie.play();
isPlaying = true;
}
}
// scale a number by a percentage, from 0 to 100
int percentage(int num, int percent) {
double mult = percentageFloat(percent);
double output = num * mult;
return (int)output;
}
// scale a number by the inverse of a percentage, from 0 to 100
int percentageInverse(int num, int percent) {
double div = percentageFloat(percent);
double output = num / div;
return (int)output;
}
// convert an integer from 0 to 100 to a float percentage
// from 0.0 to 1.0. Special cases for 1/3, 1/6, 1/7, etc
// are handled automatically to fix integer rounding.
double percentageFloat(int percent) {
if (percent == 33) return 1.0 / 3.0;
if (percent == 17) return 1.0 / 6.0;
if (percent == 14) return 1.0 / 7.0;
if (percent == 13) return 1.0 / 8.0;
if (percent == 11) return 1.0 / 9.0;
if (percent == 9) return 1.0 / 11.0;
if (percent == 8) return 1.0 / 12.0;
return (double)percent / 100.0;
}