tidalove commited on
Commit
6bc98b5
·
verified ·
1 Parent(s): c751482

Upload square_crop.py

Browse files
Files changed (1) hide show
  1. square_crop.py +109 -0
square_crop.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ square_crop.py
4
+
5
+ Crop each COCO bounding box to the smallest square that contains it.
6
+
7
+ Usage
8
+ -----
9
+ python square_crop.py --images_dir <IMG_DIR> \
10
+ --coco_json <COCO_JSON> \
11
+ --output_dir <OUT_DIR>
12
+
13
+ Dependencies
14
+ ------------
15
+ pip install pillow pycocotools tqdm
16
+ """
17
+ import argparse
18
+ import json
19
+ from pathlib import Path
20
+
21
+ from PIL import Image, ImageOps
22
+ from tqdm import tqdm
23
+
24
+
25
+ def parse_args():
26
+ parser = argparse.ArgumentParser(
27
+ description="Crop COCO bounding boxes to squares.")
28
+ parser.add_argument("--images_dir", required=True, type=Path,
29
+ help="Directory containing the source images.")
30
+ parser.add_argument("--coco_json", required=True, type=Path,
31
+ help="Path to the COCO annotation file (JSON).")
32
+ parser.add_argument("--output_dir", required=True, type=Path,
33
+ help="Directory to write the cropped images.")
34
+ parser.add_argument("--pad_color", default=0, type=int,
35
+ help="Gray-scale padding color 0–255 (default 0 = black).")
36
+ return parser.parse_args()
37
+
38
+
39
+ def load_coco(json_path):
40
+ with open(json_path, "r") as f:
41
+ coco = json.load(f)
42
+ id2fname = {img["id"]: img["file_name"] for img in coco["images"]}
43
+ return id2fname, coco["annotations"]
44
+
45
+
46
+ def square_from_bbox(x, y, w, h, img_w, img_h):
47
+ """
48
+ Compute (left, top, side) of the smallest square fully containing the bbox.
49
+ The square is centred on the bbox; if it overflows the image, it is shifted
50
+ (but not resized) so it lies inside the image. Returns the final crop box
51
+ (left, top, side).
52
+ """
53
+ side = max(w, h)
54
+ cx, cy = x + w / 2.0, y + h / 2.0
55
+ left = int(round(cx - side / 2.0))
56
+ top = int(round(cy - side / 2.0))
57
+
58
+ # Shift the square so it fits inside the image
59
+ left = max(0, min(left, img_w - side))
60
+ top = max(0, min(top, img_h - side))
61
+ return left, top, int(side)
62
+
63
+
64
+ def crop_annotation(img_path, ann, out_dir, pad_color=0):
65
+ with Image.open(img_path) as img:
66
+ img_w, img_h = img.size
67
+ x, y, w, h = ann["bbox"] # COCO bbox = [x, y, width, height]
68
+ left, top, side = square_from_bbox(x, y, w, h, img_w, img_h)
69
+
70
+ # Perform crop (may be smaller than 'side' at edges)
71
+ crop = img.crop((left, top, left + side, top + side))
72
+
73
+ # If we lost pixels at the edge, pad back to full square
74
+ if crop.size != (side, side):
75
+ delta_w = side - crop.size[0]
76
+ delta_h = side - crop.size[1]
77
+ padding = (0, 0, delta_w, delta_h) # (left, top, right, bottom)
78
+ crop = ImageOps.expand(crop, padding, fill=pad_color)
79
+
80
+ # Build output filename: <stem>_ann<id>.ext
81
+ stem = Path(img_path).stem
82
+ suffix = Path(img_path).suffix
83
+ out_name = f"{stem}_ann{ann['id']}{suffix}"
84
+ crop.save(out_dir / out_name)
85
+
86
+
87
+ def main():
88
+ args = parse_args()
89
+ args.output_dir.mkdir(parents=True, exist_ok=True)
90
+
91
+ id2fname, annotations = load_coco(args.coco_json)
92
+
93
+ # Group annotations by image for efficient loading
94
+ im2anns = {}
95
+ for ann in annotations:
96
+ im2anns.setdefault(ann["image_id"], []).append(ann)
97
+
98
+ for img_id, anns in tqdm(im2anns.items(), desc="Processing images"):
99
+ img_path = args.images_dir / id2fname[img_id]
100
+ if not img_path.is_file():
101
+ print(f"Warning: image {img_path} not found — skipping.")
102
+ continue
103
+ for ann in anns:
104
+ crop_annotation(img_path, ann, args.output_dir,
105
+ pad_color=args.pad_color)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()