月: 2023年6月

  • 「時をかける少女 (アニメ映画)」の版間の差分

    USAでの公開日が今日の5:20に変更されている。正しい情報に訂正されたなら良いけど、エビデンス的な物は確認できなかった。過去の映画の公開日なんかマスターデータやん。何で変更したのか?

    データはすべて生モノですか?真実はいつもひとーつ!

    https://ja.wikipedia.org/w/index.php?title=時をかける少女_(アニメ映画)&action=history

    The Girl Who Leapt Through Time (2006 film)
    https://en.wikipedia.org/wiki/The_Girl_Who_Leapt_Through_Time_(2006_film)

    英語版Wikipediaは、「Release date July 15, 2006」で日本での公開日のみ。

  • 扇風機

    異音がするようになったので買い替えた。

    Amazonベーシック リビング扇風機 DCモーター 9枚羽根 ソフト気流 入切タイマー リモコン付 首振り 風量調節5段階 ホワイト
    https://www.amazon.co.jp/gp/product/B09QSXQ94X/

    また中華製。15時間ぐらい連続運転すると勝手に停止するし、リモコン2回押さないと反応しないし、褒められたものではないけど安い。5738円でポチった。

    梅雨いややー夏はよー

  • サルト・サーキット徹底解剖

    🏎 🏎 🏎 🏎 🏎 🏎 🏎 🏎 🏎 🏎 🏎 🏎 🏎 🏎 🏎 🏎 がんばれー

    https://toyotagazooracing.com/archive/ms/jp/wec/special/circuit-de-la-sarthe-01.html

  • リマインダ

    不燃ごみ捨てろよー

  • ワードクラウドの定期更新 →

    1つできるとN個には簡単にスケールできる件。二番煎じ感あるけど再び。

    備忘:メモ書き___φ(. . )

    Janome
    https://github.com/mocobeta/janome

    word_cloud
    https://github.com/amueller/word_cloud

    Janome

    Janomeの形態素解析ではMeCabの辞書を使用していてパッケージのインストール時に辞書をビルドしている(setup.py、ipadic/build.sh、ipadic/build.py)。辞書に載っているか総当たりでチェックしている以下の処理がメイン処理かと思われる。

    Tokenizerクラス

    def __tokenize_partial(self, text, wakati, baseform_unk, dotfile):
        if self.wakati and not wakati:
            raise WakatiModeOnlyException
    
        chunk_size = min(len(text), Tokenizer.MAX_CHUNK_SIZE)
        lattice = Lattice(chunk_size, self.sys_dic)
        pos = 0
        while not self.__should_split(text, pos):
            encoded_partial_text = text[pos:pos + min(50, chunk_size - pos)].encode('utf-8')
            # user dictionary
            if self.user_dic:
                entries = self.user_dic.lookup(encoded_partial_text)
                for e in entries:
                    lattice.add(SurfaceNode(e, NodeType.USER_DICT))
                matched = len(entries) > 0
    
            # system dictionary
            entries = self.sys_dic.lookup(encoded_partial_text, self.matcher)
            for e in entries:
                lattice.add(SurfaceNode(e, NodeType.SYS_DICT))
            matched = len(entries) > 0
    
            # unknown
            cates = self.sys_dic.get_char_categories(text[pos])
            if cates:
                for cate in cates:
                    if matched and not self.sys_dic.unknown_invoked_always(cate):
                        continue
                    # unknown word length
                    length = self.sys_dic.unknown_length(cate) \
                        if not self.sys_dic.unknown_grouping(cate) else self.max_unknown_length
                    assert length >= 0
                    # buffer for unknown word
                    buf = text[pos]
                    for p in range(pos + 1, min(chunk_size, pos + length + 1)):
                        _cates = self.sys_dic.get_char_categories(text[p])
                        if cate in _cates or any(cate in _compat_cates for _compat_cates in _cates.values()):
                            buf += text[p]
                        else:
                            break
                    unknown_entries = self.sys_dic.unknowns.get(cate)
                    assert unknown_entries
                    for entry in unknown_entries:
                        left_id, right_id, cost, part_of_speech = entry
                        base_form = buf if baseform_unk else '*'
                        dummy_dict_entry = (buf, left_id, right_id, cost, part_of_speech, '*', '*', base_form, '*', '*')
                        lattice.add(Node(dummy_dict_entry, NodeType.UNKNOWN))
    
            pos += lattice.forward()
        lattice.end()
        min_cost_path = lattice.backward()
        assert isinstance(min_cost_path[0], BOS)
        assert isinstance(min_cost_path[-1], EOS)
        if wakati:
            tokens = [node.surface for node in min_cost_path[1:-1]]
        else:
            tokens = []
            for node in min_cost_path[1:-1]:
                if type(node) == SurfaceNode and node.node_type == NodeType.SYS_DICT:
                    tokens.append(Token(node, self.sys_dic.lookup_extra(node.num)))
                elif type(node) == SurfaceNode and node.node_type == NodeType.USER_DICT:
                    tokens.append(Token(node, self.user_dic.lookup_extra(node.num)))
                else:
                    tokens.append(Token(node))
        if dotfile:
            lattice.generate_dotfile(filename=dotfile)
        return (tokens, pos)

    ユーザー辞書にありますか?システム辞書にありますか?unknownですか?

    形態素解析ってこういうことですね。

    word_cloud

    ワードクラウドは、パラメタお化けだけど柔軟なメソッドでコマンドのラッパーも用意されている。

    classwordcloud.WordCloud(font_path=None, width=400, height=200, margin=2, ranks_only=None, prefer_horizontal=0.9, mask=None, scale=1, color_func=None, max_words=200, min_font_size=4, stopwords=None, random_state=None, background_color='black', max_font_size=None, font_step=1, mode='RGB', relative_scaling='auto', regexp=None, collocations=True, colormap=None, normalize_plurals=True, contour_width=0, contour_color='black', repeat=False, include_numbers=False, min_word_length=0, collocation_threshold=30)

    中身は画像処理で、ワードクラウドの文字列をメモリ上に描画して、矩形の領域を取得して、ベースとなる画像に上書きしている。上書きするときに他の配置済みの文字列と重ならない場所を全画素、総当たりで判定して配置位置を決定している。

    generate_from_frequencies関数

    # start drawing grey image
    for word, freq in frequencies:
        if freq == 0:
            continue
        # select the font size
        rs = self.relative_scaling
        if rs != 0:
            font_size = int(round((rs * (freq / float(last_freq))
                                    + (1 - rs)) * font_size))
        if random_state.random() < self.prefer_horizontal:
            orientation = None
        else:
            orientation = Image.ROTATE_90
        tried_other_orientation = False
        while True:
            # try to find a position
            font = ImageFont.truetype(self.font_path, font_size)
            # transpose font optionally
            transposed_font = ImageFont.TransposedFont(
                font, orientation=orientation)
            # get size of resulting text
            box_size = draw.textbbox((0, 0), word, font=transposed_font, anchor="lt")
            # find possible places using integral image:
            result = occupancy.sample_position(box_size[3] + self.margin,
                                                box_size[2] + self.margin,
                                                random_state)
    ・・・省略・・・

    occupancy.sample_position()から呼び出されるquery_integral_image()が、「query_integral_image.pyx」に書かれていて「.pyx」の拡張子となっている。調べてみるとCythonのコードでCのソースを生成しているとのこと。

    query_integral_image.pyx:35行
    query_integral_image.c:21,510行

    巨大な.cのコードは、#ifdefだらけで人間が読むコードではなくなっている。

    「query_integral_image.c」は、setup.pyに拡張モジュールとして書かれていてパッケージのインストール時にコンパイルされてlib配下にsoファイルが生成されていた。(query_integral_image.cpython-311-darwin.so)

    ext_modules=[Extension("wordcloud.query_integral_image",
                           ["wordcloud/query_integral_image.c"])],

    以下が、「query_integral_image.pyx:35行 」のソース

    # cython: language_level=3
    # cython: boundscheck=False
    # cython: wraparound=False
    import array
    import numpy as np
    
    
    def query_integral_image(unsigned int[:,:] integral_image, int size_x, int
                             size_y, random_state):
        cdef int x = integral_image.shape[0]
        cdef int y = integral_image.shape[1]
        cdef int area, i, j
        cdef int hits = 0
    
        # count how many possible locations
        for i in xrange(x - size_x):
            for j in xrange(y - size_y):
                area = integral_image[i, j] + integral_image[i + size_x, j + size_y]
                area -= integral_image[i + size_x, j] + integral_image[i, j + size_y]
                if not area:
                    hits += 1
        if not hits:
            # no room left
            return None
        # pick a location at random
        cdef int goal = random_state.randint(0, hits)
        hits = 0
        for i in xrange(x - size_x):
            for j in xrange(y - size_y):
                area = integral_image[i, j] + integral_image[i + size_x, j + size_y]
                area -= integral_image[i + size_x, j] + integral_image[i, j + size_y]
                if not area:
                    hits += 1
                    if hits == goal:
                        return i, j
    

    画像全体をピクセル単位で走査して、そこに文字列を置けるか判定している。

    area = integral_image[i, j] + integral_image[i + size_x, j + size_y]
    area -= integral_image[i + size_x, j] + integral_image[i, j + size_y]

    ただ、性能的な理由でsoファイルに切り出していると思われるけれど、なぜか全体を2回走査している。何かそれ相応の理由があるのか?

    ライブラリの中身がざっくり分かったところでAPI Referenceを眺めていたら以下のメソッドを発見。

    to_svg([embed_font, optimize_embedded_font, …])

    to_svg()のソースはテキストの書き出し処理でダラダラ長いだけの面白くないソースなので割愛。

    PNGファイルをSVGファイルに差し替えて一旦終わり。SVGファイル公開。

    フォント周り

    ワードクラウドのフォントを変更した場合、PNGファイルには反映されるけれどSVGファイルには反映されない現象が発生した。

    SVGファイルのフォントはローカル環境にインストールされているフォントが使われて指定したフォントがないと代替のフォントで表示されるとのこと。

    すべての環境で同じフォントを使用させたい場合、Webフォントを使用する必要があるが、それだとネットに繋がっていない場合や、フォントサーバが落ちている場合に現象が再現すると思われる。

    さらに調べるとWebフォントをbase64にエンコードしてSVGファイルに添付している人がいた。フォントファイルを丸ごとエンコードするとファイルサイズが巨大になるので使用している文字だけ切り出してサブセットのフォントを生成する必要があるとのこと。

    そしてサブセットのフォントを生成してSVGファイルに添付する場合、フォントの改変や再頒布が可能なライセンスが必要でありフォントのライセンスを調査した。

    • SIL Open Font License
    • Apache License 2.0
    • M+ FONT LICENSE

    ライセンスや使用条件を確認しながらフリーフォントをダウンロードした。フリーフォントを公開している人に感謝です。ありがとうございます。

    あとは修正作業。SVGファイルで使用している文字をpyftsubsetコマンドでフォントから切り出してきて、base64コマンドでエンコードして、SVGファイルを修正して、raspiにデプロイして、macOSとLinuxでbase64コマンドのオプションが異なるみたいなことにハマりつつ、SVGファイルにフリーフォントを反映できるようになった。最後にフォントをランダムに切り替える処理を追加して、バッチとして動くことを確認して、ミッションコンプリート!

    1つできるまでが長い。0から1が長い。

    正常ルートだけのやりたいことしか書いてないダメダメソースだけど40行から120行に膨らんでいる。

    # word_cloudパッケージを6/1にインストールしていたので10日間ぐらいの作業メモ。読み返すと修正したくなる。書き足したくなる。推敲したくなる。けど、書き殴りのメモ書きが正解やんなあ?