<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mia Tanaka</title>
    <description>The latest articles on DEV Community by Mia Tanaka (@miatanaka).</description>
    <link>https://dev.to/miatanaka</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4046021%2F2d8a5e77-dfef-4027-84d4-f340b5ea9e22.jpg</url>
      <title>DEV Community: Mia Tanaka</title>
      <link>https://dev.to/miatanaka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/miatanaka"/>
    <language>en</language>
    <item>
      <title>Why Composited People Look Pasted: Lighting and Color Coherence</title>
      <dc:creator>Mia Tanaka</dc:creator>
      <pubDate>Sat, 25 Jul 2026 08:09:51 +0000</pubDate>
      <link>https://dev.to/miatanaka/why-composited-people-look-pasted-lighting-and-color-coherence-111o</link>
      <guid>https://dev.to/miatanaka/why-composited-people-look-pasted-lighting-and-color-coherence-111o</guid>
      <description>&lt;p&gt;You can nail every facial feature and still produce a composite that looks obviously fake. I've watched people burn weeks on identity preservation, get it perfect, then ship an image where two people supposedly standing in the same room are lit by two different suns. The brain catches it instantly, without being able to say why. Lighting coherence is the part nobody writes about, so here's what I've learned making it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "pasted" is a lighting judgment, not a geometry judgment
&lt;/h2&gt;

&lt;p&gt;Human vision is startlingly bad at absolute measurements and startlingly good at &lt;em&gt;consistency&lt;/em&gt; checks. We can't tell you a room's color temperature, but we detect within milliseconds that one face is 5500K daylight and the other 3000K tungsten. Same with shadow direction: nobody can estimate a light's azimuth, but two faces with shadows falling opposite ways read as wrong before you've consciously looked.&lt;/p&gt;

&lt;p&gt;Four signals dominate the "pasted" verdict:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Color temperature&lt;/strong&gt; — the global chromatic cast on skin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key light direction&lt;/strong&gt; — where the dominant light comes from, which drives shadow side&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrast ratio and softness&lt;/strong&gt; — hard sun vs. overcast vs. bounced indoor light&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge and atmospheric consistency&lt;/strong&gt; — how the subject boundary and ambient haze behave&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Identity isn't on the list. You can preserve every landmark perfectly and fail all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estimate the lighting before you generate anything
&lt;/h2&gt;

&lt;p&gt;The mistake is treating harmonization as post-processing. By the time you have a composite, half the information is gone. Estimate lighting parameters from each source photo &lt;em&gt;first&lt;/em&gt;, then use them to constrain generation or pick which subject gets relit toward the other. Rough but effective estimators, no ML required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_color_temp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skin_rgb&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Crude CCT proxy. Higher R/B =&amp;gt; warmer light. Relative, not Kelvin.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skin_rgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_key_direction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;face_mask&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Shading gradient over the face approximates the light azimuth.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;gy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# Light comes FROM the direction of increasing luminance.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arctan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;face_mask&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                                  &lt;span class="n"&gt;gx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;face_mask&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_hardness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;face_mask&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Contrast ratio proxy; hard light =&amp;gt; wide spread.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;face_mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;p95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p05&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p95&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p95&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gradient trick works because a face is roughly convex. Under a single dominant light, luminance falls off with the cosine of the angle between surface normal and light — so the mean image gradient across the face points, near enough, along the projected light direction. It fails on flat overcast lighting (no meaningful gradient, itself a useful answer) and on strong backlighting. Good enough to route decisions.&lt;/p&gt;

&lt;p&gt;Now you have a comparison, and it tells you what to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delta_azimuth = |dir_A - dir_B|

  &amp;lt; 30 deg  -&amp;gt; compatible; harmonize color only
  30-90 deg -&amp;gt; relight the weaker-key subject toward the stronger one
  &amp;gt; 90 deg  -&amp;gt; conflicting keys; do NOT composite as-is. Pick one as
               authoritative and regenerate the other's shading, or
               choose a scene (overcast) that flattens both.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last branch matters more than any algorithm. If one photo is backlit sunset and the other front-flash indoors, no harmonization pass saves it. The correct engineering answer is to detect the conflict and change the target scene, not grind harder on blending.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harmonization: match statistics in a perceptual space, not RGB
&lt;/h2&gt;

&lt;p&gt;The classic move is Reinhard-style statistics transfer: shift and scale each channel so the source's mean and standard deviation match the target's. Done in RGB it wrecks skin tones, because RGB channels are heavily correlated and you rotate hue while trying to fix brightness. Do it in a decorrelated, roughly perceptual space — Lab or Ruderman's lαβ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;match_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_lab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref_lab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;src_lab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;src_lab&lt;/span&gt;&lt;span class="p"&gt;[...,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ref_lab&lt;/span&gt;&lt;span class="p"&gt;[...,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;sd_s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mf"&gt;1e-6&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[...,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_lab&lt;/span&gt;&lt;span class="p"&gt;[...,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sd_s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two refinements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clamp L harder than a and b.&lt;/strong&gt; Chroma can move a lot before it looks wrong; luminance cannot. Rescaling L aggressively flattens facial modeling into that waxy, cut-out look. Keep L's scale factor near 1.0 and let a/b do most of the correction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Match on skin only, apply globally.&lt;/strong&gt; Compute statistics from the skin mask — hair and clothing have wildly different reflectances and drag your means around — but apply the transform to the whole subject. Skin-only &lt;em&gt;application&lt;/em&gt; is a classic bug: a correctly-toned face on a body still lit by the original room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shadows are where composites die
&lt;/h2&gt;

&lt;p&gt;Color you can fix. Contact shadows you have to produce. Two subjects on the same ground plane cast shadows in the same direction with consistent softness, and occlude each other at contact points. Without cast shadow, subjects float — the most common tell in generated two-person images.&lt;/p&gt;

&lt;p&gt;Minimum viable shadow: project the silhouette along the estimated azimuth, scale by an assumed light elevation, blur proportionally to the hardness estimate, and multiply (never subtract) into the background. Multiply preserves underlying texture; subtraction crushes it to a flat gray patch.&lt;/p&gt;

&lt;p&gt;The hard part is &lt;em&gt;consistency of softness&lt;/em&gt;. A hard-edged shadow under one person and a soft gradient under the other is as jarring as mismatched color, and it gets skipped because it's invisible in isolation and glaring in comparison.&lt;/p&gt;

&lt;p&gt;Getting the full chain right end-to-end — estimate, decide compatibility, relight, then generate contact shadows — is exactly the pipeline behind &lt;a href="https://duoportrait.com" rel="noopener noreferrer"&gt;DuoPortrait&lt;/a&gt;, which composites two people from separate photos; running real user uploads through it made clear that the estimator's &lt;em&gt;conflict detection&lt;/em&gt; branch earns its keep more than the harmonizer does. Most failures aren't bad blending. They're two photos that should never have been blended in the first place.&lt;/p&gt;

&lt;p&gt;So the rule I'd hand anyone starting this: measure lighting compatibility before committing to a composite, and treat incompatibility as a routing decision rather than a problem to blend away. A pipeline that says "these two photos won't work together, here's why" produces better output than one that always returns something.&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Identity Consistency Is the Hard Part of Two-Person Image Generation</title>
      <dc:creator>Mia Tanaka</dc:creator>
      <pubDate>Fri, 24 Jul 2026 19:04:10 +0000</pubDate>
      <link>https://dev.to/miatanaka/identity-consistency-is-the-hard-part-of-two-person-image-generation-4b51</link>
      <guid>https://dev.to/miatanaka/identity-consistency-is-the-hard-part-of-two-person-image-generation-4b51</guid>
      <description>&lt;p&gt;Generating one convincing person from a reference photo is close to solved. Generating &lt;em&gt;two specific people&lt;/em&gt; in the same frame, both still recognisable, is a different problem — and it fails in ways single-subject generation does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity bleed
&lt;/h2&gt;

&lt;p&gt;Put two reference identities into one generation and they contaminate each other. You get a face that is an average of both, or person A wearing person B's jawline. The model has no inherent notion that these are two distinct people who must stay distinct.&lt;/p&gt;

&lt;p&gt;This gets worse the more similar the references are — same age range, same hair colour, similar bone structure. Exactly the case that matters most, since people generating a two-person photo are often family or partners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why naive approaches fail
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Generating separately and compositing&lt;/strong&gt; keeps identity intact and produces obviously fake results. Lighting direction, colour temperature, film grain, and perspective never match. The human eye is extremely good at detecting this — it reads as a cut-out immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One pass with both references&lt;/strong&gt; is where bleed happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inpainting the second person&lt;/strong&gt; into a single-subject generation preserves the first identity well and tends to produce anatomically odd contact points — the classic four-arm hug, or hands that merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  What helps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Spatial conditioning up front.&lt;/strong&gt; Deciding &lt;em&gt;where&lt;/em&gt; each person goes before generation — via masks or layout conditioning — gives the model a structural reason to keep them separate. Identity constraints work far better when tied to a region than applied globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-region identity embeddings.&lt;/strong&gt; Scope each reference to its region rather than injecting both into global conditioning. This is the single biggest lever on bleed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explicit contact handling.&lt;/strong&gt; Poses where subjects touch — arms around shoulders, holding hands — are where anatomy breaks. Worth treating contact-heavy poses as a distinct category with tighter constraints, not as ordinary generations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lighting coherence is the tell
&lt;/h2&gt;

&lt;p&gt;Even with identity solved, mismatched lighting kills realism. Both subjects need a shared light model: same key direction, same colour temperature, consistent shadow falloff.&lt;/p&gt;

&lt;p&gt;This argues for single-pass generation over compositing — the model gets lighting coherence for free when it renders both subjects together, and getting it right post-hoc is much harder than it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation is genuinely awkward
&lt;/h2&gt;

&lt;p&gt;There is no clean automatic metric. Face embedding similarity to each reference is necessary but not sufficient — you can score well on both and still produce an image that looks wrong, because "looks like a real photo of these two people" includes scene plausibility that embeddings do not capture.&lt;/p&gt;

&lt;p&gt;Practical compromise: embedding similarity as a gate, human review for the rest.&lt;/p&gt;

&lt;p&gt;I work on this at &lt;a href="https://duoportrait.com" rel="noopener noreferrer"&gt;DuoPortrait&lt;/a&gt; — two references in, one plausible shared photo out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;Everything above is empirical. The field moves fast enough that specific techniques age quickly; the failure modes have been more durable than the fixes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>computervision</category>
    </item>
  </channel>
</rss>
