MENU
  • ホーム
  • サービス
  • 記事一覧
  • 問い合わせ
プログラミングやサーバー設定など様々な内容を紹介しています。
HIROTRONの部屋
  • ホーム
  • サービス
  • 記事一覧
  • 問い合わせ
HIROTRONの部屋
  • ホーム
  • サービス
  • 記事一覧
  • 問い合わせ
  1. ホーム
  2. LINE_Messaging_API
  3. 第2話:画像に仕込む魔法!LINEイメージマップとスタンプの格闘記

第2話:画像に仕込む魔法!LINEイメージマップとスタンプの格闘記

2025 6/25
LINE_Messaging_API さどんでこプロジェクト
2025年6月22日2025年6月25日

私は、友達の紹介でさどんでこプロジェクトのIT担当として様々システム構築などを行っています。今回、スタンプラリー作成でかなり大変だったということから、開発秘話をブログにまとめてみました。興味ある方ぜひ御覧ください。

Sadondeko Project
佐渡を楽しむ デジタルスタンプラリー – さどんでこプロジェクト 佐渡の伝統文化「鬼太鼓」をテーマにしたデジタルスタンプラリーです!両津港をスタート地点に、佐渡金山や大野亀などの主要観光地を巡りながら、各地で鬼太鼓スタンプを集…

第1話は以下です、合わせてお読みください。

あわせて読みたい
第1話:WordPressじゃ物足りない!?LINE✕鬼太鼓スタンプラリー開発のはじまり 私は、友達の紹介でさどんでこプロジェクトのIT担当として様々システム構築などを行っています。今回、スタンプラリー作成でかなり大変だったということから、開発秘話…
目次

今回のテーマ

  • LINEの**イメージマップメッセージ(Imagemap Message)**とは?
  • スタンプカード上で「スタンプを押せるようにする」までの工夫
  • スタンプごとの領域を計算するロジック
  • Numpyを使って、未獲得スタンプをグレー化
  • 解像度調整:Imagemap用画像の即時生成
  • LINEイメージマップのキャッシュ対策:リプライトークンでURLを変化させる

LINEのImagemap Messageを使ってみた

LINEには「Imagemap Message」という、画像の一部をタップ可能にしてアクションを起こせる機能があります。

{
  "type": "imagemap",
  "baseUrl": "https://example.com/card",
  "altText": "スタンプカード",
  "baseSize": { "height": 1040, "width": 1040 },
  "actions": [
    {
      "type": "message",
      "text": "stamp_1",
      "area": { "x": 100, "y": 200, "width": 200, "height": 200 }
    }
  ]
}

画像上の任意の領域をアクション付きで送信できるのは素晴らしいのですが、ここに大きな落とし穴がありました。


正方形しかクリック領域にできない

Imagemapのarea指定は、長方形または正方形の領域しか指定できません。

鬼太鼓スタンプラリーでは、スタンプは円形であるため、画像サイズでAction領域を設定してしまうと、Action領域が被ってしまい、エラーが起こります。

画像サイズでAction領域を設定するとスタンプ同士でAction領域が被ってしまう

スタンプに収まる最大の正方形でAction領域を設定するとスタンプ同士で被らない


「円に内接する最大の正方形」を算出

ユーザーがスタンプをタップできるようにするため、各スタンプの中央座標と半径から、「タップ領域(正方形)」をPython側で算出しました。

これをもとに以下の関数を作成し、スタンプ画像の中心にぴったり重なるような「触れる」四角領域を作成できます。

def generate_imagemap_actions(request, course_id):
    scale = course.stamp_scale or 1.0
    positions = CourseStampPosition.objects.filter(course=course).select_related("spot")

    actions = []
    for pos in positions:
        spot = pos.spot
        actions.append({
            "type": "message",
            "text": spot.keyword,
            "area": {
                "x": int(pos.x + 6 * scale),
                "y": int(pos.y + 6 * scale),
                "width": int(28 * scale),
                "height": int(28 * scale),
            }
        })

    return JsonResponse(actions, safe=False)

他の関数の影響で画像サイズが40px ✕ 倍率(scale)で設定されていたため、

Actionsは

“x” = pos.x + 6 * 倍率
“y” = pos.y + 6 * 倍率
“width” = 28 * 倍率
“height” = 28 * 倍率

となりました。計算途中でルートは1.41で近似し、整数部分だけにしました。


未獲得スタンプはNumpyでグレー化

「まだ獲得していないスタンプは、グレーにしたい」

もともと、1ピクセルずつ計算させる予定でしたが、サーバーのリソースを思ったより多く食うことがわかりました。

そのため、この要望を高速に処理するために、Numpyを用いて画像を一括グレースケール化しました。

# 指定されたスタンプIDと獲得状態でスタンプ画像を返す 未獲得の場合は灰色の画像を返す
def render_stamp_image(request, reply_token, stamp_id, acquired_id):

    image_path = os.path.join(settings.MEDIA_ROOT, stamp.image.name)
    image = Image.open(image_path).convert("RGBA")

    if int(acquired_id) == 0:
        # NumPyで処理
        gray = image.convert("L")
        gray_array = np.array(gray)
        alpha_mask = gray_array < 50  # しきい値以下のみ対象

        # 新しいRGBA画像(すべて透明)
        result_array = np.zeros((image.height, image.width, 4), dtype=np.uint8)
        result_array[alpha_mask] = [200, 200, 200, 255]  # 灰色で表示

        result_image = Image.fromarray(result_array, mode="RGBA")
    else:
        result_image = image

    buffer = BytesIO()
    result_image.save(buffer, format="PNG")
    buffer.seek(0)
    return FileResponse(buffer, content_type="image/png")

スタンプをグレー化


解像度調整:Imagemap用画像の即時生成

イメージマップメッセージで使用する画像は、以下の要件を満たす必要があります。

  • 画像フォーマット: JPEGまたはPNG
  • 画像の幅:240px、300px、460px、700px、および1040px
  • 最大ファイルサイズ:10MB

要するにBaseurlがhttps://example.com/image/の場合

例:https://example.com/image/460

のように参照されるということです。

なので、URLの中にSizeを入れ、

path("course/<int:course_id>/<int:user_id>/<int:size>", views.render_user_stamp_card, name="render_user_stamp_card"),

スタンプカードにスタンプを合成+即時生成・リサイズするようにしました。

# 指定されたコースとユーザーのスタンプカードを生成
def render_user_stamp_card(request, course_id, user_id, size):

    course = get_object_or_404(StampCourse, id=course_id)
    user = get_object_or_404(User, id=user_id)

    scale = course.stamp_scale or 1.0

    # 獲得済みスタンプのID(ユーザーが獲得済み、かつこのコースに含まれる)
    acquired_spots = UserStamp.objects.filter(user=user).values_list("stamp_id", flat=True)
    valid_spot_ids = course.spots.filter(id__in=acquired_spots).values_list("id", flat=True)

    # 元画像を読み込み
    base_path = os.path.join(settings.MEDIA_ROOT, course.image.name)
    base_image = Image.open(base_path).convert("RGBA")
    orig_width, orig_height = base_image.size

    canvas = base_image.copy()

    # スタンプを合成
    positions = CourseStampPosition.objects.filter(course=course, spot_id__in=valid_spot_ids).select_related("spot")
    for pos in positions:
        stamp_path = os.path.join(settings.MEDIA_ROOT, pos.spot.image.name)
        stamp = Image.open(stamp_path).convert("RGBA")
        size_px = int(40 * scale)
        stamp = stamp.resize((size_px, size_px), Image.LANCZOS)
        canvas.paste(stamp, (pos.x, pos.y), stamp)

    # 指定サイズにリサイズ(横幅基準)
    if size != orig_width:
        ratio = size / orig_width
        new_height = int(orig_height * ratio)
        canvas = canvas.resize((size, new_height), Image.LANCZOS)

    # 出力
    buffer = BytesIO()
    canvas.save(buffer, format="PNG")
    buffer.seek(0)
    return FileResponse(buffer, content_type="image/png")

LINEのキャッシュ対策:リプライトークンでURLを変化させる

LINEイメージマップは画像をキャッシュする仕様があるため、一度送信した画像が更新されても表示が変わらない問題が発生しました。

これを解決するため、毎回画像のURLにリプライトークン(replyToken)を挟むことで、LINE側から見たURLを都度変化させてキャッシュを回避しました。

path("course/<str:reply_token>/<int:course_id>/<int:user_id>/<int:size>", views.render_user_stamp_card, name="render_user_stamp_card"),


バックエンドでは、トークンを無視して同一画像を返す処理を用意。これにより、常に最新のスタンプカードが表示されるようになりました。

完成形:スマホでスタンプを「押す」体験

この仕組みを組み合わせたことで、以下の体験が可能に:

  1. ユーザーがLINEでスタンプカードを開く
  2. スタンプ部分をタップ
  3. LINE Botがタップ情報を受け取り、獲得処理を実行
  4. 最新のスタンプカード画像を生成 → 返信

画像合成 × 座標計算 × LINE API × キャッシュ対策が絶妙に連携した瞬間でした。

次回予告:スタンプの位置情報設定 Google Maps × Django管理画面の裏側

次回は、スタンプの位置情報を地図上で登録・編集できる管理画面の制作秘話をお届けします。

  • Django + Google Mapsでスポットを登録する仕組み
  • スポットの重複・近接の確認
  • 管理者用UIのこだわり

どんな苦労があったのか…ぜひお楽しみに!

あわせて読みたい
第3話:地図の中にスタンプ!?Google Maps × Django管理画面の作成 私は、友達の紹介でさどんでこプロジェクトのIT担当として様々システム構築などを行っています。今回、スタンプラリー作成でかなり大変だったということから、開発秘話…
Sadondeko Project
佐渡を楽しむ デジタルスタンプラリー – さどんでこプロジェクト 佐渡の伝統文化「鬼太鼓」をテーマにしたデジタルスタンプラリーです!両津港をスタート地点に、佐渡金山や大野亀などの主要観光地を巡りながら、各地で鬼太鼓スタンプを集…
LINE友達登録でスタンプラリー開始!
LINE_Messaging_API さどんでこプロジェクト
LINE LINE Messaging API イメージマップ さどんでこプロジェクト
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
  • 第1話:WordPressじゃ物足りない!?LINE✕鬼太鼓スタンプラリー開発のはじまり
  • 第3話:地図の中にスタンプ!?Google Maps × Django管理画面の作成

この記事を書いた人

hiroto121022のアバター hiroto121022

医学生でありながら、さどんでこプロジェクトのITリーダーを務めるフルスタック開発者。LINE BotやGPS連携スタンプラリー、NFT販売サイトなどをすべて独学で開発。使用言語はPython、Typescript、Javascript。Django、Next.js、Reactなどのフレームワークを駆使し、サーバー構築・デプロイ・SSL対応まで一貫して担当。AIによるアートのアニメーション化にも取り組み、伝統文化とテクノロジーを融合させた新しい地域体験の創出に挑戦している。

関連記事

  • Django ✕ LINE Messaging APIでローディングアニメーションを出す方法
    2025年6月25日
  • 第4話:位置情報でスタンプ獲得!LIFFとLINE Botの連携の裏側
    2025年6月22日
  • 第3話:地図の中にスタンプ!?Google Maps × Django管理画面の作成
    2025年6月22日
  • 第1話:WordPressじゃ物足りない!?LINE✕鬼太鼓スタンプラリー開発のはじまり
    2025年6月22日

コメント

コメント一覧 (3件)

  • LINE Messaging API × 鬼太鼓スタンプラリー誕生秘話 ① より:
    2025年6月22日 8:15 PM

    […] 第2話:画像に仕込む魔法!LINEイメージマップとスタンプの格闘記 […]

    返信
  • LINE API × 鬼太鼓スタンプラリー誕生秘話③ 座標管理とDjango管理画面 - HIROTRONの部屋 より:
    2025年6月22日 8:53 PM

    […] あわせて読みたい 第2話:画像に仕込む魔法!LINEイメージマップとスタンプの格闘記 […]

    返信
  • LINE API × 鬼太鼓スタンプラリー誕生秘話④ LIFFとGPSでスタンプを獲得する仕組み - HIROTRONの部屋 より:
    2025年6月25日 11:42 PM

    […] あわせて読みたい 第2話:画像に仕込む魔法!LINEイメージマップとスタンプの格闘記 […]

    返信

コメントする コメントをキャンセル

hiroto121022
医学生でありながら、さどんでこプロジェクトのITリーダーを務めるフルスタック開発者。LINE BotやGPS連携スタンプラリー、NFT販売サイトなどをすべて独学で開発。使用言語はPython、Typescript、Javascript。Django、Next.js、Reactなどのフレームワークを駆使し、サーバー構築・デプロイ・SSL対応まで一貫して担当。
GitHub
新着記事
  • Django ✕ LINE Messaging APIでローディングアニメーションを出す方法
  • 第4話:位置情報でスタンプ獲得!LIFFとLINE Botの連携の裏側
  • 第3話:地図の中にスタンプ!?Google Maps × Django管理画面の作成
  • 第2話:画像に仕込む魔法!LINEイメージマップとスタンプの格闘記
  • 第1話:WordPressじゃ物足りない!?LINE✕鬼太鼓スタンプラリー開発のはじまり

© HIROTRONの部屋.

目次