Skip to content

Commit ccdf205

Browse files
committed
0.5.0
1 parent 1bcd126 commit ccdf205

File tree

4 files changed

+70
-26
lines changed

4 files changed

+70
-26
lines changed

docs/changelog/0.5.0.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ Improvements:
33
* Added .MZM export support for interoperability with MegaZeux.
44
* Added a "MegaZeux" platform. It behaves a bit differently from previous ones - only the board width and height can be
55
configured, and there are no elements to choose from. Rather, these decisions are performed by the MegaZeux editor.
6+
* Note that even though "MegaZeux" is a platform, many parts of the algorithm still assume an ASCII-esque character
7+
layout (solid, blending, empty characters).
68
* The open/save dialogs now remember the directory they were last used in (per-type) until you close the app.
9+
* (Very) minor performance improvements.
710

811
Bugs fixed:
912

src/main/java/pl/asie/libzzt/Stat.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ public void readZ(ZInputStream stream) throws IOException {
110110
boundStatId = (dataLen < 0) ? -dataLen : 0;
111111
}
112112

113+
public int lengthZ(Platform platform) {
114+
int len = (platform == Platform.ZZT ? 33 : 25);
115+
if (boundStatId == 0 && data != null) {
116+
byte[] dataBytes = data.getBytes(StandardCharsets.ISO_8859_1);
117+
len += dataBytes.length;
118+
}
119+
return len;
120+
}
121+
113122
public void writeZ(ZOutputStream stream) throws IOException {
114123
byte[] dataBytes = data != null ? data.getBytes(StandardCharsets.ISO_8859_1) : null;
115124
stream.writePByte(this.x);

src/main/java/pl/asie/zima/image/ImageConverter.java

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package pl.asie.zima.image;
2020

2121
import lombok.AllArgsConstructor;
22+
import lombok.Data;
2223
import lombok.Getter;
2324
import pl.asie.libzzt.Board;
2425
import pl.asie.libzzt.Element;
@@ -37,9 +38,7 @@
3738
import java.util.ArrayList;
3839
import java.util.Comparator;
3940
import java.util.HashSet;
40-
import java.util.LinkedHashMap;
4141
import java.util.List;
42-
import java.util.Map;
4342
import java.util.Set;
4443
import java.util.function.IntPredicate;
4544
import java.util.stream.Collectors;
@@ -107,17 +106,27 @@ private Pair<Result, BufferedImage> convertBoardless(BufferedImage inputImage, i
107106
List<ElementResult> rules = new ArrayList<>(256 * 256);
108107
final int progressSize = width * height;
109108
ElementResult[] previewResults = new ElementResult[width * height];
109+
ElementResult emptyResult = null;
110110
BufferedImage preview = null;
111111

112112
for (int ich = 0; ich < 256; ich++) {
113113
if (fast && (ich != 32 && ich != 176 && ich != 177 && ich != 178 && ich != 219)) continue;
114114
if (charCheck != null && !charCheck.test(ich)) continue;
115115
for (int ico = 0; ico < 256; ico++) {
116116
if (colorCheck != null && !colorCheck.test(ico)) continue;
117-
rules.add(new ElementResult(null, false, false, ich, ico));
117+
ElementResult result = new ElementResult(null, false, false, ich, ico);
118+
if (emptyResult == null) {
119+
emptyResult = result;
120+
}
121+
rules.add(result);
118122
}
119123
}
120124

125+
if (emptyResult == null) {
126+
emptyResult = new ElementResult(null, false, false, 0, 0);
127+
}
128+
final ElementResult emptyResultFinal = emptyResult;
129+
121130
// find lowest-MSE results for each tile, in parallel
122131
IntStream.range(0, width * height).parallel().forEach(pos -> {
123132
synchronized (progressCallback) {
@@ -127,7 +136,7 @@ private Pair<Result, BufferedImage> convertBoardless(BufferedImage inputImage, i
127136
int ix = pos % width;
128137
int iy = pos / width;
129138

130-
ElementResult minResult = null;
139+
ElementResult minResult = emptyResultFinal;
131140
float minMse = Float.MAX_VALUE;
132141
ImageMseCalculator.Applier applyMseFunc = mseCalculator.applyMse(inputImage, ix * visual.getCharWidth(), iy * visual.getCharHeight());
133142

@@ -166,6 +175,12 @@ private Pair<Result, BufferedImage> convertBoardless(BufferedImage inputImage, i
166175
return new Pair<>(result, preview);
167176
}
168177

178+
@Data
179+
private static class ElementRuleResult {
180+
private final ElementRule rule;
181+
private final List<ElementResult> result;
182+
}
183+
169184
public Pair<Result, BufferedImage> convert(BufferedImage inputImage, ImageConverterRuleset ruleset,
170185
int x, int y, int width, int height, int playerX, int playerY, int maxStatCount, boolean blinkingDisabled,
171186
int maxBoardSize,
@@ -190,7 +205,7 @@ public Pair<Result, BufferedImage> convert(BufferedImage inputImage, ImageConver
190205
final BufferedImage image = inputImage;
191206

192207
List<Triplet<Coord2D, ElementResult, Float>> statfulStrategies = new ArrayList<>();
193-
Map<ElementRule, List<ElementResult>> ruleResultMap = new LinkedHashMap<>();
208+
List<ElementRuleResult> ruleResultList = new ArrayList<>(ruleset.getRules().size());
194209
Set<Element> allowedElements = new HashSet<>();
195210
ElementResult[] previewResults = new ElementResult[width * height];
196211
float[] previewMse = new float[width * height];
@@ -248,7 +263,7 @@ public Pair<Result, BufferedImage> convert(BufferedImage inputImage, ImageConver
248263
break;
249264
}
250265

251-
ruleResultMap.put(rule, proposals.collect(Collectors.toList()));
266+
ruleResultList.add(new ElementRuleResult(rule, proposals.collect(Collectors.toList())));
252267
}
253268

254269
// find lowest-MSE results for each tile, in parallel
@@ -276,8 +291,8 @@ public Pair<Result, BufferedImage> convert(BufferedImage inputImage, ImageConver
276291
float statfulMse = Float.MAX_VALUE;
277292
ImageMseCalculator.Applier applyMseFunc = mseCalculator.applyMse(image, ix * visual.getCharWidth(), iy * visual.getCharHeight());
278293

279-
for (Map.Entry<ElementRule, List<ElementResult>> ruleResult : ruleResultMap.entrySet()) {
280-
List<ElementResult> proposals = ruleResult.getValue();
294+
for (ElementRuleResult ruleResult : ruleResultList) {
295+
List<ElementResult> proposals = ruleResult.getResult();
281296

282297
float lowestLocalMse = statlessMse;
283298
ElementResult lowestLocalResult = null;
@@ -336,10 +351,8 @@ public Pair<Result, BufferedImage> convert(BufferedImage inputImage, ImageConver
336351
int boardSerializationSize = count(board::writeZ);
337352
int addedStats = 0;
338353

354+
Stat stat = new Stat();
339355
for (int i = 0; i < statfulStrategies.size(); i++) {
340-
if (addedStats >= realMaxStatCount) {
341-
break;
342-
}
343356
Triplet<Coord2D, ElementResult, Float> strategyData = statfulStrategies.get(i);
344357
Coord2D coords = strategyData.getFirst();
345358
ElementResult result = strategyData.getSecond();
@@ -352,25 +365,28 @@ public Pair<Result, BufferedImage> convert(BufferedImage inputImage, ImageConver
352365
previewResults[coords.getY() * width + coords.getX()] = result;
353366
board.setElement(x + coords.getX(), y + coords.getY(), result.getElement());
354367
board.setColor(x + coords.getX(), y + coords.getY(), result.getColor());
355-
Stat stat = new Stat();
368+
356369
stat.setX(x + coords.getX());
357370
stat.setY(y + coords.getY());
358371
stat.setCycle(1); // maybe we can reduce this to save CPU cycles?
359372
stat.setP1(result.getCharacter());
360373

361374
if (boardSerializationSize < (realMaxBoardSize - 128)) {
362375
// optimization: don't recalc full board size if only RLE could be impacted
363-
boardSerializationSize += count(stat::writeZ);
376+
boardSerializationSize += stat.lengthZ(platform);
364377
} else {
365-
boardSerializationSize = count(board::writeZ) + count(stat::writeZ);
378+
boardSerializationSize = count(board::writeZ) + stat.lengthZ(platform);
366379
}
367380
if (boardSerializationSize > realMaxBoardSize) {
368381
previewResults[coords.getY() * width + coords.getX()] = prevResult;
369382
board.setElement(x + coords.getX(), y + coords.getY(), prevElement);
370383
board.setColor(x + coords.getX(), y + coords.getY(), prevColor);
371384
} else {
372385
board.addStat(stat);
373-
addedStats++;
386+
if ((++addedStats) >= realMaxStatCount) {
387+
break;
388+
}
389+
stat = new Stat();
374390
}
375391
}
376392

@@ -388,7 +404,7 @@ public Pair<Result, BufferedImage> convert(BufferedImage inputImage, ImageConver
388404
Element element = board.getElement(x + ix, y + iy);
389405
int color = board.getColor(x + ix, y + iy);
390406

391-
if (!element.isText() && !element.isStat()) {
407+
if (!element.isText() && (!element.isStat() || board.getStatAt(x + ix, y + iy) == null)) {
392408
if (!blinkingDisabled && color >= 0x80) {
393409
continue;
394410
}

src/main/java/pl/asie/zima/image/TrixImageMseCalculator.java

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222
import pl.asie.libzzt.TextVisualData;
2323

2424
import java.awt.image.BufferedImage;
25+
import java.util.HashSet;
2526
import java.util.Set;
27+
import java.util.stream.Collectors;
2628

2729
public class TrixImageMseCalculator implements ImageMseCalculator {
28-
private static final Set<Integer> blendingChars = Set.of(0, 32, 176, 177, 178, 219);
2930
private final TextVisualData visual;
3031
private final float contrastReduction;
3132
private final float accurateApproximate;
@@ -34,6 +35,7 @@ public class TrixImageMseCalculator implements ImageMseCalculator {
3435
private final boolean[][] charLut1x1Precalc;
3536
private final float[] colDistPrecalc;
3637
private final boolean blinkingDisabled;
38+
private final Set<Integer> blendingChars = new HashSet<>();
3739

3840
private static class ImageLutHolder {
3941
private final float[][] dataMacro1x1;
@@ -92,27 +94,32 @@ public TrixImageMseCalculator(TextVisualData visual, boolean blinkingDisabled, f
9294
charLutPrecalc[i] = ColorUtils.mix(bg, fg, fgColored / 4.0f);
9395
}
9496

95-
charLut2x2Precalc = new int[256][(visual.getCharWidth() >> 1) * (visual.getCharHeight() >> 1)];
97+
int charLut2x2Count = (visual.getCharWidth() >> 1) * (visual.getCharHeight() >> 1);
98+
charLut2x2Precalc = new int[256][charLut2x2Count];
9699
for (int c = 0; c < 256; c++) {
97100
int coff = c * visual.getCharHeight();
101+
int[] charLut2x2PrecalcLocal = charLut2x2Precalc[c];
102+
98103
for (int cy = 0; cy < visual.getCharHeight(); cy += 2) {
99104
int charLine1 = (int) visual.getCharData()[coff + cy] & 0xFF;
100105
int charLine2 = (int) visual.getCharData()[coff + cy + 1] & 0xFF;
101106

102107
for (int cx = 0; cx < visual.getCharWidth(); cx += 2) {
103108
int charLutIdx = ((charLine1 >> (6 - cx)) & 3) | (((charLine2 >> (6 - cx)) & 3) << 2);
104-
charLut2x2Precalc[c][(cy >> 1) * (visual.getCharWidth() >> 1) + (cx >> 1)] = charLutIdx;
109+
charLut2x2PrecalcLocal[(cy >> 1) * (visual.getCharWidth() >> 1) + (cx >> 1)] = charLutIdx;
105110
}
106111
}
107112
}
108113

109114
charLut1x1Precalc = new boolean[256][visual.getCharWidth() * visual.getCharHeight()];
110115
for (int c = 0; c < 256; c++) {
111116
int coff = c * visual.getCharHeight();
117+
boolean[] charLut1x1PrecalcLocal = charLut1x1Precalc[c];
118+
112119
for (int cy = 0; cy < visual.getCharHeight(); cy++) {
113120
int charLine = (int) visual.getCharData()[coff + cy] & 0xFF;
114121
for (int cx = 0; cx < visual.getCharWidth(); cx++) {
115-
charLut1x1Precalc[c][cy * visual.getCharWidth() + cx] = (charLine & (1 << (7 - cx))) != 0;
122+
charLut1x1PrecalcLocal[cy * visual.getCharWidth() + cx] = (charLine & (1 << (7 - cx))) != 0;
116123
}
117124
}
118125
}
@@ -123,31 +130,40 @@ public TrixImageMseCalculator(TextVisualData visual, boolean blinkingDisabled, f
123130
int fg = visual.getPalette()[i & 0x0F];
124131
colDistPrecalc[i] = ColorUtils.distance(bg, fg);
125132
}
133+
134+
blendingChars.add(176);
135+
blendingChars.add(177);
136+
blendingChars.add(178);
137+
for (int i = 0; i < 256; i++) {
138+
if (visual.isCharEmpty(i) || visual.isCharFull(i)) {
139+
blendingChars.add(i);
140+
}
141+
}
126142
}
127143

128144
@Override
129145
public Applier applyMse(BufferedImage image, int px, int py) {
130146
final ImageLutHolder holder = new ImageLutHolder(visual, image, px, py, visual.getCharWidth(), visual.getCharHeight());
131147
float[] mseContrastPrecalc = new float[256];
148+
float[] macroRatioPrecalc = new float[256];
132149
for (int i = 0; i < 256; i++) {
133150
float imgContrast = holder.maxDistance;
134151
float chrContrast = colDistPrecalc[i];
135152
float contrastDiff = (imgContrast - chrContrast);
136153
mseContrastPrecalc[i] = contrastReduction * contrastDiff * contrastDiff;
154+
155+
macroRatioPrecalc[i] = blendingChars.contains(i) ? 1.0f : accurateApproximate;
137156
}
157+
final int colorMask = blinkingDisabled ? 0xFF : 0x7F;
138158
return (proposed, maxMse) -> {
139159
int chr = proposed.getCharacter();
140-
int col = blinkingDisabled ? proposed.getColor() : (proposed.getColor() & 0x7F);
160+
int col = proposed.getColor() & colorMask;
141161

142162
float mse = 0.0f;
143163
int[] dataMacro2x2 = holder.dataMacro2x2;
144164

145165
float mseContrastReduction = mseContrastPrecalc[col];
146-
147-
float macroRatio = accurateApproximate;
148-
if (blendingChars.contains(chr)) {
149-
macroRatio = 1.0f; // use only macro when blending
150-
}
166+
float macroRatio = macroRatioPrecalc[chr];
151167

152168
mse += dataMacro2x2.length * mseContrastReduction;
153169
if (mse <= maxMse) {

0 commit comments

Comments
 (0)