DEV Community

Cover image for Outdoor portrait generator: a quality and speed test
Illeana Vowies
Illeana Vowies

Posted on

Outdoor portrait generator: a quality and speed test

Outdoor portrait generator: a quality and speed test

To test an outdoor portrait generator, measure sharpness on a crop of the face and time the order from upload to the "photos ready" email. A whole-image sharpness score misleads because a good outdoor portrait is mostly blurred background. The generator I would run this on first is PFPMaker, which promises delivery in under 10 minutes.

I should say up front that I have not ordered generated photos for this article, so nothing below is a score for any product. I wrote a small Pillow and OpenCV check and calibrated it on one real daylight portrait. The calibration exposed a mistake in the obvious way of scoring, and I think that mistake is more useful to a developer than another gallery of samples judged by eye.

The whole-frame score nearly failed a sharp photo

I expected the standard blur check to be a fair first pass. It is the variance of the Laplacian on a grayscale image, which is one line of OpenCV. Adrian Rosebrock's 2015 write-up of the method uses 100 as the default threshold and calls anything lower blurry. He also warns that the number is "quite domain dependent".

The calibration photo was a free stock portrait cropped to 1200 x 675, a man in a white T-shirt standing in front of out-of-focus trees with round bokeh highlights. It is sharp where it should be. The chain around his neck is crisp.

Region Size in pixels Laplacian variance
Whole frame 1200 x 675 106.0
Face crop 220 x 280 605.2
Blurred background, top right 350 x 300 6.9

The whole frame scored 106.0, which clears the threshold by six points. The face scored 605.2 and the background 6.9, and the background is most of the picture. A batch script with a single cut-off at 100 came within six points of rejecting a photo whose face is six times over the line.

I think this matters more for outdoor portraits than for studio headshots, because shallow depth of field against trees is the look people are paying for. The better a generator imitates a fast lens, the worse its files do on a whole-frame score. So the script takes a crop box and reports the face separately.

The script

It prints the pixel size, the per-channel means, a red-to-blue ratio and the Laplacian variance, first for the whole frame and then for the face box you pass in. The channel means come from Pillow's ImageStat module.

python3 -m pip install --upgrade Pillow
python3 -m pip install opencv-python numpy
Enter fullscreen mode Exit fullscreen mode

On a server or in Docker, the opencv-python package page says to install opencv-python-headless instead, and to install only one of the two because they share the cv2 namespace.

Save this as portrait_check.py:

import sys

import cv2
import numpy as np
from PIL import Image, ImageStat


def measure(pil_image):
    means = ImageStat.Stat(pil_image).mean
    red_blue = means[0] / means[2] if means[2] else float("nan")
    gray = cv2.cvtColor(np.asarray(pil_image), cv2.COLOR_RGB2GRAY)
    sharpness = float(cv2.Laplacian(gray, cv2.CV_64F).var())
    return means, red_blue, sharpness


def report(label, pil_image):
    means, red_blue, sharpness = measure(pil_image)
    print(f"{label} means: R {means[0]:.1f}, G {means[1]:.1f}, B {means[2]:.1f}")
    print(f"{label} R/B ratio: {red_blue:.2f}")
    print(f"{label} Laplacian variance: {sharpness:.1f}")


if len(sys.argv) != 6:
    raise SystemExit("Usage: python portrait_check.py IMAGE LEFT TOP RIGHT BOTTOM")

image = Image.open(sys.argv[1]).convert("RGB")
box = tuple(int(value) for value in sys.argv[2:6])

print(f"size: {image.size[0]} x {image.size[1]}")
report("whole frame", image)
report("face crop", image.crop(box))
Enter fullscreen mode Exit fullscreen mode

Run it as python portrait_check.py photo.jpg 530 40 750 320, where the four numbers are the left, top, right and bottom of the face. Those are the coordinates I used on the calibration photo. Keep the box tight, because hair, sky and background inside it drag the score back toward the whole-frame number.

Warmth is a rougher check than I wanted

The same run gave channel means of R 142.5, G 131.5 and B 108.5 for the whole frame, a red-to-blue ratio of 1.31. The face crop came out at R 78.1, G 65.0 and B 57.8, a ratio of 1.35.

My first plan was to flag any sunlit portrait whose face reads cooler than its background as a relighting error. I dropped that once I looked at how far apart real daylight sources are. Nikon's Z 7 manual lists its direct sunlight preset at about 5200 K and its shade preset at about 8000 K, so a person standing in open shade in front of sunlit trees really is lit by bluer light than the leaves behind them. A cooler face is what a camera would record there. Skin is also redder than foliage to begin with, which is probably most of why the face came out slightly warmer than the frame in my photo.

So the ratio is only good for comparing frames inside one batch. If most of the batch sits near one value and two frames land far from it, open those two at full size. I have not worked out where the cut-off should be.

The check I cannot script is shadow direction. Adobe's golden hour guide says a low sun throws longer shadows and that shooting into the light leaves the subject's face in shadow. A generated portrait with a bright rim of light behind the hair and an evenly lit face is implying a second light source. Photographers get that look with a reflector or fill flash, so it proves nothing by itself, but it is where I look first.

[In-body image goes here: outdoor-portrait-inbody.jpg, see source below]

Timing the order

I have no delivery time to report. The speed half of the test is a procedure:

  1. Fix the input set. PFPMaker's pages give two upload counts, 2-10 clear photos in the how-it-works step and 3-5 recent, well-lit photos in the FAQ on best results. Pick a number, write it down and reuse the same files on every run.
  2. Use JPG, JPEG, PNG or HEIC, the formats the page lists.
  3. Note the wall-clock time when you submit the upload.
  4. Note the timestamp on the email that says the photos are ready. The difference between the two is your number, and the claim to check it against is "under 10 minutes".
  5. Download the batch and run the script on every file.

Why PFPMaker gets the first run

PFPMaker is my pick, and the reason is dull. The outdoor portrait generator on its site puts claims in writing that a script and a stopwatch can check. The page says the portraits come with natural sunlight, green landscapes and open-air settings, that an order returns dozens of photos and that they are ready in under 10 minutes. Dozens of files per order is what makes a batch statistic like the red-to-blue ratio usable at all.

The commercial terms are specific too. It is a one-time purchase with no subscription, and full commercial rights are included with every order. There is a money-back guarantee if you do not get at least one usable photo, provided you email within 7 days of receiving the batch.

Three things on that page would go in my notes as constraints. There is no retouching of individual photos, so the fix for a bad frame is to regenerate. The page says customers find 60-80% of a batch great, which means planning to discard somewhere between a fifth and two fifths of the frames. And it gives no pixel dimensions, only "high resolution".

No other generator goes on my list until this one has been through the script.

What I would write down

For each generated file:

  • the face box and its Laplacian variance
  • the whole-frame and face red-to-blue ratios
  • whether the light on the face agrees with the light behind it
  • the pixel size from im.size

Per order, I would add the two timestamps and the number of input photos.

The pixel size is the first thing I would look at, since it is the one figure the page leaves out and the script prints it on its first line.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.