<?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: Lena Hart</title>
    <description>The latest articles on DEV Community by Lena Hart (@lenahart_ink).</description>
    <link>https://dev.to/lenahart_ink</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%2F4176902%2F34b8f886-b5dd-421c-ae82-a03672235448.png</url>
      <title>DEV Community: Lena Hart</title>
      <link>https://dev.to/lenahart_ink</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lenahart_ink"/>
    <language>en</language>
    <item>
      <title>CPU probes for AI tattoo generators — what we measured</title>
      <dc:creator>Lena Hart</dc:creator>
      <pubDate>Sun, 11 Oct 2026 11:29:40 +0000</pubDate>
      <link>https://dev.to/lenahart_ink/cpu-probes-for-ai-tattoo-generators-what-we-measured-23j0</link>
      <guid>https://dev.to/lenahart_ink/cpu-probes-for-ai-tattoo-generators-what-we-measured-23j0</guid>
      <description>&lt;p&gt;By Lena Hart&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tattooprobes&lt;/code&gt; resamples a design to a print size on CPU, measures lines and gaps in millimetres, and can blur the raster before it scores the skeleton. A PNG from an &lt;a href="https://tatany.app/ai-tattoo-generator" rel="noopener noreferrer"&gt;AI tattoo generator&lt;/a&gt; goes in as an array.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the probes stayed on CPU
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;probes.py&lt;/code&gt; and &lt;code&gt;simulate.py&lt;/code&gt; import NumPy, OpenCV, and scikit-image. The locked environment is still a CPU torch build, because reward scoring and the CLIP labeler share the venv. &lt;code&gt;requirements.lock.txt&lt;/code&gt; pins &lt;code&gt;torch==2.14.1+cpu&lt;/code&gt;, &lt;code&gt;open_clip_torch==3.3.0&lt;/code&gt;, &lt;code&gt;transformers==4.46.3&lt;/code&gt;, and &lt;code&gt;image-reward==1.5&lt;/code&gt;. The geometry pass ran without a GPU, without a paid scoring API, and without new annotators. Seed 0 is the simulator default, &lt;code&gt;torch.manual_seed(0)&lt;/code&gt; in the classifier, and &lt;code&gt;SEED = 0&lt;/code&gt; in the fusion script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two functions
&lt;/h2&gt;

&lt;p&gt;Excerpt from &lt;code&gt;tattooprobes/probes.py&lt;/code&gt;:&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="n"&gt;NEEDLE_MM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;   &lt;span class="c1"&gt;# approx. single-needle/3RL line (practitioner guidance; assumption, see paper)
&lt;/span&gt;&lt;span class="n"&gt;GAP_MM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt;      &lt;span class="c1"&gt;# gaps narrower than this are assumed to close after healing (assumption)
&lt;/span&gt;&lt;span class="n"&gt;WORK_PX_PER_MM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;  &lt;span class="c1"&gt;# analysis resolution after physical rescaling
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_gray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;a&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;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;RGB&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;convert&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;img&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;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cvtColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COLOR_RGB2GRAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndim&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;physical_resample&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;size_cm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Rescale so that the longest side spans size_cm at WORK_PX_PER_MM.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&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;shape&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size_cm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;WORK_PX_PER_MM&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&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;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;interp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTER_AREA&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTER_CUBIC&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&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="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&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="n"&gt;interpolation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;size_cm * 10&lt;/code&gt; converts centimetres to millimetres, then &lt;code&gt;WORK_PX_PER_MM&lt;/code&gt; scales the long side. &lt;code&gt;INTER_AREA&lt;/code&gt; handles downscales and &lt;code&gt;INTER_CUBIC&lt;/code&gt; handles upscales. &lt;code&gt;to_gray&lt;/code&gt; accepts a PIL image or an array.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ink_mask&lt;/code&gt; is Otsu, or 128 when the gray standard deviation is at most 1. Ink is &lt;code&gt;gray &amp;lt; t&lt;/code&gt;. &lt;code&gt;_skel_widths&lt;/code&gt; samples a distance transform on the skeleton, doubles the radius, and divides by 10 px/mm. &lt;code&gt;line_w_p10_mm&lt;/code&gt; is the 10th percentile of that sample. &lt;code&gt;frac_sub_needle&lt;/code&gt; is the share under 0.30 mm. &lt;code&gt;frac_tight_gap&lt;/code&gt; is the same width share on background inside the ink box, counted where the width is under 0.50 mm. &lt;code&gt;midtone_frac&lt;/code&gt; is the share of gray values strictly between 60 and 200. &lt;code&gt;edge_density_per_mm2&lt;/code&gt; is the Canny count at 50 and 150, divided by &lt;code&gt;WORK_PX_PER_MM&lt;/code&gt; and by area in mm².&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PROBE_SIGNS&lt;/code&gt; is fixed a priori. &lt;code&gt;line_w_p10_mm&lt;/code&gt; is +1. &lt;code&gt;frac_sub_needle&lt;/code&gt;, &lt;code&gt;open_ends_per_cm&lt;/code&gt;, &lt;code&gt;frac_tight_gap&lt;/code&gt;, &lt;code&gt;midtone_frac&lt;/code&gt;, &lt;code&gt;border_std&lt;/code&gt;, &lt;code&gt;edge_density_per_mm2&lt;/code&gt;, &lt;code&gt;specks_per_cm2&lt;/code&gt;, and &lt;code&gt;edge_transition_mm&lt;/code&gt; are −1. The composite is the unweighted mean of signed robust z-scores, median and IQR over the pool. &lt;code&gt;edge_transition_mm&lt;/code&gt; is post hoc, from after the first degradation run, and &lt;code&gt;probes_preregistered&lt;/code&gt; omits it.&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;from&lt;/span&gt; &lt;span class="n"&gt;tattooprobes.probes&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;compute_probes&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tattooprobes.simulate&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;retention&lt;/span&gt;

&lt;span class="n"&gt;features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_probes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_cm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;kept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_cm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spread_mm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&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="n"&gt;noise&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;features&lt;/code&gt; is a float dict. &lt;code&gt;kept&lt;/code&gt; holds &lt;code&gt;skel_f1&lt;/code&gt; and &lt;code&gt;gap_survival&lt;/code&gt;. We call both at 3, 5, 10, and 15 cm. When &lt;code&gt;spread_mm &amp;gt; 0&lt;/code&gt;, &lt;code&gt;simulate_on_skin&lt;/code&gt; runs &lt;code&gt;cv2.GaussianBlur(g, (0, 0), spread_mm * WORK_PX_PER_MM)&lt;/code&gt; and adds noise only if &lt;code&gt;noise &amp;gt; 0&lt;/code&gt;. The runs below pass &lt;code&gt;noise=0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Excerpt from &lt;code&gt;tattooprobes/simulate.py&lt;/code&gt;:&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="n"&gt;deg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;simulate_on_skin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spread_mm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# v0.3 fix: threshold the simulated image with the SAME Otsu threshold as the reference, and
&lt;/span&gt;    &lt;span class="c1"&gt;# compare skeletons with a tolerance expressed in mm (0.2 mm) instead of a fixed 5-px window,
&lt;/span&gt;    &lt;span class="c1"&gt;# which previously made the match stricter at larger physical sizes (resize artifact).
&lt;/span&gt;    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;skimage.filters&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threshold_otsu&lt;/span&gt;
    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;threshold_otsu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ref&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;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;
    &lt;span class="n"&gt;m1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deg&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;
    &lt;span class="n"&gt;s0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;skeletonize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;skeletonize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOL_MM&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;WORK_PX_PER_MM&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;tol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dilate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&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="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint8&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;ones&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&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="n"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TOL_MM&lt;/code&gt; is 0.2. F1 is the harmonic mean of the two dilated skeleton overlaps. Gap survival is the share of reference background inside the ink box that is still background after the blur.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46lt79jgxkf79bpvg9ul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46lt79jgxkf79bpvg9ul.png" alt="Probe pipeline (Illustration)" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the rows got onto disk
&lt;/h2&gt;

&lt;p&gt;HPDv2 train, prompts matching "tattoo": 2,010 pairs, 327 prompts, 1,340 images, stream-extracted from a 31.7 GB tar in 23 minutes, about 60 MB kept on disk. ImageRewardDB train+val tattoo prompts: 370 rows. 47 images are 0-byte in the upstream zips, leaving 323 usable images, 809 within-prompt rank pairs, and 51 prompts. Pick-a-Pic v2 came from a liuhuohuo2 parquet mirror of 672 shards, metadata only: 2,608 tattoo rows, 1,687 labeled and different pairs, 185 prompts. Images for that pass were not fetched. The license is unverified. The original card is gone, and the mirrors say MIT. Drozdik/tattoo_v0 is CC-BY-NC-SA-4.0, research only: 120 designs sampled, 6 defect types at 2 levels, 1,560 images.&lt;/p&gt;

&lt;p&gt;Labels are a keyword rule plus CLIP ViT-B/32 zero-shot (&lt;code&gt;open_clip&lt;/code&gt; ViT-B-32, pretrained &lt;code&gt;openai&lt;/code&gt;). HPDv2: design 334, on_skin 87, incidental 674, other 146, ambiguous 99. ImageRewardDB: design 48, on_skin 12, incidental 238, other 18, ambiguous 7. &lt;code&gt;tattoo_subject&lt;/code&gt; is design plus on_skin, which is 463 pairs and 119 prompts on HPDv2, and 148 pairs and 9 prompts on ImageRewardDB. A spot-check of 24 random design images looked tattoo-like for ~21/24 on HPDv2 and 24/24 on ImageRewardDB.&lt;/p&gt;

&lt;p&gt;Per-dimension rows below are the alignment after the reward re-score: 528 pairs and 125 prompts. The composite stays on 463 pairs and 119 prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agreement, holdout, and the spread ladder
&lt;/h2&gt;

&lt;p&gt;Pairwise accuracy is the rate at which the scorer orders a pair as the human label did. On the HPDv2 tattoo-subject per-dimension table, n_pairs is 528 and n_prompts is 125. Holm p is 0.0065 on every row.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scorer&lt;/th&gt;
&lt;th&gt;Accuracy A&lt;/th&gt;
&lt;th&gt;A−0.5&lt;/th&gt;
&lt;th&gt;Cohen h&lt;/th&gt;
&lt;th&gt;Holm p&lt;/th&gt;
&lt;th&gt;sig&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;frac_tight_gap@5&lt;/td&gt;
&lt;td&gt;0.335&lt;/td&gt;
&lt;td&gt;−0.165&lt;/td&gt;
&lt;td&gt;−0.34&lt;/td&gt;
&lt;td&gt;0.0065&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;edge_density_per_mm2@5&lt;/td&gt;
&lt;td&gt;0.335&lt;/td&gt;
&lt;td&gt;−0.165&lt;/td&gt;
&lt;td&gt;−0.34&lt;/td&gt;
&lt;td&gt;0.0065&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;midtone_frac@5&lt;/td&gt;
&lt;td&gt;0.369&lt;/td&gt;
&lt;td&gt;−0.131&lt;/td&gt;
&lt;td&gt;−0.26&lt;/td&gt;
&lt;td&gt;0.0065&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;line_w_p10_mm@5&lt;/td&gt;
&lt;td&gt;0.428&lt;/td&gt;
&lt;td&gt;−0.072&lt;/td&gt;
&lt;td&gt;−0.14&lt;/td&gt;
&lt;td&gt;0.0065&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLIPScore&lt;/td&gt;
&lt;td&gt;0.682&lt;/td&gt;
&lt;td&gt;+0.182&lt;/td&gt;
&lt;td&gt;+0.37&lt;/td&gt;
&lt;td&gt;0.0065&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PickScore&lt;/td&gt;
&lt;td&gt;0.723&lt;/td&gt;
&lt;td&gt;+0.223&lt;/td&gt;
&lt;td&gt;+0.46&lt;/td&gt;
&lt;td&gt;0.0065&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ImageReward&lt;/td&gt;
&lt;td&gt;0.684&lt;/td&gt;
&lt;td&gt;+0.184&lt;/td&gt;
&lt;td&gt;+0.38&lt;/td&gt;
&lt;td&gt;0.0065&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HPSv2.1&lt;/td&gt;
&lt;td&gt;0.790&lt;/td&gt;
&lt;td&gt;+0.290&lt;/td&gt;
&lt;td&gt;+0.62&lt;/td&gt;
&lt;td&gt;0.0065&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;probe_composite@5&lt;/code&gt; is the other alignment: 0.305 [0.256, 0.353] on 463 pairs and 119 prompts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1m8cpkqs8hgdkg0dfunv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1m8cpkqs8hgdkg0dfunv.png" alt="Per-dimension agreement with human preference, HPDv2 tattoo-subject" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fusion fits &lt;code&gt;LogisticRegression&lt;/code&gt; on winner-minus-loser features after a random side flip, with a &lt;code&gt;StandardScaler&lt;/code&gt; and &lt;code&gt;GroupKFold&lt;/code&gt; on prompt id. HPDv2 rows in &lt;code&gt;results/fusion_lopo.csv&lt;/code&gt; are &lt;code&gt;GroupKFold(prompt,k=5)&lt;/code&gt;. A literal leave-one-prompt-out loop will not match that file. Preregistered probes, with &lt;code&gt;edge_transition_mm&lt;/code&gt; excluded, score 0.708 [0.667, 0.750]. Rewards score 0.790 [0.748, 0.827]. Rewards plus probes score 0.767 [0.729, 0.803]. Probes alone beat chance in that fit only with inverted coefficient signs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmegfhtlamxeh7yeauqy4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmegfhtlamxeh7yeauqy4.png" alt="Fusion LOPO accuracy" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the default simulator, noise stays at 0. At σ = 0, mean skeleton F1 is 1.000 at every tested size. At σ = 0.30 mm, mean skeleton F1 by print size is 3 cm 0.477, 5 cm 0.563, 10 cm 0.668, 15 cm 0.733. At 5 cm and σ = 0.30 mm, Spearman ρ is −0.824 for &lt;code&gt;frac_tight_gap&lt;/code&gt; vs gap survival, −0.599 for edge density vs gap survival, and +0.507 for &lt;code&gt;line_w_p10_mm&lt;/code&gt; vs skeleton F1.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgroy3lcqpxwazco0s052.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgroy3lcqpxwazco0s052.png" alt="Minimum print size vs ink spread" width="799" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would change in the next eval
&lt;/h2&gt;

&lt;p&gt;Fusion at 0.708 [0.667, 0.750] tracks the human bit, and the probes clear chance in that fit only with inverted coefficient signs. The signed composite on 463 pairs is 0.305 [0.256, 0.353]. Log &lt;code&gt;PROBE_SIGNS&lt;/code&gt; for the millimetre direction. Fit the human bit for this preference label. A training loss built from the signed mean follows the 0.305 direction. &lt;code&gt;line_w_p10_mm&lt;/code&gt; is the only preregistered key with sign +1, so a fatter Otsu mask, whether from thickening or from blur, moves that coordinate the way the heuristic favors.&lt;/p&gt;

&lt;p&gt;HPSv2.1 was trained on HPDv2, and ImageReward was trained on ImageRewardDB, so those agreements are in-distribution leakage.&lt;/p&gt;

&lt;p&gt;ImageRewardDB tattoo-subject has 9 prompts. The Pick-a-Pic design slice has 11 prompts. Probes are not Holm-significant on either slice, so both stay out of the 528-pair table.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;frac_tight_gap&lt;/code&gt; and gap survival both read background inside the ink box: widths already under 0.50 mm, and pixels still background at σ = 0.30 mm. ρ = −0.824 is partly circular by construction. Edge density vs gap survival is ρ = −0.599. &lt;code&gt;line_w_p10_mm&lt;/code&gt; vs skeleton F1 is ρ = +0.507.&lt;/p&gt;

&lt;p&gt;The 5 px window in the excerpt made the match stricter at larger physical sizes. &lt;code&gt;TOL_MM = 0.2&lt;/code&gt; keeps the band in millimetres. At σ = 0 the published mean skeleton F1 is 1.000 at every tested size.&lt;/p&gt;

&lt;p&gt;The ink-spread model is an isotropic Gaussian, not validated on healed skin. tatany.app was not ranked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Stark-Will/tattoo-probes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The archive is &lt;a href="https://doi.org/10.5281/zenodo.23292900" rel="noopener noreferrer"&gt;doi:10.5281/zenodo.23292900&lt;/a&gt;. Install &lt;code&gt;requirements.lock.txt&lt;/code&gt;, keep the CPU torch wheel, and leave the seeds at 0. Entry points: &lt;code&gt;compute_probes(img, size_cm=5.0)&lt;/code&gt; and &lt;code&gt;retention(img, size_cm, spread_mm, seed=0, noise=0.0)&lt;/code&gt;. Read per-dimension rows from the 528-pair, 125-prompt alignment, and the composite from the 463-pair, 119-prompt alignment.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on tatany.app, an AI tattoo generator; the study does not rank it or any other product.&lt;/p&gt;

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