👄

Mojoの画像処理(PIL・OpenCV)がPythonより3倍遅く3倍速い

に公開

Mojo🔥と画像処理

兎角Mojo🔥は情報に乏しい。開発途上であるから仕方ないとはいえ、他言語と比べると明らかに「何かを作った」ような情報が見られない。

公式のチュートリアルではアプリケーションを作っているようだが、これはライフゲームという数理的なものであり、何か実務的な機能性の実装をMojo🔥でやってのけているわけではない。果ては画面を実装するためにPythonを引き合いに出され、拍子抜けという言葉が当てはまるようであった。

本記事には、「Mojo🔥で画像処理する」とは表現しない。何となれば、純粋なMojo🔥の機能で画像を扱う方法が分からなかったためである。

ファイルの扱い

少し詳しく説明すれば、単に画像をファイルとして読み込むことならばできる。

先ずは普通にテキストファイルを読み込む方法を述べよう。

https://docs.modular.com/mojo/stdlib/io/file/open/

'sleeping.mojo'というテキストファイルを読み込む
(mojo_env) $ mojo
Welcome to Mojo! 🔥

Expressions are delimited by a blank line.
Type `:quit` to exit the REPL and `:mojo help` for further assistance.

  1> with open('sleeping.mojo', 'r') as f: 
  2.   print(f.read())
from time import sleep

fn main():
    print('exit the program with Ctrl+C')
    while True:
        print('sleeping 1 second')
        sleep(1.0)

  3>  

同様にして画像も読み込むことができる。

'zenn.jpg'というファイルを読み込む
  1> with open('zenn.jpg', 'r') as f: 
  2.   print(f.read()) 
  3.    
����JFIF���ICC_PROFILE�lcms0mntrRGB XYZ �

                                        ,acspAPPL���-lcms
                                                         des8cprt@Nwtpt�chad�,rXYZ�bXYZ�gXYZ�rTRC
                      gTRC, bTRCL chrml$mluc
                                            enUSsRGB built-inmluc
                                                                 enUS2No copyright, use freelyXYZ ���-sf32
                              J����*��������������XYZ o�8��XYZ $����XYZ b���Y�araff�
Y�araff�
Y�araff�
[chrm��T{L���&f\��C



                        !"$"$����"�����
                                       ��|H(^O�~P_~_�}��r�/�Ǹ�>�R�iB��ϩ˦
                                                                        �}0pmt�i�� ���οjr�ېG���_?xg�@�8�����Xsǩ�g�L��9w��
LH��O��_kת[�EE�
��ލ������`�`�Vu_"����c%sZOD9�0�w��j3��d�L{
                                      ��S�}
k���|��F�.9����5;"�@�|����L�M�����`o7\��
��5��)�2G�K��̀��t��                          :��?v��}�U��͍f1��
��ЀU����0c�ς�L�����y��n��,�@\���~~�ڏG>���?��
                                            �P�ϲ}>G3��2K�L���̀�C���^]�n&��
��:x��B�3bs�&�e����G
*(����YH3����$ @P!����ݙ!���r�y)z�뒗�J^)z�bw�YI��,n�|��;b�8�b�m�A��
                                                                    �q��4m�|��=�ʲ?�#������MEr�����z%��"FT�����I�fQB�0؃gȒV�%#�L�#l�����6Uq���m$�&5^�`H��Tkf�d*H�*�Np��ƣ%��^쪣l����>U���m��!�H��h��"!y)z�뒗�J^�)zm��X�ߏ���J3�|�ctnV�vC�F�8�!����\��Y��^юI\seK~��p9U�ʲ?�
                                �d��*��A��9>A򧍺\�$x�_�Ũ�X1�0=��;b�#qKqr�R|��|�cu_h�h� �1����̮��nQ�64l�����*��A�:e,�$r��UF��a!#�_��?V�?�
                                                         �d�*�ۦ��z%��"FT��ɕ���d&8��&�>�Ԟ�d8��!bg�c�W�R��g� �G�SF�W2{ߕt"B'D��$�*��D��d��_��U`G���Y����Q\�@����'�Y2����n�d���.V��Z��k���.V���_�Y�7UH���k���.V��Z���u&W    �6����, !01@a#AR2C`p�?�b����mWk減_4��x]>�%"�lf�Tl*3!�i�$�E�D�s�p��do/���
                                                            �V)G��H�N���@�����ӮTs�qZ���$sR_/9|ⱼ����l2]XH�$ i�BF�hm��ZE2�i#<BN��O$�H��{�dl��y0��7\
f�k4��:#ѐ���p�hå��o��,1!@ 0aACR#2`p�?�b��tǾ+�=�]1�}��tW�����5($jjK��.�#0^r�JBF�<JG�,(�(���C��R�������.���O����B)��H   ϑ��'�ã�7kX@����T|H�o9�ڀӶx���<Nj}��s��e���N�<> X�b����Wҳ��Tv�K��o�n��N���oL忲޴N��xtky��ぴ�y��ʏ������ҦH�s���~��q��s�m6��OWRz������?�u'��=Gx�:����� !"12B@APq��3ar���Q���?�w~3y�#R�h�<F��5��x�c�*�T��].�
                                                     r�n�.]�U��O�i��aQr%��3ڰ��Ε`�
��u���@E�t�Hܡ��30��*�N��[

                          �J�q�S�QY0�cߤ�/˗i�C��zt?t�{��\�I4��
                                                             1���a���_
�       =�
                �=+�S���Z5��U�>g�3�������c�~5�
H}��                                           KUU�e�!
    /:g�]o��Q�f�yq~�Z�  �2`���d �79��Seۋ/ϗx,Md�~���`�=�猻B�'�m)�������
                                                                      ��ϋ�S�����ahה՛���E�?�a��mHjF�=�jm=���o:�Z1��(�
                                        ��Һ��h���1>]3�L1�N,��r�fj����]  =�
        ���0[h�g�a�r�=��m��Qs0�kˋ�\�Wi֍s0�k���6B���nڛ.[R7(jF��lڟ.��)º���{EC��S�?�ߡo3��ܱY��������ݭ�Y��       �6�����P�᧌�2����1��l���1iJC7 9N�raT\P���6y��cg���1��􀋐����(!Q 1AaP@q����������?!���p��h��k�Z�V����k�ZQ�XBO�R�g���������m��f��<J�4�������x�>�����#�
                         �D��;%*��K�s[�wy|H"`.��k�����|듻�Ö��8<N��l��*^]{"8<W0+���K����V.�zҢ�i�����]      ZK��m޷�|Tx�?�\�F��@�x�=l2������}7��]}�?�<W�룗Y[/q0]�˭D/�N�Һ_J�}+�����MV70���V�F�0c_X��.���
N���"���͟�����Nh��*�B��=k�3qˏ�haw_6�Kj���{�h/[��Y��撈���� 3V«�������eb��;�J\��m6�ֹ�b����FZTn���gb�~���u1�n6��#N��K��p����X���U�cd���yu�����f�gt�E[�� �
J���R��읈뗫�dm���!vγ������]�?Y�a�|kVNs�P��S�95�G�=Qkjcd�4�I[��f�Qw�߫��|fZowP�)V�+�u$-^N�־��}d=��#�_UUT1OCX7P_4q�cUUW��-�p�q��
                                                 ��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<|���<��<��<��<��<��>��|��<��<��<��<��]��<��<��<��<��<����c��<��<��<��<������<��<��<��<��'k���<��<��<��<��<�k�<��<��<��<��<}��<��<��<��<��<<,#�<��<��<��o<����<��<��<�=>��<���<��<��<��>�g���?<��<��<�������=��<��<��<�����c��~�?���<��<��$������<��<��<��<8 ���*L��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<���!1Aq0@Qa���� ���`p���?�b���OC�rz1��<�����:{� �       ����ư�&��K�C��,���?x�1~D        �]{�����K���n�����������7tP�M��_#��tPM�D����-�"�Ҏi�G���:�NfD��k^kv��+���1R����:׎��d^�Ɖ�VUb��8�qֿ�X�����m�r=�)�`�څ �
                    :�Oj�N?x��ݸ����j�$��Ƙ�e*5�����V2���v�S���b%"%�u_X�VA�x�Z�ӏ������S�ju���i�      zjp�N������H��7��!1Aa0@Qq���� ����`p�?�b
                                                                �q�O���>#[���r��u�I����H!B��[�k��H1�V�{tf>:c�E�a��wJc�}_���7t��~���3wZoVܽ�1B���)+*��d�k?W��tH�!��W�V4���C����"�913r�kFig?X�����&E���A������8�ٍq����>:c�#V�kG��Ȑ�pƓ�5}b�}�9�[
        1���,j�
)�HE6��o��+1 !AQaq��P���@��တ��?��y�=��v����E��:}#��o��OY90�׋�W��W��W��W��T�(�T     �3=�$��Ϊ�����c�v �_�����0�0����kLJ� �Me�T-�q�y\�&B�vW*�d�,,�J�l�3���H������[^/;c���_e=��� �I͌U��/��)d*n�Z
                                    �T�(Woر��K�Tl�Y�o��+DJ����c�ںzCQ�<�?
                                                                        u��0�����ȟ����ԙ�R����;ZE7����ڏH�v��,"�����<#I�4G�ː����S�A��E�����zI�m���T~���yqȅ�y��N�FyzL:d��/ݻ�|pl�߃��:i�R���S�}�4�v����Ng�7yx������qܒ�L�v.�ރoH��    ����I�7j*��k\+�U9fYAx��x���������
                             ��*�`��3fz�?��we�;@p��x�q�;�t��6�i�P�!v^�����gS��]�\�ſV�֕�S��+v,s;WJ��g�;����r�Df��Hp�?$��X{uj0�P��q�Ҝ:�R� �?.sA���1iI�(%�u.^�@i��F�ya���;�I���S�~OZ@K����3������`(*(2����u�4�M^Xa���':L�F��ߔ�b�e=�M���-"�Gtڸ��R`u{�w�u��k�f��*Q���N��KɁ�'u=�qkLQ��j.:B��ו��C�Mv�-���"��c��K�_��A�:}3��W*F-��`�21�YC��ݿH�g4X��J`L_�u.]'j:,~O�Ȅ�V����b;[;��hDa�INe�����R�Un�5(UlR���ӡo���a������j�
�+��Q�YV��*p�u��j]S�oջ��2R������o�A�%+0�?�꼏꼏꼏꼏깽ߪ
                                                      s�o-��cYa0�tJ���rFU�U�rv�Z~d�TTTQ�1��EEEG��������
  4>  

これは、画像を無理やり文字として表示した様子である。こんなことをしても意味は無い。

Pythonならば、画像を画像として扱う上で、PillowOpenCVを使う。

Pillowを使う例
(mojo_env) $ python3
Python 3.13.0 | packaged by conda-forge | (main, Nov 27 2024, 19:18:50) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from PIL import Image
>>> f = Image.open('zenn.jpg')
>>> f.format
'JPEG'
OpenCVを使う場合
(mojo_env) $ python3
Python 3.13.0 | packaged by conda-forge | (main, Nov 27 2024, 19:18:50) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
>>> f = cv2.imread('zenn.jpg')
>>> f.shape
(400, 400, 3)

併し乍ら、Mojo🔥に於いてこのように便利なものを見つけることができなかった。

MojoCVについて

2023年の記事に、「MojoCV」という言葉が使われていた形跡が見られる。

https://medium.com/artificial-corner/enhancing-ai-development-with-mojo-code-examples-and-best-practices-6341c3e66e15

この記事から該当箇所を引用しよう。

3. Harnessing the Power of MojoCV for Computer Vision
MojoCV, Mojo’s dedicated computer vision library, provides a range of powerful tools and algorithms for image processing tasks. Here’s an example of using MojoCV for image classification:

# Load image using MojoCV
image = mojo.cv.load_image('image.jpg')

# Preprocess the image
preprocessed = mojo.cv.preprocess_image(image)

# Load pre-trained model
model = mojo.cv.load_model('model.mojo')

# Classify the image
predictions = model.predict(preprocessed)

# Get the top predicted class
top_class = mojo.cv.get_top_class(predictions)

# Get the class label
label = mojo.cv.get_label(top_class)

# Display the result
mojo.cv.show_image_with_label(image, label)# Load image using MojoCV
image = mojo.cv.load_image('image.jpg')

# Preprocess the image
preprocessed = mojo.cv.preprocess_image(image)

# Load pre-trained model
model = mojo.cv.load_model('model.mojo')

# Classify the image
predictions = model.predict(preprocessed)

# Get the top predicted class
top_class = mojo.cv.get_top_class(predictions)

# Get the class label
label = mojo.cv.get_label(top_class)

# Display the result
mojo.cv.show_image_with_label(image, label)

With MojoCV, you can effortlessly perform various computer vision tasks such as image classification, object detection, and image segmentation.

MojoCVというものが使えるかのような言説だが、現在使える使えない以前に、そもそもこの存在自体を疑わずにはいられない。

探してみると、次の記事にも見られる。

https://medium.com/@Soke.AI/mojo-the-python-superset-thats-got-me-all-fired-up-961505c7c516

二つの筆者は別人のようだが、一つ目は2023/05/31、二つ目は2023/06/12と、投稿日は近いらしい。そして私の調べた限り、この二つの記事を除いて、MojoCVという言葉を使っている情報が一つとして見られない。この二者がどこからこのような言葉を、そしてコードを知ったのか、今となっては知り得ない。

この記事にあるコードを見ると、mojo.cvとある。このmojoとはMojo🔥に於けるmoduleらしく、import mojoなどと書かれている。

# Mojo code
import mojo

a := [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
b := [[9, 8, 7], [6, 5, 4], [3, 2, 1]]

c := mojo.dot(a, b)

しかし少なくとも、現在のMojo🔥にmojoなどというものは存在しない。これではmojo.cv以前の問題である。

(mojo_env) $ mojo
Welcome to Mojo! 🔥

Expressions are delimited by a blank line.
Type `:quit` to exit the REPL and `:mojo help` for further assistance.

  1> mojo 
  2.  
[User] error: Expression [0]:1:1: use of unknown declaration 'mojo'
mojo
^~~~

(null)
  1> import mojo 
  2.  
[User] error: Expression [1]:1:8: unable to locate module 'mojo'
import mojo
       ^

(null)

調査を尽くした訳ではないが、import mojoなどという記述が一体どれだけあるのか、何とも訝しいものである。

現状、Mojo🔥にできないことはPythonに代替させるのが最も簡単である。従って、Mojo🔥で画像を扱うために、PythonPillowOpenCVを用いることとした。

環境について

環境はWSL上のUbuntuを使っている。Mojo🔥(Modular)の導入はPixiによって行った。

上にPython 3.13.0とあるが、これはUbuntuにインストールされているものではなく、Pixiでインストールした仮想環境である。その他、pillownumpyopencvは全てPixiによってインストールしたものである。

$ pixi add "python==3.13"
✔ Added python==3.13
$ pixi add pillow
✔ Added pillow >=11.2.1,<12
$ pixi add numpy
✔ Added numpy >=2.3.0,<3
$ pixi add opencv
 WARN Skipped running the post-link scripts because `run-post-link-scripts` = `false`
        - bin/.librsvg-pre-unlink.sh

To enable them, run:
        pixi config set --local run-post-link-scripts insecure

More info:
        https://pixi.sh/latest/reference/pixi_configuration/#run-post-link-scripts

✔ Added opencv >=4.11.0,<5

輪郭抽出

https://tanalib.com/sobel-numpy/

Sobel filterというものを使うことで、画像から輪郭を抽出できるらしい。

輪郭抽出例
左:元の画像
右:Sobel filter処理後

今回はこれを題材に、見よう見まねで画像処理を施してみる。

実験に用いた画像は、輪郭の分かりやすいものをと選んだ。上の例に見せたZennのアイコンと、次の二つを加えた三枚である。

https://pixabay.com/ja/photos/女性-大人-ベッド-ブラジャー-1868574/

https://pixabay.com/ja/photos/女の子-顔-目を閉じて-1098612/

Pillowの場合

Pythonを使う以上、Pythonで普通に実装するとどうなるのかを踏まえておく。

edge_detect.py
import sys
import time
from PIL import Image
import numpy as np

def main():
    if len(sys.argv) < 2:
        print("Usage: python edge_detect.py <image_path>")
        sys.exit(1)
    image_path = sys.argv[1]
    try:
        # グレースケールで画像を開く
        img = Image.open(image_path).convert("L")
        img_array = np.array(img)
        height, width = img_array.shape

        # Sobel filterカーネル
        kernel_x = np.array([
            [-1, -2, -1],
            [ 0,  0,  0],
            [ 1,  2,  1]
        ])
        kernel_y = np.array([
            [-1, 0, 1],
            [-2, 0, 2],
            [-1, 0, 1]
        ])

        # エッジ画像の初期化
        edge = np.zeros((height-2, width-2), dtype=np.uint8)

        # 計測開始
        start_time = time.monotonic()

        for i in range(1, height-1):
            for j in range(1, width-1):
                region = img_array[i-1:i+2, j-1:j+2]
                gx = np.sum(kernel_x * region)
                gy = np.sum(kernel_y * region)
                edge[i-1, j-1] = min(255, int((gx**2 + gy**2)**0.5))

        # 計測終了
        elapsed_time = time.monotonic() - start_time
        print(f"Edge detection completed in {elapsed_time} seconds.")

        # 画像として保存
        edge_image = Image.fromarray(edge)
        output_path = "py_edge_detected_" + image_path.split("/")[-1]
        edge_image.save(output_path)
        print(f"Edge-detected image saved as '{output_path}'.")
    except Exception as e:
        print(e)
        sys.exit(1)

if __name__ == "__main__":
    main()

これを真似てMojo🔥で記述したものが次となる。

edge_detect.mojo
import sys
import time
from python import Python

fn main() raises:
    var img_path = sys.argv()[1]
    try:
        # import PIL
        var Image = Python.import_module("PIL.Image")
        # import numpy
        var np = Python.import_module("numpy")

        # グレースケールで画像を開く
        var img = Image.open(img_path).convert("L")
        # numpy arrayに変換する
        var img_array = np.array(img)
        var img_shape = img_array.shape
        var height = img_shape[0]
        var width = img_shape[1]

        # Sobel filterカーネル
        # var matrix_x = Python.list(
        #     Python.list(-1, -2, -1),
        #     Python.list(0,   0,  0),
        #     Python.list(1,   2,  1)
        # )
        # var kernel_x = np.array(matrix_x)
        # var matrix_y = Python.list(
        #     Python.list(-1, 0, 1),
        #     Python.list(-2, 0, 2),
        #     Python.list(-1, 0, 1)
        # )
        # var kernel_y = np.array(matrix_y)

        var kernel_x = np.array(
            np.asmatrix(
                '''
                -1 -2 -1;
                0  0  0;
                1  2  1
                '''
            )
        )
        var kernel_y = np.array(
            np.asmatrix(
                '''
                -1 0 1;
                -2 0 2;
                -1 0 1
                '''
            )
        )

        # エッジ画像の初期化
        var edge = np.zeros(Python.tuple(height-2, width-2), dtype=np.uint8)

        # 計測開始
        var start_time = time.monotonic()

        for i in range(1, height-1):
            for j in range(1, width-1):
                # Apply the Sobel filter
                region = img_array[i-1:i+2, j-1:j+2]
                gx = np.sum(kernel_x * region)
                gy = np.sum(kernel_y * region)
                edge[i-1, j-1] = min(255, Int((gx**2 + gy**2)**0.5))

        # 計測終了
        var end_time = time.monotonic()
        var elapsed_time = end_time - start_time
        print(
            String("Edge detection completed in {} ns ({} s).")
            .format(elapsed_time, elapsed_time / 1_000_000_000)
        )

        # 配列を画像に変換
        var edge_image = Image.fromarray(edge)
        # 画像を保存
        var edge_img_path = String("mj_edge_detected_{}.jpg").format(img_path.split("/")[-1].split(".")[0])
        edge_image.save(edge_img_path)
        print(
            String("Edge-detected image saved as '{}'.")
            .format(edge_img_path)
        )
    except:
        sys.exit(1)

実装上気になったのは、コメントにしていた次の箇所。

var matrix_x = Python.list(
    Python.list(-1, -2, -1),
    Python.list(0,   0,  0),
    Python.list(1,   2,  1)
)
var kernel_x = np.array(matrix_x)

Pythonならばkernel_x = np.array([[-1, -2, -1], [ 0, 0, 0], [ 1, 2, 1]])でよいが、Mojo🔥ではエラーになった。

  3> print(np.array([[-1, -2, -1], [ 0,  0,  0], [ 1,  2,  1]])) 
  4.  
[User] error: Expression [2]:1:15: invalid call to '__call__': argument #1 cannot be converted from list literal to 'PythonObject'
print(np.array([[-1, -2, -1], [ 0,  0,  0], [ 1,  2,  1]]))
      ~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Python.listはこれを回避するための策であったが、np.asmatrixを使うことでもエラーは回避された。

結果
実行結果

実行結果としては確かに輪郭が得られているように見える。

比較

Pythonはそのまま実行する。

python実行
python3 edge_detect.py 画像パス

Mojo🔥は二つの方法を試す。一つはJITコンパイル。もう一つはコンパイル。従って、あらかじめコンパイルしておく。

mojoコンパイル
mojo build edge_detect.mojo

edge_detectという名前で実行ファイルが生成される。

mojo実行(jitコンパイル)
mojo edge_detect.mojo 画像パス
mojo実行(コンパイル)
./edge_detect 画像パス

結果を表に示す。単位は秒とし、有効数字は二桁として四捨五入している。
なお、面倒なので複数回の平均を取るなどはしておらず、データとしての信憑性は保証されない。

Python Mojo🔥(JIT) Mojo🔥(build)
zenn.jpg 1.02 3.34 3.15
girl-1098612_1920.jpg 15.46 54.26 52.24
woman-1868574_1920.jpg 15.71 53.64 55.87

これだけ見ると、3~3.5倍程度、Mojo🔥の方が遅いようである。

Rustの場合

比較にならないため分けたが、Rustでも似たようなプログラムを記述して速度を見た。

lib_sob.rs
use image::{GrayImage, Luma};
use std::time::Instant;

pub fn sobel_edge_detection(image: &GrayImage) -> GrayImage {
    let (width, height) = image.dimensions();
    let mut edge_image = GrayImage::new(width, height);

    let kernel_x: [[i32; 3]; 3] = [
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1],
    ];

    let kernel_y: [[i32; 3]; 3] = [
        [-1, -2, -1],
        [0, 0, 0],
        [1, 2, 1],
    ];

    let start = Instant::now();

    for y in 1..height-1 {
        for x in 1..width-1 {
            let mut gx = 0;
            let mut gy = 0;

            for ky in 0..3 {
                for kx in 0..3 {
                    let pixel = image.get_pixel(x + kx - 1, y + ky - 1).0[0] as i32;
                    gx += kernel_x[ky as usize][kx as usize] * pixel;
                    gy += kernel_y[ky as usize][kx as usize] * pixel;
                }
            }

            let magnitude = ((gx * gx + gy * gy) as f64).sqrt().min(255.0) as u8;
            edge_image.put_pixel(x, y, Luma([magnitude]));
        }
    }

    let duration = start.elapsed();
    println!("Sobel loop took: {:?}", duration);

    edge_image
}
main.rs
use std::env;
use std::path::Path;

#[path ="lib_sob.rs"]
mod lib_sob;
use lib_sob::sobel_edge_detection;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        eprintln!("Usage: {} <image_path>", args[0]);
        std::process::exit(1);
    }

    let image_path = &args[1];
    let img = image::open(image_path).expect("Failed to open image").into_luma8();
    let edge_image = sobel_edge_detection(&img);

    let output_path = format!("rs_edge_detected_{}", Path::new(image_path).file_name().unwrap().to_string_lossy());
    edge_image.save(&output_path).expect("Failed to save edge-detected image");
    println!("Edge-detected image saved as '{}'.", &output_path);
}

Rustの結果

単位は秒に合わせた。

Rust
zenn.jpg 0.041
girl-1098612_1920.jpg 0.69
woman-1868574_1920.jpg 0.70

1秒たりとも掛かっておらず、全く比にならない。

OpenCVの場合

こちらもPythonの場合を先に記述し、それを真似てMojo🔥を記述する。

なお、こちらでは複数の処理について時間を計測した。

cv2_sobel.py
import sys
import cv2
import time

def main():
    if len(sys.argv) < 2:
        print("Usage: python edge_detect.py <image_path>")
        sys.exit(1)
    path = sys.argv[1]

    try:
        start_time = time.time()
        image = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
        end_time = time.time()
        load_time = end_time - start_time
        cv2.imshow(f'Image loading time: {load_time:.6f} s', image)

        start_time = time.time()
        median_blurred = cv2.medianBlur(image, 5)
        end_time = time.time()
        blur_time = end_time - start_time
        cv2.imshow(f'Median Blurred time: {blur_time:.6f} s', median_blurred)

        start_time = time.time()
        sobel_x = cv2.Sobel(median_blurred, cv2.CV_64F, 1, 0, ksize=3)
        sobel_y = cv2.Sobel(median_blurred, cv2.CV_64F, 0, 1, ksize=3)
        sobel_combined_magnitude = cv2.magnitude(sobel_x, sobel_y)
        end_time = time.time()
        sobel_time = end_time - start_time
        cv2.imshow(f'Sobel Combined (Magnitude) time: {sobel_time:.6f} s', sobel_combined_magnitude)

        start_time = time.time()
        sobel_x_abs = cv2.convertScaleAbs(sobel_x)
        sobel_y_abs = cv2.convertScaleAbs(sobel_y)
        sobel_combined_abs = cv2.addWeighted(sobel_x_abs, 0.5, sobel_y_abs, 0.5, 0)
        end_time = time.time()
        sobel_abs_time = end_time - start_time
        cv2.imshow(f'Sobel Combined (Absolute) time: {sobel_abs_time:.6f} s', sobel_combined_abs)

        cv2.waitKey(0)
        cv2.destroyAllWindows()
    except Exception as e:
        print(e)
        sys.exit(1)

if __name__ == "__main__":
    main()

cv2_sobel.mojo
import sys
import time
from python import Python

fn main() raises:
    var path = sys.argv()[1]
    try:
        var cv2 = Python.import_module('cv2')

        var start_time = time.monotonic()
        var image = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
        var end_time = time.monotonic()
        var title1 = String('Image loading time: {} s').format((end_time - start_time) / 1_000_000_000)

        start_time = time.monotonic()
        var median_blurred = cv2.medianBlur(image, 5)
        end_time = time.monotonic()
        var title2 = String('Median Blurred time: {} s').format((end_time - start_time) / 1_000_000_000)

        start_time = time.monotonic()
        var sobel_x = cv2.Sobel(median_blurred, cv2.CV_64F, 1, 0, ksize=3)
        var sobel_y = cv2.Sobel(median_blurred, cv2.CV_64F, 0, 1, ksize=3)
        var sobel_combined_magnitude = cv2.magnitude(sobel_x, sobel_y)
        end_time = time.monotonic()
        var title3 = String('Sobel Combined (Magnitude) time: {} s').format((end_time - start_time) / 1_000_000_000)

        start_time = time.monotonic()
        var sobel_x_abs = cv2.convertScaleAbs(sobel_x)
        var sobel_y_abs = cv2.convertScaleAbs(sobel_y)
        var sobel_combined_abs = cv2.addWeighted(sobel_x_abs, 0.5, sobel_y_abs, 0.5, 0)
        end_time = time.monotonic()
        var title4 = String('Sobel Combined (Absolute) time: {} s').format((end_time - start_time) / 1_000_000_000)

        cv2.imshow(title1, image)
        cv2.imshow(title2, median_blurred)
        cv2.imshow(title3, sobel_combined_magnitude)
        cv2.imshow(title4, sobel_combined_abs)
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    except:
        sys.exit(1)

結果比較

同様に単位は秒としている。

Python 画像読み込み ノイズ除去 輪郭抽出
(できていない)
輪郭抽出
zenn.jpg 0.0076 0.0045 0.014 0.0027
girl-1098612_1920.jpg 0.014 0.011 0.086 0.018
woman-1868574_1920.jpg 0.018 0.023 0.076 0.014
Mojo🔥(JIT) 画像読み込み ノイズ除去 輪郭抽出
(できていない)
輪郭抽出
zenn.jpg 0.00088 0.0015 0.0070 0.0017
girl-1098612_1920.jpg 0.017 0.0067 0.040 0.0064
woman-1868574_1920.jpg 0.011 0.0075 0.040 0.0052
Mojo🔥(build) 画像読み込み ノイズ除去 輪郭抽出
(できていない)
輪郭抽出
zenn.jpg 0.00089 0.00075 0.0014 0.00029
girl-1098612_1920.jpg 0.033 0.0060 0.051 0.017
woman-1868574_1920.jpg 0.015 0.0072 0.038 0.0057
全結果スクリーンショット

各ウィンドウタイトルに計測された時間を表示している。

Python

python1

python2

python3

Mojo🔥(JIT)

mojo(jit)1

mojo(jit)2

mojo(jit)3

Mojo🔥(build)

mojo1

mojo2

mojo3

こちらはPillowとは打って変わって、殆どの場合Mojo🔥の方が速いという結果となった。特にzenn.jpgの結果を見ると、10倍前後速いと言えてしまう。

とはいえ、他二枚の画像の数値を比較すると概ね1~3倍の間となっており、読み込みに至ってはPythonの方が速い場合もあった。

C++の場合

RustにもOpenCVはあるようだが、得体が知れないため、Pythonと並び公式のものであるC++を使ってみる。

main.cpp
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui_c.h>
#include <iostream>
#include <sstream>
#include <iomanip>
#include <chrono>

int main(int argc, char** argv) {
    if (argc < 2) {
        std::cout << "Usage: ./opencv_sobel_cpp <image_path>" << std::endl;
        return -1;
    }

    std::string imagePath = argv[1];

    auto start = std::chrono::high_resolution_clock::now();
    cv::Mat image = cv::imread(imagePath, cv::IMREAD_GRAYSCALE);
    auto end = std::chrono::high_resolution_clock::now();
    double loadTime = std::chrono::duration<double>(end - start).count();

    if (image.empty()) {
        std::cout << "Could not open or find the image!" << std::endl;
        return -1;
    }

    std::ostringstream oss;
    oss << "Image loading time: " << std::fixed << std::setprecision(6) << loadTime << " s";
    std::string win1 = oss.str();
    cv::namedWindow(win1, CV_WINDOW_NORMAL | CV_WINDOW_FREERATIO | CV_GUI_NORMAL);
    cv::imshow(win1, image);

    start = std::chrono::high_resolution_clock::now();
    cv::Mat medianBlurred;
    cv::medianBlur(image, medianBlurred, 5);
    end = std::chrono::high_resolution_clock::now();
    double blurTime = std::chrono::duration<double>(end - start).count();

    oss.str(""); oss.clear();
    oss << "Median Blurred time: " << std::fixed << std::setprecision(6) << blurTime << " s";
    std::string win2 = oss.str();
    cv::namedWindow(win2, CV_WINDOW_NORMAL | CV_WINDOW_FREERATIO | CV_GUI_NORMAL);
    cv::imshow(win2, medianBlurred);

    start = std::chrono::high_resolution_clock::now();
    cv::Mat sobelX, sobelY, sobelCombinedMagnitude;
    cv::Sobel(medianBlurred, sobelX, CV_64F, 1, 0, 3);
    cv::Sobel(medianBlurred, sobelY, CV_64F, 0, 1, 3);
    cv::magnitude(sobelX, sobelY, sobelCombinedMagnitude);
    end = std::chrono::high_resolution_clock::now();
    double sobelTime = std::chrono::duration<double>(end - start).count();

    oss.str(""); oss.clear();
    oss << "Sobel Combined (Magnitude) time: " << std::fixed << std::setprecision(6) << sobelTime << " s";
    std::string win3 = oss.str();
    cv::namedWindow(win3, CV_WINDOW_NORMAL | CV_WINDOW_FREERATIO | CV_GUI_NORMAL);
    cv::imshow(win3, sobelCombinedMagnitude);

    start = std::chrono::high_resolution_clock::now();
    cv::Mat sobelXAbs, sobelYAbs, sobelCombinedAbs;
    cv::convertScaleAbs(sobelX, sobelXAbs);
    cv::convertScaleAbs(sobelY, sobelYAbs);
    cv::addWeighted(sobelXAbs, 0.5, sobelYAbs, 0.5, 0, sobelCombinedAbs);
    end = std::chrono::high_resolution_clock::now();
    double sobelAbsTime = std::chrono::duration<double>(end - start).count();

    oss.str(""); oss.clear();
    oss << "Sobel Combined (Absolute) time: " << std::fixed << std::setprecision(6) << sobelAbsTime << " s";
    std::string win4 = oss.str();
    cv::namedWindow(win4, CV_WINDOW_NORMAL | CV_WINDOW_FREERATIO | CV_GUI_NORMAL);
    cv::imshow(win4, sobelCombinedAbs);

    cv::waitKey(0);
    cv::destroyAllWindows();

    return 0;
}
C++ 画像読み込み ノイズ除去 輪郭抽出
(できていない)
輪郭抽出
zenn.jpg 0.0022 0.0010 0.0017 0.00037
girl-1098612_1920.jpg 0.0092 0.0047 0.037 0.0096
woman-1868574_1920.jpg 0.012 0.0055 0.044 0.0082

Mojo🔥と大差無く、寧ろ遅い場合もあるという結果となった。

C++1

C++2

C++3

どこか腑に落ちない感を覚えたのは、「Mojo🔥はPythonより速い」とされながら、「Mojo🔥より遅いPythonを頼っている」ためであろう。PythonCを頼っていることで知られるが、これはPythonの遅さをCの速さで補おうとするものであり、理に適っている。このように、Mojo🔥もPythonで何らかの弱点を補っていると考えると、その弱点とは「汎用性の低さ」であろうか。

Pillowは別として、OpenCVをこれだけ満足に扱えるならば、確かにこのような形態でも充分と感じる。寧ろ、歴史の長さから見ても普及率の高さから見ても、後進の言語がPythonに追随することは難しいだろう。その資産を我が物顔で扱えるのだから、この設計は賢しいという外ない。

とは言え、Pillowの例で見えたように、明らかな性能の劣化が起こる可能性もある。また、プログラムの記述もPythonとは変えなければならない場合もある。そうした場合、前例から解決策を探すことには期待できない。

総じて、Pythonの真似ごとをするだけで性能が向上するのは、一つの魅力に違いない。しかし、完全に一致するようなものではなく、Mojo🔥のために記述を変える必要もあるが、その情報源は乏しい。それだけの苦労を経て、どれだけの高速化が見込まれるだろうとの疑問は憑き纏う。

一方で、確かに高速になる可能性が示されたことも無視できない。今回は画像処理を例に取ったが、全く異なる場面に持ち込んだ時、Mojo🔥はどのように働くだろうか。

参考

https://docs.modular.com/mojo/manual/get-started#13-import-and-use-a-python-package

https://docs.modular.com/mojo/stdlib/time/time/monotonic

https://docs.modular.com/mojo/manual/python/python-from-mojo

https://docs.modular.com/mojo/stdlib/sys/arg/argv

https://www.codevace.com/py-opencv-sobel/

https://zenn.dev/turing_motors/articles/daf3e8829f0967

http://opencv.jp/opencv-2svn/cpp/user_interface.html

Discussion