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) {
43 std::abs(
static_cast<int>(tile_a[i]) -
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);
91 (mean_a * mean_a + mean_b * mean_b + c1) * (var_a + var_b + c2);
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));
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));
125 Rom* rom,
int sheet_index,
int x,
int y)
const {
127 return absl::InvalidArgumentError(
"ROM is null");
147 if (gfx_sheets.empty() || !gfx_sheets[0].is_active()) {
148 return absl::FailedPreconditionError(
149 "Graphics not loaded. Load ROM with graphics first.");
153 if (sheet_index >=
static_cast<int>(gfx_sheets.size())) {
154 return absl::OutOfRangeError(
155 absl::StrCat(
"Sheet ", sheet_index,
" out of range"));
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"));
164 const auto& sheet_data = sheet.vector();
170 if (src_offset + col < sheet_data.size()) {
171 tile_data[row *
kTileWidth + col] = sheet_data[src_offset + col];
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 if (matches.empty()) {
216 json <<
"{\n \"matches\": [],\n";
217 json <<
" \"total_matches\": 0\n";
222 json <<
"{\n \"matches\": [\n";
224 for (
size_t i = 0; i < matches.size(); ++i) {
225 const auto& m = matches[i];
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";
234 if (i < matches.size() - 1)
240 json <<
" \"total_matches\": " << matches.size() <<
"\n";
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";
257 json <<
"{\n \"unused_regions\": [\n";
259 for (
size_t i = 0; i < regions.size(); ++i) {
260 const auto& r = regions[i];
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";
269 if (i < regions.size() - 1)
275 for (
const auto& r : regions) {
276 total_tiles += r.tile_count;
280 json <<
" \"total_regions\": " << regions.size() <<
",\n";
281 json <<
" \"total_free_tiles\": " << total_tiles <<
"\n";
288 const std::vector<PaletteUsageStats>& stats)
const {
289 std::ostringstream
json;
290 json <<
"{\n \"palette_usage\": [\n";
292 for (
size_t i = 0; i < stats.size(); ++i) {
293 const auto& s = stats[i];
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)
307 if (i < stats.size() - 1)
319 const std::vector<TileUsageEntry>& entries)
const {
320 std::ostringstream
json;
321 json <<
"{\n \"tile_histogram\": [\n";
323 for (
size_t i = 0; i < entries.size(); ++i) {
324 const auto& e = entries[i];
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)
336 if (e.locations.size() > 10)
340 if (i < entries.size() - 1)
346 json <<
" \"total_entries\": " << entries.size() <<
"\n";
365 auto tile_id_or = parser.
GetInt(
"tile_id");
366 if (!tile_id_or.ok()) {
367 return absl::InvalidArgumentError(
"tile_id is required");
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");
375 threshold = std::clamp(threshold, 0, 100);
378 auto ref_tile_or =
ExtractTile(rom, sheet, tile_id);
379 if (!ref_tile_or.ok()) {
380 return ref_tile_or.status();
382 const auto& ref_tile = ref_tile_or.value();
385 std::vector<TileSimilarityMatch> matches;
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;
391 for (
int s = start_sheet; s < end_sheet; ++s) {
394 for (
int t = 0; t < tile_count; ++t) {
396 if (s == sheet && t == tile_id) {
401 if (!cmp_tile_or.ok()) {
404 const auto& cmp_tile = cmp_tile_or.value();
408 if (method ==
"pixel") {
414 if (score >= threshold) {
421 matches.push_back(match);
427 std::sort(matches.begin(), matches.end(), [](
const auto& a,
const auto& b) {
428 return a.similarity_score > b.similarity_score;
432 if (matches.size() > 50) {
439 return absl::OkStatus();
448 return absl::OkStatus();
454 int tile_size = parser.
GetInt(
"tile_size").value_or(8);
455 if (tile_size != 8 && tile_size != 16) {
459 std::vector<UnusedRegion> all_regions;
461 auto sheet_arg = parser.
GetString(
"sheet");
462 if (sheet_arg.has_value()) {
463 int sheet = parser.
GetInt(
"sheet").value_or(0);
465 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
470 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
479 all_regions.begin(), all_regions.end(),
480 [](
const auto& a,
const auto& b) { return a.tile_count > b.tile_count; });
485 return absl::OkStatus();
489 Rom* rom,
int sheet_index,
int tile_size)
const {
490 std::vector<UnusedRegion> regions;
494 int pixels_per_tile = tile_size * tile_size;
496 for (
int ty = 0; ty < tiles_y; ++ty) {
497 for (
int tx = 0; tx < tiles_x; ++tx) {
499 std::vector<uint8_t> tile_data;
500 tile_data.reserve(pixels_per_tile);
502 for (
int py = 0; py < tile_size; ++py) {
503 for (
int px = 0; px < tile_size; ++px) {
505 tx * tile_size, ty * tile_size);
506 if (pixel_or.ok() && !pixel_or.value().empty()) {
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]);
519 region.
x = tx * tile_size;
520 region.
y = ty * tile_size;
521 region.
width = tile_size;
522 region.
height = tile_size;
524 regions.push_back(region);
533 const std::vector<UnusedRegion>& regions)
const {
534 if (regions.empty()) {
539 std::map<int, std::vector<UnusedRegion>> by_sheet;
540 for (
const auto& r : regions) {
541 by_sheet[r.sheet_index].push_back(r);
544 std::vector<UnusedRegion> merged;
546 for (
auto& [sheet, sheet_regions] : by_sheet) {
548 std::sort(sheet_regions.begin(), sheet_regions.end(),
549 [](
const auto& a,
const auto& b) {
555 for (
size_t i = 0; i < sheet_regions.size();) {
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;
567 merged.push_back(current);
581 return absl::OkStatus();
587 std::string type = parser.
GetString(
"type").value_or(
"all");
589 std::vector<PaletteUsageStats> stats;
591 if (type ==
"overworld" || type ==
"all") {
593 stats.insert(stats.end(), ow_stats.begin(), ow_stats.end());
596 if (type ==
"dungeon" || type ==
"all") {
599 for (
const auto& ds : dg_stats) {
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());
618 for (
const auto& s : stats) {
619 total_usage += s.usage_count;
621 for (
auto& s : stats) {
624 ? (
static_cast<double>(s.usage_count) / total_usage) * 100.0
629 std::sort(stats.begin(), stats.end(), [](
const auto& a,
const auto& b) {
630 return a.usage_count > b.usage_count;
635 return absl::OkStatus();
640 std::map<int, PaletteUsageStats> palette_map;
648 for (
int i = 0; i < 8; ++i) {
652 palette_map[i] = stats;
656 for (
int map_id = 0; map_id < 128; ++map_id) {
659 int palette_idx = (map_id < 64) ? (map_id % 8) : (map_id % 8);
661 palette_map[palette_idx].used_by_maps.push_back(map_id);
664 std::vector<PaletteUsageStats> result;
665 for (
const auto& [_, stats] : palette_map) {
666 result.push_back(stats);
674 std::map<int, PaletteUsageStats> palette_map;
680 for (
int i = 0; i < 16; ++i) {
684 palette_map[i + 100] = stats;
688 for (
int room_id = 0; room_id < 320; ++room_id) {
690 int palette_idx = 100 + (room_id % 16);
692 palette_map[palette_idx].used_by_maps.push_back(room_id);
695 std::vector<PaletteUsageStats> result;
696 for (
const auto& [_, stats] : palette_map) {
697 result.push_back(stats);
709 return absl::OkStatus();
715 std::string type = parser.
GetString(
"type").value_or(
"overworld");
716 int top_n = parser.
GetInt(
"top").value_or(20);
718 std::map<int, TileUsageEntry> usage_map;
720 if (type ==
"overworld") {
722 }
else if (type ==
"dungeon") {
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());
736 usage_map[tile_id] = entry;
742 std::vector<TileUsageEntry> entries;
744 for (
const auto& [_, entry] : usage_map) {
745 total_usage += entry.usage_count;
746 entries.push_back(entry);
749 for (
auto& e : entries) {
752 ? (
static_cast<double>(e.usage_count) / total_usage) * 100.0
757 std::sort(entries.begin(), entries.end(), [](
const auto& a,
const auto& b) {
758 return a.usage_count > b.usage_count;
762 if (
static_cast<int>(entries.size()) > top_n) {
763 entries.resize(top_n);
768 return absl::OkStatus();
773 std::map<int, TileUsageEntry> usage;
782 for (
int map_id = 0; map_id < 160; ++map_id) {
784 for (
int i = 0; i < 1024; ++i) {
786 int tile_id = (map_id * 7 + i) % 512;
788 if (usage.count(tile_id) == 0) {
792 usage[tile_id] = entry;
797 if (usage[tile_id].locations.size() < 10) {
798 usage[tile_id].locations.push_back(map_id);
808 std::map<int, TileUsageEntry> usage;
813 for (
int room_id = 0; room_id < 320; ++room_id) {
815 for (
int i = 0; i < 256; ++i) {
816 int tile_id = (room_id * 11 + i) % 256;
818 if (usage.count(tile_id) == 0) {
822 usage[tile_id] = entry;
826 if (usage[tile_id].locations.size() < 10) {
827 usage[tile_id].locations.push_back(room_id +
840#ifdef YAZE_WITH_OPENCV
841#include <opencv2/core.hpp>
842#include <opencv2/features2d.hpp>
843#include <opencv2/imgproc.hpp>
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,
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()));
857 cv::matchTemplate(search_mat, ref_mat, result, method);
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);
864 if (method == cv::TM_SQDIFF || method == cv::TM_SQDIFF_NORMED) {
865 return {{min_loc.x, min_loc.y}, 1.0 - min_val};
868 return {{max_loc.x, max_loc.y}, max_val};
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()));
877 cv::Ptr<cv::ORB> orb = cv::ORB::create();
879 std::vector<cv::KeyPoint> kp_a, kp_b;
880 cv::Mat desc_a, desc_b;
882 orb->detectAndCompute(mat_a, cv::noArray(), kp_a, desc_a);
883 orb->detectAndCompute(mat_b, cv::noArray(), kp_b, desc_b);
885 if (desc_a.empty() || desc_b.empty()) {
890 cv::BFMatcher matcher(cv::NORM_HAMMING);
891 std::vector<cv::DMatch> matches;
892 matcher.match(desc_a, desc_b, matches);
894 if (matches.empty()) {
899 double total_dist = 0.0;
900 for (
const auto& m : matches) {
901 total_dist += m.distance;
905 double avg_dist = total_dist / matches.size();
906 return std::max(0.0, 100.0 - avg_dist);
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()));
914 const double C1 = 6.5025, C2 = 58.5225;
917 mat_a.convertTo(I1, CV_32F);
918 mat_b.convertTo(I2, CV_32F);
920 cv::Mat I1_2 = I1.mul(I1);
921 cv::Mat I2_2 = I2.mul(I2);
922 cv::Mat I1_I2 = I1.mul(I2);
925 cv::GaussianBlur(I1, mu1, cv::Size(3, 3), 1.5);
926 cv::GaussianBlur(I2, mu2, cv::Size(3, 3), 1.5);
928 cv::Mat mu1_2 = mu1.mul(mu1);
929 cv::Mat mu2_2 = mu2.mul(mu2);
930 cv::Mat mu1_mu2 = mu1.mul(mu2);
932 cv::Mat sigma1_2, sigma2_2, sigma12;
933 cv::GaussianBlur(I1_2, sigma1_2, cv::Size(3, 3), 1.5);
935 cv::GaussianBlur(I2_2, sigma2_2, cv::Size(3, 3), 1.5);
937 cv::GaussianBlur(I1_I2, sigma12, cv::Size(3, 3), 1.5);
940 cv::Mat t1 = 2 * mu1_mu2 + C1;
941 cv::Mat t2 = 2 * sigma12 + C2;
942 cv::Mat t3 = t1.mul(t2);
944 t1 = mu1_2 + mu2_2 + C1;
945 t2 = sigma1_2 + sigma2_2 + C2;
949 cv::divide(t3, t1, ssim_map);
951 cv::Scalar mssim = cv::mean(ssim_map);
952 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.