/*
* ShootOFF - Software for Laser Dry Fire Training
* Copyright (C) 2015 phrack
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.shootoff.camera.shotdetection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PixelClusterManager {
private static final Logger logger = LoggerFactory.getLogger(PixelClusterManager.class);
private int feedWidth;
private int feedHeight;
private final static double MINIMUM_CONNECTEDNESS = 3.66f;
private final static double MAXIMUM_CONNECTEDNESS_SCALE = 6f;
private final static double MINIMUM_CONNECTEDNESS_FACTOR = .018f;
private final static double MINIMUM_DENSITY = .69f;
private final static double MINIMUM_SHOT_RATIO = .5f;
private final static double MAXIMUM_SHOT_RATIO = 1.43f;
// Use different values if shot width + shot height <= 16 px
private final static int SMALL_SHOT_THRESHOLD = 16;
private final static double MINIMUM_SHOT_RATIO_SMALL = .47f;
private final static double MAXIMUM_SHOT_RATIO_SMALL = 1.75f;
private final static int EXCESSIVE_PIXEL_CUTOFF = 300;
private final static int EXCESSIVE_PIXEL_REGION_COUNT = 1;
protected PixelClusterManager(int feedWidth, int feedHeight) {
this.feedWidth = feedWidth;
this.feedHeight = feedHeight;
}
public void updateFrameSize(int feedWidth, int feedHeight) {
this.feedWidth = feedWidth;
this.feedHeight = feedHeight;
}
private int preprocessClusterablePixels(Set<Pixel> clusterablePixels, Map<Pixel, Integer> pixelMapping) {
final Stack<Pixel> mustExamine = new Stack<>();
int numberOfRegions = -1;
for (final Pixel pixel : clusterablePixels) {
if (!pixelMapping.containsKey(pixel)) {
numberOfRegions++;
mustExamine.add(pixel);
pixelMapping.put(pixel, numberOfRegions);
}
if (numberOfRegions > EXCESSIVE_PIXEL_REGION_COUNT && clusterablePixels.size() > EXCESSIVE_PIXEL_CUTOFF)
break;
while (!mustExamine.isEmpty()) {
final Pixel thisPoint = mustExamine.pop();
int connectedness = 0;
for (int h = -1; h <= 1; h++) {
for (int w = -1; w <= 1; w++) {
if (h == 0 && w == 0) continue;
final int rx = thisPoint.x + w;
final int ry = thisPoint.y + h;
if (rx < 0 || ry < 0 || rx >= feedWidth || ry >= feedHeight) continue;
final Pixel nearPoint = new Pixel(rx, ry);
if (clusterablePixels.contains(nearPoint)) {
if (!pixelMapping.containsKey(nearPoint)) {
mustExamine.push(nearPoint);
pixelMapping.put(nearPoint, numberOfRegions);
}
connectedness++;
}
}
}
thisPoint.setConnectedness(connectedness);
}
}
return numberOfRegions;
}
public Set<PixelCluster> clusterPixels(Set<Pixel> clusterablePixels, int minimumShotDimension) {
final Map<Pixel, Integer> pixelMapping = new HashMap<>();
final int numberOfRegions = preprocessClusterablePixels(clusterablePixels, pixelMapping);
final Set<PixelCluster> clusters = new HashSet<>();
for (int i = 0; i <= numberOfRegions; i++) {
final PixelCluster cluster = new PixelCluster();
double averageX = 0;
double averageY = 0;
int minX = feedWidth;
int minY = feedHeight;
int maxX = 0, maxY = 0;
double avgconnectedness = 0;
for (final Entry<Pixel, Integer> pixelEntry : pixelMapping.entrySet()) {
if (pixelEntry.getValue() == i) {
final Pixel nextPixel = pixelEntry.getKey();
if (nextPixel.x < minX)
minX = nextPixel.x;
else if (nextPixel.x > maxX) maxX = nextPixel.x;
if (nextPixel.y < minY)
minY = nextPixel.y;
else if (nextPixel.y > maxY) maxY = nextPixel.y;
cluster.add(nextPixel);
final int connectedness = nextPixel.getConnectedness();
averageX += nextPixel.x * connectedness;
averageY += nextPixel.y * connectedness;
avgconnectedness += connectedness;
}
}
final int clustersize = cluster.size();
if (clustersize < minimumShotDimension) continue;
averageX /= avgconnectedness;
averageY /= avgconnectedness;
avgconnectedness = avgconnectedness / clustersize;
// We scale up the minimum in a linear scale as the cluster size
// increases. This is an approximate density
final double scaled_minimum = Math.min(
MINIMUM_CONNECTEDNESS + ((clustersize - minimumShotDimension) * MINIMUM_CONNECTEDNESS_FACTOR),
MAXIMUM_CONNECTEDNESS_SCALE);
if (logger.isTraceEnabled()) logger.trace("Cluster {}: size {} connectedness {} scaled_minimum {} - {} {}",
i, clustersize, avgconnectedness, scaled_minimum, averageX, averageY);
if (avgconnectedness < scaled_minimum) continue;
final int shotWidth = (maxX - minX) + 1;
final int shotHeight = (maxY - minY) + 1;
final double shotRatio = (double) shotWidth / (double) shotHeight;
if (logger.isTraceEnabled()) logger.trace("Cluster {}: shotRatio {} {} - {} - {} {} {} {}", i, shotWidth,
shotHeight, shotRatio, minX, minY, maxX, maxY);
if ((shotWidth + shotHeight) > SMALL_SHOT_THRESHOLD
&& (shotRatio < MINIMUM_SHOT_RATIO || shotRatio > MAXIMUM_SHOT_RATIO))
continue;
else if (shotRatio < MINIMUM_SHOT_RATIO_SMALL || shotRatio > MAXIMUM_SHOT_RATIO_SMALL) continue;
final double r = (double) (shotWidth + shotHeight) / 4.0f;
final double circleArea = Math.PI * r * r;
final double density = (clustersize) / circleArea;
if (logger.isTraceEnabled()) logger.trace("Cluster {}: density {} {} - {} {} - {}", i, shotWidth,
shotHeight, circleArea, cluster.size(), density);
if (density < MINIMUM_DENSITY) continue;
cluster.centerPixelX = averageX;
cluster.centerPixelY = averageY;
clusters.add(cluster);
}
if (logger.isTraceEnabled())
logger.trace("---- Detected {} shots from {} regions ------", clusters.size(), numberOfRegions + 1);
return clusters;
}
}