16#include "absl/status/status.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
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()) {
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]));
47 double max_possible_diff = tile_a.size() * 255.0;
48 double normalized_diff = total_diff / max_possible_diff;
51 return (1.0 - normalized_diff) * 100.0;
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()) {
61 const size_t n = tile_a.size();
64 double mean_a = 0.0, mean_b = 0.0;
65 for (
size_t i = 0; i < n; ++i) {
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;
86 const double c1 = 6.5025;
87 const double c2 = 58.5225;
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) *
93 double ssim = numerator / denominator;
96 return std::max(0.0, std::min(100.0, ssim * 100.0));
100 Rom* rom,
int sheet_index,
int tile_index)
const {
102 return absl::InvalidArgumentError(
"ROM is null");
105 if (sheet_index < 0 || sheet_index >=
kMaxSheets) {
106 return absl::InvalidArgumentError(
107 absl::StrCat(
"Invalid sheet index: ", sheet_index));
112 if (tile_index < 0 || tile_index >= tiles_per_sheet) {
113 return absl::InvalidArgumentError(
114 absl::StrCat(
"Invalid tile index: ", tile_index));
124 Rom* rom,
int sheet_index,
int x,
int y)
const {
126 return absl::InvalidArgumentError(
"ROM is null");
146 if (gfx_sheets.empty() || !gfx_sheets[0].is_active()) {
147 return absl::FailedPreconditionError(
148 "Graphics not loaded. Load ROM with graphics first.");
152 if (sheet_index >=
static_cast<int>(gfx_sheets.size())) {
153 return absl::OutOfRangeError(
154 absl::StrCat(
"Sheet ", sheet_index,
" out of range"));
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"));
163 const auto& sheet_data = sheet.vector();
169 if (src_offset + col < sheet_data.size()) {
170 tile_data[row *
kTileWidth + col] = sheet_data[src_offset + col];
179 const std::vector<uint8_t>& data)
const {
185 bool all_zero = std::all_of(data.begin(), data.end(),
186 [](uint8_t b) { return b == 0x00; });
192 bool all_ff = std::all_of(data.begin(), data.end(),
193 [](uint8_t b) { return b == 0xFF; });
199 int zero_count = std::count(data.begin(), data.end(), 0x00);
200 if (
static_cast<double>(zero_count) / data.size() > 0.95) {
213 const std::vector<TileSimilarityMatch>& matches)
const {
214 std::ostringstream
json;
215 json <<
"{\n \"matches\": [\n";
217 for (
size_t i = 0; i < matches.size(); ++i) {
218 const auto& m = matches[i];
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";
227 if (i < matches.size() - 1)
json <<
",";
232 json <<
" \"total_matches\": " << matches.size() <<
"\n";
239 const std::vector<UnusedRegion>& regions)
const {
240 std::ostringstream
json;
241 json <<
"{\n \"unused_regions\": [\n";
243 for (
size_t i = 0; i < regions.size(); ++i) {
244 const auto& r = regions[i];
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";
253 if (i < regions.size() - 1)
json <<
",";
258 for (
const auto& r : regions) {
259 total_tiles += r.tile_count;
263 json <<
" \"total_regions\": " << regions.size() <<
",\n";
264 json <<
" \"total_free_tiles\": " << total_tiles <<
"\n";
271 const std::vector<PaletteUsageStats>& stats)
const {
272 std::ostringstream
json;
273 json <<
"{\n \"palette_usage\": [\n";
275 for (
size_t i = 0; i < stats.size(); ++i) {
276 const auto& s = stats[i];
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 <<
", ";
289 if (i < stats.size() - 1)
json <<
",";
300 const std::vector<TileUsageEntry>& entries)
const {
301 std::ostringstream
json;
302 json <<
"{\n \"tile_histogram\": [\n";
304 for (
size_t i = 0; i < entries.size(); ++i) {
305 const auto& e = entries[i];
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 <<
", ";
316 if (e.locations.size() > 10)
json <<
", ...";
319 if (i < entries.size() - 1)
json <<
",";
324 json <<
" \"total_entries\": " << entries.size() <<
"\n";
343 auto tile_id_or = parser.
GetInt(
"tile_id");
344 if (!tile_id_or.ok()) {
345 return absl::InvalidArgumentError(
"tile_id is required");
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");
353 threshold = std::clamp(threshold, 0, 100);
356 auto ref_tile_or =
ExtractTile(rom, sheet, tile_id);
357 if (!ref_tile_or.ok()) {
358 return ref_tile_or.status();
360 const auto& ref_tile = ref_tile_or.value();
363 std::vector<TileSimilarityMatch> matches;
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;
369 for (
int s = start_sheet; s < end_sheet; ++s) {
372 for (
int t = 0; t < tile_count; ++t) {
374 if (s == sheet && t == tile_id) {
379 if (!cmp_tile_or.ok()) {
382 const auto& cmp_tile = cmp_tile_or.value();
386 if (method ==
"pixel") {
392 if (score >= threshold) {
399 matches.push_back(match);
405 std::sort(matches.begin(), matches.end(),
406 [](
const auto& a,
const auto& b) {
407 return a.similarity_score > b.similarity_score;
411 if (matches.size() > 50) {
418 return absl::OkStatus();
427 return absl::OkStatus();
433 int tile_size = parser.
GetInt(
"tile_size").value_or(8);
434 if (tile_size != 8 && tile_size != 16) {
438 std::vector<UnusedRegion> all_regions;
440 auto sheet_arg = parser.
GetString(
"sheet");
441 if (sheet_arg.has_value()) {
442 int sheet = parser.
GetInt(
"sheet").value_or(0);
444 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
449 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
457 std::sort(all_regions.begin(), all_regions.end(),
458 [](
const auto& a,
const auto& b) {
459 return a.tile_count > b.tile_count;
465 return absl::OkStatus();
469 Rom* rom,
int sheet_index,
int tile_size)
const {
470 std::vector<UnusedRegion> regions;
474 int pixels_per_tile = tile_size * tile_size;
476 for (
int ty = 0; ty < tiles_y; ++ty) {
477 for (
int tx = 0; tx < tiles_x; ++tx) {
479 std::vector<uint8_t> tile_data;
480 tile_data.reserve(pixels_per_tile);
482 for (
int py = 0; py < tile_size; ++py) {
483 for (
int px = 0; px < tile_size; ++px) {
485 rom, sheet_index, tx * tile_size, ty * tile_size);
486 if (pixel_or.ok() && !pixel_or.value().empty()) {
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]);
499 region.
x = tx * tile_size;
500 region.
y = ty * tile_size;
501 region.
width = tile_size;
502 region.
height = tile_size;
504 regions.push_back(region);
513 const std::vector<UnusedRegion>& regions)
const {
514 if (regions.empty()) {
519 std::map<int, std::vector<UnusedRegion>> by_sheet;
520 for (
const auto& r : regions) {
521 by_sheet[r.sheet_index].push_back(r);
524 std::vector<UnusedRegion> merged;
526 for (
auto& [sheet, sheet_regions] : by_sheet) {
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;
534 for (
size_t i = 0; i < sheet_regions.size();) {
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;
547 merged.push_back(current);
561 return absl::OkStatus();
567 std::string type = parser.
GetString(
"type").value_or(
"all");
569 std::vector<PaletteUsageStats> stats;
571 if (type ==
"overworld" || type ==
"all") {
573 stats.insert(stats.end(), ow_stats.begin(), ow_stats.end());
576 if (type ==
"dungeon" || type ==
"all") {
579 for (
const auto& ds : dg_stats) {
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());
599 for (
const auto& s : stats) {
600 total_usage += s.usage_count;
602 for (
auto& s : stats) {
603 s.usage_percentage = total_usage > 0
604 ? (
static_cast<double>(s.usage_count) / total_usage) * 100.0
609 std::sort(stats.begin(), stats.end(),
610 [](
const auto& a,
const auto& b) {
611 return a.usage_count > b.usage_count;
616 return absl::OkStatus();
621 std::map<int, PaletteUsageStats> palette_map;
629 for (
int i = 0; i < 8; ++i) {
633 palette_map[i] = stats;
637 for (
int map_id = 0; map_id < 128; ++map_id) {
640 int palette_idx = (map_id < 64) ? (map_id % 8) : (map_id % 8);
642 palette_map[palette_idx].used_by_maps.push_back(map_id);
645 std::vector<PaletteUsageStats> result;
646 for (
const auto& [_, stats] : palette_map) {
647 result.push_back(stats);
655 std::map<int, PaletteUsageStats> palette_map;
661 for (
int i = 0; i < 16; ++i) {
665 palette_map[i + 100] = stats;
669 for (
int room_id = 0; room_id < 320; ++room_id) {
671 int palette_idx = 100 + (room_id % 16);
673 palette_map[palette_idx].used_by_maps.push_back(room_id);
676 std::vector<PaletteUsageStats> result;
677 for (
const auto& [_, stats] : palette_map) {
678 result.push_back(stats);
690 return absl::OkStatus();
696 std::string type = parser.
GetString(
"type").value_or(
"overworld");
697 int top_n = parser.
GetInt(
"top").value_or(20);
699 std::map<int, TileUsageEntry> usage_map;
701 if (type ==
"overworld") {
703 }
else if (type ==
"dungeon") {
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());
717 usage_map[tile_id] = entry;
723 std::vector<TileUsageEntry> entries;
725 for (
const auto& [_, entry] : usage_map) {
726 total_usage += entry.usage_count;
727 entries.push_back(entry);
730 for (
auto& e : entries) {
731 e.usage_percentage = total_usage > 0
732 ? (
static_cast<double>(e.usage_count) / total_usage) * 100.0
737 std::sort(entries.begin(), entries.end(),
738 [](
const auto& a,
const auto& b) {
739 return a.usage_count > b.usage_count;
743 if (
static_cast<int>(entries.size()) > top_n) {
744 entries.resize(top_n);
749 return absl::OkStatus();
754 std::map<int, TileUsageEntry> usage;
763 for (
int map_id = 0; map_id < 160; ++map_id) {
765 for (
int i = 0; i < 1024; ++i) {
767 int tile_id = (map_id * 7 + i) % 512;
769 if (usage.count(tile_id) == 0) {
773 usage[tile_id] = entry;
778 if (usage[tile_id].locations.size() < 10) {
779 usage[tile_id].locations.push_back(map_id);
789 std::map<int, TileUsageEntry> usage;
794 for (
int room_id = 0; room_id < 320; ++room_id) {
796 for (
int i = 0; i < 256; ++i) {
797 int tile_id = (room_id * 11 + i) % 256;
799 if (usage.count(tile_id) == 0) {
803 usage[tile_id] = entry;
807 if (usage[tile_id].locations.size() < 10) {
808 usage[tile_id].locations.push_back(room_id + 1000);
820#ifdef YAZE_WITH_OPENCV
821#include <opencv2/core.hpp>
822#include <opencv2/imgproc.hpp>
823#include <opencv2/features2d.hpp>
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,
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()));
837 cv::matchTemplate(search_mat, ref_mat, result, method);
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);
844 if (method == cv::TM_SQDIFF || method == cv::TM_SQDIFF_NORMED) {
845 return {{min_loc.x, min_loc.y}, 1.0 - min_val};
848 return {{max_loc.x, max_loc.y}, max_val};
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()));
857 cv::Ptr<cv::ORB> orb = cv::ORB::create();
859 std::vector<cv::KeyPoint> kp_a, kp_b;
860 cv::Mat desc_a, desc_b;
862 orb->detectAndCompute(mat_a, cv::noArray(), kp_a, desc_a);
863 orb->detectAndCompute(mat_b, cv::noArray(), kp_b, desc_b);
865 if (desc_a.empty() || desc_b.empty()) {
870 cv::BFMatcher matcher(cv::NORM_HAMMING);
871 std::vector<cv::DMatch> matches;
872 matcher.match(desc_a, desc_b, matches);
874 if (matches.empty()) {
879 double total_dist = 0.0;
880 for (
const auto& m : matches) {
881 total_dist += m.distance;
885 double avg_dist = total_dist / matches.size();
886 return std::max(0.0, 100.0 - avg_dist);
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()));
894 const double C1 = 6.5025, C2 = 58.5225;
897 mat_a.convertTo(I1, CV_32F);
898 mat_b.convertTo(I2, CV_32F);
900 cv::Mat I1_2 = I1.mul(I1);
901 cv::Mat I2_2 = I2.mul(I2);
902 cv::Mat I1_I2 = I1.mul(I2);
905 cv::GaussianBlur(I1, mu1, cv::Size(3, 3), 1.5);
906 cv::GaussianBlur(I2, mu2, cv::Size(3, 3), 1.5);
908 cv::Mat mu1_2 = mu1.mul(mu1);
909 cv::Mat mu2_2 = mu2.mul(mu2);
910 cv::Mat mu1_mu2 = mu1.mul(mu2);
912 cv::Mat sigma1_2, sigma2_2, sigma12;
913 cv::GaussianBlur(I1_2, sigma1_2, cv::Size(3, 3), 1.5);
915 cv::GaussianBlur(I2_2, sigma2_2, cv::Size(3, 3), 1.5);
917 cv::GaussianBlur(I1_I2, sigma12, cv::Size(3, 3), 1.5);
920 cv::Mat t1 = 2 * mu1_mu2 + C1;
921 cv::Mat t2 = 2 * sigma12 + C2;
922 cv::Mat t3 = t1.mul(t2);
924 t1 = mu1_2 + mu2_2 + C1;
925 t2 = sigma1_2 + sigma2_2 + C2;
929 cv::divide(t3, t1, ssim_map);
931 cv::Scalar mssim = cv::mean(ssim_map);
932 return mssim[0] * 100.0;
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
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)
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Tile usage frequency entry.