Нейронная сеть на практике: Чтение PNG-изображений из MNIST
Введение
В предыдущей статье «Нейронная сеть на практике: Ручное обратное распространение» я немного рассказал о том, как можно выполнять обратное распространение в наборе последовательно соединённых перцептронов. Хотя это и не лучший способ объяснить эту тему, как мы увидим дальше, изложенное там позволяет нам более непосредственно перейти к другому вопросу.
Хорошо, поскольку некоторых вещей, нужных нам прямо сейчас, у нас нет, придётся реализовать их самим, чтобы сделать ещё несколько шагов в объяснении того, что мы хотим показать о сетях перцептронов.
Возвращаемся к работе
Сейчас мы оказались на распутье. Это связано с тем, что в зависимости от принятого решения мы в итоге реализуем тот или иной тип системы перцептронов.
Знаю, это может показаться странным, поскольку многие считают, что перцептронные сети имеют универсальное назначение. Однако это не так. Зачастую, когда мы видим какую-либо систему перцептронов, созданную с помощью универсальных механизмов, на деле оказывается, что мы используем некий фреймворк, предназначенный для реализации таких систем.
Это не та цель, которой мы хотим достичь в этой серии статей. Цель в том, чтобы объяснить, как можно строить такие системы на языке MQL5, не используя никаких внешних инструментов. Таким образом, хотя сеть, которую мы будем реализовывать, будет довольно ограниченной, она покажет вам, как на самом деле всё работает за кулисами. Это всего лишь вопрос масштаба. Если увеличить масштаб и располагать достаточным объёмом данных для обучения какой-нибудь модели, кто знает, может, нам удастся создать мини-GPT или что-то подобное. Но это не наша цель.
Итак, в чём наша текущая проблема? Ну что ж, мой друг, проблема — в данных. Неважно, какую именно сеть перцептронов вы хотите реализовать: проблема всегда будет в данных. Данные — душа любой системы перцептронов. Без них такие системы никак не построить. Хотя есть способ заставить систему перцептронов генерировать собственные данные. Но в этой серии мы не будем затрагивать подобные вопросы. Так мы не запутаем ваши бедные головки.
Понимая, что проблема — в данных, тех самых данных, которые будут использоваться при обучении и настройке самой сети перцептронов, нам, как и исследователям прошлого, нужна какая-нибудь база данных. В данном случае были приложены огромные усилия, чтобы собрать и вручную классифицировать эти данные — да, всё это делали люди — с максимальной тщательностью и старанием. Это потому, что эти данные действительно очень ценны.
Существуют разные общедоступные базы данных, доступные для использования. Тем не менее мы считаем, что для многих самой ностальгической и интересной остаётся база данных для распознавания рукописных символов. Речь идёт о MNIST, основанной на индо-арабских цифрах.
Итак, раз уж мы решили использовать одну из баз данных MNIST, давайте посмотрим, почему было принято именно такое решение. За этой базой данных стоит целая история. Но каждому стоит самостоятельно изучить этот вопрос, чтобы лучше понять, почему она существует и с чего всё началось. Если поискать в Интернете, можно найти множество материалов на эту тему. Прежде всего, вы найдёте множество материалов, где объясняется, как разработать простую сеть перцептронов с помощью Python, чтобы воспроизвести результаты, получаемые при использовании фреймворков.
По сути, многие энтузиасты начинают изучать эти фреймворки именно из-за этой базы данных MNIST и опубликованных примеров кода на Python. В конце концов, именно там изучается значительная часть доступных средств и библиотек.
Но нас не интересует Python, и не поймите меня неправильно: наша цель — MQL5 и понимание того, как всё на самом деле работает. Поэтому мы оставим в стороне возможность сделать это на Python и сосредоточимся на MQL5. Когда всё будет должным образом реализовано, вы сможете понять, как на самом деле сеть перцептронов делает то, что делает, не скрывая внутреннее устройство за готовыми фреймворками, полными, так сказать, не слишком прозрачных деталей.
Хорошо, путь будет долгим и трудоёмким. Но нужно сделать первый шаг. И, по сути, то, что нам предстоит сделать, несколько выходит за рамки основного назначения MetaTrader 5. Платформа изначально создавалась для использования в качестве торговой платформы. Однако здесь нам придётся пойти дальше и использовать платформу совершенно иначе. И многих вещей, которые нам нужны, в библиотеке MQL5 нет. В частности, нет чтения изображений в формате PNG.

Изображение 01
База данных, которую мы будем использовать, будет в основном содержать именно такую информацию. Каждое изображение — это отдельный файл и, следовательно, может использоваться для создания целого набора обучающих данных или для экспериментов с конкретной сетью перцептронов.
Если вы не спеша поищете, то найдёте множество упоминаний об использовании TensorFlow в сети именно такого типа, которую мы собираемся построить. Однако TensorFlow — лишь один из множества доступных способов реализации системы обучения. Понятно, что, если бы мы использовали нечто подобное, с самого начала у нас было бы в распоряжении всё необходимое и нам не пришлось бы разбираться, как реализовать всю связанную с этим математику. Хотя это может оказаться весьма удобным, в некоторых случаях использовать подобное решение нецелесообразно. Это связано с тем, что в итоге нам может потребоваться гораздо больше вычислительной мощности, чем действительно было бы нужно, если бы мы реализовали решение специально для конкретной задачи. Имейте в виду следующее: я не говорю, что тот или иной фреймворк хорош или плох, и не утверждаю, что нам следует отказаться от них всех и начать с нуля. Дело не в этом. Я хочу показать, как всё это устроено изнутри.
Для этого необходимо окунуться в мир, о реальном устройстве которого многие из вас даже не представляют. Именно поэтому система перцептронов — столь сложная и в то же время интересная тема. Как только нам начинает казаться, что мы уже что-то знаем, сразу появляется что-то новое, чему нужно учиться.
Хорошо, вернёмся к нашей исходной задаче. На данный момент класс C_Neuron ещё не вполне готов к использованию в реализации, которую нам нужно создать. У нас пока нет средств для настройки топологии нейронной сети, которая могла бы значительно упростить разработку системы. Мы также не можем читать файлы PNG, поскольку библиотека MQL5 по умолчанию не предоставляет такой функциональности. Кроме того, нам ещё нужно разобраться в других вопросах, которые пока не были объяснены, просто потому, что до сих пор не было необходимости затрагивать эти моменты. В общем, похоже, что впереди нас ждёт долгий путь. Но, как гласит пословица: всё начинается с первого шага. И этот шаг мы только что сделали, решив двигаться к системе распознавания рукописных цифр, которые входят в базу данных MNIST.
Понимание PNG-изображения
Многие, наверное, думают, что в информатике всё похоже на какую-то магию. Или, по крайней мере, что те, кто всё это создаёт, — какие-то божественные существа или полубоги, ведь поначалу всё это кажется совершенно бессмысленным. Однако, по сути, в информатике всё, абсолютно всё, сводится к одному: БИТАМ. Всегда помните об этом, и вы сможете сделать в информатике что угодно. Секрет в том, как организованы биты, и именно эта организация показывает нам, что они на самом деле делают. Но и это ещё не всё: каждая модель организации сама по себе ничего не говорит нам о том, что содержит набор битов. Этот набор мы называем файлом.
Чтобы этот набор битов, или файл, имел какой-то смысл, он должен быть организован совершенно определённым образом. Полностью случайного файла не существует. Если бы такой файл был создан, его содержимое было бы не чем иным, как мусором. Но даже файл, который кажется мусором, на самом деле может содержать очень ценную информацию. Всё зависит от того, как были организованы данные, из которых в итоге был сформирован файл. Зная это, вы сможете сделать что угодно.
Итак, рассматривая файл изображения, а точнее PNG-файл, вы увидите нечто похожее на следующее изображение.

Изображение 02
Теперь я хочу спросить вас: какой смысл для вас имеют данные, представленные на изображении выше? Возможно, никакого. Однако, если вы воспользуетесь программой или приложением, способным интерпретировать эти данные, вы сможете получить нечто вроде того, что показано на следующем изображении.

Изображение 03
Ну вот, теперь эти данные начинают обретать какой-то смысл, не так ли? Возможно, это даже не реальная информация, поскольку существуют методы шифрования и сжатия, цель которых как раз и заключается в том, чтобы создать нечто, имеющее смысл для одних, но на самом деле служащее для передачи другого сообщения, которое останется скрытым, если только вы не воспользуетесь соответствующим приложением, чтобы его обнаружить. В общем, возможно, в будущем мы сможем поговорить об этом подробнее и лучше объяснить, как это работает и как это можно реализовать. Это тоже очень интересная тема, и, так сказать, на её фоне работа с сетями перцептронов кажется детской забавой.

Изображение 04
Эта часть — сигнатура файла, которая указывает формат или тип файла. Теперь обратите внимание на следующее: здесь нам не нужно реализовывать все возможные варианты формата PNG; по крайней мере, я пока не собираюсь этого делать. Нам нужно лишь, чтобы код считывал тип изображения. А данные, представленные на предыдущих изображениях, как раз и представляют собой ту информацию, которую нам придётся считывать из файла PNG. Поэтому не пытайтесь использовать этот код так, будто он может считывать любое изображение PNG, потому что на практике он этого не сможет. Итак, можем продолжать.
Сначала мы создадим основу кода. Его цель — проверить, работаем ли мы с PNG-файлом или пытаемся его прочитать. Это довольно просто реализовать. Однако с самого начала мы выстроим код так, чтобы потом не пришлось его переделывать. Итак, первая часть кода приведена ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class C_PNG 05. { 06. private: 07. 08. public: 09. C_PNG(const string szFileName) 10. { 11. #define macroERROR(msg) { Print(msg); FileClose(f_handle); return; } 12. 13. const uchar File_Signature[] = 14. { 15. 0x89, 0x50, 0x4E, 0x47, 16. 0x0D, 0x0A, 0x1A, 0x0A 17. }; 18. 19. int f_handle; 20. uint size; 21. uchar buff[]; 22. 23. if((f_handle = FileOpen(szFileName, FILE_BIN)) < 0) 24. macroERROR(StringFormat("Error accessing file: %s", szFileName)); 25. 26. size = FileReadArray(f_handle, buff, 0, File_Signature.Size()); 27. if(size != File_Signature.Size()) 28. macroERROR(StringFormat("Error reading the file: %s", szFileName)); 29. 30. if(ArrayCompare(buff, File_Signature, 0, 0, size) != 0) 31. macroERROR("File is not a PNG image."); 32. 33. Print("ok.."); 34. 35. FileClose(f_handle); 36. 37. #undef macroERROR 38. } 39. }; 40. //+------------------------------------------------------------------
Код 01
Этот код — наш начальный класс, и для его проверки нам понадобится ещё один фрагмент кода. Поскольку мы только начинаем, можем использовать скрипт, так как это позволяет сделать исходный код проще. Он показан ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 04. #include <Neural Network\C_PNG.mqh> 05. //+------------------------------------------------------------------+ 06. //| Функция запуска программы-скрипта | 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. C_PNG png("mnist/10032.png"); 11. } 12. //+------------------------------------------------------------------
Код 02
Поскольку эти два фрагмента кода довольно просты для понимания, мы не будем вдаваться в подробности их работы. Однако при выполнении этого кода 02, если указанный в строке 08 файл существует, в терминале MetaTrader 5 появится следующий результат.

Изображение 05
Блоки BYTE
После завершения предыдущего этапа мы уже можем определить, работаем ли мы с изображением или с файлом другого типа. Если мы работаем с изображением, можем перейти к той части, где получим его структуру. Как правило, такая организация варьируется от одного файла к другому. Однако в целом у всех есть структура, в которой данные организованы определённым образом. Если посмотреть на сам файл, всё может показаться сплошным беспорядком. Но, поверьте, внутренняя организация там есть. В противном случае все содержащиеся там данные были бы потеряны навсегда.

Изображение 06
Этот момент нас интересует, поскольку здесь содержатся метаданные, предоставляющие нам различную информацию об изображении, хранящемся в файле. По сути, понимание того, что представляют собой эти значения и как они организованы, поможет нам восстановить изображение. Именно здесь хорошее знание используемого нами языка позволит нам получать доступ к данным с большей точностью и лёгкостью. Существует проблема, актуальная для любого ПО: разрядность типов данных.
Файлы состоят из битов, хотя часто мы читаем их так, будто это слова, числа или что-то ещё. По сути, это не более чем биты. Однако эти биты могут быть организованы по-разному, если рассматривать вопрос с точки зрения битовой ширины (разрядности) типов данных. И в зависимости от используемого языка программирования нам придётся адаптировать код, чтобы он работал с правильной разрядностью. В противном случае всё прочитанное будет бесполезным, поскольку некоторые биты займут не свои позиции, и данные исказятся. К счастью, в MQL5 есть типы данных с подходящей разрядностью. Хотя MQL5 не обладает такой же гибкостью, как C/C++, будет вполне достаточно тех структур, которые предоставляет MQL5. Поэтому нам придётся создать внутреннюю структуру для чтения блока, который предоставляет нам некоторые данные об изображении. Это довольно просто сделать — и при создании структуры, и при её чтении, как вы можете видеть в коде ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class C_PNG 05. { 06. private: 07. struct st_IHDR 08. { 09. uint lenght; 10. uchar type[4]; 11. uint width; 12. uint height; 13. uchar depth; 14. uchar colour; 15. uchar compression; 16. uchar filter; 17. uchar intelace; 18. }; 19. 20. public: 21. C_PNG(const string szFileName) 22. { 23. #define macroERROR(msg) { Print(msg); FileClose(f_handle); return; } 24. #define PrintX(a) PrintFormat("%s: %d => 0x%x", #a, a, a); 25. 26. const uchar File_Signature[] = 27. { 28. 0x89, 0x50, 0x4E, 0x47, 29. 0x0D, 0x0A, 0x1A, 0x0A 30. }; 31. 32. int f_handle; 33. uint size; 34. uchar buff[]; 35. st_IHDR ihdr; 36. 37. if((f_handle = FileOpen(szFileName, FILE_BIN)) < 0) 38. macroERROR(StringFormat("Error accessing file: %s", szFileName)); 39. 40. size = FileReadArray(f_handle, buff, 0, File_Signature.Size()); 41. if(size != File_Signature.Size()) 42. macroERROR(StringFormat("Error reading the file: %s", szFileName)); 43. 44. if(ArrayCompare(buff, File_Signature, 0, 0, size) != 0) 45. macroERROR("File is not a PNG image."); 46. 47. FileReadStruct(f_handle, ihdr); 48. 49. PrintX(ihdr.lenght); 50. PrintX(ihdr.width); 51. PrintX(ihdr.height); 52. 53. FileClose(f_handle); 54. 55. #undef PrintX 56. #undef macroERROR 57. } 58. }; 59. //+------------------------------------------------------------------
Код 03
Сама структура создаётся в восьмой строке. После этого, если снова запустить код, мы увидим следующую информацию.

Изображение 07
Вот это да. Просто зрелище. Посмотрите, какая красивая ерунда. Мы только что обнаружили баг в языке программирования. Это связано с тем, что данные, которые должны поступать из файла, в точности совпадают с теми, что были показаны на предыдущем рисунке. То есть значения записаны в обратном порядке. В этот момент многие сдаются и начинают проклинать всё на свете — от БОГА до ДЬЯВОЛА, осыпая используемый язык программирования всевозможными ругательствами, потому что при использовании другого языка значения отображаются правильно, именно так, как и ожидалось. Но прежде чем вы начнёте думать, что в языке действительно есть баг, давайте разберёмся, что на самом деле происходит.
Скорее всего, вы привыкли видеть целочисленные значения, хранящиеся в порядке LITTLE ENDIAN. Однако существует и порядок BIG ENDIAN, при котором значения хранятся немного иначе. И почему же так происходит? Ну, всё из-за процессора. Это никак не связано с языком программирования. Некоторые процессоры используют порядок LITTLE ENDIAN, а другие — порядок BIG ENDIAN. Знаю, звучит как безумие. Долгое время мне было трудно понять этот нюанс, связанный со значениями, которые на самом деле интерпретирует процессор, поскольку в зависимости от реализации способ представления значений может радикально меняться. Таким образом, то, что вы только что увидели, — не недостаток языка программирования, а вопрос, связанный с реализацией процессора.
Есть несколько способов это исправить. Но, по сути, нам нужно просто поменять порядок байтов, чтобы получить представление, которое мы действительно сможем понять. Ожидаемыми значениями были 28 по высоте и 28 по ширине, а не эти нелепые числа. Поскольку я хочу максимально поощрять использование чистого MQL5, мы будем использовать то, что предлагает нам сам язык. Как бы хорошо мы ни программировали, у нас не будет такого же доступа к процессору, как у разработчиков языка. Поэтому лучше использовать функции стандартной библиотеки, чем создавать функцию с той же целью. После исправления кода мы получим то, что показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class C_PNG 05. { 06. private: 07. union uint32 08. { 09. uint value; 10. uchar c[sizeof(uint)]; 11. }; 12. 13. struct st_IHDR 14. { 15. uint32 lenght; 16. uchar type[4]; 17. uint32 width; 18. uint32 height; 19. uchar depth; 20. uchar colour; 21. uchar compression; 22. uchar filter; 23. uchar intelace; 24. }; 25. 26. public: 27. C_PNG(const string szFileName) 28. { 29. #define macroERROR(msg) { Print(msg); FileClose(f_handle); return; } 30. #define PrintX(a) PrintFormat("%s: %d => 0x%X", #a, a, a); 31. 32. const uchar File_Signature[] = 33. { 34. 0x89, 0x50, 0x4E, 0x47, 35. 0x0D, 0x0A, 0x1A, 0x0A 36. }; 37. const uchar Header[] = { 'I', 'H', 'D', 'R' }; 38. 39. int f_handle; 40. uint size; 41. uchar buff[]; 42. st_IHDR ihdr; 43. 44. if((f_handle = FileOpen(szFileName, FILE_BIN)) < 0) 45. macroERROR(StringFormat("Error accessing file: %s", szFileName)); 46. 47. size = FileReadArray(f_handle, buff, 0, File_Signature.Size()); 48. if(size != File_Signature.Size()) 49. macroERROR(StringFormat("Error reading the file: %s", szFileName)); 50. 51. if(ArrayCompare(buff, File_Signature, 0, 0, size) != 0) 52. macroERROR("File is not a PNG image."); 53. 54. FileReadStruct(f_handle, ihdr); 55. if(ArrayCompare(ihdr.type, Header) != 0) 56. macroERROR("Error reading the header."); 57. 58. ArrayReverse(ihdr.lenght.c); 59. ArrayReverse(ihdr.width.c); 60. ArrayReverse(ihdr.height.c); 61. 62. PrintX(ihdr.lenght.value); 63. PrintX(ihdr.width.value); 64. PrintX(ihdr.height.value); 65. 66. FileClose(f_handle); 67. 68. #undef PrintX 69. #undef macroERROR 70. } 71. }; 72. //+------------------------------------------------------------------
Код 04
Это и правда выглядит сложнее, чем есть на самом деле. Но, поверьте, так сделать проще. Таким образом, когда мы снова запустим код, получим результат, который видим ниже.

Изображение 08
Отлично, у нас уже есть первая часть того, что нам нужно. Теперь нам осталось только прочитать изображение, чтобы мы могли его отрисовать или как-то использовать. Но, чтобы сделать это правильно, нам нужно обратиться к документации формата. Это позволит нам действительно правильно восстановить изображение. В конце этой статьи я оставлю в разделе «Ссылки» информацию о том, где можно посмотреть более подробные сведения, на случай, если вы захотите использовать изображения, отличные от того, которое я покажу здесь.
Загрузка PNG-изображения
Всё, что мы сделали до этого момента, — часть базового блока, который постоянно повторяется, независимо от того, что именно мы делаем. Будь то загрузка изображения, попытка распаковать файл или что-то ещё. По сути, всё делается именно так. Поскольку здесь нас интересует загрузка изображения, всё, что было сделано до этого момента, применимо к любому PNG-изображению, которое мы собираемся загружать. Однако то, что мы будем делать сейчас, будет ориентировано именно на то, что нам нужно. Поэтому, если вы хотите сделать это по-другому, вам придётся изучить документацию по формату. Блок данных изображения показан ниже.

Изображение 09
Данные изображения находятся в выделенной области, показанной ниже.

Изображение 10
Но обратите внимание: данные, которые мы видим, сжаты. Отлично, теперь мы знаем всё, что нам нужно. Однако мы можем немного упростить код. Хотя всё показанное до сих пор работает, нам пришлось сделать кое-что, что при использовании MQL5 оказывается несколько излишним. Поэтому весь приведённый выше код можно улучшить и привести к виду, показанному ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class C_PNG 05. { 06. private: 07. union uint32 08. { 09. uint value; 10. uchar c[sizeof(uint)]; 11. }; 12. 13. union un_IHDR 14. { 15. struct stIHDR 16. { 17. uint32 width; 18. uint32 height; 19. uchar depth; 20. uchar colour; 21. uchar compression; 22. uchar filter; 23. uchar intelace; 24. } infos; 25. uchar c[sizeof(stIHDR)]; 26. }; 27. 28. bool Read_Chunk(int &file,const uchar &type[],uchar &buff[],uint &crc) 29. { 30. uint32 len; 31. uchar loc[]; 32. 33. FileReadArray(file, len.c, 0, 4); 34. ArrayReverse(len.c); 35. FileReadArray(file, loc, 0, 4); 36. 37. if(len.value > 0) 38. FileReadArray(file, buff, 0, len.value); 39. 40. FileReadArray(file, len.c, 0, 4); 41. ArrayReverse(len.c); 42. crc = len.value; 43. 44. return(ArrayCompare(type, loc) == 0); 45. } 46. 47. public: 48. C_PNG(const string szFileName) 49. { 50. #define macroERROR(msg) { Print(msg); FileClose(f_handle); return; } 51. 52. const uchar File_Signature[] = 53. { 54. 0x89, 0x50, 0x4E, 0x47, 55. 0x0D, 0x0A, 0x1A, 0x0A 56. }; 57. const uchar Chunk_IHDR[] = { 'I', 'H', 'D', 'R' }; 58. const uchar Chunk_IDAT[] = { 'I', 'D', 'A', 'T' }; 59. const uchar Chunk_IEND[] = { 'I', 'E', 'N', 'D' }; 60. 61. int f_handle; 62. uint tmp; 63. uchar buff[]; 64. uchar img[]; 65. un_IHDR ihdr; 66. 67. if((f_handle = FileOpen(szFileName, FILE_BIN)) < 0) 68. macroERROR(StringFormat("Error accessing file: %s", szFileName)); 69. 70. tmp = FileReadArray(f_handle, buff, 0, File_Signature.Size()); 71. if(tmp != File_Signature.Size()) 72. macroERROR(StringFormat("Error reading the file: %s", szFileName)); 73. 74. if(ArrayCompare(buff, File_Signature, 0, 0, tmp) != 0) 75. macroERROR("File is not a PNG image."); 76. 77. if(!Read_Chunk(f_handle, Chunk_IHDR, ihdr.c, tmp)) 78. macroERROR("Error in image data."); 79. 80. ArrayReverse(ihdr.infos.width.c); 81. ArrayReverse(ihdr.infos.height.c); 82. 83. if(!Read_Chunk(f_handle, Chunk_IDAT, img, tmp)) 84. macroERROR("Error in image data."); 85. if(!Read_Chunk(f_handle, Chunk_IEND, buff, tmp)) 86. macroERROR("Error in image data."); 87. 88. FileClose(f_handle); 89. 90. PrintFormat("width: %d", ihdr.infos.width.value); 91. PrintFormat("height: %d", ihdr.infos.height.value); 92. ArrayPrint(img); 93. 94. #undef macroERROR 95. } 96. }; 97. //+------------------------------------------------------------------+
Код 05
Теперь, при выполнении этого кода, мы получаем следующий результат:

Изображение 11
То есть у нас сразу есть все нужные нам данные. Теперь осталось только распаковать изображение, хранящееся в массиве IMG, который был показан выше. Обратите внимание, что код стал гораздо проще для чтения и, поскольку нам осталось только распаковать изображение, мы можем перейти к окончательному варианту кода, который полностью приведён ниже.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. class C_PNG 005. { 006. private: 007. union uint32 008. { 009. uint value; 010. uchar c[sizeof(uint)]; 011. }; 012. 013. union un_IHDR 014. { 015. struct stIHDR 016. { 017. uint32 width; 018. uint32 height; 019. uchar depth; 020. uchar colour; 021. uchar compression; 022. uchar filter; 023. uchar intelace; 024. } infos; 025. uchar c[sizeof(stIHDR)]; 026. } m_IHDR; 027. 028. uchar m_Pixels[]; 029. 030. bool Read_Chunk(int &file,const uchar &type[],uchar &buff[],uint &crc) 031. { 032. uint32 len; 033. uchar loc[]; 034. 035. FileReadArray(file, len.c, 0, 4); 036. ArrayReverse(len.c); 037. FileReadArray(file, loc, 0, 4); 038. 039. if(len.value > 0) 040. FileReadArray(file, buff, 0, len.value); 041. 042. FileReadArray(file, len.c, 0, 4); 043. ArrayReverse(len.c); 044. crc = len.value; 045. 046. return(ArrayCompare(type, loc) == 0); 047. } 048. 049. bool Decode(const uchar &img[],uint width,uint height) 050. { 051. uchar buff[]; 052. uchar key[]; 053. 054. if(!CryptDecode(CRYPT_ARCH_ZIP, img, key, buff)) 055. return(false); 056. 057. ArrayResize(m_Pixels, width * height); 058. for(uint c = 0, c1 = 0, t = 1, m = buff.Size(); c < m; c++, c1++, t++) 059. { 060. m_Pixels[c1] = buff[c]; 061. if(t == width) 062. { 063. c++; 064. t = 0; 065. } 066. } 067. 068. return(true); 069. } 070. 071. public: 072. C_PNG(const string szFileName) 073. { 074. #define macroERROR(msg) { Print(msg); FileClose(f_handle); return; } 075. 076. const uchar File_Signature[] = 077. { 078. 0x89, 0x50, 0x4E, 0x47, 079. 0x0D, 0x0A, 0x1A, 0x0A 080. }; 081. const uchar Chunk_IHDR[] = { 'I', 'H', 'D', 'R' }; 082. const uchar Chunk_IDAT[] = { 'I', 'D', 'A', 'T' }; 083. const uchar Chunk_IEND[] = { 'I', 'E', 'N', 'D' }; 084. 085. int f_handle; 086. uint tmp; 087. uchar buff[]; 088. uchar img[]; 089. 090. ArrayResize(m_Pixels, 0); 091. 092. if((f_handle = FileOpen(szFileName, FILE_BIN)) < 0) 093. macroERROR(StringFormat("Error accessing file: %s", szFileName)); 094. 095. tmp = FileReadArray(f_handle, buff, 0, File_Signature.Size()); 096. if(tmp != File_Signature.Size()) 097. macroERROR(StringFormat("Error reading the file: %s", szFileName)); 098. 099. if(ArrayCompare(buff, File_Signature, 0, 0, tmp) != 0) 100. macroERROR("File is not a PNG image."); 101. 102. if(!Read_Chunk(f_handle, Chunk_IHDR, m_IHDR.c, tmp)) 103. macroERROR("Error in image data."); 104. 105. ArrayReverse(m_IHDR.infos.width.c); 106. ArrayReverse(m_IHDR.infos.height.c); 107. 108. if(!Read_Chunk(f_handle, Chunk_IDAT, img, tmp)) 109. macroERROR("Error in image data."); 110. if(!Read_Chunk(f_handle, Chunk_IEND, buff, tmp)) 111. macroERROR("Error in image data."); 112. 113. FileClose(f_handle); 114. 115. if(!Decode(img, m_IHDR.infos.width.value, m_IHDR.infos.height.value)) 116. macroERROR("Error in image data."); 117. 118. #undef macroERROR 119. } 120. 121. uint GetImage(uchar &arr[]) 122. { 123. if(!m_Pixels.Size()) 124. return(0); 125. 126. ArrayCopy(arr, m_Pixels); 127. 128. return(m_IHDR.infos.width.value); 129. } 130. }; 131. //+------------------------------------------------------------------+
Код 06
Обратите внимание, что в строке двадцать семь у нас есть массив. В него будет помещено уже распакованное изображение. Процесс распаковки относительно прост, поскольку в MQL5 есть вызов, который позволяет сделать это без особых трудностей. Этот вызов находится в строке пятьдесят один. Массив Key можно оставить пустым, так как здесь не используется никакой ключ безопасности. Если процесс пройдёт успешно, массив buff будет содержать распакованные данные.
Однако эти данные по-прежнему содержат информацию, которая не нужна нам для конечного изображения. Нам нужно отфильтровать содержимое, прежде чем закончить. Таким образом, в строке пятьдесят три мы выделяем необходимое место для изображения. А в строке пятьдесят четыре мы входим в цикл, цель которого — удалить некоторые значения, не входящие в итоговое изображение. Когда этот цикл завершится, в массиве m_Pixels будет находиться нужное нам изображение, которое мы и будем использовать.
В завершение этого класса в строке сто девять у нас есть функция, которая возвращает изображение, а также его ширину. Таким образом, мы можем выполнять чтение гораздо спокойнее и без лишних неожиданностей.
Итак, теперь давайте посмотрим на код, в котором используется этот класс. Его полный текст можно увидеть ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #include <Neural Network\C_PNG.mqh> 05. #define protected public 06. #include <Canvas\Canvas.mqh> 07. #undef protected 08. //+------------------------------------------------------------------+ 09. void SaveInText(string szFileName,const uchar &buff[],const uint width) 10. { 11. int file; 12. string s = ""; 13. 14. if((file = FileOpen(szFileName + ".txt", FILE_WRITE | FILE_TXT)) == INVALID_HANDLE) 15. return; 16. 17. for(uint c = 0, t = 1; c < buff.Size(); c++, t++) 18. { 19. s += StringFormat("%3d ", buff[c]); 20. if(t == width) 21. { 22. s += "\n\r"; 23. FileWriteString(file, s); 24. s = ""; 25. t = 0; 26. } 27. } 28. 29. s += "\n\r"; 30. FileWriteString(file, s); 31. 32. FileClose(file); 33. } 34. //+------------------------------------------------------------------+ 35. void View_IMAGE(uint x,uint y,uchar s,const uchar &buff[],const uint width) 36. { 37. CCanvas canvas; 38. const uint real = width * s; 39. 40. if(!canvas.CreateBitmapLabel(0, 0, "tmp", 100, 100, real, real, 41. COLOR_FORMAT_ARGB_NORMALIZE)) 42. { 43. Print("Error creating canvas: ", GetLastError()); 44. return; 45. } 46. 47. color bmp[]; 48. double fx = (real * 1.0) / width; 49. uint pyi, pyf, pxi, pxf, tmp, uc; 50. 51. ArrayResize(bmp, real * real); 52. for(uint cy = 0, y = 0; cy < width; cy++, y += width) 53. { 54. pyf = (uint)(fx * cy) * real; 55. tmp = pyi = (uint)(fx * (cy - 1)) * real; 56. 57. for(uint x = 0; x < width; x++) 58. { 59. pxf = (uint)(fx * x); 60. pxi = (uint)(fx * (x - 1)); 61. uc = ColorToARGB(buff[y + x], 255); 62. bmp[pxf + pyf] = (color)uc; 63. 64. for(pxi++; pxi < pxf; pxi++) 65. bmp[pxi + pyf] = (color)uc; 66. } 67. 68. for(pyi += real; pyi < pyf; pyi += real) 69. for(uint x = 0; x < real; x++) 70. bmp[x + pyi] = bmp[x + tmp]; 71. } 72. 73. ArrayCopy(canvas.m_pixels, bmp); 74. ArrayFree(bmp); 75. 76. canvas.Update(); 77. } 78. //+------------------------------------------------------------------+ 79. //| Функция запуска программы-скрипта | 80. //+------------------------------------------------------------------+ 81. void OnStart(void) 82. { 83. uchar pixels[]; 84. uint width; 85. 86. C_PNG png("mnist/10032.png"); 87. if(!(width = png.GetImage(pixels))) 88. return; 89. 90. View_IMAGE(10, 10, 10, pixels, width); 91. SaveInText("mnist", pixels, width); 92. 93. Print("ok..."); 94. } 95. //+------------------------------------------------------------------+
Код 07
Поскольку этот код довольно прост, я не вижу в нём ничего, что требовало бы особого внимания. Но при его запуске вы увидите на графике MetaTrader 5 результат, показанный на следующем изображении.

Изображение 12
Одно замечание: на этом изображении использованы ложные цвета, поскольку правильно было бы выполнить отрисовку в градациях серого. Но в любом случае нас интересует то, что изображение из PNG-файла было загружено корректно и уже может использоваться. Кроме того, поскольку мы также создаём текстовый файл, содержимым которого будет значение каждого пикселя, у нас получится нечто вроде того, что показано на следующем изображении.

Изображение 13
Заключительные замечания
В этой статье мы начали реализовывать элементы, необходимые для демонстрации работы, пожалуй, самой наглядной сети перцептронов из всех.
Эта сеть используется во множестве экспериментов и помогает объяснить многие вещи. В этой модели сети перцептронов используется база данных MNIST, которую можно скачать, чтобы вы тоже могли построить такую сеть в учебных целях. Поскольку речь идёт о четырёх сжатых файлах, а скачивать их или нет — дело личного интереса, я оставлю это на ваше усмотрение, по крайней мере пока. В любом случае я всегда буду добавлять несколько PNG-файлов во вложения, чтобы у вас тоже был материал, которым вы сможете воспользоваться.
Важное замечание: поскольку код, приведённый в этой статье, предназначен исключительно для работы с PNG-файлами, входящими в базу данных MNIST, он не сможет правильно отрисовывать PNG-файлы других типов, так как это не входит в его задачу. И в качестве бонуса мы также укажем, где можно скачать исходный код для отрисовки других типов PNG, разработанный другим программистом MQL5.
| Файл MQ5 | Описание |
|---|---|
| Script\Example 01 | Базовая демонстрация |
Ссылки
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/14017
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
За пределами максимальной просадки: Создание анализатора ДНК просадки на MQL5
От начального к среднему уровню: Файл шаблона графика (III)
Теория экстремальных значений в MQL5: Создание индикатора риска обвала с оценкой хвоста распределения — за рамками VaR по методу Монте-Карло
Индикатор средней цены входа маркет-мейкера
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования