/* * 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.detect.marker; import android.content.Context; import android.util.Log; import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.core.MatOfPoint; import org.opencv.core.MatOfPoint2f; import org.opencv.core.Rect; import org.opencv.core.RotatedRect; import org.opencv.core.Scalar; import org.opencv.imgproc.Imgproc; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import uk.ac.horizon.artcodes.Feature; 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.Action; import uk.ac.horizon.artcodes.model.Experience; import uk.ac.horizon.artcodes.process.ImageProcessor; import uk.ac.horizon.artcodes.process.ImageProcessorFactory; import uk.ac.horizon.artcodes.scanner.R; /** * This class detects standard Artcodes. */ public class MarkerDetector implements ImageProcessor { private Experience experience; public static class Factory implements ImageProcessorFactory { public String getName() { return "detect"; } public ImageProcessor create(Context context, Experience experience, MarkerDetectionHandler handler, Map<String, String> args) { return new MarkerDetector(experience, handler); } } private enum CodeDisplay { hidden, visible; private static final CodeDisplay[] vals = values(); public CodeDisplay next() { return vals[(this.ordinal() + 1) % vals.length]; } } private enum OutlineDisplay { none, marker, regions; private static final OutlineDisplay[] vals = values(); public OutlineDisplay next() { return vals[(this.ordinal() + 1) % vals.length]; } } static final int NEXT_NODE = 0; static final int FIRST_NODE = 2; private static final Scalar detectedColour = new Scalar(255, 255, 0, 255); private static final Scalar regionColour = new Scalar(255, 128, 0, 255); private static final Scalar outlineColour = new Scalar(0, 0, 0, 255); protected final int checksum; protected final Collection<String> validCodes = new HashSet<>(); protected final int minRegions; protected final int maxRegions; protected final int maxRegionValue; protected final int maxEmptyRegions; protected final boolean ignoreEmptyRegions; private final MarkerDetectionHandler handler; private CodeDisplay codeDisplay = CodeDisplay.hidden; private OutlineDisplay outlineDisplay = OutlineDisplay.none; public MarkerDetector(Experience experience, MarkerDetectionHandler handler) { this.experience = experience; int maxValue = 3; int minRegionCount = 20; int maxRegionCount = 3; int checksum = 0; int maxEmptyRegions = 0; for (Action action : experience.getActions()) { for (String code : action.getCodes()) { int total = 0; String[] values = code.split(":"); minRegionCount = Math.min(minRegionCount, values.length); maxRegionCount = Math.max(maxRegionCount, values.length); int emptyRegions = 0; for (String value : values) { try { int codeValue = Integer.parseInt(value); maxValue = Math.max(maxValue, codeValue); total += codeValue; if (codeValue == 0) { ++emptyRegions; } } catch (Exception e) { Log.w("", e.getMessage(), e); } } maxEmptyRegions = Math.max(maxEmptyRegions, emptyRegions); if (total > 0) { checksum = gcd(checksum, total); } validCodes.add(code); } } this.handler = handler; if (minRegionCount == 20 && maxRegionCount == 3) { minRegionCount = 3; maxRegionCount = 20; maxValue = 20; } this.maxRegionValue = maxValue; this.minRegions = minRegionCount; this.maxRegions = maxRegionCount; this.checksum = checksum; this.maxEmptyRegions = maxEmptyRegions; this.ignoreEmptyRegions = maxEmptyRegions == 0; Log.i("detect", "Regions " + minRegionCount + "-" + maxRegionCount + ", <" + maxValue + ", checksum " + checksum); } private static int gcd(int a, int b) { if (b == 0) { return a; } return gcd(b, a % b); } @Override public void process(ImageBuffers buffers) { final ArrayList<MatOfPoint> contours = new ArrayList<>(); final Mat hierarchy = new Mat(); // Make sure the image is rotated before the contours are generated, if necessary //if (Feature.get(this.context, R.bool.feature_combined_markers).isEnabled() || outlineDisplay != OutlineDisplay.none || codeDisplay == CodeDisplay.visible) //{ // if statement commented out as there was a bug where the overlay was not getting cleared // after cycling through the threshold views. buffers.getOverlay(); //} try { final List<Marker> foundMarkers = new ArrayList<>(); Imgproc.findContours(buffers.getImageInGrey(), contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_NONE); double diagonalScreenSize = Math.sqrt(Math.pow(buffers.getImageInAnyFormat().cols(), 2) + Math.pow(buffers.getImageInAnyFormat().rows(), 2)); for (int i = 0; i < contours.size(); i++) { final Marker marker = createMarkerForNode(i, contours, hierarchy); if (marker != null) { final String markerCode = getCodeKey(marker); if (validCodes.isEmpty() || validCodes.contains(markerCode)) { // If this marker has a minimum size set and is smaller: continue in loop. Action action = experience.getActionForCode(markerCode); if (diagonalScreenSize > 0 && action != null && action.getMinimumSize() != null) { double minimumSize = action.getMinimumSize(); RotatedRect rotatedRect = Imgproc.minAreaRect(new MatOfPoint2f(contours.get(i).toArray())); double markerSize = Math.max(rotatedRect.size.width, rotatedRect.size.height); if (!(markerSize/diagonalScreenSize >= minimumSize)) { continue; } } foundMarkers.add(marker); if (outlineDisplay != OutlineDisplay.none) { Mat overlay = buffers.getOverlay(); if (outlineDisplay == OutlineDisplay.regions) { double[] nodes = hierarchy.get(0, i); int currentRegionIndex = (int) nodes[FIRST_NODE]; while (currentRegionIndex >= 0) { Imgproc.drawContours(overlay, contours, currentRegionIndex, outlineColour, 4); Imgproc.drawContours(overlay, contours, currentRegionIndex, regionColour, 2); nodes = hierarchy.get(0, currentRegionIndex); currentRegionIndex = (int) nodes[NEXT_NODE]; } } Imgproc.drawContours(overlay, contours, i, outlineColour, 7); Imgproc.drawContours(overlay, contours, i, detectedColour, 5); } if (codeDisplay == CodeDisplay.visible) { Mat overlay = buffers.getOverlay(); Rect bounds = Imgproc.boundingRect(contours.get(i)); Imgproc.putText(overlay, markerCode, bounds.tl(), Core.FONT_HERSHEY_SIMPLEX, 1, outlineColour, 5); Imgproc.putText(overlay, markerCode, bounds.tl(), Core.FONT_HERSHEY_SIMPLEX, 1, detectedColour, 3); } } } } buffers.setDetected(!foundMarkers.isEmpty()); handler.onMarkersDetected(foundMarkers, contours, hierarchy, buffers.getImageInGrey().size()); } finally { contours.clear(); hierarchy.release(); } } public String getCodeKey(Marker marker) { sortCode(marker); StringBuilder builder = new StringBuilder(marker.regions.size() * 2); for (MarkerRegion region : marker.regions) { builder.append(region.value); builder.append(':'); } builder.deleteCharAt(builder.length() - 1); return builder.toString(); } @Override public void getSettings(List<DetectorSetting> settings) { settings.add(new DetectorSetting() { @Override public void nextValue() { outlineDisplay = outlineDisplay.next(); } @Override public int getIcon() { switch (outlineDisplay) { case none: return R.drawable.ic_border_clear_24dp; case marker: return R.drawable.ic_border_outer_24dp; case regions: return R.drawable.ic_border_all_24dp; } return 0; } @Override public int getText() { switch (outlineDisplay) { case none: return R.string.draw_marker_off; case marker: return R.string.draw_marker_outline; case regions: return R.string.draw_marker_regions; } return 0; } }); settings.add(new DetectorSetting() { @Override public void nextValue() { codeDisplay = codeDisplay.next(); } @Override public int getIcon() { switch (codeDisplay) { case hidden: return R.drawable.ic_filter_none_black_24dp; case visible: return R.drawable.ic_filter_1_black_24dp; } return 0; } @Override public int getText() { switch (codeDisplay) { case hidden: return R.string.draw_code_off; case visible: return R.string.draw_code; } return 0; } }); } protected boolean isValidDot(int nodeIndex, Mat hierarchy) { double[] nodes = hierarchy.get(0, nodeIndex); return nodes[FIRST_NODE] < 0; } protected Marker createMarkerForNode(int nodeIndex, List<MatOfPoint> contours, Mat hierarchy) { List<MarkerRegion> regions = null; for (int currentNodeIndex = (int) hierarchy.get(0, nodeIndex)[FIRST_NODE]; currentNodeIndex >= 0; currentNodeIndex = (int) hierarchy.get(0, currentNodeIndex)[NEXT_NODE]) { final MarkerRegion region = createRegionForNode(currentNodeIndex, contours, hierarchy); if (region != null) { if (this.ignoreEmptyRegions && region.value == 0) { continue; } else if (regions == null) { regions = new ArrayList<>(); } else if (regions.size() >= maxRegions) { return null; } regions.add(region); } else { return null; } } if (regions != null) { Marker marker = new Marker(nodeIndex, regions); sortCode(marker); if (isValidRegionList(marker)) { return marker; } } return null; } protected MarkerRegion createRegionForNode(int regionIndex, List<MatOfPoint> contours, Mat hierarchy) { // Find the first dot index: double[] nodes = hierarchy.get(0, regionIndex); int currentNodeIndex = (int) nodes[FIRST_NODE]; if (currentNodeIndex < 0 && !(this.ignoreEmptyRegions || this.maxEmptyRegions > 0)) { return null; // There are no dots in this region, and empty regions are not allowed. } // Count all the dots and check if they are leaf nodes in the hierarchy: int dotCount = 0; while (currentNodeIndex >= 0) { if (isValidDot(currentNodeIndex, hierarchy)) { dotCount++; // Get next dot node: nodes = hierarchy.get(0, currentNodeIndex); currentNodeIndex = (int) nodes[NEXT_NODE]; if (dotCount > maxRegionValue) { // Too many dots return null; } } else { // Not a dot return null; } } return new MarkerRegion(regionIndex, dotCount); } /** * Override this method to change the sorted order of the code. */ protected void sortCode(Marker marker) { Collections.sort(marker.regions, new Comparator<MarkerRegion>() { @Override public int compare(MarkerRegion region1, MarkerRegion region2) { return region1.value < region2.value ? -1 : (region1.value == region2.value ? 0 : 1); } }); } /** * Override this method to change validation method. */ protected boolean isValidRegionList(Marker marker) { if (marker.regions == null) { return false; // No CodeDisplay } else if (marker.regions.size() < minRegions) { return false; // Too Short } else if (marker.regions.size() > maxRegions) { return false; // Too long } int numberOfEmptyRegions = 0; for (MarkerRegion region : marker.regions) { //check if leaves are using in accepted range. if (region.value > maxRegionValue) { return false; // value is too Big } else if (region.value == 0 && ++numberOfEmptyRegions > this.maxEmptyRegions) { return false; // too many empty regions } } return hasValidChecksum(marker); } /** * This function divides the total number of leaves in the marker by the * value given in the checksum preference. CodeDisplay is valid if the modulo is 0. * * @return true if the number of leaves are divisible by the checksum value * otherwise false. */ protected boolean hasValidChecksum(Marker marker) { if (checksum <= 1) { return true; } int numberOfLeaves = 0; for (MarkerRegion region : marker.regions) { numberOfLeaves += region.value; } return (numberOfLeaves % checksum) == 0; } }