From b515d9e9b28b64e2682e804a39d0a8f1d32aa40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Eisfeld?= Date: Wed, 24 Feb 2016 22:41:14 +0100 Subject: [PATCH] Copied PupilAndIrisDetector to Android project --- .../util/imagefile/PupilAndIrisDetector.java | 293 ++---- .../activities/CameraActivity.java | 3 + .../activities/OrganizeNewPhotosActivity.java | 4 + .../components/OverlayPinchImageView.java | 100 +- .../fragments/DisplayImageFragment.java | 7 + .../util/imagefile/FileUtil.java | 13 +- .../util/imagefile/ImageUtil.java | 46 +- .../util/imagefile/JpegMetadata.java | 48 +- .../util/imagefile/PupilAndIrisDetector.java | 991 ++++++++++++++++++ 9 files changed, 1255 insertions(+), 250 deletions(-) create mode 100644 AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/PupilAndIrisDetector.java diff --git a/AugendiagnoseFX/src/de/eisfeldj/augendiagnosefx/util/imagefile/PupilAndIrisDetector.java b/AugendiagnoseFX/src/de/eisfeldj/augendiagnosefx/util/imagefile/PupilAndIrisDetector.java index d331eb10..d2334939 100644 --- a/AugendiagnoseFX/src/de/eisfeldj/augendiagnosefx/util/imagefile/PupilAndIrisDetector.java +++ b/AugendiagnoseFX/src/de/eisfeldj/augendiagnosefx/util/imagefile/PupilAndIrisDetector.java @@ -18,27 +18,23 @@ public class PupilAndIrisDetector { /** * The resolution of the image when searching for a point within the pupil. */ - private static final int[] PUPIL_SEARCH_RESOLUTIONS = {100, 200, 600, 1800}; - /** - * The resolution of the image when searching for a point within the pupil. - */ - private static final int[] IRIS_SEARCH_RESOLUTIONS = {100}; + private static final int[] PUPIL_SEARCH_RESOLUTIONS = {100, 200, 600}; /** * The size of the maximum change distance from one zone (pupil/iris/outer) to the next, relative to the image size. */ - private static final double MAX_LEAP_WIDTH = 0.05; + private static final float MAX_LEAP_WIDTH = 0.05f; /** * The minimum brightness difference accepted as a leap. */ - private static final double MIN_LEAP_DIFF = 0.05; + private static final float MIN_LEAP_DIFF = 0.05f; /** * The minimum pupil radius, relative to the image size. */ - private static final double MIN_PUPIL_RADIUS = 0.04; + private static final float MIN_PUPIL_RADIUS = 0.04f; /** * The minimum distance between iris and pupil, relative to the image size. */ - private static final double MIN_IRIS_PUPIL_DISTANCE = 0.1; + private static final float MIN_IRIS_PUPIL_DISTANCE = 0.1f; /** * The maximum steps of position refinement that should be done at each resolution. */ @@ -46,51 +42,51 @@ public class PupilAndIrisDetector { /** * The brightness of the pupil assumed when calculating the leaps. */ - private static final double ASSUMED_PUPIL_BRIGHTNESS = 0.3; + private static final float ASSUMED_PUPIL_BRIGHTNESS = 0.3f; /** * The minimum white quota expected outside the iris. */ - private static final double MIN_WHITE_QUOTA = 0.3; + private static final float MIN_WHITE_QUOTA = 0.3f; /** * The secondary minimum white quota expected outside the iris. */ - private static final double MIN_WHITE_QUOTA2 = 0.7; + private static final float MIN_WHITE_QUOTA2 = 0.7f; /** * The minimum black quota expected within the pupil. */ - private static final double MIN_BLACK_QUOTA = 0.7; + private static final float MIN_BLACK_QUOTA = 0.7f; /** * The maximum black quota expected outside the pupil. */ - private static final double MAX_BLACK_QUOTA = 0.3; + private static final float MAX_BLACK_QUOTA = 0.3f; /** * The vertical range where iris boundary points should be searched for. */ - private static final double IRIS_BOUNDARY_SEARCH_RANGE = 0.7; + private static final float IRIS_BOUNDARY_SEARCH_RANGE = 0.7f; /** * The uncertainty of the positions of the iris boundary points. */ - private static final double IRIS_BOUNDARY_UNCERTAINTY_FACTOR = 0.2; + private static final float IRIS_BOUNDARY_UNCERTAINTY_FACTOR = 0.2f; /** * The minimum range considered when determining the iris boundary. */ - private static final double IRIS_BOUNDARY_MIN_RANGE = 0.02; + private static final float IRIS_BOUNDARY_MIN_RANGE = 0.02f; /** * Factor by which the range is changed with each retry after a search failure. */ - private static final double IRIS_BOUNDARY_RETRY_FACTOR = 0.7; + private static final float IRIS_BOUNDARY_RETRY_FACTOR = 0.7f; /** * The quota of points that are allowed to be too bright in the iris or too dark outside the iris. */ - private static final double IRIS_BOUNDARY_WRONG_BRIGHTNESS_QUOTA = 0.2; + private static final float IRIS_BOUNDARY_WRONG_BRIGHTNESS_QUOTA = 0.2f; /** * The quota of points around the center considered for determining the vertical center. */ - private static final double IRIS_BOUNDARY_POINTS_CONSIDERED_FOR_YCENTER = 0.3; + private static final float IRIS_BOUNDARY_POINTS_CONSIDERED_FOR_YCENTER = 0.3f; /** * The minimum number of boundary points needed to refine the iris position. */ - private static final double IRIS_BOUNDARY_MIN_BOUNDARY_POINTS = 10; + private static final float IRIS_BOUNDARY_MIN_BOUNDARY_POINTS = 10; /** * The image to be analyzed. @@ -100,54 +96,54 @@ public class PupilAndIrisDetector { /** * The horizontal center of the pupil (in the interval [0,1]). */ - private double mPupilXCenter = 0; + private float mPupilXCenter = 0; - public final double getPupilXCenter() { + public final float getPupilXCenter() { return mPupilXCenter; } /** * The vertical center of the pupil (in the interval [0,1]). */ - private double mPupilYCenter = 0; + private float mPupilYCenter = 0; - public final double getPupilYCenter() { + public final float getPupilYCenter() { return mPupilYCenter; } /** * The radius of the pupil (in the interval [0,1], relative to the minimum of width and height). */ - private double mPupilRadius = 0; + private float mPupilRadius = 0; - public final double getPupilRadius() { + public final float getPupilRadius() { return mPupilRadius; } /** * The horizontal center of the iris (in the interval [0,1]). */ - private double mIrisXCenter = 0; + private float mIrisXCenter = 0; - public final double getIrisXCenter() { + public final float getIrisXCenter() { return mIrisXCenter; } /** * The vertical center of the iris (in the interval [0,1]). */ - private double mIrisYCenter = 0; + private float mIrisYCenter = 0; - public final double getIrisYCenter() { + public final float getIrisYCenter() { return mIrisYCenter; } /** * The radius of the iris (in the interval [0,1], relative to the minimum of width and height). */ - private double mIrisRadius = 0; + private float mIrisRadius = 0; - public final double getIrisRadius() { + public final float getIrisRadius() { return mIrisRadius; } @@ -166,17 +162,24 @@ public PupilAndIrisDetector(final Image image) { break; } } -// refineIrisPosition(IRIS_SEARCH_RESOLUTIONS[0], true); -// for (int i = 1; i < IRIS_SEARCH_RESOLUTIONS.length; i++) { -// int resolution = IRIS_SEARCH_RESOLUTIONS[i]; -// refineIrisPosition(resolution, false); -// if (resolution >= image.getWidth() && resolution >= image.getHeight()) { -// break; -// } -// } refineIrisPosition(); } + /** + * Update the stored metadata with the iris and pupil position from the detector. + * + * @param metadata The metadata to be updated. + */ + public final void updateMetadata(final JpegMetadata metadata) { + metadata.setXCenter(mIrisXCenter); + metadata.setYCenter(mIrisYCenter); + metadata.setOverlayScaleFactor(mIrisRadius * 8 / 3); // MAGIC_NUMBER + + metadata.setPupilXOffset((mPupilXCenter - mIrisXCenter) / (2 * mIrisRadius)); + metadata.setPupilYOffset((mPupilYCenter - mIrisYCenter) / (2 * mIrisRadius)); + metadata.setPupilSize(mPupilRadius / mIrisRadius); + } + /** * Find initial values of pupil center and pupil and iris radius. */ @@ -192,7 +195,7 @@ private void determineInitialParameterValues() { } } - double maxLeapValue = Double.MIN_VALUE; + float maxLeapValue = Float.MIN_VALUE; PupilCenterInfo bestPupilCenter = null; for (PupilCenterInfo pupilCenterInfo : pupilCenterInfoList) { pupilCenterInfo.calculateStatistics(0); @@ -202,12 +205,12 @@ private void determineInitialParameterValues() { } } if (bestPupilCenter != null) { - mPupilXCenter = bestPupilCenter.mXCenter / image.getWidth(); - mPupilYCenter = bestPupilCenter.mYCenter / image.getHeight(); - mPupilRadius = bestPupilCenter.mPupilRadius / Math.min(image.getWidth(), image.getHeight()); + mPupilXCenter = bestPupilCenter.mXCenter / (float) image.getWidth(); + mPupilYCenter = bestPupilCenter.mYCenter / (float) image.getHeight(); + mPupilRadius = bestPupilCenter.mPupilRadius / (float) Math.max(image.getWidth(), image.getHeight()); mIrisXCenter = mPupilXCenter; mIrisYCenter = mPupilYCenter; - mIrisRadius = bestPupilCenter.mIrisRadius / Math.min(image.getWidth(), image.getHeight()); + mIrisRadius = bestPupilCenter.mIrisRadius / (float) Math.max(image.getWidth(), image.getHeight()); } } @@ -222,7 +225,7 @@ private void refinePupilPosition(final int resolution) { int pupilXCenter = (int) Math.round(mPupilXCenter * image.getWidth()); int pupilYCenter = (int) Math.round(mPupilYCenter * image.getHeight()); - int pupilRadius = (int) Math.round(mPupilRadius * Math.min(image.getWidth(), image.getHeight())); + int pupilRadius = (int) Math.round(mPupilRadius * Math.max(image.getWidth(), image.getHeight())); boolean isStable = false; @@ -235,7 +238,7 @@ private void refinePupilPosition(final int resolution) { } } - double maxLeapValue = Double.MIN_VALUE; + float maxLeapValue = Float.MIN_VALUE; PupilCenterInfo bestPupilCenter = null; for (PupilCenterInfo pupilCenterInfo : pupilCenterInfoList) { pupilCenterInfo.calculateStatistics(pupilRadius); @@ -255,60 +258,9 @@ private void refinePupilPosition(final int resolution) { } } - mPupilXCenter = pupilXCenter / image.getWidth(); - mPupilYCenter = pupilYCenter / image.getHeight(); - mPupilRadius = pupilRadius / Math.min(image.getWidth(), image.getHeight()); - } - - /** - * Refine the iris position based on the previously found position and a higher resolution. - * - * @param resolution The resolution. - * @param allAtOnce Flag indicating if all refinement steps should be done at once. - */ - private void refineIrisPosition(final int resolution, final boolean allAtOnce) { - Image image = ImageUtil.resizeImage(mImage, resolution, false); - List pupilCenterInfoList = new ArrayList<>(); - int stepCount = allAtOnce ? 1 : MAX_REFINEMENT_STEPS; - int stepSize = allAtOnce ? MAX_REFINEMENT_STEPS : 1; - - int irisXCenter = (int) Math.round(mIrisXCenter * image.getWidth()); - int irisYCenter = (int) Math.round(mIrisYCenter * image.getHeight()); - int irisRadius = (int) Math.round(mIrisRadius * Math.min(image.getWidth(), image.getHeight())); - - boolean isStable = false; - for (int step = 0; step < stepCount && !isStable; step++) { - for (int x = irisXCenter - stepCount; x <= irisXCenter + stepCount; x++) { - for (int y = irisYCenter - stepSize; y <= irisYCenter + stepSize; y++) { - PupilCenterInfo pupilCenterInfo = new PupilCenterInfo(image, x, y, PupilCenterInfo.Phase.IRIS_REFINEMENT); - pupilCenterInfo.collectCircleInfo((int) (irisRadius + MAX_REFINEMENT_STEPS + MAX_LEAP_WIDTH * resolution)); - pupilCenterInfoList.add(pupilCenterInfo); - } - } - - double maxLeapValue = Double.MIN_VALUE; - PupilCenterInfo bestIrisCenter = null; - for (PupilCenterInfo pupilCenterInfo : pupilCenterInfoList) { - pupilCenterInfo.calculateStatistics(irisRadius); - if (pupilCenterInfo.mLeapValue > maxLeapValue) { - maxLeapValue = pupilCenterInfo.mLeapValue; - bestIrisCenter = pupilCenterInfo; - } - } - - isStable = bestIrisCenter == null - || (bestIrisCenter.mXCenter == irisXCenter && bestIrisCenter.mYCenter == irisYCenter - && bestIrisCenter.mIrisRadius == irisRadius); - if (bestIrisCenter != null) { - irisXCenter = bestIrisCenter.mXCenter; - irisYCenter = bestIrisCenter.mYCenter; - irisRadius = bestIrisCenter.mIrisRadius; - } - } - - mIrisXCenter = irisXCenter / image.getWidth(); - mIrisYCenter = irisYCenter / image.getHeight(); - mIrisRadius = irisRadius / Math.min(image.getWidth(), image.getHeight()); + mPupilXCenter = pupilXCenter / (float) image.getWidth(); + mPupilYCenter = pupilYCenter / (float) image.getHeight(); + mPupilRadius = pupilRadius / (float) Math.max(image.getWidth(), image.getHeight()); } /** @@ -318,39 +270,13 @@ private void refineIrisPosition() { IrisBoundary irisBoundary = new IrisBoundary(mImage, (int) (mImage.getWidth() * mIrisXCenter), (int) (mImage.getHeight() * mIrisYCenter), - (int) (Math.min(mImage.getWidth(), mImage.getHeight()) * mIrisRadius)); + (int) (Math.max(mImage.getWidth(), mImage.getHeight()) * mIrisRadius)); irisBoundary.analyzeBoundary(); - mIrisXCenter = irisBoundary.mXCenter / mImage.getWidth(); - mIrisYCenter = irisBoundary.mYCenter / mImage.getHeight(); - mIrisRadius = irisBoundary.mRadius / Math.min(mImage.getWidth(), mImage.getHeight()); - - mIrisBoundary = irisBoundary; - } - - // TODO: remove later - this is only used to help displaying the points in the debug phase. - private IrisBoundary mIrisBoundary; - - public List getIrisBoundaryPoints() { - List result = new ArrayList<>(); - if(mIrisBoundary != null && mIrisBoundary.mLeftPoints != null) { - for (Integer y : mIrisBoundary.mLeftPoints.keySet()) { - result.add(new Point(mIrisBoundary.mLeftPoints.get(y), y)); - result.add(new Point(mIrisBoundary.mRightPoints.get(y), y)); - } - } - return result; - } - - public static class Point { - public Point(final int x, final int y) { - this.x = x; - this.y = y; - } - - public int x; - public int y; + mIrisXCenter = irisBoundary.mXCenter / (float) mImage.getWidth(); + mIrisYCenter = irisBoundary.mYCenter / (float) mImage.getHeight(); + mIrisRadius = irisBoundary.mRadius / (float) Math.max(mImage.getWidth(), mImage.getHeight()); } /** @@ -390,7 +316,7 @@ private static final class PupilCenterInfo { /** * The brightness leap value for this center. */ - private double mLeapValue = Double.MIN_VALUE; + private float mLeapValue = Float.MIN_VALUE; /** * Create a PupilCenterInfo with certain coordinates. @@ -414,9 +340,9 @@ private PupilCenterInfo(final Image image, final int xCoord, final int yCoord, f */ private void collectCircleInfo(final int maxRelevantRadius) { PixelReader pixelReader = mImage.getPixelReader(); - int maxPossibleRadius = (int) (Math.min( + int maxPossibleRadius = (int) Math.min( Math.min(mImage.getWidth() - 1 - mXCenter, mXCenter), - Math.min(mImage.getHeight() - 1 - mYCenter, mYCenter))); + Math.min(mImage.getHeight() - 1 - mYCenter, mYCenter)); int maxRadius = Math.min(maxRelevantRadius, maxPossibleRadius); // For iris refinement, ignore points on top and bottom long maxRadius2 = (maxRadius + 1) * (maxRadius + 1); @@ -425,7 +351,7 @@ private void collectCircleInfo(final int maxRelevantRadius) { long d2 = (x - mXCenter) * (x - mXCenter) + (y - mYCenter) * (y - mYCenter); if (d2 <= maxRadius2) { int d = (int) Math.round(Math.sqrt(d2)); - double brightness = getBrightness(pixelReader.getColor(x, y)); + float brightness = getBrightness(pixelReader.getColor(x, y)); addInfo(d, brightness); } } @@ -439,9 +365,9 @@ private void collectCircleInfo(final int maxRelevantRadius) { * @param color The color * @return The brightness value. */ - private static double getBrightness(final Color color) { - double min = Math.min(Math.min(color.getRed(), color.getGreen()), color.getBlue()); - double sum = color.getRed() + color.getGreen() + color.getBlue(); + private static float getBrightness(final Color color) { + float min = (float) Math.min(Math.min(color.getRed(), color.getGreen()), color.getBlue()); + float sum = (float) (color.getRed() + color.getGreen() + color.getBlue()); // Ensure that colors count more than dark grey, but white counts more then colors. return sum - min; } @@ -452,7 +378,7 @@ private static double getBrightness(final Color color) { * @param distance The distance of the pixel. * @param brightness The brightness of the pixel. */ - private void addInfo(final int distance, final double brightness) { + private void addInfo(final int distance, final float brightness) { CircleInfo circleInfo = mCircleInfos.get(distance); if (circleInfo == null) { circleInfo = new CircleInfo(distance); @@ -480,11 +406,11 @@ private void calculateStatistics(final int baseRadius) { : Math.max(0, baseRadius - MAX_REFINEMENT_STEPS - (int) (MAX_LEAP_WIDTH * resolution)); // Calculate the minimum of medians outside each circle. - double innerQuantileSum = 0; - double[] innerDarkness = new double[mCircleInfos.size()]; + float innerQuantileSum = 0; + float[] innerDarkness = new float[mCircleInfos.size()]; for (int i = minRadius; i <= maxRadius; i++) { - double currentQuantile = mCircleInfos.get(Integer.valueOf(i)).getQuantile(MIN_BLACK_QUOTA); + float currentQuantile = mCircleInfos.get(Integer.valueOf(i)).getQuantile(MIN_BLACK_QUOTA); innerQuantileSum += currentQuantile * i; innerDarkness[i] = i == 0 ? 0 : 2 * innerQuantileSum / (i * (i + 1)); } @@ -500,18 +426,22 @@ private void calculateStatistics(final int baseRadius) { if (mPhase == Phase.INITIAL || mPhase == Phase.PUPIL_REFINEMENT) { // determine pupil leap for (int i = minRadius; i <= maxRadius; i++) { - double pupilLeapValue = 0; - int maxLeapDistance = Math.min((int) Math.round(MAX_LEAP_WIDTH * resolution), + float pupilLeapValue = 0; + int maxLeapDistance = Math.min(Math.round(MAX_LEAP_WIDTH * resolution), Math.min(i / 2, (mCircleInfos.size() - 1 - i) / 2)); for (int j = 1; j <= maxLeapDistance; j++) { - double diff = mPhase == Phase.INITIAL + float diff = mPhase == Phase.INITIAL ? (ASSUMED_PUPIL_BRIGHTNESS + getMinMaxQuantile(MAX_BLACK_QUOTA, i + j, i + j + maxLeapDistance, false)) - / (ASSUMED_PUPIL_BRIGHTNESS + getMinMaxQuantile(MIN_BLACK_QUOTA, i - j - Math.max(j, 2), i - j, true)) - 1 + / (ASSUMED_PUPIL_BRIGHTNESS + + getMinMaxQuantile(MIN_BLACK_QUOTA, i - j - Math.min(maxLeapDistance, Math.max(j, 2)), i - j, true)) + - 1 : (ASSUMED_PUPIL_BRIGHTNESS + getMinMaxQuantile(MAX_BLACK_QUOTA, i + j, i + j + maxLeapDistance, false)) - / (ASSUMED_PUPIL_BRIGHTNESS + getMinMaxQuantile(MIN_BLACK_QUOTA, i - j - Math.max(j, 2), i, true)) - 1; + / (ASSUMED_PUPIL_BRIGHTNESS + + getMinMaxQuantile(MIN_BLACK_QUOTA, i - Math.min(maxLeapDistance, Math.max(j, 2)), i, true)) + - 1; if (diff > MIN_LEAP_DIFF) { // prefer big jumps in small radius difference. - double newLeapValue = diff / Math.pow(j, 0.8); // MAGIC_NUMBER + float newLeapValue = (float) (diff / Math.pow(j, 0.8)); // MAGIC_NUMBER if (newLeapValue > pupilLeapValue) { pupilLeapValue = newLeapValue; } @@ -520,7 +450,7 @@ private void calculateStatistics(final int baseRadius) { if (pupilLeapValue > 0) { CircleInfo circleInfo = mCircleInfos.get(Integer.valueOf(i)); // prefer big, dark circles - circleInfo.mPupilLeapValue = Math.sqrt(i) * pupilLeapValue / innerDarkness[i]; + circleInfo.mPupilLeapValue = (float) (Math.sqrt(i) * pupilLeapValue / innerDarkness[i]); relevantPupilCircles.add(circleInfo); } } @@ -529,9 +459,9 @@ private void calculateStatistics(final int baseRadius) { if (mPhase == Phase.INITIAL || mPhase == Phase.IRIS_REFINEMENT) { // determine iris leap for (int i = minRadius; i <= maxRadius; i++) { - double irisLeapValue = 0; - double irisQuantileSum = 0; - int maxLeapDistance = Math.min((int) Math.round(MAX_LEAP_WIDTH * resolution), + float irisLeapValue = 0; + float irisQuantileSum = 0; + int maxLeapDistance = Math.min(Math.round(MAX_LEAP_WIDTH * resolution), Math.min(i, mCircleInfos.size() - 1 - i)); for (int j = 1; j <= maxLeapDistance; j++) { irisQuantileSum += @@ -542,7 +472,7 @@ private void calculateStatistics(final int baseRadius) { / (2 * Math.sqrt(j)); if (irisQuantileSum > 0) { // prefer big jumps in small radius difference. - double newLeapValue = irisQuantileSum / j; + float newLeapValue = irisQuantileSum / j; if (newLeapValue > irisLeapValue) { irisLeapValue = newLeapValue; } @@ -564,7 +494,7 @@ private void calculateStatistics(final int baseRadius) { for (CircleInfo irisCircleInfo : relevantIrisCircles) { if (irisCircleInfo.mRadius - pupilCircleInfo.mRadius >= resolution * MIN_IRIS_PUPIL_DISTANCE) { - double newLeapValue = pupilCircleInfo.mPupilLeapValue * (1 + irisCircleInfo.mIrisLeapValue); + float newLeapValue = pupilCircleInfo.mPupilLeapValue * (1 + irisCircleInfo.mIrisLeapValue); if (newLeapValue > mLeapValue) { mLeapValue = newLeapValue; mPupilRadius = pupilCircleInfo.mRadius; @@ -576,7 +506,7 @@ private void calculateStatistics(final int baseRadius) { break; case PUPIL_REFINEMENT: for (CircleInfo pupilCircleInfo : relevantPupilCircles) { - double newLeapValue = pupilCircleInfo.mPupilLeapValue; + float newLeapValue = pupilCircleInfo.mPupilLeapValue; if (newLeapValue > mLeapValue) { mLeapValue = newLeapValue; mPupilRadius = pupilCircleInfo.mRadius; @@ -586,7 +516,7 @@ private void calculateStatistics(final int baseRadius) { case IRIS_REFINEMENT: default: for (CircleInfo irisCircleInfo : relevantIrisCircles) { - double newLeapValue = irisCircleInfo.mIrisLeapValue; + float newLeapValue = irisCircleInfo.mIrisLeapValue; if (newLeapValue > mLeapValue) { mLeapValue = newLeapValue; mIrisRadius = irisCircleInfo.mRadius; @@ -605,10 +535,10 @@ private void calculateStatistics(final int baseRadius) { * @param max if true, the maximum is returned, otherwise the minimum. * @return The minimum quantile. */ - private double getMinMaxQuantile(final double p, final int fromRadius, final int toRadius, final boolean max) { - double result = max ? Double.MIN_VALUE : Double.MAX_VALUE; + private float getMinMaxQuantile(final float p, final int fromRadius, final int toRadius, final boolean max) { + float result = max ? Float.MIN_VALUE : Float.MAX_VALUE; for (int radius = fromRadius; radius <= toRadius; radius++) { - double newValue = mCircleInfos.get(Integer.valueOf(radius)).getQuantile(p); + float newValue = mCircleInfos.get(Integer.valueOf(radius)).getQuantile(p); if ((!max && newValue < result) || (max && newValue > result)) { result = newValue; } @@ -655,22 +585,22 @@ private CircleInfo(final int radius) { /** * The brightnesses. */ - private List mBrightnesses = new ArrayList<>(); + private List mBrightnesses = new ArrayList<>(); /** * The brightness leap at this radius used for pupil identification. */ - private double mPupilLeapValue; + private float mPupilLeapValue; /** * The brightness leap at this radius used for iris identification. */ - private double mIrisLeapValue; + private float mIrisLeapValue; /** * Add a brightness to the information of this circle. * * @param brightness the brightness. */ - private void addBrightness(final double brightness) { + private void addBrightness(final float brightness) { mBrightnesses.add(brightness); } @@ -687,7 +617,7 @@ private void calculateStatistics() { * @param p the quantile parameter. * @return the p-quantile of the brightnesses (not considering equality). */ - private double getQuantile(final double p) { + private float getQuantile(final float p) { return mBrightnesses.get((int) (mBrightnesses.size() * p)); } } @@ -744,14 +674,12 @@ private IrisBoundary(final Image image, final int xCenter, final int yCenter, fi private void determineBoundaryPoints() { PixelReader pixelReader = mImage.getPixelReader(); - boolean found = true; for (int yCoord = mYCenter; yCoord <= mYCenter + mRadius * IRIS_BOUNDARY_SEARCH_RANGE && yCoord < mImage.getHeight(); yCoord++) { - found = determineBoundaryPoints(pixelReader, yCoord); + determineBoundaryPoints(pixelReader, yCoord); } - found = true; for (int yCoord = mYCenter - 1; yCoord >= mYCenter - mRadius * IRIS_BOUNDARY_SEARCH_RANGE && yCoord >= 0; yCoord--) { - found = determineBoundaryPoints(pixelReader, yCoord); + determineBoundaryPoints(pixelReader, yCoord); } } @@ -763,8 +691,8 @@ private void determineBoundaryPoints() { * @return true if a boundary point has been found. */ private boolean determineBoundaryPoints(final PixelReader pixelReader, final int yCoord) { - int xDistanceRange = (int) Math.round(IRIS_BOUNDARY_UNCERTAINTY_FACTOR * mRadius); - int xDistanceMinRange = (int) Math.round(IRIS_BOUNDARY_MIN_RANGE * mRadius); + int xDistanceRange = Math.round(IRIS_BOUNDARY_UNCERTAINTY_FACTOR * mRadius); + int xDistanceMinRange = Math.round(IRIS_BOUNDARY_MIN_RANGE * mRadius); boolean found = false; while (!found && xDistanceRange >= xDistanceMinRange) { @@ -791,13 +719,13 @@ private boolean determineBoundaryPoints(final PixelReader pixelReader, final int int expectedXDistance = (int) Math.round(Math.sqrt(mRadius * mRadius - yDiff * yDiff)); // Left side - calculate average brightness - double brightnessSum = 0; + float brightnessSum = 0; int leftBoundary = Math.max(mXCenter - expectedXDistance - xDistanceRange, 0); int rightBoundary = Math.min(mXCenter - expectedXDistance + xDistanceRange, (int) mImage.getWidth() - 1); for (int x = leftBoundary; x <= rightBoundary; x++) { brightnessSum += getBrightness(pixelReader.getColor(x, yCoord)); } - double avgBrightness = brightnessSum / (2 * xDistanceRange + 1); + float avgBrightness = brightnessSum / (2 * xDistanceRange + 1); // Left side - find transition from light to dark int leftCounter = 0; @@ -819,13 +747,13 @@ private boolean determineBoundaryPoints(final PixelReader pixelReader, final int } // Right side - calculate average brightness - double brightnessSum2 = 0; + float brightnessSum2 = 0; int leftBoundary2 = Math.max(mXCenter + expectedXDistance - xDistanceRange, 0); int rightBoundary2 = Math.min(mXCenter + expectedXDistance + xDistanceRange, (int) mImage.getWidth() - 1); for (int x = leftBoundary2; x <= rightBoundary2; x++) { brightnessSum2 += getBrightness(pixelReader.getColor(x, yCoord)); } - double avgBrightness2 = brightnessSum2 / (2 * xDistanceRange + 1); + float avgBrightness2 = brightnessSum2 / (2 * xDistanceRange + 1); // Right side - find transition from light to dark int leftCounter2 = 0; @@ -873,12 +801,7 @@ private void determineXCenter() { xSumValues.add(mLeftPoints.get(yCoord) + mRightPoints.get(yCoord)); } - xSumValues.sort(new Comparator() { - @Override - public int compare(final Integer integer1, final Integer integer2) { - return Integer.compare(integer1, integer2); - } - }); + Collections.sort(xSumValues); mXCenter = xSumValues.get(xSumValues.size() / 2) / 2; } @@ -928,7 +851,7 @@ public int compare(final Integer integer1, final Integer integer2) { * Determine the radius from boundary points, after center is known. */ private void determineRadius() { - double sum = 0; + float sum = 0; for (Integer y : mLeftPoints.keySet()) { int yDistance = y - mYCenter; int xDistance = mLeftPoints.get(y) - mXCenter; @@ -940,7 +863,7 @@ private void determineRadius() { sum += Math.sqrt(xDistance * xDistance + yDistance * yDistance); } - mRadius = (int) Math.round(sum / (2 * mLeftPoints.size())); + mRadius = Math.round(sum / (2 * mLeftPoints.size())); } /** @@ -949,9 +872,9 @@ private void determineRadius() { * @param color The color * @return The brightness value. */ - private static double getBrightness(final Color color) { + private static float getBrightness(final Color color) { // Blue seems to be particulary helpful in the separation. - return Math.min(Math.min(color.getRed(), color.getGreen()), color.getBlue()) + color.getBlue(); + return (float) (Math.min(Math.min(color.getRed(), color.getGreen()), color.getBlue()) + color.getBlue()); } } diff --git a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/activities/CameraActivity.java b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/activities/CameraActivity.java index 48506530..cd813276 100644 --- a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/activities/CameraActivity.java +++ b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/activities/CameraActivity.java @@ -62,6 +62,7 @@ import de.jeisfeld.augendiagnoselib.util.imagefile.JpegMetadata; import de.jeisfeld.augendiagnoselib.util.imagefile.JpegSynchronizationUtil; import de.jeisfeld.augendiagnoselib.util.imagefile.MediaStoreUtil; +import de.jeisfeld.augendiagnoselib.util.imagefile.PupilAndIrisDetector; import static de.jeisfeld.augendiagnoselib.activities.CameraActivity.Action.CANCEL_AND_VIEW_IMAGES; import static de.jeisfeld.augendiagnoselib.activities.CameraActivity.Action.CHECK_PHOTO; @@ -412,6 +413,7 @@ public void onClick(final View v) { if (mLeftEyeFile == null) { setAction(TAKE_PHOTO, LEFT); + PupilAndIrisDetector.determineAndStoreIrisPosition(mRightEyeFile.getAbsolutePath()); } else { setAction(FINISH_CAMERA, null); @@ -428,6 +430,7 @@ public void onClick(final View v) { if (mRightEyeFile == null) { setAction(TAKE_PHOTO, RIGHT); + PupilAndIrisDetector.determineAndStoreIrisPosition(mLeftEyeFile.getAbsolutePath()); } else { setAction(FINISH_CAMERA, null); diff --git a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/activities/OrganizeNewPhotosActivity.java b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/activities/OrganizeNewPhotosActivity.java index 23aa8214..9e9de78c 100644 --- a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/activities/OrganizeNewPhotosActivity.java +++ b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/activities/OrganizeNewPhotosActivity.java @@ -50,6 +50,7 @@ import de.jeisfeld.augendiagnoselib.util.imagefile.ImageUtil; import de.jeisfeld.augendiagnoselib.util.imagefile.JpegMetadataUtil; import de.jeisfeld.augendiagnoselib.util.imagefile.MediaStoreUtil; +import de.jeisfeld.augendiagnoselib.util.imagefile.PupilAndIrisDetector; /** * Activity to display a pair of new eye photos, choose a name and a date for them, and shift them into the @@ -432,6 +433,9 @@ private void updateImages(final boolean updateDate) { mEditDate.setText(DateUtil.getDisplayDate(mPictureDate)); mEditDate.invalidate(); } + + PupilAndIrisDetector.determineAndStoreIrisPosition(mPhotoRight.getAbsolutePath()); + PupilAndIrisDetector.determineAndStoreIrisPosition(mPhotoLeft.getAbsolutePath()); } /** diff --git a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/components/OverlayPinchImageView.java b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/components/OverlayPinchImageView.java index 8f6810e5..9c792a3e 100644 --- a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/components/OverlayPinchImageView.java +++ b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/components/OverlayPinchImageView.java @@ -27,6 +27,7 @@ import de.jeisfeld.augendiagnoselib.Application; import de.jeisfeld.augendiagnoselib.R; +import de.jeisfeld.augendiagnoselib.fragments.DisplayImageFragment.OverlayStatus; import de.jeisfeld.augendiagnoselib.util.PreferenceUtil; import de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto; import de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto.RightLeft; @@ -261,7 +262,7 @@ public OverlayPinchImageView(final Context context) { * Standard constructor to be implemented for all views. * * @param context The Context the view is running in, through which it can access the current theme, resources, etc. - * @param attrs The attributes of the XML tag that is inflating the view. + * @param attrs The attributes of the XML tag that is inflating the view. * @see android.view.View#View(Context, AttributeSet) */ public OverlayPinchImageView(final Context context, final AttributeSet attrs) { @@ -271,10 +272,10 @@ public OverlayPinchImageView(final Context context, final AttributeSet attrs) { /** * Standard constructor to be implemented for all views. * - * @param context The Context the view is running in, through which it can access the current theme, resources, etc. - * @param attrs The attributes of the XML tag that is inflating the view. + * @param context The Context the view is running in, through which it can access the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view. * @param defStyle An attribute in the current theme that contains a reference to a style resource that supplies default - * values for the view. Can be 0 to not look for defaults. + * values for the view. Can be 0 to not look for defaults. * @see android.view.View#View(Context, AttributeSet, int) */ public OverlayPinchImageView(final Context context, final AttributeSet attrs, final int defStyle) { @@ -293,8 +294,8 @@ public final EyePhoto getEyePhoto() { /** * Fill with an image, initializing from metadata. * - * @param pathName The pathname of the image - * @param activity The triggering activity (required for bitmap caching) + * @param pathName The pathname of the image + * @param activity The triggering activity (required for bitmap caching) * @param cacheIndex A unique index of the view in the activity */ @Override @@ -336,7 +337,8 @@ public void run() { mOverlayScaleFactor = mMetadata.getOverlayScaleFactor() * Math.max(mBitmap.getHeight(), mBitmap.getWidth()) / OVERLAY_SIZE; - boolean shouldBeLocked = !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY); + boolean shouldBeLocked = !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY) + && !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_POSITION_DETERMINED_AUTOMATICALLY); lockOverlay(shouldBeLocked, false); if (mGuiElementUpdater != null) { mGuiElementUpdater.setLockChecked(shouldBeLocked); @@ -405,8 +407,9 @@ public void run() { refresh(HIGH); showFullResolutionSnapshot(true); // Update lock status - required in the case that orientation change happened while loading image. - if (mMetadata != null && mMetadata.hasOverlayPosition() && mGuiElementUpdater != null - && !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY)) { + if (mMetadata != null && mMetadata.hasOverlayPosition() && mGuiElementUpdater != null // BOOLEAN_EXPRESSION_COMPLEXITY + && !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY) + && !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_POSITION_DETERMINED_AUTOMATICALLY)) { mGuiElementUpdater.setLockChecked(true); } } @@ -564,7 +567,7 @@ private List getOverlayPositions() { * Get information if the view can handle overlays. * * @return true if the view can handle overlays. This is possible only if the right/left position of the eye photo - * is defined. + * is defined. */ public final boolean canHandleOverlays() { return mEyePhoto != null && mEyePhoto.getRightLeft() != null; @@ -573,7 +576,7 @@ public final boolean canHandleOverlays() { /** * Trigger one overlay either for activation or for deactivation. * - * @param position number of the overlay + * @param position number of the overlay * @param pinchMode the way in which pinching should be done. ALL indicates that the overlay should not be shown. */ public final void triggerOverlay(final int position, final PinchMode pinchMode) { @@ -591,7 +594,7 @@ public final void triggerOverlay(final int position, final PinchMode pinchMode) /** * Switch the lock status of the overlays. * - * @param lock the target lock status + * @param lock the target lock status * @param store a flag indicating if the lock status should be stored. */ public final void lockOverlay(final boolean lock, final boolean store) { @@ -619,6 +622,58 @@ public final void lockOverlay(final boolean lock, final boolean store) { updatePinchMode(); } + /** + * Change the positioning of the image dependent on the overlay setup phase. + * + * @param overlayStatus The overlay status. + * @param circleRadius The relative circle radius (compared to min view dimension) + */ + public final void updatePosition(final OverlayStatus overlayStatus, final float circleRadius) { + switch (overlayStatus) { + case GUIDE_IRIS: + if (!mMetadata.hasOverlayPosition()) { + return; + } + mOverlayX = mMetadata.getXCenter(); + mOverlayY = mMetadata.getYCenter(); + mOverlayScaleFactor = mMetadata.getOverlayScaleFactor() * Math.max(mBitmap.getHeight(), mBitmap.getWidth()) / OVERLAY_SIZE; + + mPosX = mOverlayX; + mPosY = mOverlayY; + + float bitmapPixelDiameter = mOverlayScaleFactor * OVERLAY_SIZE * OVERLAY_CIRCLE_RATIO; + mScaleFactor = Math.min(getWidth(), getHeight()) * 2 * circleRadius / bitmapPixelDiameter; + mLastScaleFactor = mScaleFactor; + showNormalResolution(); + refresh(); + break; + case GUIDE_PUPIL: + if (mMetadata.getPupilSize() == null || mMetadata.getPupilSize() == 0) { + return; + } + mPupilOverlayX = mMetadata.getPupilXOffset(); + mPupilOverlayY = mMetadata.getPupilYOffset(); + mPupilOverlayScaleFactor = mMetadata.getPupilSize(); + + float overlaySizeOnBitmap = OVERLAY_CIRCLE_RATIO * mMetadata.getOverlayScaleFactor() * Math.max(mBitmap.getHeight(), mBitmap.getWidth()); + mPosX = mPupilOverlayX * overlaySizeOnBitmap / mBitmap.getWidth() + mOverlayX; + mPosY = mPupilOverlayY * overlaySizeOnBitmap / mBitmap.getHeight() + mOverlayY; + + float bitmapPixelDiameter2 = mPupilOverlayScaleFactor * overlaySizeOnBitmap; + mScaleFactor = Math.min(getWidth(), getHeight()) * 2 * circleRadius / bitmapPixelDiameter2; + mLastScaleFactor = mScaleFactor; + showNormalResolution(); + refresh(); + break; + case ALLOWED: + mInitialized = false; + doInitialScaling(); + break; + default: + break; + } + } + /** * Set the overlay position, so that it matches a centered circle. * @@ -640,6 +695,7 @@ public final void storePupilPosition() { mMetadata.setPupilSize(mPupilOverlayScaleFactor); mMetadata.setPupilXOffset(mPupilOverlayX); mMetadata.setPupilYOffset(mPupilOverlayY); + mMetadata.removeFlag(JpegMetadata.FLAG_OVERLAY_POSITION_DETERMINED_AUTOMATICALLY); mEyePhoto.storeImageMetadata(mMetadata); resetOverlayCache(); @@ -799,17 +855,17 @@ private Drawable getOverlayDrawable(final int position) { * Create a drawable from a black image drawable, having a changed colour. * * @param sourceDrawable The black image drawable - * @param color The target color - * @param origPupilSize The pupil size (relative to iris) in the original overlay bitmap. - * @param destPupilSize The pupil size (relative to iris) in the target overlay bitmap. - * @param pupilOffsetX The relative x offset of the pupil center - * @param pupilOffsetY The relative y offset of the pupil center + * @param color The target color + * @param origPupilSize The pupil size (relative to iris) in the original overlay bitmap. + * @param destPupilSize The pupil size (relative to iris) in the target overlay bitmap. + * @param pupilOffsetX The relative x offset of the pupil center + * @param pupilOffsetY The relative y offset of the pupil center * @return The modified drawable, with the intended color. */ @NonNull private Drawable getModifiedDrawable(@NonNull final Drawable sourceDrawable, @Nullable final Integer color, - final float origPupilSize, @Nullable final Float destPupilSize, - final Float pupilOffsetX, final Float pupilOffsetY) { + final float origPupilSize, @Nullable final Float destPupilSize, + final Float pupilOffsetX, final Float pupilOffsetY) { Bitmap bitmap = ((BitmapDrawable) sourceDrawable).getBitmap(); Bitmap colouredBitmap = color == null ? bitmap : ImageUtil.changeBitmapColor(bitmap, color); @@ -1138,8 +1194,8 @@ protected final void finishPointerMove(final MotionEvent ev) { /** * Update contrast and brightness of a bitmap. * - * @param bmp input bitmap - * @param contrast 0..infinity - 1 is default + * @param bmp input bitmap + * @param contrast 0..infinity - 1 is default * @param brightness -1..1 - 0 is default * @return new bitmap */ @@ -1674,7 +1730,7 @@ private void setBitmapFullResolution(final Bitmap bitmapFullResolution) { /** * Get the retainFragment - search it by the index. If not found, create a new one. * - * @param fm The fragment manager handling this fragment. + * @param fm The fragment manager handling this fragment. * @param index The index of the view (required in case of multiple PinchImageViews to be retained). * @return the retainFragment. */ diff --git a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/DisplayImageFragment.java b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/DisplayImageFragment.java index c8f642ec..dbd6fef4 100644 --- a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/DisplayImageFragment.java +++ b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/DisplayImageFragment.java @@ -53,6 +53,7 @@ import de.jeisfeld.augendiagnoselib.util.SystemUtil; import de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto.RightLeft; import de.jeisfeld.augendiagnoselib.util.imagefile.JpegMetadataUtil; +import de.jeisfeld.augendiagnoselib.util.imagefile.PupilAndIrisDetector; /** * Variant of DisplayOneFragment that includes overlay handling. @@ -598,6 +599,10 @@ private void drawOverlayCircle() { ImageView overlayView = (ImageView) getView().findViewById(R.id.circleOverlay); TextView textViewGuide = (TextView) getView().findViewById(R.id.textViewGuide); + mImageView.updatePosition(mOverlayStatus, mOverlayStatus == OverlayStatus.GUIDE_IRIS + ? (float) CIRCLE_RADIUS_IRIS / CIRCLE_BITMAP_SIZE + : (float) CIRCLE_RADIUS_PUPIL / CIRCLE_BITMAP_SIZE); + if (mOverlayStatus != OverlayStatus.GUIDE_IRIS && mOverlayStatus != OverlayStatus.GUIDE_PUPIL) { overlayView.setVisibility(View.GONE); textViewGuide.setVisibility(View.GONE); @@ -1074,6 +1079,8 @@ public final void initializeImages() { } else { mImageView.setImage(mFile, getActivity(), mImageIndex); + + PupilAndIrisDetector.determineAndStoreIrisPosition(mFile); } if (mImageView.getEyePhoto().getRightLeft() == null && mRightLeft != null) { diff --git a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java index 85004168..365f7102 100644 --- a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java +++ b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java @@ -217,14 +217,19 @@ public static boolean deleteFile(@NonNull final File file) { */ public static boolean moveFile(@NonNull final File source, @NonNull final File target) { // First try the normal rename. - if (source.renameTo(target)) { - return true; + boolean success = source.renameTo(target); + + if (!success) { + success = copyFile(source, target); + if (success) { + success = deleteFile(source); + } } - boolean success = copyFile(source, target); if (success) { - success = deleteFile(source); + PupilAndIrisDetector.notifyFileRename(source.getAbsolutePath(), target.getAbsolutePath()); } + return success; } diff --git a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/ImageUtil.java b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/ImageUtil.java index 79df4986..3c04af70 100644 --- a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/ImageUtil.java +++ b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/ImageUtil.java @@ -217,27 +217,39 @@ public static Bitmap getImageBitmap(@NonNull final byte[] data, final int maxSiz BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = getBitmapFactor(data, maxSize); bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options); + bitmap = resizeBitmap(bitmap, maxSize, false); + } - if (bitmap.getWidth() == 0 || bitmap.getHeight() == 0) { - return bitmap; - } + return bitmap; + } - if (bitmap.getWidth() > maxSize || bitmap.getHeight() > maxSize) { - // Only if bitmap is bigger than maxSize, then resize it - if (bitmap.getWidth() > bitmap.getHeight()) { - int targetWidth = maxSize; - int targetHeight = bitmap.getHeight() * maxSize / bitmap.getWidth(); - bitmap = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false); - } - else { - int targetWidth = bitmap.getWidth() * maxSize / bitmap.getHeight(); - int targetHeight = maxSize; - bitmap = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false); - } + /** + * Resize a bitmap to the given size. + * + * @param baseBitmap The original bitmap. + * @param targetSize The target size. + * @param allowGrowing flag indicating if the image is allowed to grow. + * @return the resized image. + */ + public static Bitmap resizeBitmap(final Bitmap baseBitmap, final int targetSize, final boolean allowGrowing) { + if (baseBitmap.getWidth() == 0 || baseBitmap.getHeight() == 0) { + return baseBitmap; + } + else if (baseBitmap.getWidth() > targetSize || baseBitmap.getHeight() > targetSize || allowGrowing) { + if (baseBitmap.getWidth() > baseBitmap.getHeight()) { + int targetWidth = targetSize; + int targetHeight = baseBitmap.getHeight() * targetSize / baseBitmap.getWidth(); + return Bitmap.createScaledBitmap(baseBitmap, targetWidth, targetHeight, false); + } + else { + int targetWidth = baseBitmap.getWidth() * targetSize / baseBitmap.getHeight(); + int targetHeight = targetSize; + return Bitmap.createScaledBitmap(baseBitmap, targetWidth, targetHeight, false); } } - - return bitmap; + else { + return baseBitmap; + } } /** diff --git a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/JpegMetadata.java b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/JpegMetadata.java index 58e3d99e..0c328adc 100644 --- a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/JpegMetadata.java +++ b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/JpegMetadata.java @@ -17,6 +17,10 @@ public final class JpegMetadata implements Parcelable { * Flag indicating that the overlay size has been set automatically by camera activity ant not by user. */ public static final int FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY = 0b10; + /** + * Flag indicating that the overlay size has been determined automatically by PupilAndIrisDetector. + */ + public static final int FLAG_OVERLAY_POSITION_DETERMINED_AUTOMATICALLY = 0b100; // JAVADOC:OFF private static final String LINE_BREAK = "\n"; @@ -454,28 +458,28 @@ public boolean hasFlag(final int flag) { @Override public String toString() { - StringBuffer str = new StringBuffer(); - str.append("Title: " + mTitle + LINE_BREAK); - str.append("Description: " + mDescription + LINE_BREAK); - str.append("Subject: " + mSubject + LINE_BREAK); - str.append("Comment: " + mComment + LINE_BREAK); - str.append("Person: " + mPerson + LINE_BREAK); - str.append("X-Center: " + mXCenter + LINE_BREAK); - str.append("Y-Center: " + mYCenter + LINE_BREAK); - str.append("OverlayScaleFactor: " + mOverlayScaleFactor + LINE_BREAK); - str.append("X-Position: " + mXPosition + LINE_BREAK); - str.append("Y-Position: " + mYPosition + LINE_BREAK); - str.append("ZoomFactor: " + mZoomFactor + LINE_BREAK); - str.append("OrganizeDate: " + mOrganizeDate + LINE_BREAK); - str.append("RightLeft: " + mRightLeft + LINE_BREAK); - str.append("Brightness: " + mBrightness + LINE_BREAK); - str.append("Contrast: " + mContrast + LINE_BREAK); - str.append("OverlayColor: " + getOverlayColorString() + LINE_BREAK); - str.append("Pupil-Size: " + mPupilSize + LINE_BREAK); - str.append("Pupil-X-Offset: " + mPupilXOffset + LINE_BREAK); - str.append("Pupil-Y-Offset: " + mPupilYOffset + LINE_BREAK); - str.append("Flags: " + mFlags + LINE_BREAK); - str.append("Orientation: " + getOrientationString() + LINE_BREAK); + StringBuilder str = new StringBuilder(); + str.append("Title: ").append(mTitle).append(LINE_BREAK); + str.append("Description: ").append(mDescription).append(LINE_BREAK); + str.append("Subject: ").append(mSubject).append(LINE_BREAK); + str.append("Comment: ").append(mComment).append(LINE_BREAK); + str.append("Person: ").append(mPerson).append(LINE_BREAK); + str.append("X-Center: ").append(mXCenter).append(LINE_BREAK); + str.append("Y-Center: ").append(mYCenter).append(LINE_BREAK); + str.append("OverlayScaleFactor: ").append(mOverlayScaleFactor).append(LINE_BREAK); + str.append("X-Position: ").append(mXPosition).append(LINE_BREAK); + str.append("Y-Position: ").append(mYPosition).append(LINE_BREAK); + str.append("ZoomFactor: ").append(mZoomFactor).append(LINE_BREAK); + str.append("OrganizeDate: ").append(mOrganizeDate).append(LINE_BREAK); + str.append("RightLeft: ").append(mRightLeft).append(LINE_BREAK); + str.append("Brightness: ").append(mBrightness).append(LINE_BREAK); + str.append("Contrast: ").append(mContrast).append(LINE_BREAK); + str.append("OverlayColor: ").append(getOverlayColorString()).append(LINE_BREAK); + str.append("Pupil-Size: ").append(mPupilSize).append(LINE_BREAK); + str.append("Pupil-X-Offset: ").append(mPupilXOffset).append(LINE_BREAK); + str.append("Pupil-Y-Offset: ").append(mPupilYOffset).append(LINE_BREAK); + str.append("Flags: ").append(mFlags).append(LINE_BREAK); + str.append("Orientation: ").append(getOrientationString()).append(LINE_BREAK); return str.toString(); } diff --git a/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/PupilAndIrisDetector.java b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/PupilAndIrisDetector.java new file mode 100644 index 00000000..ab8fd692 --- /dev/null +++ b/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/PupilAndIrisDetector.java @@ -0,0 +1,991 @@ +package de.jeisfeld.augendiagnoselib.util.imagefile; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.util.Log; + +import de.jeisfeld.augendiagnoselib.Application; + +/** + * Class that serves to detect the pupil and iris within an eye photo. + */ +public class PupilAndIrisDetector { + /** + * The resolution of the image when searching for a point within the pupil. + */ + private static final int[] PUPIL_SEARCH_RESOLUTIONS = {100, 200, 600}; + /** + * The size of the maximum change distance from one zone (pupil/iris/outer) to the next, relative to the image size. + */ + private static final float MAX_LEAP_WIDTH = 0.05f; + /** + * The minimum brightness difference accepted as a leap. + */ + private static final float MIN_LEAP_DIFF = 0.05f; + /** + * The minimum pupil radius, relative to the image size. + */ + private static final float MIN_PUPIL_RADIUS = 0.04f; + /** + * The minimum distance between iris and pupil, relative to the image size. + */ + private static final float MIN_IRIS_PUPIL_DISTANCE = 0.1f; + /** + * The maximum steps of position refinement that should be done at each resolution. + */ + private static final int MAX_REFINEMENT_STEPS = 5; + /** + * The brightness of the pupil assumed when calculating the leaps. + */ + private static final float ASSUMED_PUPIL_BRIGHTNESS = 0.3f; + /** + * The minimum white quota expected outside the iris. + */ + private static final float MIN_WHITE_QUOTA = 0.3f; + /** + * The secondary minimum white quota expected outside the iris. + */ + private static final float MIN_WHITE_QUOTA2 = 0.7f; + /** + * The minimum black quota expected within the pupil. + */ + private static final float MIN_BLACK_QUOTA = 0.7f; + /** + * The maximum black quota expected outside the pupil. + */ + private static final float MAX_BLACK_QUOTA = 0.3f; + /** + * The vertical range where iris boundary points should be searched for. + */ + private static final float IRIS_BOUNDARY_SEARCH_RANGE = 0.7f; + /** + * The uncertainty of the positions of the iris boundary points. + */ + private static final float IRIS_BOUNDARY_UNCERTAINTY_FACTOR = 0.2f; + /** + * The minimum range considered when determining the iris boundary. + */ + private static final float IRIS_BOUNDARY_MIN_RANGE = 0.02f; + /** + * Factor by which the range is changed with each retry after a search failure. + */ + private static final float IRIS_BOUNDARY_RETRY_FACTOR = 0.7f; + /** + * The quota of points that are allowed to be too bright in the iris or too dark outside the iris. + */ + private static final float IRIS_BOUNDARY_WRONG_BRIGHTNESS_QUOTA = 0.2f; + /** + * The quota of points around the center considered for determining the vertical center. + */ + private static final float IRIS_BOUNDARY_POINTS_CONSIDERED_FOR_YCENTER = 0.3f; + /** + * The minimum number of boundary points needed to refine the iris position. + */ + private static final float IRIS_BOUNDARY_MIN_BOUNDARY_POINTS = 10; + + /** + * The files which are currently processed by this class. The value is used for previous file names in case of renaming. + */ + private static final Map> FILES_IN_PROCESS = new HashMap<>(); + /** + * The files which are currently processed by this class. The value contains the current file name in case of renaming. + */ + private static final Map FILES_IN_PROCESS2 = new HashMap<>(); + + /** + * The image to be analyzed. + */ + private Bitmap mImage; + + /** + * The horizontal center of the pupil (in the interval [0,1]). + */ + private float mPupilXCenter = 0; + + public final float getPupilXCenter() { + return mPupilXCenter; + } + + /** + * The vertical center of the pupil (in the interval [0,1]). + */ + private float mPupilYCenter = 0; + + public final float getPupilYCenter() { + return mPupilYCenter; + } + + /** + * The radius of the pupil (in the interval [0,1], relative to the minimum of width and height). + */ + private float mPupilRadius = 0; + + public final float getPupilRadius() { + return mPupilRadius; + } + + /** + * The horizontal center of the iris (in the interval [0,1]). + */ + private float mIrisXCenter = 0; + + public final float getIrisXCenter() { + return mIrisXCenter; + } + + /** + * The vertical center of the iris (in the interval [0,1]). + */ + private float mIrisYCenter = 0; + + public final float getIrisYCenter() { + return mIrisYCenter; + } + + /** + * The radius of the iris (in the interval [0,1], relative to the minimum of width and height). + */ + private float mIrisRadius = 0; + + public final float getIrisRadius() { + return mIrisRadius; + } + + /** + * Create a detector for a certain image. + * + * @param image The image to be analyzed. + */ + public PupilAndIrisDetector(final Bitmap image) { + mImage = image; + determineInitialParameterValues(); + for (int i = 1; i < PUPIL_SEARCH_RESOLUTIONS.length; i++) { + int resolution = PUPIL_SEARCH_RESOLUTIONS[i]; + refinePupilPosition(resolution); + if (resolution >= image.getWidth() && resolution >= image.getHeight()) { + break; + } + } + refineIrisPosition(); + } + + /** + * Determine the iris position in an image path and store it in the metadata. + * + * @param imagePath The path of the image. + */ + public static final void determineAndStoreIrisPosition(final String imagePath) { + JpegMetadata origMetadata = JpegSynchronizationUtil.getJpegMetadata(imagePath); + if (origMetadata == null + || origMetadata.hasOverlayPosition() && !origMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY)) { + // Do not overwrite manually set overlay position. + return; + } + + new Thread() { + @Override + public void run() { + synchronized (FILES_IN_PROCESS) { + if (FILES_IN_PROCESS2.keySet().contains(imagePath)) { + return; + } + else { + FILES_IN_PROCESS.put(imagePath, new HashSet()); + FILES_IN_PROCESS2.put(imagePath, imagePath); + } + } + + try { + Log.v(Application.TAG, "Start finding iris for " + imagePath); + long timestamp = System.currentTimeMillis(); + PupilAndIrisDetector detector = new PupilAndIrisDetector(ImageUtil.getImageBitmap(imagePath, 0)); + Log.v(Application.TAG, "Finished finding iris for " + imagePath + ". Duration: " + + ((System.currentTimeMillis() - timestamp) / 1000.0)); // MAGIC_NUMBER + + // Retrieve image path - in case the file has moved. + String newImagePath = FILES_IN_PROCESS2.get(imagePath); + + JpegMetadata metadata = JpegSynchronizationUtil.getJpegMetadata(newImagePath); + // re-check if position has been set manually. + if (metadata != null && (!metadata.hasOverlayPosition() || metadata.hasFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY))) { + detector.updateMetadata(metadata); + JpegSynchronizationUtil.storeJpegMetadata(newImagePath, metadata); + } + } + catch (Throwable e) { + Log.e(Application.TAG, "Failed to find iris and pupil position for file " + imagePath, e); + } + finally { + synchronized (FILES_IN_PROCESS) { + String newImagePath = FILES_IN_PROCESS2.get(imagePath); + Set oldPaths = FILES_IN_PROCESS.get(newImagePath); + if (oldPaths != null) { + for (String oldPath : oldPaths) { + FILES_IN_PROCESS2.remove(oldPath); + } + } + FILES_IN_PROCESS.remove(newImagePath); + FILES_IN_PROCESS2.remove(newImagePath); + } + } + } + }.start(); + } + + /** + * Inform about the move of a file during determination of iris position, so that the result may be applied to the moved file. + * + * @param oldFileName The old file name. + * @param newFileName the new file name. + */ + public static final void notifyFileRename(final String oldFileName, final String newFileName) { + if (!FILES_IN_PROCESS.containsKey(oldFileName)) { + return; + } + + synchronized (FILES_IN_PROCESS) { + Set oldPaths = FILES_IN_PROCESS.get(oldFileName); + if (oldPaths != null) { + for (String oldPath : oldPaths) { + FILES_IN_PROCESS2.put(oldPath, newFileName); + } + FILES_IN_PROCESS2.put(oldFileName, newFileName); + FILES_IN_PROCESS2.put(newFileName, newFileName); + + oldPaths.add(oldFileName); + FILES_IN_PROCESS.remove(oldFileName); + FILES_IN_PROCESS.put(newFileName, oldPaths); + } + } + } + + /** + * Update the stored metadata with the iris and pupil position from the detector. + * + * @param metadata The metadata to be updated. + */ + public final void updateMetadata(final JpegMetadata metadata) { + if (mPupilRadius > 0 && mIrisRadius > mPupilRadius) { + metadata.setXCenter(mIrisXCenter); + metadata.setYCenter(mIrisYCenter); + metadata.setOverlayScaleFactor(mIrisRadius * 8 / 3); // MAGIC_NUMBER + + metadata.setPupilXOffset((mPupilXCenter - mIrisXCenter) / (2 * mIrisRadius)); + metadata.setPupilYOffset((mPupilYCenter - mIrisYCenter) / (2 * mIrisRadius)); + metadata.setPupilSize(mPupilRadius / mIrisRadius); + + metadata.addFlag(JpegMetadata.FLAG_OVERLAY_POSITION_DETERMINED_AUTOMATICALLY); + metadata.removeFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY); + } + } + + /** + * Find initial values of pupil center and pupil and iris radius. + */ + private void determineInitialParameterValues() { + Bitmap image = ImageUtil.resizeBitmap(mImage, PUPIL_SEARCH_RESOLUTIONS[0], false); + List pupilCenterInfoList = new ArrayList<>(); + + int[] pixels = new int[image.getWidth() * image.getHeight()]; + image.getPixels(pixels, 0, image.getWidth(), 0, 0, image.getWidth(), image.getHeight()); + + for (int x = image.getWidth() / 4; x < image.getWidth() * 3 / 4; x++) { // MAGIC_NUMBER + for (int y = image.getHeight() / 4; y < image.getHeight() * 3 / 4; y++) { // MAGIC_NUMBER + PupilCenterInfo pupilCenterInfo = new PupilCenterInfo(image, pixels, x, y, PupilCenterInfo.Phase.INITIAL); + pupilCenterInfo.collectCircleInfo(Integer.MAX_VALUE); + pupilCenterInfoList.add(pupilCenterInfo); + } + } + + float maxLeapValue = Float.MIN_VALUE; + PupilCenterInfo bestPupilCenter = null; + for (PupilCenterInfo pupilCenterInfo : pupilCenterInfoList) { + pupilCenterInfo.calculateStatistics(0); + if (pupilCenterInfo.mLeapValue > maxLeapValue) { + maxLeapValue = pupilCenterInfo.mLeapValue; + bestPupilCenter = pupilCenterInfo; + } + } + if (bestPupilCenter != null) { + mPupilXCenter = (float) bestPupilCenter.mXCenter / image.getWidth(); + mPupilYCenter = (float) bestPupilCenter.mYCenter / image.getHeight(); + mPupilRadius = (float) bestPupilCenter.mPupilRadius / Math.max(image.getWidth(), image.getHeight()); + mIrisXCenter = mPupilXCenter; + mIrisYCenter = mPupilYCenter; + mIrisRadius = (float) bestPupilCenter.mIrisRadius / Math.max(image.getWidth(), image.getHeight()); + } + } + + /** + * Refine the pupil position based on the previously found position and a higher resolution. + * + * @param resolution The resolution. + */ + private void refinePupilPosition(final int resolution) { + Bitmap image = ImageUtil.resizeBitmap(mImage, resolution, false); + List pupilCenterInfoList = new ArrayList<>(); + + int pupilXCenter = Math.round(mPupilXCenter * image.getWidth()); + int pupilYCenter = Math.round(mPupilYCenter * image.getHeight()); + int pupilRadius = Math.round(mPupilRadius * Math.max(image.getWidth(), image.getHeight())); + + boolean isStable = false; + int[] pixels = new int[image.getWidth() * image.getHeight()]; + image.getPixels(pixels, 0, image.getWidth(), 0, 0, image.getWidth(), image.getHeight()); + + for (int step = 0; step < MAX_REFINEMENT_STEPS && !isStable; step++) { + for (int x = pupilXCenter - 1; x <= pupilXCenter + 1; x++) { + for (int y = pupilYCenter - 1; y <= pupilYCenter + 1; y++) { + PupilCenterInfo pupilCenterInfo = new PupilCenterInfo(image, pixels, x, y, PupilCenterInfo.Phase.PUPIL_REFINEMENT); + pupilCenterInfo.collectCircleInfo((int) (pupilRadius + MAX_REFINEMENT_STEPS + MAX_LEAP_WIDTH * resolution)); + pupilCenterInfoList.add(pupilCenterInfo); + } + } + + float maxLeapValue = Float.MIN_VALUE; + PupilCenterInfo bestPupilCenter = null; + for (PupilCenterInfo pupilCenterInfo : pupilCenterInfoList) { + pupilCenterInfo.calculateStatistics(pupilRadius); + if (pupilCenterInfo.mLeapValue > maxLeapValue) { + maxLeapValue = pupilCenterInfo.mLeapValue; + bestPupilCenter = pupilCenterInfo; + } + } + + isStable = bestPupilCenter == null + || (bestPupilCenter.mXCenter == pupilXCenter && bestPupilCenter.mYCenter == pupilYCenter + && bestPupilCenter.mPupilRadius == pupilRadius); + if (bestPupilCenter != null) { + pupilXCenter = bestPupilCenter.mXCenter; + pupilYCenter = bestPupilCenter.mYCenter; + pupilRadius = bestPupilCenter.mPupilRadius; + } + } + + mPupilXCenter = (float) pupilXCenter / image.getWidth(); + mPupilYCenter = (float) pupilYCenter / image.getHeight(); + mPupilRadius = (float) pupilRadius / Math.max(image.getWidth(), image.getHeight()); + } + + /** + * Refine the iris position based on the previously found position. + */ + private void refineIrisPosition() { + IrisBoundary irisBoundary = new IrisBoundary(mImage, + (int) (mImage.getWidth() * mIrisXCenter), + (int) (mImage.getHeight() * mIrisYCenter), + (int) (Math.max(mImage.getWidth(), mImage.getHeight()) * mIrisRadius)); + + irisBoundary.analyzeBoundary(); + + mIrisXCenter = (float) irisBoundary.mXCenter / mImage.getWidth(); + mIrisYCenter = (float) irisBoundary.mYCenter / mImage.getHeight(); + mIrisRadius = (float) irisBoundary.mRadius / Math.max(mImage.getWidth(), mImage.getHeight()); + } + + /** + * The collected info about the circles around a potential pupil center. + */ + private static final class PupilCenterInfo { + /** + * The x coordinate of the center. + */ + private int mXCenter; + /** + * The y coordinate of the center. + */ + private int mYCenter; + /** + * The calculated pupil radius for this center. + */ + private int mPupilRadius = 0; + /** + * The calculated iris radius for this center. + */ + private int mIrisRadius = 0; + /** + * The image. + */ + private Bitmap mImage; + /** + * The image pixels. + */ + private int[] mPixels; + /** + * The phase in which the info is used. + */ + private Phase mPhase; + + /** + * The information about the circles around this point. + */ + private Map mCircleInfos = new HashMap<>(); + + /** + * The brightness leap value for this center. + */ + private float mLeapValue = Float.MIN_VALUE; + + /** + * Create a PupilCenterInfo with certain coordinates. + * + * @param image the image. + * @param pixels the image pixels. + * @param xCoord The x coordinate. + * @param yCoord The y coordinate. + * @param phase The phase in which the info is used. + */ + private PupilCenterInfo(final Bitmap image, final int[] pixels, final int xCoord, final int yCoord, final Phase phase) { + mXCenter = xCoord; + mYCenter = yCoord; + mImage = image; + mPixels = pixels; + mPhase = phase; + } + + /** + * Collect the information of all circles around the center. + * + * @param maxRelevantRadius The maximal circle radius considered + */ + private void collectCircleInfo(final int maxRelevantRadius) { + int width = mImage.getWidth(); + int maxPossibleRadius = Math.min( + Math.min(mImage.getWidth() - 1 - mXCenter, mXCenter), + Math.min(mImage.getHeight() - 1 - mYCenter, mYCenter)); + int maxRadius = Math.min(maxRelevantRadius, maxPossibleRadius); + long maxRadius2 = (maxRadius + 1) * (maxRadius + 1); + for (int x = mXCenter - maxRadius; x <= mXCenter + maxRadius; x++) { + for (int y = mYCenter - maxRadius; y <= mYCenter + maxRadius; y++) { + long d2 = (x - mXCenter) * (x - mXCenter) + (y - mYCenter) * (y - mYCenter); + if (d2 <= maxRadius2) { + int d = (int) Math.round(Math.sqrt(d2)); + int brightness = getBrightness(mPixels[y * width + x]); + // short brightness = getBrightness(mImage.getPixel(x, y)); + addInfo(d, brightness); + } + } + } + } + + /** + * Get a brightness value from a color. + * + * @param color The color + * @return The brightness value. + */ + private static int getBrightness(final int color) { + int min = Math.min(Math.min(Color.red(color), Color.green(color)), Color.blue(color)); + int sum = Color.red(color) + Color.green(color) + Color.blue(color); + // Ensure that colors count more than dark grey, but white counts more then colors. + return sum - min; + } + + /** + * Add pixel info for another pixel. + * + * @param distance The distance of the pixel. + * @param brightness The brightness of the pixel. + */ + private void addInfo(final int distance, final int brightness) { + CircleInfo circleInfo = mCircleInfos.get(distance); + if (circleInfo == null) { + circleInfo = new CircleInfo(distance); + mCircleInfos.put(distance, circleInfo); + } + circleInfo.addBrightness(brightness); + } + + /** + * Do statistical calculations after all brightnesses are available. + * + * @param baseRadius the base radius to be used in refinement phases. + */ + private void calculateStatistics(final int baseRadius) { + // Base calculations for each circle. + for (CircleInfo circleInfo : mCircleInfos.values()) { + circleInfo.calculateStatistics(); + } + + int resolution = Math.max(mImage.getWidth(), mImage.getHeight()); + int maxRadius = mPhase == Phase.INITIAL + ? mCircleInfos.size() - 1 + : Math.min(mCircleInfos.size() - 1, baseRadius + MAX_REFINEMENT_STEPS + (int) (MAX_LEAP_WIDTH * resolution)); + int minRadius = mPhase == Phase.INITIAL ? 0 + : Math.max(0, baseRadius - MAX_REFINEMENT_STEPS - (int) (MAX_LEAP_WIDTH * resolution)); + + // Calculate the minimum of medians outside each circle. + float innerQuantileSum = 0; + float[] innerDarkness = new float[mCircleInfos.size()]; + + for (int i = minRadius; i <= maxRadius; i++) { + float currentQuantile = mCircleInfos.get(Integer.valueOf(i)).getQuantile(MIN_BLACK_QUOTA); + innerQuantileSum += currentQuantile * i; + innerDarkness[i] = i == 0 ? 0 : 2 * innerQuantileSum / (i * (i + 1)); + } + + List relevantPupilCircles = new ArrayList<>(); + List relevantIrisCircles = new ArrayList<>(); + maxRadius = mPhase == Phase.INITIAL + ? mCircleInfos.size() - 2 + : Math.min(mCircleInfos.size() - 2, baseRadius + MAX_REFINEMENT_STEPS); + minRadius = mPhase == Phase.INITIAL ? (int) (resolution * MIN_PUPIL_RADIUS) + : Math.max(1, baseRadius - MAX_REFINEMENT_STEPS); + + if (mPhase == Phase.INITIAL || mPhase == Phase.PUPIL_REFINEMENT) { + // determine pupil leap + for (int i = minRadius; i <= maxRadius; i++) { + float pupilLeapValue = 0; + int maxLeapDistance = Math.min(Math.round(MAX_LEAP_WIDTH * resolution), + Math.min(i / 2, (mCircleInfos.size() - 1 - i) / 2)); + for (int j = 1; j <= maxLeapDistance; j++) { + float diff = mPhase == Phase.INITIAL + ? (ASSUMED_PUPIL_BRIGHTNESS + getMinMaxQuantile(MAX_BLACK_QUOTA, i + j, i + j + maxLeapDistance, false)) + / (ASSUMED_PUPIL_BRIGHTNESS + + getMinMaxQuantile(MIN_BLACK_QUOTA, i - j - Math.min(maxLeapDistance, Math.max(j, 2)), i - j, true)) + - 1 + : (ASSUMED_PUPIL_BRIGHTNESS + getMinMaxQuantile(MAX_BLACK_QUOTA, i + j, i + j + maxLeapDistance, false)) + / (ASSUMED_PUPIL_BRIGHTNESS + + getMinMaxQuantile(MIN_BLACK_QUOTA, i - Math.min(maxLeapDistance, Math.max(j, 2)), i, true)) + - 1; + if (diff > MIN_LEAP_DIFF) { + // prefer big jumps in small radius difference. + float newLeapValue = (float) (diff / Math.pow(j, 0.8)); // MAGIC_NUMBER + if (newLeapValue > pupilLeapValue) { + pupilLeapValue = newLeapValue; + } + } + } + if (pupilLeapValue > 0) { + CircleInfo circleInfo = mCircleInfos.get(Integer.valueOf(i)); + // prefer big, dark circles + circleInfo.mPupilLeapValue = (float) (Math.sqrt(i) * pupilLeapValue / innerDarkness[i]); + relevantPupilCircles.add(circleInfo); + } + } + } + + if (mPhase == Phase.INITIAL || mPhase == Phase.IRIS_REFINEMENT) { + // determine iris leap + for (int i = minRadius; i <= maxRadius; i++) { + float irisLeapValue = 0; + float irisQuantileSum = 0; + int maxLeapDistance = Math.min(Math.round(MAX_LEAP_WIDTH * resolution), + Math.min(i, mCircleInfos.size() - 1 - i)); + for (int j = 1; j <= maxLeapDistance; j++) { + irisQuantileSum += + (mCircleInfos.get(Integer.valueOf(i + j)).getQuantile(1 - MIN_WHITE_QUOTA) + - mCircleInfos.get(Integer.valueOf(i - j)).getQuantile(1 - MIN_WHITE_QUOTA) + + mCircleInfos.get(Integer.valueOf(i + j)).getQuantile(1 - MIN_WHITE_QUOTA2) + - mCircleInfos.get(Integer.valueOf(i - j)).getQuantile(1 - MIN_WHITE_QUOTA2)) + / (2 * Math.sqrt(j)); + if (irisQuantileSum > 0) { + // prefer big jumps in small radius difference. + float newLeapValue = irisQuantileSum / j; + if (newLeapValue > irisLeapValue) { + irisLeapValue = newLeapValue; + } + } + } + if (irisLeapValue > 0) { + CircleInfo circleInfo = mCircleInfos.get(Integer.valueOf(i)); + // prefer big radius in order to prevent selection of small spots. + // prefer dark inner area + circleInfo.mIrisLeapValue = irisLeapValue; + relevantIrisCircles.add(circleInfo); + } + } + } + + switch (mPhase) { + case INITIAL: + for (CircleInfo pupilCircleInfo : relevantPupilCircles) { + for (CircleInfo irisCircleInfo : relevantIrisCircles) { + if (irisCircleInfo.mRadius - pupilCircleInfo.mRadius >= resolution + * MIN_IRIS_PUPIL_DISTANCE) { + float newLeapValue = pupilCircleInfo.mPupilLeapValue * (1 + irisCircleInfo.mIrisLeapValue); + if (newLeapValue > mLeapValue) { + mLeapValue = newLeapValue; + mPupilRadius = pupilCircleInfo.mRadius; + mIrisRadius = irisCircleInfo.mRadius; + } + } + } + } + break; + case PUPIL_REFINEMENT: + for (CircleInfo pupilCircleInfo : relevantPupilCircles) { + float newLeapValue = pupilCircleInfo.mPupilLeapValue; + if (newLeapValue > mLeapValue) { + mLeapValue = newLeapValue; + mPupilRadius = pupilCircleInfo.mRadius; + } + } + break; + case IRIS_REFINEMENT: + default: + for (CircleInfo irisCircleInfo : relevantIrisCircles) { + float newLeapValue = irisCircleInfo.mIrisLeapValue; + if (newLeapValue > mLeapValue) { + mLeapValue = newLeapValue; + mIrisRadius = irisCircleInfo.mRadius; + } + } + break; + } + } + + /** + * Get the minimum p-quantile for a certain set of radii. + * + * @param p The quantile parameter. + * @param fromRadius The start radius. + * @param toRadius The end radius. + * @param max if true, the maximum is returned, otherwise the minimum. + * @return The minimum quantile. + */ + private float getMinMaxQuantile(final float p, final int fromRadius, final int toRadius, final boolean max) { + float result = max ? Float.MIN_VALUE : Float.MAX_VALUE; + for (int radius = fromRadius; radius <= toRadius; radius++) { + float newValue = mCircleInfos.get(Integer.valueOf(radius)).getQuantile(p); + if ((!max && newValue < result) || (max && newValue > result)) { + result = newValue; + } + } + return result; + } + + /** + * The phase in which the algorithm is. + */ + private enum Phase { + /** + * Initial positioning of pupil and iris. + */ + INITIAL, + /** + * Refinement of pupil position. + */ + PUPIL_REFINEMENT, + /** + * Refinement of iris position. + */ + IRIS_REFINEMENT + } + } + + /** + * Class for storing information about a circle of points. + */ + private static final class CircleInfo { + /** + * Create a pixelInfo with certain coordinates. + * + * @param radius The radius. + */ + private CircleInfo(final int radius) { + mRadius = radius; + } + + /** + * The radius. + */ + private int mRadius; + /** + * The brightnesses. + */ + private List mBrightnesses = new ArrayList<>(); + /** + * The brightness leap at this radius used for pupil identification. + */ + private float mPupilLeapValue; + /** + * The brightness leap at this radius used for iris identification. + */ + private float mIrisLeapValue; + + /** + * Add a brightness to the information of this circle. + * + * @param brightness the brightness. + */ + private void addBrightness(final int brightness) { + mBrightnesses.add(brightness); + } + + /** + * Do statistical calculations after all brightnesses are available. Here, only sorting is required. + */ + private void calculateStatistics() { + Collections.sort(mBrightnesses); + } + + /** + * Get the p-quantile of the brightnesses. Prerequisite: calculateStatistics must have been run before. + * + * @param p the quantile parameter. + * @return the p-quantile of the brightnesses (not considering equality). + */ + private int getQuantile(final float p) { + return mBrightnesses.get((int) (mBrightnesses.size() * p)); + } + } + + /** + * Class for collecting information about the iris boundary. + */ + private static final class IrisBoundary { + /** + * The image. + */ + private Bitmap mImage; + + /** + * The x coordinate of the center. + */ + private int mXCenter; + /** + * The y coordinate of the center. + */ + private int mYCenter; + /** + * The iris radius. + */ + private int mRadius = 0; + + /** + * The points on the left side of the iris boundary (map from y to x coordinate). + */ + private Map mLeftPoints = new HashMap<>(); + /** + * The points on the right side of the iris boundary (map from y to x coordinate). + */ + private Map mRightPoints = new HashMap<>(); + + /** + * Initialize the IrisBoundary. + * + * @param image The image. + * @param xCenter the initial x coordinate of the center. + * @param yCenter the initial y coordinate of the center. + * @param radius the initial iris radius. + */ + private IrisBoundary(final Bitmap image, final int xCenter, final int yCenter, final int radius) { + mImage = image; + mXCenter = xCenter; + mYCenter = yCenter; + mRadius = radius; + } + + /** + * Search points on the iris boundary. + */ + private void determineBoundaryPoints() { + for (int yCoord = mYCenter; yCoord <= mYCenter + mRadius * IRIS_BOUNDARY_SEARCH_RANGE && yCoord < mImage.getHeight(); yCoord++) { + determineBoundaryPoints(yCoord); + } + + for (int yCoord = mYCenter - 1; yCoord >= mYCenter - mRadius * IRIS_BOUNDARY_SEARCH_RANGE && yCoord >= 0; yCoord--) { + determineBoundaryPoints(yCoord); + } + } + + /** + * Determine the boundary points for a certain y coordinate. + * + * @param yCoord The y coordinate for which to find the boundary points. + * @return true if a boundary point has been found. + */ + private boolean determineBoundaryPoints(final int yCoord) { + int xDistanceRange = Math.round(IRIS_BOUNDARY_UNCERTAINTY_FACTOR * mRadius); + int xDistanceMinRange = Math.round(IRIS_BOUNDARY_MIN_RANGE * mRadius); + boolean found = false; + + while (!found && xDistanceRange >= xDistanceMinRange) { + found = determineBoundaryPoints(yCoord, xDistanceRange); + xDistanceRange *= IRIS_BOUNDARY_RETRY_FACTOR; + } + return found; + } + + /** + * Determine the boundary points for a certain y coordinate. + * + * @param yCoord The y coordinate for which to find the boundary points. + * @param xDistanceRange the horizontal range which is considered. + * @return true if a boundary point has been found. + */ + private boolean determineBoundaryPoints(final int yCoord, final int xDistanceRange) { + int yDiff = yCoord - mYCenter; + if (Math.abs(yDiff) > IRIS_BOUNDARY_SEARCH_RANGE * mRadius) { + return false; + } + + int expectedXDistance = (int) Math.round(Math.sqrt(mRadius * mRadius - yDiff * yDiff)); + + // Left side - calculate average brightness + float brightnessSum = 0; + int leftBoundary = Math.max(mXCenter - expectedXDistance - xDistanceRange, 0); + int rightBoundary = Math.min(mXCenter - expectedXDistance + xDistanceRange, mImage.getWidth() - 1); + for (int x = leftBoundary; x <= rightBoundary; x++) { + brightnessSum += getBrightness(mImage.getPixel(x, yCoord)); + } + float avgBrightness = brightnessSum / (2 * xDistanceRange + 1); + + // Left side - find transition from light to dark + int leftCounter = 0; + int rightCounter = 0; + while (leftBoundary < rightBoundary) { + if (rightCounter > leftCounter) { + if (getBrightness(mImage.getPixel(leftBoundary++, yCoord)) < avgBrightness) { + leftCounter++; + } + } + else { + if (getBrightness(mImage.getPixel(rightBoundary--, yCoord)) > avgBrightness) { + rightCounter++; + } + } + } + if (leftCounter > IRIS_BOUNDARY_WRONG_BRIGHTNESS_QUOTA * xDistanceRange) { + return false; + } + + // Right side - calculate average brightness + float brightnessSum2 = 0; + int leftBoundary2 = Math.max(mXCenter + expectedXDistance - xDistanceRange, 0); + int rightBoundary2 = Math.min(mXCenter + expectedXDistance + xDistanceRange, mImage.getWidth() - 1); + for (int x = leftBoundary2; x <= rightBoundary2; x++) { + brightnessSum2 += getBrightness(mImage.getPixel(x, yCoord)); + } + float avgBrightness2 = brightnessSum2 / (2 * xDistanceRange + 1); + + // Right side - find transition from light to dark + int leftCounter2 = 0; + int rightCounter2 = 0; + while (leftBoundary2 < rightBoundary2) { + if (leftCounter2 > rightCounter2) { + if (getBrightness(mImage.getPixel(rightBoundary2--, yCoord)) < avgBrightness2) { + rightCounter2++; + } + } + else { + if (getBrightness(mImage.getPixel(leftBoundary2++, yCoord)) > avgBrightness2) { + leftCounter2++; + } + } + } + if (rightCounter2 > IRIS_BOUNDARY_WRONG_BRIGHTNESS_QUOTA * xDistanceRange) { + return false; + } + + mLeftPoints.put(yCoord, rightBoundary); + mRightPoints.put(yCoord, leftBoundary2); + return true; + } + + /** + * Determine the iris center and radius from the iris boundary points. + */ + private void analyzeBoundary() { + determineBoundaryPoints(); + if (mLeftPoints.size() > IRIS_BOUNDARY_MIN_BOUNDARY_POINTS) { + determineXCenter(); + determineYCenter(); + determineRadius(); + } + } + + /** + * Determine the x center from the boundary points. + */ + private void determineXCenter() { + // Determine x center as median of the boundary mid points + List xSumValues = new ArrayList<>(); + for (Integer yCoord : mLeftPoints.keySet()) { + xSumValues.add(mLeftPoints.get(yCoord) + mRightPoints.get(yCoord)); + } + + Collections.sort(xSumValues); + + mXCenter = xSumValues.get(xSumValues.size() / 2) / 2; + } + + /** + * Determine the y center from the boundary points, knowing the x center. + */ + private void determineYCenter() { + // Consider the sum of left and right distance. + Map> distanceSums = new HashMap<>(); + for (Integer y : mLeftPoints.keySet()) { + int sum = mRightPoints.get(y) - mLeftPoints.get(y); + List listForSum = distanceSums.get(sum); + if (listForSum == null) { + listForSum = new ArrayList<>(); + distanceSums.put(sum, listForSum); + } + listForSum.add(y); + } + + // Sort distances in descending order + List distances = new ArrayList<>(distanceSums.keySet()); + Collections.sort(distances); + Collections.reverse(distances); + + int count = 0; + int sum = 0; + int countUntil = (int) (IRIS_BOUNDARY_POINTS_CONSIDERED_FOR_YCENTER * mLeftPoints.size()); + for (Integer distance : distances) { + for (int y : distanceSums.get(distance)) { + sum += y; + count++; + } + if (count >= countUntil) { + break; + } + } + + mYCenter = sum / count; + } + + /** + * Determine the radius from boundary points, after center is known. + */ + private void determineRadius() { + float sum = 0; + for (Integer y : mLeftPoints.keySet()) { + int yDistance = y - mYCenter; + int xDistance = mLeftPoints.get(y) - mXCenter; + sum += Math.sqrt(xDistance * xDistance + yDistance * yDistance); + } + for (Integer y : mRightPoints.keySet()) { + int yDistance = y - mYCenter; + int xDistance = mRightPoints.get(y) - mXCenter; + sum += Math.sqrt(xDistance * xDistance + yDistance * yDistance); + } + + mRadius = Math.round(sum / (2 * mLeftPoints.size())); + } + + /** + * Get a brightness value from a color. + * + * @param color The color + * @return The brightness value. + */ + private static int getBrightness(final int color) { + // Blue seems to be particulary helpful in the separation. + return Math.min(Math.min(Color.red(color), Color.green(color)), Color.blue(color)) + Color.blue(color); + } + + } + +}