埋める所と守る所 / original
DEVELOPMENT JOURNAL / 28
Photoshot
CODE × UI
無料Photoshop代替の開発記

埋める所と守る所

採取領域がなくなった原因を直し、ステッカー除去をもう一度完了しました。さらに人物保護チャンネルで幅を縮め、保護なしの同条件と保存画素を比較しました。

PHOTOSHOT DEVELOPMENT JOURNAL / 28

AI生成の動作例用写真。編集画面と出力画像は実際のPhotoshotで作成しました。

Photoshot無料Photoshop代替の開発記 / 28FOUNDATION

ソースから始める。

周囲からパッチ候補を探す

content-aware.tsは選択の境界から内側へ埋めます。未充填領域を除いた周囲の色を候補と比べ、近傍と全体の候補を探します。学習済み生成モデルが新しい物体を想像する処理ではありません。

候補に選択中・採取除外中の画素が含まれてはいけません。対象と採取領域を分けてステッカーを再び複製する問題を減らします。適切な周囲の質感がなければ自然な結果も難しくなります。

新規レイヤーに結果を分ける

2026年10月22日、ステッカー周辺をFeather 4の矩形で選びました。Patch Size 4、All Layers、Auto — outside selection、New Layerで、明るい壁に置き換わるプレビューを確認して適用しました。

プロジェクトにはoriginalとContent-Aware Fillが残りました。原本srcのPNGバイトは入力と完全一致しました。出力の不透明部分は選択周辺に限られ、境界には柔らかいアルファが残っています。

app/studio/content-aware.ts · SHA-256 3f1bafefa260 · L7–15Photoshotの実装コード ↗
/** Exemplar patch synthesis. Donor patches never include selected or excluded pixels. */
export async function contentAwarePixels(image:{width:number;height:number;data:Uint8ClampedArray|Float32Array},mask:ArrayLike<number>,options:Pick<FillOptions,'radius'|'sampling'|'signal'|'progress'>){
 check(options.signal);const {width:w,height:h}=image,n=w*h,r=Math.round(options.radius);if(!supportedSize(w,h)||!Number.isInteger(w)||!Number.isInteger(h)||image.data.length!==n*4||mask.length!==n||!Number.isFinite(r)||r<1||r>15)throw Error('Invalid fill image or patch size.');if(options.sampling&&(options.sampling.length!==n||options.sampling.some(v=>!Number.isFinite(v)||v<0||v>255)))throw Error('Invalid sampling area.');
 const source=image.data,out=source.slice(),missing=new Uint8Array(n),queue=new Int32Array(n),seen=new Uint8Array(n),integral=new Int32Array((w+1)*(h+1));let total=0,tail=0;
 for(let y=0;y<h;y++){if(y%64===0){check(options.signal);await new Promise(r=>setTimeout(r,0));}let row=0;for(let x=0;x<w;x++){const i=y*w+x;if(!Number.isFinite(mask[i])||mask[i]<0||mask[i]>255)throw Error('Invalid fill selection.');missing[i]=mask[i]>0?1:0;total+=missing[i];row+=missing[i]||source[i*4+3]<250||options.sampling&&options.sampling[i]<128?1:0;integral[(y+1)*(w+1)+x+1]=integral[y*(w+1)+x+1]+row;}}
 if(!total)throw Error('Select an area to fill.');
 const candidates:number[]=[],valid=new Uint8Array(n),stride=w+1;
 for(let y=r;y<h-r;y++){if(y%64===0){check(options.signal);await new Promise(r=>setTimeout(r,0));}for(let x=r;x<w-r;x++){const x0=x-r,y0=y-r,x1=x+r+1,y1=y+r+1;if(integral[y1*stride+x1]-integral[y0*stride+x1]-integral[y1*stride+x0]+integral[y0*stride+x0]===0){valid[y*w+x]=1;candidates.push(y*w+x);}}}
 if(!candidates.length)throw Error('No clean sampling patch. Expand the sampling area or reduce Patch Size.');
Photoshot無料Photoshop代替の開発記 / 28IMPLEMENTATION

計算の
内側へ。

選択範囲を直して塗りつぶしを完了

全選択で失敗したのは選択外の採取領域が残らなかったためです。全選択を解除し原本を開き直して、ステッカーと小さな余白だけを選びました。きれいな壁を選択外に残すと同じPatch Size 4で正常に完了しました。

新規レイヤー出力で原本を保持しました。ガードを無理に通すのではなく、対象と採取領域を分ける修正です。再実行結果を保存し、原本バイトと出力レイヤーを確認しました。

人物保護を指定して1536pxを1200pxへ

人物側を選択してPortrait protectとして保存しました。Content-Aware ScaleはWidth 1200、Height 1024、Protect Portrait protect、基準点Middle Rightを指定し、プレビュー更新後に適用しました。

文書は1536×1024を維持し、レイヤーは1200×1024、X 336、Y 0です。右端を固定するため左336pxは透明です。この移動を反映して保存PNGを比較すると、保護範囲757,020画素のRGBA値はすべて原本と一致しました。

PHOTOSHOT / SOURCE NOTES28

コードを読む。

app/studio/content-aware.ts · SHA-256 3f1bafefa260 · L33–37Photoshotの実装コード ↗
export async function contentAwareFill(doc:StudioDoc,layer:Layer,options:FillOptions):Promise<StudioDoc>{

 if(!doc.selection)throw Error('Make a selection first.');if(options.output==='new'&&doc.layers.length>=256)throw Error('Up to 256 layers.');if(options.output==='current'&&(layerLocked(doc,layer,'pixels')||layer.kind==='text'||layer.kind==='adjustment'||layer.shape||layer.smartObject||layer.gridWarp||layer.puppet))throw Error('Choose New Layer or an unlocked raster layer.');
 check(options.signal);const edit=await openPixelEdit(options.sample==='all'?doc:{...doc,layers:[layer]}),pixels=edit.image,result=await contentAwarePixels(pixels,doc.selection.alpha,options);check(options.signal);
 if(options.output==='new'){for(let i=0;i<doc.width*doc.height;i++)result[i*4+3]*=doc.selection.alpha[i]/255;const output=await newPixelEditLayer(edit,'Content-Aware Fill',result);check(options.signal);return {...doc,layers:[...doc.layers,output]};}
app/studio/content-scale.ts · SHA-256 ff2884f5e7a4 · L9–12Photoshotの実装コード ↗
/** Dynamic-programming seams, including masks, follow the same pixel correspondence. */
export async function carveImage(input:ImageBuffer,width:number,height:number,options:{signal?:AbortSignal;progress?:(n:number)=>void}={}){
 if(!supportedSize(width,height)||!Number.isInteger(width)||!Number.isInteger(height)||!supportedSize(input.width,input.height)||input.data.length!==input.width*input.height*4||input.channels.some(c=>c.length!==input.width*input.height||c.some(v=>!Number.isFinite(v)||v<0||v>255)))throw Error('Invalid content-aware size or channels.');let image:ImageBuffer={...input,data:input.data.slice(),channels:input.channels.map(c=>c.slice())},done=0,total=Math.abs(width-input.width)+Math.abs(height-input.height),yieldAt=performance.now();
 const checkpoint=async()=>{if(options.signal?.aborted)throw new DOMException('Scale canceled.','AbortError');if(performance.now()-yieldAt>16){options.progress?.(total?done/total:1);await new Promise(r=>setTimeout(r,0));yieldAt=performance.now();if(options.signal?.aborted)throw new DOMException('Scale canceled.','AbortError');}};
app/studio/content-scale.ts · SHA-256 ff2884f5e7a4 · L20–24Photoshotの実装コード ↗
export async function contentAwareScale(doc:StudioDoc,layer:Layer,options:ScaleOptions):Promise<StudioDoc>{

 if(layerLocked(doc,layer,'pixels')||layerLocked(doc,layer,'position')||layer.kind==='adjustment'||layer.kind==='text'||layer.smartObject||layer.shape||layer.gridWarp||layer.puppet)throw Error('Select an unlocked raster layer. Rasterize type, shapes, Smart Objects or existing warps first.');if(!Number.isInteger(options.anchor)||options.anchor<0||options.anchor>8)throw Error('Invalid scale reference point.');
 const edit=await openPixelEdit(doc,layer),canvas=edit.canvas,pixels=edit.image,selected=options.protect==='$selection'?doc.selection:options.protect?doc.savedSelections?.[options.protect]:undefined;if(options.protect&&!selected)throw Error('Protection selection is unavailable.');const channels=[selected?Float32Array.from(selectionForLayer(selected,layer).alpha):new Float32Array(canvas.width*canvas.height)],maskKeys=(['mask','vectorMask'] as const).filter(k=>layer[k]);for(const key of maskKeys){const m=maskSurface({...layer[key]!,enabled:true},canvas.width,canvas.height),p=m.getContext('2d')!.getImageData(0,0,m.width,m.height).data;channels.push(Float32Array.from({length:m.width*m.height},(_,i)=>p[i*4+3]));}
 const result=await carveImage({width:pixels.width,height:pixels.height,data:pixels.data,channels},options.width,options.height,options);const raster=await closePixelEdit(edit,layer,result.data,result.width,result.height),masks:Partial<Layer>={};if(options.signal?.aborted)throw new DOMException('Scale canceled.','AbortError');maskKeys.forEach((key,i)=>{masks[key]={width:result.width,height:result.height,enabled:layer[key]!.enabled,inverted:false,strokes:[],raster:{width:result.width,height:result.height,alpha:Array.from(result.channels[i+1],Math.round)}};});if(layer.vectorMask){const enabled=maskKeys.filter(key=>layer[key]!.enabled),merge=enabled.length?enabled:maskKeys;masks.mask={width:result.width,height:result.height,enabled:enabled.length>0,inverted:false,strokes:[],raster:{width:result.width,height:result.height,alpha:Array.from({length:result.width*result.height},(_,n)=>Math.round(merge.reduce((a,key)=>a*result.channels[maskKeys.indexOf(key)+1][n]/255,255)))}};masks.vectorMask=undefined;}const next=aroundAnchor(layer,{...raster,...masks,width:result.width,height:result.height},options.anchor);return {...doc,layers:doc.layers.map(l=>l.id===layer.id?next:l)};
Photoshot無料Photoshop代替の開発記 / 28IN PRACTICE

エディターで見る。

埋める所と守る所 / workspace
選択を修正し、正常完了した塗りつぶしプレビュー。

保護なしと同条件で比較する

同じ原本を新しく開き、ProtectだけNoneにして同じ寸法と基準点を適用しました。前の保護範囲を同じ座標基準で比較すると522,490画素に変化があり、保護ありでは0でした。この写真の1200px条件での測定で、画素差を美観の点数とは扱いません。

content-scale.tsは低コストのシームを除き、保護画素に大きな重みを加えます。マスクも同じ画素対応を追います。今回は再実行の完了、保護あり・なし、保存結果まで直接検証しました。人物はAI生成の成人架空モデルです。29回は整列・HDR・パノラマです。

Photoshot無料Photoshop代替の開発記 / 28BEFORE / AFTER

原画像と新規レイヤーへの塗りつぶし。

埋める所と守る所 / original
01 / AI生成の実例原画像
埋める所と守る所 / result
02 / 小さなステッカーを削除・原本保持

人物保護を比較する。

03 / 保護あり・保護画素の差0
03 / 保護あり・保護画素の差0
04 / 保護なし・同じ寸法と右基準点
04 / 保護なし・同じ寸法と右基準点

AI生成の動作例用写真。編集画面と出力画像は実際のPhotoshotで作成しました。

確認したコードと検査

コード・検査記録・日付 ↗