CoreImage avec MacRuby
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".
Lors 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 :

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...

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.