Skip to content

SHEETtoPNG

Converter class to convert input sample sheet to character PNGs.

Source code in handwrite/sheettopng.py
class SHEETtoPNG:
    """Converter class to convert input sample sheet to character PNGs."""

    def convert(self, sheet, characters_dir, config, cols=8, rows=10):
        """Convert a sheet of sample writing input to a custom directory structure of PNGs.

        Detect all characters in the sheet as a separate contours and convert each to
        a PNG image in a temp/user provided directory.

        Parameters
        ----------
        sheet : str
            Path to the sheet file to be converted.
        characters_dir : str
            Path to directory to save characters in.
        config: str
            Path to config file.
        cols : int, default=8
            Number of columns of expected contours. Defaults to 8 based on the default sample.
        rows : int, default=10
            Number of rows of expected contours. Defaults to 10 based on the default sample.
        """
        with open(config) as f:
            threshold_value = json.load(f).get("threshold_value", 200)
        if os.path.isdir(sheet):
            raise IsADirectoryError("Sheet parameter should not be a directory.")
        characters = self.detect_characters(
            sheet, threshold_value, cols=cols, rows=rows
        )
        self.save_images(
            characters,
            characters_dir,
        )

    def detect_characters(self, sheet_image, threshold_value, cols=8, rows=10):
        """Detect contours on the input image and filter them to get only characters.

        Uses opencv to threshold the image for better contour detection. After finding all
        contours, they are filtered based on area, cropped and then sorted sequentially based
        on coordinates. Finally returs the cols*rows top candidates for being the character
        containing contours.

        Parameters
        ----------
        sheet_image : str
            Path to the sheet file to be converted.
        threshold_value : int
            Value to adjust thresholding of the image for better contour detection.
        cols : int, default=8
            Number of columns of expected contours. Defaults to 8 based on the default sample.
        rows : int, default=10
            Number of rows of expected contours. Defaults to 10 based on the default sample.

        Returns
        -------
        sorted_characters : list of list
            Final rows*cols contours in form of list of list arranged as:
            sorted_characters[x][y] denotes contour at x, y position in the input grid.
        """
        # TODO Raise errors and suggest where the problem might be

        # Read the image and convert to grayscale
        image = cv2.imread(sheet_image)
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        # Threshold and filter the image for better contour detection
        _, thresh = cv2.threshold(gray, threshold_value, 255, 1)
        close_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
        close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, close_kernel, iterations=2)

        # Search for contours.
        contours, h = cv2.findContours(
            close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )

        # Filter contours based on number of sides and then reverse sort by area.
        contours = sorted(
            filter(
                lambda cnt: len(
                    cv2.approxPolyDP(cnt, 0.01 * cv2.arcLength(cnt, True), True)
                )
                == 4,
                contours,
            ),
            key=cv2.contourArea,
            reverse=True,
        )

        # Calculate the bounding of the first contour and approximate the height
        # and width for final cropping.
        x, y, w, h = cv2.boundingRect(contours[0])
        space_h, space_w = 7 * h // 16, 7 * w // 16

        # Since amongst all the contours, the expected case is that the 4 sided contours
        # containing the characters should have the maximum area, so we loop through the first
        # rows*colums contours and add them to final list after cropping.
        characters = []
        for i in range(rows * cols):
            x, y, w, h = cv2.boundingRect(contours[i])
            cx, cy = x + w // 2, y + h // 2

            roi = image[cy - space_h : cy + space_h, cx - space_w : cx + space_w]
            characters.append([roi, cx, cy])

        # Now we have the characters but since they are all mixed up we need to position them.
        # Sort characters based on 'y' coordinate and group them by number of rows at a time. Then
        # sort each group based on the 'x' coordinate.
        characters.sort(key=lambda x: x[2])
        sorted_characters = []
        for k in range(rows):
            sorted_characters.extend(
                sorted(characters[cols * k : cols * (k + 1)], key=lambda x: x[1])
            )

        return sorted_characters

    def save_images(self, characters, characters_dir):
        """Create directory for each character and save as PNG.

        Creates directory and PNG file for each image as following:

            characters_dir/ord(character)/ord(character).png  (SINGLE SHEET INPUT)
            characters_dir/sheet_filename/ord(character)/ord(character).png  (MULTIPLE SHEETS INPUT)

        Parameters
        ----------
        characters : list of list
            Sorted list of character images each inner list representing a row of images.
        characters_dir : str
            Path to directory to save characters in.
        """
        os.makedirs(characters_dir, exist_ok=True)

        # Create directory for each character and save the png for the characters
        # Structure (single sheet): UserProvidedDir/ord(character)/ord(character).png
        # Structure (multiple sheets): UserProvidedDir/sheet_filename/ord(character)/ord(character).png
        for k, images in enumerate(characters):
            character = os.path.join(characters_dir, str(ALL_CHARS[k]))
            if not os.path.exists(character):
                os.mkdir(character)
            cv2.imwrite(
                os.path.join(character, str(ALL_CHARS[k]) + ".png"),
                images[0],
            )

convert(self, sheet, characters_dir, config, cols=8, rows=10)

Convert a sheet of sample writing input to a custom directory structure of PNGs.

Detect all characters in the sheet as a separate contours and convert each to a PNG image in a temp/user provided directory.

Parameters:

Name Type Description Default
sheet str

Path to the sheet file to be converted.

required
characters_dir str

Path to directory to save characters in.

required
config str

Path to config file.

required
cols int, default=8

Number of columns of expected contours. Defaults to 8 based on the default sample.

8
rows int, default=10

Number of rows of expected contours. Defaults to 10 based on the default sample.

10
Source code in handwrite/sheettopng.py
def convert(self, sheet, characters_dir, config, cols=8, rows=10):
    """Convert a sheet of sample writing input to a custom directory structure of PNGs.

    Detect all characters in the sheet as a separate contours and convert each to
    a PNG image in a temp/user provided directory.

    Parameters
    ----------
    sheet : str
        Path to the sheet file to be converted.
    characters_dir : str
        Path to directory to save characters in.
    config: str
        Path to config file.
    cols : int, default=8
        Number of columns of expected contours. Defaults to 8 based on the default sample.
    rows : int, default=10
        Number of rows of expected contours. Defaults to 10 based on the default sample.
    """
    with open(config) as f:
        threshold_value = json.load(f).get("threshold_value", 200)
    if os.path.isdir(sheet):
        raise IsADirectoryError("Sheet parameter should not be a directory.")
    characters = self.detect_characters(
        sheet, threshold_value, cols=cols, rows=rows
    )
    self.save_images(
        characters,
        characters_dir,
    )

detect_characters(self, sheet_image, threshold_value, cols=8, rows=10)

Detect contours on the input image and filter them to get only characters.

Uses opencv to threshold the image for better contour detection. After finding all contours, they are filtered based on area, cropped and then sorted sequentially based on coordinates. Finally returs the cols*rows top candidates for being the character containing contours.

Parameters:

Name Type Description Default
sheet_image str

Path to the sheet file to be converted.

required
threshold_value int

Value to adjust thresholding of the image for better contour detection.

required
cols int, default=8

Number of columns of expected contours. Defaults to 8 based on the default sample.

8
rows int, default=10

Number of rows of expected contours. Defaults to 10 based on the default sample.

10

Returns:

Type Description
list of list

Final rows*cols contours in form of list of list arranged as: sorted_characters[x][y] denotes contour at x, y position in the input grid.

Source code in handwrite/sheettopng.py
def detect_characters(self, sheet_image, threshold_value, cols=8, rows=10):
    """Detect contours on the input image and filter them to get only characters.

    Uses opencv to threshold the image for better contour detection. After finding all
    contours, they are filtered based on area, cropped and then sorted sequentially based
    on coordinates. Finally returs the cols*rows top candidates for being the character
    containing contours.

    Parameters
    ----------
    sheet_image : str
        Path to the sheet file to be converted.
    threshold_value : int
        Value to adjust thresholding of the image for better contour detection.
    cols : int, default=8
        Number of columns of expected contours. Defaults to 8 based on the default sample.
    rows : int, default=10
        Number of rows of expected contours. Defaults to 10 based on the default sample.

    Returns
    -------
    sorted_characters : list of list
        Final rows*cols contours in form of list of list arranged as:
        sorted_characters[x][y] denotes contour at x, y position in the input grid.
    """
    # TODO Raise errors and suggest where the problem might be

    # Read the image and convert to grayscale
    image = cv2.imread(sheet_image)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # Threshold and filter the image for better contour detection
    _, thresh = cv2.threshold(gray, threshold_value, 255, 1)
    close_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, close_kernel, iterations=2)

    # Search for contours.
    contours, h = cv2.findContours(
        close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    # Filter contours based on number of sides and then reverse sort by area.
    contours = sorted(
        filter(
            lambda cnt: len(
                cv2.approxPolyDP(cnt, 0.01 * cv2.arcLength(cnt, True), True)
            )
            == 4,
            contours,
        ),
        key=cv2.contourArea,
        reverse=True,
    )

    # Calculate the bounding of the first contour and approximate the height
    # and width for final cropping.
    x, y, w, h = cv2.boundingRect(contours[0])
    space_h, space_w = 7 * h // 16, 7 * w // 16

    # Since amongst all the contours, the expected case is that the 4 sided contours
    # containing the characters should have the maximum area, so we loop through the first
    # rows*colums contours and add them to final list after cropping.
    characters = []
    for i in range(rows * cols):
        x, y, w, h = cv2.boundingRect(contours[i])
        cx, cy = x + w // 2, y + h // 2

        roi = image[cy - space_h : cy + space_h, cx - space_w : cx + space_w]
        characters.append([roi, cx, cy])

    # Now we have the characters but since they are all mixed up we need to position them.
    # Sort characters based on 'y' coordinate and group them by number of rows at a time. Then
    # sort each group based on the 'x' coordinate.
    characters.sort(key=lambda x: x[2])
    sorted_characters = []
    for k in range(rows):
        sorted_characters.extend(
            sorted(characters[cols * k : cols * (k + 1)], key=lambda x: x[1])
        )

    return sorted_characters

save_images(self, characters, characters_dir)

Create directory for each character and save as PNG.

Creates directory and PNG file for each image as following:

characters_dir/ord(character)/ord(character).png  (SINGLE SHEET INPUT)
characters_dir/sheet_filename/ord(character)/ord(character).png  (MULTIPLE SHEETS INPUT)

Parameters:

Name Type Description Default
characters list of list

Sorted list of character images each inner list representing a row of images.

required
characters_dir str

Path to directory to save characters in.

required
Source code in handwrite/sheettopng.py
def save_images(self, characters, characters_dir):
    """Create directory for each character and save as PNG.

    Creates directory and PNG file for each image as following:

        characters_dir/ord(character)/ord(character).png  (SINGLE SHEET INPUT)
        characters_dir/sheet_filename/ord(character)/ord(character).png  (MULTIPLE SHEETS INPUT)

    Parameters
    ----------
    characters : list of list
        Sorted list of character images each inner list representing a row of images.
    characters_dir : str
        Path to directory to save characters in.
    """
    os.makedirs(characters_dir, exist_ok=True)

    # Create directory for each character and save the png for the characters
    # Structure (single sheet): UserProvidedDir/ord(character)/ord(character).png
    # Structure (multiple sheets): UserProvidedDir/sheet_filename/ord(character)/ord(character).png
    for k, images in enumerate(characters):
        character = os.path.join(characters_dir, str(ALL_CHARS[k]))
        if not os.path.exists(character):
            os.mkdir(character)
        cv2.imwrite(
            os.path.join(character, str(ALL_CHARS[k]) + ".png"),
            images[0],
        )