MokaByte 89 - 8bre 2004 
Video grabbing con JMF

di
Vincenzo Viola

Dopo l'articolo del mese scorso, in cui abbiamo visto come creare un file video a partire da una serie di immagini, questo mese vedremo come realizzare il processo contrario, ovvero come "grabbare" un video, ottenendo i suoi frames in formato JPEG, sempre utilizzando le JMF API.

Introduzione
Consideriamo lo schema illustrato nella Figura 1: l'input è il file video, l'output è costituito dai file in cui saranno salvati i frames componenti il video, in formato JPEG. L'elaborazione è realizzata utilizzando le JMF API.


Figura 1
- Modello di elaborazione di dati multimediali

Le uniche due classi di JMF che utilizzeremo e che non abbiamo già incontrato nell'articolo dello scorso mese sono:

  • javax.media.control.FramePositioningControl: interfaccia per a controllare il posizionamento di un frame all'interno di un video, per i Player e i Processor. C'è una corrispondenza diretta tra il numero identificativo di un frame e l'istante di tempo in cui esso viene visualizzato (media time). Non tutti i Player sono in grado di realizzare quest'associazione, ma per quelli per i quali ciò è possibile, si possono usare i metodi mapFrameToTime e mapTimeToFrame. In realtà, utilizzando formati video standard e soprattutto file in "buone condizioni", il Player sarà sempre in grado di fare quanto detto e quando sarà "spostato" su un certo frame, il media time del Player diventerà quello del frame.
  • javax.media.control.FrameGrabbingControl: interfaccia per estarre un frame di un video da un Player.


Preparazione delle risorse: utilizzo del Player.
Per semplicità facciamo riferimento ad un video di tipo QuickTime e salviamo i frames in formato JPEG, ma si possono utilizzare tutti i formati video supportati da JMF.
Il primo passo consiste nel definire le risorse in ingresso e in uscita. Costruiamo quindi un MediaLocator sulla URL del video che vogliamo grabbare e attraverso il Manager creiamo il DataSource:

MediaLocator ml = new MediaLocator(urlVideo);
DataSource ds = Manager.createDataSource(ml);

A questo punto non utilizzeremo, ai fini della nostra elaborazione, un Processor, come fatto nell'articolo del mese scorso, bensì un Player. Il Player processa i dati multimediali che riceve in ingresso mediante il DataSource, ed è capace di scorrere sul flusso di dati.


Figura 2
- Modello del Player in JMF

Un Player può trovarsi in uno dei seguenti stati: i due stati primari, Stopped and Started, definiti dall'interfaccia Clock; per facilitare la gestione delle risorse, l'interfaccia Controller suddivide lo stato Stopped in cinque stati di "standby": Unrealized, Realizing, Realized, Prefetching e Prefetched.


Figura 3
- Stati del Player

L'interfaccia ControllerListener fornisce i metodi per determinare in quale stato si trova il Player.
Prima di poter effettuare le operazioni preliminari di video grabbing abbiamo bisogno che il Player si trovi nello stato Realized.

Player p = Manager.createPlayer(ds);
p.addControllerListener(this);
p.realize();

Quando il Player viene creato si trova nello stato Unrealized e non conosce nulla riguardo il flusso multimediale. Quando viene chiamato il metodo realize, il Player transita nello stato Realizing. In questa fase il Player acquisisce le risorse per poi passare allo stato Realized nel quale sa come renderizzare i dati e può disporre di componenti visuali e controlli.
Sono proprio i controlli FramePositioningControl e FrameGrabbingControl che ci serviranno. Li otteniamo dal Player tramite il metodo getControl:

FramePositioningControl fpc = (FramePositioningControl)p.getControl
                              ("javax.media.control.FramePositioningControl");
FrameGrabbingControl fgc = (FrameGrabbingControl)p.getControl
                           ("javax.media.control.FrameGrabbingControl");

La prima cosa che facciamo ora è ottenere la durata e il numero di frames del video:

Time duration = p.getDuration();
int totalFrames = fpc.mapTimeToFrame(duration);

Portiamo quindi il Player nello stato Prefetched:

p.prefetch();

Quando viene chiamato il metodo prefetch il Player transita dallo stato Realized allo stato Prefetching. Durante questa fase il Player carica i dati multimediali e si prepara a visualizzarli. Fatto ciò passa nello stato Prefetched, in cui è pronto ad essere mandato in esecuzione.

 

Costruzione di un'interfaccia grafica per il grabbing
Prima di entrare nel vivo del processo di grabbing, costruiamo un'interfaccia grafica mediante la quale l'utente potrà avviare il grabbing e seguirne l'evoluzione.
Utilizzeremo un oggetto Panel a cui aggiungeremo dei tasti "Grab" e "Close" e che inseriremo nella componente visuale del Player, di cui possiamo modificare a nostro piacimento il layout, il titolo, la posizione sullo schermo,ecc. Questo perché la nostra classe principale VideoGrabbing è un'estensione della classe Frame:

setLayout(new BorderLayout());

Panel controlPanel = new Panel();

Button grabButton = new Button("Grab");
Button closeButton = new Button("Close");

grabButton.addActionListener(this);
closeButton.addActionListener(this);

controlPanel.add(new Label(urlVideo.substring(5,urlVideo.length())));
controlPanel.add(grabButton);
controlPanel.add(closeButton);

Component vc;
if ((vc = p.getVisualComponent()) != null) {
add("Center", vc);
}

add("South", controlPanel);
setTitle("VideoGrabber");
setLocation(500,100);
setVisible(true);

L'interfaccia grafica avrà quindi il seguente aspetto:


Figura 4
- Interfaccia grafica del VideoGrabber


Grabbing del video.

Ovviamente il grabbing del video avrà inizio alla pressione del tasto Grab. Per rendere sensibile l'applicazione a questa azione, la classe VideoGrabbing implementa l'ActionListener. Avremo quindi un metodo actionPerformed(ActionEvent ae) che a seguito della pressione del tasto Grab o del tasto Close provvederà a realizzare il grabbing oppure a chiudere l'interfaccia e uscire dall'applicazione.
Supponiamo di aver avviato il grabbing. L'utente quando avvia l'applicazione ha la possibilità di scegliere il campionamento, ovvero il numero di frame che intercorre tra quelli che vorrà che siano estratti: con un campionamento pari a 15 saranno estratti i frames numero 0, 15, 30, 45, ecc., con un campionamento pari a 1 saranno estratti tutti i frames.
Prima di tutto spostiamo il Player di un numero di frame che sarà 0 all'inizio (prendiamo il frame corrispondente all'istante 0) e poi sarà pari al campionamento:

fpc.skip(camp);

Quindi otteniamo il numero identificativo del frame e il corrispondente media time:

int currentFrame = fpc.mapTimeToFrame(p.getMediaTime());
Time currentTime = fpc.mapFrameToTime(i);

dove i = 0 all'inizio e poi i = i + camp.

Possiamo ora grabbare il frame corrente, utilizzando il metodo grabFrame() di FrameGrabbingControl, che ci restituisce il nostro frame sotto forma di Buffer:

Buffer buff = new Buffer();
buff = fgc.grabFrame();

Salvataggio dei frames in formato JPEG.
Poiché noi vogliamo delle immagini, la prima cosa che facciamo è una conversione Buffer - Image:

BufferToImage bti = new BufferToImage((VideoFormat)buff.getFormat());
Image img = bti.createImage(buff);

Costruiamo i file su cui salvare le immagini:

String pathFrame = (urlFrames+"/"+currentFrame+".jpg");
File fileFrame = new File(pathFrame);

La conversione nel formato JPEG e il salvataggio su file vengono effettuati dal metodo ImageSaver, a cui passiamo come argomenti l'immagine e il file su cui salvarla:

ImageSaver(img , fileFrame);

Questo metodo converte Image in BufferedImage e, utilizzando la classe ImageIO, contemporaneamente la converte in JPEG e la scrive su File:

BufferedImage bi=(BufferedImage)img;
ImageIO.write(bi, "JPG", fileFrame);

Implementazione.
Riportiamo di seguito, il codice che esegue il video grabbing. Per l'esecuzione corretta bisogna passare come argomenti la url del video, il path dove salvare i frames e il passo di campionamento:

java VideoGrabbing <url video> <path frames> <passo di campionamento>

java VideoGrabbing file:c:\video\\prova.MOVi c:\immagini\prova 5

import java.awt.*;
import java.awt.event.*;
import javax.media.*;
import javax.media.format.VideoFormat;
import javax.media.control.FramePositioningControl;
import javax.media.protocol.DataSource;
import javax.media.control.FrameGrabbingControl;
import javax.media.util.*;
import java.io.*;
import java.lang.Object;
import java.awt.image.ImageObserver;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import java.awt.Image;


public class VideoGrabbing extends Frame implements ControllerListener,
ActionListener {

Player p;
FramePositioningControl fpc;
FrameGrabbingControl fgc;
Object waitSync = new Object();
boolean stateTransitionOK = true;
int totalFrames = FramePositioningControl.FRAME_UNKNOWN;
Image img;
int currentFrame;
Time currentTime;
static String urlVideo,urlFrames;
static int camp;

Panel controlPanel;
Button grabButton;
Button closeButton;

/**
* Creazione del Player che verrà utilizzato per il grabbing.
*/

public boolean open(DataSource ds) {

System.err.println("Creato player per video in formato " +
ds.getContentType());

try {
p = Manager.createPlayer(ds);
} catch (Exception e) {
System.err.println("Creazione del player per il DataSource: "
+ e + " fallita!");
return false;
}

p.addControllerListener(this);

p.realize();
if (!waitForState(p.Realized)) {
System.err.println("Realizzazione del player fallita!");
return false;
}

// Provo a ottenere FramePositioningControl dal player.
fpc = (FramePositioningControl)p.getControl
("javax.media.control.FramePositioningControl");
// Provo a ottenere FrameGrabbingControl dal player.
fgc = (FrameGrabbingControl)p.getControl
("javax.media.control.FrameGrabbingControl");

if (fpc == null) {
System.err.println("Il player non supporta il " +
"FramePositioningControl.");
return false;
}

Time duration = p.getDuration();

if (duration != Duration.DURATION_UNKNOWN) {
System.err.println("Durata del video: " +
duration.getSeconds() + " secondi.");

totalFrames = fpc.mapTimeToFrame(duration);

if (totalFrames != FramePositioningControl.FRAME_UNKNOWN){
System.err.println("Numero totale di frames del video: "
+ totalFrames);
} else
System.err.println("Il FramePositiongControl non " +
" supporta mapTimeToFrame.");

} else {
System.err.println("Durata del video sconosciuta!");
}

// Prefetch del player.
p.prefetch();
if (!waitForState(p.Prefetched)) {
System.err.println("Prefetch del player fallito.");
return false;
}

// Costruzione dell'interfaccia grafica.

setLayout(new BorderLayout());

controlPanel = new Panel();

grabButton = new Button("Grab");
closeButton = new Button("Close");

grabButton.addActionListener(this);
closeButton.addActionListener(this);

controlPanel.add(new Label
(urlVideo.substring(5,urlVideo.length())));
controlPanel.add(grabButton);
controlPanel.add(closeButton);

Component vc;
if ((vc = p.getVisualComponent()) != null) {
add("Center", vc);
}

add("South", controlPanel);
setTitle("VideoGrabber");
setLocation(500,100);
setVisible(true);
return true;
}


public void addNotify() {
super.addNotify();
pack();
}


/**
* Aspetta finché il player non è transitato in un dato stato.
* Return false se la transizione è fallita.
*/
boolean waitForState(int state) {
synchronized (waitSync) {
try {
while (p.getState() < state && stateTransitionOK)
waitSync.wait();
} catch (Exception e) {}
}
return stateTransitionOK;
}


public void actionPerformed(ActionEvent ae) {
String command = ae.getActionCommand();
if (command.equals("Grab")){
int frameId=0;
for ( int i=0; i<=totalFrames; i=i+camp) {
frameId++;
// inizialmente mi posiziono sul frame 0 (istante 0)
if (i==0)
fpc.skip(0);
// poi mi posiziono sui frame successivi, secondo il
// campionamento
else
fpc.skip(camp);
// grabba il frame relativo
if (currentFrame !=
FramePositioningControl.FRAME_UNKNOWN) {
currentFrame =
fpc.mapTimeToFrame(p.getMediaTime());
currentTime = fpc.mapFrameToTime(i);
}
Buffer buff = new Buffer();
buff = fgc.grabFrame();
// converte buffer - image
BufferToImage bti = new BufferToImage((VideoFormat)
buff.getFormat());
img = bti.createImage(buff);
String pathFrame = (urlFrames+"/"+currentFrame+".jpg");
File fileFrame = new File(pathFrame);

try {
System.err.println("Sto salvando il frame n." +
currentFrame + " del video " +
" Tempo: " +
currentTime.getSeconds());
ImageSaver(img , fileFrame);
} catch (Exception e) {
System.err.println("Non riesco a salvare il " +
"frame, forse non esiste " +
"la cartella " + e);
}
}
System.err.println("Numero di frames salvati:" + frameId);

} else if (command.equals("Close")) {
this.dispose();
System.exit(0);
}
}

/**
* Controller Listener.
*/
public void controllerUpdate(ControllerEvent evt) {

if (evt instanceof ConfigureCompleteEvent ||
evt instanceof RealizeCompleteEvent ||
evt instanceof PrefetchCompleteEvent) {
synchronized (waitSync) {
stateTransitionOK = true;
waitSync.notifyAll();
}
} else if (evt instanceof ResourceUnavailableEvent) {
synchronized (waitSync) {
stateTransitionOK = false;
waitSync.notifyAll();
}
} else if (evt instanceof EndOfMediaEvent) {
p.setMediaTime(new Time(0));
} else if (evt instanceof SizeChangeEvent) {
}
}

void this_windowClosing(WindowEvent e) {
this.dispose();
}

public void ImageSaver (Image img,File fileFrame) throws IOException {

/**
* Codifica un BufferedImage in formato Jpeg format e lo salva su
* file
* @param img è l'immagine da salvare
* @param fileFrame è un File che decrive dove si vuole salvare
* l'immagine
* @throws java.io.IOException se l'immagine non può essere
* codificata
* e/o salvata
*/

BufferedImage bi=null;
try {
if (!(img instanceof BufferedImage)) {
ImageObserver io=new ImageObserver() {
public boolean imageUpdate(Image im, int info,int
x, int y, int w, int h) {
return false;
}
};
bi = new
BufferedImage(img.getWidth(io),img.getHeight(io),
BufferedImage.TYPE_INT_RGB);
bi.getGraphics().drawImage(img,0,0,io);
img=bi;
} else
bi=(BufferedImage)img;
ImageIO.write(bi, "JPG", fileFrame);
} catch (Exception e) {
throw new IOException("Salvataggio dell'immagine fallito: " +
e);
}

}

/**
* Main program
*/
public static void main(String[] args) {
if (args.length <= 2) {
prUsage();
System.exit(0);
} else {
urlVideo = args[0];
urlFrames = args[1];
camp = new Integer(args[2]).intValue();
MediaLocator ml;
if ((ml = new MediaLocator(urlVideo)) == null)
System.err.println("Impossibile costruire MediaLocator "
+ "dalla url: " + urlVideo);
DataSource ds = null;
// Crea un DataSource da un dato MediaLocator
try {
ds = Manager.createDataSource(ml);
} catch (Exception e) {
System.err.println("Impossibile creare Datasource da: "
+ ml);
System.err.println("Eccezione: "+e);
System.exit(0);
}
VideoGrabbing VideoGrabbing = new VideoGrabbing();
if (!VideoGrabbing.open(ds))
System.exit(0);
}
}

static void prUsage() {
System.err.println("Usage: java VideoGrabbing <url video> " +
"<url frames> <passo di campionamento> ");
System.err.println("Example: java VideoGrabbing " +
"file:c:\\video\\prova.avi c:\\immagini\\prova 5");

}

}

Conclusioni
Con questo articolo abbiamo dato un'altra dimostrazione delle enormi potenzialità di JMF. In particolare abbiamo visto come grabbare un video, cioè poter salvare su file tutte le immagini di cui è composto.
Un'applicazione di questo tipo può essere utile in sistemi di video sorveglianza, in cui si vuole isolare un evento all'interno di un filmato, oppure per tutti quelli che vogliono realizzare un bell'album fotografico dopo aver girato un filmino con la propria videocamera digitale.

 

Bibliografia
[1] "JMF API Specification", java.sun.com/products/java-media/jmf/index.jsp


Vincenzo Viola, n
ato a Formia (LT) il 03/11/1977, è laureato in Ingegneria delle Telecomunicazioni presso l'Università Federico II di Napoli con una tesi sulla segmentazione automatica di video digitali in scene, con sviluppo software implementato su piattaforma Java - Oracle.
Lavora come progettista software per una Mobile Company che offre la sua consulenza ai gestori di telefonia e alle major del settore ICT.



MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it