Skip to content

Commit

Permalink
fix: minor fixes for debugging; add white padding for dots in CropOnM…
Browse files Browse the repository at this point in the history
…arkers
  • Loading branch information
Udayraj123 committed Jan 28, 2025
1 parent ef86906 commit ccbd72f
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
],
"save_detections": true,
"save_image_level": 0,
"show_image_level": 4
"show_image_level": 5
}
}
1 change: 1 addition & 0 deletions src/algorithm/template/detection/ocr/lib/easyocr.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# TODO: Import heavy dependencies at runtime to save load time
import easyocr

from src.algorithm.template.detection.ocr.lib.textocr import TextOCR
Expand Down
9 changes: 6 additions & 3 deletions src/algorithm/template/template_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@


class TemplateLayout:
# TODO: pass 'set_layout' arg as part of 'output_mode' value 'set_layout' and consume it in template + pre_processors
def __init__(self, template, template_path, tuning_config):
self.path = template_path
self.template = template
Expand Down Expand Up @@ -356,7 +357,9 @@ def parse_and_add_field_block(self, block_name, field_block_object):
)
# TODO: support custom field types like Barcode and OCR
self.field_blocks.append(block_instance)
self.validate_parsed_labels(field_block_object["fieldLabels"], block_instance)
self.validate_parsed_field_block(
field_block_object["fieldLabels"], block_instance
)
return block_instance

def prefill_field_block(self, field_block_object):
Expand Down Expand Up @@ -387,7 +390,7 @@ def prefill_field_block(self, field_block_object):

return filled_field_block_object

def validate_parsed_labels(self, field_labels, block_instance):
def validate_parsed_field_block(self, field_labels, block_instance):
parsed_field_labels, block_name = (
block_instance.parsed_field_labels,
block_instance.name,
Expand Down Expand Up @@ -420,7 +423,7 @@ def validate_parsed_labels(self, field_labels, block_instance):
or block_start_y < 0
):
raise Exception(
f"Overflowing field block '{block_name}' with origin {block_instance.bounding_box_origins} and dimensions {block_instance.bounding_box_dimensions} in template with dimensions {self.template_dimensions}"
f"Overflowing field block '{block_name}' with origin {block_instance.bounding_box_origin} and dimensions {block_instance.bounding_box_dimensions} in template with dimensions {self.template_dimensions}"
)

def reset_all_shifts(self):
Expand Down
5 changes: 4 additions & 1 deletion src/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ def show_template_layouts(omr_files, template, tuning_config):
colored_image,
template,
) = template.apply_preprocessors(file_path, gray_image, colored_image)

gray_layout, colored_layout = TemplateDrawing.draw_template_layout(
gray_image,
colored_image,
Expand Down Expand Up @@ -328,7 +329,9 @@ def process_directory_files(
f"(/{files_counter}) Graded with score: {round(score, 2)}\t {default_answers_summary} \t file: '{file_id}'"
)
if evaluation_config_for_response.get_should_export_explanation_csv():
explanation_table = evaluation_config_for_response.get_explanation_table()
explanation_table = (
evaluation_config_for_response.get_explanation_table()
)
explanation_table = table_to_df(explanation_table)
explanation_table.to_csv(
template.get_evaluations_dir().joinpath(file_name + ".csv"),
Expand Down
65 changes: 50 additions & 15 deletions src/processors/internal/CropOnDotLines.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
from src.utils.interaction import InteractionUtils
from src.utils.math import MathUtils

# from rich.table import Table
# from src.utils.logger import console
# from src.utils.logger import logger


class CropOnDotLines(CropOnPatchesCommon):
Expand Down Expand Up @@ -286,7 +285,7 @@ def find_line_corners_and_contours(self, image, zone_description):
(
_,
edge_contours_map,
) = self.find_morph_corners_and_contours_map(
) = self.find_corners_and_contours_map_using_canny(
zone_start, line_morphed, zone_description
)

Expand All @@ -307,33 +306,63 @@ def find_dot_corners_from_options(self, image, zone_description, _file_path):

dot_blur_kernel = tuning_options.get("dotBlurKernel", None)
if dot_blur_kernel:
zone_h, zone_w = zone.shape
blur_h, blur_w = dot_blur_kernel

assert (
zone_h > blur_h and zone_w > blur_w
), f"The zone '{zone_label}' is smaller than provided dotBlurKernel: {zone.shape} < {dot_blur_kernel}"
zone = cv2.GaussianBlur(zone, dot_blur_kernel, 0)

# add white padding (to avoid dilations sticking to edges)
kernel_height, kernel_width = self.dot_kernel_morph.shape[:2]
white_padded_zone, pad_range = ImageUtils.pad_image_from_center(
zone, kernel_width, kernel_height, 255
)

# Open : erode then dilate
morph_c = cv2.morphologyEx(
zone, cv2.MORPH_OPEN, self.dot_kernel_morph, iterations=3
morphed_zone = cv2.morphologyEx(
white_padded_zone, cv2.MORPH_OPEN, self.dot_kernel_morph, iterations=3
)

# TODO: try pyrDown to 64 values and find the outlier for black threshold?
# Dots are expected to be fairly dark
dot_threshold = tuning_options.get("dotThreshold", 150)
_, thresholded = cv2.threshold(morph_c, dot_threshold, 255, cv2.THRESH_TRUNC)
normalised = ImageUtils.normalize(thresholded)

# Threshold-Normalize after morph + white padding
_, white_thresholded = cv2.threshold(
morphed_zone, dot_threshold, 255, cv2.THRESH_TRUNC
)
white_normalised = ImageUtils.normalize(white_thresholded)

# TODO: check if thresholding before morph gives better results (similar to line)

# remove white padding
white_normalised = white_normalised[
pad_range[0] : pad_range[1], pad_range[2] : pad_range[3]
]

# Debug images
if config.outputs.show_image_level >= 5:
self.debug_hstack += [zone, morph_c, thresholded, normalised]
elif config.outputs.show_image_level == 4:
self.debug_hstack += [
zone,
morphed_zone,
white_thresholded,
white_normalised,
]

if config.outputs.show_image_level >= 4:
InteractionUtils.show(
f"threshold_normalised: {zone_label}", normalised, pause=False
f"{zone_label}: threshold_normalised", white_normalised, pause=False
)

corners, _ = self.find_morph_corners_and_contours_map(
zone_start, normalised, zone_description
corners, _ = self.find_corners_and_contours_map_using_canny(
zone_start, white_normalised, zone_description
)
if corners is None:
if config.outputs.show_image_level >= 1:
hstack = ImageUtils.get_padded_hstack(
[self.debug_image, zone, thresholded]
[self.debug_image, zone, white_thresholded]
)
InteractionUtils.show(
f"No patch/dot debug hstack",
Expand All @@ -349,7 +378,9 @@ def find_dot_corners_from_options(self, image, zone_description, _file_path):
return corners

# TODO: >> create a Scanzone class and move some methods there
def find_morph_corners_and_contours_map(self, zone_start, zone, zone_description):
def find_corners_and_contours_map_using_canny(
self, zone_start, zone, zone_description
):
scanner_type, zone_label = (
zone_description["scannerType"],
zone_description["label"],
Expand All @@ -372,7 +403,11 @@ def find_morph_corners_and_contours_map(self, zone_start, zone, zone_description
return None, None

ordered_patch_corners, edge_contours_map = None, None
largest_contour = sorted(all_contours, key=cv2.contourArea, reverse=True)[0]
sorted_contours_by_area = sorted(
all_contours, key=cv2.contourArea, reverse=True
)
largest_contour = sorted_contours_by_area[0]

if config.outputs.show_image_level >= 5:
h, w = canny_edges.shape[:2]
contour_overlay = 255 * np.ones((h, w), np.uint8)
Expand Down
18 changes: 17 additions & 1 deletion src/processors/internal/CropOnPatchesCommon.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from src.processors.internal.WarpOnPointsCommon import WarpOnPointsCommon
from src.utils.constants import CLR_DARK_GREEN
from src.utils.drawing import DrawingUtils
from src.utils.image import ImageUtils
from src.utils.interaction import InteractionUtils
from src.utils.logger import logger
from src.utils.math import MathUtils
from src.utils.parsing import OVERRIDE_MERGER
Expand Down Expand Up @@ -164,6 +166,20 @@ def extract_control_destination_points(self, image, _colored_image, file_path):
zone_control_points, zone_destination_points
)

if config.outputs.show_image_level >= 4:
# TODO: show this if --setLayout is passed as well.
InteractionUtils.show(
f"Zones of the debug image: {file_path}",
self.debug_image,
pause=1,
)
if len(self.debug_hstack) > 0 and config.outputs.show_image_level >= 5:
InteractionUtils.show(
f"Zones debug stack of the image: {file_path}",
ImageUtils.get_padded_hstack(self.debug_hstack),
pause=1,
)

# Fill edge contours
edge_contours_map = self.get_edge_contours_map_from_zone_points(
zone_preset_points
Expand Down Expand Up @@ -287,7 +303,7 @@ def select_point_from_rectangle(rectangle, points_selector):

def compute_scan_zone_util(self, image, zone_description):
zone, scan_zone_rectangle = ShapeUtils.extract_image_from_zone_description(
image, zone_description
image, zone_description, throw_on_overflow=True
)

zone_start = scan_zone_rectangle[0]
Expand Down
31 changes: 17 additions & 14 deletions src/schemas/template_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,14 +363,27 @@
"required": [
"fieldDetectionType",
],
"properties": _common_field_block_properties,
"properties": {**_common_field_block_properties},
"allOf": [
{
"if": {
"properties": {
"fieldDetectionType": {
"const": FieldDetectionType.BUBBLES_THRESHOLD
},
},
},
"then": {
"required": [
"origin",
"bubbleFieldType",
"bubblesGap",
"labelsGap",
"fieldLabels",
],
"additionalProperties": False,
"properties": {
**_traditional_field_block_properties,
"bubbleFieldType": {
"oneOf": [
{
Expand All @@ -385,17 +398,6 @@
]
},
},
"required": [
"origin",
"bubbleFieldType",
"bubblesGap",
"labelsGap",
"fieldLabels",
],
},
"then": {
"additionalProperties": False,
"properties": _traditional_field_block_properties,
},
},
# {
Expand All @@ -405,6 +407,8 @@
# "const": FieldDetectionType.BARCODE_QR
# }
# },
# },
# "then": {
# # TODO: move barcode specific properties into this if-else
# "required": [
# "scanZone",
Expand All @@ -413,8 +417,6 @@
# # TODO: "failIfNotFound"
# # "emptyValue",
# ],
# },
# "then": {
# "additionalProperties": False,
# "properties": {
# **_common_field_block_properties,
Expand Down Expand Up @@ -486,6 +488,7 @@
"required": [
"bubbleDimensions",
"templateDimensions",
"processingImageShape",
"preProcessors",
"fieldBlocks",
],
Expand Down
4 changes: 1 addition & 3 deletions src/utils/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,7 @@ def split_patch_contour_on_corners(patch_corners, source_contour):
edge_contour = edge_contours_map[edge_type]

if len(edge_contour) == 0:
logger.critical(
ordered_patch_corners, source_contour, edge_contours_map
)
logger.warning(ordered_patch_corners, source_contour, edge_contours_map)
logger.warning(
f"No closest points found for {edge_type}: {edge_contours_map}"
)
Expand Down
20 changes: 11 additions & 9 deletions src/utils/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,36 @@ def compute_scan_zone_rectangle(zone_description, include_margins):
return MathUtils.get_rectangle_points(x, y, w, h)

@staticmethod
def extract_image_from_zone_description(image, zone_description):
def extract_image_from_zone_description(
image, zone_description, throw_on_overflow=False
):
# TODO: check bug in margins for scan zone
zone_label = zone_description["label"]
scan_zone_rectangle = ShapeUtils.compute_scan_zone_rectangle(
zone_description, include_margins=True
)
print(
"'zone_description",
zone_description,
"scan_zone_rectangle",
scan_zone_rectangle,
)
return (
ShapeUtils.extract_image_from_zone_rectangle(
image, zone_label, scan_zone_rectangle
image, zone_label, scan_zone_rectangle, throw_on_overflow
),
scan_zone_rectangle,
)

@staticmethod
def extract_image_from_zone_rectangle(image, zone_label, scan_zone_rectangle):
def extract_image_from_zone_rectangle(
image, zone_label, scan_zone_rectangle, throw_on_overflow=False
):
# parse arguments
h, w = image.shape[:2]
# compute zone and clip to image dimensions
zone_start = list(map(int, scan_zone_rectangle[0]))
zone_end = list(map(int, scan_zone_rectangle[2]))

if zone_start[0] < 0 or zone_start[1] < 0 or zone_end[0] > w or zone_end[1] > h:
if throw_on_overflow:
raise Exception(
f"Zone '{zone_label}' with scan rectangle: {[zone_start, zone_end]} overflows the image boundary {[w, h]}."
)
logger.warning(
f"Clipping label {zone_label} with scan rectangle: {[zone_start, zone_end]} to image boundary {[w, h]}."
)
Expand Down

0 comments on commit ccbd72f

Please sign in to comment.