第二步SAM模型接入
This commit is contained in:
@@ -267,7 +267,7 @@ test('pipeline new mode reaches a mock relay with geometry; model listing and un
|
||||
const list = await exec(process.execPath, ['tools/model-check.mjs', '--list-models'], {cwd: projectRoot, env});
|
||||
assert.match(list.stdout, /relay-vision-test/u);
|
||||
assert.equal(requests, 2);
|
||||
await assert.rejects(exec(process.execPath, ['pipeline.mjs', '--mode', 'annotated-parts', '--stop-after', 'segment'], {cwd: projectRoot, env}), (error) => /仅支持 understand/u.test(error.stderr));
|
||||
await assert.rejects(exec(process.execPath, ['pipeline.mjs', '--mode', 'annotated-parts', '--stop-after', 'export'], {cwd: projectRoot, env}), (error) => /仅支持 understand/u.test(error.stderr));
|
||||
assert.equal(requests, 2);
|
||||
});
|
||||
|
||||
@@ -290,3 +290,15 @@ test('Python geometry regression suite', async () => {
|
||||
const result = await exec(geometryPython(), ['-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_geometry.py'], {cwd: projectRoot});
|
||||
assert.match(result.stderr, /OK/u);
|
||||
});
|
||||
|
||||
test('Python segmentation QA, prompt integrity and rendering regressions (no model/GPU needed)', async () => {
|
||||
const result = await exec(geometryPython(), ['-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_segmentation.py'], {cwd: projectRoot});
|
||||
assert.match(result.stderr, /OK/u);
|
||||
});
|
||||
|
||||
test('SAM stage validates local input without requiring relay credentials', async (t) => {
|
||||
const dir = temporary(t);
|
||||
const envFile = path.join(dir, 'empty.env'); writeFileSync(envFile, '');
|
||||
const env = {...process.env, VECTOR_ENGINE_BASE_URL: '', VECTOR_ENGINE_API_KEY: ''};
|
||||
await assert.rejects(exec(process.execPath, ['tools/model-check.mjs', '--env-file', envFile, '--stage', 'segmentation', '--out-dir', path.join(dir, 'result')], {cwd: projectRoot, env}), (error) => /--input-dir/u.test(error.stderr));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from sam_segment import choose_candidate, validate_prompts
|
||||
from segmentation_outputs import assess_candidate, render_outputs, write_report
|
||||
|
||||
|
||||
class SegmentationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.rgba = np.zeros((24, 32, 4), dtype=np.uint8)
|
||||
self.rgba[4:20, 5:27] = [120, 50, 200, 255]
|
||||
self.rgba[5, 6, 3] = 80
|
||||
self.image = Image.fromarray(self.rgba)
|
||||
self.mask = self.rgba[:, :, 3] > 0
|
||||
|
||||
def test_background_leak_measured_before_clipping(self):
|
||||
raw = np.ones((24, 32), dtype=bool)
|
||||
qa = assess_candidate(raw, self.rgba[:, :, 3], [5, 4, 27, 20], [[6, 5]])
|
||||
self.assertGreater(qa['outside_alpha_ratio'], .5)
|
||||
self.assertEqual(qa['seed_hit_fraction'], 1)
|
||||
self.assertEqual(qa['area_px'], 16 * 22)
|
||||
|
||||
def test_rank_prefers_local_mask_and_never_invents_empty_mask(self):
|
||||
wrong = self.mask.copy()
|
||||
local = np.zeros_like(wrong)
|
||||
local[8:13, 8:13] = True
|
||||
best, _ = choose_candidate([wrong, local], [.95, .7], self.rgba[:, :, 3], [8, 8, 13, 13], [[10, 10]])
|
||||
self.assertEqual(best['index'], 1)
|
||||
best, _ = choose_candidate([np.zeros_like(local)], [.99], self.rgba[:, :, 3], [8, 8, 13, 13], [[10, 10]])
|
||||
self.assertIsNone(best)
|
||||
|
||||
def test_prompts_reject_transparent_points_mismatched_canvas_and_duplicate_ids(self):
|
||||
plan = {'parts': [{'id': 'hand', 'visibility': 'visible'}]}
|
||||
prompts = {'width': 32, 'height': 24, 'parts': [{'id': 'hand', 'status': 'candidate',
|
||||
'seed_source': 'alpha_roi_distance_transform', 'bbox_px_xyxy': [0, 0, 32, 24],
|
||||
'positive_points_px': [[10, 10]]}]}
|
||||
validate_prompts(plan, prompts, self.image)
|
||||
for variant in range(3):
|
||||
bad = copy.deepcopy(prompts)
|
||||
if variant == 0:
|
||||
bad['parts'][0]['positive_points_px'] = [[0, 0]]
|
||||
elif variant == 1:
|
||||
bad['width'] = 99
|
||||
else:
|
||||
bad['parts'].append(copy.deepcopy(bad['parts'][0]))
|
||||
with self.assertRaises(ValueError):
|
||||
validate_prompts(plan, bad, self.image)
|
||||
|
||||
def test_render_preserves_cutout_pixels_alpha_and_measures_overlap(self):
|
||||
parts = [{'id': 'hand', 'semantic': 'hand', 'mask': self.mask, 'model_score': .9},
|
||||
{'id': 'forearm', 'semantic': 'forearm', 'mask': self.mask, 'model_score': .8}]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = render_outputs(self.image, parts, directory)
|
||||
self.assertEqual(result['coverage']['coverage_ratio'], 1)
|
||||
self.assertEqual(result['coverage']['overlap_pixels'], int(self.mask.sum()))
|
||||
self.assertEqual(result['near_duplicate_masks'][0]['iou'], 1)
|
||||
self.assertIn('NEAR_DUPLICATE_MASK', result['parts'][0]['warnings'])
|
||||
cut = np.array(Image.open(Path(directory) / 'parts/hand.png'))
|
||||
np.testing.assert_array_equal(cut, self.rgba[4:20, 5:27])
|
||||
self.assertEqual(Image.open(Path(directory) / 'annotated.png').size, self.image.size)
|
||||
sheet = np.array(Image.open(Path(directory) / 'parts-sheet.png'))
|
||||
self.assertTrue(np.all(sheet[0, 0] == 255))
|
||||
a, b = [p['sheet_xyxy'] for p in result['parts']]
|
||||
self.assertTrue(a[2] <= b[0] or b[2] <= a[0] or a[3] <= b[1] or b[3] <= a[1])
|
||||
result['skipped'] = []
|
||||
write_report(directory, result)
|
||||
self.assertTrue((Path(directory) / 'report.html').exists())
|
||||
json.dumps(result)
|
||||
|
||||
def test_renderer_rejects_empty_masks_and_unsafe_ids(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for name, mask in [('hand', np.zeros_like(self.mask)), ('../bad', self.mask)]:
|
||||
with self.assertRaises(ValueError):
|
||||
render_outputs(self.image, [{'id': name, 'mask': mask}], directory)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user