Module AmpliVision.src.objs.image

Sub-modules

AmpliVision.src.objs.image.detectors
AmpliVision.src.objs.image.image
AmpliVision.src.objs.image.image_scanner
AmpliVision.src.objs.image.processors
AmpliVision.src.objs.image.utils

Classes

class ColorContourExtractor

"

ColorContourExtractor

This class is responsible for processing an image to isolate the color of the pins.

Methods

  • process_image(scanned_image: np.ndarray) -> np.ndarray

    • This method pre-processes the image to isolate the color of the pins.
  • show_result(edges: np.ndarray) -> None

    • This method shows the result of the pre-processing.

Example

import cv2 as cv
import numpy as np
from src.objs.image.processors.image_processor import ImageProcessor

scanned_image = cv.imread('path/to/image.jpg')
edges = ImageProcessor.process_image(scanned_image)
ImageProcessor.show_result(edges)
Expand source code
class ColorContourExtractor:
    """"   
    ## ColorContourExtractor
    
    This class is responsible for processing an image to isolate the color of the pins.
    
    ### Methods
    - `process_image(scanned_image: np.ndarray) -> np.ndarray`
        - This method pre-processes the image to isolate the color of the pins.
        
    - `show_result(edges: np.ndarray) -> None`
        - This method shows the result of the pre-processing.
    
    ### Example
    ```python
    import cv2 as cv
    import numpy as np
    from src.objs.image.processors.image_processor import ImageProcessor

    scanned_image = cv.imread('path/to/image.jpg')
    edges = ImageProcessor.process_image(scanned_image)
    ImageProcessor.show_result(edges)
    ```
    """

    # A function that pre-processes the image to isolate the color of the pins.
    @staticmethod
    def process_image(
        scanned_image: np.ndarray, 
        hsv_lower = [0, 55, 0], 
        hsv_upper = [360, 255,255], 
        double_thresh:bool = False, 
        display:bool=False) -> np.ndarray:
        """ 
        This method pre-processes the image to isolate the color of the pins in Grid to find blocks. 
        It is also used in TestAnalyzer to find the positive spots with hsv mask.
        Thread carefully
        """

        # Copy the image to avoid modifying the original image
        scanned_image_copy = scanned_image.copy()
        
        # Convert the image to HSV color space. Hue Saturation Value. 
        # Similar to RGB but more useful for color isolation.
        img_hsv = cv.cvtColor(scanned_image_copy, cv.COLOR_BGR2HSV)

        # Define the lower and upper bounds for the color you want to isolate
        # These values are the product of trial and error and are not necessarily perfect.
        hsv_lower_color = np.array(hsv_lower)
        hsv_upper_color = np.array(hsv_upper)

        # Create a mask to filter out the grayscale colors isolating the color of the pins.
        color_mask = cv.inRange(img_hsv, hsv_lower_color, hsv_upper_color)

        # Visualize the mask on top of the original image before thresholding
        #mask_before_thresholding = color_mask.copy()

        edges = cv.Canny(color_mask, 0, 255)

        if double_thresh:
            
            second_mask = cv.bitwise_and(scanned_image_copy, scanned_image_copy, mask=color_mask)
            #cv.imshow('bitwise and image + mask 1', cv.resize(color_mask,(200,200)))
            
            # make all black pixels white
            second_mask[second_mask == 0] = 255
            #cv.imshow('make black pixels white',  cv.resize(color_mask, (200, 200)))
            
            #  thresholding
            second_mask = cv.cvtColor(second_mask, cv.COLOR_BGR2GRAY)
            #cv.imshow('grey',  cv.resize(color_mask, (200, 200)))
            
            second_mask = cv.bitwise_not(second_mask) 
            #cv.imshow('bitwise not',  cv.resize(second_mask, (200, 200)))
            #cv.waitKey(0)

            cv.threshold(second_mask, 125, 255, cv.THRESH_BINARY, second_mask)
            
            edges = cv.Canny(second_mask, 0, 255)
            
            #cv.imshow('second thresholding ',  cv.resize(
             #   cv.bitwise_and(scanned_image_copy,scanned_image_copy, mask=second_mask), (400, 400)))   
            #cv.imshow('first thresholding ',  cv.resize(
              #  cv.bitwise_and(scanned_image_copy,scanned_image_copy, mask=color_mask), (400, 400)))
            #cv.waitKey(0)

            
        #cv.destroyAllWindows()
            
        contours, _ = cv.findContours(edges, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE)
    
        if display:
            pass
            contours = ColorContourExtractor.show_result(contours, scanned_image_copy)

        return contours
    
    # Show the result of the pre-processing.
    @staticmethod
    def show_result(contours: np.ndarray, image) -> None:
        """ this method shows the result of the pre-processing."""

        copy = image.copy()

        # show only the pixels where sqrt(a^2 + b^2) > 10
        # this will remove the background noise
        lab = cv.cvtColor(copy, cv.COLOR_BGR2LAB)
        lab = cv.blur(lab, (3, 3))

        # split the image into L, A, and B channels
        l, a, b = cv.split(lab)
        r, g, b = cv.split(copy)

        plt.imshow(copy)
        plt.title('Color Contour Extractor - COPY')
        plt.show()
        

        for row in range(len(l)):
            for col in range(len(l[0])):
                if  l[row][col] >= 225:
                    l[row][col] = 0
                    a[row][col] = 0
                    b[row][col] = 0
                else:
                    l[row][col] = 255
                    a[row][col] = 255
                    b[row][col] = 255
        
        lab = cv.merge((l, a, b))
    
        mask = cv.bitwise_and(lab, copy)
        mask = cv.cvtColor(mask, cv.COLOR_BGR2GRAY)
        mask = cv.bitwise_not(mask)

        # draw the contours
        edges = cv.Canny(mask, 0, 255)
        contours, _ = cv.findContours(edges, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE)
        cv.drawContours(copy, contours, -1, (0, 255, 0), 1)
        copy = cv.resize(copy, (400, 400))


        """
        copy = image.copy()
        cv.drawContours(copy, contours, -1, (0, 255, 0), 1)
        copy = cv.resize(copy, (400, 400))
        """
        plt.imshow(copy)
        plt.title('Color Contour Extractor')
        plt.show()
        
        return max(contours, key = cv.contourArea)

Static methods

def process_image(scanned_image: numpy.ndarray, hsv_lower=[0, 55, 0], hsv_upper=[360, 255, 255], double_thresh: bool = False, display: bool = False) ‑> numpy.ndarray

This method pre-processes the image to isolate the color of the pins in Grid to find blocks. It is also used in TestAnalyzer to find the positive spots with hsv mask. Thread carefully

def show_result(contours: numpy.ndarray, image) ‑> None

this method shows the result of the pre-processing.

class GridImageNormalizer

Image Normalizer

Class to normalize the image of the grid by scanning the grid and making it square ratio.

Methods:

  • scan(id: int, image: ndarray, resize_factor: float = 1) -> (Image, int)
    • This method scans the image and returns the scanned image.
  • resize_2_std(img: ndarray, factor: float, w:int=None, h:int = None) -> ndarray
    • This method resizes the image to a given percentage of the current size.
Expand source code
class GridImageNormalizer:
    """
    ### Image Normalizer
    Class to normalize the image of the grid by scanning the grid and making it square ratio.

    #### Methods:
    - `scan(id: int, image: ndarray, resize_factor: float = 1) -> (Image, int)`
        - This method scans the image and returns the scanned image.
    - `resize_2_std(img: ndarray, factor: float, w:int=None, h:int = None) -> ndarray`
        - This method resizes the image to a given percentage of the current size.
    """
    @classmethod
    def scan(cls, image_name: str, image: ndarray, do_white_balance:bool):
        """
        ### Scan image
        Scan the image and return the scanned image.

        #### Args:
        * id : id of the image
        * image : image to be scanned

        #### Returns:
        * scanned image
        """
        print(f"{image_name} loaded")

        # Scan the image isolating the grid
        Image_i = ImageScanner.scan(
            image,
            do_white_balance=do_white_balance 
        )
        print(f"{image_name} scanned!")

        # Resize image so that its height and width are the same
        w, h = Image_i.shape[:2]
        Image_i = cls.resize(Image_i, 1, w, w)

        return Image_i

    @staticmethod
    def resize(img: ndarray, factor: float, w: int = None, h: int = None):
        """
        ### Resize
        Resize image to a given percentage of current size.

        #### Args:
        * img : image to be resized
        * factor : percentage of current size to resize to
        * w : width of image
        * h : height of image

        #### Returns:
        * resized image
        """

        # If width and height are not given, get them from the image
        if w == None and h == None:
            w, h = img.shape[:2]

        resized_image = cv.resize(
            img, (int(w*factor), int(h*factor)), interpolation=cv.INTER_CUBIC)

        return resized_image

Static methods

def resize(img: numpy.ndarray, factor: float, w: int = None, h: int = None)

Resize

Resize image to a given percentage of current size.

Args:

  • img : image to be resized
  • factor : percentage of current size to resize to
  • w : width of image
  • h : height of image

Returns:

  • resized image
def scan(image_name: str, image: numpy.ndarray, do_white_balance: bool)

Scan image

Scan the image and return the scanned image.

Args:

  • id : id of the image
  • image : image to be scanned

Returns:

  • scanned image
class ImageLoader

ImageLoader

This class is responsible for loading images from a given folder and converting HEIC images to JPG.

Methods

  • load_images(path_to_imgs: str) -> list
    • This method loads all the images in a folder and returns a list of images.
  • heic2jpg(path_to_heic: str) -> None
    • This method creates .jpg images from the .HEIC images of given folder.

Example

from src.objs.image.utils.image_loader import ImageLoader

images = ImageLoader.load_images('path/to/images')
or 
images = ImageLoader.heic2jpg('path/to/heic')
images = ImageLoader.load_images('path/to/images')
Expand source code
class ImageLoader:
    """
    ## ImageLoader

    This class is responsible for loading images from a given folder and converting HEIC images to JPG.

    ### Methods
    - `load_images(path_to_imgs: str) -> list`
        - This method loads all the images in a folder and returns a list of images.
    - `heic2jpg(path_to_heic: str) -> None`
        - This method creates .jpg images from the .HEIC images of given folder.

    ### Example
    ```python
    from src.objs.image.utils.image_loader import ImageLoader

    images = ImageLoader.load_images('path/to/images')
    or 
    images = ImageLoader.heic2jpg('path/to/heic')
    images = ImageLoader.load_images('path/to/images')
    ```
    """
    @staticmethod
    def load_images(path_to_imgs: str, return_paths_only:bool = False, display: int = 0):
        """
        ### Image loader
        Loads all the images in a folder and returns a list of images

        #### Args:
        path_to_images: path to image folder

        #### Returns:
        List of images
        """

        # acceptable image types
        types = ('.png', '.jpg', 'JPEG')

        # reading single image if path is only one image
        end = path_to_imgs[-4:]

        if end in types:
            return [cv.imread(path_to_imgs)]

        # reading all images of acceptable types from given directory
        imgs = []
        for f_type in types:
            files = [file for file in glob(f"{path_to_imgs}*{f_type}")]
            
            if return_paths_only:
                return files 

            if display:
                for i, f in enumerate(files):
                    name = f[f.rfind('\\') + 1:]
                    print(f"{i} -> {name}")

            imgs.extend([cv.imread(file) for file in files])

        return imgs

    @staticmethod
    def heic2png(path_to_heic: str):
        """
        ### HEIC to PNG converte
        Creates .png images from the .HEIC images of given folder.    

        #### Args:
        path_to_heic: path to image folder

        #### Returns:
        None
        """

        # finding all .HEIC images in the given folder
        # and converting them to .png
        paths = glob(f"{path_to_heic}*.HEIC")
        print(paths)
        for path in paths:
            pillow_heif.register_heif_opener()

            img = im.open(path)
            img.save(path[:-4] + 'png', format="png")
            print(f"{path} converted to PNG")

    @staticmethod
    def heic2jpg(path_to_heic: str):
        """
        ### HEIC to JPG converte
        Creates .jpg images from the .HEIC images of given folder.    

        #### Args:
        path_to_heic: path to image folder

        #### Returns:
        None
        """

        # finding all .HEIC images in the given folder
        # and converting them to .jpg
        paths = glob(f"{path_to_heic}*.HEIC")
        print(paths)
        for path in paths:
            pillow_heif.register_heif_opener()

            img = im.open(path)
            img.save(path[:-4] + 'jpg', format="jpeg")
            print(f"{path} converted to JPG")

Static methods

def heic2jpg(path_to_heic: str)

HEIC to JPG converte

Creates .jpg images from the .HEIC images of given folder.

Args:

path_to_heic: path to image folder

Returns:

None

def heic2png(path_to_heic: str)

HEIC to PNG converte

Creates .png images from the .HEIC images of given folder.

Args:

path_to_heic: path to image folder

Returns:

None

def load_images(path_to_imgs: str, return_paths_only: bool = False, display: int = 0)

Image loader

Loads all the images in a folder and returns a list of images

Args:

path_to_images: path to image folder

Returns:

List of images

class ImageScanner

Class to scan the image and return the scanned image.

Methods:

  • scan(image_og: np.ndarray) -> np.ndarray

    • This method scans the image and returns the scanned image.
  • morphological_transform(gpu_img: cv.cuda_GpuMat) -> cv.cuda_GpuMat - This method applies morphological transformations to highlight the grid.

  • remove_background(img: np.ndarray) -> np.ndarray - This method gets rid of the background through masking + grabcut algorithm.

  • find_contours(gpu_img: cv.cuda_GpuMat) -> list - This method finds the contours of the image.

  • detect_corners(contours: list, img: np.ndarray) -> list - This method detects the corners of the grid.

  • perspective_transform(img: np.ndarray, corners: list) -> np.ndarray - This method applies perspective transform to the image.

  • find_dest(pts: list) -> list - This method finds the destination coordinates.

  • order_points(pts: list) -> list - This method orders the points.

reference

<https://learnopencv.com/automatic-document-scanner-using-opencv/>
Expand source code
class ImageScanner:
    """
    Class to scan the image and return the scanned image.

    ## Methods:
    - `scan(image_og: np.ndarray) -> np.ndarray`
        - This method scans the image and returns the scanned image.    

    - `morphological_transform(gpu_img: cv.cuda_GpuMat) -> cv.cuda_GpuMat`
            - This method applies morphological transformations to highlight the grid.

    - `remove_background(img: np.ndarray) -> np.ndarray`
            - This method gets rid of the background through masking + grabcut algorithm.

    - `find_contours(gpu_img: cv.cuda_GpuMat) -> list`
            - This method finds the contours of the image.

    - `detect_corners(contours: list, img: np.ndarray) -> list`
            - This method detects the corners of the grid.

    - `perspective_transform(img: np.ndarray, corners: list) -> np.ndarray`
            - This method applies perspective transform to the image.

    - `find_dest(pts: list) -> list`
            - This method finds the destination coordinates.

    - `order_points(pts: list) -> list`
            - This method orders the points.

    ## reference
        https://learnopencv.com/automatic-document-scanner-using-opencv/
    """

    @classmethod
    def scan(cls, img_og: np.ndarray, do_white_balance: bool = False) -> np.ndarray:
        # Applying morphological transformations to highlight the grid
        # Utilizing the GPU for faster processing
        img = cls.hsv_threshold(img_og.copy(), 100)

        morph_img = MorphologicalTransformer.apply_morph(img)

        # Isolate the grid by removing background (Only works with CPU)
        no_bkg_img = BackgroundRemover.remove_background(morph_img)

        # Adjusting the image to highlight the grid
        contours = ContourFinder.find_contours(no_bkg_img)

        """
        a = no_bkg_img.copy()
        cv.drawContours(a, contours, -1, (0, 255, 0), 3)
        display(a, 0)  # """
        corners = CornerDetector.detect_corners(contours, no_bkg_img)

        final_image = cls.perspective_transform(img_og, corners)

        if do_white_balance:
            final_image = WhiteBalanceAdjuster.adjust(final_image)

        return final_image
    # ----------------- Helper Functions ----------------- #

    @classmethod
    # function to turn everything that isnt kinda white to black
    def hsv_threshold(cls, img: np.ndarray, threshold: int) -> np.ndarray:
        # convert image to hsv
        hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
        # define range of white color in HSV
        lower_white = np.array([0, 0, 255-threshold])
        upper_white = np.array([255, threshold, 255])
        # create a mask
        mask = cv.inRange(hsv, lower_white, upper_white)
        # apply the mask to the image
        res = cv.bitwise_and(img, img, mask=mask)
        return res

    @classmethod
    def perspective_transform(cls, img: np.ndarray, corners: list) -> np.ndarray:
        # REARRANGING THE CORNERS
        destination_corners = cls.find_dest(corners)

        # Getting the homography. (aka scanning the image)
        M = cv.getPerspectiveTransform(np.float32(
            corners), np.float32(destination_corners))

        # Perspective transform using homography.
        final = cv.warpPerspective(
            img, M, (destination_corners[2][0], destination_corners[2][1]), flags=cv.INTER_LINEAR)

        return final

    @classmethod
    def find_dest(cls, pts: list) -> list:
        # DESTINATION COORDINATES
        (tl, tr, br, bl) = pts

        # Finding the maximum width.
        widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
        widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
        maxWidth = max(int(widthA), int(widthB))

        # Finding the maximum height.
        heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
        heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
        maxHeight = max(int(heightA), int(heightB))

        # Final destination co-ordinates.
        destination_corners = [[0, 0], [maxWidth, 0],
                               [maxWidth, maxHeight], [0, maxHeight]]
        return cls.order_points(destination_corners)

    @staticmethod
    def order_points(pts: list) -> list:
        # Initialising a list of coordinates that will be ordered.
        rect = np.zeros((4, 2), dtype='float32')
        pts = np.array(pts)
        s = pts.sum(axis=1)

        # Top-left point will have the smallest sum.
        rect[0] = pts[np.argmin(s)]

        # Bottom-right point will have the largest sum.
        rect[2] = pts[np.argmax(s)]

        # Computing the difference between the points.
        diff = np.diff(pts, axis=1)

        # Top-right point will have the smallest difference.
        rect[1] = pts[np.argmin(diff)]

        # Bottom-left will have the largest difference.
        rect[3] = pts[np.argmax(diff)]

        # Return the ordered coordinates.
        return rect.astype('int').tolist()

Static methods

def find_dest(pts: list) ‑> list
def hsv_threshold(img: numpy.ndarray, threshold: int) ‑> numpy.ndarray
def order_points(pts: list) ‑> list
def perspective_transform(img: numpy.ndarray, corners: list) ‑> numpy.ndarray
def scan(img_og: numpy.ndarray, do_white_balance: bool = False) ‑> numpy.ndarray
class WhiteBalanceAdjuster

WhiteBalanceAdjuster

This class is responsible for adjusting the white balance of an image.

Methods

  • adjust(image: np.ndarray, reference_region: tuple[int, int, int, int] = (62, 80, 20, 20)) -> np.ndarray
    • This method adjusts the white balance of the image.

Example

import cv2 as cv
import numpy as np
from src.objs.image.utils.image_white_balancer import WhiteBalanceAdjuster

scanned_image = cv.imread('path/to/image.jpg')
adjusted_image = WhiteBalanceAdjuster.adjust(scanned_image)
Expand source code
class WhiteBalanceAdjuster:
    """
    # WhiteBalanceAdjuster
    This class is responsible for adjusting the white balance of an image.

    ## Methods
    - `adjust(image: np.ndarray, reference_region: tuple[int, int, int, int] = (62, 80, 20, 20)) -> np.ndarray`
        - This method adjusts the white balance of the image.
    
    ### Example
    ```python
    import cv2 as cv
    import numpy as np
    from src.objs.image.utils.image_white_balancer import WhiteBalanceAdjuster

    scanned_image = cv.imread('path/to/image.jpg')
    adjusted_image = WhiteBalanceAdjuster.adjust(scanned_image)
    ```
    """

    @staticmethod
    def adjust( image: np.ndarray, 
                reference_region: tuple[int, int, int, int] = (62, 80, 20, 20)
            ) -> np.ndarray:
        """
        Adjust the white balance of the image.
        Args:
            image: The image to adjust.
            reference_region: The top-left coordinates and size of the reference region.
        Returns:
            The white-balanced image.
        """

        # Get the top-left coordinates and size of the reference region
        reference_top_left, reference_size = reference_region[:2], reference_region[2:]

        # Create the reference 10x10 square for the reference region for white balancing
        reference_region = image[reference_top_left[1]:reference_top_left[1] + reference_size[1],
                                reference_top_left[0]:reference_top_left[0] + reference_size[0]]

        # Calculate the mean RGB values of the reference region - image white baseline value
        mean_reference = np.mean(reference_region, axis=(0, 1))

        # Scaling factors for each channel
        scale_factors = 255.0 / mean_reference

        # Apply white balancing to the entire image by multiplying the image to the scale factor
        balanced_image = cv.merge([cv.multiply(image[:, :, i], scale_factors[i]) for i in range(3)])

        # Clip the values to the valid range [0, 255]
        balanced_image = np.clip(balanced_image, 0, 255).astype(np.uint8)
        
        return balanced_image

Static methods

def adjust(image: numpy.ndarray, reference_region: tuple[int, int, int, int] = (62, 80, 20, 20)) ‑> numpy.ndarray

Adjust the white balance of the image.

Args

image
The image to adjust.
reference_region
The top-left coordinates and size of the reference region.

Returns

The white-balanced image.