Welcome to mirror list, hosted at ThFree Co, Russian Federation.

NesTiler.git/NesTiler.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2022-10-28 16:59:42 +0300
committerAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2022-10-28 16:59:42 +0300
commit7ede863fcfa68e44fceb766c3191e5bd98f2cdc9 (patch)
treec5cec1622c49ce8b6951823ef2f28959969bd452 /NesTiler
parentcedf29ff0250a06d99570eaafa1caa64a0ff81bb (diff)
Lossy levels, some optimization, "ignore-tiled-range" option removed.
Diffstat (limited to 'NesTiler')
-rw-r--r--NesTiler/CmdArgs.cs16
-rw-r--r--NesTiler/ColorExtensions.cs7
-rw-r--r--NesTiler/Config.cs30
-rw-r--r--NesTiler/Palette.cs30
-rw-r--r--NesTiler/Program.cs85
5 files changed, 108 insertions, 60 deletions
diff --git a/NesTiler/CmdArgs.cs b/NesTiler/CmdArgs.cs
index 7eeec5c..51c58a9 100644
--- a/NesTiler/CmdArgs.cs
+++ b/NesTiler/CmdArgs.cs
@@ -19,7 +19,6 @@
new ArgPatternOffset(),
new ArgAttributeTableYOffset(),
new ArgSharePatternTable(),
- new ArgIgnoreTilesRange(),
new ArgLossy(),
new ArgOutPreview(),
new ArgOutPalette(),
@@ -132,23 +131,12 @@
public string Long { get; } = L;
}
- class ArgIgnoreTilesRange : IArg
- {
- public const string S = "r";
- public const string L = "ignore-tiles-range";
- public string? Params { get; } = null;
- public string Description { get; } = "option to disable tile ID overflow check";
- public bool HasIndex { get; } = false;
- public string Short { get; } = S;
- public string Long { get; } = L;
- }
-
class ArgLossy : IArg
{
public const string S = "l";
public const string L = "lossy";
- public string? Params { get; } = null;
- public string Description { get; } = "option to ignore palettes loss, produces distorted image\nif there are too many colors";
+ public string? Params { get; } = "<level>";
+ public string Description { get; } = "lossy level 0-3, defines how many color distortion is allowed\nwithout throwing an error (default - 2)";
public bool HasIndex { get; } = false;
public string Short { get; } = S;
public string Long { get; } = L;
diff --git a/NesTiler/ColorExtensions.cs b/NesTiler/ColorExtensions.cs
index 239497d..e6e888c 100644
--- a/NesTiler/ColorExtensions.cs
+++ b/NesTiler/ColorExtensions.cs
@@ -28,6 +28,10 @@ namespace com.clusterrr.Famicom.NesTiler
public static uint ToArgb(this SKColor color)
=> (uint)((color.Alpha << 24) | (color.Red << 16) | (color.Green << 8) | color.Blue);
+ public static string ToHtml(this Color color) => ColorTranslator.ToHtml(color);
+
+ public static string ToHtml(this SKColor color) => color.ToColor().ToHtml();
+
public static double GetDelta(this SKColor color1, SKColor color2)
{
var pair = new ColorPair()
@@ -35,8 +39,7 @@ namespace com.clusterrr.Famicom.NesTiler
Color1 = color1,
Color2 = color2
};
- if (cache.ContainsKey(pair))
- return cache[pair];
+ if (cache.ContainsKey(pair)) return cache[pair];
var a = new Rgb { R = color1.Red, G = color1.Green, B = color1.Blue };
var b = new Rgb { R = color2.Red, G = color2.Green, B = color2.Blue };
var delta = a.Compare(b, comparer);
diff --git a/NesTiler/Config.cs b/NesTiler/Config.cs
index 3046748..d039e68 100644
--- a/NesTiler/Config.cs
+++ b/NesTiler/Config.cs
@@ -30,8 +30,7 @@ namespace com.clusterrr.Famicom.NesTiler
public int TilePalWidth { get; private set; } = 16;
public int TilePalHeight { get; private set; } = 16;
public bool SharePatternTable { get; private set; } = false;
- public bool IgnoreTilesRange { get; private set; } = false;
- public bool Lossy { get; private set; } = false;
+ public int LossyLevel { get; private set; } = 2;
public int PatternTableStartOffsetShared { get; private set; } = 0;
public Dictionary<int, int> PatternTableStartOffsets { get; private set; } = new Dictionary<int, int>();
public Dictionary<int, int> PattributeTableYOffsets { get; private set; } = new Dictionary<int, int>();
@@ -157,7 +156,17 @@ namespace com.clusterrr.Famicom.NesTiler
case ArgPalette.S:
case ArgPalette.L:
{
- var colors = value.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(c => ColorTranslator.FromHtml(c).ToSKColor());
+ var colors = value.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(c =>
+ {
+ try
+ {
+ return ColorTranslator.FromHtml(c).ToSKColor();
+ }
+ catch (FormatException)
+ {
+ throw new ArgumentException($"{c} - invalid color.", param);
+ }
+ });
config.FixedPalettes[indexNum] = new Palette(colors);
}
i++;
@@ -165,7 +174,7 @@ namespace com.clusterrr.Famicom.NesTiler
case ArgPatternOffset.S:
case ArgPatternOffset.L:
if (!int.TryParse(value, out valueInt))
- throw new ArgumentException($"\"{valueInt}\" is not valid integer value.", param);
+ throw new ArgumentException($"\"{value}\" is not valid integer value.", param);
if (valueInt < 0 || valueInt >= 256)
throw new ArgumentException($"Value ({valueInt}) must be between 0 and 255.", param);
config.PatternTableStartOffsets[indexNum] = valueInt;
@@ -175,7 +184,7 @@ namespace com.clusterrr.Famicom.NesTiler
case ArgAttributeTableYOffset.S:
case ArgAttributeTableYOffset.L:
if (!int.TryParse(value, out valueInt))
- throw new ArgumentException($"\"{valueInt}\" is not valid integer value.", param);
+ throw new ArgumentException($"\"{value}\" is not valid integer value.", param);
if (valueInt % 8 != 0)
throw new ArgumentException($"Value ({valueInt}) must be divisible by 8.", param);
if (valueInt < 0 || valueInt >= 256)
@@ -187,13 +196,14 @@ namespace com.clusterrr.Famicom.NesTiler
case ArgSharePatternTable.L:
config.SharePatternTable = true;
break;
- case ArgIgnoreTilesRange.S:
- case ArgIgnoreTilesRange.L:
- config.IgnoreTilesRange = true;
- break;
case ArgLossy.S:
case ArgLossy.L:
- config.Lossy = true;
+ if (!int.TryParse(value, out valueInt))
+ throw new ArgumentException($"\"{value}\" is not valid integer value.", param);
+ if (valueInt < 0 || valueInt > 3)
+ throw new ArgumentException($"Value ({valueInt}) must be between 0 and 3.", param);
+ config.LossyLevel = valueInt;
+ i++;
break;
case ArgOutPreview.S:
case ArgOutPreview.L:
diff --git a/NesTiler/Palette.cs b/NesTiler/Palette.cs
index 5944b1c..921f9af 100644
--- a/NesTiler/Palette.cs
+++ b/NesTiler/Palette.cs
@@ -3,6 +3,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
+using System.IO;
using System.Linq;
using Color = System.Drawing.Color;
@@ -10,8 +11,19 @@ namespace com.clusterrr.Famicom.NesTiler
{
class Palette : IEquatable<Palette>, IEnumerable<SKColor>
{
+ public struct LossyInfo
+ {
+ public int ImageNum { get; init; }
+ public int ColorCount { get; init; }
+ public int TileX { get; init; }
+ public int TileY { get; init; }
+ public int TileWidth { get; init; }
+ public int TileHeight { get; init; }
+ }
+
private SKColor[] colors;
private Dictionary<ColorPair, (SKColor color, double delta)> deltaCache = new();
+ public LossyInfo? ColorLossy { get; init; } = null;
public SKColor? this[int i]
{
@@ -24,13 +36,13 @@ namespace com.clusterrr.Famicom.NesTiler
}
public int Count => colors.Length;
- public Palette(FastBitmap image, int leftX, int topY, int width, int height, SKColor bgColor)
+ public Palette(int imageNum, FastBitmap image, int tileX, int tileY, int tileWidth, int tileHeight, SKColor bgColor)
{
Dictionary<SKColor, int> colorCounter = new();
- for (int y = topY; y < topY + height; y++)
+ for (int y = tileY; y < tileY + tileHeight; y++)
{
if (y < 0) continue;
- for (int x = leftX; x < leftX + width; x++)
+ for (int x = tileX; x < tileX + tileWidth; x++)
{
var color = image.GetPixelColor(x, y);
if (color == bgColor) continue;
@@ -40,7 +52,17 @@ namespace com.clusterrr.Famicom.NesTiler
}
// TODO: one more lossy level?
- colors = colorCounter.OrderByDescending(kv => kv.Value).Take(3).OrderBy(kv => kv.Key.ToArgb()).Select(kv => kv.Key).ToArray();
+ var colorsCandidates = colorCounter.OrderByDescending(kv => kv.Value);
+ if (colorsCandidates.Count() > 3) ColorLossy = new()
+ {
+ ImageNum = imageNum,
+ ColorCount = colorsCandidates.Count(),
+ TileX = tileX,
+ TileY = tileY,
+ TileWidth = tileWidth,
+ TileHeight = tileHeight
+ };
+ colors = colorsCandidates.Take(3).OrderBy(kv => kv.Key.ToArgb()).Select(kv => kv.Key).ToArray();
}
public Palette(IEnumerable<SKColor> colors)
diff --git a/NesTiler/Program.cs b/NesTiler/Program.cs
index 92f50fd..990f0dd 100644
--- a/NesTiler/Program.cs
+++ b/NesTiler/Program.cs
@@ -141,8 +141,10 @@ namespace com.clusterrr.Famicom.NesTiler
var color = image.GetPixelColor(x, y);
if (color.Alpha >= 0x80 || c.Mode == Config.TilesMode.Backgrounds)
{
- // TODO: more lossy levels?
var similarColor = nesColors.FindSimilarColor(color);
+ if (c.LossyLevel <= 0 && similarColor != color)
+ throw new InvalidDataException($"Image #{imageNum}, pixel X={x} Y={y} has color {color.ToHtml()} " +
+ $"but most similar NES color is {similarColor.ToHtml()}.");
image.SetPixelColor(x, y, similarColor);
}
else
@@ -154,22 +156,25 @@ namespace com.clusterrr.Famicom.NesTiler
}
}
- List<Palette> calculatedPalettes;
+ Palette[] calculatedPalettes;
var maxCalculatedPaletteCount = Enumerable.Range(0, 4)
.Select(i => c.PaletteEnabled[i] && c.FixedPalettes[i] == null).Count();
SKColor bgColor;
+ Palette.LossyInfo? lossyInfo;
// Detect background color
if (c.BgColor.HasValue)
{
// Manually
bgColor = nesColors.FindSimilarColor(c.BgColor.Value);
- calculatedPalettes = CalculatePalettes(images,
+ (calculatedPalettes, lossyInfo) = CalculatePalettes(images,
c.PaletteEnabled,
c.FixedPalettes,
c.PattributeTableYOffsets,
c.TilePalWidth,
c.TilePalHeight,
- c.BgColor.Value).ToList();
+ c.BgColor.Value);
+ if ((c.LossyLevel <= 1) && (lossyInfo != null))
+ throw new InvalidDataException($"Image #{lossyInfo?.ImageNum}, tile at X={lossyInfo?.TileX} Y={lossyInfo?.TileY} has {lossyInfo?.ColorCount + 1} colors while only 4 is possible.");
}
else
{
@@ -207,7 +212,7 @@ namespace com.clusterrr.Famicom.NesTiler
// Most used colors
var candidates = colorPerTileCounter.OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray();
// Try to calculate palettes for every background color
- var calcResults = new Dictionary<SKColor, Palette[]>();
+ var calcResults = new Dictionary<SKColor, (Palette[] Palettes, Palette.LossyInfo? LossyInfo)>();
for (int i = 0; i < Math.Min(candidates.Length, MAX_BG_COLOR_AUTODETECT_ITERATIONS); i++)
{
calcResults[candidates[i]] = CalculatePalettes(images,
@@ -219,19 +224,37 @@ namespace com.clusterrr.Famicom.NesTiler
candidates[i]);
}
// Select background color which uses minimum palettes
- var kv = calcResults.OrderBy(kv => kv.Value.Length).First();
- (bgColor, calculatedPalettes) = (kv.Key, kv.Value.ToList());
- Trace.WriteLine(ColorTranslator.ToHtml(bgColor.ToColor()));
+ // TODO: less palettes != best solution? Take in account tile count.
+ var kvAllLossless = calcResults.Where(kv => kv.Value.LossyInfo == null).OrderBy(kv => kv.Value.Palettes.Length);
+ if (kvAllLossless.Any())
+ {
+ // Lossless combinations found, get best
+ var kv = kvAllLossless.First();
+ (bgColor, calculatedPalettes) = (kv.Key, kv.Value.Palettes);
+ } else
+ {
+ // Lossy combinations found
+ var kvLossy = calcResults.OrderBy(kv => kv.Value.Palettes.Length).First();
+ lossyInfo = kvLossy.Value.LossyInfo;
+ if (c.LossyLevel <= 1)
+ throw new InvalidDataException($"Image #{lossyInfo?.ImageNum}, tile at X={lossyInfo?.TileX} Y={lossyInfo?.TileY} has {lossyInfo?.ColorCount + 1} colors while only 4 is possible.");
+ (bgColor, calculatedPalettes) = (kvLossy.Key, kvLossy.Value.Palettes);
+ }
+ Trace.WriteLine(bgColor.ToHtml());
}
- if (calculatedPalettes.Count > maxCalculatedPaletteCount && !c.Lossy)
+ if (calculatedPalettes.Length > maxCalculatedPaletteCount)
{
- throw new InvalidOperationException($"Can't fit {calculatedPalettes.Count} palettes, {maxCalculatedPaletteCount} is maximum.");
+ // Check lossy and throw error in case it's too low
+ if (c.LossyLevel <= 2) throw new InvalidDataException($"Can't fit {calculatedPalettes.Length} palettes, {maxCalculatedPaletteCount} is maximum.");
+ // Just warning
+ Trace.WriteLine($"WARNING! Can't fit {calculatedPalettes.Length} palettes, {maxCalculatedPaletteCount} is maximum. {calculatedPalettes.Length - maxCalculatedPaletteCount} will be discarded.");
}
// Select palettes
var palettes = new Palette?[4] { null, null, null, null };
outPalettesCsvLines?.Add("palette_id,color0,color1,color2,color3");
+ var calculatedPalettesList = new List<Palette>(calculatedPalettes);
for (var i = 0; i < palettes.Length; i++)
{
if (c.PaletteEnabled[i])
@@ -240,20 +263,21 @@ namespace com.clusterrr.Famicom.NesTiler
{
palettes[i] = c.FixedPalettes[i];
}
- else if (calculatedPalettes.Any())
+ else if (calculatedPalettesList.Any())
{
- palettes[i] = calculatedPalettes.First();
- calculatedPalettes.RemoveAt(0);
+ palettes[i] = calculatedPalettesList.First();
+ calculatedPalettesList.RemoveAt(0);
}
if (palettes[i] != null)
{
- Trace.WriteLine($"Palette #{i}: {ColorTranslator.ToHtml(bgColor.ToColor())}(BG) {string.Join(" ", palettes[i]!.Select(p => ColorTranslator.ToHtml(p.ToColor())))}");
+ Trace.WriteLine($"Palette #{i}: {bgColor.ToHtml()}(BG) {string.Join(" ", palettes[i]!.Select(p => p.ToHtml()))}");
// Write CSV if required
- outPalettesCsvLines?.Add($"{i},{ColorTranslator.ToHtml(bgColor.ToColor())},{string.Join(",", Enumerable.Range(1, 3).Select(c => (palettes[i]![c] != null ? ColorTranslator.ToHtml(palettes[i]![c]!.Value.ToColor()) : "")))}");
+ outPalettesCsvLines?.Add($"{i},{bgColor.ToHtml()},{string.Join(",", Enumerable.Range(1, 3).Select(c => (palettes[i]![c] != null ? palettes[i]![c]!.Value.ToHtml() : "")))}");
}
}
}
+ calculatedPalettes = calculatedPalettesList.ToArray();
// Calculate palette as color indices and save them to files
var bgColorIndex = nesColors.FindSimilarColorIndex(bgColor);
@@ -325,8 +349,8 @@ namespace com.clusterrr.Famicom.NesTiler
similarColor);
}
}
- } // tile X
- } // tile Y
+ } // tile palette X
+ } // tile palette Y
// Save preview if required
if (c.OutPreview.ContainsKey(imageNum))
@@ -454,8 +478,7 @@ namespace com.clusterrr.Famicom.NesTiler
Trace.WriteLine($"#{imageNum} tiles range: {c.PatternTableStartOffsets[imageNum]}-{tileID - 1}");
else
Trace.WriteLine($"Pattern table is empty.");
- if (tileID > 256 && !c.IgnoreTilesRange)
- throw new ArgumentOutOfRangeException("Tiles out of range.");
+ if (tileID > 256) throw new InvalidDataException("Tiles out of range.");
// Save pattern table to file
if (c.OutPatternTable.ContainsKey(imageNum) && !c.SharePatternTable)
@@ -530,11 +553,12 @@ namespace com.clusterrr.Famicom.NesTiler
}
}
- static Palette[] CalculatePalettes(Dictionary<int, FastBitmap> images, bool[] paletteEnabled, Palette?[] fixedPalettes, Dictionary<int, int> attributeTableOffsets, int tilePalWidth, int tilePalHeight, SKColor bgColor)
+ static (Palette[] palettes, Palette.LossyInfo? lossyInfo) CalculatePalettes(Dictionary<int, FastBitmap> images, bool[] paletteEnabled, Palette?[] fixedPalettes, Dictionary<int, int> attributeTableOffsets, int tilePalWidth, int tilePalHeight, SKColor bgColor)
{
var required = Enumerable.Range(0, 4).Select(i => paletteEnabled[i] && fixedPalettes[i] == null);
// Creating and counting the palettes
var paletteCounter = new Dictionary<Palette, int>();
+ Palette.LossyInfo? lossyInfo = null;
foreach (var imageNum in images.Keys)
{
var image = images[imageNum];
@@ -546,8 +570,9 @@ namespace com.clusterrr.Famicom.NesTiler
{
// Create palette using up to three most used colors
var palette = new Palette(
- image, tileX * tilePalWidth, (tileY * tilePalHeight) - attributeTableOffset,
+ imageNum, image, tileX * tilePalWidth, (tileY * tilePalHeight) - attributeTableOffset,
tilePalWidth, tilePalHeight, bgColor);
+ lossyInfo ??= palette.ColorLossy;
// Skip tiles with only background color
if (!palette.Any()) continue;
@@ -565,18 +590,18 @@ namespace com.clusterrr.Famicom.NesTiler
}
// Group palettes
- var result = new Palette[0];
+ var resultPalettes = new Palette[0];
// Multiple iterations
while (true)
{
// Remove unused palettes
paletteCounter = paletteCounter.Where(kv => kv.Value > 0).ToDictionary(kv => kv.Key, kv => kv.Value);
// Sort by usage
- result = paletteCounter.OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray();
+ resultPalettes = paletteCounter.OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray();
// Some palettes can contain all colors from other palettes, so we need to combine them
- foreach (var palette2 in result)
- foreach (var palette1 in result)
+ foreach (var palette2 in resultPalettes)
+ foreach (var palette1 in resultPalettes)
{
if ((palette2 != palette1) && (palette2.Count >= palette1.Count) && palette2.Contains(palette1))
{
@@ -589,17 +614,17 @@ namespace com.clusterrr.Famicom.NesTiler
// Remove unused palettes
paletteCounter = paletteCounter.Where(kv => kv.Value > 0).ToDictionary(kv => kv.Key, kv => kv.Value);
// Sort them again
- result = paletteCounter.OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray();
+ resultPalettes = paletteCounter.OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray();
// Get most used palettes
- var top = result.Take(required.Count()).ToList();
+ var top = resultPalettes.Take(required.Count()).ToList();
// Use free colors in palettes to store less popular palettes
bool grouped = false;
foreach (var t in top)
{
if (paletteCounter[t] > 0 && t.Count < 3)
{
- foreach (var p in result)
+ foreach (var p in resultPalettes)
{
var newColors = p.Where(c => !t.Contains(c));
if ((p != t) && (paletteCounter[p] > 0) && (newColors.Count() + t.Count <= 3))
@@ -622,9 +647,9 @@ namespace com.clusterrr.Famicom.NesTiler
// Remove unused palettes
paletteCounter = paletteCounter.Where(kv => kv.Value > 0).ToDictionary(kv => kv.Key, kv => kv.Value);
// Sort them again
- result = paletteCounter.OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray();
+ resultPalettes = paletteCounter.OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray();
- return result;
+ return (resultPalettes, lossyInfo);
}
}
}