copy_move_regions — IMGFORENSICS copy_move op

• Data kinds: image2d → table

• Call: import fullseye as fs; fs.ledger.copy_move_regions(image, method: 'str' = 'keypoint', min_matches: 'int' = 4, min_offset: 'float' = 16.0, offset_tol: 'float' = 2.0, ratio: 'float' = 0.6, patch: 'int' = 11, block: 'int' = 8, step: 'int' = 1, n_dct: 'int' = 10, min_variance: 'float' = 0.0001, max_feature_dist: 'float' = 0.02, neighbours: 'int' = 2, ransac_thresh: 'float' = 3.0, ransac_iters: 'int' = 300, seed: 'int' = 0) -> 'list' (to call the implementation directly, import imgforensics; imgforensics.copy_move_regions(image, method: 'str' = 'keypoint', min_matches: 'int' = 4, min_offset: 'float' = 16.0, offset_tol: 'float' = 2.0, ratio: 'float' = 0.6, patch: 'int' = 11, block: 'int' = 8, step: 'int' = 1, n_dct: 'int' = 10, min_variance: 'float' = 0.0001, max_feature_dist: 'float' = 0.02, neighbours: 'int' = 2, ransac_thresh: 'float' = 3.0, ransac_iters: 'int' = 300, seed: 'int' = 0) -> 'list'; from the registry, opsimgforensics.get("copy_move_regions"))

Usage

Pairs of copy-move (self-duplicated) regions within one image. table.

method="keypoint" (default)

takes corners with features.harris_corners,

builds normalized patch descriptors with features.describe_patches,

and forms matches using the nearest neighbour at least min_offset px away from itself and Lowe's ratio test

(_self_match). Matches are bundled by shift vector, and for each group

geometric consistency is checked with mosaic.proj_match_points_ransac.

A similarity transform is fitted with fit_transform.vector_to_similarity (Umeyama) and

stored in similarity.

method="block"

Fridrich, Soukal & Lukáš 2003. Overlapping block-square blocks are taken at step px

intervals (default step=1 —— this is not decoration; see "stride" below),

the low-frequency n_dct DCT coefficients of each block are used as features and sorted lexicographically,

up to neighbours entries close in lexicographic order become candidates, and only those with a feature distance

of at most max_feature_dist are counted by shift vector.

It does not cope with rotation or scaling, but picks up more than the keypoint method on images with few corners.

Blocks with variance below min_variance are discarded (copying uniform sky onto sky gives the same features = the main cause of the false positives the detector inevitably produces).

Why the stride (step) is 1 (decided by measurement): the block method can find "the same features" only when the source and the destination of the copy lie on the same grid. With step=4, a copy whose shift is not a multiple of 4 (e.g. (110, 128)) never matches, in principle. In measurements step=4 returned not even 1 true shift, and returned 60 false groups instead. With step=1 the true shift comes as group 1. You may raise step when large images are heavy, but raise it knowing that only shifts that are multiples of the raised stride can then be found.

The return is a list of region pairs (in descending order of match count). Each element:

offset shift (dy, dx) (row, col). Its sign is normalized by position

(the orientation that is positive in lexicographic order) —— deciding it by index would make the same copy

both (110, 128) and (-110, -128) (measured and fixed)

n_matches number of matches in that group

n_inliers number of RANSAC inliers (same as n_matches for method="block")

inlier_ratio inlier ratio src_bbox / dst_bbox (r0, c0, r1, c1) src_points / dst_points (row, col) of shape (N, 2)

similarity 3x3 fitted by Umeyama (method="keypoint" only; otherwise None)

method the method used

caveats what this result cannot tell

Because the answer is at hand, the fact that it hits is pinned in numbers (tests/test_imgforensics.py::test_copy_move_finds_the_known_offset, a 64x64 patch at (40, 32) of a 256x256 texture image copied to (150, 160) = true shift (110, 128)):

============ ================= ============ ========= ==============

method Group 1 offset n_matches Groups Error

============ ================= ============ ========= ==============

keypoint (110.0, 128.0) 15 1 0 px

block (110.0, 128.0) 3249 1 0 px

============ ================= ============ ========= ==============

False positives are measured too: on 3 untampered images of the same kind, both the keypoint and the block method give 0 groups (seeds 4/5/6).

What it cannot tell (all measured in the same test):

• Uniform regions (sky, walls) have the same features even without copying. On an (untampered) image whose upper half is a uniform "sky", min_variance=1e-4 and 1e-6 both give 0 groups, but with min_variance=0 1 false group of 264 pairs appears (shift (1, -247) = arbitrary matches within the sky).

• method="keypoint" uses normalized patch descriptors, so it does not cope with rotation. Rotating the copy before pasting gives group counts 0 deg: 1 → 5 deg: 0 → 15 deg: 0 → 30 deg: 0. In practice similarity never comes back with a rotation in it.

• method="block" also does not cope with rotation at all (0 at 5 deg).

• Zero detections does not mean there is no copy. For a copy with recompression in between, the feature distance grows and exceeds max_feature_dist. Measured (the same copied image passed through JPEG before detection):

========== ============================ ==========================

Quality keypoint group 1 n_matches block group 1 n_matches

========== ============================ ==========================

Uncompr. 15 3249

95 10 138

85 7 10

75 7 0 (no group)

========== ============================ ==========================

The shift stays correct at (110.0, 128.0) at every quality. What breaks is "whether it is found", not the answer when it is found.

References (sample data, literature)

• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).

• Operator provenance and references — the sources of the research/methods this op family came from.

• The canonical algorithm (author, year) and its uses are named in the family usage guide above.

Runnable examples (verified samples that actually call this op)

• image_forensics_audit — py -3.11 examples/image_forensics_audit.py

• poc_forensics_roc — py -3.11 examples/poc_forensics_roc.py

Ops the type connects to (they accept table as input)

evidence_quantile

Same category (copy_move)

—


*Provenance: imgforensics.py — IMGFORENSICS operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.