Add style controls, presets, and edge-aware variation quantization
This commit is contained in:
66
README.md
66
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Prototype Flutter app for generating bottle-cap mosaics from imported images.
|
Prototype Flutter app for generating bottle-cap mosaics from imported images.
|
||||||
|
|
||||||
## Implemented MVP
|
## Features
|
||||||
|
|
||||||
- Import target image from gallery (`image_picker`)
|
- Import target image from gallery (`image_picker`)
|
||||||
- Resolution controls:
|
- Resolution controls:
|
||||||
@@ -12,46 +12,50 @@ Prototype Flutter app for generating bottle-cap mosaics from imported images.
|
|||||||
- list caps with name + color
|
- list caps with name + color
|
||||||
- add color via picker and/or manual hex
|
- add color via picker and/or manual hex
|
||||||
- remove caps
|
- remove caps
|
||||||
- Mosaic generation:
|
- Mosaic preview + bill of materials counts per cap color
|
||||||
- resize source to grid
|
|
||||||
- nearest cap color match using CIELAB + DeltaE (CIE76)
|
|
||||||
- fallback concept is RGB distance, but LAB path is implemented directly
|
|
||||||
- Output:
|
|
||||||
- mosaic grid preview
|
|
||||||
- bill of materials counts per cap color
|
|
||||||
|
|
||||||
## Current blocker on this machine
|
## Style controls (new)
|
||||||
|
|
||||||
`flutter` SDK is not installed (`flutter: command not found`), so I could not run:
|
User-facing controls are integrated directly in the main screen:
|
||||||
|
|
||||||
- `flutter create`
|
- **Fidelity ↔ Structure** slider
|
||||||
- `flutter pub get`
|
- fidelity side keeps direct color faithfulness
|
||||||
- `flutter analyze`
|
- structure side applies edge-aware smoothing for cleaner large forms
|
||||||
- `flutter build apk --debug`
|
- **Dithering strength** slider
|
||||||
|
- scales Floyd–Steinberg error diffusion
|
||||||
|
- **Edge emphasis** slider
|
||||||
|
- boosts edge readability during color assignment
|
||||||
|
- **Color tolerance / variation** slider
|
||||||
|
- allows controlled variation among similar cap colors in flatter regions
|
||||||
|
|
||||||
## Setup commands (Ubuntu/Debian)
|
## Presets
|
||||||
|
|
||||||
|
Three presets are provided and selectable via segmented buttons:
|
||||||
|
|
||||||
|
- **Realistisch**
|
||||||
|
- **Ausgewogen**
|
||||||
|
- **Künstlerisch**
|
||||||
|
|
||||||
|
Selecting a preset sets all four style controls at once.
|
||||||
|
|
||||||
|
## Pipeline notes
|
||||||
|
|
||||||
|
The generation pipeline still uses fast CIELAB (`DeltaE CIE76`) nearest-color matching,
|
||||||
|
with precomputed palette LAB values, LAB caching, and low-cost edge-aware processing to keep performance responsive.
|
||||||
|
|
||||||
|
## Build (arm64 split debug)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/yadciel/.openclaw/workspace
|
|
||||||
sudo snap install flutter --classic
|
|
||||||
# OR: install manually from flutter.dev and add to PATH
|
|
||||||
|
|
||||||
flutter doctor
|
|
||||||
|
|
||||||
cd /home/yadciel/.openclaw/workspace/korken_mosaic
|
cd /home/yadciel/.openclaw/workspace/korken_mosaic
|
||||||
flutter create .
|
|
||||||
flutter pub get
|
flutter pub get
|
||||||
flutter run
|
flutter build apk --debug --split-per-abi
|
||||||
flutter build apk --debug
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected APK artifact:
|
Expected arm64 artifact:
|
||||||
|
|
||||||
`build/app/outputs/flutter-apk/app-debug.apk`
|
`build/app/outputs/flutter-apk/app-arm64-v8a-debug.apk`
|
||||||
|
|
||||||
## Project files
|
## Main files
|
||||||
|
|
||||||
- `lib/main.dart` – complete MVP UI + mosaic logic
|
- `lib/main.dart` – UI + mosaic logic
|
||||||
- `pubspec.yaml` – dependencies
|
- `README.md` – overview and build instructions
|
||||||
- `analysis_options.yaml`
|
|
||||||
- `.gitignore`
|
|
||||||
|
|||||||
342
lib/main.dart
342
lib/main.dart
@@ -25,6 +25,8 @@ class KorkenMosaicApp extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum StylePreset { realistisch, ausgewogen, kuenstlerisch }
|
||||||
|
|
||||||
class MosaicHomePage extends StatefulWidget {
|
class MosaicHomePage extends StatefulWidget {
|
||||||
const MosaicHomePage({super.key});
|
const MosaicHomePage({super.key});
|
||||||
|
|
||||||
@@ -47,6 +49,12 @@ class _MosaicHomePageState extends State<MosaicHomePage> {
|
|||||||
Timer? _debounceTimer;
|
Timer? _debounceTimer;
|
||||||
int _generationToken = 0;
|
int _generationToken = 0;
|
||||||
|
|
||||||
|
double _fidelityStructure = 0.5;
|
||||||
|
double _ditheringStrength = 0.35;
|
||||||
|
double _edgeEmphasis = 0.4;
|
||||||
|
double _colorVariation = 0.3;
|
||||||
|
StylePreset _selectedPreset = StylePreset.ausgewogen;
|
||||||
|
|
||||||
final List<CapColor> _palette = [
|
final List<CapColor> _palette = [
|
||||||
CapColor(name: 'White', color: const Color(0xFFF2F2F2)),
|
CapColor(name: 'White', color: const Color(0xFFF2F2F2)),
|
||||||
CapColor(name: 'Black', color: const Color(0xFF222222)),
|
CapColor(name: 'Black', color: const Color(0xFF222222)),
|
||||||
@@ -85,7 +93,41 @@ class _MosaicHomePageState extends State<MosaicHomePage> {
|
|||||||
void _scheduleRegenerate() {
|
void _scheduleRegenerate() {
|
||||||
if (_sourceImageBytes == null || _result == null) return;
|
if (_sourceImageBytes == null || _result == null) return;
|
||||||
_debounceTimer?.cancel();
|
_debounceTimer?.cancel();
|
||||||
_debounceTimer = Timer(const Duration(milliseconds: 300), _generate);
|
_debounceTimer = Timer(const Duration(milliseconds: 250), _generate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyPreset(StylePreset preset) {
|
||||||
|
setState(() {
|
||||||
|
_selectedPreset = preset;
|
||||||
|
switch (preset) {
|
||||||
|
case StylePreset.realistisch:
|
||||||
|
_fidelityStructure = 0.2;
|
||||||
|
_ditheringStrength = 0.15;
|
||||||
|
_edgeEmphasis = 0.25;
|
||||||
|
_colorVariation = 0.1;
|
||||||
|
break;
|
||||||
|
case StylePreset.ausgewogen:
|
||||||
|
_fidelityStructure = 0.5;
|
||||||
|
_ditheringStrength = 0.35;
|
||||||
|
_edgeEmphasis = 0.4;
|
||||||
|
_colorVariation = 0.3;
|
||||||
|
break;
|
||||||
|
case StylePreset.kuenstlerisch:
|
||||||
|
_fidelityStructure = 0.82;
|
||||||
|
_ditheringStrength = 0.75;
|
||||||
|
_edgeEmphasis = 0.75;
|
||||||
|
_colorVariation = 0.78;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_scheduleRegenerate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStyleChanged() {
|
||||||
|
if (_selectedPreset != StylePreset.ausgewogen) {
|
||||||
|
setState(() => _selectedPreset = StylePreset.ausgewogen);
|
||||||
|
}
|
||||||
|
_scheduleRegenerate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addCapDialog() {
|
void _addCapDialog() {
|
||||||
@@ -172,6 +214,10 @@ class _MosaicHomePageState extends State<MosaicHomePage> {
|
|||||||
'gridW': gridW,
|
'gridW': gridW,
|
||||||
'gridH': gridH,
|
'gridH': gridH,
|
||||||
'capSize': capSize,
|
'capSize': capSize,
|
||||||
|
'fidelityStructure': _fidelityStructure,
|
||||||
|
'ditheringStrength': _ditheringStrength,
|
||||||
|
'edgeEmphasis': _edgeEmphasis,
|
||||||
|
'colorVariation': _colorVariation,
|
||||||
'palette': _palette
|
'palette': _palette
|
||||||
.map((p) =>
|
.map((p) =>
|
||||||
<String, dynamic>{'name': p.name, 'value': p.color.toARGB32()})
|
<String, dynamic>{'name': p.name, 'value': p.color.toARGB32()})
|
||||||
@@ -287,6 +333,81 @@ class _MosaicHomePageState extends State<MosaicHomePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'Style Preset',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SegmentedButton<StylePreset>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
value: StylePreset.realistisch,
|
||||||
|
label: Text('Realistisch'),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: StylePreset.ausgewogen,
|
||||||
|
label: Text('Ausgewogen'),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: StylePreset.kuenstlerisch,
|
||||||
|
label: Text('Künstlerisch'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {_selectedPreset},
|
||||||
|
onSelectionChanged: (s) => _applyPreset(s.first),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_SliderRow(
|
||||||
|
label: 'Fidelity ↔ Structure',
|
||||||
|
leftLabel: 'Fidelity',
|
||||||
|
rightLabel: 'Structure',
|
||||||
|
value: _fidelityStructure,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() => _fidelityStructure = v);
|
||||||
|
_onStyleChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_SliderRow(
|
||||||
|
label: 'Dithering strength',
|
||||||
|
leftLabel: 'Off',
|
||||||
|
rightLabel: 'Strong',
|
||||||
|
value: _ditheringStrength,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() => _ditheringStrength = v);
|
||||||
|
_onStyleChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_SliderRow(
|
||||||
|
label: 'Edge emphasis',
|
||||||
|
leftLabel: 'Soft',
|
||||||
|
rightLabel: 'Crisp',
|
||||||
|
value: _edgeEmphasis,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() => _edgeEmphasis = v);
|
||||||
|
_onStyleChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_SliderRow(
|
||||||
|
label: 'Color tolerance / variation',
|
||||||
|
leftLabel: 'Strict',
|
||||||
|
rightLabel: 'Varied',
|
||||||
|
value: _colorVariation,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() => _colorVariation = v);
|
||||||
|
_onStyleChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Expanded(
|
const Expanded(
|
||||||
@@ -366,6 +487,43 @@ class _MosaicHomePageState extends State<MosaicHomePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SliderRow extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String leftLabel;
|
||||||
|
final String rightLabel;
|
||||||
|
final double value;
|
||||||
|
final ValueChanged<double> onChanged;
|
||||||
|
|
||||||
|
const _SliderRow({
|
||||||
|
required this.label,
|
||||||
|
required this.leftLabel,
|
||||||
|
required this.rightLabel,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
Slider(value: value, onChanged: onChanged),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(leftLabel, style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
const Spacer(),
|
||||||
|
Text(rightLabel, style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CapColor {
|
class CapColor {
|
||||||
final String name;
|
final String name;
|
||||||
final Color color;
|
final Color color;
|
||||||
@@ -399,6 +557,10 @@ Map<String, dynamic> _generateMosaicIsolate(Map<String, dynamic> request) {
|
|||||||
final defaultGridW = request['gridW'] as int;
|
final defaultGridW = request['gridW'] as int;
|
||||||
final defaultGridH = request['gridH'] as int;
|
final defaultGridH = request['gridH'] as int;
|
||||||
final capSize = request['capSize'] as int;
|
final capSize = request['capSize'] as int;
|
||||||
|
final fidelityStructure = (request['fidelityStructure'] as num).toDouble();
|
||||||
|
final ditheringStrength = (request['ditheringStrength'] as num).toDouble();
|
||||||
|
final edgeEmphasis = (request['edgeEmphasis'] as num).toDouble();
|
||||||
|
final colorVariation = (request['colorVariation'] as num).toDouble();
|
||||||
final paletteRaw = (request['palette'] as List).cast<Map>();
|
final paletteRaw = (request['palette'] as List).cast<Map>();
|
||||||
|
|
||||||
final decoded = img.decodeImage(source);
|
final decoded = img.decodeImage(source);
|
||||||
@@ -422,49 +584,188 @@ Map<String, dynamic> _generateMosaicIsolate(Map<String, dynamic> request) {
|
|||||||
? math.max(1, (decoded.height / capSize).round())
|
? math.max(1, (decoded.height / capSize).round())
|
||||||
: defaultGridH;
|
: defaultGridH;
|
||||||
|
|
||||||
|
final interpolation = fidelityStructure < 0.4
|
||||||
|
? img.Interpolation.average
|
||||||
|
: img.Interpolation.linear;
|
||||||
final scaled = img.copyResize(
|
final scaled = img.copyResize(
|
||||||
decoded,
|
decoded,
|
||||||
width: gridW,
|
width: gridW,
|
||||||
height: gridH,
|
height: gridH,
|
||||||
interpolation: img.Interpolation.average,
|
interpolation: interpolation,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final pixelCount = gridW * gridH;
|
||||||
|
final workingR = List<double>.filled(pixelCount, 0);
|
||||||
|
final workingG = List<double>.filled(pixelCount, 0);
|
||||||
|
final workingB = List<double>.filled(pixelCount, 0);
|
||||||
|
final luminance = List<double>.filled(pixelCount, 0);
|
||||||
|
|
||||||
|
for (int y = 0; y < gridH; y++) {
|
||||||
|
for (int x = 0; x < gridW; x++) {
|
||||||
|
final idx = y * gridW + x;
|
||||||
|
final pix = scaled.getPixel(x, y);
|
||||||
|
final r = pix.r.toDouble();
|
||||||
|
final g = pix.g.toDouble();
|
||||||
|
final b = pix.b.toDouble();
|
||||||
|
workingR[idx] = r;
|
||||||
|
workingG[idx] = g;
|
||||||
|
workingB[idx] = b;
|
||||||
|
luminance[idx] = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final edgeStrength = List<double>.filled(pixelCount, 0);
|
||||||
|
for (int y = 0; y < gridH; y++) {
|
||||||
|
final ym = y > 0 ? y - 1 : y;
|
||||||
|
final yp = y + 1 < gridH ? y + 1 : y;
|
||||||
|
for (int x = 0; x < gridW; x++) {
|
||||||
|
final xm = x > 0 ? x - 1 : x;
|
||||||
|
final xp = x + 1 < gridW ? x + 1 : x;
|
||||||
|
final left = luminance[y * gridW + xm];
|
||||||
|
final right = luminance[y * gridW + xp];
|
||||||
|
final up = luminance[ym * gridW + x];
|
||||||
|
final down = luminance[yp * gridW + x];
|
||||||
|
final gx = right - left;
|
||||||
|
final gy = down - up;
|
||||||
|
edgeStrength[y * gridW + x] =
|
||||||
|
(math.sqrt(gx * gx + gy * gy) / 255.0).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fidelityStructure > 0.001) {
|
||||||
|
final blurredR = List<double>.filled(pixelCount, 0);
|
||||||
|
final blurredG = List<double>.filled(pixelCount, 0);
|
||||||
|
final blurredB = List<double>.filled(pixelCount, 0);
|
||||||
|
for (int y = 0; y < gridH; y++) {
|
||||||
|
for (int x = 0; x < gridW; x++) {
|
||||||
|
double sr = 0, sg = 0, sb = 0;
|
||||||
|
int c = 0;
|
||||||
|
for (int ny = math.max(0, y - 1);
|
||||||
|
ny <= math.min(gridH - 1, y + 1);
|
||||||
|
ny++) {
|
||||||
|
for (int nx = math.max(0, x - 1);
|
||||||
|
nx <= math.min(gridW - 1, x + 1);
|
||||||
|
nx++) {
|
||||||
|
final ni = ny * gridW + nx;
|
||||||
|
sr += workingR[ni];
|
||||||
|
sg += workingG[ni];
|
||||||
|
sb += workingB[ni];
|
||||||
|
c++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final idx = y * gridW + x;
|
||||||
|
blurredR[idx] = sr / c;
|
||||||
|
blurredG[idx] = sg / c;
|
||||||
|
blurredB[idx] = sb / c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < pixelCount; i++) {
|
||||||
|
final smoothBlend = fidelityStructure * (1.0 - edgeStrength[i] * 0.75);
|
||||||
|
workingR[i] = _mix(workingR[i], blurredR[i], smoothBlend);
|
||||||
|
workingG[i] = _mix(workingG[i], blurredG[i], smoothBlend);
|
||||||
|
workingB[i] = _mix(workingB[i], blurredB[i], smoothBlend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final paletteValues = paletteRaw.map((e) => e['value'] as int).toList();
|
final paletteValues = paletteRaw.map((e) => e['value'] as int).toList();
|
||||||
final paletteLab = paletteValues.map(_argbToLab).toList(growable: false);
|
final paletteLab = paletteValues.map(_argbToLab).toList(growable: false);
|
||||||
|
|
||||||
final assignments = List<int>.filled(gridW * gridH, 0);
|
final assignments = List<int>.filled(pixelCount, 0);
|
||||||
final counts = List<int>.filled(paletteValues.length, 0);
|
final counts = List<int>.filled(paletteValues.length, 0);
|
||||||
|
|
||||||
final nearestCache = <int, int>{};
|
final errorR = List<double>.filled(pixelCount, 0);
|
||||||
|
final errorG = List<double>.filled(pixelCount, 0);
|
||||||
|
final errorB = List<double>.filled(pixelCount, 0);
|
||||||
final labCache = <int, List<double>>{};
|
final labCache = <int, List<double>>{};
|
||||||
|
|
||||||
for (int y = 0; y < gridH; y++) {
|
for (int y = 0; y < gridH; y++) {
|
||||||
for (int x = 0; x < gridW; x++) {
|
for (int x = 0; x < gridW; x++) {
|
||||||
final pix = scaled.getPixel(x, y);
|
final idx = y * gridW + x;
|
||||||
final rgb = (pix.r.toInt() << 16) | (pix.g.toInt() << 8) | pix.b.toInt();
|
|
||||||
|
|
||||||
final cached = nearestCache[rgb];
|
final srcR = (workingR[idx] + errorR[idx]).clamp(0.0, 255.0);
|
||||||
if (cached != null) {
|
final srcG = (workingG[idx] + errorG[idx]).clamp(0.0, 255.0);
|
||||||
assignments[y * gridW + x] = cached;
|
final srcB = (workingB[idx] + errorB[idx]).clamp(0.0, 255.0);
|
||||||
counts[cached]++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
final rgb = ((srcR.round() & 0xFF) << 16) |
|
||||||
|
((srcG.round() & 0xFF) << 8) |
|
||||||
|
(srcB.round() & 0xFF);
|
||||||
final srcLab = labCache.putIfAbsent(rgb, () => _rgbToLab(rgb));
|
final srcLab = labCache.putIfAbsent(rgb, () => _rgbToLab(rgb));
|
||||||
|
|
||||||
int bestIdx = 0;
|
int bestIdx = 0;
|
||||||
double bestDistance = double.infinity;
|
double bestDistance = double.infinity;
|
||||||
|
final candidates = <_Candidate>[];
|
||||||
|
final localVar =
|
||||||
|
colorVariation * (1.0 - edgeStrength[idx]).clamp(0.0, 1.0);
|
||||||
|
final tolerance = 2.0 + localVar * 22.0;
|
||||||
|
|
||||||
for (int i = 0; i < paletteLab.length; i++) {
|
for (int i = 0; i < paletteLab.length; i++) {
|
||||||
final d = _deltaE76(srcLab, paletteLab[i]);
|
final lDiff = (srcLab[0] - paletteLab[i][0]).abs();
|
||||||
|
final d = _deltaE76(srcLab, paletteLab[i]) +
|
||||||
|
(edgeEmphasis * edgeStrength[idx] * lDiff * 0.35);
|
||||||
if (d < bestDistance) {
|
if (d < bestDistance) {
|
||||||
bestDistance = d;
|
bestDistance = d;
|
||||||
bestIdx = i;
|
bestIdx = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nearestCache[rgb] = bestIdx;
|
for (int i = 0; i < paletteLab.length; i++) {
|
||||||
assignments[y * gridW + x] = bestIdx;
|
final lDiff = (srcLab[0] - paletteLab[i][0]).abs();
|
||||||
counts[bestIdx]++;
|
final d = _deltaE76(srcLab, paletteLab[i]) +
|
||||||
|
(edgeEmphasis * edgeStrength[idx] * lDiff * 0.35);
|
||||||
|
if (d <= bestDistance + tolerance) {
|
||||||
|
candidates.add(_Candidate(index: i, distance: d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int chosen = bestIdx;
|
||||||
|
if (localVar > 0.001 && candidates.length > 1) {
|
||||||
|
candidates.sort((a, b) => a.distance.compareTo(b.distance));
|
||||||
|
final usable = math.min(candidates.length, 4);
|
||||||
|
final h = ((x + 1) * 73856093) ^ ((y + 1) * 19349663);
|
||||||
|
final pick = h.abs() % usable;
|
||||||
|
chosen = candidates[pick].index;
|
||||||
|
}
|
||||||
|
|
||||||
|
assignments[idx] = chosen;
|
||||||
|
counts[chosen]++;
|
||||||
|
|
||||||
|
if (ditheringStrength > 0.001) {
|
||||||
|
final argb = paletteValues[chosen];
|
||||||
|
final qr = ((argb >> 16) & 0xFF).toDouble();
|
||||||
|
final qg = ((argb >> 8) & 0xFF).toDouble();
|
||||||
|
final qb = (argb & 0xFF).toDouble();
|
||||||
|
|
||||||
|
final er = (srcR - qr) * ditheringStrength;
|
||||||
|
final eg = (srcG - qg) * ditheringStrength;
|
||||||
|
final eb = (srcB - qb) * ditheringStrength;
|
||||||
|
|
||||||
|
if (x + 1 < gridW) {
|
||||||
|
final n = idx + 1;
|
||||||
|
errorR[n] += er * (7 / 16);
|
||||||
|
errorG[n] += eg * (7 / 16);
|
||||||
|
errorB[n] += eb * (7 / 16);
|
||||||
|
}
|
||||||
|
if (y + 1 < gridH) {
|
||||||
|
if (x > 0) {
|
||||||
|
final n = idx + gridW - 1;
|
||||||
|
errorR[n] += er * (3 / 16);
|
||||||
|
errorG[n] += eg * (3 / 16);
|
||||||
|
errorB[n] += eb * (3 / 16);
|
||||||
|
}
|
||||||
|
final nDown = idx + gridW;
|
||||||
|
errorR[nDown] += er * (5 / 16);
|
||||||
|
errorG[nDown] += eg * (5 / 16);
|
||||||
|
errorB[nDown] += eb * (5 / 16);
|
||||||
|
|
||||||
|
if (x + 1 < gridW) {
|
||||||
|
final n = idx + gridW + 1;
|
||||||
|
errorR[n] += er * (1 / 16);
|
||||||
|
errorG[n] += eg * (1 / 16);
|
||||||
|
errorB[n] += eb * (1 / 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +795,15 @@ Map<String, dynamic> _generateMosaicIsolate(Map<String, dynamic> request) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _Candidate {
|
||||||
|
final int index;
|
||||||
|
final double distance;
|
||||||
|
|
||||||
|
const _Candidate({required this.index, required this.distance});
|
||||||
|
}
|
||||||
|
|
||||||
|
double _mix(double a, double b, double t) => a + (b - a) * t;
|
||||||
|
|
||||||
List<double> _argbToLab(int argb) {
|
List<double> _argbToLab(int argb) {
|
||||||
final rgb = argb & 0x00FFFFFF;
|
final rgb = argb & 0x00FFFFFF;
|
||||||
return _rgbToLab(rgb);
|
return _rgbToLab(rgb);
|
||||||
|
|||||||
Reference in New Issue
Block a user