How to stretch a line to fit image with Python OpenCV?
Asked Answered
S

3

5

I have an image with the size of W * H, I want to draw a line on this and the line should be automatically fit to the image, for example if I draw it:

enter image description here

I want this:

enter image description here

How can I do this in Python and OpenCV? Thanks

Stockdale answered 2/5, 2022 at 8:5 Comment(5)
If there is no problem even if the position of the endpoint is outside the image, the problem will be simplified if we think about the circumscribed circle of the image instead of the four sides of the image. i.e. the position of endpoint can be calculated based on the distance between endpoint and image center. (Of couse, the distance is circle radius)Epideictic
@Stockdale - do you actually need to calculate valid coordinates (within the image) for the streched line, or is it enough that you can use cv2.line to draw it ?Occlusive
@Occlusive yes I need valid coordinates for the lineStockdale
In that case have a look at my answer. It's not a complete solution, but might help.Occlusive
@Occlusive thank you, I will take a look and consider as a solution if it worksStockdale
H
6

Method #1: Just drawing the extended line (no coordinates)

Before -> After

Here's a function when given points p1 and p2, will only draw the extended line. By default, the line is clipped by the image boundaries. There is also a distance parameter to determine how far to draw from the original starting point or until the line hits the border of the image. If you need the new (x1, y1) and (x2, y2) coordinates, see section #2

import cv2
import numpy as np

"""
@param: p1 - starting point (x, y)
@param: p2 - ending point (x, y)
@param: distance - distance to extend each side of the line
"""
def extend_line(p1, p2, distance=10000):
    diff = np.arctan2(p1[1] - p2[1], p1[0] - p2[0])
    p3_x = int(p1[0] + distance*np.cos(diff))
    p3_y = int(p1[1] + distance*np.sin(diff))
    p4_x = int(p1[0] - distance*np.cos(diff))
    p4_y = int(p1[1] - distance*np.sin(diff))
    return ((p3_x, p3_y), (p4_x, p4_y))

# Create blank black image using Numpy
original = np.zeros((500,500,3), dtype=np.uint8)
image1 = original.copy()
p1 = (250, 100)
p2 = (375, 250)
cv2.line(image1, p1, p2, [255,255,255], 2)

# Second image, calculate new extended points
image2 = original.copy()
p3, p4 = extend_line(p1, p2)
cv2.line(image2, p3, p4, [255,255,255], 2)

cv2.imshow('image1', image1)
cv2.imshow('image2', image2)
cv2.waitKey()

Method #2: Full drawing with coordinates

If you need the new (x1, y1) and (x2, y2) coordinates, it gets a little more complicated since we need to calculate the resulting new points for each possible case. The possible cases are horizontal, vertical, positively sloped, negatively sloped, and exact diagonals. Here's the result for each of the cases with the new two coordinate points: white is the original line and the green is the extended line

Vertical

(250, 0) (250, 500)

Horizontal

(0, 300) (500, 300)

Positive slope

(0, 450) (450, 0)

Negative slope

(0, 142) (500, 428)

Left corner diagonal

(0, 0) (500, 500)

Right corner diagonal

(0, 0) (500, 500)

Code

import numpy as np
import cv2
import math

"""
@param: dimensions - image shape from Numpy (h, w, c)
@param: p1 - starting point (x1, y1)
@param: p2 - ending point (x2, y2)
@param: SCALE - default parameter to ensure that extended lines go through borders
"""
def extend_line(dimensions, p1, p2, SCALE=10):
    # Calculate the intersection point given (x1, y1) and (x2, y2)
    def line_intersection(line1, line2):
        x_diff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0])
        y_diff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1])

        def detect(a, b):
            return a[0] * b[1] - a[1] * b[0]

        div = detect(x_diff, y_diff)
        if div == 0:
           raise Exception('lines do not intersect')

        dist = (detect(*line1), detect(*line2))
        x = detect(dist, x_diff) / div
        y = detect(dist, y_diff) / div
        return int(x), int(y)

    x1, x2 = 0, 0
    y1, y2 = 0, 0
    
    # Extract w and h regardless of grayscale or BGR image
    if len(dimensions) == 3:
        h, w, _ = dimensions
    elif len(dimensions) == 2:
        h, w = dimensions
    
    # Take longest dimension and use it as maxed out distance
    if w > h:
        distance = SCALE * w
    else:
        distance = SCALE * h
    
    # Reorder smaller X or Y to be the first point
    # and larger X or Y to be the second point
    try:
        slope = (p2[1] - p1[1]) / (p1[0] - p2[0])
        # HORIZONTAL or DIAGONAL
        if p1[0] <= p2[0]:
            x1, y1 = p1
            x2, y2 = p2
        else:
            x1, y1 = p2
            x2, y2 = p1
    except ZeroDivisionError:
        # VERTICAL
        if p1[1] <= p2[1]:
            x1, y1 = p1
            x2, y2 = p2
        else:
            x1, y1 = p2
            x2, y2 = p1
    
    # Extend after end-point A
    length_A = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
    p3_x = int(x1 + (x1 - x2) / length_A * distance)
    p3_y = int(y1 + (y1 - y2) / length_A * distance)

    # Extend after end-point B
    length_B = math.sqrt((x1 - x2)**2 + (y1 - y2)**2)
    p4_x = int(x2 + (x2 - x1) / length_B * distance)
    p4_y = int(y2 + (y2 - y1) / length_B * distance)
   
    # -------------------------------------- 
    # Limit coordinates to borders of image
    # -------------------------------------- 
    # HORIZONTAL
    if y1 == y2:
        if p3_x < 0: 
            p3_x = 0
        if p4_x > w: 
            p4_x = w
        return ((p3_x, p3_y), (p4_x, p4_y))
    # VERTICAL
    elif x1 == x2:
        if p3_y < 0: 
            p3_y = 0
        if p4_y > h: 
            p4_y = h
        return ((p3_x, p3_y), (p4_x, p4_y))
    # DIAGONAL
    else:
        A = (p3_x, p3_y)
        B = (p4_x, p4_y)

        C = (0, 0)  # C-------D
        D = (w, 0)  # |-------|
        E = (w, h)  # |-------|
        F = (0, h)  # F-------E
        
        if slope > 0:
            # 1st point, try C-F side first, if OTB then F-E
            new_x1, new_y1 = line_intersection((A, B), (C, F))
            if new_x1 > w or new_y1 > h:
                new_x1, new_y1 = line_intersection((A, B), (F, E))

            # 2nd point, try C-D side first, if OTB then D-E
            new_x2, new_y2 = line_intersection((A, B), (C, D))
            if new_x2 > w or new_y2 > h:
                new_x2, new_y2 = line_intersection((A, B), (D, E))

            return ((new_x1, new_y1), (new_x2, new_y2))
        elif slope < 0:
            # 1st point, try C-F side first, if OTB then C-D
            new_x1, new_y1 = line_intersection((A, B), (C, F))
            if new_x1 < 0 or new_y1 < 0:
                new_x1, new_y1 = line_intersection((A, B), (C, D))
            # 2nd point, try F-E side first, if OTB then E-D
            new_x2, new_y2 = line_intersection((A, B), (F, E))
            if new_x2 > w or new_y2 > h:
                new_x2, new_y2 = line_intersection((A, B), (E, D))
            return ((new_x1, new_y1), (new_x2, new_y2))

# Vertical
# -------------------------------
# p1 = (250, 100)
# p2 = (250, 300)
# -------------------------------

# Horizontal
# -------------------------------
# p1 = (100, 300)
# p2 = (400, 300)
# -------------------------------

# Positive slope
# -------------------------------
# C-F, C-D
# p1 = (50, 400)
# p2 = (400, 50)

# C-F, E-D
# p1 = (50, 400)
# p2 = (400, 50)

# F-E, E-D
# p2 = (250, 400)
# p1 = (400, 250)

# F-E, C-D
# p2 = (250, 400)
# p1 = (300, 250)
# -------------------------------

# Negative slope
# -------------------------------
# C-F, E-D
# p1 = (100, 200)
# p2 = (450, 400)

# C-F, F-E
# p2 = (100, 200)
# p1 = (250, 400)

# C-D, D-E
# p1 = (100, 50)
# p2 = (450, 400)

# C-D, F-E
p1 = (100, 50)
p2 = (250, 400)
# -------------------------------

# Exact corner diagonals
# -------------------------------
# p1 = (50,50)
# p2 = (300, 300)

# p2 = (375, 125)
# p1 = (125, 375)
# -------------------------------

image = np.zeros((500,500,3), dtype=np.uint8)
p3, p4 = extend_line(image.shape, p1, p2)
print(p3, p4)
cv2.line(image, p3, p4, [255,255,255], 2)
cv2.line(image, p1, p3, [36,255,12], 2)
cv2.line(image, p2, p4, [36,255,12], 2)
cv2.imshow('image', image)
cv2.waitKey()
Hittite answered 2/5, 2022 at 8:50 Comment(1)
If we know the bounds of the image like height and width, wouldn't solving for x (when y hits the bound first) or y (when x hits the bound) using the 1st method gives us the coordinates of the 2nd method? We can find which of the x and y hits the image boundary first just by hit and trial.Adame
O
3

I assumed you meant the following: You have 2 points - e.g. p1, p2 within the image. Instead of drawing a line between p1 and p2 you want the line that contains these points but streches till the edge of the image.

You will need to handle several cases, depending on which edges the line should reach. This depends on the location and angle of the original line segment.

Below is a code example of how to handle one of the cases - where the line should reach the top and right edges. You will need to modify it to handle the other cases similarly and also to determine which case should be applied.

import cv2
import numpy as np

# At the moment only a line that hits the top and right edges is handled.
# You need to handle the rest of the cases in a similar way.
def draw_line_fit_topright(img, p1, p2, color, thinkness):
    h = img.shape[0]
    w = img.shape[1]
    dx = float(p2[0]-p1[0])
    dy = float(p2[1]-p1[1])
    p1new = (int(p1[0] - dx/dy*p1[1]), 0)
    p2new = (w-1, int(p2[1] + dy/dx*(w-p2[0]-1)))
    cv2.line(img, p1new, p2new, color, thickness=thinkness)
    cv2.line(img, p1, p2, (0,0,255), thickness=thinkness-3)

w = 1024
h = 768
p1 = (240,50)
p2 = (440,150)
img1 = np.zeros((h, w, 3), dtype = "uint8")
draw_line_fit_topright(img1, p1, p2, (255, 0, 0), 5)
cv2.imshow("img1", img1)
cv2.waitKey(0)

Note: I am aware that my code does not solve the problem entirely. But I thought it will steer you in the right direction.

Occlusive answered 2/5, 2022 at 8:41 Comment(0)
G
1

Presumably, your line is known by its two endpoints, let P and Q (you should have said that). A general point along this line has coordinates given vectorially by P + t (Q - P). Now you have to take the intersections with the image edges, for example with the upper (Y=0) or lower (Y=Height-1) side. This gives the point of coordinates (X, Y), where

X = Px + (Y - Py) (Qx - Px) / (Qy - Py)

and you need to check if 0 ≤ X < Width. Repeat this for the four sides (switching X and Y where appropriate), and you will find two points in the end.


This gives you the main idea. In practice, there are corner cases such as a vertical or horizontal line, or a line going though a corner or two.

Gran answered 2/5, 2022 at 9:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.