package fr.unistra.pelican.algorithms.segmentation.video;
import java.awt.Point;
import java.util.Vector;
import fr.unistra.pelican.Algorithm;
import fr.unistra.pelican.AlgorithmException;
import fr.unistra.pelican.BooleanImage;
import fr.unistra.pelican.Image;
import fr.unistra.pelican.IntegerImage;
import fr.unistra.pelican.algorithms.conversion.RGBToGray;
import fr.unistra.pelican.algorithms.morphology.gray.GrayGradient;
import fr.unistra.pelican.algorithms.segmentation.Watershed;
import fr.unistra.pelican.util.morphology.FlatStructuringElement2D;
/***
* This class is an implementation of the predictive watershed
* presented in Chien, S.Y., Huang, Y.W. and Chen, L.G.,
* Predictive watershed: a fast watershed algorithm for video segmentation.
* IEEE Trans. Circuits Systems Video Technol. vol. 13 n. 5. 453-461.
* @author Jonathan Weber
*
*/
public class PredictiveWatershed extends Algorithm {
public final static int CONNEXITY4 = 4;
public final static int CONNEXITY8 = 8;
/**
* Density image used by the watershed (i.e. gradient image)
*/
public Image originalVideo;
/**
* Watershed result
*/
public IntegerImage outputVideo;
private Image currentFrame;
private Image correspondingFrame;
private Image currentGradient;
private IntegerImage currentLabelImage;
private BooleanImage updatingAreaMask;
private BooleanImage se;
/**
* Connexity used for the gradient computing
*/
public int connexity = PredictiveWatershed.CONNEXITY8;
/**
* Size of the square blocks
*/
public int blockSize = 8;
/**
* Threshold used for the block-based change detection step
*/
public int blockChangeDetectionThreshold = blockSize*blockSize*30;
public PredictiveWatershed()
{
super();
super.inputs = "originalVideo";
super.options = "blockSize,blockChangeDetectionThreshold,connexity";
super.outputs = "outputVideo";
}
public void launch() throws AlgorithmException
{
if(connexity==CONNEXITY8)
{
se = FlatStructuringElement2D.createSquareFlatStructuringElement(3);
}
else
{
se = FlatStructuringElement2D.createCrossFlatStructuringElement(1);
}
//Video conversion in gray if color
if(originalVideo.getBDim()==3)
originalVideo = RGBToGray.exec(originalVideo);
outputVideo = new IntegerImage(originalVideo.getXDim(),originalVideo.getYDim(),1,originalVideo.getTDim(),1);
//First frame
correspondingFrame = originalVideo.getImage4D(0,Image.T);
currentGradient = GrayGradient.exec(correspondingFrame,se);
currentLabelImage = (IntegerImage) Watershed.exec(currentGradient);
outputVideo.setImage4D(currentLabelImage, 0, Image.T);
//Other frame
for(int frame = 1; frame<originalVideo.getTDim(); frame++)
{
currentFrame = originalVideo.getImage4D(frame, Image.T);
updatingAreaMask = createUAM();
updateGradient();
currentLabelImage = new WatershedInUpdatedAreas(currentGradient,currentLabelImage,updatingAreaMask).exec();
outputVideo.setImage4D(currentLabelImage, frame, Image.T);
}
}
//TODO : Gestion des bords
private BooleanImage createUAM()
{
BooleanImage uAM = new BooleanImage (originalVideo.getXDim(),originalVideo.getYDim(),1,1,1);
uAM.fill(false);
int xDim = originalVideo.getXDim();
int yDim = originalVideo.getYDim();
for(int xBlock=0;(xBlock+1)*blockSize-1<xDim;xBlock++)
for(int yBlock=0;(yBlock+1)*blockSize-1<yDim;yBlock++)
{
int absoluteDifference = 0;
for(int xImage=xBlock*blockSize;xImage<(xBlock+1)*blockSize;xImage++)
for(int yImage=yBlock*blockSize;yImage<(yBlock+1)*blockSize;yImage++)
{
absoluteDifference+= Math.abs(correspondingFrame.getPixelXYByte(xImage,yImage)-currentFrame.getPixelXYByte(xImage,yImage));
}
if(absoluteDifference>blockChangeDetectionThreshold)
{
for(int xImage=xBlock*blockSize;xImage<(xBlock+1)*blockSize;xImage++)
for(int yImage=yBlock*blockSize;yImage<(yBlock+1)*blockSize;yImage++)
uAM.setPixelXYBoolean(xImage, yImage, true);
}
}
return uAM;
}
private void updateGradient()
{
Image updatingGradient = GrayGradient.exec(currentFrame, se, updatingAreaMask);
for(int pixel=0; pixel<currentGradient.size(); pixel++)
if(updatingAreaMask.getPixelBoolean(pixel))
currentGradient.setPixelByte(pixel,updatingGradient.getPixelByte(pixel));
}
public static IntegerImage exec(Image video) {
return (IntegerImage)new PredictiveWatershed().process(video);
}
public static IntegerImage exec(Image video, int blockSize, int blockChangeDetectionThreshold)
{
return (IntegerImage)new PredictiveWatershed().process(video, blockSize, blockChangeDetectionThreshold);
}
/*
public static void main(String[] args) {
//Image original = PelicanImageLoad.exec("C:\\Documents and Settings\\Jonathan.weber\\Mes documents\\videos\\segmentation_tests\\lambor_global.pelican");
Image original = VideoLoader.exec("../pelican2/samples/sample.avi");
ViewerVideo.exec(LabelsToRandomColors.exec(PredictiveWatershed.exec(original)));
}
*/
private class WatershedInUpdatedAreas{
private Image gradientImage;
private IntegerImage previousWatershed;
private BooleanImage updatingAreaMask;
private Image watershed;
/**
* A constant to represent watershed lines
*/
private static final int WSHED = 0;
/*
* Private attributes
*/
private static final int INIT = -1;
private static final int MASK = -2;
private final Point fictitious = new Point(-1, -1);
/**
* Constructor
*/
WatershedInUpdatedAreas(Image gradientImage, IntegerImage previousWatershed, BooleanImage updatingAreaMask)
{
this.gradientImage = gradientImage;
this.previousWatershed = previousWatershed;
this.updatingAreaMask = updatingAreaMask;
this.exec();
}
/*
* (non-Javadoc)
*
* @see fr.unistra.pelican.Algorithm#launch()
*/
public IntegerImage exec() throws AlgorithmException {
IntegerImage work = new IntegerImage(previousWatershed.getXDim(), previousWatershed.getYDim(), 1, 1, 1);
IntegerImage dist = new IntegerImage(previousWatershed.getXDim(), previousWatershed.getYDim(), 1, 1, 1);
IntegerImage workOut = new IntegerImage(previousWatershed.getXDim(), gradientImage.getYDim(), 1, 1, 1);
watershed = new IntegerImage(previousWatershed, false);
for (int x = 0; x < gradientImage.getXDim(); x++)
for (int y = 0; y < gradientImage.getYDim(); y++)
// That's a nice hack, isn't it? No Byte to Integer
// conversion.
// Work still have values from 0 to 255.
if(updatingAreaMask.getPixelXYBoolean(x,y))
{
work.setPixelInt(x, y, 0, 0, 0, gradientImage.getPixelByte(x, y, 0, 0, 0));
}
int currentLabel = WSHED;
dist.fill(0);
Fifo fifo = new Fifo();
Point p;
// Initialise the workout image and the current label
workOut.fill(INIT);
for(int i=0;i<workOut.size();i++)
if(!updatingAreaMask.getPixelBoolean(i))
{
workOut.setPixelInt(i, previousWatershed.getPixelInt(i));
if(workOut.getPixelInt(i)>currentLabel)
{
currentLabel=workOut.getPixelInt(i);
}
}
// pixel value distribution,
Vector[] distro = calculateDistro(work);
// start flooding
for (int i = 0; i < 256; i++) {
// geodesic SKIZ of level i - 1 inside level i
int size = distro[i].size();
for (int j = 0; j < size; j++)
{
p = (Point) distro[i].elementAt(j);
workOut.setPixelXYInt(p.x, p.y, MASK);
if (areThereLabelledNeighbours(workOut, p.x, p.y) == true)
{
dist.setPixelXYInt(p.x, p.y, 1);
fifo.add(p);
}
}
int curDist = 1;
fifo.add(fictitious);
do {
p = (Point) fifo.retrieve();
if (p.x == -1 && p.y == -1)
{
if (fifo.isEmpty() == true)
break;
else
{
fifo.add(fictitious);
curDist++;
p = fifo.retrieve();
}
}
// labelling p by inspecting its neighbours
for (int j = p.y - 1; j <= p.y + 1; j++)
{
for (int k = p.x - 1; k <= p.x + 1; k++)
{
if (k < 0 || k >= gradientImage.getXDim()
|| j < 0
|| j >= gradientImage.getYDim())
continue;
// if the pixel is
// already labelled
if (!(j == p.y && k == p.x)
&& dist.getPixelXYInt(k, j) < curDist
&& workOut.getPixelXYInt(k, j) > WSHED)
{
if (workOut.getPixelXYInt(k, j) > 0)
{
if (workOut.getPixelXYInt(p.x, p.y) == MASK
|| workOut.getPixelXYInt(p.x, p.y) == WSHED)
{
workOut.setPixelXYInt(p.x, p.y,
workOut.getPixelXYInt(
k, j));
}
else if (workOut.getPixelXYInt(p.x,
p.y) != workOut
.getPixelXYInt(k, j))
{
workOut.setPixelXYInt(p.x, p.y, WSHED);
}
} else if (workOut.getPixelXYInt(p.x,
p.y) == MASK)
{
workOut.setPixelXYInt(p.x, p.y, WSHED);
}
// if the neighbour is a plateau pixel
} else if (workOut.getPixelXYInt(k, j) == MASK
&& dist.getPixelXYInt(k, j) == 0)
{
dist.setPixelXYInt(k, j, curDist + 1);
fifo.add(new Point(k, j));
}
}
}
} while (true);
// check for new minima
size = distro[i].size();
// detect and process new minima at level i
for (int j = 0; j < size; j++) {
p = (Point) distro[i].elementAt(j);
// reset distance to 0
dist.setPixelXYInt(p.x, p.y, 0);
// if p is inside a new minimum
if (workOut.getPixelXYInt(p.x, p.y) == MASK) {
// create a new label
currentLabel++;
fifo.add(p);
workOut.setPixelXYInt(p.x, p.y, currentLabel);
while (fifo.isEmpty() == false) {
Point q = fifo.retrieve();
// for every pixel in the 8-neighbourhood of
// q
for (int l = q.y - 1; l <= q.y + 1; l++) {
for (int k = q.x - 1; k <= q.x + 1; k++) {
if (k < 0
|| k >= previousWatershed.getXDim()
|| l < 0
|| l >= previousWatershed.getYDim())
continue;
if (!(k == q.x && l == q.y)
&& workOut.getPixelXYInt(k,
l) == MASK) {
fifo.add(new Point(k, l));
workOut.setPixelXYInt(k, l,
currentLabel);
}
}
}
}
}
}
// Copy the result to the outputImage
for (int _x = 0; _x < gradientImage.getXDim(); _x++)
for (int _y = 0; _y < gradientImage.getYDim(); _y++) {
// That's a nice hack, isn't it? No Integer to
// Byte
// conversion.
// Values are inside [0,255] if the algo is
// correct.
watershed.setPixelInt(_x, _y, 0, 0, 0, workOut.getPixelInt(_x, _y, 0, 0, 0));
}
}
return (IntegerImage) watershed;
}
private Vector[] calculateDistro(IntegerImage img) {
Vector[] distro = new Vector[256];
for (int i = 0; i < 256; i++)
distro[i] = new Vector();
for (int x = 0; x < img.getXDim(); x++) {
for (int y = 0; y < img.getYDim(); y++)
if(updatingAreaMask.getPixelXYBoolean(x,y))
distro[img.getPixelXYInt(x, y)].add(new Point(x, y));
}
return distro;
}
private boolean areThereLabelledNeighbours(IntegerImage img, int x, int y)
throws AlgorithmException {
for (int j = y - 1; j <= y + 1; j++) {
if (j >= img.getYDim() || j < 0)
continue;
for (int i = x - 1; i <= x + 1; i++) {
if (i >= img.getXDim() || i < 0)
continue;
// try{
if(updatingAreaMask.getPixelXYBoolean(i,j))
if (!(i == x && j == y) && img.getPixelXYInt(i, j) >= WSHED)
return true;
}
}
return false;
}
private class Fifo {
private Vector<Object> v;
Fifo() {
v = new Vector<Object>();
}
void add(Object o) {
v.add(o);
}
Point retrieve() {
Object o = v.firstElement();
v.remove(0);
return (Point) o;
}
boolean isEmpty() {
return (v.size() == 0);
}
}
}
}