{"slug": "outdoor-portrait-generator-a-quality-and-speed-test", "title": "Outdoor portrait generator: a quality and speed test", "summary": "A developer built a small Pillow and OpenCV script for evaluating outdoor portrait generators, finding that whole-frame Laplacian variance misleads because blurred backgrounds dominate the image. On a calibration daylight portrait, the whole frame scored 106.0 — barely above the conventional 100 blur threshold — while the face crop scored 605.2 and the blurred background just 6.9, so the script reports a face crop separately. The author notes no generated photos were actually ordered, so the results are not a product score.", "body_md": "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](https://pfpmaker.ai), which promises delivery in under 10 minutes.\n\nI 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.\n\nI 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](https://pyimagesearch.com/2015/09/07/blur-detection-with-opencv/) uses 100 as the default threshold and calls anything lower blurry. He also warns that the number is \"quite domain dependent\".\n\nThe 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.\n\n| Region | Size in pixels | Laplacian variance | \n|---|---|---|\n| Whole frame | 1200 x 675 | 106.0 | \n| Face crop | 220 x 280 | 605.2 | \n| Blurred background, top right | 350 x 300 | 6.9 | \n\nThe 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.\n\nI 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.\n\nIt 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](https://pillow.readthedocs.io/en/stable/reference/ImageStat.html).\n\n```\npython3 -m pip install --upgrade Pillow\npython3 -m pip install opencv-python numpy\n```\n\nOn a server or in Docker, the [opencv-python package page](https://pypi.org/project/opencv-python/) says to install `opencv-python-headless` instead, and to install only one of the two because they share the `cv2` namespace.\n\nSave this as `portrait_check.py`:\n\n``` python\nimport sys\n\nimport cv2\nimport numpy as np\nfrom PIL import Image, ImageStat\n\ndef measure(pil_image):\n    means = ImageStat.Stat(pil_image).mean\n    red_blue = means[0] / means[2] if means[2] else float(\"nan\")\n    gray = cv2.cvtColor(np.asarray(pil_image), cv2.COLOR_RGB2GRAY)\n    sharpness = float(cv2.Laplacian(gray, cv2.CV_64F).var())\n    return means, red_blue, sharpness\n\ndef report(label, pil_image):\n    means, red_blue, sharpness = measure(pil_image)\n    print(f\"{label} means: R {means[0]:.1f}, G {means[1]:.1f}, B {means[2]:.1f}\")\n    print(f\"{label} R/B ratio: {red_blue:.2f}\")\n    print(f\"{label} Laplacian variance: {sharpness:.1f}\")\n\nif len(sys.argv) != 6:\n    raise SystemExit(\"Usage: python portrait_check.py IMAGE LEFT TOP RIGHT BOTTOM\")\n\nimage = Image.open(sys.argv[1]).convert(\"RGB\")\nbox = tuple(int(value) for value in sys.argv[2:6])\n\nprint(f\"size: {image.size[0]} x {image.size[1]}\")\nreport(\"whole frame\", image)\nreport(\"face crop\", image.crop(box))\n```\n\nRun 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.\n\nThe 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.\n\nMy 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.\n\nSo 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.\n\nThe 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.\n\n[In-body image goes here: outdoor-portrait-inbody.jpg, see source below]\n\nI have no delivery time to report. The speed half of the test is a procedure:\n\nPFPMaker is my pick, and the reason is dull. The [outdoor portrait generator](https://pfpmaker.ai/outdoor-photos) 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.\n\nThe 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.\n\nThree 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\".\n\nNo other generator goes on my list until this one has been through the script.\n\nFor each generated file:\n\n`im.size`\nPer order, I would add the two timestamps and the number of input photos.\n\nThe 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.", "url": "https://wpnews.pro/news/outdoor-portrait-generator-a-quality-and-speed-test", "canonical_source": "https://dev.to/illeanavoviews/outdoor-portrait-generator-a-quality-and-speed-test-aml", "published_at": "2026-10-11 09:33:17+00:00", "updated_at": "2026-10-11 09:51:34.039565+00:00", "lang": "en", "topics": ["computer-vision", "ai-tools", "generative-ai"], "entities": ["PFPMaker", "OpenCV", "Pillow", "Adrian Rosebrock", "PyImageSearch"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/outdoor-portrait-generator-a-quality-and-speed-test", "markdown": "https://wpnews.pro/news/outdoor-portrait-generator-a-quality-and-speed-test.md", "text": "https://wpnews.pro/news/outdoor-portrait-generator-a-quality-and-speed-test.txt", "jsonld": "https://wpnews.pro/news/outdoor-portrait-generator-a-quality-and-speed-test.jsonld"}}