card-recog-demo/tool/hashgen.dart

119 lines
3.8 KiB
Dart
Raw Normal View History

import 'dart:convert';
import 'dart:io';
import 'package:carddex_demo/native/phash_loader.dart';
import 'package:carddex_demo/services/hash_library.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as p;
const _imageExts = {'.jpg', '.jpeg', '.png', '.webp', '.bmp'};
/// Same pipeline as CardCropper.cropAndCorrect so library hashes match
/// the same conditions that query hashes are computed under.
const _cropW = 280;
const _cropH = 390;
const _borderTrim = 10;
Future<void> main(List<String> args) async {
final root = Directory.current.path;
final assetsDir = Directory(p.join(root, 'test_assets'));
final outHashes = p.join(root, 'data', 'hashes.json');
final outCatalog = p.join(root, 'data', 'catalog.json');
final libPath = _resolveLib(root, args);
if (!assetsDir.existsSync()) {
stderr.writeln('test_assets/ not found at ${assetsDir.path}');
exit(1);
}
final native = PhashNative(hostLibraryPath: libPath);
final files = assetsDir
.listSync()
.whereType<File>()
.where((f) => _imageExts.contains(p.extension(f.path).toLowerCase()))
.toList()
..sort((a, b) => p.basename(a.path).compareTo(p.basename(b.path)));
stdout.writeln('hashgen — ${files.length} images from ${assetsDir.path}');
stdout.writeln('libphash — $libPath');
final catalogItems = <Map<String, String>>[];
final hashItems = <HashEntry>[];
for (final file in files) {
final filename = p.basename(file.path);
final cardId = p.basenameWithoutExtension(filename);
try {
final raw = await file.readAsBytes();
var decoded = img.decodeImage(raw);
if (decoded == null) {
stderr.writeln('skip $cardId: decode failed');
continue;
}
decoded = img.bakeOrientation(decoded);
// Match the App pipeline: resize → trim border → re-encode JPEG q=98.
var resized = img.copyResize(decoded,
width: _cropW, height: _cropH,
interpolation: img.Interpolation.cubic);
resized = img.copyCrop(resized,
x: _borderTrim,
y: _borderTrim,
width: _cropW - _borderTrim * 2,
height: _cropH - _borderTrim * 2);
final hashedBytes = img.encodeJpg(resized, quality: 98);
final hash = native.hashFromBytes(hashedBytes);
hashItems.add(HashEntry(cardId: cardId, hash: hash, filename: filename));
catalogItems.add({
'card_id': cardId,
'name': cardId,
'image': 'test_assets/$filename',
'filename': filename,
});
stdout.writeln(' $cardId');
} catch (e) {
stderr.writeln('skip $cardId: $e');
}
}
final library = HashLibrary(
version: 'assets-v1',
algorithm: 'rgb-phash-v1',
hashBits: 105,
items: hashItems,
);
await library.saveFile(outHashes);
final catalog = {
'version': 'assets-v1',
'algorithm': 'rgb-phash-v1',
'items': catalogItems,
};
final catalogFile = File(outCatalog);
await catalogFile.parent.create(recursive: true);
await catalogFile.writeAsString(
'${const JsonEncoder.withIndent(' ').convert(catalog)}\n',
);
stdout.writeln('saved ${hashItems.length} hashes → $outHashes');
stdout.writeln('catalog → $outCatalog');
}
String _resolveLib(String root, List<String> args) {
for (var i = 0; i < args.length; i++) {
if (args[i] == '--lib' && i + 1 < args.length) {
return p.normalize(p.absolute(args[i + 1]));
}
}
final dylib = p.join(root, 'native', 'phash', 'build', 'libphash.dylib');
final so = p.join(root, 'native', 'phash', 'build', 'libphash.so');
if (File(dylib).existsSync()) return dylib;
if (File(so).existsSync()) return so;
stderr.writeln(
'libphash not found. Build first:\n'
' cmake -S native/phash -B native/phash/build -DPHASH_BUILD_SHARED=ON\n'
' cmake --build native/phash/build',
);
exit(1);
}