yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
visual_analysis_tool.cc
Go to the documentation of this file.
1
7
8#include <algorithm>
9#include <cmath>
10#include <map>
11#include <numeric>
12#include <set>
13#include <sstream>
14#include <vector>
15
16#include "absl/status/status.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
21#include "rom/rom.h"
22
23namespace yaze {
24namespace cli {
25namespace agent {
26namespace tools {
27
28// =============================================================================
29// VisualAnalysisBase Implementation
30// =============================================================================
31
33 const std::vector<uint8_t>& tile_a,
34 const std::vector<uint8_t>& tile_b) const {
35 if (tile_a.size() != tile_b.size() || tile_a.empty()) {
36 return 0.0;
37 }
38
39 // Compute Mean Absolute Error
40 double total_diff = 0.0;
41 for (size_t i = 0; i < tile_a.size(); ++i) {
42 total_diff += std::abs(static_cast<int>(tile_a[i]) -
43 static_cast<int>(tile_b[i]));
44 }
45
46 // Normalize to 0-100 scale (max diff per pixel is 255)
47 double max_possible_diff = tile_a.size() * 255.0;
48 double normalized_diff = total_diff / max_possible_diff;
49
50 // Convert to similarity (100 = identical, 0 = completely different)
51 return (1.0 - normalized_diff) * 100.0;
52}
53
55 const std::vector<uint8_t>& tile_a,
56 const std::vector<uint8_t>& tile_b) const {
57 if (tile_a.size() != tile_b.size() || tile_a.empty()) {
58 return 0.0;
59 }
60
61 const size_t n = tile_a.size();
62
63 // Compute means
64 double mean_a = 0.0, mean_b = 0.0;
65 for (size_t i = 0; i < n; ++i) {
66 mean_a += tile_a[i];
67 mean_b += tile_b[i];
68 }
69 mean_a /= n;
70 mean_b /= n;
71
72 // Compute variances and covariance
73 double var_a = 0.0, var_b = 0.0, covar = 0.0;
74 for (size_t i = 0; i < n; ++i) {
75 double diff_a = tile_a[i] - mean_a;
76 double diff_b = tile_b[i] - mean_b;
77 var_a += diff_a * diff_a;
78 var_b += diff_b * diff_b;
79 covar += diff_a * diff_b;
80 }
81 var_a /= n;
82 var_b /= n;
83 covar /= n;
84
85 // SSIM-like formula with stability constants
86 const double c1 = 6.5025; // (0.01 * 255)^2
87 const double c2 = 58.5225; // (0.03 * 255)^2
88
89 double numerator = (2.0 * mean_a * mean_b + c1) * (2.0 * covar + c2);
90 double denominator = (mean_a * mean_a + mean_b * mean_b + c1) *
91 (var_a + var_b + c2);
92
93 double ssim = numerator / denominator;
94
95 // Convert to 0-100 scale
96 return std::max(0.0, std::min(100.0, ssim * 100.0));
97}
98
99absl::StatusOr<std::vector<uint8_t>> VisualAnalysisBase::ExtractTile(
100 Rom* rom, int sheet_index, int tile_index) const {
101 if (!rom) {
102 return absl::InvalidArgumentError("ROM is null");
103 }
104
105 if (sheet_index < 0 || sheet_index >= kMaxSheets) {
106 return absl::InvalidArgumentError(
107 absl::StrCat("Invalid sheet index: ", sheet_index));
108 }
109
110 // Calculate tile position in sheet
111 int tiles_per_sheet = (kSheetWidth / kTileWidth) * (kSheetHeight / kTileHeight);
112 if (tile_index < 0 || tile_index >= tiles_per_sheet) {
113 return absl::InvalidArgumentError(
114 absl::StrCat("Invalid tile index: ", tile_index));
115 }
116
117 int x = (tile_index % kTilesPerRow) * kTileWidth;
118 int y = (tile_index / kTilesPerRow) * kTileHeight;
119
120 return ExtractTileAtPosition(rom, sheet_index, x, y);
121}
122
123absl::StatusOr<std::vector<uint8_t>> VisualAnalysisBase::ExtractTileAtPosition(
124 Rom* rom, int sheet_index, int x, int y) const {
125 if (!rom) {
126 return absl::InvalidArgumentError("ROM is null");
127 }
128
129 // Get graphics sheet data from ROM
130 // Graphics sheets are stored in a compressed format in the ROM
131 // For now, we'll work with the decompressed data if available
132
133 // Calculate the base address for the graphics sheet
134 // ALTTP graphics are stored at various locations depending on the sheet
135 // This is a simplified extraction - in practice, this would need to
136 // interface with the existing graphics loading code
137
138 std::vector<uint8_t> tile_data(kTilePixels, 0);
139
140 // For the agent tool, we should interface with existing graphics
141 // decompression. For now, return a placeholder that indicates
142 // we need ROM context with loaded graphics.
143
144 // Access graphics from Arena if available
145 const auto& gfx_sheets = gfx::Arena::Get().gfx_sheets();
146 if (gfx_sheets.empty() || !gfx_sheets[0].is_active()) {
147 return absl::FailedPreconditionError(
148 "Graphics not loaded. Load ROM with graphics first.");
149 }
150
151 // Check sheet index is valid
152 if (sheet_index >= static_cast<int>(gfx_sheets.size())) {
153 return absl::OutOfRangeError(
154 absl::StrCat("Sheet ", sheet_index, " out of range"));
155 }
156
157 const auto& sheet = gfx_sheets[sheet_index];
158 if (!sheet.is_active()) {
159 return absl::FailedPreconditionError(
160 absl::StrCat("Sheet ", sheet_index, " is not loaded"));
161 }
162
163 const auto& sheet_data = sheet.vector();
164
165 // Extract 8x8 tile from the sheet
166 for (int row = 0; row < kTileHeight; ++row) {
167 size_t src_offset = (y + row) * kSheetWidth + x;
168 for (int col = 0; col < kTileWidth; ++col) {
169 if (src_offset + col < sheet_data.size()) {
170 tile_data[row * kTileWidth + col] = sheet_data[src_offset + col];
171 }
172 }
173 }
174
175 return tile_data;
176}
177
179 const std::vector<uint8_t>& data) const {
180 if (data.empty()) {
181 return true;
182 }
183
184 // Check if all bytes are 0x00 (fully transparent/black)
185 bool all_zero = std::all_of(data.begin(), data.end(),
186 [](uint8_t b) { return b == 0x00; });
187 if (all_zero) {
188 return true;
189 }
190
191 // Check if all bytes are 0xFF (common empty pattern)
192 bool all_ff = std::all_of(data.begin(), data.end(),
193 [](uint8_t b) { return b == 0xFF; });
194 if (all_ff) {
195 return true;
196 }
197
198 // Check if mostly empty (>95% zeroes)
199 int zero_count = std::count(data.begin(), data.end(), 0x00);
200 if (static_cast<double>(zero_count) / data.size() > 0.95) {
201 return true;
202 }
203
204 return false;
205}
206
208 // Standard SNES sheet is 128x32 pixels = 16x4 = 64 tiles
210}
211
213 const std::vector<TileSimilarityMatch>& matches) const {
214 std::ostringstream json;
215 json << "{\n \"matches\": [\n";
216
217 for (size_t i = 0; i < matches.size(); ++i) {
218 const auto& m = matches[i];
219 json << " {\n";
220 json << " \"tile_id\": " << m.tile_id << ",\n";
221 json << " \"similarity_score\": "
222 << absl::StrFormat("%.2f", m.similarity_score) << ",\n";
223 json << " \"sheet_index\": " << m.sheet_index << ",\n";
224 json << " \"x_position\": " << m.x_position << ",\n";
225 json << " \"y_position\": " << m.y_position << "\n";
226 json << " }";
227 if (i < matches.size() - 1) json << ",";
228 json << "\n";
229 }
230
231 json << " ],\n";
232 json << " \"total_matches\": " << matches.size() << "\n";
233 json << "}\n";
234
235 return json.str();
236}
237
239 const std::vector<UnusedRegion>& regions) const {
240 std::ostringstream json;
241 json << "{\n \"unused_regions\": [\n";
242
243 for (size_t i = 0; i < regions.size(); ++i) {
244 const auto& r = regions[i];
245 json << " {\n";
246 json << " \"sheet_index\": " << r.sheet_index << ",\n";
247 json << " \"x\": " << r.x << ",\n";
248 json << " \"y\": " << r.y << ",\n";
249 json << " \"width\": " << r.width << ",\n";
250 json << " \"height\": " << r.height << ",\n";
251 json << " \"tile_count\": " << r.tile_count << "\n";
252 json << " }";
253 if (i < regions.size() - 1) json << ",";
254 json << "\n";
255 }
256
257 int total_tiles = 0;
258 for (const auto& r : regions) {
259 total_tiles += r.tile_count;
260 }
261
262 json << " ],\n";
263 json << " \"total_regions\": " << regions.size() << ",\n";
264 json << " \"total_free_tiles\": " << total_tiles << "\n";
265 json << "}\n";
266
267 return json.str();
268}
269
271 const std::vector<PaletteUsageStats>& stats) const {
272 std::ostringstream json;
273 json << "{\n \"palette_usage\": [\n";
274
275 for (size_t i = 0; i < stats.size(); ++i) {
276 const auto& s = stats[i];
277 json << " {\n";
278 json << " \"palette_index\": " << s.palette_index << ",\n";
279 json << " \"usage_count\": " << s.usage_count << ",\n";
280 json << " \"usage_percentage\": "
281 << absl::StrFormat("%.2f", s.usage_percentage) << ",\n";
282 json << " \"used_by_maps\": [";
283 for (size_t j = 0; j < s.used_by_maps.size(); ++j) {
284 json << s.used_by_maps[j];
285 if (j < s.used_by_maps.size() - 1) json << ", ";
286 }
287 json << "]\n";
288 json << " }";
289 if (i < stats.size() - 1) json << ",";
290 json << "\n";
291 }
292
293 json << " ]\n";
294 json << "}\n";
295
296 return json.str();
297}
298
300 const std::vector<TileUsageEntry>& entries) const {
301 std::ostringstream json;
302 json << "{\n \"tile_histogram\": [\n";
303
304 for (size_t i = 0; i < entries.size(); ++i) {
305 const auto& e = entries[i];
306 json << " {\n";
307 json << " \"tile_id\": " << e.tile_id << ",\n";
308 json << " \"usage_count\": " << e.usage_count << ",\n";
309 json << " \"usage_percentage\": "
310 << absl::StrFormat("%.2f", e.usage_percentage) << ",\n";
311 json << " \"locations\": [";
312 for (size_t j = 0; j < std::min(e.locations.size(), size_t(10)); ++j) {
313 json << e.locations[j];
314 if (j < std::min(e.locations.size(), size_t(10)) - 1) json << ", ";
315 }
316 if (e.locations.size() > 10) json << ", ...";
317 json << "]\n";
318 json << " }";
319 if (i < entries.size() - 1) json << ",";
320 json << "\n";
321 }
322
323 json << " ],\n";
324 json << " \"total_entries\": " << entries.size() << "\n";
325 json << "}\n";
326
327 return json.str();
328}
329
330// =============================================================================
331// TileSimilarityTool Implementation
332// =============================================================================
333
335 const resources::ArgumentParser& parser) {
336 return parser.RequireArgs({"tile_id"});
337}
338
340 Rom* rom, const resources::ArgumentParser& parser,
341 resources::OutputFormatter& formatter) {
342 // Parse arguments
343 auto tile_id_or = parser.GetInt("tile_id");
344 if (!tile_id_or.ok()) {
345 return absl::InvalidArgumentError("tile_id is required");
346 }
347 int tile_id = tile_id_or.value();
348 int sheet = parser.GetInt("sheet").value_or(0);
349 int threshold = parser.GetInt("threshold").value_or(80);
350 std::string method = parser.GetString("method").value_or("structural");
351
352 // Validate threshold
353 threshold = std::clamp(threshold, 0, 100);
354
355 // Extract reference tile
356 auto ref_tile_or = ExtractTile(rom, sheet, tile_id);
357 if (!ref_tile_or.ok()) {
358 return ref_tile_or.status();
359 }
360 const auto& ref_tile = ref_tile_or.value();
361
362 // Find similar tiles across all sheets (or specified sheet)
363 std::vector<TileSimilarityMatch> matches;
364
365 bool has_sheet = parser.GetString("sheet").has_value();
366 int start_sheet = has_sheet ? sheet : 0;
367 int end_sheet = has_sheet ? sheet + 1 : kMaxSheets;
368
369 for (int s = start_sheet; s < end_sheet; ++s) {
370 int tile_count = GetTileCountForSheet(s);
371
372 for (int t = 0; t < tile_count; ++t) {
373 // Skip the reference tile itself
374 if (s == sheet && t == tile_id) {
375 continue;
376 }
377
378 auto cmp_tile_or = ExtractTile(rom, s, t);
379 if (!cmp_tile_or.ok()) {
380 continue; // Skip tiles that can't be extracted
381 }
382 const auto& cmp_tile = cmp_tile_or.value();
383
384 // Compute similarity
385 double score;
386 if (method == "pixel") {
387 score = ComputePixelDifference(ref_tile, cmp_tile);
388 } else {
389 score = ComputeStructuralSimilarity(ref_tile, cmp_tile);
390 }
391
392 if (score >= threshold) {
394 match.tile_id = t;
395 match.similarity_score = score;
396 match.sheet_index = s;
397 match.x_position = (t % kTilesPerRow) * kTileWidth;
398 match.y_position = (t / kTilesPerRow) * kTileHeight;
399 matches.push_back(match);
400 }
401 }
402 }
403
404 // Sort by similarity score descending
405 std::sort(matches.begin(), matches.end(),
406 [](const auto& a, const auto& b) {
407 return a.similarity_score > b.similarity_score;
408 });
409
410 // Limit to top 50 matches
411 if (matches.size() > 50) {
412 matches.resize(50);
413 }
414
415 // Output results
416 std::cout << FormatMatchesAsJson(matches);
417
418 return absl::OkStatus();
419}
420
421// =============================================================================
422// SpritesheetAnalysisTool Implementation
423// =============================================================================
424
426 const resources::ArgumentParser& /*parser*/) {
427 return absl::OkStatus();
428}
429
431 Rom* rom, const resources::ArgumentParser& parser,
432 resources::OutputFormatter& formatter) {
433 int tile_size = parser.GetInt("tile_size").value_or(8);
434 if (tile_size != 8 && tile_size != 16) {
435 tile_size = 8;
436 }
437
438 std::vector<UnusedRegion> all_regions;
439
440 auto sheet_arg = parser.GetString("sheet");
441 if (sheet_arg.has_value()) {
442 int sheet = parser.GetInt("sheet").value_or(0);
443 auto regions = FindUnusedRegions(rom, sheet, tile_size);
444 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
445 } else {
446 // Analyze all sheets
447 for (int s = 0; s < kMaxSheets; ++s) {
448 auto regions = FindUnusedRegions(rom, s, tile_size);
449 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
450 }
451 }
452
453 // Merge adjacent regions
454 all_regions = MergeAdjacentRegions(all_regions);
455
456 // Sort by size (largest first)
457 std::sort(all_regions.begin(), all_regions.end(),
458 [](const auto& a, const auto& b) {
459 return a.tile_count > b.tile_count;
460 });
461
462 // Output results
463 std::cout << FormatRegionsAsJson(all_regions);
464
465 return absl::OkStatus();
466}
467
469 Rom* rom, int sheet_index, int tile_size) const {
470 std::vector<UnusedRegion> regions;
471
472 int tiles_x = kSheetWidth / tile_size;
473 int tiles_y = kSheetHeight / tile_size;
474 int pixels_per_tile = tile_size * tile_size;
475
476 for (int ty = 0; ty < tiles_y; ++ty) {
477 for (int tx = 0; tx < tiles_x; ++tx) {
478 // Extract tile region
479 std::vector<uint8_t> tile_data;
480 tile_data.reserve(pixels_per_tile);
481
482 for (int py = 0; py < tile_size; ++py) {
483 for (int px = 0; px < tile_size; ++px) {
484 auto pixel_or = ExtractTileAtPosition(
485 rom, sheet_index, tx * tile_size, ty * tile_size);
486 if (pixel_or.ok() && !pixel_or.value().empty()) {
487 // Get the specific pixel
488 int local_idx = py * tile_size + px;
489 if (local_idx < static_cast<int>(pixel_or.value().size())) {
490 tile_data.push_back(pixel_or.value()[local_idx]);
491 }
492 }
493 }
494 }
495
496 if (IsRegionEmpty(tile_data)) {
497 UnusedRegion region;
498 region.sheet_index = sheet_index;
499 region.x = tx * tile_size;
500 region.y = ty * tile_size;
501 region.width = tile_size;
502 region.height = tile_size;
503 region.tile_count = (tile_size == 8) ? 1 : 4;
504 regions.push_back(region);
505 }
506 }
507 }
508
509 return regions;
510}
511
513 const std::vector<UnusedRegion>& regions) const {
514 if (regions.empty()) {
515 return regions;
516 }
517
518 // Group by sheet
519 std::map<int, std::vector<UnusedRegion>> by_sheet;
520 for (const auto& r : regions) {
521 by_sheet[r.sheet_index].push_back(r);
522 }
523
524 std::vector<UnusedRegion> merged;
525
526 for (auto& [sheet, sheet_regions] : by_sheet) {
527 // Simple horizontal merging for adjacent tiles
528 std::sort(sheet_regions.begin(), sheet_regions.end(),
529 [](const auto& a, const auto& b) {
530 if (a.y != b.y) return a.y < b.y;
531 return a.x < b.x;
532 });
533
534 for (size_t i = 0; i < sheet_regions.size();) {
535 UnusedRegion current = sheet_regions[i];
536
537 // Try to merge with following regions on the same row
538 size_t j = i + 1;
539 while (j < sheet_regions.size() &&
540 sheet_regions[j].y == current.y &&
541 sheet_regions[j].x == current.x + current.width) {
542 current.width += sheet_regions[j].width;
543 current.tile_count += sheet_regions[j].tile_count;
544 ++j;
545 }
546
547 merged.push_back(current);
548 i = j;
549 }
550 }
551
552 return merged;
553}
554
555// =============================================================================
556// PaletteUsageTool Implementation
557// =============================================================================
558
560 const resources::ArgumentParser& /*parser*/) {
561 return absl::OkStatus();
562}
563
565 Rom* rom, const resources::ArgumentParser& parser,
566 resources::OutputFormatter& formatter) {
567 std::string type = parser.GetString("type").value_or("all");
568
569 std::vector<PaletteUsageStats> stats;
570
571 if (type == "overworld" || type == "all") {
572 auto ow_stats = AnalyzeOverworldPalettes(rom);
573 stats.insert(stats.end(), ow_stats.begin(), ow_stats.end());
574 }
575
576 if (type == "dungeon" || type == "all") {
577 auto dg_stats = AnalyzeDungeonPalettes(rom);
578 // Merge with overworld stats or add new entries
579 for (const auto& ds : dg_stats) {
580 bool found = false;
581 for (auto& s : stats) {
582 if (s.palette_index == ds.palette_index) {
583 s.usage_count += ds.usage_count;
584 s.used_by_maps.insert(s.used_by_maps.end(),
585 ds.used_by_maps.begin(),
586 ds.used_by_maps.end());
587 found = true;
588 break;
589 }
590 }
591 if (!found) {
592 stats.push_back(ds);
593 }
594 }
595 }
596
597 // Calculate percentages
598 int total_usage = 0;
599 for (const auto& s : stats) {
600 total_usage += s.usage_count;
601 }
602 for (auto& s : stats) {
603 s.usage_percentage = total_usage > 0
604 ? (static_cast<double>(s.usage_count) / total_usage) * 100.0
605 : 0.0;
606 }
607
608 // Sort by usage count
609 std::sort(stats.begin(), stats.end(),
610 [](const auto& a, const auto& b) {
611 return a.usage_count > b.usage_count;
612 });
613
614 std::cout << FormatPaletteUsageAsJson(stats);
615
616 return absl::OkStatus();
617}
618
619std::vector<PaletteUsageStats> PaletteUsageTool::AnalyzeOverworldPalettes(
620 Rom* rom) const {
621 std::map<int, PaletteUsageStats> palette_map;
622
623 // Overworld palette indices used by each map are stored in ROM
624 // Light World uses palettes 0-7, Dark World uses 8-15 typically
625 // For now, return basic structure - full implementation would
626 // read actual palette assignments from ROM
627
628 // Initialize with 8 main palettes
629 for (int i = 0; i < 8; ++i) {
630 PaletteUsageStats stats;
631 stats.palette_index = i;
632 stats.usage_count = 0;
633 palette_map[i] = stats;
634 }
635
636 // Count usage across 128 overworld maps (0-63 Light, 64-127 Dark)
637 for (int map_id = 0; map_id < 128; ++map_id) {
638 // In actual implementation, read palette from map header
639 // For now, use a heuristic: Light World maps use lower palettes
640 int palette_idx = (map_id < 64) ? (map_id % 8) : (map_id % 8);
641 palette_map[palette_idx].usage_count++;
642 palette_map[palette_idx].used_by_maps.push_back(map_id);
643 }
644
645 std::vector<PaletteUsageStats> result;
646 for (const auto& [_, stats] : palette_map) {
647 result.push_back(stats);
648 }
649
650 return result;
651}
652
653std::vector<PaletteUsageStats> PaletteUsageTool::AnalyzeDungeonPalettes(
654 Rom* rom) const {
655 std::map<int, PaletteUsageStats> palette_map;
656
657 // Dungeons use a different palette system
658 // Each room can reference one of several dungeon palettes
659
660 // Initialize with dungeon palettes (typically 0-15)
661 for (int i = 0; i < 16; ++i) {
662 PaletteUsageStats stats;
663 stats.palette_index = i + 100; // Offset to distinguish from OW
664 stats.usage_count = 0;
665 palette_map[i + 100] = stats;
666 }
667
668 // Count usage across ~320 dungeon rooms
669 for (int room_id = 0; room_id < 320; ++room_id) {
670 // In actual implementation, read palette from room header
671 int palette_idx = 100 + (room_id % 16);
672 palette_map[palette_idx].usage_count++;
673 palette_map[palette_idx].used_by_maps.push_back(room_id);
674 }
675
676 std::vector<PaletteUsageStats> result;
677 for (const auto& [_, stats] : palette_map) {
678 result.push_back(stats);
679 }
680
681 return result;
682}
683
684// =============================================================================
685// TileHistogramTool Implementation
686// =============================================================================
687
689 const resources::ArgumentParser& /*parser*/) {
690 return absl::OkStatus();
691}
692
694 Rom* rom, const resources::ArgumentParser& parser,
695 resources::OutputFormatter& formatter) {
696 std::string type = parser.GetString("type").value_or("overworld");
697 int top_n = parser.GetInt("top").value_or(20);
698
699 std::map<int, TileUsageEntry> usage_map;
700
701 if (type == "overworld") {
702 usage_map = CountOverworldTiles(rom);
703 } else if (type == "dungeon") {
704 usage_map = CountDungeonTiles(rom);
705 } else {
706 // Both
707 auto ow = CountOverworldTiles(rom);
708 auto dg = CountDungeonTiles(rom);
709 usage_map = ow;
710 for (const auto& [tile_id, entry] : dg) {
711 if (usage_map.count(tile_id)) {
712 usage_map[tile_id].usage_count += entry.usage_count;
713 usage_map[tile_id].locations.insert(
714 usage_map[tile_id].locations.end(),
715 entry.locations.begin(), entry.locations.end());
716 } else {
717 usage_map[tile_id] = entry;
718 }
719 }
720 }
721
722 // Convert to vector and calculate percentages
723 std::vector<TileUsageEntry> entries;
724 int total_usage = 0;
725 for (const auto& [_, entry] : usage_map) {
726 total_usage += entry.usage_count;
727 entries.push_back(entry);
728 }
729
730 for (auto& e : entries) {
731 e.usage_percentage = total_usage > 0
732 ? (static_cast<double>(e.usage_count) / total_usage) * 100.0
733 : 0.0;
734 }
735
736 // Sort by usage count descending
737 std::sort(entries.begin(), entries.end(),
738 [](const auto& a, const auto& b) {
739 return a.usage_count > b.usage_count;
740 });
741
742 // Limit to top N
743 if (static_cast<int>(entries.size()) > top_n) {
744 entries.resize(top_n);
745 }
746
747 std::cout << FormatHistogramAsJson(entries);
748
749 return absl::OkStatus();
750}
751
752std::map<int, TileUsageEntry> TileHistogramTool::CountOverworldTiles(
753 Rom* rom) const {
754 std::map<int, TileUsageEntry> usage;
755
756 // Overworld uses Tile16 (16x16 metatiles) composed of Tile32 (32x32 blocks)
757 // Each overworld map is 32x32 Tile16s = 1024 tiles per map
758 // There are 160 overworld maps total
759
760 // For demonstration, create synthetic data
761 // In actual implementation, read tilemap data from ROM
762
763 for (int map_id = 0; map_id < 160; ++map_id) {
764 // Simulate reading 1024 tiles per map
765 for (int i = 0; i < 1024; ++i) {
766 // In actual implementation: int tile_id = ReadTileFromMap(rom, map_id, i);
767 int tile_id = (map_id * 7 + i) % 512; // Synthetic distribution
768
769 if (usage.count(tile_id) == 0) {
770 TileUsageEntry entry;
771 entry.tile_id = tile_id;
772 entry.usage_count = 0;
773 usage[tile_id] = entry;
774 }
775 usage[tile_id].usage_count++;
776
777 // Track first 10 locations only to save memory
778 if (usage[tile_id].locations.size() < 10) {
779 usage[tile_id].locations.push_back(map_id);
780 }
781 }
782 }
783
784 return usage;
785}
786
787std::map<int, TileUsageEntry> TileHistogramTool::CountDungeonTiles(
788 Rom* rom) const {
789 std::map<int, TileUsageEntry> usage;
790
791 // Dungeons use different tile arrangements
792 // Each room varies in size but typically uses floor, wall, and object tiles
793
794 for (int room_id = 0; room_id < 320; ++room_id) {
795 // Simulate tile usage per room
796 for (int i = 0; i < 256; ++i) {
797 int tile_id = (room_id * 11 + i) % 256; // Synthetic distribution
798
799 if (usage.count(tile_id) == 0) {
800 TileUsageEntry entry;
801 entry.tile_id = tile_id;
802 entry.usage_count = 0;
803 usage[tile_id] = entry;
804 }
805 usage[tile_id].usage_count++;
806
807 if (usage[tile_id].locations.size() < 10) {
808 usage[tile_id].locations.push_back(room_id + 1000); // Offset for dungeons
809 }
810 }
811 }
812
813 return usage;
814}
815
816// =============================================================================
817// OpenCV Integration (Optional)
818// =============================================================================
819
820#ifdef YAZE_WITH_OPENCV
821#include <opencv2/core.hpp>
822#include <opencv2/imgproc.hpp>
823#include <opencv2/features2d.hpp>
824
825namespace opencv {
826
827std::pair<std::pair<int, int>, double> TemplateMatch(
828 const std::vector<uint8_t>& reference,
829 const std::vector<uint8_t>& search_region, int width, int height,
830 int method) {
831 // Convert to cv::Mat
832 cv::Mat ref_mat(8, 8, CV_8UC1, const_cast<uint8_t*>(reference.data()));
833 cv::Mat search_mat(height, width, CV_8UC1,
834 const_cast<uint8_t*>(search_region.data()));
835
836 cv::Mat result;
837 cv::matchTemplate(search_mat, ref_mat, result, method);
838
839 double min_val, max_val;
840 cv::Point min_loc, max_loc;
841 cv::minMaxLoc(result, &min_val, &max_val, &min_loc, &max_loc);
842
843 // For TM_SQDIFF methods, lower is better
844 if (method == cv::TM_SQDIFF || method == cv::TM_SQDIFF_NORMED) {
845 return {{min_loc.x, min_loc.y}, 1.0 - min_val};
846 }
847
848 return {{max_loc.x, max_loc.y}, max_val};
849}
850
851double FeatureMatch(const std::vector<uint8_t>& tile_a,
852 const std::vector<uint8_t>& tile_b) {
853 cv::Mat mat_a(8, 8, CV_8UC1, const_cast<uint8_t*>(tile_a.data()));
854 cv::Mat mat_b(8, 8, CV_8UC1, const_cast<uint8_t*>(tile_b.data()));
855
856 // ORB detector
857 cv::Ptr<cv::ORB> orb = cv::ORB::create();
858
859 std::vector<cv::KeyPoint> kp_a, kp_b;
860 cv::Mat desc_a, desc_b;
861
862 orb->detectAndCompute(mat_a, cv::noArray(), kp_a, desc_a);
863 orb->detectAndCompute(mat_b, cv::noArray(), kp_b, desc_b);
864
865 if (desc_a.empty() || desc_b.empty()) {
866 return 0.0; // No features found
867 }
868
869 // BFMatcher
870 cv::BFMatcher matcher(cv::NORM_HAMMING);
871 std::vector<cv::DMatch> matches;
872 matcher.match(desc_a, desc_b, matches);
873
874 if (matches.empty()) {
875 return 0.0;
876 }
877
878 // Calculate match score based on distances
879 double total_dist = 0.0;
880 for (const auto& m : matches) {
881 total_dist += m.distance;
882 }
883
884 // Normalize: lower distance = higher similarity
885 double avg_dist = total_dist / matches.size();
886 return std::max(0.0, 100.0 - avg_dist);
887}
888
889double ComputeSSIM(const std::vector<uint8_t>& tile_a,
890 const std::vector<uint8_t>& tile_b) {
891 cv::Mat mat_a(8, 8, CV_8UC1, const_cast<uint8_t*>(tile_a.data()));
892 cv::Mat mat_b(8, 8, CV_8UC1, const_cast<uint8_t*>(tile_b.data()));
893
894 const double C1 = 6.5025, C2 = 58.5225;
895
896 cv::Mat I1, I2;
897 mat_a.convertTo(I1, CV_32F);
898 mat_b.convertTo(I2, CV_32F);
899
900 cv::Mat I1_2 = I1.mul(I1);
901 cv::Mat I2_2 = I2.mul(I2);
902 cv::Mat I1_I2 = I1.mul(I2);
903
904 cv::Mat mu1, mu2;
905 cv::GaussianBlur(I1, mu1, cv::Size(3, 3), 1.5);
906 cv::GaussianBlur(I2, mu2, cv::Size(3, 3), 1.5);
907
908 cv::Mat mu1_2 = mu1.mul(mu1);
909 cv::Mat mu2_2 = mu2.mul(mu2);
910 cv::Mat mu1_mu2 = mu1.mul(mu2);
911
912 cv::Mat sigma1_2, sigma2_2, sigma12;
913 cv::GaussianBlur(I1_2, sigma1_2, cv::Size(3, 3), 1.5);
914 sigma1_2 -= mu1_2;
915 cv::GaussianBlur(I2_2, sigma2_2, cv::Size(3, 3), 1.5);
916 sigma2_2 -= mu2_2;
917 cv::GaussianBlur(I1_I2, sigma12, cv::Size(3, 3), 1.5);
918 sigma12 -= mu1_mu2;
919
920 cv::Mat t1 = 2 * mu1_mu2 + C1;
921 cv::Mat t2 = 2 * sigma12 + C2;
922 cv::Mat t3 = t1.mul(t2);
923
924 t1 = mu1_2 + mu2_2 + C1;
925 t2 = sigma1_2 + sigma2_2 + C2;
926 t1 = t1.mul(t2);
927
928 cv::Mat ssim_map;
929 cv::divide(t3, t1, ssim_map);
930
931 cv::Scalar mssim = cv::mean(ssim_map);
932 return mssim[0] * 100.0; // Convert to percentage
933}
934
935} // namespace opencv
936#endif // YAZE_WITH_OPENCV
937
938} // namespace tools
939} // namespace agent
940} // namespace cli
941} // namespace yaze
942
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
std::vector< PaletteUsageStats > AnalyzeOverworldPalettes(Rom *rom) const
Analyze overworld palette usage.
std::vector< PaletteUsageStats > AnalyzeDungeonPalettes(Rom *rom) const
Analyze dungeon palette usage.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
std::vector< UnusedRegion > MergeAdjacentRegions(const std::vector< UnusedRegion > &regions) const
Merge adjacent empty regions.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
std::vector< UnusedRegion > FindUnusedRegions(Rom *rom, int sheet_index, int tile_size) const
Find contiguous empty regions in a sheet.
std::map< int, TileUsageEntry > CountDungeonTiles(Rom *rom) const
Count tile usage in dungeons.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
std::map< int, TileUsageEntry > CountOverworldTiles(Rom *rom) const
Count tile usage in overworld.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
double ComputePixelDifference(const std::vector< uint8_t > &tile_a, const std::vector< uint8_t > &tile_b) const
Compute simple pixel difference (Mean Absolute Error)
int GetTileCountForSheet(int sheet_index) const
Get the number of tiles in a graphics sheet.
absl::StatusOr< std::vector< uint8_t > > ExtractTile(Rom *rom, int sheet_index, int tile_index) const
Extract 8x8 tile data from ROM graphics.
std::string FormatHistogramAsJson(const std::vector< TileUsageEntry > &entries) const
Format tile histogram as JSON.
std::string FormatRegionsAsJson(const std::vector< UnusedRegion > &regions) const
Format unused regions as JSON.
std::string FormatMatchesAsJson(const std::vector< TileSimilarityMatch > &matches) const
Format similarity matches as JSON.
absl::StatusOr< std::vector< uint8_t > > ExtractTileAtPosition(Rom *rom, int sheet_index, int x, int y) const
Extract 8x8 tile at specific pixel coordinates.
bool IsRegionEmpty(const std::vector< uint8_t > &data) const
Check if a tile region is empty (all 0x00 or 0xFF)
double ComputeStructuralSimilarity(const std::vector< uint8_t > &tile_a, const std::vector< uint8_t > &tile_b) const
Compute structural similarity index (SSIM-like)
std::string FormatPaletteUsageAsJson(const std::vector< PaletteUsageStats > &stats) const
Format palette usage as JSON.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
absl::Status RequireArgs(const std::vector< std::string > &required) const
Validate that required arguments are present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Utility for consistent output formatting across commands.
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:101
static Arena & Get()
Definition arena.cc:19
Result of a tile similarity comparison.
Represents a contiguous unused region in a spritesheet.
Visual analysis tools for AI agents.