class_docstr: Class for image processing. Methods for detecting lines in an image and clustering them. Helper methods for
  drawing.
docstr: Find page rotation or horizontal/vertical skew using detected lines in <lines>. The lines list must consist
  of arrays with the line rotation "theta" at array index 1 like the returned list from detect_lines().
  <rot_thresh> is the minimum threshold in radians for a rotation to be counted as such.
  <rot_same_dir_thresh> is the maximum threshold for the difference between horizontal and vertical line
  rotation.
  <omit_on_rot_thresh> is an optional threshold to filter out "stray" lines whose angle is too far apart from
  the median angle of all other lines that go in the same direction.
  <only_direction> optional parameter: only use lines in this direction to find out the rotation/skew
docstr_tok: ["Find","page","rotation","or","horizontal\/vertical","skew","using","detected","lines","in","<","lines",">",".","The","lines","list","must","consist","of","arrays","with","the","line","rotation","``","theta","''","at","array","index","1","like","the","returned","list","from","detect_lines","(",")",".","<","rot_thresh",">","is","the","minimum","threshold","in","radians","for","a","rotation","to","be","counted","as","such",".","<","rot_same_dir_thresh",">","is","the","maximum","threshold","for","the","difference","between","horizontal","and","vertical","line","rotation",".","<","omit_on_rot_thresh",">","is","an","optional","threshold","to","filter","out","``","stray","''","lines","whose","angle","is","too","far","apart","from","the","median","angle","of","all","other","lines","that","go","in","the","same","direction",".","<","only_direction",">","optional","parameter",":","only","use","lines","in","this","direction","to","find","out","the","rotation\/skew"]
code: def find_rotation_or_skew(
      self,
      rot_thresh,
      rot_same_dir_thresh,
      omit_on_rot_thresh=None,
      only_direction=None,
  ):
      """
      Find page rotation or horizontal/vertical skew using detected lines in <lines>. The lines list must consist
      of arrays with the line rotation "theta" at array index 1 like the returned list from detect_lines().
      <rot_thresh> is the minimum threshold in radians for a rotation to be counted as such.
      <rot_same_dir_thresh> is the maximum threshold for the difference between horizontal and vertical line
      rotation.
      <omit_on_rot_thresh> is an optional threshold to filter out "stray" lines whose angle is too far apart from
      the median angle of all other lines that go in the same direction.
      <only_direction> optional parameter: only use lines in this direction to find out the rotation/skew
      """
      if not self.lines_hough:
          raise ValueError(
              "no lines present. did you run detect_lines()?"
          )
  
      if only_direction is not None:
          if only_direction not in (
              DIRECTION_HORIZONTAL,
              DIRECTION_VERTICAL,
          ):
              raise ValueError(
                  "invalid value for 'only_direction': '%s'"
                  % only_direction
              )
  
      # get the deviations
  
      hori_deviations = []  # deviation from unit vector in x-direction
      vert_deviations = []  # deviation from unit vector in y-direction
  
      lines_w_deviations = (
          [] if omit_on_rot_thresh is not None else None
      )
  
      for rho, theta, theta_norm, line_dir in self.lines_hough:
          if line_dir == DIRECTION_VERTICAL and (
              only_direction is None
              or only_direction == DIRECTION_VERTICAL
          ):
              deviation = -theta_norm
              if deviation < -PIHLF:
                  deviation += np.pi
              vert_deviations.append(-deviation)
          elif line_dir == DIRECTION_HORIZONTAL and (
              only_direction is None
              or only_direction == DIRECTION_HORIZONTAL
          ):
              deviation = PIHLF - theta_norm
              hori_deviations.append(-deviation)
          else:
              deviation = None
  
          if omit_on_rot_thresh is not None and deviation is not None:
              assert abs(deviation) <= PI4TH
              lines_w_deviations.append(
                  (rho, theta, theta_norm, line_dir, -deviation)
              )
  
      # get the medians
  
      if hori_deviations:
          median_hori_dev = np.median(hori_deviations)
          hori_rot_above_thresh = abs(median_hori_dev) > rot_thresh
      else:
          if only_direction is None:
              warning("no horizontal lines found")
          median_hori_dev = None
          hori_rot_above_thresh = False
  
      if vert_deviations:
          median_vert_dev = np.median(vert_deviations)
          vert_rot_above_thresh = abs(median_vert_dev) > rot_thresh
      else:
          if only_direction is None:
              warning("no vertical lines found")
          median_vert_dev = None
          vert_rot_above_thresh = False
  
      if omit_on_rot_thresh is not None:
          if only_direction is None:
              assert len(lines_w_deviations) == len(self.lines_hough)
          else:
              assert len(lines_w_deviations) == len(
                  [
                      l
                      for l in self.lines_hough
                      if l[3] == only_direction
                  ]
              )
          lines_filtered = []
          for (
              rho,
              theta,
              theta_norm,
              line_dir,
              deviation,
          ) in lines_w_deviations:
              dir_dev = (
                  median_hori_dev
                  if line_dir == DIRECTION_HORIZONTAL
                  else median_vert_dev
              )
              if (
                  dir_dev is None
                  or abs(abs(dir_dev) - abs(deviation))
                  < omit_on_rot_thresh
              ):
                  lines_filtered.append(
                      (rho, theta, theta_norm, line_dir)
                  )
          assert len(lines_filtered) <= len(self.lines_hough)
          self.lines_hough = lines_filtered
  
      if hori_rot_above_thresh and vert_rot_above_thresh:
          if (
              abs(median_hori_dev - median_vert_dev)
              < rot_same_dir_thresh
          ):
              return ROTATION, (median_hori_dev + median_vert_dev) / 2
          else:
              warning(
                  "horizontal / vertical rotation not in same direction (%f / %f)"
                  % (degrees(median_hori_dev), degrees(median_vert_dev))
              )
      elif hori_rot_above_thresh:
          return SKEW_Y, median_hori_dev
      elif vert_rot_above_thresh:
          return SKEW_X, median_vert_dev
  
      return None, None
  
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 7 for calling the ImageProc obj's underlying member method code with example usage: obj.find_rotation_or_skew(rot_thresh, rot_same_dir_thresh, omit_on_rot_thresh, only_direction) and returns: None,  None, ROTATION,  unknown, SKEW_Y,  median_hori_dev, SKEW_X,  median_vert_dev