fix perspective correction

This commit is contained in:
2026-04-16 22:41:56 -07:00
parent a4c8d2b01c
commit d35cdd6971
8 changed files with 365 additions and 42 deletions

View File

@@ -6,25 +6,34 @@ import numpy as np
from scipy.ndimage import map_coordinates from scipy.ndimage import map_coordinates
from backend.node_registry import register_node from backend.node_registry import register_node
from backend.data_types import DataField from backend.data_types import DataField, datafield_to_uint8, encode_preview
from backend.execution_context import emit_overlay
@register_node(display_name="Perspective Correction") @register_node(display_name="Perspective Correction")
class PerspectiveCorrection: class PerspectiveCorrection:
_CUSTOM_PREVIEW = True
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"field": ("DATA_FIELD",), "field": ("DATA_FIELD",),
"top_left_x": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}), "top_left_x": ("FLOAT", {"default": 0.1, "min": -1.0, "max": 1.0, "step": 0.01, "hidden": True}),
"top_left_y": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}), "top_left_y": ("FLOAT", {"default": 0.1, "min": -1.0, "max": 1.0, "step": 0.01, "hidden": True}),
"top_right_x": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}), "top_right_x": ("FLOAT", {"default": -0.1, "min": -1.0, "max": 1.0, "step": 0.01, "hidden": True}),
"top_right_y": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}), "top_right_y": ("FLOAT", {"default": 0.1, "min": -1.0, "max": 1.0, "step": 0.01, "hidden": True}),
"bottom_left_x": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}), "bottom_left_x": ("FLOAT", {"default": 0.1, "min": -1.0, "max": 1.0, "step": 0.01, "hidden": True}),
"bottom_left_y": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}), "bottom_left_y": ("FLOAT", {"default": -0.1, "min": -1.0, "max": 1.0, "step": 0.01, "hidden": True}),
"bottom_right_x": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}), "bottom_right_x": ("FLOAT", {"default": -0.1, "min": -1.0, "max": 1.0, "step": 0.01, "hidden": True}),
"bottom_right_y": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}), "bottom_right_y": ("FLOAT", {"default": -0.1, "min": -1.0, "max": 1.0, "step": 0.01, "hidden": True}),
} },
"optional": {
"top_left": ("COORD",),
"top_right": ("COORD",),
"bottom_left": ("COORD",),
"bottom_right": ("COORD",),
},
} }
OUTPUTS = ( OUTPUTS = (
@@ -33,9 +42,8 @@ class PerspectiveCorrection:
FUNCTION = "process" FUNCTION = "process"
DESCRIPTION = ( DESCRIPTION = (
"Fix perspective distortion by specifying corner offsets. Each corner " "Fix perspective distortion by dragging corner handles. Each corner "
"can be shifted by a fractional amount (relative to image size) to " "offset defines a distorted quadrilateral that is warped back to "
"define the distorted quadrilateral. The image is then warped back to "
"a rectangle." "a rectangle."
) )
@@ -45,11 +53,23 @@ class PerspectiveCorrection:
top_left_x: float, top_left_y: float, top_left_x: float, top_left_y: float,
top_right_x: float, top_right_y: float, top_right_x: float, top_right_y: float,
bottom_left_x: float, bottom_left_y: float, bottom_left_x: float, bottom_left_y: float,
bottom_right_x: float, bottom_right_y: float) -> tuple: bottom_right_x: float, bottom_right_y: float,
top_left: tuple[float, float] | None = None,
top_right: tuple[float, float] | None = None,
bottom_left: tuple[float, float] | None = None,
bottom_right: tuple[float, float] | None = None) -> tuple:
if top_left is not None:
top_left_x, top_left_y = float(top_left[0]), float(top_left[1])
if top_right is not None:
top_right_x, top_right_y = float(top_right[0]), float(top_right[1])
if bottom_left is not None:
bottom_left_x, bottom_left_y = float(bottom_left[0]), float(bottom_left[1])
if bottom_right is not None:
bottom_right_x, bottom_right_y = float(bottom_right[0]), float(bottom_right[1])
data = np.asarray(field.data, dtype=np.float64) data = np.asarray(field.data, dtype=np.float64)
yres, xres = data.shape yres, xres = data.shape
# Source corners (distorted) as fractional offsets from ideal corners
src = np.array([ src = np.array([
[top_left_y * yres, top_left_x * xres], [top_left_y * yres, top_left_x * xres],
[top_right_y * yres, top_right_x * xres + (xres - 1)], [top_right_y * yres, top_right_x * xres + (xres - 1)],
@@ -57,7 +77,6 @@ class PerspectiveCorrection:
[(1 + bottom_right_y) * yres - 1, bottom_right_x * xres + (xres - 1)], [(1 + bottom_right_y) * yres - 1, bottom_right_x * xres + (xres - 1)],
], dtype=np.float64) ], dtype=np.float64)
# Destination corners (ideal rectangle)
dst = np.array([ dst = np.array([
[0, 0], [0, 0],
[0, xres - 1], [0, xres - 1],
@@ -65,33 +84,54 @@ class PerspectiveCorrection:
[yres - 1, xres - 1], [yres - 1, xres - 1],
], dtype=np.float64) ], dtype=np.float64)
# Solve for perspective transform matrix (3x3)
H = _solve_perspective(src, dst) H = _solve_perspective(src, dst)
# Apply inverse warp
yy, xx = np.mgrid[:yres, :xres] yy, xx = np.mgrid[:yres, :xres]
coords = np.stack([yy.ravel(), xx.ravel(), np.ones(yres * xres)]) coords = np.stack([xx.ravel(), yy.ravel(), np.ones(yres * xres)])
src_coords = H @ coords src_coords = H @ coords
src_coords /= src_coords[2:3, :] src_coords /= src_coords[2:3, :]
sy = src_coords[0].reshape(yres, xres) sx = src_coords[0].reshape(yres, xres)
sx = src_coords[1].reshape(yres, xres) sy = src_coords[1].reshape(yres, xres)
result = map_coordinates(data, [sy, sx], order=1, mode='nearest') result = map_coordinates(data, [sy, sx], order=1, mode='nearest')
return (field.replace(data=result),) corrected = field.replace(data=result)
source_rgb = datafield_to_uint8(field, field.colormap)
corrected_rgb = datafield_to_uint8(corrected, corrected.colormap)
corners = [
{"x": float(top_left_x), "y": float(top_left_y)},
{"x": float(top_right_x), "y": float(top_right_y)},
{"x": float(bottom_left_x), "y": float(bottom_left_y)},
{"x": float(bottom_right_x), "y": float(bottom_right_y)},
]
emit_overlay({
"kind": "perspective",
"section_title": "Perspective",
"image": encode_preview(source_rgb),
"corrected_image": encode_preview(corrected_rgb),
"corners": corners,
})
return (corrected,)
def _solve_perspective(src: np.ndarray, dst: np.ndarray) -> np.ndarray: def _solve_perspective(src: np.ndarray, dst: np.ndarray) -> np.ndarray:
"""Solve for 3x3 perspective matrix mapping dst -> src (for inverse warp).""" """Solve for 3x3 perspective matrix mapping dst -> src (for inverse warp).
Coordinates are (col, row) — the matrix is applied to [col, row, 1] vectors.
"""
n = len(src) n = len(src)
A = np.zeros((2 * n, 8)) A = np.zeros((2 * n, 8))
b = np.zeros(2 * n) b = np.zeros(2 * n)
for i in range(n): for i in range(n):
dy, dx = dst[i] dr, dc = dst[i] # dest row, col
sy, sx = src[i] sr, sc = src[i] # src row, col
A[2 * i] = [dx, dy, 1, 0, 0, 0, -sx * dx, -sx * dy] A[2 * i] = [dc, dr, 1, 0, 0, 0, -sc * dc, -sc * dr]
A[2 * i + 1] = [0, 0, 0, dx, dy, 1, -sy * dx, -sy * dy] A[2 * i + 1] = [0, 0, 0, dc, dr, 1, -sr * dc, -sr * dr]
b[2 * i] = sx b[2 * i] = sc
b[2 * i + 1] = sy b[2 * i + 1] = sr
h, _, _, _ = np.linalg.lstsq(A, b, rcond=None) h, _, _, _ = np.linalg.lstsq(A, b, rcond=None)
H = np.array([[h[0], h[1], h[2]], H = np.array([[h[0], h[1], h[2]],
[h[3], h[4], h[5]], [h[3], h[4], h[5]],

View File

@@ -1,12 +1,16 @@
# Perspective Correction # Perspective Correction
Fix perspective distortion in a DATA_FIELD via a projective (homography) transform. Each corner can be shifted by a fractional offset to map a distorted quadrilateral back to a rectangle. Equivalent to Gwyddion's `correct_perspective.c` module. Fix perspective distortion in a DATA_FIELD via a projective (homography) transform. Each corner can be shifted by a fractional offset to map a distorted quadrilateral back to a rectangle.
## Inputs ## Inputs
| Name | Type | Required | Description | | Name | Type | Required | Description |
|------|------|----------|-------------| |------|------|----------|-------------|
| field | DATA_FIELD | Yes | Input field with perspective distortion | | field | DATA_FIELD | Yes | Input field with perspective distortion |
| top_left | COORD | No | Override top-left corner offset (x, y) |
| top_right | COORD | No | Override top-right corner offset (x, y) |
| bottom_left | COORD | No | Override bottom-left corner offset (x, y) |
| bottom_right | COORD | No | Override bottom-right corner offset (x, y) |
## Outputs ## Outputs
@@ -14,22 +18,15 @@ Fix perspective distortion in a DATA_FIELD via a projective (homography) transfo
|------|------|-------------| |------|------|-------------|
| corrected | DATA_FIELD | Perspective-corrected field | | corrected | DATA_FIELD | Perspective-corrected field |
## Controls ## Interactive preview
| Name | Type | Default | Description | The preview shows the source image with a draggable quadrilateral overlay. Drag any corner handle to adjust the perspective correction. Use the Source/Corrected tabs to switch between the input image (with handles) and the corrected result.
|------|------|---------|-------------|
| top_left_x | FLOAT | 0.0 | Horizontal offset of the top-left corner as a fraction of image width (-0.5-0.5) | Corner positions can also be set by connecting Coordinate nodes to the optional COORD inputs, which override the handle-driven values.
| top_left_y | FLOAT | 0.0 | Vertical offset of the top-left corner as a fraction of image height (-0.5-0.5) |
| top_right_x | FLOAT | 0.0 | Horizontal offset of the top-right corner as a fraction of image width (-0.5-0.5) |
| top_right_y | FLOAT | 0.0 | Vertical offset of the top-right corner as a fraction of image height (-0.5-0.5) |
| bottom_left_x | FLOAT | 0.0 | Horizontal offset of the bottom-left corner as a fraction of image width (-0.5-0.5) |
| bottom_left_y | FLOAT | 0.0 | Vertical offset of the bottom-left corner as a fraction of image height (-0.5-0.5) |
| bottom_right_x | FLOAT | 0.0 | Horizontal offset of the bottom-right corner as a fraction of image width (-0.5-0.5) |
| bottom_right_y | FLOAT | 0.0 | Vertical offset of the bottom-right corner as a fraction of image height (-0.5-0.5) |
## Notes ## Notes
- All offsets are given as fractions of the image dimensions (0.0 = no shift, 0.1 = 10% shift). Positive x shifts right, positive y shifts down. - All offsets are given as fractions of the image dimensions (0.0 = no shift, 0.1 = 10% shift). Positive x shifts right, positive y shifts down.
- The transform uses bilinear interpolation to resample pixel values at non-integer locations. - The transform uses bilinear interpolation to resample pixel values at non-integer locations.
- For trapezoidal distortions (common in tilted AFM scans), typically only two corners need adjustment. - For trapezoidal distortions (common in tilted AFM scans), typically only two corners need adjustment.
- Set all offsets to 0.0 to pass the field through unchanged. - When all offsets are zero (default), the field passes through unchanged.

View File

@@ -15,6 +15,7 @@ const ThresholdHistogram = lazy(() => import('./ThresholdHistogram'));
const RadialProfileOverlay = lazy(() => import('./RadialProfileOverlay')); const RadialProfileOverlay = lazy(() => import('./RadialProfileOverlay'));
const StraightenPathOverlay = lazy(() => import('./StraightenPathOverlay')); const StraightenPathOverlay = lazy(() => import('./StraightenPathOverlay'));
const MultiProfileOverlay = lazy(() => import('./MultiProfileOverlay')); const MultiProfileOverlay = lazy(() => import('./MultiProfileOverlay'));
const PerspectiveOverlay = lazy(() => import('./PerspectiveOverlay'));
import TextNoteNode from './TextNoteNode'; import TextNoteNode from './TextNoteNode';
@@ -1202,6 +1203,7 @@ function CustomNode({ id, data }: { id: string; data: NodeData }) {
|| data.overlay.kind === 'radial_profile' || data.overlay.kind === 'radial_profile'
|| data.overlay.kind === 'straighten_path' || data.overlay.kind === 'straighten_path'
|| data.overlay.kind === 'multi_profile' || data.overlay.kind === 'multi_profile'
|| data.overlay.kind === 'perspective'
); );
const hidePreviewForInteractiveMask = data.overlay?.kind === 'mask_paint' || data.overlay?.kind === 'markup'; const hidePreviewForInteractiveMask = data.overlay?.kind === 'mask_paint' || data.overlay?.kind === 'markup';
const overlayTitle = data.overlay?.section_title const overlayTitle = data.overlay?.section_title
@@ -1223,6 +1225,8 @@ function CustomNode({ id, data }: { id: string; data: NodeData }) {
? 'Path' ? 'Path'
: data.overlay?.kind === 'multi_profile' : data.overlay?.kind === 'multi_profile'
? 'Preview' ? 'Preview'
: data.overlay?.kind === 'perspective'
? 'Perspective'
: 'Cross Section'); : 'Cross Section');
const headerMeta = (() => { const headerMeta = (() => {
if (data.className === 'Folder') { if (data.className === 'Folder') {
@@ -1597,6 +1601,14 @@ function CustomNode({ id, data }: { id: string; data: NodeData }) {
nodeId={id} nodeId={id}
onWidgetChange={ctx!.onWidgetChange} onWidgetChange={ctx!.onWidgetChange}
/> />
) : data.overlay!.kind === 'perspective' ? (
<PerspectiveOverlay
image={data.overlay!.image ?? ''}
correctedImage={data.overlay!.corrected_image ?? ''}
corners={(data.overlay!.corners ?? []) as Array<{ x: number; y: number }>}
nodeId={id}
onWidgetChange={ctx!.onWidgetChange}
/>
) : data.overlay!.kind === 'angle_measure' ? ( ) : data.overlay!.kind === 'angle_measure' ? (
<AngleMeasureOverlay <AngleMeasureOverlay
image={data.overlay!.image ?? ''} image={data.overlay!.image ?? ''}

View File

@@ -0,0 +1,150 @@
import React, { useRef, useState, useCallback, useEffect } from 'react';
import { pointerToFraction } from './overlayUtils';
export const CAPTURE_SELECTOR = '.perspective-overlay';
const CORNER_NAMES = ['top_left', 'top_right', 'bottom_left', 'bottom_right'] as const;
type CornerName = typeof CORNER_NAMES[number];
const CORNER_ANCHORS: Record<CornerName, { ax: number; ay: number }> = {
top_left: { ax: 0, ay: 0 },
top_right: { ax: 1, ay: 0 },
bottom_left: { ax: 0, ay: 1 },
bottom_right: { ax: 1, ay: 1 },
};
interface Corner { x: number; y: number }
interface Props {
image: string;
correctedImage: string;
corners: Corner[];
nodeId: string;
onWidgetChange: (nodeId: string, name: string, value: unknown) => void;
}
function cornerToPercent(corner: Corner, name: CornerName) {
const anchor = CORNER_ANCHORS[name];
return {
left: (anchor.ax + corner.x) * 100,
top: (anchor.ay + corner.y) * 100,
};
}
function cornersKey(c: Corner[]): string {
return c.map((p) => `${p.x},${p.y}`).join(';');
}
export default function PerspectiveOverlay({
image, correctedImage, corners, nodeId, onWidgetChange,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const draggingRef = useRef<CornerName | null>(null);
const [draft, setDraft] = useState<Corner[] | null>(null);
const pendingCommitRef = useRef<string | null>(null);
const [showCorrected, setShowCorrected] = useState(false);
useEffect(() => {
if (pendingCommitRef.current && cornersKey(corners) === pendingCommitRef.current) {
pendingCommitRef.current = null;
setDraft(null);
}
}, [corners]);
const liveCorners = draft ?? corners;
const onPointerDown = useCallback((corner: CornerName) => (e: React.PointerEvent<Element>) => {
e.stopPropagation();
e.preventDefault();
(e.target as HTMLElement).setPointerCapture(e.pointerId);
draggingRef.current = corner;
setDraft([...liveCorners]);
}, [liveCorners]);
const onPointerMove = useCallback((e: React.PointerEvent<Element>) => {
const name = draggingRef.current;
if (!name || !containerRef.current) return;
const { fx, fy } = pointerToFraction(e, containerRef.current);
const anchor = CORNER_ANCHORS[name];
const cx = Math.max(-1, Math.min(1, parseFloat((fx - anchor.ax).toFixed(3))));
const cy = Math.max(-1, Math.min(1, parseFloat((fy - anchor.ay).toFixed(3))));
const idx = CORNER_NAMES.indexOf(name);
setDraft((prev) => {
if (!prev) return prev;
const next = [...prev];
next[idx] = { x: cx, y: cy };
return next;
});
}, []);
const onPointerUp = useCallback(() => {
const name = draggingRef.current;
if (!name || !draft) {
draggingRef.current = null;
return;
}
draggingRef.current = null;
pendingCommitRef.current = cornersKey(draft);
for (let i = 0; i < CORNER_NAMES.length; i++) {
onWidgetChange(nodeId, `${CORNER_NAMES[i]}_x`, draft[i].x);
onWidgetChange(nodeId, `${CORNER_NAMES[i]}_y`, draft[i].y);
}
}, [draft, nodeId, onWidgetChange]);
const positions = CORNER_NAMES.map((name, i) => cornerToPercent(liveCorners[i] || { x: 0, y: 0 }, name));
const quadPoints = `${positions[0].left},${positions[0].top} ${positions[1].left},${positions[1].top} ${positions[3].left},${positions[3].top} ${positions[2].left},${positions[2].top}`;
return (
<div className="perspective-overlay-wrap">
<div className="perspective-tab-bar">
<button
className={`perspective-tab nodrag${!showCorrected ? ' active' : ''}`}
onClick={() => setShowCorrected(false)}
>
Source
</button>
<button
className={`perspective-tab nodrag${showCorrected ? ' active' : ''}`}
onClick={() => setShowCorrected(true)}
>
Corrected
</button>
</div>
{showCorrected ? (
<div className="perspective-overlay perspective-corrected">
<img src={correctedImage} alt="corrected" draggable={false} />
</div>
) : (
<div
ref={containerRef}
className="nodrag nowheel perspective-overlay"
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onLostPointerCapture={onPointerUp}
>
<img src={image} alt="source" draggable={false} />
<svg className="perspective-quad" viewBox="0 0 100 100" preserveAspectRatio="none">
<polygon
points={quadPoints}
fill="none"
stroke="var(--selection, #3b82f6)"
strokeWidth="0.4"
strokeLinejoin="round"
/>
</svg>
{CORNER_NAMES.map((name, i) => (
<div
key={name}
className={`perspective-handle${draggingRef.current === name ? ' dragging' : ''}`}
style={{ left: `${positions[i].left}%`, top: `${positions[i].top}%` }}
onPointerDown={onPointerDown(name)}
/>
))}
</div>
)}
</div>
);
}

View File

@@ -1801,6 +1801,73 @@ html, body, #root {
pointer-events: none; pointer-events: none;
} }
/* ── Perspective correction overlay ──────────────────────────────────── */
.perspective-overlay-wrap {
display: flex;
flex-direction: column;
}
.perspective-tab-bar {
display: flex;
gap: 1px;
background: var(--border-default);
border-bottom: 1px solid var(--border-default);
}
.perspective-tab {
flex: 1;
padding: 4px 0;
font-size: 11px;
font-weight: 500;
border: none;
background: var(--bg-deep);
color: var(--text-secondary);
cursor: pointer;
transition: background 0.1s, color 0.1s;
}
.perspective-tab:hover {
background: var(--bg-panel);
}
.perspective-tab.active {
background: var(--bg-panel);
color: var(--text-primary);
}
.perspective-overlay {
position: relative;
user-select: none;
touch-action: none;
overflow: hidden;
}
.perspective-overlay img {
width: 100%;
display: block;
}
.perspective-quad {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.perspective-handle {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--selection, #3b82f6);
border: 2px solid #fff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
transform: translate(-50%, -50%);
cursor: grab;
z-index: 2;
}
.perspective-handle:hover,
.perspective-handle.dragging {
cursor: grabbing;
transform: translate(-50%, -50%) scale(1.2);
}
.is-panning .perspective-overlay {
pointer-events: none;
}
.angle-overlay { .angle-overlay {
--angle-line-color: #ff9800; --angle-line-color: #ff9800;
--angle-arc-color: rgb(255, 166, 77); --angle-arc-color: rgb(255, 166, 77);

View File

@@ -82,6 +82,8 @@ export interface OverlayData {
row?: number; row?: number;
direction?: 'horizontal' | 'vertical'; direction?: 'horizontal' | 'vertical';
max_index?: number; max_index?: number;
corrected_image?: string;
corners?: Array<{ x: number; y: number }>;
section_title?: string; section_title?: string;
line?: number[]; line?: number[];
shape?: string; shape?: string;

View File

@@ -14,6 +14,7 @@ export const OVERLAY_CAPTURE_SELECTORS = [
'.radial-overlay', // RadialProfileOverlay '.radial-overlay', // RadialProfileOverlay
'.straighten-overlay', // StraightenPathOverlay '.straighten-overlay', // StraightenPathOverlay
'.multiprofile-overlay', // MultiProfileOverlay '.multiprofile-overlay', // MultiProfileOverlay
'.perspective-overlay', // PerspectiveOverlay
]; ];
function encodeBase64(bytes: Uint8Array) { function encodeBase64(bytes: Uint8Array) {

View File

@@ -51,3 +51,57 @@ def test_output_shape():
bottom_right_x=0.0, bottom_right_y=0.0, bottom_right_x=0.0, bottom_right_y=0.0,
) )
assert result.data.shape == (48, 96) assert result.data.shape == (48, 96)
def test_emits_perspective_overlay():
from backend.execution_context import active_node, execution_callbacks
from backend.nodes.perspective_correction import PerspectiveCorrection
node = PerspectiveCorrection()
field = make_field(shape=(64, 64))
overlays = []
with execution_callbacks(overlay=lambda nid, d: overlays.append(d)), active_node("test"):
node.process(
field,
top_left_x=0.05, top_left_y=0.05,
top_right_x=-0.05, top_right_y=0.05,
bottom_left_x=0.05, bottom_left_y=-0.05,
bottom_right_x=-0.05, bottom_right_y=-0.05,
)
assert len(overlays) == 1
ov = overlays[0]
assert ov["kind"] == "perspective"
assert ov["section_title"] == "Perspective"
assert ov["image"].startswith("data:image/png;base64,")
assert ov["corrected_image"].startswith("data:image/png;base64,")
assert len(ov["corners"]) == 4
assert ov["corners"][0] == {"x": 0.05, "y": 0.05}
assert ov["corners"][3] == {"x": -0.05, "y": -0.05}
def test_coord_input_overrides_floats():
from backend.nodes.perspective_correction import PerspectiveCorrection
node = PerspectiveCorrection()
field = make_field(shape=(64, 64))
result_floats, = node.process(
field,
top_left_x=0.1, top_left_y=0.1,
top_right_x=0.0, top_right_y=0.0,
bottom_left_x=0.0, bottom_left_y=0.0,
bottom_right_x=0.0, bottom_right_y=0.0,
)
result_coord, = node.process(
field,
top_left_x=0.0, top_left_y=0.0,
top_right_x=0.0, top_right_y=0.0,
bottom_left_x=0.0, bottom_left_y=0.0,
bottom_right_x=0.0, bottom_right_y=0.0,
top_left=(0.1, 0.1),
)
assert np.allclose(result_floats.data, result_coord.data)