Skip to content

Commit f362dae

Browse files
committed
Add MetFaces-U support
1 parent c58f03b commit f362dae

File tree

2 files changed

+54
-17
lines changed

2 files changed

+54
-17
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,16 @@ All data is hosted on Google Drive:
3737
MetFaces 1024x1024 images can be reproduced with the `metfaces.py` script as follows:
3838

3939
1. Download the contents of the metfaces-dataset Google Drive folder. Retain the original folder structure (e.g., you should have `local/path/metfaces.json`, `local/path/unprocessed`.)
40-
2. Run `metfaces.py --json data/metfaces.json --source-images data --output-dir out`
40+
2. Run `metfaces.py --json data/metfaces-dataset.json --source-images data --output-dir out`
41+
42+
To reproduce the MetFaces-U dataset ("unaligned MetFaces"), use the following command:
43+
44+
```
45+
python metfaces.py --json data/metfaces-dataset.json --source-images data \
46+
--random-shift=0.2 --retry-crops --no-rotation \
47+
--output-dir out-unaligned
48+
```
49+
4150

4251
## Metadata
4352

metfaces.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
python %(prog)s --output=tmp
2727
'''
2828

29-
def extract_face(face, source_images, output_dir, target_size=1024, supersampling=4, enable_padding=True):
29+
def extract_face(face, source_images, output_dir, rng, target_size=1024, supersampling=4, enable_padding=True, random_shift=0.0, retry_crops=False, rotate_level=True):
3030
def rot90(v) -> np.ndarray:
3131
return np.array([-v[1], v[0]])
3232

@@ -49,23 +49,45 @@ def rot90(v) -> np.ndarray:
4949
eye_to_mouth = mouth_avg - eye_avg
5050

5151
# Choose oriented crop rectangle.
52-
x = eye_to_eye - rot90(eye_to_mouth)
53-
x /= np.hypot(*x)
54-
x *= max(np.hypot(*eye_to_eye) * 2.0, np.hypot(*eye_to_mouth) * 1.8)
55-
y = rot90(x)
56-
c = eye_avg + eye_to_mouth * 0.1
57-
58-
# Calculate auxiliary data.
59-
qsize = np.hypot(*x) * 2
60-
quad = np.stack([c - x - y, c - x + y, c + x + y, c + x - y])
61-
lo = np.min(quad, axis=0)
62-
hi = np.max(quad, axis=0)
63-
lm_rel = np.dot(landmarks - c, np.transpose([x, y])) / qsize**2 * 2 + 0.5
64-
rp = np.dot(np.random.RandomState(123).uniform(-1, 1, size=(1024, 2)), [x, y]) + c
52+
if rotate_level:
53+
# Orient according to tilt of the input image
54+
x = eye_to_eye - rot90(eye_to_mouth)
55+
x /= np.hypot(*x)
56+
x *= max(np.hypot(*eye_to_eye) * 2.0, np.hypot(*eye_to_mouth) * 1.8)
57+
y = rot90(x)
58+
c0 = eye_avg + eye_to_mouth * 0.1
59+
else:
60+
# Do not match the tilt in the source data, i.e., use an axis-aligned rectangle
61+
x = np.array([1, 0], dtype=np.float64)
62+
x *= max(np.hypot(*eye_to_eye) * 2.0, np.hypot(*eye_to_mouth) * 1.8)
63+
y = np.flipud(x) * [-1, 1]
64+
c0 = eye_avg + eye_to_mouth * 0.1
6565

6666
# Load.
6767
img = PIL.Image.open(os.path.join(source_images, face['source_path'])).convert('RGB')
6868

69+
# Calculate auxiliary data.
70+
qsize = np.hypot(*x) * 2
71+
quad = np.stack([c0 - x - y, c0 - x + y, c0 + x + y, c0 + x - y])
72+
73+
# Keep drawing new random crop offsets until we find one that is contained in the image
74+
# and does not require padding
75+
if random_shift != 0:
76+
for _ in range(1000):
77+
# Offset the crop rectange center by a random shift proportional to image dimension
78+
# and the requested standard deviation (by default 0)
79+
c = (c0 + np.hypot(*x)*2 * random_shift * rng.normal(0, 1, c0.shape))
80+
quad = np.stack([c - x - y, c - x + y, c + x + y, c + x - y])
81+
crop = (int(np.floor(min(quad[:,0]))), int(np.floor(min(quad[:,1]))), int(np.ceil(max(quad[:,0]))), int(np.ceil(max(quad[:,1]))))
82+
if not retry_crops or not (crop[0] < 0 or crop[1] < 0 or crop[2] >= img.width or crop[3] >= img.height):
83+
# We're happy with this crop (either it fits within the image, or retries are disabled)
84+
break
85+
else:
86+
# rejected N times, give up and move to next image
87+
# (does not happen in practice with the MetFaces data)
88+
print('rejected image %s' % face['source_path'])
89+
return
90+
6991
# Shrink.
7092
shrink = int(np.floor(qsize / target_size * 0.5))
7193
if shrink > 1:
@@ -116,14 +138,20 @@ def main():
116138
parser.add_argument('--json', help='MetFaces metadata json file path', required=True)
117139
parser.add_argument('--source-images', help='Location of MetFaces raw image data', required=True)
118140
parser.add_argument('--output-dir', help='Where to save output files')
141+
parser.add_argument('--random-shift', help='Standard deviation of random crop rectangle jitter', type=float, default=0.0, metavar='SHIFT')
142+
parser.add_argument('--retry-crops', help='Retry random shift if crop rectangle falls outside image (up to 1000 times)', dest='retry_crops', default=False, action='store_true')
143+
parser.add_argument('--no-rotation', help='Keep the original orientation of images', dest='no_rotation', default=False, action='store_true')
119144
args = parser.parse_args()
120145

121146
os.makedirs(args.output_dir, exist_ok=True)
122147

123-
with open(args.json) as fin:
148+
rng = np.random.RandomState(12345) # fix the random seed for reproducibility
149+
150+
with open(args.json, encoding="utf8") as fin:
124151
faces = json.load(fin)
125152
for f in tqdm(faces):
126-
extract_face(f, source_images=args.source_images, output_dir=args.output_dir)
153+
extract_face(f, source_images=args.source_images, output_dir=args.output_dir, rng=rng,
154+
random_shift=args.random_shift, retry_crops=args.retry_crops, rotate_level=not args.no_rotation)
127155

128156
if __name__ == "__main__":
129157
main()

0 commit comments

Comments
 (0)