处理尺寸不一致
This commit is contained in:
@@ -222,6 +222,8 @@ node .\tools\model-check.mjs --stage completion `
|
||||
|
||||
`--part` 可重复;省略则处理所有具备可见 mask、补全需求和允许区域的部件。当前 SAM 示例仍有错分,应先复核。补全不会自动纠正上游语义;通过几何检查的 PNG 保存到 `parts/`,仍需人工复核。完整参数、离线候选导入与限制见[第三阶段文档](docs/3-completion-stage.md)。
|
||||
|
||||
第三阶段请求会显式发送与局部画布一致的 `size`(默认 `1024x1024`)。如果中转站仍返回其他尺寸,可使用 `--candidates-dir 旧补全目录 --candidate-size-policy uniform` 离线复查:只允许同宽高比的画布等比例缩放,保留原候选,继续检查位置与新增区域并恢复原像素。默认 strict 模式仍拒绝尺寸不符;详细命令见第三阶段文档。
|
||||
|
||||
### 单独测试图片补全接口
|
||||
|
||||
此命令只验证图片编辑适配器并保存原始候选,不代表补全部件已经验收通过:
|
||||
|
||||
+27
-5
@@ -140,16 +140,32 @@ def prepare(directory, selected_ids, size=1024, radius=32):
|
||||
return result
|
||||
|
||||
|
||||
def merge_candidate(folder, candidate_file, output_file):
|
||||
def merge_candidate(folder, candidate_file, output_file, size_policy='strict'):
|
||||
if size_policy not in ['strict', 'uniform']:
|
||||
raise ValueError('Unknown candidate size policy')
|
||||
folder = Path(folder)
|
||||
task = read_json(folder/'task.json')
|
||||
original = np.array(Image.open(folder/'visible.png').convert('RGBA'))
|
||||
allowed = np.array(Image.open(folder/'allowed-mask.png').convert('L')) > 0
|
||||
candidate = Image.open(candidate_file).convert('RGBA')
|
||||
qa = {'accepted': False, 'semantic_verified': False, 'reasons': []}
|
||||
qa = {'accepted': False, 'semantic_verified': False, 'reasons': [],
|
||||
'expected_size': task['canvas_size'], 'returned_size': list(candidate.size),
|
||||
'size_policy': size_policy, 'canvas_resampled': False}
|
||||
if list(candidate.size) != task['canvas_size']:
|
||||
qa['reasons'].append('CANVAS_SIZE_MISMATCH')
|
||||
return qa
|
||||
if size_policy == 'strict':
|
||||
qa['reasons'].append('CANVAS_SIZE_MISMATCH')
|
||||
return qa
|
||||
tw, th = task['canvas_size']
|
||||
cw, ch = candidate.size
|
||||
if cw * th != ch * tw:
|
||||
qa['reasons'].append('CANVAS_ASPECT_RATIO_MISMATCH')
|
||||
return qa
|
||||
# Only reverse uniform whole-canvas scaling. Do not translate, stretch,
|
||||
# crop, or register the object to make a changed pose appear correct.
|
||||
candidate = candidate.resize((tw, th), Image.Resampling.LANCZOS)
|
||||
qa.update(canvas_resampled=True, canvas_scale=tw/cw,
|
||||
resampling='LANCZOS', warnings=['CANDIDATE_CANVAS_RESAMPLED'])
|
||||
candidate.save(Path(output_file).with_name(Path(output_file).stem + '-normalized.png'))
|
||||
try:
|
||||
candidate, preprocessing = prepare_rgba(candidate)
|
||||
except ValueError as error:
|
||||
@@ -213,6 +229,11 @@ def finalize(directory):
|
||||
ox, oy = meta['origin_xy']
|
||||
task.update(image=f'parts/{name}.png', crop_xyxy=[bbox[0]+ox, bbox[1]+oy, bbox[2]+ox, bbox[3]+oy])
|
||||
view = f'<img src="{task["image"]}">' if task.get('image') else ''
|
||||
for index, item in enumerate(task.get('candidates', []), 1):
|
||||
view += f'<p><a href="{html.escape(item["file"], quote=True)}">原候选 {index}</a> · <a href="tasks/{name}/qa-{index}.json">QA</a>'
|
||||
if item['qa'].get('canvas_resampled'):
|
||||
view += f' · <a href="tasks/{name}/merged-{index}-normalized.png">缩放后候选</a>'
|
||||
view += '</p>'
|
||||
warnings = html.escape(', '.join(task.get('upstream_warnings', [])))
|
||||
rows.append(f'<tr><td>{html.escape(name)}<p>{warnings}</p><a href="tasks/{name}/task.json">任务参数</a> · <a href="tasks/{name}/prompt.txt">提示词</a></td><td>{html.escape(task["status"])}</td><td><a href="tasks/{name}/visible.png"><img src="tasks/{name}/visible.png"></a></td><td><a href="tasks/{name}/allowed-mask.png"><img src="tasks/{name}/allowed-mask.png"></a><p><a href="tasks/{name}/context.png">原图局部参考</a></p></td><td>{view}</td><td><pre>{html.escape(json.dumps(qa, ensure_ascii=False, indent=2))}</pre></td></tr>')
|
||||
write_json(out/'completion-result.json', result)
|
||||
@@ -232,13 +253,14 @@ def main():
|
||||
parser.add_argument('--candidate')
|
||||
parser.add_argument('--merged')
|
||||
parser.add_argument('--qa')
|
||||
parser.add_argument('--candidate-size-policy', choices=['strict', 'uniform'], default='strict')
|
||||
args = parser.parse_args()
|
||||
if args.action == 'prepare':
|
||||
if not 128 <= args.size <= 4096 or not 1 <= args.radius <= 256:
|
||||
parser.error('size must be 128..4096 and radius 1..256')
|
||||
prepare(args.out, args.part, args.size, args.radius)
|
||||
elif args.action == 'merge':
|
||||
write_json(args.qa, merge_candidate(args.out, args.candidate, args.merged))
|
||||
write_json(args.qa, merge_candidate(args.out, args.candidate, args.merged, args.candidate_size_policy))
|
||||
else:
|
||||
finalize(args.out)
|
||||
|
||||
|
||||
@@ -50,6 +50,27 @@ node .\tools\model-check.mjs --stage completion `
|
||||
|
||||
候选文件名为 `completion-candidates/head.png`,必须与对应任务的局部画布尺寸、坐标完全一致。准备与合并必须使用相同输入、`--completion-size` 和 `--extension-radius`。缺失候选记为 `missing_candidate`,不会联网补发。可将先前任务保存的原始候选复制为 `ID.png`,离线检查它,避免重复计费。
|
||||
|
||||
`--candidates-dir` 也支持直接指向旧补全输出目录:读取 `tasks/<id>/candidate-1.png`(亦支持 jpg/webp),不下载、不重新生成。程序先核对旧 manifest 中的源图、语义计划、SAM mask 哈希及画布/延展参数是否与本次一致;不同则拒绝复用。该模式只取每部件首张原候选。
|
||||
|
||||
### 返回尺寸与输入不一致时
|
||||
|
||||
`--completion-size` 控制局部画布,不能单靠提示词保证模型输出相同尺寸。修复后第三阶段会同时发送 multipart 参数 `size=画布宽x画布高`,默认 `1024x1024`,并在每部件 `request.json` 记录实际请求尺寸。配置中的 `completion.params.size` 若与画布不同会在调用前报错。[OpenAI 编辑接口的 size 参数](https://developers.openai.com/api/reference/cli/resources/images/methods/edit)有明确的输出尺寸含义,但中转站是否遵守仍以实测结果为准。
|
||||
|
||||
默认 `--candidate-size-policy strict` 仍拒绝尺寸不一致。已有候选可以使用 `uniform` 模式离线尝试恢复:仅允许相同宽高比的整张画布等比例缩放,再执行原有的颜色/位置、可见像素保留率、新增区域和连通性检查。不对物体单独缩放、不平移、不裁剪,也不放宽对齐阈值。原图和 SAM mask 始终不缩放。
|
||||
|
||||
```powershell
|
||||
node .\tools\model-check.mjs --stage completion `
|
||||
--config .\configs\tiantoken.json `
|
||||
--input-dir .\output\20260918-183532\sam `
|
||||
--candidates-dir .\output\20260918-183532\complete2_gpt `
|
||||
--out-dir ".\output\$runId-size-recheck" `
|
||||
--candidate-size-policy uniform
|
||||
```
|
||||
|
||||
该命令不请求图片模型。原始候选保留,缩放后的候选另存为 `merged-N-normalized.png`,QA 记录原始/目标尺寸、比例、重采样方法和告警。即便最终通过几何检查也仍需语义复核;如果模型同时重绘、放大或移动了部件,可能继续报 `VISIBLE_ALIGNMENT_MISMATCH`。
|
||||
|
||||
2026-09-20 对 `complete2_gpt` 的 12 张原始候选实测:输入均为 1024×1024,输出为 1248×1248 或 1254×1254。旧请求未发送 size。离线 uniform 复查后 8 个通过几何检查、4 个因对齐不符或无有效新增像素拒绝;原始文件未覆盖,也没有重新请求生成。报告位于 `output/20260918-183532/complete2_gpt-size-recheck-v2/report.html`。22 项测试通过,含请求 size、等比例恢复后的像素保真、宽高比/偏移拒绝及旧任务输入匹配检查。显式 size 已通过请求构造测试,尚未再次付费验证中转站是否遵守。
|
||||
|
||||
每个输出目录必须是新目录;`--resume` 仅复用已完整结束且输入、参数、源码和产物哈希一致的任务。准备模式与生成模式使用不同目录。失败任务不自动续发请求。
|
||||
|
||||
| 参数 | 作用 |
|
||||
@@ -57,7 +78,8 @@ node .\tools\model-check.mjs --stage completion `
|
||||
| `--input-dir` | SAM 输出目录,必须含 `source.png`、v2 计划、`segmentation.json`、`masks/<id>.png` |
|
||||
| `--part ID` | 指定部件,可重复;省略时处理全部有补全需求的部件 |
|
||||
| `--prepare-only` | 只准备画布、区域与提示词,不请求图片模型 |
|
||||
| `--candidates-dir` | 离线读取 `ID.png` 候选,与 prepare-only 互斥 |
|
||||
| `--candidates-dir` | 离线读取 `ID.png` 或旧补全目录首张候选,与 prepare-only 互斥 |
|
||||
| `--candidate-size-policy` | strict(默认):严格尺寸一致;uniform:同宽高比缩放后重新检查 |
|
||||
| `--completion-size` | 方形局部画布边长,默认 1024,128~4096;不缩放原像素 |
|
||||
| `--extension-radius` | 新增区域距可见 mask 的最大距离,默认 32 像素,1~256 |
|
||||
| `--completion-model` | 图片模型原始 ID,当前示例使用 qwen-image-3.0 |
|
||||
@@ -81,7 +103,7 @@ node .\tools\model-check.mjs --stage completion `
|
||||
|
||||
## 返回候选的检查和合并
|
||||
|
||||
1. 尺寸必须与局部画布完全一致。拒绝自动缩放、重定位或裁切来迎合返回图。
|
||||
1. 默认要求尺寸与局部画布完全一致。显式选择 uniform 时,只对同宽高比候选做整画布等比例恢复;宽高比不同仍拒绝。所有模式都不自动重定位或裁切。
|
||||
2. 已有透明背景保留;不透明白底使用边界连通近白区域去底。无法提取前景则拒绝。
|
||||
3. 使用原部件 Alpha ≥240 的像素检查对齐:候选保留率至少 95%,RGB 平均绝对误差不大于 30。无不透明锚点则拒绝。这只能发现明显偏移或重绘,不能保证语义正确。
|
||||
4. 只保留允许区域中的新增像素,并去掉与原可见部分不连通的新增块。越界和不连通像素数量写入 QA。没有新增像素则拒绝。
|
||||
|
||||
@@ -24,11 +24,12 @@ function filesIn(dir, prefix = '') {
|
||||
|
||||
export async function runCompletion({stage, inputDir, outDir, partIds = [], prepareOnly = false,
|
||||
candidatesDir, completionSize = 1024, extensionRadius = 32, python: requestedPython,
|
||||
resume = false, dryRun = false}, transport) {
|
||||
resume = false, dryRun = false, candidateSizePolicy = 'strict'}, transport) {
|
||||
if (!inputDir) throw new Error('部件补全需要 --input-dir 指向 SAM 输出目录');
|
||||
if (!outDir && !dryRun) throw new Error('部件补全需要独立的 --out-dir');
|
||||
if (prepareOnly && candidatesDir) throw new Error('--prepare-only 与 --candidates-dir 不能同时使用');
|
||||
const size = Number(completionSize), radius = Number(extensionRadius);
|
||||
if (!['strict', 'uniform'].includes(candidateSizePolicy)) throw new Error('--candidate-size-policy 仅支持 strict / uniform');
|
||||
if (!Number.isInteger(size) || size < 128 || size > 4096 || !Number.isInteger(radius) || radius < 1 || radius > 256) {
|
||||
throw new Error('--completion-size 必须为 128~4096 的整数;--extension-radius 必须为 1~256 的整数');
|
||||
}
|
||||
@@ -47,15 +48,24 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
|
||||
const inputs = Object.fromEntries(inputFiles.map((file) => [file, sha256(readFileSync(path.join(input, file)))]));
|
||||
const imported = {};
|
||||
if (candidatesDir) {
|
||||
const priorManifest = path.resolve(candidatesDir, 'manifest.json');
|
||||
const previousRun = existsSync(priorManifest) ? readJson(priorManifest) : null;
|
||||
if (previousRun && (previousRun.output_kind !== 'bounded_part_completion' || previousRun.completion_size !== size || previousRun.extension_radius !== radius ||
|
||||
Object.entries(inputs).some(([file, hash]) => previousRun.inputs?.[file] !== hash))) {
|
||||
throw new Error('旧补全目录的输入或画布/延展参数不一致,不能复用其候选');
|
||||
}
|
||||
for (const p of plan.parts.filter((p) => p.needs_completion && (!partIds.length || partIds.includes(p.id)))) {
|
||||
const file = path.resolve(candidatesDir, `${p.id}.png`);
|
||||
const file = previousRun
|
||||
? ['png', 'jpg', 'webp'].map((ext) => path.resolve(candidatesDir, 'tasks', p.id, `candidate-1.${ext}`)).find(existsSync)
|
||||
: path.resolve(candidatesDir, `${p.id}.png`);
|
||||
if (!file) continue;
|
||||
if (existsSync(file)) imported[p.id] = {file, sha256: sha256(readFileSync(file))};
|
||||
}
|
||||
}
|
||||
const python = selectPython(requestedPython);
|
||||
if (dryRun) {
|
||||
console.log(JSON.stringify({stage: 'completion', input, width: source.width, height: source.height,
|
||||
selected_parts: partIds, completion_size: size, extension_radius: radius,
|
||||
selected_parts: partIds, completion_size: size, extension_radius: radius, candidate_size_policy: candidateSizePolicy,
|
||||
mode: prepareOnly ? 'prepare' : candidatesDir ? 'offline_candidates' : 'image_edit', network_requested: false}, null, 2));
|
||||
return {dryRun: true};
|
||||
}
|
||||
@@ -65,7 +75,7 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
|
||||
const identity = {schema_version: 1, stage: 'completion', output_kind: 'bounded_part_completion',
|
||||
mode: prepareOnly ? 'prepare' : candidatesDir ? 'offline_candidates' : 'image_edit',
|
||||
resolved_stage: publicStage(stage), endpoint_sha256: sha256(stage.baseUrl), prompt_sha256: sha256(stage.prompt),
|
||||
inputs, part_ids: partIds, completion_size: size, extension_radius: radius,
|
||||
inputs, part_ids: partIds, completion_size: size, extension_radius: radius, candidate_size_policy: candidateSizePolicy,
|
||||
imported_candidates: Object.fromEntries(Object.entries(imported).map(([id, value]) => [id, value.sha256])),
|
||||
code_sha256: sha256(codeFiles.map((file) => `${file}\n${readFileSync(path.join(projectRoot, file), 'utf8')}`).join('\n'))};
|
||||
const fingerprint = sha256(JSON.stringify(identity));
|
||||
@@ -75,6 +85,11 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
|
||||
jsonFile(run.manifestPath, manifest, stage.apiKey);
|
||||
const result = {schema_version: 1, quality_status: 'needs_review', parts: [], skipped: []};
|
||||
try {
|
||||
const requestSize = `${size}x${size}`;
|
||||
if (!prepareOnly && !candidatesDir && stage.params.size !== undefined && stage.params.size !== requestSize) {
|
||||
throw new Error(`completion.params.size 必须与局部画布一致:${requestSize}`);
|
||||
}
|
||||
const editStage = {...stage, params: {...stage.params, size: requestSize}};
|
||||
for (const file of inputFiles) {
|
||||
mkdirSync(path.dirname(path.join(run.out, file)), {recursive: true});
|
||||
copyFileSync(path.join(input, file), path.join(run.out, file));
|
||||
@@ -94,17 +109,17 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
|
||||
let outputs;
|
||||
if (candidatesDir) {
|
||||
if (!imported[task.id]) { record.status = 'missing_candidate'; continue; }
|
||||
outputs = [{bytes: readFileSync(imported[task.id].file), extension: 'png'}];
|
||||
outputs = [{bytes: readFileSync(imported[task.id].file), extension: path.extname(imported[task.id].file).slice(1)}];
|
||||
} else {
|
||||
const images = [readImage(path.join(folder, 'input.png'), {pngOnly: true})];
|
||||
if (stage.capabilities.multiple_images) images.push(readImage(path.join(folder, 'context.png'), {pngOnly: true}));
|
||||
const mask = stage.capabilities.mask ? readImage(path.join(folder, 'edit-mask.png'), {pngOnly: true}) : null;
|
||||
jsonFile(path.join(folder, 'request.json'), {model: stage.model, images: images.map((im) => im.sha256),
|
||||
jsonFile(path.join(folder, 'request.json'), {model: stage.model, size: requestSize, images: images.map((im) => im.sha256),
|
||||
edit_mask_sent: Boolean(mask), allowed_mask_sha256: sha256(readFileSync(path.join(folder, 'allowed-mask.png')))}, stage.apiKey);
|
||||
manifest.network_requested = true;
|
||||
jsonFile(run.manifestPath, manifest, stage.apiKey);
|
||||
try {
|
||||
const {payload, metadata} = await editImage(stage, {images, mask, prompt: fullPrompt}, transport);
|
||||
const {payload, metadata} = await editImage(editStage, {images, mask, prompt: fullPrompt}, transport);
|
||||
jsonFile(path.join(folder, 'response.raw.json'), payload, stage.apiKey);
|
||||
jsonFile(path.join(folder, 'request-metadata.json'), metadata, stage.apiKey);
|
||||
outputs = await decodeEditedImages(payload, stage, transport);
|
||||
@@ -119,7 +134,7 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
|
||||
const filename = `candidate-${i + 1}.${output.extension}`;
|
||||
writeFileSync(path.join(folder, filename), output.bytes);
|
||||
const merged = path.join(folder, `merged-${i + 1}.png`), qaFile = path.join(folder, `qa-${i + 1}.json`);
|
||||
pythonCall(python, ['merge', '--out', folder, '--candidate', path.join(folder, filename), '--merged', merged, '--qa', qaFile]);
|
||||
pythonCall(python, ['merge', '--out', folder, '--candidate', path.join(folder, filename), '--merged', merged, '--qa', qaFile, '--candidate-size-policy', candidateSizePolicy]);
|
||||
const qa = readJson(qaFile);
|
||||
record.candidates.push({file: `tasks/${task.id}/${filename}`, qa});
|
||||
if (qa.accepted && !record.selected_file) {
|
||||
@@ -128,7 +143,8 @@ export async function runCompletion({stage, inputDir, outDir, partIds = [], prep
|
||||
}
|
||||
}
|
||||
record.status = record.selected_file ? 'geometry_passed_needs_review' : 'rejected';
|
||||
if (!record.qa) record.qa = {accepted: false, reasons: record.candidates.flatMap((c) => c.qa.reasons)};
|
||||
if (!record.qa) record.qa = {...record.candidates[0]?.qa, accepted: false,
|
||||
reasons: [...new Set(record.candidates.flatMap((c) => c.qa.reasons))]};
|
||||
jsonFile(path.join(run.out, 'completion-result.json'), result, stage.apiKey);
|
||||
}
|
||||
manifest.quality_status = prepareOnly ? 'prepared_needs_review' : 'needs_review';
|
||||
|
||||
@@ -36,6 +36,7 @@ test('part completion prepares without HTTP, sends declared capabilities, merges
|
||||
const transport = {fetchImpl: async (_url, request) => {
|
||||
requests++;
|
||||
assert.equal(request.body.get('model'), stage.model);
|
||||
assert.equal(request.body.get('size'), '128x128');
|
||||
assert.equal(request.body.getAll('image').length, 1);
|
||||
assert.equal(request.body.has('mask'), false);
|
||||
return new Response(JSON.stringify({data: [{b64_json: readFileSync(path.join(input, 'candidate.png')).toString('base64')}]}), {headers: {'Content-Type': 'application/json'}});
|
||||
@@ -50,6 +51,11 @@ test('part completion prepares without HTTP, sends declared capabilities, merges
|
||||
assert.deepEqual(result.parts[0].crop_xyxy, [20, 20, 50, 40]);
|
||||
assert.equal((await runCompletion({...opts, resume: true}, transport)).reused, true);
|
||||
assert.equal(requests, 1);
|
||||
const recovered = await runCompletion({...opts, outDir: path.join(root, 'recovered'), candidatesDir: opts.outDir, candidateSizePolicy: 'uniform'}, {fetchImpl: () => { throw new Error('Must remain offline'); }});
|
||||
assert.equal(recovered.accepted, 1);
|
||||
await assert.rejects(runCompletion({...opts, outDir: path.join(root, 'wrong-canvas'), candidatesDir: opts.outDir, completionSize: 256}, transport), /画布/u);
|
||||
await assert.rejects(runCompletion({...opts, outDir: path.join(root, 'wrong-request'), stage: {...stage, params: {...stage.params, size: 'auto'}}}, transport), /size 必须/u);
|
||||
assert.equal(requests, 1);
|
||||
writeFileSync(path.join(input, 'masks', 'torso.png'), readFileSync(path.join(input, 'masks', 'collar.png')));
|
||||
await assert.rejects(runCompletion({...opts, resume: true}, transport), /变化/u);
|
||||
assert.equal(requests, 1);
|
||||
@@ -65,6 +71,8 @@ test('offline candidate import needs no HTTP; mismatched candidate is retained a
|
||||
assert.equal(result.accepted, 0);
|
||||
const data = JSON.parse(readFileSync(path.join(outDir, 'completion-result.json')));
|
||||
assert.deepEqual(data.parts[0].qa.reasons, ['CANVAS_SIZE_MISMATCH']);
|
||||
assert.deepEqual(data.parts[0].qa.expected_size, [128, 128]);
|
||||
assert.deepEqual(data.parts[0].qa.returned_size, [80, 80]);
|
||||
assert.ok(readFileSync(path.join(outDir, 'tasks', 'torso', 'candidate-1.png')).length);
|
||||
});
|
||||
|
||||
|
||||
@@ -106,6 +106,30 @@ class CompletionTests(unittest.TestCase):
|
||||
self.assertTrue(qa['accepted'])
|
||||
self.assertEqual(qa['new_pixels'], 200)
|
||||
|
||||
def test_uniform_canvas_recovery_preserves_source_and_still_rejects_changes(self):
|
||||
original_candidate = Image.open(self.root/'candidate.png').convert('RGBA')
|
||||
larger = original_candidate.resize((256, 256), Image.Resampling.NEAREST)
|
||||
file = self.root/'large.png'
|
||||
larger.save(file)
|
||||
output = self.folder/'recovered.png'
|
||||
qa = merge_candidate(self.folder, file, output, 'uniform')
|
||||
self.assertTrue(qa['accepted'])
|
||||
self.assertTrue(qa['canvas_resampled'])
|
||||
self.assertEqual(qa['returned_size'], [256, 256])
|
||||
self.assertEqual(qa['canvas_scale'], .5)
|
||||
merged = np.array(Image.open(output))
|
||||
np.testing.assert_array_equal(merged[54:74, 49:69], self.source[20:40, 20:40])
|
||||
self.assertEqual(Image.open(file).size, (256, 256))
|
||||
for name, im, reason in [
|
||||
('wide', larger.resize((260, 256)), 'CANVAS_ASPECT_RATIO_MISMATCH'),
|
||||
('shifted', Image.fromarray(np.roll(np.array(larger), 32, axis=1)), 'VISIBLE_ALIGNMENT_MISMATCH'),
|
||||
]:
|
||||
file = self.root/f'{name}.png'
|
||||
im.save(file)
|
||||
qa = merge_candidate(self.folder, file, self.folder/f'{name}-merged.png', 'uniform')
|
||||
self.assertFalse(qa['accepted'])
|
||||
self.assertIn(reason, qa['reasons'])
|
||||
|
||||
def test_disconnected_additions_are_not_merged(self):
|
||||
candidate = np.array(Image.open(self.folder/'visible.png'))
|
||||
candidate[54:57, 76:79] = [180, 50, 20, 255]
|
||||
|
||||
@@ -30,7 +30,8 @@ const help = `模型接入与结构理解(新增模式,不调用旧拆件流
|
||||
--input-dir DIR SAM 阶段读理解目录;补全阶段读 SAM 目录
|
||||
--part ID 补全部件 ID,可重复;默认处理所有需补全部件
|
||||
--prepare-only 仅准备补全画布、掩码和提示词,不请求模型
|
||||
--candidates-dir DIR 离线导入 ID.png 补全候选,不请求模型
|
||||
--candidates-dir DIR 离线导入 ID.png,或旧补全目录的首张候选,不请求模型
|
||||
--candidate-size-policy NAME strict(默认)/ uniform(同宽高比缩放后重新检查对齐)
|
||||
--completion-size INT 补全方形画布边长,默认 1024,不缩放原像素
|
||||
--extension-radius INT 补全区域距可见轮廓的最大像素距离,默认 32
|
||||
--sam-dir DIR 本地 SAM 根目录,包含 sam3 与 sam3.1checkpoint
|
||||
@@ -56,6 +57,7 @@ export function parseOptions(argv) {
|
||||
['--input-dir', 'inputDir'], ['--sam-dir', 'samDir'], ['--sam-code-dir', 'samCodeDir'],
|
||||
['--sam-checkpoint', 'samCheckpoint'], ['--sam-confidence', 'samConfidence'], ['--sam-precision', 'samPrecision'],
|
||||
['--candidates-dir', 'candidatesDir'], ['--completion-size', 'completionSize'], ['--extension-radius', 'extensionRadius'],
|
||||
['--candidate-size-policy', 'candidateSizePolicy'],
|
||||
]);
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const flag = argv[i];
|
||||
@@ -88,7 +90,7 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
}
|
||||
const loaded = loadConfig(options);
|
||||
const partCompletion = options.stage === 'completion' && options.inputDir;
|
||||
if (!partCompletion && (options.prepareOnly || options.candidatesDir || options.partIds.length || options.completionSize || options.extensionRadius)) {
|
||||
if (!partCompletion && (options.prepareOnly || options.candidatesDir || options.partIds.length || options.completionSize || options.extensionRadius || options.candidateSizePolicy)) {
|
||||
throw new Error('部件补全参数需要 --stage completion --input-dir');
|
||||
}
|
||||
if (partCompletion) {
|
||||
|
||||
Reference in New Issue
Block a user