Поиск изображения в изображении

261
12 апреля 2018, 10:57

Пишу автокликер для одной мобильной игры (скрин ниже). Задача находить на карте объекты. Точнее, программа делает скриншот экрана игры запущенной в эмуляторе Bluestacks, получаю bitmap, и нужно искать объекты на карте по фрагментам на скриншоте. Подскажите алгоритм поиска фрагмента на изображении?

Answer 1

Думаю, тебе хватит функционала реализованного в OpenCV

OpenCV (англ. Open Source Computer Vision Library, библиотека компьютерного зрения с открытым исходным кодом) — библиотека алгоритмов компьютерного зрения, обработки изображений и численных алгоритмов общего назначения с открытым кодом.

(с) Википедия.

Вот неплохие уроки по ней, а вот то, что тебе в теории может пригодиться.

Answer 2

Использование OpenCV конечно должно быть более успешным. Я поэкспериментировал с AForge

получилось тоже неплохо

Вот класс работы с библиотекой, фактически само использование занимает 2 строки, каюсь, взял эту часть из интернета, остальное написал сам

public class AforgeService
{
    //найденные совпадения
    private TemplateMatch[] _matchings;
    /// <summary>
    /// Количество найденных совпадений
    /// </summary>
    public int CountMatchings
    {
        get => _matchings != null ?  _matchings.Length : 0;
    }

    //ctor
    public AforgeService()
    {
    }
    /// <summary>
    /// Содержит ли исходное изображение представленый образец
    /// </summary>
    /// <param name="pathOriginalImage">путь к файлу исходного изображения</param>
    /// <param name="pathSampleImage">путь к файлу образца</param>
    /// <returns>true если содержит</returns>
    public async Task<bool> IsContains(string pathOriginalImage, string pathSampleImage)
    {
        if (String.IsNullOrEmpty(pathOriginalImage)) throw new ArgumentNullException(nameof(pathOriginalImage));
        if (String.IsNullOrEmpty(pathSampleImage)) throw new ArgumentNullException(nameof(pathSampleImage));
        var sample = new Bitmap(pathSampleImage);
        var orig = new Bitmap(pathOriginalImage);
        //пользуемся библиотекой
        ExhaustiveTemplateMatching tm = new ExhaustiveTemplateMatching(0.921f);
        _matchings = await Task.Run(() => tm.ProcessImage(orig, sample));
        return _matchings.Any();
    }

    /// <summary>
    /// Получение коллекции найденных мест где находится образец
    /// </summary>
    /// <returns>коллекция найденных мест</returns>
    public List<FoundPlace> GetPlaces()
    {
        List<FoundPlace> result = new List<FoundPlace>();
        if (CountMatchings == 0) return result;
        int id = 0;
        foreach (var match in _matchings)
        {
            FoundPlace place = new FoundPlace
            {
                Id = ++id,
                Similarity = match.Similarity,
                Top = match.Rectangle.Top,
                Left = match.Rectangle.Left,
                Height = match.Rectangle.Height,
                Width = match.Rectangle.Width
            };
            result.Add(place);
        }
        return result;
    }
}

Этот класс для сохранения найденного места

public class FoundPlace
{
    public int Id { get; set; }
    public double Left { get; set; }
    public double Top { get; set; }
    public double Width { get; set; }
    public double Height { get; set; }
    public double Similarity { get; set; }
}

Это класс вьюмодели

public class MainViewModel : INotifyPropertyChanged, IDisposable
{
    public event PropertyChangedEventHandler PropertyChanged;
    private readonly IMainWindow _mainWindow;
    private string _pathOriginalImage;
    //ctor
    public MainViewModel(IMainWindow mainWindow)
    {
        _mainWindow = mainWindow;
    }
    /// <summary>
    /// Флаг запуска поиска, для выкл./вкл. кнопок
    /// </summary>
    private bool _IsSearching;
    public bool IsSearching
    {
        get => _IsSearching;
        set
        {
            _IsSearching = value;
            SelectSampleCommand.RaiseCanExecuteChanged();
            SearchCommand.RaiseCanExecuteChanged();
        }
    }
    /// <summary>
    /// Исходное изображение (поле игры)
    /// </summary>
    public string OriginalImage
    {
        get => @"~\..\Assets\Original.jpg";
    }
    /// <summary>
    /// Образец для поиска
    /// </summary>
    private string _SampleImage;
    public string SampleImage
    {
        get => _SampleImage;
        set
        {
            _SampleImage = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SampleImage)));
            SearchCommand.RaiseCanExecuteChanged();
        }
    }
    /// <summary>
    /// Текстовое сообщение о процессе
    /// </summary>
    private string _Message;
    public string Message
    {
        get => _Message;
        set
        {
            _Message = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Message)));
        }
    }
    /// <summary>
    /// Список найденных мест для ListBox
    /// </summary>
    private List<FoundPlace> _Places;
    public List<FoundPlace> Places
    {
        get { return _Places; }
        set
        {
            _Places = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Places)));
        }
    }
    private FoundPlace _SelectedPlace;
    public FoundPlace SelectedPlace
    {
        get => _SelectedPlace;
        set
        {
            _SelectedPlace = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedPlace)));
            _mainWindow.DrawPlace(_SelectedPlace);
        }
    }

    /// <summary>
    /// Кнопка Выбрать
    /// </summary>
    private RelayCommand _SelectSampleCommand;
    public RelayCommand SelectSampleCommand
    {
        get => _SelectSampleCommand = _SelectSampleCommand ?? new RelayCommand(OnSelectSample, CanSelectSample);
    }
    private bool CanSelectSample()
    {
        if (IsSearching)
        {
            return false;
        }
        return true;
    }
    private void OnSelectSample()
    {
        string file = _mainWindow.SelectSample();
        if (String.IsNullOrEmpty(file)) return;
        SampleImage = file;
    }
    /// <summary>
    /// Кнопка Искать
    /// </summary>
    private RelayCommand _SearchCommand;
    public RelayCommand SearchCommand
    {
        get => _SearchCommand = _SearchCommand ?? new RelayCommand(OnSearch, CanSearch);
    }
    private bool CanSearch()
    {
        if (String.IsNullOrEmpty(SampleImage) || IsSearching)
        {
            return false;
        }
        return true;
    }
    private async void OnSearch()
    {
        Message = "Ждите...";
        IsSearching = true;
        AforgeService service = new AforgeService();
        try
        {
            using (OverrideCursor cursor = OverrideCursor.GetWaitOverrideCursor())
            {
                string pathOrigin = GetOriginalImage();
                bool isContains = await service.IsContains(pathOrigin, SampleImage);
                if (isContains)
                {
                    Places = service.GetPlaces();
                }
                else
                {
                    Places = new List<FoundPlace>();
                }
            }
        }
        catch (Exception ex)
        {
            var message = $"Возникла ошибка: {ex.Message}";
            _mainWindow.ShowMessage(message, "Ошибка");
        }
        finally
        {
            Message = $"Найдено мест: {service.CountMatchings}";
            IsSearching = false;
        }
    }
    /// <summary>
    /// Получение пути к оригинальному изображению (полю игры)
    /// </summary>
    /// <returns></returns>
    private string GetOriginalImage()
    {
        if (!String.IsNullOrEmpty(_pathOriginalImage) && File.Exists(_pathOriginalImage))
        {
            return _pathOriginalImage;
        }
        _pathOriginalImage = Path.Combine(Path.GetTempPath(), "Original.jpg");

        Uri imgUri = new Uri("pack://application:,,,/Assets/Original.jpg");
        StreamResourceInfo imgStream = Application.GetResourceStream(imgUri);
        using (Stream imgs = imgStream.Stream)
        using (FileStream fs = File.Create(_pathOriginalImage))
        {
            byte[] ar = new byte[imgs.Length];
            imgs.Read(ar, 0, ar.Length);
            fs.Write(ar, 0, ar.Length);
        }
        return _pathOriginalImage;
    }
    /// <summary>
    /// IDisposable
    /// </summary>
    public void Dispose()
    {
        if (!String.IsNullOrEmpty(_pathOriginalImage) && File.Exists(_pathOriginalImage))
        {
            File.Delete(_pathOriginalImage);
        }
    }
}

Весь пример можно скачать здесь

Answer 3

Есть большая, сложная и популярная область технологий, называемая распознанием образов. По большей части там используются глубокие нейронные сети, но не только. И вряд ли в Вашем случае нужна такая сложность (хотя и может вдохновить на правильный вектор дальнейших действий).

Простой способ – разбить игровое поле на части (кажется, в данной игре это особенно актуально) и сверять каждую часть на сходство с известными предметами. Если же предметы могут находиться в случайных местах, а не только на заданной сетке, то нужен более продвинутый анализ: можно сначала сверять по сетке, затем по той же сетке со смещение на половину (или даже треть и две трети). Ещё можно масштабировать, брать некий хэш от части изображения...

Кстати, если не удастся добиться 99% точности (что в принципе возможно, ведь все предметы будут статично, а не поворачиваться под разным освещением, как в реальном мире), то можно компенсировать точность продуманностью кликов: сохранять позиции, куда уже безуспешно кликах, чтобы в следующие разы не тупить, не тратить время. Тогда главное, чтобы алгоритм поиска выдавал как можно меньше ложно отрицательных срабатываний, а ложно положительные будут быстро отсекаться на практике.

Полезные ссылки:

  • Закрытый вопрос на ruSO, но всё же с некоторыми интересными идеями.

  • Статья о распознании образов (Хабр)

  • Метод Виолы-Джонса для поиска и распознания лиц (Хабр)

READ ALSO
Работа со сценами Unity

Работа со сценами Unity

Всем привет, как загрузить сцену с дефолтными настройками и данными? SceneManagerLoadScene("sceneName"); загружает сцену, но это работает только при первом...

231
Как лучше всего проверить, что окно существует?

Как лучше всего проверить, что окно существует?

Допустим, я пишу функцию, которая по значению дескриптора должна вернуть логическое значение, существует ли окно с таким дескрипторомЯ представляю...

202
Как получить состояние видеокарты NVIDIA?

Как получить состояние видеокарты NVIDIA?

Стоит задача, программно получить напряжение, и желательно нагрузку и частоты как в NVIDIA Inspector с видеокарт NVIDIAБыл реализован вариант с использованием...

204
Как из DataGridView получить в textBox номер колонки?

Как из DataGridView получить в textBox номер колонки?

Задача: На нашей форме есть DataGridView и несколько, к примеру пять textBoxНужно выбирая мышкой ячейки DataGridView получать номер колонки в каждом textBox-е

181