Archive | novembre 2017

Traitement d’image avec Fsharp et OpenCL

En ce moment je découvre les possibilités de F# en matière de scientific computing. F# (prononcé « Fsharp ») est un langage calqué sur OCaml dont il partage la syntaxe de base et une grosse partie de la bibliothèque standard. Quelques fonctionnalités disparaissent (les foncteurs et plus généralement tout ce qui concerne les modules, les types algébriques généralisés), des fonctionnalités sont légèrement retouchées (le quotation mark devient <@… @>, indentation non libre…), en contrepartie le langage a accès à la totalité de l’écosystème DotNet ou leur équivalent Mono et NetCore. Ces environnements étant particulièrement populaires il n’est pas étonnant de voir qu’il existe déjà des bibliothèques pour le calcul statistique, la recherche opérationnelle, les divers algorithmes d’intelligence artificielle, et pour ce qui nous concerne ici l’interfaçage avec des APIs de GPGPU comme OpenCL. Ce qui me donne l’occasion de tester les capacités de F# en tant que langage pour le traitement d’image face à un Python ou à un Matlab.

Commençons déjà par le code permettant de charger une image. De base DotNet fournit la classe Bitmap permettant de charger à peu près n’importe quel format courant et nous proposant d’accéder à ses pixels. Cette dernière est présente dans l’espace System.Drawing (dont il faudra référencer l’assembly) :

open System.Drawing
[<EntryPoint>]
let main argv =
    let img = new Bitmap(@"C:\Users\moi\Documents\fichier.jpg")
    // ….
    0

Afin de vérifier que tout se passe comme attendu il est souhaitable d’afficher cette image. Pour cela nous allons créer un System.Windows.Forms et dessiner l’image dedans via ce code :

open System.Windows.Forms
// ….
let form = new Form(Visible=true)
form.Paint.Add(function e-> e.Graphics.DrawImage(img, e.ClipRectangle, e.ClipRectangle, GraphicsUnit.Pixel))
System.Windows.Forms.Application.Run(form)

Si tout se passe bien vous devriez voir apparaitre votre image à l’écran dans une fenêtre relativement simple.

Maintenant que nous avons chargé notre image nous pouvons nous pencher sur l’utilisation d’OpenCL. Pour cela nous allons passer par la bibliothèque Brahma.OpenCL qui fournit à Fsharp une interface de relativement haut niveau autour d’OpenCL.Net. Cette dernière est disponible via Nuget.
L’initialisation de Brahma est relativement simple, il faut deux objets, un provider (qui représente votre installation OpenCL sur votre machine) et une CommandQueue à laquelle nous soumettrons les tâches que nous voulons effectuer :

open Brahma.OpenCL
open Brahma.FSharp.OpenCL.Core
open Brahma.FSharp.OpenCL.Extensions
open OpenCL.Net
open FSharp.Core
// ….
let provider = ComputeProvider.Create("*", DeviceType.Gpu)
let mutable commandQueue = new Brahma.OpenCL.CommandQueue(provider, provider.Devices |> Seq.head)

Ici nous utiliserons le premier GPU disponible, d’où le Seq.head.

L’avantage de Brahma.OpenCL est qu’elle repose sur les quotations marks pour la création de kernel OpenCL. Une quotation mark est un code F# sous forme d’arbre de syntaxe abstrait, ie une expression qui peut ensuite être passée comme argument à une fonction F#, par exemple pour la modifier, l’interpréter, ou dans le cas de Brahma, la traduire en code OpenCL. Ici nous allons écrire un filtre de Sobel (norme du gradient) prenant en argument un tableau 1d source et un tableau 1d destination (le gradient a besoin des informations des pixels l’entourant, il n’est pas possible de modifier « inplace » les données d’un tableau de façon concurrente. A noter que les quotations mark peuvent capturer des variables comme ici stride.

open Microsoft.FSharp.Quotations
// ….
let stride = img.Height;
let command = <@ fun (range:_2D) (buf:array<byte>) (dst:array<byte>) ->
    let i = range.GlobalID0
    let j = range.GlobalID1
    let mutable h = float32 0.
    if (i > 0 && i < stride) then
        let left = float32 buf.[i - 1 + stride * j]
        let right = float32 buf.[i + 1 + stride * j]
        h <- abs (left - right)
    let mutable v = float32 0.
    if (j > 0) then
        let top = float32 buf.[i + stride * (j - 1)]
        let bottom = float32 buf.[i + stride * (j + 1)]
        v <- abs (top - bottom)
    dst.[i + stride * j] <- byte (sqrt (h * h + v * v)) @>

Bien entendu il n’est pas possible d’écrire n’importe quel code, les kernels OpenCL ne pouvant pas être récursifs par exemple ou appeler des lambdas. La compilation d’une quotation mark sur du code qui ne peut être traduit génèrera une exception.

let kernel, kernelprepare, kernelrun = provider.Compile command

Les valeurs kernelprepare et kernelrun de la compilation permettent respectivement de mettre à disposition les dimensions et arguments (ici les tableaux buf et dst) du kernel au GPU, et d’exécuter ce code kernel via la syntaxe suivante :

kernelprepare d src dst
commandQueue.Add(kernelrun()) |> ignore
commandQueue.Add(dst.ToHost provider).Finish() |> ignore

d est un objet contenant les dimensions du workgroup (global et local) à utilizer pour le kernel, src et dst sont des Arrays F# classiques. On aura au préalable transféré le contenu 2d de img dans src 1d :

let d = _2D(img.Width, img.Height, 8, 8)
let src = Array.init (img.Width * img.Height) (function i -> img.GetPixel(i / stride, i % stride).R)
let dst = Array.zeroCreate (img.Width * img.Height)

Enfin une fois le kernel execute nous copions le contenu de dst dans img :

Array.iteri (fun i (v:byte) -> img.SetPixel(i / stride, i % stride, Color.FromArgb(255, int(v), int(v), int(v)))) dst

Voilà !