/*
* Artcodes recognises a different marker scheme that allows the
* creation of aesthetically pleasing, even beautiful, codes.
* Copyright (C) 2013-2016 The University of Nottingham
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package uk.ac.horizon.artcodes.process;
import android.content.Context;
import android.util.Log;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfInt;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import uk.ac.horizon.artcodes.detect.DetectorSetting;
import uk.ac.horizon.artcodes.detect.ImageBuffers;
import uk.ac.horizon.artcodes.detect.handler.MarkerDetectionHandler;
import uk.ac.horizon.artcodes.model.Experience;
/**
* This class contains a few implementations of reducing a colour image to greyscale by taking a
* single channel. Use the ImageProcessorFactory classes to get the required ImageProcessor.
* The MixChannels implementation seems to be the fastest, other implementations left here for
* reference.
*/
public class RgbColourFilter
{
public enum Channel
{
red, green, blue
}
public static class RedFactory implements ImageProcessorFactory
{
public String getName()
{
return "redFilter";
}
public ImageProcessor create(Context context, Experience experience, MarkerDetectionHandler handler, Map<String, String> args)
{
return new RgbColourFilter_MixChannelsImpl(Channel.red);
}
}
public static class GreenFactory implements ImageProcessorFactory
{
public String getName()
{
return "greenFilter";
}
public ImageProcessor create(Context context, Experience experience, MarkerDetectionHandler handler, Map<String, String> args)
{
return new RgbColourFilter_MixChannelsImpl(Channel.green);
}
}
public static class BlueFactory implements ImageProcessorFactory
{
public String getName()
{
return "blueFilter";
}
public ImageProcessor create(Context context, Experience experience, MarkerDetectionHandler handler, Map<String, String> args)
{
return new RgbColourFilter_MixChannelsImpl(Channel.blue);
}
}
/**
* This implementation uses OpenCV's mixChannels. It converts to BGR and then uses mixChannels
* to separate the channels, a 2 channel buffer is require so mixChannels has somewhere to put
* the undesired channels (crashes without it).
*
* It appears to be the fastest (average 50ms/frame on Nexus S).
*/
public static class RgbColourFilter_MixChannelsImpl implements ImageProcessor
{
private final Channel channel;
public Channel getChannel()
{
return channel;
}
public RgbColourFilter_MixChannelsImpl(Channel channel)
{
this.channel = channel;
}
private Mat extraChannelMat;
private MatOfInt mix;
@Override
public void process(ImageBuffers buffers)
{
Mat greyscaleImage = buffers.getGreyBuffer();
Mat colorImage = buffers.getImageInBgr();
/*
Imgproc.putText(colorImage, "RED", new Point(40,40), Core.FONT_HERSHEY_PLAIN, 2, new Scalar(0,0,255));
Imgproc.putText(colorImage, "GREEN", new Point(60,60), Core.FONT_HERSHEY_PLAIN, 2, new Scalar(0,255,0));
Imgproc.putText(colorImage, "BLUE", new Point(80,80), Core.FONT_HERSHEY_PLAIN, 2, new Scalar(255,0,0));
*/
List<Mat> src = new ArrayList<>(1), dst = new ArrayList<>(1);
src.add(colorImage);
dst.add(greyscaleImage);
if (mix==null)
{
if (colorImage.channels()==3)
{
if (channel == Channel.red)
{
// mix is a list of pairs that tell openCV to map input channel to output channels
mix = new MatOfInt(2, 0, 1, 1, 0, 2);
}
else if (channel == Channel.green)
{
mix = new MatOfInt(2, 1, 1, 0, 0, 2);
}
else if (channel == Channel.blue)
{
mix = new MatOfInt(2, 2, 1, 1, 0, 0);
}
}
else if (colorImage.channels()==4)
{
if (channel == Channel.red)
{
mix = new MatOfInt(2, 0, 1, 1, 0, 2, 3, 3);
}
else if (channel == Channel.green)
{
mix = new MatOfInt(2, 1, 1, 0, 0, 2, 3, 3);
}
else if (channel == Channel.blue)
{
mix = new MatOfInt(2, 2, 1, 1, 0, 0, 3, 3);
}
}
else
{
Log.w(getClass().getSimpleName(), "Colour image has unsupported number of channels: "+colorImage.channels());
}
}
if (extraChannelMat == null)
{
if (colorImage.channels()==3)
{
extraChannelMat = new Mat(greyscaleImage.rows(), greyscaleImage.cols(), CvType.CV_8UC2);
}
else if (colorImage.channels()==4)
{
extraChannelMat = new Mat(greyscaleImage.rows(), greyscaleImage.cols(), CvType.CV_8UC3);
}
}
dst.add(extraChannelMat);
Core.mixChannels(src, dst, mix);
buffers.setImage(greyscaleImage);
}
@Override
public void getSettings(List<DetectorSetting> settings)
{
}
}
/**
* This implementation used OpenCV's split. Split seems to always re-allocate Mats even if you
* populate the destination list so you either have to copy the data or use the Mat it creates.
*
* It's pretty fast (average 60ms/frame on Nexus S).
*/
public static class RgbColourFilter_SplitImpl implements ImageProcessor
{
private final Channel channel;
public RgbColourFilter_SplitImpl(Channel channel)
{
this.channel = channel;
}
@Override
public void process(ImageBuffers buffers)
{
Mat greyscaleImage = buffers.getGreyBuffer();
Mat colorImage = buffers.getImageInBgr();
List<Mat> dst = new ArrayList<>(3);
Core.split(colorImage, dst);
// Note: Split seems to always re-allocate Mats even if you populate the list. :(
if (channel == Channel.red)
{
dst.get(2).copyTo(greyscaleImage);
}
else if (channel == Channel.green)
{
dst.get(1).copyTo(greyscaleImage);
}
else if (channel == Channel.blue)
{
dst.get(0).copyTo(greyscaleImage);
}
buffers.setImage(greyscaleImage);
}
@Override
public void getSettings(List<DetectorSetting> settings)
{
// TODO
}
}
/**
* Convert YUV data straight to a single channel.
*
* It's pretty fast (average 70ms/frame on Nexus S).
*/
public static class RgbColourFilter_YUV2ChannelImpl implements ImageProcessor
{
private final Channel channel;
private final UvProcessor uvProcessor;
private static interface UvProcessor
{
int process(byte[] uvData, int index);
}
public RgbColourFilter_YUV2ChannelImpl(Channel channel)
{
this.channel = channel;
if (channel==Channel.red)
{
this.uvProcessor = new UvProcessor()
{
@Override
public int process(byte[] uvData, int index)
{
return (int) (1.370705 * ((uvData[index] & 0xFF) - 128));
}
};
}
else if (channel==Channel.green)
{
this.uvProcessor = new UvProcessor()
{
@Override
public int process(byte[] uvData, int index)
{
return (int) (- (0.698001 * ((uvData[index] & 0xFF) - 128)) - (0.337633 * ((uvData[index+1] & 0xFF) - 128)));
}
};
}
else // if (channel==Channel.blue)
{
this.uvProcessor = new UvProcessor()
{
@Override
public int process(byte[] uvData, int index)
{
return (int) (1.732446 * ((uvData[index+1] & 0xFF) - 128));
}
};
}
}
private byte[] yRow1;
private byte[] yRow2;
private byte[] uvRow;
@Override
public void process(ImageBuffers buffers)
{
Mat greyscaleImage = buffers.getGreyBuffer();
Mat yuvImage = buffers.getImageInYuv();
final int yBufferSize = yuvImage.cols();
final int uvBufferSize = yuvImage.cols();
if (yRow1 == null || yRow1.length != yBufferSize)
yRow1 = new byte[yBufferSize];
if (yRow2 == null || yRow2.length != yBufferSize)
yRow2 = new byte[yBufferSize];
if (uvRow == null || uvRow.length != uvBufferSize)
uvRow = new byte[uvBufferSize];
final int totalUvRows = yuvImage.rows() / 3;
final int uvRowOffset = totalUvRows * 2;
final int width = yuvImage.cols();
int uvRowIndex = 0;
while (uvRowIndex<totalUvRows)
{
yuvImage.get(uvRowIndex*2, 0, yRow1);
yuvImage.get(uvRowIndex*2+1, 0, yRow2);
yuvImage.get(uvRowIndex+uvRowOffset, 0, uvRow);
int uvColIndex = 0, uvComponent = 0;
while (uvColIndex < width)
{
//uvComponent = (int) (1.370705 * ((uvRow[uvColIndex] & 0xFF) - 128));
uvComponent = this.uvProcessor.process(uvRow, uvColIndex);
yRow1[uvColIndex] = (byte) ((yRow1[uvColIndex] & 0xFF) + uvComponent);
yRow1[uvColIndex + 1] = (byte) ((yRow1[uvColIndex + 1] & 0xFF) + uvComponent);
yRow2[uvColIndex] = (byte) ((yRow2[uvColIndex] & 0xFF) + uvComponent);
yRow2[uvColIndex + 1] = (byte) ((yRow2[uvColIndex + 1] & 0xFF) + uvComponent);
uvColIndex += 2;
}
greyscaleImage.put(uvRowIndex*2, 0, yRow1);
greyscaleImage.put(uvRowIndex*2+1, 0, yRow2);
++uvRowIndex;
}
buffers.setImage(greyscaleImage);
}
@Override
public void getSettings(List<DetectorSetting> settings)
{
}
}
/**
* This is the implementation from Storicodes, it converts to BGR and then uses a single byte[]
* buffer to write the chosen channel back to the grey buffer Mat.
*
* It's reasonably fast (average 75ms/frame on Nexus S).
*/
public static class RgbColourFilter_StoricodesImpl implements ImageProcessor
{
private final Channel channel;
public RgbColourFilter_StoricodesImpl(Channel channel)
{
this.channel = channel;
}
private byte[] pixelBuffer;
@Override
public void process(ImageBuffers buffers)
{
Mat greyscaleImage = buffers.getGreyBuffer();
Mat colorImage = buffers.getImageInBgr();
int desiredColorBufferSize = colorImage.rows() * colorImage.cols() * colorImage.channels();
if (pixelBuffer == null || pixelBuffer.length < desiredColorBufferSize)
pixelBuffer = new byte[desiredColorBufferSize]; //bufferManager.getByteArray(desiredColorBufferSize);
int desiredGreyBufferSize = colorImage.rows() * colorImage.cols();
colorImage.get(0, 0, pixelBuffer);
int c = channel == Channel.red ? 2 : (channel == Channel.green ? 1 : 0), g = 0, channels = colorImage.channels();
while (g < desiredGreyBufferSize)
{
pixelBuffer[g] = pixelBuffer[c];
++g;
c += channels;
}
greyscaleImage.put(0, 0, pixelBuffer);
//bufferManager.returnByteArray(pixelBuffer);
buffers.setImage(greyscaleImage);
}
@Override
public void getSettings(List<DetectorSetting> settings)
{
}
}
}