class Scan(Transform): """ Scan is a transform that scans and decodes all QR codes and barcodes in a image. The \ transform returns the number of detections, the data encoded on each code and their bounding \ boxes. """ outputs = { "detections": Number(min_value=0, only_integer=True), "data": List(Type(str)), "rectangles": List( List(List(Number(min_value=0, only_integer=True), length=2), length=2)), } def process(self, image, **kwargs): data = [] rectangles = [] decoded = pyzbar.decode(image) for code in decoded: data.append(code.data.decode("utf-8")) (x, y, width, height) = code.rect rectangles.append([(x, y), (x + width, y + height)]) return { "detections": len(decoded), "data": data, "rectangles": rectangles }
class CascadeDetector(Transform): arguments = { "cascade": File(), "scale": Number(default=1.1), "min_neighbors": Number(min_value=0, only_integer=True, default=3), "min_size": Number(min_value=0, default="auto"), "max_size": Number(min_value=0, default="auto"), } outputs = { "rectangles": List( List(List(Number(min_value=0, only_integer=True), length=2), length=2)), } def process(self, image, **kwargs): cascade = cv2.CascadeClassifier(kwargs["cascade"]) gray = GrayScale().apply(image) detections = cascade.detectMultiScale( gray, scaleFactor=kwargs["scale"], minNeighbors=kwargs["min_neighbors"], minSize=kwargs["min_size"] if kwargs["min_size"] != "auto" else None, maxSize=kwargs["max_size"] if kwargs["max_size"] != "auto" else None, ) rectangles = [] for x, y, w, h in detections: rectangles.append([(x, y), (x + w, y + h)]) return {"rectangles": rectangles}
class Smile(Transform): arguments = { "scale": Number(default=1.2), "min_neighbors": Number(min_value=0, only_integer=True, default=20), } outputs = { "rectangles": List( List(List(Number(min_value=0, only_integer=True), length=2), length=2)), } def process(self, image, **kwargs): faces = Faces().apply(image) cascade_file = get_resource("haar-smile-cascade", "haarcascade_smile.xml") rectangles = [] for face in faces["rectangles"]: face_image = Crop(rectangle=face).apply(image) smile = CascadeDetector(cascade=str(cascade_file), **kwargs).apply(face_image)["rectangles"] if smile: adjusted = [] for i in range(len(smile[0])): adjusted.append((smile[0][i][0] + face[0][0], smile[0][i][1] + face[0][1])) rectangles.append(adjusted) return {"rectangles": rectangles}
class Eyes(Transform): arguments = { "scale": Number(default=1.1), "min_neighbors": Number(min_value=0, only_integer=True, default=3), } outputs = { "rectangles": List( List(List(Number(min_value=0, only_integer=True), length=2), length=2)), } def process(self, image, **kwargs): cascade_file = get_resource("haar-eye-cascade", "haarcascade_eye.xml") rectangles = [] for face in Faces().apply(image)["rectangles"]: face_image = Crop(rectangle=face).apply(image) eyes = CascadeDetector(cascade=str(cascade_file), **kwargs).apply(face_image)["rectangles"] for eye in eyes: adjusted = [] for i in range(len(eye)): adjusted.append( (eye[i][0] + face[0][0], eye[i][1] + face[0][1])) rectangles.append(adjusted) return {"rectangles": rectangles}
class Circles(Transform): """ Circles is a transform that can detect where there are circles in an image using the hough space :param size: Kernel size for media blur, defaults to 1 :type size: :class:`int`, optional :param dp: Inverse ratio of the accumulator resolution to the image resolution, defaults to 1. :type dp: :class:`int`, optional :param min_dist: Minimal distance between centers of circles, defaults to 1. :type min_dist: :class:`int`/:class:`float`, optional :param min_radius: Minimum radius of a circle, defaults to 0 :type min_radius: :class:`int`/:class:`float`, optional :param max_radius: Maximum radius of a circle, defaults to 0 :type max_radius: :class:`int`/:class:`float`, optional :param high: High canny threshold, defaults to 200 :type high: :class:`int`, optional :param threshold: Threshold of votes, defaults to 200 :type threshold: :class:`int`, optional """ arguments = { "size": Number(min_value=1, only_integer=True, only_odd=True, default=1), "dp": Number(min_value=1, only_integer=True, default=1), "min_dist": Number(min_value=0, default=1), "min_radius": Number(min_value=0, default=0), "max_radius": Number(min_value=0, default=0), "high": Number(min_value=0, only_integer=True, default=200), "threshold": Number(min_value=0, only_integer=True, default=200), } outputs = { "circles": List(List(Number(min_value=0, only_integer=True), length=3)) } def process(self, image, **kwargs): blur = easycv.transforms.filter.Blur(method="median", size=kwargs["size"]).apply(image) gray = GrayScale().apply(blur) circles = cv2.HoughCircles( gray, cv2.HOUGH_GRADIENT, kwargs["dp"], kwargs["min_dist"], param1=kwargs["high"], param2=kwargs["threshold"], minRadius=kwargs["min_radius"], maxRadius=kwargs["max_radius"], ) return {"circles": circles[0]}
class ColorPick(Transform): """ ColorPick is a transform that returns the color of a selected point or the \ average color of a selected rectangle. Returns the color in RGB. :param method: Method to be used, defaults to "point" :type method: :class:`str`, optional """ methods = ["point", "rectangle"] default_method = "point" outputs = { "color": List(Number(min_value=0, max_value=255, only_integer=True), length=3) } def process(self, image, **kwargs): if kwargs["method"] == "point": point = Select(method="point", n=1).apply(image)["points"][0] return {"color": list(image[point[1]][point[0]][::-1])} if kwargs["method"] == "rectangle": rectangle = Select(method="rectangle").apply(image)["rectangle"] cropped = Crop(rectangle=rectangle).apply(image) return { "color": list(cropped.mean(axis=(1, 0)).round().astype("uint8"))[::-1] }
class Mask(Transform): """ Mask applies a mask to an image. :param mask: Mask to apply :type brush: :class:`Image` :param inverse: Inverts mask :type inverse: :class:`bool` :param fill_color: Color to fill :type fill_color: :class:`List` """ arguments = { "mask": Image(), "inverse": Type(bool, default=False), "fill_color": List( Number(only_integer=True, min_value=0, max_value=255), length=3, default=(0, 0, 0), ), } def process(self, image, **kwargs): if kwargs["inverse"]: mask = cv2.bitwise_not(kwargs["mask"].array) else: mask = kwargs["mask"].array image = cv2.bitwise_and(image, image, mask=mask) image[mask == 0] = kwargs["fill_color"] return image
class Faces(Transform): arguments = { "scale": Number(default=1.3), "min_neighbors": Number(min_value=0, only_integer=True, default=5), } outputs = { "rectangles": List( List(List(Number(min_value=0, only_integer=True), length=2), length=2)), } def process(self, image, **kwargs): cascade_file = get_resource("haar-face-cascade", "haarcascade_frontalface_default.xml") return CascadeDetector(cascade=str(cascade_file), **kwargs).apply(image)
class Perspective(Transform): """ Perspective is a transform that changes the perspective of the image to the one given by \ the provided corners/points. The new image will have the given points as corners. :param points: Corners of the desired perspective :type points: :class:`list` """ arguments = { "points": List(List(Number(min_value=0, only_integer=True), length=2)), } def process(self, image, **kwargs): if len(kwargs["points"]) != 4: raise ValueError("Must receive 4 points.") corners = order_corners(kwargs["points"]) tl, tr, br, bl = corners corners = np.array(corners, dtype="float32") new_width = max(distance(br, bl), distance(tr, tl)) new_height = max(distance(tr, br), distance(tl, bl)) dst = np.array( [ [0, 0], [new_width - 1, 0], [new_width - 1, new_height - 1], [0, new_height - 1], ], dtype="float32", ) shift_matrix = cv2.getPerspectiveTransform(corners, dst) warped = cv2.warpPerspective(image, shift_matrix, (new_width, new_height)) return warped
class FilterChannels(Transform): """ FilterChannels is a transform that removes color channel(s). :param channels: List of channels to remove :type channels: :class:`list` :param scheme: Image color scheme (rgb or bgr), defaults to "rgb" :type scheme: :class:`str`, optional """ arguments = { "channels": List(Number(min_value=0, max_value=2, only_integer=True)), "scheme": Option(["rgb", "bgr"], default=0), } def process(self, image, **kwargs): channels = np.array(kwargs["channels"]) if kwargs["scheme"] == "rgb": channels = 2 - channels if len(channels) > 0: image[:, :, channels] = 0 return image
class Draw(Transform): """ The Draw transform provides a way to draw 2D shapes or text on a image. Currently supported\ methods are: \t**∙ ellipse** - Draw an ellipse on an image\n \t**∙ line** - Draw a line on an image\n \t**∙ polygon** - Draw a polygon on an image\n \t**∙ rectangle** - Draw a rectangle on an image\n \t**∙ text** - Write text on an image\n :param ellipse: Tuple made up by: three arguments(center(int,int), axis1(int), axis2(int)) \ regarding the ellipse. :type ellipse: :class:`tuple` :param color: color of the shape in BGR, defaults to "(0,0,0)" - black. :type color: :class:`list`/:class:`tuple`, optional :param end_angle: Ending angle of the elliptic arc in degrees, defaults to "360". :type end_angle: :class:`int`, optional :param font: Font to be used, defaults to "SIMPLEX". :type font: :class:`str`, optional :param filled: True if shape is to be filled, defaults to "false". :type filled: :class:`bool`, optional :param closed: If true, a line is drawn from the last vertex to the first :type closed: :class:`str`, optional :param line_type: Line type to be used when drawing a shape, defaults to "line_AA". :type line_type: :class:`str`, optional :param org: Bottom-left corner of the text in the image. :type org: :class:`tuple` :param pt1: First point to define a shape. :type pt1: :class:`tuple` :param pt2: Second point to define a shape. :type pt2: :class:`tuple` :param points: A list of point. :type points: :class:`list`/:class:`tuple` :param radius: Radius of the circle. :type radius: :class:`int` :param rectangle: Tuple made up by: two points(upper left corner(int,int), lower right \ corner(int,int))regarding the rectangle. :type rectangle: :class:`tuple` :param rotation_angle: Angle of rotation. :type rotation_angle: :class:`int` :param size: Size of the text to be drawn, defaults to "5". :type size: :class:`int`, optional :param start_angle: Starting angle of the elliptic arc in degrees, defaults to "0". :type start_angle: :class:`int`, optional :param text: Text string to be drawn. :type text: :class:`str` :param thickness: Thickness of the line used, defaults to "5". :type thickness: :class:`int`, optional :param x_mirror: When true the text is mirrored in the x axis, defaults to "false". :type x_mirror: :class:`bool` """ methods = { "ellipse": { "arguments": [ "ellipse", "rotation_angle", "start_angle", "end_angle", "filled", "color", "thickness", "line_type", ] }, "line": { "arguments": ["pt1", "pt2", "color", "thickness", "line_type"] }, "polygon": { "arguments": [ "points", "closed", "color", "thickness", "line_type", "filled", ] }, "rectangle": { "arguments": ["rectangles", "color", "thickness", "line_type", "filled"] }, "text": { "arguments": [ "text", "org", "font", "size", "x_mirror", "color", "thickness", "line_type", ] }, "boxes": { "arguments": ["boxes", "line_type", "font", "size"] }, "rectangles": { "arguments": ["rectangles", "color", "thickness", "line_type", "filled"] }, } arguments = { "ellipse": List( List(Number(only_integer=True, min_value=0), length=2), Number(min_value=0, only_integer=True), Number(min_value=0, only_integer=True), ), "rectangle": List(List(Number(min_value=0, only_integer=True), length=2), length=2), "rectangles": List( List(List(Number(min_value=0, only_integer=True), length=2), length=2)), "color": List(Number(min_value=0, max_value=255), length=3, default=(0, 0, 0)), "end_angle": Number(default=360), "font": Option( [ "SIMPLEX", "PLAIN", "DUPLEX", "COMPLEX", "TRIPLEX", "COMPLEX_SMALL", "SCRIPT_SIMPLEX", "SCRIPT_COMPLEX", ], default=0, ), "filled": Type(bool, default=False), "closed": Type(bool, default=True), "line_type": Option(["line_AA", "line_4", "line_8"], default=0), "org": List(Number(min_value=0, only_integer=True), length=2), "pt1": List(Number(min_value=0, only_integer=True), length=2), "pt2": List(Number(min_value=0, only_integer=True), length=2), "points": List(List(Number(min_value=0, only_integer=True), length=2)), "radius": Number(min_value=0), "rotation_angle": Number(default=0), "size": Number(min_value=0, default=5), "start_angle": Number(default=0), "text": Type(str), "thickness": Number(min_value=0, default=5, only_integer=True), "x_mirror": Type(bool, default=False), "boxes": List( List( List(List(Number(min_value=0, only_integer=True), length=2), length=2), List(Number(min_value=0, max_value=255, only_integer=True), length=3), Type(str), )), } def process(self, image, **kwargs): if len(image.shape) < 3: kwargs["color"] = ((0.3 * kwargs["color"][0]) + (0.59 * kwargs["color"][1]) + (0.11 * kwargs["color"][2])) method = kwargs.pop("method") kwargs["lineType"] = lines[kwargs.pop("line_type")] if method == "line": kwargs["pt1"] = tuple(kwargs["pt1"]) kwargs["pt2"] = tuple(kwargs["pt2"]) return cv.line(image, **kwargs) if method == "polygon": kwargs["pts"] = kwargs.pop("points") kwargs["pts"] = np.array(kwargs.pop("pts"), np.int32) kwargs["pts"] = [kwargs["pts"].reshape((-1, 1, 2))] if kwargs.pop("filled"): kwargs.pop("thickness") kwargs.pop("closed") return cv.fillPoly(image, **kwargs) else: kwargs["isClosed"] = kwargs.pop("closed") return cv.polylines(image, **kwargs) if method == "text": kwargs["org"] = tuple(kwargs["org"]) kwargs["font"] = font[kwargs["font"]] kwargs["fontFace"] = kwargs.pop("font") kwargs["fontScale"] = kwargs.pop("size") kwargs["bottomLeftOrigin"] = kwargs.pop("x_mirror") return cv.putText(image, **kwargs) # to make a circle/ellipse/line/rectangle filled thickness must be negative/cv.FILLED if kwargs.pop("filled", False): kwargs["thickness"] = cv.FILLED if method == "ellipse": return cv.ellipse( image, kwargs["ellipse"][0], (kwargs["ellipse"][1], kwargs["ellipse"][2]), kwargs["rotation_angle"], kwargs["start_angle"], kwargs["end_angle"], kwargs["color"], kwargs["thickness"], kwargs["lineType"], ) if method == "rectangle": pts = kwargs.pop("rectangle") return cv.rectangle(image, pts[0], pts[1], **kwargs) if method == "boxes": kwargs["font"] = font[kwargs["font"]] kwargs["fontFace"] = kwargs.pop("font") size = kwargs.pop("size") for box in kwargs.pop("boxes"): image = cv.rectangle( image, box[0][0], (box[0][0][0] + box[0][1][0], box[0][0][1] + box[0][1][1]), tuple(box[1]), thickness=2, ) kwargs["org"] = (box[0][0][0], box[0][0][1] - 5) kwargs["color"] = tuple(box[1]) kwargs["fontScale"] = size * (box[0][1][0]) * 0.0008 kwargs["thickness"] = int(2 / (box[0][1][0] * 0.5)) image = cv.putText(image, box[2], **kwargs) return image if method == "rectangles": for rectangle in kwargs.pop("rectangles"): image = cv.rectangle(image, rectangle[0], rectangle[1], **kwargs) return image
class Select(Transform): """ Select is a transform that allows the user to select a shape or a mask in an image. Currently \ supported shapes: \t**∙ rectangle** - Rectangle Shape\n \t**∙ point** - Point\n \t**∙ ellipse** - Ellipse Shape\n \t**∙ mask** - Mask\n :param n: Number of points to select :type n: :class:`int` :param brush: Brush size :type brush: :class:`int` :param color: Brush color :type color: :class:`List` """ methods = { "rectangle": {"arguments": [], "outputs": ["rectangle"]}, "point": {"arguments": ["n"], "outputs": ["points"]}, "ellipse": {"arguments": [], "outputs": ["ellipse"]}, "mask": {"arguments": ["brush", "color"], "outputs": ["mask"]}, } default_method = "rectangle" arguments = { "n": Number(only_integer=True, min_value=0, default=2), "brush": Number(only_integer=True, min_value=0, default=20), "color": List( Number(only_integer=True, min_value=0, max_value=255), length=3, default=(0, 255, 0), ), } outputs = { # rectangle "rectangle": List( List(Number(min_value=0, only_integer=True), length=2), length=2 ), # ellipse "ellipse": List( List(Number(only_integer=True, min_value=0), length=2), Number(min_value=0, only_integer=True), Number(min_value=0, only_integer=True), ), # point "points": List(List(Number(min_value=0, only_integer=True), length=2)), # mask "mask": Image(), } def process(self, image, **kwargs): if "DISPLAY" not in os.environ: raise Exception("Can't run selectors without a display!") if kwargs["method"] == "mask": mask = np.zeros(image.shape, np.uint8) global drawing drawing = False def paint_draw(event, x, y, flags, param): global ix, iy, drawing if event == cv2.EVENT_LBUTTONDOWN: drawing = True elif event == cv2.EVENT_LBUTTONUP: drawing = False elif event == cv2.EVENT_MOUSEMOVE and drawing: cv2.line(mask, (ix, iy), (x, y), kwargs["color"], kwargs["brush"]) ix, iy = x, y return x, y cv2.namedWindow("Select Mask", cv2.WINDOW_KEEPRATIO) cv2.resizeWindow("Select Mask", image.shape[0], image.shape[1]) cv2.setMouseCallback("Select Mask", paint_draw) while cv2.getWindowProperty("Select Mask", cv2.WND_PROP_VISIBLE) >= 1: cv2.imshow("Select Mask", cv2.addWeighted(image, 0.8, mask, 0.2, 0)) key_code = cv2.waitKey(1) if (key_code & 0xFF) == ord("q"): cv2.destroyAllWindows() break elif (key_code & 0xFF) == ord("+"): kwargs["brush"] += 1 elif (key_code & 0xFF) == ord("-") and kwargs["brush"] > 1: kwargs["brush"] -= 1 mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY) mask[mask != 0] = 255 return {"mask": easycv.image.Image(mask)} mpl.use("Qt5Agg") fig, current_ax = plt.subplots() plt.tick_params( axis="both", which="both", bottom=False, top=False, left=False, right=False, labelbottom=False, labelleft=False, ) def empty_callback(e1, e2): pass def selector(event): if event.key in ["Q", "q"]: plt.close(fig) res = [] current_ax.imshow(prepare_image_to_output(image)) plt.gcf().canvas.set_window_title("Selector") if kwargs["method"] == "rectangle": selector.S = RectangleSelector( current_ax, empty_callback, useblit=True, button=[1, 3], minspanx=5, minspany=5, spancoords="pixels", interactive=True, ) elif kwargs["method"] == "ellipse": selector.S = EllipseSelector( current_ax, empty_callback, drawtype="box", interactive=True, useblit=True, ) else: def onclick(event): if event.xdata is not None and event.ydata is not None: res.append((int(event.xdata), int(event.ydata))) plt.plot( event.xdata, event.ydata, marker="o", color="cyan", markersize=4 ) fig.canvas.draw() if len(res) == kwargs["n"]: plt.close(fig) plt.connect("button_press_event", onclick) plt.connect("key_press_event", selector) plt.show(block=True) if kwargs["method"] == "rectangle": x, y = selector.S.to_draw.get_xy() x = int(round(x)) y = int(round(y)) width = int(round(selector.S.to_draw.get_width())) height = int(round(selector.S.to_draw.get_height())) if width == 0 or height == 0: raise InvalidSelectionError("Must select a rectangle.") return {"rectangle": [(x, y), (x + width, y + height)]} elif kwargs["method"] == "ellipse": width = int(round(selector.S.to_draw.width)) height = int(round(selector.S.to_draw.height)) center = [int(round(x)) for x in selector.S.to_draw.get_center()] if width == 0 or height == 0: raise InvalidSelectionError("Must select an ellipse.") return {"ellipse": [tuple(center), int(width / 2), int(height / 2)]} else: if len(res) != kwargs["n"]: raise InvalidSelectionError( "Must select {} points.".format(kwargs["n"]) ) return {"points": res}
class Detect(Transform): methods = { "yolo": { "arguments": ["confidence", "threshold"] }, "ssd": { "arguments": ["confidence"] }, } default_method = "yolo" arguments = { "confidence": Number(min_value=0, max_value=1, default=0.5), "threshold": Number(min_value=0, max_value=1, default=0.3), } outputs = { "boxes": List( List( List(List(Number(min_value=0, only_integer=True), length=2), length=2), List(Number(min_value=0, max_value=255, only_integer=True), length=3), Type(str), )) } @staticmethod def labels(model): if model == "yolo": labels_path = get_resource("yolov3", "coco.names") labels = open(str(labels_path)).read().strip().split("\n") else: labels = [ "background", "aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor", ] return labels def process(self, image, **kwargs): labels = self.labels(kwargs["method"]) colors = np.random.randint(0, 255, size=(len(labels), 3), dtype="uint8") if kwargs["method"] == "yolo": config = get_resource("yolov3", "yolov3.cfg") weights = get_resource("yolov3", "yolov3.weights") net = cv2.dnn.readNetFromDarknet(str(config), str(weights)) layers = net.getLayerNames() layers = [layers[i[0] - 1] for i in net.getUnconnectedOutLayers()] blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (416, 416), swapRB=True, crop=False) h, w = image.shape[:2] net.setInput(blob) outputs = net.forward(layers) rectangles = [] confidences = [] class_ids = [] for output in outputs: for detection in output: scores = detection[5:] class_id = np.argmax(scores) confidence = scores[class_id] if confidence > kwargs["confidence"]: # scale the bounding box back rectangle = detection[0:4] * np.array([w, h, w, h]) (centerX, centerY, width, height) = rectangle.astype("int") # compute top-left corner x = int(centerX - (width / 2)) y = int(centerY - (height / 2)) rectangles.append([x, y, int(width), int(height)]) confidences.append(float(confidence)) class_ids.append(class_id) # apply non-maximum suppression indexes_to_keep = cv2.dnn.NMSBoxes(rectangles, confidences, kwargs["confidence"], kwargs["threshold"]) boxes = [] if len(indexes_to_keep) > 0: for i in indexes_to_keep.flatten(): (x, y) = (rectangles[i][0], rectangles[i][1]) (w, h) = (rectangles[i][2], rectangles[i][3]) color = [int(c) for c in colors[class_ids[i]]] label = "{}: {:.4f}".format(labels[int(class_ids[i])], confidences[i]) boxes.append([[(x, y), (w, h)], color, label]) return {"boxes": boxes} else: prototxt = get_resource("ssd-mobilenet", "MobileNetSSD_deploy.prototxt") model = get_resource("ssd-mobilenet", "MobileNetSSD_deploy.caffemodel") net = cv2.dnn.readNetFromCaffe(str(prototxt), str(model)) (h, w) = image.shape[:2] blob = cv2.dnn.blobFromImage(cv2.resize(image, (300, 300)), 0.007843, (300, 300), 127.5) net.setInput(blob) detections = net.forward() boxes = [] for i in np.arange(0, detections.shape[2]): confidence = detections[0, 0, i, 2] if confidence > kwargs["confidence"]: idx = int(detections[0, 0, i, 1]) box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (startX, startY, endX, endY) = box.astype("int") width, height = int(endX - startX), int(endY - startY) label = "{}: {:.4f}".format(labels[idx], confidence) color = [int(c) for c in colors[idx]] boxes.append([[(startX, startY), (width, height)], color, label]) return {"boxes": boxes}
class Lines(Transform): """ Lines is a transform that detects lines in an image using the Hough Transform. :param low: Low canny threshold, defaults to 50 :type low: :class:`int`, optional :param high: High canny threshold, defaults to 150 :type high: :class:`int`, optional :param size: Gaussian kernel size (canny), defaults to 3 :type size: :class:`int`, optional :param rho: Distance resolution of the accumulator in pixels, defaults to 1 :type rho: :class:`int`/:class:`float`, optional :param theta: Angle resolution of the accumulator in radians, defaults to pi/180 :type theta: :class:`int`/:class:`float`, optional :param threshold: Threshold of votes, defaults to 200 :type threshold: :class:`int`, optional :param min_size: Minimum line length, defaults to 3 :type min_size: :class:`int`/:class:`float`, optional :param max_gap: Maximum gap between lines to treat them as single line, defaults to 3 :type max_gap: :class:`int`/:class:`float`, optional """ methods = { "normal": { "arguments": ["low", "high", "size", "rho", "theta", "threshold"] }, "probablistic": { "arguments": [ "low", "high", "size", "rho", "theta", "threshold", "min_size", "max_gap", ] }, } default_method = "normal" arguments = { "low": Number(min_value=1, max_value=255, only_integer=True, default=50), "high": Number(min_value=1, max_value=255, only_integer=True, default=150), "size": Number(min_value=3, max_value=7, only_integer=True, only_odd=True, default=3), "rho": Number(min_value=0, default=1), "theta": Number(min_value=0, max_value=np.pi / 2, default=np.pi / 180), "threshold": Number(min_value=0, only_integer=True, default=200), "min_size": Number(min_value=0, default=3), "max_gap": Number(min_value=0, default=3), } outputs = { "lines": List( List(List(Number(min_value=0, only_integer=True), length=2), length=2)) } def process(self, image, **kwargs): gray = GrayScale().apply(image) edges = Canny(low=kwargs["low"], high=kwargs["high"], size=kwargs["size"]).apply(gray) if kwargs["method"] == "normal": h_lines = cv2.HoughLines(edges, kwargs["rho"], kwargs["theta"], kwargs["threshold"]) lines = [] if h_lines is not None: for rho, theta in h_lines[:, 0]: x1 = int(rho * np.cos(theta)) y1 = int(rho * np.sin(theta)) if x1 > y1: y1 = 0 elif y1 > x1: x1 = 0 x2 = (rho - image.shape[0] * np.sin(theta)) / np.cos(theta) y2 = (rho - image.shape[1] * np.cos(theta)) / np.sin(theta) if 0 <= x2 <= image.shape[1] or np.sin(theta) == 0: y2 = image.shape[0] elif 0 <= y2 <= image.shape[0] or np.cos(theta) == 0: x2 = image.shape[1] lines.append([[int(x1), int(y1)], [int(x2), int(y2)]]) else: lines = cv2.HoughLines( edges, kwargs["rho"], kwargs["theta"], kwargs["threshold"], kwargs["min_size"], kwargs["max_gap"], ) return {"lines": lines}