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