class_docstr: Class for image processing. Methods for detecting lines in an image and clustering them. Helper methods for
  drawing.
docstr: Find line clusters for detected lines in direction <direction> using clustering method <method> (from clustering
  module). <kwargs> will be passed to <method>.
  Optionally remove clusters with high standard deviation values (<remove_cluster_sections_stddev_thresh>).
  Optionally remove clusters with empty sections, meaning that there are only a few text boxes inside these
  sections.
  <remove_empty_cluster_sections_use_texts>: list of text boxes for empty cluster section removal
  <remove_empty_cluster_sections_n_texts_ratio>: ratio for min. texts threshold
  <remove_empty_cluster_sections_scaling>: text box coord. system scaling in relation to image coord. system
docstr_tok: ["Find","line","clusters","for","detected","lines","in","direction","<","direction",">","using","clustering","method","<","method",">","(","from","clustering","module",")",".","<","kwargs",">","will","be","passed","to","<","method",">",".","Optionally","remove","clusters","with","high","standard","deviation","values","(","<","remove_cluster_sections_stddev_thresh",">",")",".","Optionally","remove","clusters","with","empty","sections",",","meaning","that","there","are","only","a","few","text","boxes","inside","these","sections",".","<","remove_empty_cluster_sections_use_texts",">",":","list","of","text","boxes","for","empty","cluster","section","removal","<","remove_empty_cluster_sections_n_texts_ratio",">",":","ratio","for","min",".","texts","threshold","<","remove_empty_cluster_sections_scaling",">",":","text","box","coord",".","system","scaling","in","relation","to","image","coord",".","system"]
code: def find_clusters(self, direction, method, **kwargs):
      """
      Find line clusters for detected lines in direction <direction> using clustering method <method> (from clustering
      module). <kwargs> will be passed to <method>.
      Optionally remove clusters with high standard deviation values (<remove_cluster_sections_stddev_thresh>).
      Optionally remove clusters with empty sections, meaning that there are only a few text boxes inside these
      sections.
      <remove_empty_cluster_sections_use_texts>: list of text boxes for empty cluster section removal
      <remove_empty_cluster_sections_n_texts_ratio>: ratio for min. texts threshold
      <remove_empty_cluster_sections_scaling>: text box coord. system scaling in relation to image coord. system
      """
      remove_empty_cluster_sections_use_texts = kwargs.pop(
          "remove_empty_cluster_sections_use_texts", None
      )
      if remove_empty_cluster_sections_use_texts is not None:
          remove_empty_cluster_sections_n_texts_ratio = kwargs.pop(
              "remove_empty_cluster_sections_n_texts_ratio"
          )
          remove_empty_cluster_sections_scaling = kwargs.pop(
              "remove_empty_cluster_sections_scaling"
          )
          remove_empty_cluster_sections_clust_center_fn = kwargs.pop(
              "remove_empty_cluster_sections_clust_center_fn", np.median
          )
  
      remove_cluster_sections_stddev_thresh = kwargs.pop(
          "remove_cluster_sections_stddev_thresh", None
      )
  
      if not self.lines_hough:
          raise ValueError(
              "no lines in lines_hough. did you call detect_lines before?"
          )
  
      if direction not in (DIRECTION_HORIZONTAL, DIRECTION_VERTICAL):
          raise ValueError(
              "invalid value for 'direction': '%s'" % direction
          )
  
      if not callable(method):
          raise ValueError("'method' must be callable")
  
      lines_in_dir = [l for l in self.lines_hough if l[3] == direction]
  
      if len(lines_in_dir) == 0:  # no lines in that direction
          return []
  
      lines_ab = self.ab_lines_from_hough_lines(lines_in_dir)
  
      coord_idx = 0 if direction == DIRECTION_VERTICAL else 1
      positions = np.array(
          [(l[0][coord_idx] + l[1][coord_idx]) / 2 for l in lines_ab]
      )
  
      clusters = method(positions, **kwargs)
  
      if type(clusters) != list:
          raise ValueError(
              "'method' returned invalid clusters (must be list)"
          )
  
      if len(clusters) > 0 and type(clusters[0]) != np.ndarray:
          raise ValueError(
              "'method' returned invalid cluster elements (must be list of numpy.ndarray objects)"
          )
  
      clusters_w_vals = zip_clusters_and_values(clusters, positions)
  
      if remove_cluster_sections_stddev_thresh is not None:
          clusters_w_vals = [
              (ind, vals)
              for ind, vals in clusters_w_vals
              if np.std(vals) < remove_cluster_sections_stddev_thresh
          ]
  
      if remove_empty_cluster_sections_use_texts is not None:
          if direction == DIRECTION_HORIZONTAL:
              t_attr = "top", "bottom"
          else:
              t_attr = "left", "right"
  
          clusters_w_vals_and_centers = [
              (
                  ind,
                  vals,
                  remove_empty_cluster_sections_clust_center_fn(vals)
                  / remove_empty_cluster_sections_scaling,
              )
              for ind, vals in clusters_w_vals
          ]
          clusters_w_vals_and_centers = sorted(
              clusters_w_vals_and_centers, key=lambda x: x[2]
          )
          clusters_w_vals_and_n_texts = []
          prev_center = -1
          for ind, vals, center in clusters_w_vals_and_centers:
              texts = [
                  t
                  for t in remove_empty_cluster_sections_use_texts
                  if prev_center < t[t_attr[0]] <= center
                  or prev_center < t[t_attr[1]] <= center
              ]
              clusters_w_vals_and_n_texts.append(
                  (ind, vals, len(texts))
              )
              prev_center = center
          assert len(clusters_w_vals_and_n_texts) == len(
              clusters_w_vals
          )
  
          max_n_texts = np.median(
              [tup[2] for tup in clusters_w_vals_and_n_texts]
          )
          n_texts_thresh = round(
              max_n_texts * remove_empty_cluster_sections_n_texts_ratio
          )
          clusters_w_vals_filtered = []
          prev_clust = None
          for ind, vals, n_texts in clusters_w_vals_and_n_texts:
              if n_texts >= n_texts_thresh:
                  if (
                      not clusters_w_vals_filtered
                      and prev_clust is not None
                  ):
                      clusters_w_vals_filtered.append(prev_clust)
  
                  clusters_w_vals_filtered.append((ind, vals))
              prev_clust = (ind, vals)
          assert len(clusters_w_vals_filtered) <= len(clusters_w_vals)
          clusters_w_vals = clusters_w_vals_filtered
  
      return clusters_w_vals
  
imports: from logging import warning
  from math import degrees
  import numpy
  import cv2
  from pdftabextract.common import ROTATION, SKEW_X, SKEW_Y, DIRECTION_HORIZONTAL, DIRECTION_VERTICAL
  from pdftabextract.geom import normalize_angle, project_polarcoord_lines
  from pdftabextract.clustering import zip_clusters_and_values, find_clusters_1d_break_dist, calc_cluster_centers_1d
label_desc: Use image node_id 8 for calling the ImageProc obj's underlying member method code with example usage: obj.find_clusters(direction, method) and returns: clusters_w_vals, list