Skip to content

feat: add CropOnLMarkers processor for L-shaped corner markers#294

Open
Praddy2006 wants to merge 1 commit into
Udayraj123:masterfrom
Praddy2006:feature/l-shaped-corner-markers
Open

feat: add CropOnLMarkers processor for L-shaped corner markers#294
Praddy2006 wants to merge 1 commit into
Udayraj123:masterfrom
Praddy2006:feature/l-shaped-corner-markers

Conversation

@Praddy2006

Copy link
Copy Markdown

Summary

Adds a new CropOnLMarkers pre-processor that detects L-shaped corner
markers and uses them to crop and align OMR sheets.

Approach

  • Uses OpenCV contour detection to find L-shaped markers
  • Extracts the inner corner (turning point) of each L as the precise anchor
  • Picks one marker per quadrant (TL, TR, BL, BR)
  • Warps/crops the sheet using ImageUtils.four_point_transform
    reusing existing crop utilities

Changes

  • src/processors/CropOnLMarkers.py — new processor
  • src/schemas/template_schema.py — added CropOnLMarkers to validator
  • samples/sample_lmarkers/ — sample template for testing

Usage in template.json

{
"name": "CropOnLMarkers",
"options": {
"marker_size": 40,
"thickness": 10,
"min_contour_area": 200,
"max_contour_area": 6000
}
}

Closes #270

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add CropOnLMarkers preprocessor for L-corner marker based sheet alignment
✨ Enhancement ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

Description

• Add CropOnLMarkers preprocessor to detect L-corner markers and warp/crop sheets.
• Extend template schema to allow CropOnLMarkers in preProcessors.
• Add a sample template/config demonstrating L-marker based cropping.
Diagram

graph TD
  A["template.json (preProcessors)"] --> B(["CropOnLMarkers"])
  B --> C[["OpenCV contours + threshold"]] --> D["Inner corners"] --> E["Quadrant pick"] --> F["ImageUtils.four_point_transform"] --> G["Warped/cropped image"]
  H["template_schema.py"] --> A

  subgraph Legend
    direction LR
    _cfg["Config/Schema"] ~~~ _proc(["Processor"]) ~~~ _lib[["Library step"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Template-match an L-marker image via existing CropOnMarkers
  • ➕ Reuses existing, battle-tested matching pipeline and option schema
  • ➕ Easier to reason about expected marker appearance via a reference image
  • ➖ More sensitive to scale/print variations unless multi-scale matching is tuned
  • ➖ Requires managing a reference marker image file in each template directory
2. Use fiducial markers (ArUco/AprilTag) for corner anchoring
  • ➕ Robust detection/ID decoding; less ambiguity than generic contours
  • ➕ Can recover orientation/identity without quadrant heuristics
  • ➖ Requires changing printed sheet design and adding tag dictionaries
  • ➖ Adds conceptual and dependency surface area to the project
3. Detect L corners via line extraction (Canny + Hough) and intersections
  • ➕ Can be resilient when contours are broken or partially occluded
  • ➕ Makes the “corner” concept explicit (two dominant perpendicular lines)
  • ➖ Typically more parameters to tune; can be brittle on noisy scans
  • ➖ Harder to constrain false positives without additional heuristics

Recommendation: The contour-based approach in this PR is a reasonable fit because it avoids external reference images and reuses the existing perspective-warp utility. To reduce integration risk, consider (in a follow-up) adding explicit schema validation for CropOnLMarkers options (marker_size/thickness/min/max area) and adding at least one automated regression test using the included sample assets.

Files changed (4) +363 / -0

Enhancement (1) +152 / -0
CropOnLMarkers.pyImplement L-shaped corner marker based cropping/warping +152/-0

Implement L-shaped corner marker based cropping/warping

• Adds a new 'ImagePreprocessor' that thresholds and morphologically closes the image, finds candidate contours by area, derives an inner-corner anchor per candidate via bounding-box proximity to image center, selects one anchor per quadrant, and warps/crops using 'ImageUtils.four_point_transform'. Includes debug visualization hooks and warning logs for insufficient markers.

src/processors/CropOnLMarkers.py

Other (3) +211 / -0
config.jsonAdd sample runtime config for L-marker demo +11/-0

Add sample runtime config for L-marker demo

• Introduces a sample configuration defining display/processing dimensions and a high debug 'show_image_level' to help visualize preprocessing output while tuning L-marker detection.

samples/sample_lmarkers/config.json

template.jsonAdd sample template using CropOnLMarkers preprocessor +199/-0

Add sample template using CropOnLMarkers preprocessor

• Adds a complete example template including field blocks and a 'preProcessors' pipeline that runs 'CropPage' followed by 'CropOnLMarkers' with tuned contour/size thresholds.

samples/sample_lmarkers/template.json

template_schema.pyAllow CropOnLMarkers in template preProcessors enum +1/-0

Allow CropOnLMarkers in template preProcessors enum

• Extends the template JSON schema to accept 'CropOnLMarkers' as a valid preprocessor name so templates using it pass validation.

src/schemas/template_schema.py

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (3) 📜 Skill insights (0)

Context used

Grey Divider


Action required

1. No L-marker anchor regions 📎 Requirement gap ≡ Correctness
Description
The template schema and sample configuration add CropOnLMarkers but provide no way to define
anchor-point regions/ROIs for L-marker detection. This prevents templates from explicitly expressing
where L-markers should be found and increases the risk of unreliable detection across varied scans.
Code

src/schemas/template_schema.py[R70-76]

                    "name": {
                        "type": "string",
                        "enum": [
+                            "CropOnLMarkers",
                            "CropOnMarkers",
                            "CropPage",
                            "FeatureBasedAlignment",
Evidence
PR Compliance ID 1 requires schema support for anchor-point regions for L-markers, but the schema
only adds CropOnLMarkers to the enum and does not define any options schema/fields for anchor
regions. The sample template and processor options likewise contain no anchor/ROI definitions to
parse.

Template schema supports anchor points for L-shaped corner markers
src/schemas/template_schema.py[64-183]
samples/sample_lmarkers/template.json[179-198]
src/processors/CropOnLMarkers.py[13-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CropOnLMarkers` is introduced, but the template schema does not support defining anchor-point regions (ROIs) for L-shaped corner markers, and the processor options currently expose only global detection knobs (e.g., `min_contour_area`). This violates the requirement to support anchor point definitions in the template without breaking existing templates.

## Issue Context
The schema currently enumerates `CropOnLMarkers` but does not define an `if name == CropOnLMarkers` options schema, nor any structure for anchor/ROI definitions. The sample template also lacks anchor definitions, and the processor does not read them.

## Fix Focus Areas
- src/schemas/template_schema.py[64-183]
- src/processors/CropOnLMarkers.py[13-35]
- samples/sample_lmarkers/template.json[179-198]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. No Canny edge detection 📎 Requirement gap ☼ Reliability
Description
CropOnLMarkers.apply_filter() performs thresholding and then directly extracts contours, but never
runs cv2.Canny() edge detection as required. This can reduce detection robustness for L-markers
under varying lighting and print artifacts.
Code

src/processors/CropOnLMarkers.py[R44-58]

+        # Step 1: Blur + threshold to get clean black/white image
+        gray = cv2.GaussianBlur(image, (5, 5), 0)
+        _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
+
+        # Step 2: Morphological close - connects broken edges of L-shapes
+        kernel = np.ones((3, 3), np.uint8)
+        binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=2)
+
+        if config.outputs.show_image_level >= 2:
+            InteractionUtils.show("CropOnLMarkers: binary", binary, config=config)
+
+        # Step 3: Find all contours
+        contours, _ = cv2.findContours(
+            binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
+        )
Evidence
PR Compliance ID 3 requires Canny edge detection plus contour extraction. The implementation
thresholds and uses cv2.findContours() on the binary image without any cv2.Canny() call.

L-shaped markers are detected using Canny edge detection and contour extraction of corner/turn points
src/processors/CropOnLMarkers.py[44-58]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The L-marker detection pipeline does not use Canny edge detection before contour extraction, despite the compliance requirement calling for a Canny + contour approach to locate L-shaped markers and their corner/turning points.

## Issue Context
Current flow: blur -> Otsu threshold -> morphology close -> `findContours(binary, ...)`. There is no `cv2.Canny(...)` stage.

## Fix Focus Areas
- src/processors/CropOnLMarkers.py[44-58]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Hard-coded 4 marker points 📎 Requirement gap ☼ Reliability
Description
The processor requires at least 4 candidates and then selects exactly one per quadrant, enforcing a
4-point warp instead of aggregating all detected points and computing an outer bounding rectangle.
This violates the requirement to avoid a fixed 4-point assumption and can fail when detections are
more/less than four.
Code

src/processors/CropOnLMarkers.py[R67-116]

+        if len(candidates) < 4:
+            logger.warning(
+                f"CropOnLMarkers: Found only {len(candidates)} candidates,"  # noqa: E231
+                " need 4. Check min/max contour area settings."
+            )
+            if config.outputs.show_image_level >= 1:
+                InteractionUtils.show(
+                    "CropOnLMarkers: not enough markers",
+                    image,
+                    config=config,
+                )
+            return None
+
+        # Step 5: Find the inner corner (turning point) of each L candidate
+        # We get the bounding box and pick the corner closest to image center
+        h, w = image.shape[:2]
+        cx, cy = w / 2, h / 2  # image center
+
+        inner_corners = []
+        for cnt in candidates:
+            x, y, bw, bh = cv2.boundingRect(cnt)
+
+            # The 4 possible corners of the bounding box
+            box_corners = [
+                [x, y],  # top-left
+                [x + bw, y],  # top-right
+                [x, y + bh],  # bottom-left
+                [x + bw, y + bh],  # bottom-right
+            ]
+
+            # Inner corner = corner of bounding box closest to image center
+            inner = min(
+                box_corners,
+                key=lambda p: (p[0] - cx) ** 2 + (p[1] - cy) ** 2,
+            )
+            inner_corners.append(inner)
+
+        # Step 6: Pick one marker per quadrant (TL, TR, BL, BR)
+        quadrant_corners = self._pick_one_per_quadrant(inner_corners, cx, cy)
+
+        if quadrant_corners is None or len(quadrant_corners) != 4:
+            logger.warning("CropOnLMarkers: Could not find one marker per quadrant.")
+            return None
+
+        logger.info(f"CropOnLMarkers: Detected inner corners: {quadrant_corners}")
+
+        # Step 7: Crop and warp using the 4 inner corners
+        centres = np.array(quadrant_corners, dtype="float32")
+        image = ImageUtils.four_point_transform(image, centres)
+
Evidence
PR Compliance ID 4 forbids limiting aggregation to exactly four points and requires computing an
outer bounding box over all detected points. The code explicitly requires 4 candidates and then
selects exactly four quadrant points for warping, with no outer bounding rectangle computed from all
detections.

Marker aggregation is not limited to 4 points and uses an outer rectangular bounding box over all detected points
src/processors/CropOnLMarkers.py[67-116]
src/processors/CropOnLMarkers.py[119-152]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The algorithm is constrained to exactly 4 points (one per quadrant) and warps using those points, instead of aggregating all detected L-marker points and computing an outer bounding rectangle/bounding box to drive alignment/cropping.

## Issue Context
Current behavior:
- returns `None` when `len(candidates) < 4`
- computes `inner_corners` per candidate
- forces one-per-quadrant selection and requires `len(...) == 4`
- warps via `ImageUtils.four_point_transform(image, centres)`

## Fix Focus Areas
- src/processors/CropOnLMarkers.py[67-116]
- src/processors/CropOnLMarkers.py[119-152]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. findContours unpacking crash 🐞 Bug ☼ Reliability
Description
CropOnLMarkers unpacks cv2.findContours(...) into two values, which raises a runtime unpacking
error on OpenCV versions that return a 3-tuple (e.g., OpenCV 3). This can crash preprocessing before
any cropping is attempted.
Code

src/processors/CropOnLMarkers.py[R55-58]

+        # Step 3: Find all contours
+        contours, _ = cv2.findContours(
+            binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
+        )
Evidence
CropOnLMarkers uses 2-value unpacking for findContours, while the repo’s ImageUtils.grab_contours
documents and handles the 2-vs-3 tuple difference; this mismatch can raise during unpacking on
OpenCV versions returning 3 values.

src/processors/CropOnLMarkers.py[55-58]
src/utils/image.py[41-69]
src/processors/CropPage.py[110-114]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CropOnLMarkers.apply_filter()` assumes `cv2.findContours()` returns exactly `(contours, hierarchy)`, but the repo already accounts for OpenCV’s signature differences elsewhere. On OpenCV builds returning 3 values, preprocessing will fail immediately.

## Issue Context
The codebase includes `ImageUtils.grab_contours()` specifically to normalize the OpenCV v2/v3/v4 `findContours` return shape, and other processors (e.g., `CropPage`) use it.

## Fix Focus Areas
- src/processors/CropOnLMarkers.py[55-58]

## Implementation notes
- Replace the 2-value unpacking with:
 - `contours = ImageUtils.grab_contours(cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE))`
- Keep the rest of the loop unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Warp anchored to bbox 🐞 Bug ≡ Correctness
Description
CropOnLMarkers derives the 'inner corner' from cv2.boundingRect corners, but bounding-box corners
(notably [x + bw, y + bh]) are not guaranteed to be on the L-marker contour, so the warp can be
anchored to points in whitespace and mis-crop/mis-align the sheet. This is especially risky because
those derived points are used directly as inputs to four_point_transform.
Code

src/processors/CropOnLMarkers.py[R85-103]

+        inner_corners = []
+        for cnt in candidates:
+            x, y, bw, bh = cv2.boundingRect(cnt)
+
+            # The 4 possible corners of the bounding box
+            box_corners = [
+                [x, y],  # top-left
+                [x + bw, y],  # top-right
+                [x, y + bh],  # bottom-left
+                [x + bw, y + bh],  # bottom-right
+            ]
+
+            # Inner corner = corner of bounding box closest to image center
+            inner = min(
+                box_corners,
+                key=lambda p: (p[0] - cx) ** 2 + (p[1] - cy) ** 2,
+            )
+            inner_corners.append(inner)
+
Evidence
The code explicitly constructs candidate anchors from boundingRect corners rather than from contour
points, and then uses those anchors for the perspective warp; boundingRect corners are not
guaranteed to coincide with the L contour, so the warp can be based on points unrelated to the
actual marker geometry.

src/processors/CropOnLMarkers.py[80-102]
src/processors/CropOnLMarkers.py[113-116]
src/utils/image.py[101-139]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The processor currently picks the corner closest to the image center from the *bounding rectangle’s* four corners. For an L-shaped contour, a bounding-rect corner can be outside the drawn marker (inside the empty area of the bounding box), which makes the perspective transform anchor inaccurate.

## Issue Context
The output of this step is fed into `ImageUtils.four_point_transform()`, so corner inaccuracies directly affect cropping/alignment.

## Fix Focus Areas
- src/processors/CropOnLMarkers.py[85-116]

## Implementation notes
Implement corner extraction using contour geometry rather than bbox corners. Options (any one is acceptable if robust):
- Use the actual contour points: `pts = cnt.reshape(-1,2)` and choose an anchor that is guaranteed to be on the contour (e.g., the contour point minimizing distance to `(cx, cy)` as the 'inner' point), then optionally refine.
- Or approximate polygon (`cv2.approxPolyDP`) / fit two dominant perpendicular lines and compute their intersection.
- Ensure the chosen anchor point lies on/near the contour (or within a small distance threshold) before accepting it as a marker corner.
- Keep quadrant selection, but operate on these contour-derived anchor points.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Unused L-marker options 🐞 Bug ⚙ Maintainability
Description
CropOnLMarkers parses marker_size, thickness, and excludeImage but they are never used in
detection/cropping (and exclude_files() always returns an empty list), so template configuration
knobs are misleading and excludeImage cannot take effect. This increases the likelihood of
misconfiguration and makes debugging/template tuning harder.
Code

src/processors/CropOnLMarkers.py[R17-39]

+        # Size of each arm of the L-shape (in pixels on the processing image)
+        self.marker_size = int(options.get("marker_size", 30))
+
+        # How thick each arm of the L is
+        self.thickness = int(options.get("thickness", 10))
+
+        # Minimum area of a contour to be considered a valid L marker
+        self.min_contour_area = int(options.get("min_contour_area", 200))
+
+        # Max area - filters out huge blobs that are not markers
+        self.max_contour_area = int(options.get("max_contour_area", 6000))
+
+        # Optional: path to exclude (e.g. a reference image)
+        self.exclude_image_path = options.get("excludeImage", None)
+        if self.exclude_image_path:
+            self.exclude_image_path = os.path.join(
+                self.relative_dir, self.exclude_image_path
+            )
+
+    @staticmethod
+    def exclude_files():
+        # No extra reference image needed for L-marker detection
+        return []
Evidence
The processor stores configuration fields that are not referenced elsewhere in the file, and it
unconditionally returns no excluded files, so excludeImage cannot affect processing and the other
knobs currently don’t influence the algorithm.

src/processors/CropOnLMarkers.py[17-39]
src/processors/CropOnMarkers.py[34-51]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several options are parsed and stored but never used (`marker_size`, `thickness`, `excludeImage`). Additionally, `exclude_files()` always returns `[]`, so even if a user supplies `excludeImage`, it won’t be excluded from processing.

## Issue Context
Other preprocessors that depend on reference images implement `exclude_files()` to exclude those assets from directory processing.

## Fix Focus Areas
- src/processors/CropOnLMarkers.py[17-39]

## Implementation notes
Choose one path:
1) **Implement the options**:
  - Use `marker_size`/`thickness` to add shape/size validation for candidate contours (e.g., expected bbox size ranges, aspect constraints, arm-thickness heuristics).
  - If `excludeImage` is supported, return it from `exclude_files()`.
2) **Or remove the options**:
  - Delete unused option parsing and remove them from docs/samples to avoid suggesting they work.

Keep behavior consistent with other processors (e.g., `CropOnMarkers.exclude_files()`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. No schema for options 🐞 Bug ⚙ Maintainability
Description
The template schema whitelists the new CropOnLMarkers preprocessor name but does not define an
if/then branch to validate its options shape/fields, unlike other preprocessors. As a result,
typos or wrong option types for CropOnLMarkers will pass schema validation and fail later (or
silently behave unexpectedly).
Code

src/schemas/template_schema.py[R70-76]

                    "name": {
                        "type": "string",
                        "enum": [
+                            "CropOnLMarkers",
                            "CropOnMarkers",
                            "CropPage",
                            "FeatureBasedAlignment",
Evidence
The schema now allows the processor name, but only other processor names have dedicated
option-validation branches; CropOnLMarkers has none, so its options are effectively unchecked by the
schema.

src/schemas/template_schema.py[64-83]
src/schemas/template_schema.py[84-183]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CropOnLMarkers` is added to the allowed `preProcessors[].name` enum, but there is no corresponding `allOf` rule to validate `preProcessors[].options` for this processor.

## Issue Context
Other preprocessors (CropOnMarkers, CropPage, FeatureBasedAlignment, etc.) validate options via `if: {name const} -> then: { options: { additionalProperties: false, ... } }`.

## Fix Focus Areas
- src/schemas/template_schema.py[64-183]

## Implementation notes
- Add a new `allOf` entry:
 - `if name == "CropOnLMarkers"`
 - `then options` must be an object and optionally constrain/describe:
   - `marker_size`, `thickness`, `min_contour_area`, `max_contour_area` as integers/numbers
   - `excludeImage` as string
 - Consider `additionalProperties: False` to catch typos early.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines 70 to 76
"name": {
"type": "string",
"enum": [
"CropOnLMarkers",
"CropOnMarkers",
"CropPage",
"FeatureBasedAlignment",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. No l-marker anchor regions 📎 Requirement gap ≡ Correctness

The template schema and sample configuration add CropOnLMarkers but provide no way to define
anchor-point regions/ROIs for L-marker detection. This prevents templates from explicitly expressing
where L-markers should be found and increases the risk of unreliable detection across varied scans.
Agent Prompt
## Issue description
`CropOnLMarkers` is introduced, but the template schema does not support defining anchor-point regions (ROIs) for L-shaped corner markers, and the processor options currently expose only global detection knobs (e.g., `min_contour_area`). This violates the requirement to support anchor point definitions in the template without breaking existing templates.

## Issue Context
The schema currently enumerates `CropOnLMarkers` but does not define an `if name == CropOnLMarkers` options schema, nor any structure for anchor/ROI definitions. The sample template also lacks anchor definitions, and the processor does not read them.

## Fix Focus Areas
- src/schemas/template_schema.py[64-183]
- src/processors/CropOnLMarkers.py[13-35]
- samples/sample_lmarkers/template.json[179-198]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +44 to +58
# Step 1: Blur + threshold to get clean black/white image
gray = cv2.GaussianBlur(image, (5, 5), 0)
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# Step 2: Morphological close - connects broken edges of L-shapes
kernel = np.ones((3, 3), np.uint8)
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=2)

if config.outputs.show_image_level >= 2:
InteractionUtils.show("CropOnLMarkers: binary", binary, config=config)

# Step 3: Find all contours
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. No canny edge detection 📎 Requirement gap ☼ Reliability

CropOnLMarkers.apply_filter() performs thresholding and then directly extracts contours, but never
runs cv2.Canny() edge detection as required. This can reduce detection robustness for L-markers
under varying lighting and print artifacts.
Agent Prompt
## Issue description
The L-marker detection pipeline does not use Canny edge detection before contour extraction, despite the compliance requirement calling for a Canny + contour approach to locate L-shaped markers and their corner/turning points.

## Issue Context
Current flow: blur -> Otsu threshold -> morphology close -> `findContours(binary, ...)`. There is no `cv2.Canny(...)` stage.

## Fix Focus Areas
- src/processors/CropOnLMarkers.py[44-58]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +67 to +116
if len(candidates) < 4:
logger.warning(
f"CropOnLMarkers: Found only {len(candidates)} candidates," # noqa: E231
" need 4. Check min/max contour area settings."
)
if config.outputs.show_image_level >= 1:
InteractionUtils.show(
"CropOnLMarkers: not enough markers",
image,
config=config,
)
return None

# Step 5: Find the inner corner (turning point) of each L candidate
# We get the bounding box and pick the corner closest to image center
h, w = image.shape[:2]
cx, cy = w / 2, h / 2 # image center

inner_corners = []
for cnt in candidates:
x, y, bw, bh = cv2.boundingRect(cnt)

# The 4 possible corners of the bounding box
box_corners = [
[x, y], # top-left
[x + bw, y], # top-right
[x, y + bh], # bottom-left
[x + bw, y + bh], # bottom-right
]

# Inner corner = corner of bounding box closest to image center
inner = min(
box_corners,
key=lambda p: (p[0] - cx) ** 2 + (p[1] - cy) ** 2,
)
inner_corners.append(inner)

# Step 6: Pick one marker per quadrant (TL, TR, BL, BR)
quadrant_corners = self._pick_one_per_quadrant(inner_corners, cx, cy)

if quadrant_corners is None or len(quadrant_corners) != 4:
logger.warning("CropOnLMarkers: Could not find one marker per quadrant.")
return None

logger.info(f"CropOnLMarkers: Detected inner corners: {quadrant_corners}")

# Step 7: Crop and warp using the 4 inner corners
centres = np.array(quadrant_corners, dtype="float32")
image = ImageUtils.four_point_transform(image, centres)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Hard-coded 4 marker points 📎 Requirement gap ☼ Reliability

The processor requires at least 4 candidates and then selects exactly one per quadrant, enforcing a
4-point warp instead of aggregating all detected points and computing an outer bounding rectangle.
This violates the requirement to avoid a fixed 4-point assumption and can fail when detections are
more/less than four.
Agent Prompt
## Issue description
The algorithm is constrained to exactly 4 points (one per quadrant) and warps using those points, instead of aggregating all detected L-marker points and computing an outer bounding rectangle/bounding box to drive alignment/cropping.

## Issue Context
Current behavior:
- returns `None` when `len(candidates) < 4`
- computes `inner_corners` per candidate
- forces one-per-quadrant selection and requires `len(...) == 4`
- warps via `ImageUtils.four_point_transform(image, centres)`

## Fix Focus Areas
- src/processors/CropOnLMarkers.py[67-116]
- src/processors/CropOnLMarkers.py[119-152]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +55 to +58
# Step 3: Find all contours
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Findcontours unpacking crash 🐞 Bug ☼ Reliability

CropOnLMarkers unpacks cv2.findContours(...) into two values, which raises a runtime unpacking
error on OpenCV versions that return a 3-tuple (e.g., OpenCV 3). This can crash preprocessing before
any cropping is attempted.
Agent Prompt
## Issue description
`CropOnLMarkers.apply_filter()` assumes `cv2.findContours()` returns exactly `(contours, hierarchy)`, but the repo already accounts for OpenCV’s signature differences elsewhere. On OpenCV builds returning 3 values, preprocessing will fail immediately.

## Issue Context
The codebase includes `ImageUtils.grab_contours()` specifically to normalize the OpenCV v2/v3/v4 `findContours` return shape, and other processors (e.g., `CropPage`) use it.

## Fix Focus Areas
- src/processors/CropOnLMarkers.py[55-58]

## Implementation notes
- Replace the 2-value unpacking with:
  - `contours = ImageUtils.grab_contours(cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE))`
- Keep the rest of the loop unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +85 to +103
inner_corners = []
for cnt in candidates:
x, y, bw, bh = cv2.boundingRect(cnt)

# The 4 possible corners of the bounding box
box_corners = [
[x, y], # top-left
[x + bw, y], # top-right
[x, y + bh], # bottom-left
[x + bw, y + bh], # bottom-right
]

# Inner corner = corner of bounding box closest to image center
inner = min(
box_corners,
key=lambda p: (p[0] - cx) ** 2 + (p[1] - cy) ** 2,
)
inner_corners.append(inner)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Warp anchored to bbox 🐞 Bug ≡ Correctness

CropOnLMarkers derives the 'inner corner' from cv2.boundingRect corners, but bounding-box corners
(notably [x + bw, y + bh]) are not guaranteed to be on the L-marker contour, so the warp can be
anchored to points in whitespace and mis-crop/mis-align the sheet. This is especially risky because
those derived points are used directly as inputs to four_point_transform.
Agent Prompt
## Issue description
The processor currently picks the corner closest to the image center from the *bounding rectangle’s* four corners. For an L-shaped contour, a bounding-rect corner can be outside the drawn marker (inside the empty area of the bounding box), which makes the perspective transform anchor inaccurate.

## Issue Context
The output of this step is fed into `ImageUtils.four_point_transform()`, so corner inaccuracies directly affect cropping/alignment.

## Fix Focus Areas
- src/processors/CropOnLMarkers.py[85-116]

## Implementation notes
Implement corner extraction using contour geometry rather than bbox corners. Options (any one is acceptable if robust):
- Use the actual contour points: `pts = cnt.reshape(-1,2)` and choose an anchor that is guaranteed to be on the contour (e.g., the contour point minimizing distance to `(cx, cy)` as the 'inner' point), then optionally refine.
- Or approximate polygon (`cv2.approxPolyDP`) / fit two dominant perpendicular lines and compute their intersection.
- Ensure the chosen anchor point lies on/near the contour (or within a small distance threshold) before accepting it as a marker corner.
- Keep quadrant selection, but operate on these contour-derived anchor points.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@Udayraj123

Copy link
Copy Markdown
Owner

@Praddy2006 I'll be able to take a look at this PR in 2-3 weeks. Until then can you address the AI comments? Update some tests and add a working screenshot/recording?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Support for L shaped corner markers

2 participants