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 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 Morphology(Transform): """ Morphology is a transform that applies different morphological operation to images. Available \ operations are: \t**∙ opening** - Opening is an Erosion followed by a dilation\n \t**∙ closing** - Closing is the reverse of Opening\n \t**∙ tophat** - Difference between the image and it's opening\n \t**∙ blackhat** - Difference between the image and it's closing\n :param size: Kernel size, defaults to 5 :type size: :class:`int`, optional :param iterations: Number of iterations, defaults to 1 :type iterations: :class:`int`, optional """ arguments = { "size": Number(min_value=1, only_integer=True, only_odd=True, default=5), "iterations": Number(min_value=1, only_integer=True, default=1), } methods = ["opening", "closing", "tophat", "blackhat"] default_method = "opening" def process(self, image, **kwargs): kernel = np.ones((kwargs["size"], kwargs["size"]), np.uint8) return cv2.morphologyEx( image, morp_methods[kwargs["method"]], kernel, iterations=kwargs["iterations"], )
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 Sharpness(Transform): """ Sharpness is a transform that measures how sharpen an image is. Images are classified as \ sharpen when above a certain value of sharpness given by the threshold. \ Currently supported interpolation methods: \t**∙ laplace** - Uses laplacian to calculate Sharpness\n \t**∙ fft** - Uses Fast Fourier Transform to calculate Sharpness\n :param threshold: Threshold to classify images as sharpen, defaults to 100 (for fft this \ should be arround 10) :type threshold: :class:`int`/:class:`float`, optional :param size: Radius around the centerpoint to zero out the FFT shift :type size: :class:`int`, optional """ methods = { "laplace": { "arguments": ["threshold"] }, "fft": { "arguments": ["size", "threshold"] }, } default_method = "laplace" arguments = { "threshold": Number(min_value=0, default=100), "size": Number(min_value=0, only_integer=True, default=60), } outputs = {"sharpness": Number(), "sharpen": Type(bool)} def process(self, image, **kwargs): grayscale = GrayScale().apply(image) if kwargs["method"] == "laplace": sharpness = (easycv.transforms.edges.Gradient( method="laplace").apply(grayscale).var()) else: h, w = grayscale.shape centerx, centery = (int(w / 2.0), int(h / 2.0)) fft = np.fft.fft2(grayscale) fft_shift = np.fft.fftshift(fft) size = kwargs["size"] fft_shift[centery - size:centery + size, centerx - size:centerx + size] = 0 fft_shift = np.fft.ifftshift(fft_shift) recon = np.fft.ifft2(fft_shift) sharpness = np.mean(20 * np.log(np.abs(recon))) return { "sharpness": sharpness, "sharpen": sharpness >= kwargs["threshold"] }
class Quantitization(Transform): """ Quantitization is a Transform that reduces the number of colors to the on give :param clusters: Number of colors that the image will have :type clusters: :class:`int`, required """ arguments = { "clusters": Number(min_value=1, only_integer=True), } def process(self, image, **kwargs): (h, w) = image.shape[:2] image = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) image = image.reshape((image.shape[0] * image.shape[1], 3)) clt = MiniBatchKMeans(n_clusters=kwargs["clusters"]) labels = clt.fit_predict(image) quant = clt.cluster_centers_.astype("uint8")[labels] quant = quant.reshape((h, w, 3)) quant = cv2.cvtColor(quant, cv2.COLOR_LAB2BGR) return quant
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 Inpaint(Transform): """ Inpaint applies an inpainting technique to an image. :param radius: Inpainting radius :type radius: :class:`int` :param mask: Mask to apply inpaint :type mask: :class:`Image` """ methods = { "telea": {"arguments": ["radius", "mask"]}, "ns": {"arguments": ["radius", "mask"]}, } default_method = "telea" arguments = { "radius": Number(only_integer=True, min_value=0, default=3), "mask": Image(), } def process(self, image, **kwargs): flag = cv2.INPAINT_TELEA if kwargs["method"] == "telea" else cv2.INPAINT_NS return cv2.inpaint(image, kwargs["mask"].array, kwargs["radius"], flags=flag)
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 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 Cartoon(Transform): """ Cartoon is a transform that creates a stylized / cartoonized image. :param smoothing: Determines the amount of smoothing, defaults to 60. :type smoothing: :class:`float`, optional :param region_size: Determines the size of regions of constant color, defaults to 0.45. :type region_size: :class:`float`, optional """ arguments = { "smoothing": Number(min_value=0, max_value=200, default=60), "region_size": Number(min_value=0, max_value=1, default=0.45), } def process(self, image, **kwargs): return cv2.stylization(image, sigma_s=kwargs["smoothing"], sigma_r=kwargs["region_size"])
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 Sharpen(Transform): """ Sharpen is a transform that sharpens an image. :param sigma: Kernel sigma, defaults to 1 :type sigma: :class:`float`, optional :param amount: Amount to sharpen, defaults to 1 :type amount: :class:`float`, optional :param multichannel: `True` if diferent processing for each color layer `False` otherwise :type multichannel: :class:`bool` """ arguments = { "sigma": Number(min_value=0, default=1), "amount": Number(default=1), "multichannel": Type(bool, default=False), } def process(self, image, **kwargs): kwargs["radius"] = kwargs.pop("sigma") return unsharp_mask(image, preserve_range=True, **kwargs)
class Erode(Transform): """ Erode is a transform that erodes away the boundaries of objects on an image. :param size: Kernel size, defaults to 5 :type size: :class:`int`, optional :param iterations: Number of iterations, defaults to 1 :type iterations: :class:`int`, optional """ arguments = { "size": Number(min_value=1, only_integer=True, only_odd=True, default=5), "iterations": Number(min_value=1, only_integer=True, default=1), } def process(self, image, **kwargs): kernel = np.ones((kwargs["size"], kwargs["size"]), np.uint8) return cv2.erode(image, kernel, iterations=kwargs["iterations"])
class Canny(Transform): """ Canny is a transform that extracts the edges from the image using canny edge detection. :param low: Low threshold, defaults to 100 :type low: :class:`int`, optional :param high: High threshold, defaults to 200 :type high: :class:`int`, optional :param size: Aperture size, defaults to 5 :type size: :class:`int`, optional :param sigma: Sigma for auto canny size, defaults to 0.33 :type sigma: :class:`float`, optional """ arguments = { "low": Number(min_value=1, max_value=255, only_integer=True, default="auto"), "high": Number(min_value=1, max_value=255, only_integer=True, default="auto"), "size": Number(min_value=3, max_value=7, only_integer=True, only_odd=True, default=3), "sigma": Number(min_value=0, default=0.33), } def process(self, image, **kwargs): if kwargs["low"] == "auto": v = np.median(image) kwargs["low"] = int(max(0, (1.0 - kwargs["sigma"]) * v)) if kwargs["high"] == "auto": v = np.median(image) kwargs["high"] = int(min(255, (1.0 + kwargs["sigma"]) * v)) return cv2.Canny(image, kwargs["low"], kwargs["high"], apertureSize=kwargs["size"])
class Gradient(Transform): """ Gradient is a transform that computes the gradient of an image. Available methods: \t**∙ sobel** - Gradient using Sobel kernel\n \t**∙ laplace** - Laplacian of an image\n \t**∙ morphological** - Morphological Gradient (difference between dilation and erosion).\n :param axis: Axis to compute, defaults to "both" (magnitude) :type axis: :class:`str`, optional :param size: Kernel size, defaults to 5 :type size: :class:`int`, optional """ methods = { "sobel": { "arguments": ["axis", "size"] }, "morphological": { "arguments": ["size"] }, "laplace": {}, } default_method = "sobel" arguments = { "axis": Option(["both", "x", "y"], default=0), "size": Number(min_value=1, max_value=31, only_integer=True, only_odd=True, default=5), } def process(self, image, **kwargs): image = GrayScale().apply(image) if kwargs["method"] == "sobel": if kwargs["axis"] == "both": x = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=kwargs["size"]) y = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=kwargs["size"]) return (x**2 + y**2)**0.5 if kwargs["axis"] == "x": return cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=kwargs["size"]) else: return cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=kwargs["size"]) elif kwargs["method"] == "laplace": return cv2.Laplacian(image, cv2.CV_64F) else: kernel = np.ones((kwargs["size"], kwargs["size"]), np.uint8) return cv2.morphologyEx(image, cv2.MORPH_GRADIENT, kernel)
class Brightness(Transform): """ Brightness is a transform that changes the image brightness :param beta: Value of brightness to Add :type beta: :class:`int` """ arguments = { "beta": Number(only_integer=True), } def process(self, image, **kwargs): image = cv2.addWeighted(image, 1, image, 0, kwargs["beta"]) return image
class Contrast(Transform): """ Contrast is a transform that changes the image contrast :param alpha: Value of contrast to Add :type alpha: :class:`float` """ arguments = { "alpha": Number(only_integer=False), } def process(self, image, **kwargs): image = cv2.addWeighted(image, kwargs["alpha"], image, 0, 0) return image
class GammaCorrection(Transform): """ GammaCorrection is a transform that corrects the contrast of images and displays. :param gamma: Gamma value :type gamma: :class:`Float` """ arguments = { "gamma": Number(min_value=1e-30, default=1), } def process(self, image, **kwargs): table = np.array([((i / 255.0)**(1.0 / kwargs["gamma"])) * 255 for i in np.arange(0, 256)]).astype("uint8") return cv2.LUT(image, table)
class Hue(Transform): """ Hue is a transform that changes the image hue :param value: Value of Hue to Add :type value: :class:`int` """ arguments = { "value": Number(only_integer=True), } def process(self, image, **kwargs): image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) image[:, :, 0] = (image[:, :, 0] + kwargs["value"]) % 180 return cv2.cvtColor(image, cv2.COLOR_HSV2BGR)
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 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 GradientAngle(Transform): """ GradientAngle is a transform that computes the angles of the image gradient. :param size: Kernel size, defaults to 5 :type size: :class:`int`, optional """ arguments = { "size": Number(min_value=1, max_value=31, only_integer=True, only_odd=True, default=5) } def process(self, image, **kwargs): image = GrayScale().apply(image) x = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=kwargs["size"]) y = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=kwargs["size"]) return np.arctan2(x, y)
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}
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 Noise(Transform): """ Noise is a transform that adds various types of noise to the image. Currently supported\ types are: \t**∙ gaussian** - Gaussian-distributed additive noise\n \t**∙ poisson** - Poisson-distributed noise generated from the data\n \t**∙ salt** - Replaces random pixels with 255\n \t**∙ pepper** - Replaces random pixels with 0\n \t**∙ sp** - Replaces random pixels with either 1 or 0.\n \t**∙ speckle** - Multiplicative noise using ``out = image + n * image``, where \ n is uniform noise with specified mean & variance.\n :param method: Type of noise to add :type method: :class:`str`, optional :param seed: Seed for the random generator, by default generates random seed :type seed: :class:`int`, optional :param clip: If True the output will be clipped to [0, 255], defaults to True :type clip: :class:`bool`, optional :param mean: Mean of random distribution. Used in ‘gaussian’ and ‘speckle’, defaults to 0 :type mean: :class:`float`, optional :param var: Variance of random distribution. Used in ‘gaussian’ and ‘speckle’, \ defaults to 0.01 :type var: :class:`float`, optional :param amount: Percentage of image pixels to replace with noise.Used in ‘salt’, \ ‘pepper’, and ‘s&p’. Default : 0.05 :type amount: :class:`float`, optional :param salt_vs_pepper: Proportion of salt vs. pepper noise for ‘s&p’ on range [0, 1].\ Higher values represent more salt. Default : 0.5 (equal amounts) :type salt_vs_pepper: :class:`float`, optional """ methods = { "gaussian": { "arguments": ["mean", "var", "seed", "clip"] }, "salt": { "arguments": ["amount", "seed", "clip"] }, "pepper": { "arguments": ["amount", "seed", "clip"] }, "sp": { "arguments": ["amount", "salt_vs_pepper", "seed", "clip"] }, "poisson": { "arguments": ["seed", "clip"] }, } method_name = "mode" default_method = "gaussian" arguments = { "seed": Number(min_value=0, max_value=2**32 - 1, default=False), "clip": Type(bool, default=True), "mean": Number(default=0), "var": Number(min_value=0, max_value=255, default=2.5), "amount": Number(min_value=0, max_value=1, default=0.05), "salt_vs_pepper": Number(min_value=0, max_value=1, default=0.5), } def process(self, image, **kwargs): kwargs["seed"] = kwargs["seed"] if kwargs["seed"] else None if kwargs["mode"] == "gaussian": kwargs["var"] = kwargs["var"] / 255 if kwargs["mode"] == "sp": kwargs["mode"] = "s&p" return random_noise(image, **kwargs)
class Blur(Transform): """ Blur is a transform that blurs an image. \t**∙ uniform** - Uniform Filter\n \t**∙ gaussian** - Gaussian-distributed additive noise\n \t**∙ median** - Median Filter\n \t**∙ bilateral** - Edge preserving blur\n :param method: Blur method to be used, defaults to "uniform" :type method: :class:`str`, optional :param size: Kernel size, defaults to auto :type size: :class:`int`, optional :param sigma: Sigma value, defaults to 0 :type sigma: :class:`int`, optional :param sigma_color: Sigma for color space, defaults to 75 :type sigma_color: :class:`int`, optional :param sigma_space: Sigma for coordinate space, defaults to 75 :type sigma_space: :class:`int`, optional :param truncate: Truncate the filter at this many standard deviations., defaults to 4 :type truncate: :class:`int`, optional """ methods = { "uniform": { "arguments": ["size"] }, "gaussian": { "arguments": ["size", "sigma", "truncate"] }, "median": { "arguments": ["size"] }, "bilateral": { "arguments": ["size", "sigma_color", "sigma_space"] }, } default_method = "gaussian" arguments = { "size": Number(min_value=1, only_integer=True, only_odd=True, default="auto"), "sigma": Number(min_value=0, default=1), "sigma_color": Number(min_value=0, default=75), "sigma_space": Number(min_value=0, default=75), "truncate": Number(min_value=0, default=4), } def process(self, image, **kwargs): if kwargs["method"] == "uniform": return cv2.blur(image, (kwargs["size"], kwargs["size"])) elif kwargs["method"] == "gaussian": if kwargs["size"] == "auto": kwargs["size"] = 2 * int(kwargs["sigma"] * kwargs["truncate"] + 0.5) + 1 return cv2.GaussianBlur(image, (kwargs["size"], kwargs["size"]), kwargs["sigma"]) elif kwargs["method"] == "median": return cv2.medianBlur(image, kwargs["size"]) else: if kwargs["size"] == "auto": kwargs["size"] = 5 return cv2.bilateralFilter(image, kwargs["size"], kwargs["sigma_color"], kwargs["sigma_space"])
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