/* * 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 org.opencv.core.Mat; 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; public class CmykColourFilter { public enum Channel { cyan, magenta, yellow, black } public static class CyanCmykColourFilterFactory implements ImageProcessorFactory { public String getName() { return "cyanKFilter"; } public ImageProcessor create(Context context, Experience experience, MarkerDetectionHandler handler, Map<String, String> args) { return new CmykColourFilter_UpdatedCMYImpl(Channel.cyan); } } public static class MagentaCmykColourFilterFactory implements ImageProcessorFactory { public String getName() { return "magentaKFilter"; } public ImageProcessor create(Context context, Experience experience, MarkerDetectionHandler handler, Map<String, String> args) { return new CmykColourFilter_UpdatedCMYImpl(Channel.magenta); } } public static class YellowCmykColourFilterFactory implements ImageProcessorFactory { public String getName() { return "yellowKFilter"; } public ImageProcessor create(Context context, Experience experience, MarkerDetectionHandler handler, Map<String, String> args) { return new CmykColourFilter_UpdatedCMYImpl(Channel.yellow); } } public static class BlackCmykColourFilterFactory implements ImageProcessorFactory { public String getName() { return "blackKFilter"; } public ImageProcessor create(Context context, Experience experience, MarkerDetectionHandler handler, Map<String, String> args) { return new CmykColourFilter_UpdatedKImpl(); } } /** * This is the implementation from Storicodes. * * It's very slow (average 490ms/frame on Nexus S). Possibly due to unnecessary use of floats * and using Mat.put when greyscale buffer is not continuous (as it's a sub-Mat of a YUV image * here, I think it is continuous in Storicodes). */ public static class CmykColourFilter_StoricodesImpl implements ImageProcessor { private final Channel channel; public CmykColourFilter_StoricodesImpl(Channel channel) { this.channel = channel; } private byte[] pixelBuffer; @Override public void process(ImageBuffers buffers) { Mat greyscaleImage = buffers.getGreyBuffer(); Mat colorImage = buffers.getImageInBgr(); int desiredBufferSize = colorImage.rows() * colorImage.cols() * colorImage.channels(); if (pixelBuffer==null || pixelBuffer.length<desiredBufferSize) { pixelBuffer = new byte[desiredBufferSize]; } colorImage.get(0, 0, pixelBuffer); if (this.channel==Channel.black) // k only { for (int i = 0, j = 0; i < desiredBufferSize; i += 3, ++j) { pixelBuffer[j] = (byte) Math.min(255 - (pixelBuffer[i] & 0xFF), Math.min(255 - (pixelBuffer[i+1] & 0xFF), 255 - (pixelBuffer[i+2] & 0xFF))); } } else // c, y or m only { int channelIndex = channel==Channel.cyan ? 2 : (channel==Channel.magenta ? 1 : 0); float k; float[] bgr = new float[3]; for (int i = 0, j = 0; i < desiredBufferSize; i += 3, ++j) { bgr[0] = (pixelBuffer[i] & 0xFF) / 255f; bgr[1] = (pixelBuffer[i + 1] & 0xFF) / 255f; bgr[2] = (pixelBuffer[i + 2] & 0xFF) / 255f; k = Math.min(1f - bgr[0], Math.min(1f - bgr[1], 1f - bgr[2])); float result = k==1 ? 0 : (1f - bgr[channelIndex] - k); pixelBuffer[j] = (byte) (result*255f); } } greyscaleImage.put(0, 0, pixelBuffer); buffers.setImage(greyscaleImage); } @Override public void getSettings(List<DetectorSetting> settings) { } } /** * This is an updated version of the Storicodes implementation that just uses integers and * only handles cyan, magenta and yellow. * * It's slow (average 100ms/frame on Nexus S). */ public static class CmykColourFilter_UpdatedCMYImpl implements ImageProcessor { private final Channel channel; private final int channelIndex; public CmykColourFilter_UpdatedCMYImpl(Channel channel) { this.channel = channel; this.channelIndex = channel==Channel.cyan ? 2 : (channel==Channel.magenta ? 1 : 0); } private byte[] colorPixelBuffer, rowBuffer; @Override public void process(ImageBuffers buffers) { Mat greyscaleImage = buffers.getGreyBuffer(); Mat colorImage = buffers.getImageInBgr(); int desiredBufferSize = colorImage.rows() * colorImage.cols() * colorImage.channels(); int desiredRowBufferSize = greyscaleImage.cols(); if (colorPixelBuffer ==null || colorPixelBuffer.length<desiredBufferSize) { colorPixelBuffer = new byte[desiredBufferSize]; } if (rowBuffer==null || rowBuffer.length!=desiredBufferSize) { rowBuffer = new byte[desiredRowBufferSize]; } colorImage.get(0, 0, colorPixelBuffer); int k, result; int colorPixelBufferIndex = 0, colIndex = 0, rowIndex = 0; while (colorPixelBufferIndex < desiredBufferSize) { colIndex = 0; while (colIndex < desiredRowBufferSize) { k = 255 - Math.max(colorPixelBuffer[colorPixelBufferIndex] & 0xFF, Math.max(colorPixelBuffer[colorPixelBufferIndex + 1] & 0xFF, colorPixelBuffer[colorPixelBufferIndex + 2] & 0xFF)); result = k == 255 ? 0 : (255 - (colorPixelBuffer[colorPixelBufferIndex + channelIndex] & 0xFF) - k); rowBuffer[colIndex] = (byte) (result > 255 ? 255 : (result < 0 ? 0 : result)); colorPixelBufferIndex += 3; ++colIndex; } greyscaleImage.put(rowIndex++, 0, rowBuffer); } buffers.setImage(greyscaleImage); } @Override public void getSettings(List<DetectorSetting> settings) { } } /** * This is an updated version of the Storicodes implementation that just uses integers and * only handles black. * * It's reasonably fast (average 75ms/frame on Nexus S). */ public static class CmykColourFilter_UpdatedKImpl implements ImageProcessor { private byte[] pixelBuffer, rowBuffer; @Override public void process(ImageBuffers buffers) { Mat greyscaleImage = buffers.getGreyBuffer(); Mat colorImage = buffers.getImageInBgr(); final int desiredBufferSize = colorImage.rows() * colorImage.cols() * colorImage.channels(); final int desiredRowBufferSize = greyscaleImage.cols(); if (pixelBuffer==null || pixelBuffer.length<desiredBufferSize) { pixelBuffer = new byte[desiredBufferSize]; } if (rowBuffer==null || rowBuffer.length!=desiredBufferSize) { rowBuffer = new byte[desiredRowBufferSize]; } colorImage.get(0, 0, pixelBuffer); final int rows = colorImage.rows(); final int colorChannels = colorImage.channels(); int colorImageIndex = 0; for (int rowIndex = 0; rowIndex < rows; ++rowIndex) { for (int colIndex = 0; colIndex < desiredRowBufferSize; ++colIndex) { rowBuffer[colIndex] = (byte) (255 - Math.max(pixelBuffer[colorImageIndex] & 0xFF, Math.max(pixelBuffer[colorImageIndex + 1] & 0xFF, pixelBuffer[colorImageIndex + 2] & 0xFF))); colorImageIndex += colorChannels; } greyscaleImage.put(rowIndex, 0, rowBuffer); } buffers.setImage(greyscaleImage); } @Override public void getSettings(List<DetectorSetting> settings) { } } }