Follow me
RSS feed
My sources
My Viadeo

CoreImage avec MacRuby

Greg | 15 May 2010

Projets Dans la suite de notre découverte de MacRuby, je vous propose aujourd'hui de jouer à manipuler des images. L'idée de cet article est venue d'un besoin très concret. Lors de la présentation de Ceres, aux membres de RubyFrance, quelqu'un m'a demandé s'il ne serait pas possible de gérer les thumbnails un peu plus finement. La question précise était de faire en sorte que ces miniatures soient plus à jour et que nous puissions également en choisir la taille. Je me suis alors posé la question de savoir si nous ne pourrions pas les créer nous même. A l'époque, une recherche rapide m'avait fait tomber sur la solution de Tom Ward : "Taking screenshots of web pages with macruby".

ProjetsLors de l'écriture de son article, Tom ne proposait rien pour gérer la taille de la capture et le résultat était quelque peu disproportionné... Je m'étais donc penché sur une esquisse de solution qui m'a rapidement fait dévier sur la manipulation d'image avec CoreImage et MacRuby1.

Les filtres de CoreImage

CoreImage est une API faisant partie de QuartzCore qui ajoute à Quartz un système de gestion de filtres et effets. Dans la suite de cet article, nous allons voir comment utiliser certains de ces filtres et effets. Nous ne pourrons pas passer toutes les possibilités en revue, mais vous verrez qu'une fois les principes assimilés via quelques exemples, il est très facile de découvrir le reste par soi-même.

Si vous souhaitez explorer les possibilités de CoreImage avant de vous lancer, vous pouvez jeter un oeil à Core Image Fun House qui se trouve dans le répertoire /Developer/Applications/Graphics Tools. Cet outil vous permet de tester l'ensemble des filtres et effets disponibles. Pour aller un peu plus loin et commencer à manipuler la bête, vous pouvez également jouer avec Quartz Composer (situé dans /Developer/Applications). Voici un petit exemple d'utilisation du filtre de distorsion circulaire :

Quartz Composer

Cet exemple est intéressant à plus d'un titre. Non seulement il vous permet de voir facilement le rendu d'un filtre, mais il reprend la logique que nous allons utiliser.

Dans le principe, nous allons commencer par charger une image. Ensuite nous lui appliquerons le filtre choisi. Nous terminerons en rendant le résultat.

L'utilisation des filtres se fait via la classe CIFilter. Pour cela nous initialisons cette classe en indiquant le nom du filtre. Si nous reprenons l'exemple réalisé avec Quartz Composer, le filtre utilisé pour la distorsion circulaire est CICircularWrap. Nous initialisons donc notre filtre de la façon suivante :

filtre = CIFilter.filterWithName("CICircularWrap")

Nous utilisons ensuite la méthode setValue pour positionner les valeurs des différents paramètres utilisés par le filtre. Parmi tous ces paramètres, vous verrez que la plus part des paramètres sont optionnels, car possédant une valeur par défaut. Mais il y en a un que vous ne pourrez jamais omettre, c'est l'image à laquelle vous souhaitez appliquer le filtre. Ce paramètre est toujours nommé inputImage et sa valeur doit être un objet de type CIImage

ciImage = ...

filtre = CIFilter.filterWithName("CICircularWrap")
filter.setValue(ciImage, forKey:"inputImage"))

La récupération de l'image transformée se fait en utilisant la méthode valueForKey de CIFilter en utilisant la clé outputImage :

ciImage = ...

filtre = CIFilter.filterWithName("CICircularWrap")
filter.setValue(ciImage, forKey:"inputImage"))
...
ciOutputImage = filter.valueForKey("outputImage")

L'image obtenue est également une instance de CIImage.

Pour mettre cela en application, je vous propose de faire un petit exemple dans lequel nous appliquons un filtre à une image. Et puisque nous avons vu il y a peu de temps comment utiliser la vidéo, autant utiliser ce que nous savons et appliquer le filtre directement au flux vidéo capté par notre iSight.

Commencez par créer un nouveau projet de type MacRuby Application. Ajoutez-lui une nouvelle classe qui servira de contrôleur. Dans ce contrôleur, nous remettons en place la méthode vue pour récupérer le flux vidéo de notre iSight :

# AppController.rb
# iSightFilter
#
# Created by greg on 16/05/10.
# Copyright 2010 __MyCompanyName__. All rights reserved.


class AppController
  attr_accessor :qtView
  
  def awakeFromNib
    captureDevice = QTCaptureDevice.defaultInputDeviceWithMediaType("vide") #QTMediaTypeVideo
    captureDevice.open(nil)

    inputDevice = QTCaptureDeviceInput.deviceInputWithDevice(captureDevice)
    
    captureSession = QTCaptureSession.alloc.init
    captureSession.addInput(inputDevice, error:nil)
    
    @qtView.setCaptureSession(captureSession)
    
    captureSession.startRunning()
  end  
end

Il faut maintenant modifier l'interface en ajoutant dans la fenêtre une vue de type QTCaptureView que l'on liera à l'outlet qtView du contrôleur.

Pour appliquer un filtre au flux vidéo, nous allons utiliser la méthode de délégation view:willDisplayImage: de QTCaptureView. Cette méthode prend en paramètre l'objet QTCaptureView et un objet de type CIImage correspond à la prochaine image devant être affichée par la vue.

Pour le moment, comme nous n'appliquons aucun filtre, nous allons nous contenter de renvoyer l'image passée en entrée. Avant cela, nous n'oublierons pas de déclarer l'objet courant comme étant le délégué de la vue QTCaptureView :

# AppController.rb
# iSightFilter
#
# Created by greg on 16/05/10.
# Copyright 2010 __MyCompanyName__. All rights reserved.


class AppController
  attr_accessor :qtView
  
  def awakeFromNib
    puts "awakeFromNib()"
    
    captureDevice = QTCaptureDevice.defaultInputDeviceWithMediaType("vide") #QTMediaTypeVideo
    captureDevice.open(nil)

    inputDevice = QTCaptureDeviceInput.deviceInputWithDevice(captureDevice)
    
    captureSession = QTCaptureSession.alloc.init
    captureSession.addInput(inputDevice, error:nil)
    
    @qtView.setCaptureSession(captureSession)
    @qtView.setDelegate(self)
    
    captureSession.startRunning()
  end
  
  def view(v, willDisplayImage:image)
    return image
  end
end

Afin de voir les possibilités de plusieurs filtres, nous allons ajouter dans la fenêtre de notre application une liste déroulante dont nous fixerons en dure le contenu. Chaque entrée de cette liste correspondant au nom d'un filtre choisi parmi ceux offerts par CoreImage. Pour ma part, voici les entrées que j'ai placées dans cette liste : Aucun, CIDiscBlur, CIColorInvert, CIColorPosterize, CIComicEffect, CICrystallize, CIEdges, CICircularWrap, CICircularScreen, CIBoxBlur, CILineScreen, CILineOverlay, CIHexagonalPixellate, CIGloom, CIGaussianBlur, CIEdgeWork, CIDotScreen.

Il suffit maintenant d'ajouter un outlet dans le contrôler pour pouvoir récupérer l'entrée choisie dans cette liste, puis appliquer le filtre correspondant à la sortie vidéo :

# AppController.rb
# iSightFilter
#
# Created by greg on 16/05/10.
# Copyright 2010 __MyCompanyName__. All rights reserved.


class AppController
  attr_accessor :qtView, :effect
  
  def awakeFromNib
    puts "awakeFromNib()"
    
    captureDevice = QTCaptureDevice.defaultInputDeviceWithMediaType("vide") #QTMediaTypeVideo
    captureDevice.open(nil)

    inputDevice = QTCaptureDeviceInput.deviceInputWithDevice(captureDevice)
    
    captureSession = QTCaptureSession.alloc.init
    captureSession.addInput(inputDevice, error:nil)
    
    @qtView.setCaptureSession(captureSession)
    @qtView.setDelegate(self)
    
    captureSession.startRunning()
  end
  
  def view(v, willDisplayImage:image)
    unless effect.objectValueOfSelectedItem.nil? or effect.objectValueOfSelectedItem == "Aucun"
      @filter = CIFilter.filterWithName(effect.objectValueOfSelectedItem)
      @filter.setDefaults
    
      @filter.setValue(image, forKey:"inputImage")
      return @filter.valueForKey "outputImage"
    else
      return image
    end
  end
end

Voilà, il ne vous reste plus qu'à tester...

Filtre vidéo

CIImage et NSImage

Si nous revenons à mon problème de départ, vous verrez qu'il y a une chose dont nous allons rapidement avoir besoin : transformer un objet de type NSImage en objet de type CIImage, et inversement. Voici la solution que j'utilise pour cela :

def nsImageToCIImage(nsImage)
  bitmapimagerep = NSBitmapImageRep.imageRepWithData(nsImage.TIFFRepresentation)
  im = CIImage.alloc.initWithBitmapImageRep(bitmapimagerep)
  return im
end

def ciImage(im, toNSImageFromRect:r)
  outputImageRect = NSRectFromCGRect(r)
  image = NSImage.alloc.initWithSize(outputImageRect.size)
  image.lockFocus
  im.drawAtPoint(NSZeroPoint, fromRect:outputImageRect, operation:NSCompositeCopy, fraction:1.0)
  image.unlockFocus
  return image
end

def ciImageToNSImage(im)
  return ciImage(im, toNSImageFromRect:im.extent)
end

Il est donc très facile de créer une méthode permettant de couper une image "trop longue", comme celle récupérée via snapper.rb, en imposant un ratio largeur/hauteur souhaité :

def cropNSImage( image, withRatio:ratio )
  ciimage = nsImageToCIImage( image )
  filter = CIFilter.filterWithName("CICrop")
  filter.setValue(ciimage, forKey:"inputImage")
   
  imageRep = image.representations.objectAtIndex(0)
  imageSize = NSMakeSize(imageRep.pixelsWide, imageRep.pixelsHigh)
  cropHeight = imageSize.width/ratio
   
  filter.setValue( 
    CIVector.vectorWithX( 0, Y:(imageSize.height - cropHeight), Z:imageSize.width, W:cropHeight ), 
    forKey:"inputRectangle"
  )
  ciimage = filter.valueForKey("outputImage")
  return ciImageToNSImage(ciimage)
end

Nous utiliserons donc cette méthode de la façon suivante pour obtenir une capture en 4/3 :

image43 = cropNSImage( image, withRatio:(4.0/3.0) )

1 Notez que depuis, macruby-snapper permet de gérer la taille des captures.

Copyright © 2009 - 2011 Grégoire Lejeune.
All documents licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License, except ones with specified licence.
Powered by Jekyll.