/ocr/ocrservice/src/com/googlecode/eyesfree/ocr/intent/NoisyDetector.java

http://eyes-free.googlecode.com/ · Java · 200 lines · 116 code · 47 blank · 37 comment · 28 complexity · 1910c47b65469071f7dcc2bc4f8663c9 MD5 · raw file

  1. /*
  2. * Copyright (C) 2011 Google Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.googlecode.eyesfree.ocr.intent;
  17. import android.content.Context;
  18. import android.graphics.RectF;
  19. import android.media.AudioManager;
  20. import android.media.SoundPool;
  21. import com.googlecode.eyesfree.env.Metronome;
  22. import com.googlecode.eyesfree.ocr.R;
  23. import java.util.ArrayList;
  24. /**
  25. * @author alanv@google.com (Alan Viverette)
  26. */
  27. public class NoisyDetector {
  28. /* Minimum height of a text item in pixels */
  29. private static final int MIN_HEIGHT = 30;
  30. /* Percent error allowed for height threshold */
  31. private static final float HEIGHT_ERROR = 0.4f;
  32. private boolean mPaused;
  33. private Metronome mMetronome;
  34. private SoundPool mSoundPool;
  35. private int mBeepId;
  36. public NoisyDetector(Context context) {
  37. mPaused = false;
  38. mSoundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
  39. mBeepId = mSoundPool.load(context, R.raw.loud_beep, 1);
  40. mMetronome = new Metronome(context, R.raw.click);
  41. }
  42. public void start() {
  43. mPaused = false;
  44. mMetronome.start();
  45. }
  46. public void pause() {
  47. mPaused = true;
  48. mMetronome.stop();
  49. }
  50. public void onTextDetected(RectF pixaBound, ArrayList<RectF> pixBounds) {
  51. if (mPaused)
  52. return;
  53. // Compute whether this is likely a text block
  54. int likelyText = 0;
  55. int likelySmallText = 0;
  56. for (int i = 0; i < pixBounds.size() && likelyText < 4; i++) {
  57. if (pixBounds.get(i) == null)
  58. break;
  59. if (BoundsClassifier.isLikelyTextRect(pixBounds, i)) {
  60. // Lines must be at least MIN_HEIGHT pixels high
  61. if (pixBounds.get(i).height() < MIN_HEIGHT) {
  62. // Allow a tolerance of HEIGHT_ERROR percent around
  63. // MIN_HEIGHT
  64. if (BoundsClassifier.computeError(pixBounds.get(i).height(), MIN_HEIGHT)
  65. < HEIGHT_ERROR) {
  66. likelyText++;
  67. }
  68. likelySmallText++;
  69. } else {
  70. likelyText++;
  71. }
  72. }
  73. }
  74. if (mPaused)
  75. return;
  76. if (pixBounds.size() > 0) {
  77. float pixaArea = pixaBound.width() * pixaBound.height();
  78. float pixaCenterX = pixaBound.centerX();
  79. float textAreaL = 0.0f;
  80. float textAreaR = 0.0f;
  81. for (RectF textBounds : pixBounds) {
  82. float textArea = textBounds.width() * textBounds.height();
  83. float textCenterX = textBounds.centerX();
  84. float offsetRatio = (pixaCenterX - textCenterX) / textBounds.width();
  85. float leftPortion = Math.min(1.0f, Math.max(0.0f, 0.5f - offsetRatio));
  86. float rightPortion = 1.0f - leftPortion;
  87. textAreaL += leftPortion * textArea;
  88. textAreaR += rightPortion * textArea;
  89. }
  90. float volumeL = Math.min(1.0f, 5.0f * textAreaL / pixaArea);
  91. float volumeR = Math.min(1.0f, 5.0f * textAreaR / pixaArea);
  92. long delay = (long) (300 / Math.log(1 + pixBounds.size()));
  93. // Make clicking noises according to how much area is text
  94. mMetronome.setVolume(volumeL, volumeR);
  95. mMetronome.setDelay(delay);
  96. } else {
  97. mMetronome.setDelay(3000);
  98. }
  99. if (mPaused)
  100. return;
  101. // Vibrate if we're looking at text
  102. if (likelySmallText > likelyText && likelySmallText > 4) {
  103. mSoundPool.play(mBeepId, 0.3f, 0.3f, 1, 0, 1.0f);
  104. } else if (likelyText >= 4) {
  105. mSoundPool.play(mBeepId, 1.0f, 1.0f, 1, 0, 1.0f);
  106. }
  107. }
  108. /**
  109. * @author alanv@google.com (Alan Viverette)
  110. */
  111. private static class BoundsClassifier {
  112. /** Minimum number of matching lines. */
  113. private static final int MIN_PAIRS = 1;
  114. /** Minimum width/height ratio. */
  115. private static final int MIN_WRATIO = 10;
  116. /** Top offset in height units. */
  117. private static final float TOP_ERROR = 2.0f;
  118. /** Left offset in height units. */
  119. private static final float LEFT_ERROR = 1.0f;
  120. public static boolean isLikelyTextRect(ArrayList<RectF> results, int position) {
  121. RectF bounds = results.get(position);
  122. // Lines must be at least MIN_WRATIO times as wide as they are tall
  123. if (bounds.width() / bounds.height() < MIN_WRATIO) {
  124. return false;
  125. }
  126. int pairs = 0;
  127. for (int i = 0; i < results.size() && pairs < MIN_PAIRS; i++) {
  128. if (i == position)
  129. continue;
  130. if (results.get(i) == null)
  131. break;
  132. RectF otherBounds = results.get(i);
  133. // Pairs must be within HEIGHT_ERROR similar height
  134. float hError = computeError(bounds.height(), otherBounds.height());
  135. if (hError > HEIGHT_ERROR)
  136. continue;
  137. // Pairs must have similar left margins within LEFT_ERROR heights
  138. float xDist = Math.abs(bounds.left - otherBounds.left);
  139. if (xDist > LEFT_ERROR * bounds.height())
  140. continue;
  141. // Pairs must be within SPACING_ERROR heights apart
  142. float yDist = Math.abs(bounds.top - otherBounds.top);
  143. if (yDist > TOP_ERROR * bounds.height())
  144. continue;
  145. pairs++;
  146. }
  147. return pairs >= MIN_PAIRS;
  148. }
  149. public static float computeError(float expected, float experimental) {
  150. return Math.abs((experimental - expected) / expected);
  151. }
  152. }
  153. }