16#include "absl/status/status.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
22#include "nlohmann/json.hpp"
36 const std::vector<uint8_t>& tile_a,
37 const std::vector<uint8_t>& tile_b)
const {
38 if (tile_a.size() != tile_b.size() || tile_a.empty()) {
43 double total_diff = 0.0;
44 for (
size_t i = 0; i < tile_a.size(); ++i) {
46 std::abs(
static_cast<int>(tile_a[i]) -
static_cast<int>(tile_b[i]));
50 double max_possible_diff = tile_a.size() * 255.0;
51 double normalized_diff = total_diff / max_possible_diff;
54 return (1.0 - normalized_diff) * 100.0;
58 const std::vector<uint8_t>& tile_a,
59 const std::vector<uint8_t>& tile_b)
const {
60 if (tile_a.size() != tile_b.size() || tile_a.empty()) {
64 const size_t n = tile_a.size();
67 double mean_a = 0.0, mean_b = 0.0;
68 for (
size_t i = 0; i < n; ++i) {
76 double var_a = 0.0, var_b = 0.0, covar = 0.0;
77 for (
size_t i = 0; i < n; ++i) {
78 double diff_a = tile_a[i] - mean_a;
79 double diff_b = tile_b[i] - mean_b;
80 var_a += diff_a * diff_a;
81 var_b += diff_b * diff_b;
82 covar += diff_a * diff_b;
89 const double c1 = 6.5025;
90 const double c2 = 58.5225;
92 double numerator = (2.0 * mean_a * mean_b + c1) * (2.0 * covar + c2);
94 (mean_a * mean_a + mean_b * mean_b + c1) * (var_a + var_b + c2);
96 double ssim = numerator / denominator;
99 return std::max(0.0, std::min(100.0, ssim * 100.0));
103 Rom* rom,
int sheet_index,
int tile_index)
const {
105 return absl::InvalidArgumentError(
"ROM is null");
108 if (sheet_index < 0 || sheet_index >=
kMaxSheets) {
109 return absl::InvalidArgumentError(
110 absl::StrCat(
"Invalid sheet index: ", sheet_index));
114 int tiles_per_sheet =
116 if (tile_index < 0 || tile_index >= tiles_per_sheet) {
117 return absl::InvalidArgumentError(
118 absl::StrCat(
"Invalid tile index: ", tile_index));
128 Rom* rom,
int sheet_index,
int x,
int y)
const {
130 return absl::InvalidArgumentError(
"ROM is null");
150 if (gfx_sheets.empty() || !gfx_sheets[0].is_active()) {
151 return absl::FailedPreconditionError(
152 "Graphics not loaded. Load ROM with graphics first.");
156 if (sheet_index >=
static_cast<int>(gfx_sheets.size())) {
157 return absl::OutOfRangeError(
158 absl::StrCat(
"Sheet ", sheet_index,
" out of range"));
161 const auto& sheet = gfx_sheets[sheet_index];
162 if (!sheet.is_active()) {
163 return absl::FailedPreconditionError(
164 absl::StrCat(
"Sheet ", sheet_index,
" is not loaded"));
167 const auto& sheet_data = sheet.vector();
173 if (src_offset + col < sheet_data.size()) {
174 tile_data[row *
kTileWidth + col] = sheet_data[src_offset + col];
185 return !data.empty() && std::all_of(data.begin(), data.end(),
186 [](uint8_t b) { return b == 0x00; });
195 const std::vector<TileSimilarityMatch>& matches)
const {
196 std::ostringstream
json;
197 if (matches.empty()) {
198 json <<
"{\n \"matches\": [],\n";
199 json <<
" \"total_matches\": 0\n";
204 json <<
"{\n \"matches\": [\n";
206 for (
size_t i = 0; i < matches.size(); ++i) {
207 const auto& m = matches[i];
209 json <<
" \"tile_id\": " << m.tile_id <<
",\n";
210 json <<
" \"similarity_score\": "
211 << absl::StrFormat(
"%.2f", m.similarity_score) <<
",\n";
212 json <<
" \"sheet_index\": " << m.sheet_index <<
",\n";
213 json <<
" \"x_position\": " << m.x_position <<
",\n";
214 json <<
" \"y_position\": " << m.y_position <<
"\n";
216 if (i < matches.size() - 1)
222 json <<
" \"total_matches\": " << matches.size() <<
"\n";
229 const std::vector<UnusedRegion>& regions)
const {
230 std::ostringstream
json;
231 if (regions.empty()) {
232 json <<
"{\n \"unused_regions\": [],\n";
233 json <<
" \"total_regions\": 0,\n";
234 json <<
" \"total_free_tiles\": 0\n";
239 json <<
"{\n \"unused_regions\": [\n";
241 for (
size_t i = 0; i < regions.size(); ++i) {
242 const auto& r = regions[i];
244 json <<
" \"sheet_index\": " << r.sheet_index <<
",\n";
245 json <<
" \"x\": " << r.x <<
",\n";
246 json <<
" \"y\": " << r.y <<
",\n";
247 json <<
" \"width\": " << r.width <<
",\n";
248 json <<
" \"height\": " << r.height <<
",\n";
249 json <<
" \"tile_count\": " << r.tile_count <<
"\n";
251 if (i < regions.size() - 1)
257 for (
const auto& r : regions) {
258 total_tiles += r.tile_count;
262 json <<
" \"total_regions\": " << regions.size() <<
",\n";
263 json <<
" \"total_free_tiles\": " << total_tiles <<
"\n";
270 const std::vector<PaletteUsageStats>& stats)
const {
271 std::ostringstream
json;
272 json <<
"{\n \"palette_usage\": [\n";
274 for (
size_t i = 0; i < stats.size(); ++i) {
275 const auto& s = stats[i];
277 json <<
" \"palette_index\": " << s.palette_index <<
",\n";
278 json <<
" \"usage_count\": " << s.usage_count <<
",\n";
279 json <<
" \"usage_percentage\": "
280 << absl::StrFormat(
"%.2f", s.usage_percentage) <<
",\n";
281 json <<
" \"used_by_maps\": [";
282 for (
size_t j = 0; j < s.used_by_maps.size(); ++j) {
283 json << s.used_by_maps[j];
284 if (j < s.used_by_maps.size() - 1)
289 if (i < stats.size() - 1)
301 const std::vector<TileUsageEntry>& entries)
const {
302 std::ostringstream
json;
303 json <<
"{\n \"tile_histogram\": [\n";
305 for (
size_t i = 0; i < entries.size(); ++i) {
306 const auto& e = entries[i];
308 json <<
" \"tile_id\": " << e.tile_id <<
",\n";
309 json <<
" \"usage_count\": " << e.usage_count <<
",\n";
310 json <<
" \"usage_percentage\": "
311 << absl::StrFormat(
"%.2f", e.usage_percentage) <<
",\n";
312 json <<
" \"locations\": [";
313 for (
size_t j = 0; j < std::min(e.locations.size(),
size_t(10)); ++j) {
314 json << e.locations[j];
315 if (j < std::min(e.locations.size(),
size_t(10)) - 1)
318 if (e.locations.size() > 10)
322 if (i < entries.size() - 1)
328 json <<
" \"total_entries\": " << entries.size() <<
"\n";
347 auto tile_id_or = parser.
GetInt(
"tile_id");
348 if (!tile_id_or.ok()) {
349 return absl::InvalidArgumentError(
"tile_id is required");
351 int tile_id = tile_id_or.value();
352 int sheet = parser.
GetInt(
"sheet").value_or(0);
353 int threshold = parser.
GetInt(
"threshold").value_or(80);
354 std::string method = parser.
GetString(
"method").value_or(
"structural");
357 threshold = std::clamp(threshold, 0, 100);
360 auto ref_tile_or =
ExtractTile(rom, sheet, tile_id);
361 if (!ref_tile_or.ok()) {
362 return ref_tile_or.status();
364 const auto& ref_tile = ref_tile_or.value();
367 std::vector<TileSimilarityMatch> matches;
369 bool has_sheet = parser.
GetString(
"sheet").has_value();
370 int start_sheet = has_sheet ? sheet : 0;
371 int end_sheet = has_sheet ? sheet + 1 :
kMaxSheets;
373 for (
int s = start_sheet; s < end_sheet; ++s) {
376 for (
int t = 0; t < tile_count; ++t) {
378 if (s == sheet && t == tile_id) {
383 if (!cmp_tile_or.ok()) {
386 const auto& cmp_tile = cmp_tile_or.value();
390 if (method ==
"pixel") {
396 if (score >= threshold) {
403 matches.push_back(match);
409 std::sort(matches.begin(), matches.end(), [](
const auto& a,
const auto& b) {
410 return a.similarity_score > b.similarity_score;
414 if (matches.size() > 50) {
421 return absl::OkStatus();
430 return absl::OkStatus();
436 int tile_size = parser.
GetInt(
"tile_size").value_or(8);
437 if (tile_size != 8 && tile_size != 16) {
441 std::vector<UnusedRegion> all_regions;
443 auto sheet_arg = parser.
GetString(
"sheet");
444 if (sheet_arg.has_value()) {
445 int sheet = parser.
GetInt(
"sheet").value_or(0);
447 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
452 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
461 all_regions.begin(), all_regions.end(),
462 [](
const auto& a,
const auto& b) { return a.tile_count > b.tile_count; });
466 const auto document =
468 for (
const auto& [key, value] : document.items()) {
473 for (
const auto& region : all_regions) {
474 total_tiles += region.tile_count;
476 absl::StrFormat(
"sheet 0x%02X", region.sheet_index),
477 absl::StrFormat(
"x=%d y=%d %dx%d (%d tiles)", region.x, region.y,
478 region.width, region.height, region.tile_count));
480 formatter.
AddField(
"total_regions",
static_cast<int>(all_regions.size()));
481 formatter.
AddField(
"total_free_tiles", total_tiles);
484 return absl::OkStatus();
488 Rom* rom,
int sheet_index,
int tile_size)
const {
489 std::vector<UnusedRegion> regions;
490 if (rom ==
nullptr || !rom->
is_loaded() || sheet_index < 0 ||
496 const auto sheet =
static_cast<uint16_t
>(sheet_index);
501 std::vector<uint16_t> reserved_blocks;
504 const auto& manifest_reserved =
506 auto contains = [sheet](
const std::vector<uint16_t>& ids) {
507 return std::find(ids.begin(), ids.end(), sheet) != ids.end();
509 if (contains(settings.reserved_sheets) || contains(manifest_reserved)) {
512 if (
auto it = settings.reserved_blocks.find(sheet);
513 it != settings.reserved_blocks.end()) {
514 reserved_blocks = it->second;
524 constexpr size_t kTileBytes = 24;
525 auto tile_empty = [&data](
int tile) {
526 const auto begin = data->begin() + tile * kTileBytes;
527 return std::all_of(begin, begin + kTileBytes,
528 [](uint8_t b) {
return b == 0; });
530 auto block_reserved = [&reserved_blocks](
int block) {
531 return std::find(reserved_blocks.begin(), reserved_blocks.end(), block) !=
532 reserved_blocks.end();
535 if (tile_size == 16) {
537 if (block_reserved(block) ||
542 {sheet_index, (block % 8) * 16, (block / 8) * 16, 16, 16, 4});
548 const int block = (tile / 32) * 8 + (tile % 16) / 2;
549 if (block_reserved(block) || !tile_empty(tile)) {
552 regions.push_back({sheet_index, (tile % 16) * 8, (tile / 16) * 8, 8, 8, 1});
558 const std::vector<UnusedRegion>& regions)
const {
559 if (regions.empty()) {
564 std::map<int, std::vector<UnusedRegion>> by_sheet;
565 for (
const auto& r : regions) {
566 by_sheet[r.sheet_index].push_back(r);
569 std::vector<UnusedRegion> merged;
571 for (
auto& [sheet, sheet_regions] : by_sheet) {
573 std::sort(sheet_regions.begin(), sheet_regions.end(),
574 [](
const auto& a,
const auto& b) {
580 for (
size_t i = 0; i < sheet_regions.size();) {
585 while (j < sheet_regions.size() && sheet_regions[j].y == current.
y &&
586 sheet_regions[j].x == current.
x + current.
width) {
587 current.
width += sheet_regions[j].width;
588 current.
tile_count += sheet_regions[j].tile_count;
592 merged.push_back(current);
606 return absl::OkStatus();
612 std::string type = parser.
GetString(
"type").value_or(
"all");
614 std::vector<PaletteUsageStats> stats;
616 if (type ==
"overworld" || type ==
"all") {
618 stats.insert(stats.end(), ow_stats.begin(), ow_stats.end());
621 if (type ==
"dungeon" || type ==
"all") {
624 for (
const auto& ds : dg_stats) {
626 for (
auto& s : stats) {
627 if (s.palette_index == ds.palette_index) {
628 s.usage_count += ds.usage_count;
629 s.used_by_maps.insert(s.used_by_maps.end(), ds.used_by_maps.begin(),
630 ds.used_by_maps.end());
643 for (
const auto& s : stats) {
644 total_usage += s.usage_count;
646 for (
auto& s : stats) {
649 ? (
static_cast<double>(s.usage_count) / total_usage) * 100.0
654 std::sort(stats.begin(), stats.end(), [](
const auto& a,
const auto& b) {
655 return a.usage_count > b.usage_count;
660 return absl::OkStatus();
665 std::map<int, PaletteUsageStats> palette_map;
673 for (
int i = 0; i < 8; ++i) {
677 palette_map[i] = stats;
681 for (
int map_id = 0; map_id < 128; ++map_id) {
684 int palette_idx = (map_id < 64) ? (map_id % 8) : (map_id % 8);
686 palette_map[palette_idx].used_by_maps.push_back(map_id);
689 std::vector<PaletteUsageStats> result;
690 for (
const auto& [_, stats] : palette_map) {
691 result.push_back(stats);
699 std::map<int, PaletteUsageStats> palette_map;
705 for (
int i = 0; i < 16; ++i) {
709 palette_map[i + 100] = stats;
713 for (
int room_id = 0; room_id < 320; ++room_id) {
715 int palette_idx = 100 + (room_id % 16);
717 palette_map[palette_idx].used_by_maps.push_back(room_id);
720 std::vector<PaletteUsageStats> result;
721 for (
const auto& [_, stats] : palette_map) {
722 result.push_back(stats);
734 return absl::OkStatus();
740 std::string type = parser.
GetString(
"type").value_or(
"overworld");
741 int top_n = parser.
GetInt(
"top").value_or(20);
743 std::map<int, TileUsageEntry> usage_map;
745 if (type ==
"overworld") {
747 }
else if (type ==
"dungeon") {
754 for (
const auto& [tile_id, entry] : dg) {
755 if (usage_map.count(tile_id)) {
756 usage_map[tile_id].usage_count += entry.usage_count;
757 usage_map[tile_id].locations.insert(usage_map[tile_id].locations.end(),
758 entry.locations.begin(),
759 entry.locations.end());
761 usage_map[tile_id] = entry;
767 std::vector<TileUsageEntry> entries;
769 for (
const auto& [_, entry] : usage_map) {
770 total_usage += entry.usage_count;
771 entries.push_back(entry);
774 for (
auto& e : entries) {
777 ? (
static_cast<double>(e.usage_count) / total_usage) * 100.0
782 std::sort(entries.begin(), entries.end(), [](
const auto& a,
const auto& b) {
783 return a.usage_count > b.usage_count;
787 if (
static_cast<int>(entries.size()) > top_n) {
788 entries.resize(top_n);
793 return absl::OkStatus();
798 std::map<int, TileUsageEntry> usage;
807 for (
int map_id = 0; map_id < 160; ++map_id) {
809 for (
int i = 0; i < 1024; ++i) {
811 int tile_id = (map_id * 7 + i) % 512;
813 if (usage.count(tile_id) == 0) {
817 usage[tile_id] = entry;
822 if (usage[tile_id].locations.size() < 10) {
823 usage[tile_id].locations.push_back(map_id);
833 std::map<int, TileUsageEntry> usage;
838 for (
int room_id = 0; room_id < 320; ++room_id) {
840 for (
int i = 0; i < 256; ++i) {
841 int tile_id = (room_id * 11 + i) % 256;
843 if (usage.count(tile_id) == 0) {
847 usage[tile_id] = entry;
851 if (usage[tile_id].locations.size() < 10) {
852 usage[tile_id].locations.push_back(room_id +
865#ifdef YAZE_WITH_OPENCV
866#include <opencv2/core.hpp>
867#include <opencv2/features2d.hpp>
868#include <opencv2/imgproc.hpp>
872std::pair<std::pair<int, int>,
double> TemplateMatch(
873 const std::vector<uint8_t>& reference,
874 const std::vector<uint8_t>& search_region,
int width,
int height,
877 cv::Mat ref_mat(8, 8, CV_8UC1,
const_cast<uint8_t*
>(reference.data()));
878 cv::Mat search_mat(height, width, CV_8UC1,
879 const_cast<uint8_t*
>(search_region.data()));
882 cv::matchTemplate(search_mat, ref_mat, result, method);
884 double min_val, max_val;
885 cv::Point min_loc, max_loc;
886 cv::minMaxLoc(result, &min_val, &max_val, &min_loc, &max_loc);
889 if (method == cv::TM_SQDIFF || method == cv::TM_SQDIFF_NORMED) {
890 return {{min_loc.x, min_loc.y}, 1.0 - min_val};
893 return {{max_loc.x, max_loc.y}, max_val};
896double FeatureMatch(
const std::vector<uint8_t>& tile_a,
897 const std::vector<uint8_t>& tile_b) {
898 cv::Mat mat_a(8, 8, CV_8UC1,
const_cast<uint8_t*
>(tile_a.data()));
899 cv::Mat mat_b(8, 8, CV_8UC1,
const_cast<uint8_t*
>(tile_b.data()));
902 cv::Ptr<cv::ORB> orb = cv::ORB::create();
904 std::vector<cv::KeyPoint> kp_a, kp_b;
905 cv::Mat desc_a, desc_b;
907 orb->detectAndCompute(mat_a, cv::noArray(), kp_a, desc_a);
908 orb->detectAndCompute(mat_b, cv::noArray(), kp_b, desc_b);
910 if (desc_a.empty() || desc_b.empty()) {
915 cv::BFMatcher matcher(cv::NORM_HAMMING);
916 std::vector<cv::DMatch> matches;
917 matcher.match(desc_a, desc_b, matches);
919 if (matches.empty()) {
924 double total_dist = 0.0;
925 for (
const auto& m : matches) {
926 total_dist += m.distance;
930 double avg_dist = total_dist / matches.size();
931 return std::max(0.0, 100.0 - avg_dist);
934double ComputeSSIM(
const std::vector<uint8_t>& tile_a,
935 const std::vector<uint8_t>& tile_b) {
936 cv::Mat mat_a(8, 8, CV_8UC1,
const_cast<uint8_t*
>(tile_a.data()));
937 cv::Mat mat_b(8, 8, CV_8UC1,
const_cast<uint8_t*
>(tile_b.data()));
939 const double C1 = 6.5025, C2 = 58.5225;
942 mat_a.convertTo(I1, CV_32F);
943 mat_b.convertTo(I2, CV_32F);
945 cv::Mat I1_2 = I1.mul(I1);
946 cv::Mat I2_2 = I2.mul(I2);
947 cv::Mat I1_I2 = I1.mul(I2);
950 cv::GaussianBlur(I1, mu1, cv::Size(3, 3), 1.5);
951 cv::GaussianBlur(I2, mu2, cv::Size(3, 3), 1.5);
953 cv::Mat mu1_2 = mu1.mul(mu1);
954 cv::Mat mu2_2 = mu2.mul(mu2);
955 cv::Mat mu1_mu2 = mu1.mul(mu2);
957 cv::Mat sigma1_2, sigma2_2, sigma12;
958 cv::GaussianBlur(I1_2, sigma1_2, cv::Size(3, 3), 1.5);
960 cv::GaussianBlur(I2_2, sigma2_2, cv::Size(3, 3), 1.5);
962 cv::GaussianBlur(I1_I2, sigma12, cv::Size(3, 3), 1.5);
965 cv::Mat t1 = 2 * mu1_mu2 + C1;
966 cv::Mat t2 = 2 * sigma12 + C2;
967 cv::Mat t3 = t1.mul(t2);
969 t1 = mu1_2 + mu2_2 + C1;
970 t2 = sigma1_2 + sigma2_2 + C2;
974 cv::divide(t3, t1, ssim_map);
976 cv::Scalar mssim = cv::mean(ssim_map);
977 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)
project::YazeProject * project_
const GraphicsSheetLayout & graphics_sheet_layout() const
Graphics-sheet free space and reserved sheets. Empty when the optional graphics_sheet_regions section...
GraphicsSheets & gfx_sheets()
Locates graphics sheets in the ROM and writes edited sheets back.
constexpr size_t kGfxSheet3bppBytes
absl::StatusOr< std::vector< uint8_t > > ReadGfxSheetData(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
Returns the sheet's decoded bytes (SNES planar data).
bool IsGfxSheetBlockEmpty(const std::vector< uint8_t > &data, int block, int bpp)
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
constexpr int kGfxSheetBlockCount
16x16 blocks per sheet: 8 columns x 2 rows over the 128x32 sheet.
Tile usage frequency entry.
std::vector< uint16_t > reserved_sheets
core::HackManifest hack_manifest
GraphicsSheetSettings graphics_sheets