乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

python

今回はDuckDBを用いた
簡単SQLデータ分析についてです

解説動画はこちら



DuckDB

データ分析したいけどSQLならわかるんだけど
Python良く分からないなー

そんな時はDuckDB使えばいいじゃない!!

ということで
DuckDBのご紹介です。

Python上でSQLでデータ取得できるライブラリで
DataFrameをそのままSQLテーブルとして扱えます。

pandasの複雑なメソッドを覚えなくても
SQL操作で任意のデータが抽出できるものです。
pandasも加えると更に深掘りも出来てお得です。


早速使い方を見ていきましょう。


インストール

Google Colabでは
インストール済みなので不要です。

ローカルなどでインストールしていない人は
下記コマンドを実行しておいてください。
pip install duckdb


ライブラリの呼び出し

これだけです。
import duckdb


操作の基本
# 実行結果を表示
duckdb.sql("SQL文").show()

# 実行結果を変数に格納
データフレーム変数 = duckdb.sql("SQL文").df()

データフレームを作ってSQL文を投げてみます
import pandas as pd

df = pd.DataFrame({
    "a":[1,2,3],
    "b":[10,20,30]
})

sql = "SELECT * FROM df WHERE a > 1"

result_df = duckdb.sql(sql).df()
result_df

ab
0220
1330

DuckDBではデータフレームや
ファイルに直接SQL文のクエリを実行し
データ抽出が行えます。


SQL分をファイルから読み込みする
sql = open("ファイルパス").read()
df = duckdb.sql(sql).df()


ファイルを直接クエリする

sql = open("ファイルパス").read()
df = duckdb.sql(sql).df()
Google Colabなどでもファイルを置いておけば参照できます。



サンプルデータで簡易分析

sklearnサンプルデータを用いて
DuckDBでSQL分析してみましょう。

今回用いるデータは
「California Housing Dataset」

カリフォルニアの
地域ごとの住宅価格などのデータです。
下記コードで読み込みできます。
import duckdb
import pandas as pd
from sklearn.datasets import fetch_california_housing

# データロード
data = fetch_california_housing(as_frame=True)

df = data.frame

# DuckDBに接続
con = duckdb.connect()

# DataFrameを登録
con.register("housing", df)

データの準備が整ったら
DuckDBでSQLを実行してみましょう。

1.平均住宅価格
sql = """
SELECT
    AVG(MedHouseVal) as avg_price
FROM housing
"""

print(con.sql(sql).df())

2.住宅価格ランキング
sql = """
SELECT
    MedHouseVal
FROM housing
GROUP BY MedHouseVal
ORDER BY MedHouseVal DESC
LIMIT 10
"""

print(con.sql(sql).df())

3.地域ごとの住宅価格
sql = """
SELECT
    ROUND(Latitude,1) as lat,
    ROUND(Longitude,1) as lon,
    AVG(MedHouseVal) as avg_price,
    COUNT(*) as houses
FROM housing
GROUP BY lat, lon
ORDER BY avg_price DESC, houses DESC
LIMIT 15
"""

print(con.sql(sql).df())

4.収入と住宅価格
sql = """
SELECT
    CASE
        WHEN MedInc < 2 THEN 'low'
        WHEN MedInc < 5 THEN 'middle'
        ELSE 'high'
    END as income_level,
    AVG(MedHouseVal) as avg_price,
    COUNT(*) as cnt
FROM housing
GROUP BY income_level
ORDER BY avg_price DESC
"""

print(con.sql(sql).df())

5.家の広さと価格
sql = """
SELECT
    ROUND(AveRooms,0) as rooms,
    AVG(MedHouseVal) as price,
    COUNT(*) as n
FROM housing
GROUP BY rooms
HAVING n > 50
ORDER BY rooms
"""

print(con.sql(sql).df())


応用編

海沿い住宅は高いのかどうか

カリフォルニアは 西側が海なので
経度(Longitude) で判定


sql = """
SELECT
    CASE
        WHEN Longitude < -121 THEN 'coastal'
        ELSE 'inland'
    END AS location_type,
    AVG(MedHouseVal) AS avg_house_price,
    COUNT(*) AS houses
FROM housing
GROUP BY location_type
"""

result = con.sql(sql).df()

print("海沿い住宅(coastal) vs 内陸住宅(inland)")
print(result)

結果はコードを試すか
動画の方をご覧ください





まとめ


DuckDBを用いるとSQLを使ったデータ分析が
簡単に行えます。

Pythonのコードに慣れていなくても
直接ファイルにSQLを実行して分析結果を得られます。

複雑な集計もSQLを書ければ出来ちゃうので
データアナリストでなくても簡単に分析できちゃいます。

手元にデータが有るけどデータベース作れない
SQLは分かるけどpython分からない人向け
超お手軽ライブラリなので、おすすめです

ぜひ試してみてください
それでは。





 

今回は
FastAPI × htmx で作るプロトタイプを爆速実装する方法
についてです。


解説動画はこちら




 FastAPI × htmx

昨今のPython界隈は streamlit というライブラリのおかげで
フロントエンドもまとめて作ることができる様になりました。

そのおかげで、似たような見た目ばかりになる
streamlit臭のするデザインのモックばかり
という哀しい現象が起きてきています。

なので、こんな状況を打破すべく
 - デザイン部分は自由に作り込める
 - フロントエンド(React/Vue)の学習コストが高すぎて、モック作りが進まない
 - FastAPI(堅牢なバックエンド) + htmx(HTML だけで Ajax)というシンプル構成

という FastAPI + htmx の組み合わせの方法をお伝えします。


FastAPI とは何か

爆速で動く、現代的な『注文受け付け窓口』
Python言語で作られたWEBフレームワークです。

プログラムの実行速度が非常に速く
開発者がコードを書くスピードも速くなるように設計されており

仕様書(APIドキュメント)を自動で作成
型(データ)のチェックなども行ってくれます。


htmx とは何か

HTMLを『魔法の言葉』に変える道具
通常、Webページの一部を書き換えるには
JavaScriptをたくさん書く必要があります。

htmxだと「HTMLを数文字書き足すだけ」で実現でき
JavaScriptの部分をHTMLの属性だけで指示できるます。

学習コストが劇的に低い(HTMLが分かるなら)
ので初学者でも取り組みが楽です。


なので
この組み合わせが事業モックに最適です。

フロントとバックエンド、2つの学習コストが低い
シンプルでサクサク動くし、修正が簡単

モックとして作った後も
そのまま本番サービス転用も可能です。



環境構築と基本設定

Python側のインストール
pip install fastapi uvicorn jinja2 python-multipart


FASTAPIの実行方法
main.py のあるディレクトリで
uvicorn main:app --reload

WEBブラウザー側で http://127.0.0.1:8000/
にアクセスするとFASTAPIの画面が見れます。



デモプロジェクトの概要


デモプロジェクトのディレクトリ構成

├── main.py
└── templates/
    ├── index.html        (ベースレイアウト)
    ├── customer_list.html (顧客一覧)
    ├── customer_detail.html (顧客詳細)
    ├── stock_list.html    (在庫一覧)
    ├── stock_detail.html  (在庫詳細)
    └── chat.html          (AIチャット)


こんな内容のものを作ることができます。
スクリーンショット 2026-02-21 17.28.21
スクリーンショット 2026-02-21 17.28.28
スクリーンショット 2026-02-21 17.28.34




main.py のコード

FAST API のコードはこんな感じになっています。
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from datetime import datetime

app = FastAPI()
templates = Jinja2Templates(directory="templates")

# --- ダミーデータ ---
CUSTOMERS = {
    "C001": {"id": "C001", "name": "山田 太郎", "birth": "1985/05/20", "gender": "男性", "address": "東京都渋谷区...", "rank": "S", "items": [{"name": "ルンバ", "qty": 1, "price": 50000}]},
    "C002": {"id": "C002", "name": "佐藤 花子", "birth": "1992/11/02", "gender": "女性", "address": "大阪府大阪市...", "rank": "A", "items": [{"name": "化粧水", "qty": 2, "price": 4000}]},
    # ...他3件(実装上は同様に定義)
}

STOCKS = {
    "S001": {"id": "S001", "name": "ロキソニンS", "category": "解熱鎮痛剤", "count": 150, "desc": "第1類医薬品。速効性に優れています。"},
    "S002": {"id": "S002", "name": "アレグラFX", "category": "鼻炎薬", "count": 45, "desc": "眠くなりにくいアレルギー専用鼻炎薬。"},
}

def calc_age(birth_str):
    birth_date = datetime.strptime(birth_str, "%Y/%m/%d")
    today = datetime.today()
    return today.year - birth_date.year - ((today.month, today.day) < (birth_date.month, birth_date.day))

# --- Routes ---
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

# 1. 顧客管理
@app.get("/customers", response_class=HTMLResponse)
async def list_customers(request: Request):
    data = []
    for c in CUSTOMERS.values():
        data.append({**c, "age": calc_age(c['birth']), "birth_month": c['birth'][:7]})
    return templates.TemplateResponse("customer_list.html", {"request": request, "customers": data})

@app.get("/customers/{cid}", response_class=HTMLResponse)
async def detail_customer(request: Request, cid: str):
    c = CUSTOMERS.get(cid)
    return templates.TemplateResponse("customer_detail.html", {"request": request, "customer": c, "age": calc_age(c['birth'])})

# 2. 在庫チェック
@app.get("/stocks", response_class=HTMLResponse)
async def list_stocks(request: Request):
    return templates.TemplateResponse("stock_list.html", {"request": request, "stocks": STOCKS.values()})

@app.get("/stocks/{sid}", response_class=HTMLResponse)
async def detail_stock(request: Request, sid: str):
    return templates.TemplateResponse("stock_detail.html", {"request": request, "stock": STOCKS.get(sid)})

# 3. AIチャット
@app.get("/chat", response_class=HTMLResponse)
async def chat_page(request: Request):
    return templates.TemplateResponse("chat.html", {"request": request})

@app.post("/chat/send", response_class=HTMLResponse)
async def chat_send(request: Request, message: str = Form(...)):
    # 簡易的なAIレスポンスのシミュレーション
    ai_response = f"「{message}」について承知いたしました。AIとしての回答をここに表示します。"
    return f"""ここは省略しています"""

index.htmlなどは
AIエージェントを使って作ってみてください





まとめ


FAST API + htmx を用いると爆速でモック開発ができる
こちらは必要最低限の知識を学ぶだけで済む
デザイン部分は自由に作り込めるので好きなCSSを勉強しよう

足りない部分はAIエージェント使ってください
こちらは作りたいものを指示するだけ
本番作りもAIエージェントで爆速開発できます

PS
最近はこれでプロトタイプ開発していたりします。
AIエージェントも使いこなせると
爆速で開発できる様にできますよ

それでは。

 

今回は地震データの可視化についてです
最近地震が多いので、plotlyで可視化してみました。


解説動画はこちら








データの入手先


気象庁の地震データベース

コードを動かしてみたい方は
CSVがダウンロードできるようなので
手元にダウンロードしてみてください。



データの読み込み

こちらのコードはGoogle Colabで動くようになっています。
動かしたい場合は Colabの画面左メニューから
フォルダマークをクリックすると
ファイル置き場が見えるので、そこにCSVファイルを
ドラッグなどで配置します。

ファイルを読み込みするには下記のコードです。

# 必要なライブラリのインポート
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import numpy as np
import warnings
warnings.filterwarnings('ignore')

file_path = "/content/地震リスト.csv"
df = pd.read_csv(file_path)
df.head()

df変数にデータが読み込まれると思います。



データの整形


CSVそのままのデータでは
うまく可視化が行えないため
可視化用にデータを加工する必要があります。

下記のコードを実行すると
可視化用のカラムなどが追加されます。


# データの前処理
s_order = ['震度1', '震度2', '震度3', '震度4', '震度5弱', '震度5強', '震度6弱', '震度6強', '震度7']
target = "トカラ列島近海"

def preprocess_data(df):
    # 緯度・経度の変換(度分秒から度へ)
    def convert_coordinate(coord_str):
        # 例: "29°28.2′N" -> 29.47
        if '°' in coord_str and '′' in coord_str:
            parts = coord_str.replace('N', '').replace('E', '').replace('S', '').replace('W', '')
            degree_part = parts.split('°')[0]
            minute_part = parts.split('°')[1].replace('′', '')
            return float(degree_part) + float(minute_part) / 60
        return float(coord_str)
    
    df['緯度_数値'] = df['緯度'].apply(convert_coordinate)
    df['経度_数値'] = df['経度'].apply(convert_coordinate)
    
    # 深さの数値化
    df['深さ_数値'] = df['深さ'].str.replace(' km', '').astype(float)
    
    # 震度の数値化
    s_mapping = {
        '震度1': 1, '震度2': 2, '震度3': 3, '震度4': 4, '震度5弱': 5,
        '震度5強': 5.5, '震度6弱': 6, '震度6強': 6.5, '震度7': 7
    }
    df['最大震度_数値'] = df['最大震度'].map(s_mapping)
    df['最大震度'] = pd.Categorical(df['最大震度'], categories=s_order, ordered=True)
    return df

# データの前処理を実行
df = preprocess_data(df)
df = df[df["震央地名"]==target]
df.head()



時系列の可視化


最初は時系列で
どれだけ地震が発生しているのかを
見てみましょう

日別、震度別で時系列で地震の回数を
表示してみます。

# 時系列分析(日付別)
df['日付'] = pd.to_datetime(df['地震の発生日'])
daily_shindo_counts = df.groupby(['日付', '最大震度']).size().reset_index(name='地震回数')

# 震度の順序を維持
daily_shindo_counts['最大震度'] = pd.Categorical(
    daily_shindo_counts['最大震度'], 
    categories=s_order, 
    ordered=True
)

# 震度別横並び棒グラフ
fig_time = px.bar(
    daily_shindo_counts,
    x='日付',
    y='地震回数',
    color='最大震度',
    title='日付別地震発生回数(震度別)',
    labels={'地震回数': '地震発生回数', '日付': '発生日'},
    category_orders={'最大震度': s_order},
    barmode='group'  # 横並びに表示
)

# レイアウトの調整
fig_time.update_layout(
    xaxis_tickangle=-45,
    height=600,
    width=1000,
    legend=dict(
        orientation="v",
        yanchor="top",
        y=1,
        xanchor="left",
        x=1.01
    )
)

fig_time.show()
スクリーンショット 2025-07-05 17.27.01

データは直近の1週間分しか無いようです。
7月前後で急増しているのが分かります。



箱ひげ図の可視化


次は箱ひげ図で
「トカラ列島近海」の詳細を見てみましょう。


震度ごとにマグニチュードをまとめると
このような感じになります。

# 震央地名 最大震度別での箱ひげ図

fig_box = px.box(df, 
                 x='震央地名', 
                 y='M',
                 color='最大震度',
                 title='震央地名別 マグニチュード分布(最大震度別)',
                 labels={'M': 'マグニチュード', '震央地名': '震央地名'},
                 category_orders={'最大震度': s_order})

fig_box.update_layout(
    xaxis_tickangle=-45,
    height=600,
    width=1000
)
fig_box.show()
スクリーンショット 2025-07-05 17.29.52
同じ震度でも、マグニチュードにはバラツキがありますが
マグニチュードが上がるにつれ、震度も大きくなっています。



地図表示

今後は地図で震度を表示してみましょう
OpenStreetMapを使用して、緯度軽度を用いて
地図に震度を表示させてみます。

# マップ表示用のデータ準備
df_map = df.copy()
df_map['緯度_数値'] = pd.to_numeric(df_map['緯度_数値'], errors='coerce')
df_map['経度_数値'] = pd.to_numeric(df_map['経度_数値'], errors='coerce')
df_map['M'] = pd.to_numeric(df_map['M'], errors='coerce')
df_map = df_map.dropna(subset=['緯度_数値', '経度_数値', 'M'])
df_map['最大震度'] = pd.Categorical(df_map['最大震度'], categories=s_order, ordered=True)

# 緯度経度と最大震度・Mを用いたmap表示(OpenStreetMap使用)
fig_map2 = go.Figure()

# 震度別に色分けしてプロット
震度_colors = {
    '震度1': '#90EE90',  # 薄緑
    '震度2': '#FFD700',  # 金色
    '震度3': '#FFA500',  # オレンジ
    '震度4': '#FF6347',  # トマト色
    '震度5弱': '#FF4500',  # 赤オレンジ
    '震度5強': '#FF0000',  # 赤
    '震度6弱': '#8B0000',  # 濃い赤
    '震度6強': '#4B0082',  # インディゴ
    '震度7': '#000000'   # 黒
}

for 震度 in s_order:
    if 震度 in df_map['最大震度'].values:
        subset = df_map[df_map['最大震度'] == 震度]
        fig_map2.add_trace(go.Scattermapbox(
            lat=subset['緯度_数値'],
            lon=subset['経度_数値'],
            mode='markers',
            marker=dict(
                size=subset['最大震度_数値'] ** 2 ,  # サイズ調整
                color=震度_colors.get(震度, '#000000'),
                sizemode='diameter'
            ),
            text=subset['震央地名'],
            hovertemplate='%{text}
' + '緯度: %{lat:.2f}
' + '経度: %{lon:.2f}
' + 'マグニチュード: %{customdata[0]}
' + '最大震度: %{customdata[1]}
' + '', customdata=list(zip(subset['M'], subset['最大震度'])), name=震度 )) fig_map2.update_layout( title='地震発生位置(最大震度・マグニチュード)- OpenStreetMap', mapbox=dict( style="open-street-map", center=dict(lat=df_map['緯度_数値'].mean(), lon=df_map['経度_数値'].mean()), zoom=8 ), height=700, width=1000 ) fig_map2.show()
スクリーンショット 2025-07-05 17.34.15

今回の地震は、かなり局所的に起きていることが分かります。
特に震度5以上が発生した箇所がかなり近く
この近辺は注意が必要な地域に見えます。


散布図表示


最後に
マグニチュード、震度、深さを
散布図で見てみましょう

# 深さとマグニチュードの関係
fig_scatter = px.scatter(
    df,
    x='深さ_数値',
    y='M',
    color='最大震度',
    size='最大震度_数値',
    hover_name='震央地名',
    hover_data={
        '地震の発生日': True,
        '地震の発生時刻': True
    },
    title='深さとマグニチュードの関係(最大震度別)',
    labels={'深さ_数値': '深さ (km)', 'M': 'マグニチュード'},
    category_orders={'最大震度': s_order}
)

fig_scatter.show()
スクリーンショット 2025-07-05 17.36.27


どの深さでも、満遍なく起きているように見えますね
地震自体は、いつ何時、どこでも起きてしまいます。



まとめ

緯度経度が記載されているデータは
plotlyなどを用いれば地図表示が比較的
簡単に行うことができます。

可視化の表現の幅が広がるので
覚えておくと良いかもしれませんね


今日はここまでです
それでは




今回は金融庁の
NISA積立シミュレーターがやばいので
その解説です。


解説動画はこちら


 

金融庁の積立シミュレーター

これがそのシミュレーターです。

サイト

ここで
毎月の積立1万円
年利12%
40年間

で試算してみます。
スクリーンショット 2025-06-14 17.12.47


こんな感じになりました。

年利が高すぎますが
計算がわかりやすいようにして
計算してみた結果です。

40年後の資産は
11765万円になりました



Pythonでのシミュレーションコード


先程のシミュレーターと同じ計算を
Pythonでも計算してみましょう。

シミュレーションコード

monthly_rate = 0.12 / 12       # 月利(年利12%)
months = 40 * 12               # 総月数(480ヶ月)
monthly_payment = 10000       # 毎月の積立額(マイナス表記にする)

total = 0
for _ in range(months):
    total = total * (1 + monthly_rate) + monthly_payment

print(f"将来の資産総額: {total:,.0f} 円")
将来の資産総額: 117,647,725 円


金融庁のシミュレーターの結果は
11765万円 なので
ほぼ同じ結果になりました。


落とし穴

実はここに落とし穴があります!!!!

年利計算での正しいシミュレーション結果は
こうなります。
(年末に積立するパターン)

year_rate = 0.12            # 年利12%
years = 40                  # 総年数(40年)
year_payment = 120000    # 年の積立額

total = 0
for _ in range(years):
    total = total * (1 + year_rate) + year_payment

print(f"将来の資産総額: {total:,.0f} 円")
将来の資産総額: 92,050,970 円

先程の計算結果 : 117,647,725 円
とは大きく乖離してきます。


からくり

金融庁のサイトのシミュレーションは年利ではなく
年利を12ヶ月で割った月利の複利(年利 / 12)で計算しているようです。

そのため年利と月利では、計算結果が大きく異なってきます。

もし月利複利計算を正しくするとしたら
12乗して年利になるような値を月利にしないといけません。

月利 = (1 + 0.12) ** (1/12) - 1
= 0.009488792934583046 = 0.948%

年利から月利に直して計算するコードはこちら
monthly_rate = (1 + 0.12) ** (1/12) - 1  # 月利(年利12%)
months = 40 * 12                          # 総月数(480ヶ月)
monthly_payment = 10000                 # 毎月の積立額(マイナス表記にする)

total = 0
for _ in range(months):
    total = total * (1 + monthly_rate) + monthly_payment

print(f"将来の資産総額: {total:,.0f} 円")
将来の資産総額: 97,010,200 円

月利計算だとこのくらいの金額になります。




資産推移のシミュレーター

年利を月利換算しても正しく計算するようにしたものは
こんな感じになります。
import pandas as pd
import plotly.express as px

def plot_investment(rate_percent, years, payment_man):
    annual_rate = rate_percent / 100
    monthly_rate = (1 + annual_rate) ** (1/12) - 1
    months = years * 12
    monthly_payment = payment_man * 10000

    results, ci_total, am_total = [], 0, 0
    for month in range(1, months + 1):
        ci_total = ci_total * (1 + monthly_rate) + monthly_payment
        am_total += monthly_payment
        results.append({'月': month, '資産総額': ci_total, '積立金額': am_total})

    df = pd.DataFrame(results)
    fig = px.line(df, x='月', y=['資産総額', '積立金額'],
                  labels={'value': '金額(円)', 'variable': '項目'},
                  title=f'毎月{payment_man}万円積立(年利{rate_percent:.1f}%・{years}年)の資産推移')
    
    fig.update_layout(
        yaxis_tickformat=',',
        height=400
    )
    fig.show()

積立シミュレーション設定

#@title 積立シミュレーション設定
rate_percent = 5.1 # @param {type:"slider", min:1.0, max:20.0, step:0.1}
years = 30 # @param {type:"slider", min:10, max:50, step:1}
payment_man = 20 # @param {type:"slider", min:1, max:30, step:1}

plot_investment(rate_percent, years, payment_man)

colabで実行するとこのような画面が出てきます
スクリーンショット 2025-06-14 17.34.43

これを設定して実行すると
スクリーンショット 2025-06-14 17.34.53
積立のシミュレーション結果が反映されます。

colabで試せるので
コピペして試してみてください。



まとめ

金融庁のサイトのシミュレーターは
年利と月利を齟齬しているので
数十年後の金額が大きく乖離してくるようです。

単利と複利、月利と年利
この辺りの関係を正しく計算できるように
日々プログラムでシミュレーションすると安全です。

資産形成に取り組みたい方は
この機会にプログラミングも併せて
学んでみてはいかがでしょうか?


それでは。


今回は画像に隠しメッセージを仕込める技術
ステガノグラフィーのご紹介です。


解説動画はこちら



ステガノグラフィーとは


ステガノグラフィー(Steganography)
画像、音声、動画などのデジタルデータに
別の情報を隠す技術のことです

これにより、情報を目に見えない形で
秘匿することができます。

早速情報を仕込んだ画像を作成してみましょう



ステガノグラフィーの仕組み

画像への埋め込み

最下位ビット(LSB)を利用して
画像の色をわずかに変更しメッセージを隠します。

最下位ビット(Least Significant Bit):
二進数の右端に位置する値

バイナリ変換:
メッセージをバイナリ形式に変換し終了ビットを追加

ピクセルの変更:
各ピクセルのRGB成分の最下位ビットを変更して
メッセージを埋め込み



ビット演算

変更対象のRGB値の各成分は0から255の範囲の整数
~1 は、ビット反転演算子

1 のビットを反転すると、全てのビットが反転し
...11111110 というビット列になる

RGB値 が 5の場合(バイナリで 00000101)
~1 は ...11111110 となり
5 & ~1 は 00000101 & 11111110 となり
結果は 00000100(つまり 4)になる

00000101 &
11111110



00000100


ピクセルの変更

各ピクセルのRGB成分から最下位ビットを取得し
バイナリメッセージの値に変更します


メッセージの復元

バイナリメッセージを8ビットごとに分割し、文字に変換
終了ビット(11111111)が見つかるまでメッセージを復元する



音声ファイルや動画ファイルにも同様の手法が使われ
音声の波形や動画のフレームに情報を埋め込むことが可能です。


埋め込む画像


元画像はこんな感じです。
sample





画像にメッセージを埋め込むコードサンプル

from PIL import Image

def encode_image(image_path, secret_message, output_path):
    # 画像を開く
    image = Image.open(image_path)
    encoded_image = image.copy()

    # メッセージをバイナリに変換
    binary_message = ''.join(format(ord(char), '08b') for char in secret_message) + '11111111'  # 終了ビット
    data_index = 0

    # 画像のピクセルを走査
    for y in range(encoded_image.height):
        for x in range(encoded_image.width):
            pixel = list(encoded_image.getpixel((x, y)))
            for i in range(3):  # RGBの各成分
                if data_index < len(binary_message):
                    pixel[i] = (pixel[i] & ~1) | int(binary_message[data_index])  # 最下位ビットを変更
                    data_index += 1
            encoded_image.putpixel((x, y), tuple(pixel))
            if data_index >= len(binary_message):
                break
        if data_index >= len(binary_message):
            break

    encoded_image.save(output_path)

# 使用例
input_img_path = "画像のパス"
input_message = "埋め込むメッセージ"
output_img_path = "出力後のパス"
encode_image(input_img_path, input_message, output_img_path)

実行するとメッセージを埋め込んだ画像が生成されます。

元の画像と比較すると

download


差分はほとんどわからないですね!!


画像からメッセージを取り出すコードサンプル

from PIL import Image

# バイナリメッセージを8ビットごとに分割して文字に変換
def make_message(binary_message):
    message = ''
    for i in range(0, len(binary_message), 8):
        byte = binary_message[i:i+8]
        if byte == '11111111':  # 終了ビットを検出
            break
        message += chr(int(byte, 2))
    return message

def decode_image(image_path):
    image = Image.open(image_path)
    binary_message = ''

    # 画像のピクセルを走査
    for y in range(image.height):
        for x in range(image.width):
            pixel = image.getpixel((x, y))
            for i in range(3):  # RGBの各成分
                binary_message += str(pixel[i] & 1)  # 最下位ビットを取得

    return make_message(binary_message)

# 使用例
decoded_message = decode_image('output_image.png')
print("Decoded message:", decoded_message)
Decoded message: Hello Otupy

メッセージが復元されました。




最後に

こちらの画像にはメッセージを埋め込んでいるので
ダウンロードして解読してみてくださいね

bbbb




今回はステガノグラフィーをご紹介しました
画像などに隠しメッセージを残せるので

暗号のように秘匿したいやりとりで
使うこともできたりして
なかなか面白い技術です。

いろいろ遊んでみてみてくださいね
それでは



このページのトップヘ