/**
   * Этот метод выполняет обнаружение объектов.
   *
   * <p>В результате получаем множество возможных положений объектов.
   *
   * @return double наибольший наблюдаемый отклик.
   */
  private double detect(double scale) {

    LOG.debug("Detecting, scale = " + scale);

    // Выбираем каскад Хаара

    final HaarCascade cascade = getHaarCascade(scale);

    // Создаем кластеризатор

    KMeansClusterer kmeans = new KMeansClusterer(params.k);

    kmeans.setThreshold(params.threshold);

    // Шаг по осям будет зависеть от размера

    final int d = Math.max(1, (int) (scale / 4.0));

    // Haar Detection

    for (int y = ROI.getY1(); y < ROI.getY2() - cascade.height; y += d) {

      for (int x = ROI.getX1(); x < ROI.getX2() - cascade.width; x += d) {

        final double r = HaarCascadeEvaluator.evaluateHaarCascade(cascade, integralImage, x, y);

        if (r > 0.0) {

          kmeans.addPoint(x + cascade.width / 2, y + cascade.height / 2);
        }
      }
    }

    // Кластеризуем отклики

    kmeans.cluster();

    // Группируем кластеры

    kmeans.group();

    // Сохраняем обнаруженные возможные положения объектов

    double maxScore = 0;

    for (Cluster c : kmeans.getClusters()) {

      // Объектом становится отклик который ближе других к центру кластера

      final ClusterPoint p = kmeans.getNearestPoint(c);

      // Создаем и сохраняем объект

      objects.add(
          new HaarObject(
              new Rectangle(
                  p.getPoint().getX() - cascade.width / 2,
                  p.getPoint().getY() - cascade.height / 2,
                  cascade.width,
                  cascade.height),
              c.getM()));

      // Обновляем рекорд

      if (c.getM() > maxScore) {

        maxScore = c.getM();
      }
    }

    LOG.debug("Found " + objects.size() + " candidates, maximum score = " + maxScore);

    return maxScore;
  }
  /** @see ImageProcessor#process() */
  @Override
  public void process() {

    LOG.debug("Processing...");

    if (integralImage == null) {

      throw new RuntimeException("Integral image undefined!");
    }

    // Cleanup

    objects.clear();

    // Если ROI не определен, придется анализировать все изображение

    if (ROI == null) {

      ROI = new Rectangle(0, 0, integralImage.width - 1, integralImage.height - 1);
    }

    // Используется для оптимизации (alpha-pruning)

    double a = 0;

    // Обнаружение ведем от максимального масштаба к минимальному

    double scale =
        Math.min(ROI.getWidth() / params.cascade.width, ROI.getHeight() / params.cascade.height)
            * params.maxScale;

    do {

      final double r = detect(scale);

      if (params.detectBiggest) {

        // Pruning

        if (r > 0) {

          if (r > a) {

            a = r;

          } else break;
        }
      }

    } while ((scale -= params.scaleStep) >= params.minScale);

    if (params.detectBiggest) {

      // Сохраняем положение объекта с максимальным откликом

      ArrayList<HaarObject> t = new ArrayList<>();

      HaarObject mainObject = getMainObject();

      if (mainObject != null) {

        LOG.debug("Main object has score = " + mainObject.score);

        t.add(mainObject);
      }

      objects = t;

    } else {

      // Группируем обнаруженные возможные положения объектов

      groupByIntersection();
    }

    // Если надо выполняем трассировку откликов

    if (params.trace) {

      for (HaarObject object : objects) {

        final Rectangle r = object.rectangle;

        final HaarCascade cascade = getHaarCascade(r.getWidth() / (double) params.cascade.width);

        object.trace =
            HaarCascadeEvaluator.traceHaarCascade(cascade, integralImage, r.getX(), r.getY());
      }
    }

    LOG.debug("Done.");
  }