Feb.09

SlightBox V1 : un système « Ambilight » avec C# et Gadgeteer

Cet article a été initialement publié dans le magazine Programmez! n°165 paru en Juillet/Aout 2013.

Programmez n°165

Inventée en 2002 par Phillips, la technologie Ambilight consiste à générer des effets lumineux autour de l’écran en fonction du contenu vidéo. Nous allons découvrir dans cet article comment développer notre propre système « Ambilight » en utilisant des modules « .NET Gadgeteer » et un peu de C#. Préparez Visual Studio, une perceuse et un fer à souder !


Principe de fonctionnement

Il n’est pas très complexe de développer un système « Ambilight » performant. Dans cette première version, le prototype « SLightBox » est conçu pour fonctionner avec Windows. Cela vous permettra de profiter de ce système sur vos ordinateurs ou media-centers quel que soit le contenu vidéo (photo, vidéo, jeu ou toute autre application ou média).

Slight logo

Pour réaliser un prototype rapide du concept, les modules .NET Gadgeteer basés sur le .NET Microframework offrent une plateforme de développement très facile à utiliser pour les développeurs.

Comme cette première solution se base sur l’utilisation d’un PC, l’analyse du contenu vidéo sera relativement simple à entreprendre : capturer l’écran afin de récupérer l’ensemble des pixels affichés !

Pour obtenir une expérience visuelle immersive nous avons besoin de dispositifs lumineux derrière l’écran longeant chacun des côtés. Pour que l’effet soit optimal, l’écran devra être placé à plus ou moins 30 cm d’un mur, si possible de couleur claire (idéalement blanc). De plus ces dispositifs lumineux, comme des LEDs par exemple, devront être contrôlables individuellement.

Une fois la capture de l’écran réalisée, le logiciel calculera la moyenne des couleurs pour chaque zone afin d’envoyer cette couleur à sa LED associée, située derrière l’écran (on établit qu’une zone est associée à une LED uniquement). Cette opération doit avoir lieu plus de 10 fois par seconde pour rester fluide et réactive par rapport au contenu vidéo.

Le hardware

Les LEDs

La solution la plus adaptée au besoin sera d’utiliser des bandes de LEDs multicolores. Ces bandes de LED offrent la possibilité de les sectionner à certains endroits de la bande (en général par pas de 3 à 10 cm) permettant ainsi une plus grande souplesse d’utilisation. Il existe cependant deux types de bandes LEDs multicolores : les analogiques et les digitales.

Les bandes analogiques sont les plus répandues dans le commerce et les moins couteuses car l’ensemble des LEDs qui la constitue (généralement des SMD5050 qui intègrent 3 LEDS : une rouge, une bleue et une verte) sont connectées en parallèle. On contrôle ainsi la couleur de toutes les LEDs en modulant le signal avec une sortie analogique PWM (et quelques MOSFETs pour amplifier le signal).

Pour permettre un contrôle individuel de la couleur de chaque LED, les bandes dites « digitales » intègrent des microcontrôleurs pour pouvoir adresser chaque LED individuellement.

On trouve dans le commerce une multitude de microcontrôleurs pour cet usage (HL1606, WS2801, WS2811, LPD8603, TM1812, UCS1903, etc…) les plus répandus étant le WS2801 et le LPD8806.

Ce dernier permet de contrôler 6 canaux séparés de 7 bits. Il y a donc un microcontrôleur pour deux LEDs où chaque couleur est codée sur 21 bits soit 2 097 152 combinaisons de couleur/brillance possible.

En général il y a 32 LEDs (donc 16 microcontrôleurs LPD8806) par mètre mais certains fabricants proposent jusqu’à 52 LEDs/mètre.

LPD8806

Pour l’installation, il suffit de découper votre bande de LEDs suivant les dimensions de votre écran. Chacune de vos bandes doit être connectée en série en les reliant ensemble. Il y a deux connecteurs pour l’alimentation (5V) et deux connecteurs pour l’interface SPI (données et horloge).

Sur l’une des extrémités, vous souderez un connecteur JST 4 pôles pour la connexion à la « SLightBox ».

Installation des LEDs

La « SLight Box »

Pour piloter le bus SPI depuis une application Windows nous utiliserons des modules .NET Gadgeteer. Il faut donc une carte mère (mainboard) Gadgeteer comme la FEZ Spider, FEZ Hydra ou moins cher, la FEZ Cerberus (ainsi que son module d’alimentation comme le module « SP Client »).

Pour la communication avec l’ordinateur nous utiliserons le module « USB-Serial » qui expose un port COM virtuel pour dialoguer depuis notre PC.

Pour le contrôle des LEDs, les cartes .NET Gadgeteer disposent d’une interface SPI sur les sockets de type « S » où la pin 7 est utilisée pour écrire les données (le MOSI) et la pin 9 pour l’horloge (SCK). Nous utiliserons le module Extender en soudant une petite barrette mâle (pas standard 2,54mm) pour faciliter les connexions.

Module Extender

L’ensemble de ces modules sera installé dans une petite boite en plastique avec une connectique pour :

  • l’alimentation 5V,
  • le connecteur USB du module « USB-Serial »,
  • le connecteur JST vers les bandes de LEDs.

En façade :

  • un interrupteur,
  • une LED témoin rouge d’alimentation.

SlightBox V1 (avant)SlightBox V1 (arrière) 

L’entrée 5V est coupée par l’interrupteur puis alimente le module SP (donc la mainboard Gadgeteer), la sortie JST (pour les LEDs) et la LED témoin en façade.

figure 8 v2 alt

Le software

Côté Gadgeteer

La «  SlightBox Gadgeteer » est ni plus ni moins qu’une interface USB/SPI vers les bandes de LEDs. Son rôle est simple : attendre les données du PC via le port USB et les recopier sur le port SPI.

Designer Visual Studio

Au démarrage de la carte Gadgeteer, nous initialiserons la sortie SPI sur la socket du module Extender en spécifiant l’horloge à 16mhz, ainsi que le module USB en spécifiant le débit du port COM au maximum (115200 bauds) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Configure SPI Port
GTI.SPI.Configuration spiConfig = new GTI.SPI.Configuration(
         false,	// chip select active state
         0,		// chip select setup time
         0,		// chip select hold time
         false,	// clock idle state
         true,	// clock edge (true = rising)
         16000);	// 16mhz
mLedStrip = new GTI.SPI(Socket.GetSocket(extender.ExtenderSocketNumber, true, extender, null), spiConfig, GTI.SPI.Sharing.Exclusive, null);
// Configure USB Serial port
usbSerial.Configure(115200, Gadgeteer.Interfaces.Serial.SerialParity.None, Gadgeteer.Interfaces.Serial.SerialStopBits.One, 8);
// Open USB Serial port
usbSerial.SerialLine.Open();
// Ready !
Debug.Print("SLightBox Ready !");


Pour la suite, il suffit d’attendre les données du port USB pour les recopier sur l’interface SPI :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (true)
{
    if (usbSerial.SerialLine.BytesToRead > 0)
    {
        int bytesRead;
        if ((bytesRead = usbSerial.SerialLine.Read(buffer, 0, usbSerial.SerialLine.BytesToRead)) > 0)
        {
            byte[] dataToWrite = new byte[bytesRead];
            Array.Copy(buffer, dataToWrite, bytesRead);
            mLedStrip.Write(dataToWrite);
            Thread.Sleep(10);
        }
    }
    else
    {
        Thread.Sleep(1);
    }
}

La « SlightBox » est prête, il suffit de la connecter aux LEDs par le connecteur JST, au PC par un câble USB et à un transformateur 5V. En enclenchant l’interrupteur, la « SlightBox » sera détectée comme un « port COM virtuel » et attendra les données à envoyer aux LEDs !

Pour finir, n’oubliez pas de bien dimensionner la puissance de l’alimentation 5V : il faut prévoir 0,5 ampères pour les modules Gadgeteer puis 2 ampères par mètre de LEDs (une LED consomme 60mA au maximum lorsqu’on affiche du blanc. Pour 32 LEDs/mètre, on peut théoriquement consommer près de 2A).

Côté Windows

Une fois la partie « hardware » terminée, nous pouvons nous attaquer au « cœur » de cette solution : le programme Windows : Slight.

Ce programme doit être capable de capturer l’image de votre écran afin d’analyser les couleurs dominantes et les envoyer sur le port COM virtuel qui seront ensuite transmises par SPI aux LEDs via le programme Gadgeteer de la « SlightBox ».

Pour la capture de l’écran, nous nous servirons de DirectX pour des questions de performance à l’aide de SlimDX, un framework .NET open-source pour DirectX.

Par exemple pour capturer l’écran principal :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Initialization 
this.dxDevice = new Device(
    new Direct3D(), 0, DeviceType.Hardware, IntPtr.Zero, CreateFlags.SoftwareVertexProcessing,
    new PresentParameters() { Windowed = true, SwapEffect = SwapEffect.Discard });
 
// To capture, get the screen surface
Surface surface = Surface.CreateOffscreenPlain(
                this.dxDevice,
                Screen.PrimaryScreen.Bounds.Width,
                Screen.PrimaryScreen.Bounds.Height,
                Format.A8R8G8B8, Pool.Scratch);
// Capture surface screen buffer data
Surface screenSurface = this.dxDevice.GetFrontBufferData(0, surface);
DataRectangle screenRectangle = screenSurface.LockRectangle(LockFlags.None);
DataStream screenDataStream = screenRectangle.Data;
 
// - Do work -
 
// Dispose screen surface
screenSurface.UnlockRectangle();
screenSurface.Dispose();

Le “DataStream” est un flux où chaque pixel est représenté sur 4 bits (B, G, R, A).

Au démarrage le programme Slight calcule les coordonnées de chaque zone en fonction de la disposition des bandes de LEDs et de la résolution de l’écran. On sélectionne ensuite les coordonnées d’un pixel sur vingt dans cette zone comme échantillon pour calculer la couleur moyenne de la zone.

Comme chaque zone est associée à une LED uniquement, nous ajouterons des Panels dans notre fenêtre pour visualiser les couleurs calculées pour chaque LED.

A la fin de l’initialisation nous disposons donc de la position dans le DataStream de chaque « pixel échantillon » pour chaque zone.

A l’aide d’un « Timer » déclenché toutes les 50ms, nous capturerons le DataStream de l’écran afin de calculer la couleur moyenne de chaque zone par la méthode GetColorAverage :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Color GetColorAverage(DataStream screenDataStream, Collection positions)
{
    byte[] buffer = new byte[BITS_PER_PIXEL];
    int r = 0, g = 0, b = 0, count = 0;
    // For each pixels
    foreach (long pos in positions)
    {
        screenDataStream.Position = pos;
        screenDataStream.Read(buffer, 0, BITS_PER_PIXEL);
        r += buffer[2];
        g += buffer[1];
        b += buffer[0];
        count++;
    }
    // Return the average color for the list of pixels
    return Color.FromArgb(r / count, g / count, b / count);
}

Pour adresser la couleur à la bonne LED nous devons écrire la liste des couleurs des zones les unes derrière les autres.

Pour rappel, avec le LPD8806 les couleurs sont définies en GRB (Vert, Rouge, Bleu) sur 7 bits. On pourrait donc diviser la valeur capturée du R, G et B par deux. Néanmoins pour rendre les couleurs plus riches, nous appliquerons une petite correction gamma : (127 * (La_Couleur_sur_8_bits / 255) ^ Correction_Gamma) + 0.5 (où « Correction_Gamma » est par défaut à 2,5 mais peut varier entre 1 et 5 en fonction de vos préférences).

Ainsi pour chaque capture :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
byte[] colorData = new byte[ledBytesCount];
// For each zones (= for each leds)
for (int i = 0; i < numberOfLeds; i++) 
{
    Color pixelColor = Color.Black;
    if (pixelsList.Count > i)
    {
        // Get color average
        pixelColor = this.GetColorAverage(screenDataStream, pixelsList[i]);
        // Update UI
        this.updatePanelColorActions[i](pixelColor);
    }
    // Build colorData
    int index = i * BITS_PER_LED;
    if (this.GammaCorrectionEnabled)
    {
        colorData[index + 0] = (byte)(LPD8806_MASK | (byte)(Math.Pow((double)pixelColor.G / 255.0, this.gammaCorrectionValue) * 127.0 + 0.5));
        colorData[index + 1] = (byte)(LPD8806_MASK | (byte)(Math.Pow((double)pixelColor.R / 255.0, this.gammaCorrectionValue) * 127.0 + 0.5));
        colorData[index + 2] = (byte)(LPD8806_MASK | (byte)(Math.Pow((double)pixelColor.B / 255.0, this.gammaCorrectionValue) * 127.0 + 0.5));
    }
    else
    {
        colorData[index + 0] = (byte)(LPD8806_MASK | (pixelColor.G / 2));
        colorData[index + 1] = (byte)(LPD8806_MASK | (pixelColor.R / 2));
        colorData[index + 2] = (byte)(LPD8806_MASK | (pixelColor.B / 2));
    }
}
// Send datas to the SerialPort
byte[] datas = new byte[ledBytesCount + LPD8806_LATCH_BITS];
Array.Copy(colorData, 0, datas, LPD8806_LATCH_BITS, ledBytesCount);
this.port.Write(datas, 0, datas.Length);


Avec les déclarations suivantes :

1
2
3
4
5
6
SerialPort port = new SerialPort("COMx", 115200, Parity.None, 8, StopBits.One);
const int BITS_PER_PIXEL = 4; 
const int BITS_PER_LED = 3; 
const int LPD8806_LATCH_BITS = 21;
const byte LPD8806_MASK = 0x80;
int ledBytesCount = numberOfLeds * BITS_PER_LED;

Le tableau d’actions « updatePanelColorActions » permet de mettre à jour la couleur du Panel associée dans la fenêtre :

Programme Slight

Voilà l’application terminée, il ne reste plus qu’à tester son bon fonctionnement et ses performances grâce à des vidéos de test spécialement conçues pour Ambilight que vous trouverez sur YouTube.

Demo Slight

Nous pouvons encore enrichir son fonctionnement en ajoutant la capacité de s’adapter dynamiquement au format de la vidéo ! En effet, lorsque vous regardez une vidéo en 4:3 ou 21:9 sur un écran 16:9 vous n’échapperez pas aux bandes noires horizontales ou verticales. Les zones calculées par Slight risqueraient de ne « rien voir » d’autre que du noir sur certain bord.

Pour combler cette lacune, il faut recalculer les coordonnées des zones en prenant compte de la taille des bandes noires.

Pour cela, au démarrage de l’application, on calcule les coordonnées des pixels d’une ligne fictive partant de chaque bord de l’écran vers le centre.

Plusieurs fois par seconde, on remonte cette ligne tant que le pixel est noir ! Dès lors qu’il ne l’est plus, cela signifie alors que nous sommes au début du contenu vidéo !

Pour éviter un comportement instable, chaque côté utilise trois lignes différentes (au ¼, ½ et ¾) où chaque pixel de couleur trouvé doit être à la même distance du bord sur les deux autres lignes. De plus, le redimensionnement n’a lieu que si les dix dernières mesures ont donné le même résultat.

Ainsi peu importe le contenu vidéo, Slight s’adaptera en toute circonstance !

Conclusion

Le résultat en vidéo :

Comme vous l’avez découvert dans cet article, la réalisation d’un système Ambilight pour PC est relativement aisée à entreprendre avec quelques modules Gadgeteer, un Visual Studio et quelques composants, sans oublier les bandes de LEDs digitales.

Les modules Gadgeteer sont très pratiques pour confectionner des prototypes de tout genre bien que dans ce projet ils soient largement sous-utilisés.

Pour rendre le système moins cher, la « SlightBox v2 » est basée sur une puce MCP2210 USB/SPI dédiée et remplace ainsi les 4 modules Gadgeteer. L’application Windows reste la même dans sa logique mais communique avec la SlightBox non pas via un port COM virtuel du module Gadgeteer mais par le driver HID du fabricant de cette puce.

Pour aller encore plus loin, la « SlightBox v3 » sort du cadre du PC : branchez-la directement sur vos périphériques HDMI (box internet, câble/satellite, DVD/Bluray, consoles de jeux, etc..) et profitez des avantages de Slight sur votre TV sans aucun PC !

Retrouvez plus d’informations et photos sur la page Facebook SlightBox.

Dev,HighTech,.NET,SlightBox
Share this Story:
  • facebook
  • twitter
  • gplus

Comments(2)

  1. Trackback: SlightBox V2 – La version “mini” | Sebastien.warin.fr

  2. Trackback: SlightBox V3 : La version “HDMI” | Sebastien.warin.fr

Leave a comment

Comment