image = cv2.imread("0.0/01.pgm", cv2.COLOR_BGR2GRAY)


def get_coords_by_threshold(image: np.ndarray, threshold: int = 150) -> tuple:
    """Gets pixel coords with intensity more then 'theshold'."""
    coords = np.where(image > threshold)
    return coords


def delete_pixels_by_min_threshold(
        image: np.ndarray, min_threshold: int = 100
) -> np.ndarray:
    """Deletes coords from image and returns this image."""
    anti_coords = np.where(image < min_threshold)
    image[anti_coords] = 0
    return image


def get_neigh_nums_list(
        coords: tuple, kernel_size: int = 3
) -> list:
    """
    Gets list with number of neighborhoods by kernel.
    Returns list like [[x1, y1, N1], [x2, y2, N2]],
    Where N - neighborhood's number.
    """
    neigh_nums = []
    offset = (kernel_size - 1) // 2

    for x, y in zip(coords[0], coords[1]):
        if x < 0 + offset or x > image.shape[0] - offset:
            continue
        if y < 0 + offset or y > image.shape[1] - offset:
            continue
        
        kernel = image[x-offset:x+offset+1, y-offset:y+offset+1]
        step_neigh_num = np.count_nonzero(kernel) - 1
        neigh_nums.append([x, y, step_neigh_num])

    return neigh_nums


def sort_neight_nums_by_N(neigh_nums: list) -> np.ndarray:
    """
    Sort neigh_nums by N parameter.
    Removes N=-1, N=0 and sorts by N desc. 
    """
    np_neigh_nums = np.array(neigh_nums)

    np_neigh_nums = np_neigh_nums[
        (np_neigh_nums[:, 2] != -1) & (np_neigh_nums[:, 2] != 0)]

    np_neigh_nums = np_neigh_nums[
        np.argsort(-np_neigh_nums[:, 2])]

    return np_neigh_nums


def get_potential_points_coords(
        sorted_niegh_nums: np.ndarray, potential_points_num: int = 30
) -> np.ndarray:
    """
    Gets best candidates, potential points coords only.
    Returns np.ndarray like [[x1, y1], [x2, y2]].
    """
    sorted_neigh_nums = np.delete(sorted_niegh_nums, 2, axis=1)
    return sorted_neigh_nums[:potential_points_num]


# def get_best_lines(potential_points: np.ndarray) -> np.ndarray:
#     """
#     Gets best combinations of lines by all points.
#     Line y = kx + b.
#     Returns np.ndarray like [[k1, b1], [k2, b2]].
#     """
    # best_lines = []
    # best_inliers = []
    # iterations_num = 1000
    # threshold_distance = 3
    # threshold_k = 0.1
    # threshold_b = 10
    
    # for i in range(iterations_num):
    #     sample_indices = np.random.choice(potential_points.shape[0], size=3, replace=False)
        
    #     sample_points = potential_points[sample_indices]
    #     sample_x = sample_points[:, 1]
    #     sample_y = sample_points[:, 0]
        
    #     coefficients = np.polyfit(sample_x, sample_y, deg=1)
        
    #     distances = np.abs(potential_points[:, 0] - np.matmul(np.vstack((potential_points[:, 1], np.ones_like(potential_points[:, 1]))).T, coefficients)) / np.sqrt(coefficients[0] ** 2 + 1)
    #     inliers = np.where(distances < threshold_distance)[0]
        
    #     is_similar = False
    #     for line in best_lines:
    #         diff_k = abs(coefficients[0] - line[0])
    #         diff_b = abs(coefficients[0] - line[0])
            
    #         if diff_k <= threshold_k and diff_b <= threshold_b:
    #             is_similar = True
    #             break
        
    #     if not is_similar and len(inliers) > len(best_inliers):
    #         best_lines = [coefficients]
    #         best_inliers = inliers
    #     elif not is_similar and len(inliers) == len(best_inliers):
    #         best_lines.append(coefficients)
    
    # return np.array(best_lines)
def angle_between_lines(line1: np.ndarray, line2: np.ndarray) -> float:
    """Calculates the angle in degrees between two lines."""
    import math
    k1, b1 = line1
    k2, b2 = line2
    angle_rad = math.atan2(k2 - k1, 1 + k1 * k2)
    angle_deg = math.degrees(angle_rad)
    return angle_deg

def get_best_lines(potential_points: np.ndarray, min_angle: float = 30.0) -> np.ndarray:
    """
    Gets best combinations of lines by all points.
    Line y = kx + b.
    Returns np.ndarray like [[k1, b1], [k2, b2]].
    """
    best_lines = []
    best_inliers = []
    iterations_num = 1000
    threshold_distance = 5
    threshold_k = 0.25
    threshold_b = 10

    for _ in range(3):
        local_best_lines = []
        local_best_inliers = []

        if potential_points.shape[0] < 3:
            break

        for i in range(iterations_num):
            sample_indices = np.random.choice(potential_points.shape[0], size=3, replace=False)

            sample_points = potential_points[sample_indices]
            sample_x = sample_points[:, 1]
            sample_y = sample_points[:, 0]

            coefficients = np.polyfit(sample_x, sample_y, deg=1)

            distances = np.abs(potential_points[:, 0] - np.matmul(np.vstack((potential_points[:, 1], np.ones_like(potential_points[:, 1]))).T, coefficients)) / np.sqrt(coefficients[0] ** 2 + 1)
            inliers = np.where(distances < threshold_distance)[0]

            # Check if the current line is similar to any of the existing lines
            is_similar = False
            for line in local_best_lines:
                # Calculate the difference between k and b of the current line and the existing lines
                diff_k = abs(coefficients[0] - line[0])
                diff_b = abs(coefficients[0] - line[0])

                # If the difference is smaller than the threshold, consider the current line as similar to the existing line
                if diff_k <= threshold_k and diff_b <= threshold_b:
                    is_similar = True
                    break

            # If the current line is not similar to any of the existing lines and has more inliers, consider it as a potential best line
            if not is_similar and len(inliers) > len(local_best_inliers):
                local_best_lines = [coefficients]
                local_best_inliers = inliers
            elif not is_similar and len(inliers) == len(local_best_inliers):
                local_best_lines.append(coefficients)

        # Update the potential points by removing the points that belong to the vicinity of the current best line
        mask = np.ones(len(potential_points), dtype=bool)
        for equation in local_best_lines:
            k, b = equation
            x = potential_points[:, 1]
            y = potential_points[:, 0]
            distances = np.abs(y - (k * x + b))
            mask[np.where(distances < threshold_distance)[0]] = False

        best_lines.extend(local_best_lines)
        best_inliers.extend(local_best_inliers)

        # Update the potential points by keeping only the points that were not removed in the vicinity of the current best line
        potential_points = potential_points[mask]

        # Check the angles between the best lines
        if len(best_lines) >= 2:
            angles = []
            for i in range(len(best_lines)):
                for j in range(i+1, len(best_lines)):
                    angle = angle_between_lines(best_lines[i], best_lines[j])
                    angles.append(angle)
            if all(angle >= min_angle for angle in angles):
                break

    return np.array(best_lines[:3])



# main
coords = get_coords_by_threshold(image)

image = delete_pixels_by_min_threshold(image)

neigh_nums = get_neigh_nums_list(coords)

sorted_neigh_nums = sort_neight_nums_by_N(neigh_nums)

potential_points = get_potential_points_coords(sorted_neigh_nums)

best_lines = get_best_lines(potential_points)
print(best_lines)

# Visualization
plt.imshow(image, cmap="gray")
plt.scatter(potential_points[:, 1], potential_points[:, 0], color="red", marker="o")
plt.show()

# Plot the lines on the image
for equation in best_lines:
    k, b = equation
    x = np.arange(0, image.shape[1])
    y = k * x + b
    plt.imshow(image, cmap="gray")
    plt.scatter(potential_points[:, 1], potential_points[:, 0], color="red", marker="o")
    plt.plot(x, y, color="green")
    plt.scatter(potential_points[:, 1], potential_points[:, 0], color="red", marker="o")
plt.show()

Нужно переписать код так, чтобы он искал прямые, угол между которыми БОЛЕЕ 30 градусов, прямые должны содержать как можно большее количество potential_points, без использования math, только numpy