<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE feed [
      <!ENTITY lt "&#38;#60;">
      <!ENTITY gt "&#62;">
      <!ENTITY amp "&#38;#38;">
      <!ENTITY apos "&#39;">
      <!ENTITY quot "&#34;">
      <!ENTITY nbsp "&#160;">
      <!ENTITY copy "&#169;">
]>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title type="text">ブログ</title>
    <subtitle type="text">UJP-Unwired Job Professional</subtitle>
    <updated>2026-04-24T20:50:33+09:00</updated>
    <id>http://www.ujp.jp/modules/d3blog/index.php</id>
    <link rel="alternate" type="text/xhtml" hreflang="ja" href="http://www.ujp.jp/" />
    <link rel="self" type="application/atom+xml" href="http://www.ujp.jp/modules/d3blog/index.php?page=atom" />
    <rights>Copyright (c) 1995-2020</rights>
    <generator uri="http://www.ujp.jp/">D3BLOG - XOOPS BLOG MODULE</generator>
    <entry>
        <title>写真，360度全天球写真，動画ファイルを１つのフォルダに入れてアルバムページを作る</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=11146" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=11146</id>
        <published>2026-04-13T00:46:55+09:00</published>
        <updated>2026-04-13T00:56:42+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　件名にある通り，１つのフォルダに写真，360度全天球写真，動画ファイルを入れて，そのフォルダを指定して実行すると，アルバムを作成してくれるツール．Microsoft Copilotを使って作り...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　件名にある通り，１つのフォルダに写真，360度全天球写真，動画ファイルを入れて，そのフォルダを指定して実行すると，アルバムを作成してくれるツール．Microsoft Copilotを使って作りました．<br /><br />　こんな感じで使います．<br /><div class="xoopsCode"><pre><code>$ python3 generate_viewer.py -f 20260409SunriseIzumo -o ¥ 
20260409SunriseIzumo.html -t サンライズ出雲乗車記録2026.04.09_150分おくれ
$

-f メディアの入ったフォルダ
-o HTMLファイル名
-t ページタイトル</code></pre></div>　-fで指定されたフォルダに，サムネイルファイルや生成されたHTMLファイルが入ります．<br />　-oで生成されたファイルをブラウザで読み込むとアルバムとして使えます．<br />　写真の説明は，ファイル名から取得する仕様なので，ファイル名に長く記載してください．ファイルの掲載順は，ファイルのタイムスタンプです．<br /><br />　iPhoneの写真をmacOSにAirDropするとHEIFファイルになりますが，そのままだとうまくサムネイルが作成できないので，Finderで「イメージ変換」してJPEGにしてください．画像サイズは「実際のサイズ」で良いかと思います．<br />　以下のプログラムは，macOS Montereyで動作確認していますが，<a href="http://www.ujp.jp/modules/d3blog/details.php?bid=11082" rel="external">RICOH THETAで撮影した360度写真と共に写真アルバムを作る</a>にあるPannellumを使っています．<br /><br />　作ったものはこんな感じ．<br /><br /><a href="http://www.ujp.jp/images/theta/20260409SunriseIzumo/20260409SunriseIzumo.html" rel="external">サンライズ出雲乗車記録2026.04.09_150分おくれ</a><br /><br /><div class="xoopsCode"><pre><code>$ cat generate_viewer.py🆑
#!/usr/bin/env python3
import argparse
from pathlib import Path
import subprocess
import json
from datetime import datetime, timezone, timedelta
import urllib.request
import urllib.parse
from collections import OrderedDict

# ==========================
# 共通
# ==========================
created_date = datetime.now().strftime(&quot;%Y年%m月%d日&quot;)
geocode_cache = {}
JST = timezone(timedelta(hours=9))

# ==========================
# 逆ジオコーディング
# ==========================
def reverse_geocode(lat, lon):
    key = f&quot;{lat},{lon}&quot;
    if key in geocode_cache:
        return geocode_cache[key]
    try:
        url = &quot;https://nominatim.openstreetmap.org/reverse?&quot; + urllib.parse.urlencode({
            &quot;lat&quot;: lat,
            &quot;lon&quot;: lon,
            &quot;format&quot;: &quot;json&quot;,
            &quot;zoom&quot;: 16,
            &quot;addressdetails&quot;: 1
        })
        req = urllib.request.Request(url, headers={&quot;User-Agent&quot;: &quot;Mozilla/5.0&quot;})
        with urllib.request.urlopen(req, timeout=5) as res:
            data = json.loads(res.read().decode(&quot;utf-8&quot;))
            addr = data.get(&quot;address&quot;, {})
            geocode_cache[key] = addr
            return addr
    except:
        geocode_cache[key] = {}
        return {}

def extract_city(addr):
    return (
        addr.get(&quot;city&quot;) or
        addr.get(&quot;town&quot;) or
        addr.get(&quot;village&quot;) or
        addr.get(&quot;municipality&quot;) or
        addr.get(&quot;ward&quot;) or
        addr.get(&quot;city_district&quot;) or
        addr.get(&quot;county&quot;) or
        addr.get(&quot;suburb&quot;) or
        &quot;&quot;
    )

def format_location_short(addr):
    if not addr:
        return &quot;ー&quot;
    state = addr.get(&quot;state&quot;, &quot;&quot;)
    city = extract_city(addr)
    if state and city:
        return f&quot;{state} {city}&quot;
    elif state:
        return state
    elif city:
        return city
    else:
        return &quot;ー&quot;

def format_location_full(addr):
    if not addr:
        return &quot;ー&quot;
    state = addr.get(&quot;state&quot;, &quot;&quot;)
    city = extract_city(addr)
    detail_parts = []
    for key in [&quot;suburb&quot;, &quot;neighbourhood&quot;, &quot;hamlet&quot;, &quot;quarter&quot;, &quot;road&quot;, &quot;house_number&quot;]:
        if addr.get(key):
            detail_parts.append(addr[key])
    detail = &quot; &quot;.join(detail_parts)
    if state and city:
        base = f&quot;{state} {city}&quot;.strip()
    elif state:
        base = state
    elif city:
        base = city
    else:
        base = &quot;&quot;
    if base and detail:
        return f&quot;{base} {detail}&quot;.strip()
    elif base:
        return base
    elif detail:
        return detail
    else:
        return &quot;ー&quot;
# ==========================
# 日時関連
# ==========================
def parse_dt(dt_str):
    try:
        return datetime.strptime(dt_str, &quot;%Y:%m:%d %H:%M:%S&quot;)
    except:
        return None

def format_datetime(dt_str):
    try:
        dt = datetime.strptime(dt_str, &quot;%Y:%m:%d %H:%M:%S&quot;)
        return dt.strftime(&quot;%Y年%m月%d日 %H:%M:%S&quot;)
    except:
        return &quot;ー&quot;

def parse_creation_time(s):
    if not s:
        return None
    try:
        return datetime.fromisoformat(s.replace(&quot;Z&quot;, &quot;+00:00&quot;))
    except:
        return None

def format_datetime_from_dt(dt):
    if not dt:
        return &quot;ー&quot;
    return dt.strftime(&quot;%Y年%m月%d日 %H:%M:%S&quot;)

def format_date_only_from_dt(dt):
    if not dt:
        return &quot;その他&quot;
    return dt.strftime(&quot;%Y年%m月%d日&quot;)

# ==========================
# EXIF / 動画メタデータ
# ==========================
def get_exif(path):
    result = subprocess.run(
        [&quot;exiftool&quot;, &quot;-json&quot;, &quot;-n&quot;, str(path)],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    return json.loads(result.stdout)[0]

def get_video_metadata(path):
    cmd = [
        &quot;ffprobe&quot;, &quot;-v&quot;, &quot;quiet&quot;, &quot;-print_format&quot;, &quot;json&quot;,
        &quot;-show_format&quot;, &quot;-show_streams&quot;, str(path)
    ]
    result = subprocess.run(cmd, stdout=subprocess.PIPE, text=True)
    if not result.stdout.strip():
        return {}
    return json.loads(result.stdout)

def parse_video_location(loc):
    if not loc:
        return None, None
    try:
        loc = loc.strip(&quot;/&quot;)
        if loc[0] in &quot;+-&quot;:
            sign = loc[0]
            rest = loc[1:]
            parts = rest.split(&quot;+&quot;)
            if len(parts) &gt;= 2:
                lat = float(sign + parts[0])
                lon = float(&quot;+&quot; + parts[1])
                return lat, lon
    except:
        pass
    return None, None

def format_duration(sec):
    try:
        sec = int(float(sec))
        m = sec // 60
        s = sec % 60
        return f&quot;{m:02d}:{s:02d}&quot;
    except:
        return &quot;ー&quot;

# ==========================
# HTML テンプレート
# ==========================
INDEX_HEAD = &quot;&quot;&quot;&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ja&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;{page_title}&lt;/title&gt;
&lt;style&gt;
body {{
  font-family: sans-serif;
  background: #fafafa;
  margin: 0;
}}
header {{
  text-align: center;
  padding: 20px;
}}
h1 {{
  margin: 10px 0 0 0;
  font-size: 26px;
}}
h3 {{
  margin: 5px 0 10px 0;
  font-size: 16px;
  color: #555;
}}
.topnav {{
  text-align: center;
  margin: 10px 0;
  font-size: 18px;
}}
.section-title {{
  max-width: 1200px;
  margin: 10px auto 0 auto;
  padding: 0 20px;
  font-size: 18px;
  font-weight: bold;
}}
.count-info {{
  text-align: center;
  font-size: 14px;
  color: #555;
  margin-bottom: 10px;
}}
.grid {{
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 20px;
  padding: 10px 20px 20px 20px;
  max-width: 1200px;
  margin: 0 auto 20px auto;
}}
.card {{
  background: white;
  border-radius: 8px;
  padding: 10px;
  box-shadow: 0 0 6px rgba(0,0,0,0.2);
  text-align: center;
}}
.thumb-img {{
  width: 100%;
  height: auto;
  border-radius: 6px;
}}
.filename {{
  text-align: left;
  font-size: 14px;
  margin-top: 5px;
}}
.datetime {{
  text-align: left;
  font-size: 12px;
  color: #666;
}}
.location {{
  text-align: left;
  font-size: 12px;
  color: #444;
  margin-top: 3px;
}}
.map-link {{
  text-align: left;
  font-size: 12px;
  margin-top: 3px;
}}
.map-link a {{
  text-decoration: none;
  color: #1a73e8;
}}
.badge {{
  display: inline-block;
  padding: 2px 6px;
  font-size: 10px;
  border-radius: 4px;
  background: #eee;
  margin-left: 4px;
}}
.badge-360 {{
  background: #4caf50;
  color: #fff;
}}
.badge-video {{
  background: #f44336;
  color: #fff;
}}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class=&quot;topnav&quot;&gt;
  &lt;a href=&quot;../index.html&quot;&gt;↑ 1つ上に戻る&lt;/a&gt;
&lt;/div&gt;

&lt;header&gt;
  &lt;hr&gt;
  &lt;h1&gt;{page_title}&lt;/h1&gt;
  &lt;h3&gt;作成日：{created_date}&lt;/h3&gt;
  &lt;div class=&quot;count-info&quot;&gt;全 {total_count} 件&lt;/div&gt;
  &lt;hr&gt;
&lt;/header&gt;
&quot;&quot;&quot;
INDEX_FOOT = &quot;&quot;&quot;
&lt;/body&gt;
&lt;/html&gt;
&quot;&quot;&quot;

VIEWER_HEAD = &quot;&quot;&quot;&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ja&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;{title}&lt;/title&gt;

&lt;link rel=&quot;stylesheet&quot; href=&quot;../pannellum.css&quot;&gt;
&lt;script src=&quot;../pannellum.js&quot;&gt;&lt;/script&gt;

&lt;style&gt;
body {{
  margin: 0;
  font-family: sans-serif;
  background: #fafafa;
}}
header {{
  text-align: center;
  padding: 20px;
}}
h1 {{
  margin: 10px 0 0 0;
  font-size: 26px;
}}
h3 {{
  margin: 5px 0 10px 0;
  font-size: 16px;
  color: #555;
}}
.pano {{
  width: 100%;
  height: 500px;
}}
.info-table {{
  margin: 20px auto;
  width: 90%;
  max-width: 700px;
  border-collapse: collapse;
}}
.info-table td {{
  border: 1px solid #ccc;
  padding: 8px;
}}
.info-table td:first-child {{
  width: 30%;
  background: #f0f0f0;
  font-weight: bold;
}}
.nav {{
  margin: 20px 0;
  font-size: 22px;
  text-align: center;
}}
.nav a {{
  margin: 0 25px;
  text-decoration: none;
}}
.map-btn {{
  display: inline-block;
  padding: 10px 20px;
  background: #4285F4;
  color: white;
  border-radius: 6px;
  text-decoration: none;
  font-size: 16px;
}}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;header&gt;
  &lt;hr&gt;
  &lt;h1&gt;{title}&lt;/h1&gt;
  &lt;h3&gt;作成日：{created_date}&lt;/h3&gt;
  &lt;hr&gt;
&lt;/header&gt;

&lt;div class=&quot;nav&quot;&gt;
  {nav_top}
&lt;/div&gt;
&quot;&quot;&quot;

VIEWER_FOOT = &quot;&quot;&quot;
&lt;/body&gt;
&lt;/html&gt;
&quot;&quot;&quot;

# ==========================
# メイン処理
# ==========================
def main():
    parser = argparse.ArgumentParser(description=&quot;THETA 360° / 写真 / 動画 ビューア生成（HEIC変換 + 動画メタデータ + JST対応）&quot;)
    parser.add_argument(&quot;-f&quot;, &quot;--folder&quot;, required=True)
    parser.add_argument(&quot;-o&quot;, &quot;--output&quot;, required=True)
    parser.add_argument(&quot;-t&quot;, &quot;--title&quot;, required=True)
    args = parser.parse_args()

    folder = Path(args.folder).resolve()

    image_ext = {&quot;.jpg&quot;, &quot;.jpeg&quot;, &quot;.JPG&quot;, &quot;.JPEG&quot;, &quot;.png&quot;, &quot;.PNG&quot;, &quot;.heic&quot;, &quot;.HEIC&quot;}
    video_ext = {&quot;.mp4&quot;, &quot;.mov&quot;, &quot;.m4v&quot;, &quot;.webm&quot;, &quot;.MP4&quot;, &quot;.MOV&quot;, &quot;.M4V&quot;, &quot;.WEBM&quot;}

    files = []
    for p in sorted(folder.iterdir()):
        if p.suffix in image_ext or p.suffix in video_ext:
            files.append(p)

    items = []

    # ==========================
    # ファイルごとの処理
    # ==========================
    for f in files:
        ext = f.suffix
        lower_ext = f.suffix.lower()
        is_video = lower_ext in {e.lower() for e in video_ext}
        is_heic = lower_ext in {&quot;.heic&quot;}

        # HEIC → JPEG 変換（元ファイルは残す）
        if is_heic:
            jpg_path = f.with_suffix(&quot;.jpg&quot;)
            if not jpg_path.exists():
                subprocess.run(
                    [&quot;ffmpeg&quot;, &quot;-y&quot;, &quot;-i&quot;, str(f), str(jpg_path)],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL
                )
            f = jpg_path
            lower_ext = &quot;.jpg&quot;
            is_video = False

        exif = {}
        dt = None
        dt_raw = None
        camera_model = &quot;ー&quot;
        lat = None
        lon = None
        projection = None

        video_meta = {}
        video_duration = &quot;ー&quot;
        video_codec = &quot;ー&quot;
        video_resolution = &quot;ー&quot;

        # ==========================
        # 写真（EXIF）
        # ==========================
        if not is_video:
            exif = get_exif(f)
            dt_raw = exif.get(&quot;DateTimeOriginal&quot;)
            dt = parse_dt(dt_raw) if dt_raw else None
            camera_model = exif.get(&quot;Model&quot;, &quot;ー&quot;)
            lat = exif.get(&quot;GPSLatitude&quot;)
            lon = exif.get(&quot;GPSLongitude&quot;)
            projection = exif.get(&quot;ProjectionType&quot;)

        # ==========================
        # 動画（ffprobe）
        # ==========================
        else:
            video_meta = get_video_metadata(f)
            fmt = video_meta.get(&quot;format&quot;, {})
            streams = video_meta.get(&quot;streams&quot;, [])

            tags = fmt.get(&quot;tags&quot;, {}) if isinstance(fmt, dict) else {}
            creation_time = tags.get(&quot;creation_time&quot;)
            dt = parse_creation_time(creation_time)

            # ★ UTC → JST → naive
            if dt and dt.tzinfo:
                dt = dt.astimezone(JST)
            if dt:
                dt = dt.replace(tzinfo=None)

            dt_raw = creation_time

            camera_model = tags.get(&quot;com.apple.quicktime.model&quot;) or tags.get(&quot;model&quot;) or &quot;ー&quot;

            loc = tags.get(&quot;location&quot;)
            if loc:
                lat, lon = parse_video_location(loc)

            dur = fmt.get(&quot;duration&quot;)
            if dur:
                video_duration = format_duration(dur)

            if streams:
                vstream = None
                for s in streams:
                    if s.get(&quot;codec_type&quot;) == &quot;video&quot;:
                        vstream = s
                        break
                if not vstream:
                    vstream = streams[0]

                codec_name = vstream.get(&quot;codec_name&quot;)
                if codec_name:
                    video_codec = codec_name

                w = vstream.get(&quot;width&quot;)
                h = vstream.get(&quot;height&quot;)
                if w and h:
                    video_resolution = f&quot;{w}×{h}&quot;

        items.append({
            &quot;path&quot;: f,
            &quot;is_video&quot;: is_video,
            &quot;exif&quot;: exif,
            &quot;dt_raw&quot;: dt_raw,
            &quot;dt&quot;: dt,
            &quot;camera_model&quot;: camera_model,
            &quot;lat&quot;: lat,
            &quot;lon&quot;: lon,
            &quot;projection&quot;: projection,
            &quot;video_duration&quot;: video_duration,
            &quot;video_codec&quot;: video_codec,
            &quot;video_resolution&quot;: video_resolution,
        })
    # ==========================
    # 日付順にソート
    # ==========================
    items.sort(key=lambda x: x[&quot;dt&quot;] if x[&quot;dt&quot;] else datetime.max)

    # ==========================
    # サムネイル生成フォルダ
    # ==========================
    thumb_dir = folder / &quot;thumb&quot;
    thumb_dir.mkdir(exist_ok=True)

    index_html = folder / args.output
    total_count = len(items)
    index_parts = [
        INDEX_HEAD.format(
            page_title=args.title,
            created_date=created_date,
            total_count=total_count
        )
    ]

    # ==========================
    # 日付グループ化
    # ==========================
    groups = OrderedDict()
    for idx, item in enumerate(items):
        dt = item[&quot;dt&quot;]
        if dt:
            date_key = dt.strftime(&quot;%Y-%m-%d&quot;)
            date_label = format_date_only_from_dt(dt)
        else:
            date_key = &quot;other&quot;
            date_label = &quot;その他&quot;

        if date_key not in groups:
            groups[date_key] = {&quot;label&quot;: date_label, &quot;items&quot;: []}

        groups[date_key][&quot;items&quot;].append((idx, item))

    total = len(items)

    # ==========================
    # 個別ページ生成
    # ==========================
    for i, item in enumerate(items):
        f = item[&quot;path&quot;]
        is_video = item[&quot;is_video&quot;]
        dt = item[&quot;dt&quot;]
        dt_raw = item[&quot;dt_raw&quot;]
        camera_model = item[&quot;camera_model&quot;]
        lat = item[&quot;lat&quot;]
        lon = item[&quot;lon&quot;]
        projection = item[&quot;projection&quot;]
        video_duration = item[&quot;video_duration&quot;]
        video_codec = item[&quot;video_codec&quot;]
        video_resolution = item[&quot;video_resolution&quot;]

        viewer_name = f&quot;viewer_{i+1:04d}.html&quot;
        viewer_path = folder / viewer_name

        # --------------------------
        # サムネイル生成
        # --------------------------
        thumb_name = f&quot;thumb_{i+1:04d}.jpg&quot;
        thumb_path = thumb_dir / thumb_name

        if not thumb_path.exists():
            if not is_video:
                subprocess.run(
                    [&quot;convert&quot;, str(f), &quot;-resize&quot;, &quot;300x300&quot;, str(thumb_path)]
                )
            else:
                subprocess.run(
                    [
                        &quot;ffmpeg&quot;, &quot;-y&quot;,
                        &quot;-i&quot;, str(f),
                        &quot;-ss&quot;, &quot;00:00:01&quot;,
                        &quot;-vframes&quot;, &quot;1&quot;,
                        &quot;-vf&quot;, &quot;scale=300:-1&quot;,
                        str(thumb_path)
                    ],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL
                )

        # --------------------------
        # 日付表示
        # --------------------------
        if dt_raw and isinstance(dt_raw, str) and &quot;:&quot; in dt_raw and dt_raw.count(&quot;:&quot;) == 2 and &quot;T&quot; not in dt_raw:
            dt_html_full = format_datetime(dt_raw)
        else:
            dt_html_full = format_datetime_from_dt(dt)

        # --------------------------
        # 位置情報
        # --------------------------
        if lat and lon:
            addr = reverse_geocode(lat, lon)
            location_short = format_location_short(addr)
            location_full = format_location_full(addr)
            lat_str = str(lat)
            lon_str = str(lon)
        else:
            location_short = &quot;ー&quot;
            location_full = &quot;ー&quot;
            lat_str = &quot;&quot;
            lon_str = &quot;&quot;

        # --------------------------
        # ナビゲーション
        # --------------------------
        prev_link = f&quot;viewer_{i:04d}.html&quot; if i &gt; 0 else &quot;&quot;
        next_link = f&quot;viewer_{i+2:04d}.html&quot; if i &lt; total - 1 else &quot;&quot;

        nav_html = &quot;&quot;
        nav_html += f&#039;&lt;a href=&quot;{prev_link}&quot;&gt;←&lt;/a&gt;&#039; if prev_link else &#039;←&#039;
        nav_html += f&#039;&lt;a href=&quot;{args.output}&quot;&gt;TOPに戻る&lt;/a&gt;&#039;
        nav_html += f&#039;&lt;a href=&quot;{next_link}&quot;&gt;→&lt;/a&gt;&#039; if next_link else &#039;→&#039;

        prev_js = f&#039;window.location.href=&quot;{prev_link}&quot;;&#039; if prev_link else &#039;&#039;
        next_js = f&#039;window.location.href=&quot;{next_link}&quot;;&#039; if next_link else &#039;&#039;

        comment_name = f.stem

        # --------------------------
        # Google Map ボタン
        # --------------------------
        if lat_str and lon_str:
            map_button_html = f&quot;&quot;&quot;
&lt;div style=&quot;text-align:center; margin:20px;&quot;&gt;
  &lt;a class=&quot;map-btn&quot; href=&quot;https://www.google.com/maps?q={lat_str},{lon_str}&quot; target=&quot;_blank&quot;&gt;
    Google Map で開く
  &lt;/a&gt;
&lt;/div&gt;
&quot;&quot;&quot;
        else:
            map_button_html = &quot;&quot;

        # --------------------------
        # 表示方式（360° / 写真 / 動画）
        # --------------------------
        ext = f.suffix.lower()
        video_ext_lower = {&quot;.mp4&quot;, &quot;.mov&quot;, &quot;.m4v&quot;, &quot;.webm&quot;}

        if projection == &quot;equirectangular&quot; and not is_video:
            viewer_html = f&quot;&quot;&quot;
&lt;div id=&quot;panorama&quot; class=&quot;pano&quot;&gt;&lt;/div&gt;

&lt;script&gt;
pannellum.viewer(&#039;panorama&#039;, {{
  type: &#039;equirectangular&#039;,
  panorama: &#039;{f.name}&#039;,
  autoLoad: true,
  hfov: 120
}});
&lt;/script&gt;
&quot;&quot;&quot;
        elif ext in video_ext_lower:
            viewer_html = f&quot;&quot;&quot;
&lt;div style=&quot;text-align:center; margin:20px;&quot;&gt;
  &lt;video controls style=&quot;max-width:100%; border-radius:8px;&quot;&gt;
    &lt;source src=&quot;{f.name}&quot; type=&quot;video/mp4&quot;&gt;
    お使いのブラウザは動画再生に対応していません。
  &lt;/video&gt;
&lt;/div&gt;
&quot;&quot;&quot;
        else:
            viewer_html = f&quot;&quot;&quot;
&lt;div style=&quot;text-align:center; margin:20px;&quot;&gt;
  &lt;img src=&quot;{f.name}&quot; style=&quot;max-width:100%; height:auto; border-radius:8px;&quot;&gt;
&lt;/div&gt;
&quot;&quot;&quot;

        # --------------------------
        # 個別ページ HTML 書き込み
        # --------------------------
        with open(viewer_path, &quot;w&quot;, encoding=&quot;utf-8&quot;) as vf:
            vf.write(VIEWER_HEAD.format(
                title=args.title,
                created_date=created_date,
                nav_top=nav_html
            ))

            vf.write(viewer_html)

            vf.write(f&quot;&quot;&quot;
&lt;script&gt;
document.addEventListener(&#039;keydown&#039;, function(e) {{
    if (e.key === &quot;ArrowLeft&quot;) {{
        {prev_js}
    }}
    if (e.key === &quot;ArrowRight&quot;) {{
        {next_js}
    }}
}});
&lt;/script&gt;

&lt;table class=&quot;info-table&quot;&gt;
  &lt;tr&gt;&lt;td&gt;撮影日&lt;/td&gt;&lt;td&gt;{dt_html_full}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;撮影場所&lt;/td&gt;&lt;td&gt;{location_full}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;カメラ&lt;/td&gt;&lt;td&gt;{camera_model}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;コメント&lt;/td&gt;&lt;td&gt;{comment_name}&lt;/td&gt;&lt;/tr&gt;
&quot;&quot;&quot;)

            if is_video:
                vf.write(f&quot;&quot;&quot;
  &lt;tr&gt;&lt;td&gt;動画の長さ&lt;/td&gt;&lt;td&gt;{video_duration}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;コーデック&lt;/td&gt;&lt;td&gt;{video_codec}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;解像度&lt;/td&gt;&lt;td&gt;{video_resolution}&lt;/td&gt;&lt;/tr&gt;
&quot;&quot;&quot;)

            vf.write(&quot;&lt;/table&gt;\n&quot;)
            vf.write(map_button_html)

            vf.write(f&quot;&quot;&quot;
&lt;div class=&quot;nav&quot;&gt;
  {nav_html}
&lt;/div&gt;
&quot;&quot;&quot;)

            vf.write(VIEWER_FOOT)

        # --------------------------
        # 一覧ページ用データ保存
        # --------------------------
        item[&quot;viewer_name&quot;] = viewer_name
        item[&quot;thumb_name&quot;] = thumb_name
        item[&quot;dt_html_full&quot;] = dt_html_full
        item[&quot;location_short&quot;] = location_short
        item[&quot;lat_str&quot;] = lat_str
        item[&quot;lon_str&quot;] = lon_str

        if projection == &quot;equirectangular&quot; and not is_video:
            item[&quot;badge&quot;] = &quot;360&quot;
        elif is_video:
            item[&quot;badge&quot;] = &quot;video&quot;
        else:
            item[&quot;badge&quot;] = &quot;photo&quot;

    # ==========================
    # 一覧ページ生成
    # ==========================
    for date_key, group in groups.items():
        label = group[&quot;label&quot;]
        index_parts.append(f&#039;&lt;div class=&quot;section-title&quot;&gt;{label}&lt;/div&gt;\n&#039;)
        index_parts.append(&#039;&lt;div class=&quot;grid&quot;&gt;\n&#039;)

        for idx, _ in group[&quot;items&quot;]:
            img_item = items[idx]
            viewer_name = img_item[&quot;viewer_name&quot;]
            thumb_name = img_item[&quot;thumb_name&quot;]
            dt_html_full = img_item[&quot;dt_html_full&quot;]
            location_short = img_item[&quot;location_short&quot;]
            lat_str = img_item[&quot;lat_str&quot;]
            lon_str = img_item[&quot;lon_str&quot;]
            short_comment = img_item[&quot;path&quot;].stem

            if len(short_comment) &gt; 15:
                short_comment = short_comment[:15] + &quot;…&quot;

            if lat_str and lon_str:
                map_link_html = f&#039;&lt;div class=&quot;map-link&quot;&gt;&lt;a href=&quot;https://www.google.com/maps?q={lat_str},{lon_str}&quot; target=&quot;_blank&quot;&gt;Google Map&lt;/a&gt;&lt;/div&gt;&#039;
            else:
                map_link_html = &quot;&quot;

            if img_item[&quot;badge&quot;] == &quot;360&quot;:
                badge_html = &#039;&lt;span class=&quot;badge badge-360&quot;&gt;360°&lt;/span&gt;&#039;
            elif img_item[&quot;badge&quot;] == &quot;video&quot;:
                badge_html = &#039;&lt;span class=&quot;badge badge-video&quot;&gt;動画&lt;/span&gt;&#039;
            else:
                badge_html = &#039;&lt;span class=&quot;badge&quot;&gt;写真&lt;/span&gt;&#039;

            index_parts.append(f&quot;&quot;&quot;
&lt;div class=&quot;card&quot;&gt;
  &lt;a href=&quot;{viewer_name}&quot;&gt;
    &lt;img class=&quot;thumb-img&quot; src=&quot;thumb/{thumb_name}&quot; alt=&quot;{img_item[&#039;path&#039;].name}&quot;&gt;
  &lt;/a&gt;
  &lt;div class=&quot;datetime&quot;&gt;{dt_html_full}&lt;/div&gt;
  &lt;div class=&quot;location&quot;&gt;{location_short} {badge_html}&lt;/div&gt;
  {map_link_html}
  &lt;div class=&quot;filename&quot;&gt;{short_comment}&lt;/div&gt;
&lt;/div&gt;
&quot;&quot;&quot;)

        index_parts.append(&quot;&lt;/div&gt;\n&quot;)

    index_parts.append(INDEX_FOOT)

    # ==========================
    # 一覧ページ書き込み
    # ==========================
    with open(index_html, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
        f.write(&quot;&quot;.join(index_parts))

    print(&quot;サムネイル・一覧ページ:&quot;, index_html)
    print(&quot;個別ビューア / プレイヤー:&quot;, total_count, &quot;件生成しました。&quot;)


if __name__ == &quot;__main__&quot;:
    main()</code></pre></div></div>]]>
       </content>
    </entry>
    <entry>
        <title>RICOH THETAで撮影した360度写真と共に写真アルバムを作る</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=11082" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=11082</id>
        <published>2026-02-19T15:57:17+09:00</published>
        <updated>2026-02-20T12:07:09+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　9年ほど前に買ったリコー THETA S撮影中に三脚が倒れてレンズに傷が入り修理して分譲して手に入れたTHETA Vですが，撮影した写真データは，theta360[.]comにアップロードしてブログパーツと...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　9年ほど前に買った<a href="http://www.ujp.jp/modules/d3blog/details.php?bid=4901" rel="external">リコー THETA S</a><a href="http://www.ujp.jp/modules/d3blog/details.php?bid=5610" rel="external">撮影中に三脚が倒れてレンズに傷が入り修理</a>して<a href="http://www.ujp.jp/modules/d3blog/details.php?bid=5649" rel="external">分譲して手に入れたTHETA V</a>ですが，撮影した写真データは，theta360[.]comにアップロードしてブログパーツとして貼り付けていた．<br />　しかしいつの間にかサービス終了．<br /><br />　仕方ないので，静的HTMLを生成するpannellumを使って他のサイトに依存しないで表示させるHTMLを生成するプログラムを作ってみた．<br /><br />Pannellum<br /><a href="https://github.com/mpetroff/pannellum/releases" rel="external">https://github.com/mpetroff/pannellum/releases</a><br /><br />　今回は，このサイトからpannellum-2.5.6.zipをダウンロードし，その中からpannellum.jsとpannellum.cssファイルを取り出し，htmlと同じフォルダに配置し，以下のHTMLファイルも同じ場所に保存して，360度写真ファイル名部分を変更すれば良い．<br /><br />　使ったソースコードはこれ．<div class="xoopsCode"><pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ja&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;title&gt;THETA 360 Viewer&lt;/title&gt;

  &lt;!-- pannellum --&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;pannellum.css&quot;&gt;

  &lt;style&gt;
    body {
      margin: 0;
      font-family: sans-serif;
      background: #fafafa;
    }

    /* --- ヘッダ部分 --- */
    header {
      text-align: center;
      padding: 20px 10px;
    }

    /* 3ドット風の区切り線 */
    .dot-hr {
      border: none;
      text-align: center;
      margin: 10px auto;
    }
    .dot-hr:before {
      content: &quot;···&quot;;
      font-size: 20px;
      color: #999;
    }

    /* 戻るボタン */
    .back-button {
      display: inline-block;
      margin-top: 10px;
      padding: 8px 16px;
      background: #eee;
      border-radius: 6px;
      text-decoration: none;
      color: #333;
      font-size: 14px;
      border: 1px solid #ccc;
    }
    .back-button:hover {
      background: #ddd;
    }

    /* pannellum 表示領域 */
    #panorama {
      width: 100%;
      height: 500px;
    }
  &lt;/style&gt;
&lt;/head&gt;

&lt;body&gt;

&lt;header&gt;
  &lt;hr class=&quot;dot-hr&quot;&gt;
  &lt;h1&gt;マルチアルバム&lt;/h1&gt;
  &lt;hr class=&quot;dot-hr&quot;&gt;

  &lt;!-- 戻るボタン --&gt;
  &lt;a href=&quot;java script:history.back();&quot; class=&quot;back-button&quot;&gt;戻る&lt;/a&gt;
&lt;/header&gt;

&lt;div id=&quot;panorama&quot;&gt;&lt;/div&gt;

&lt;script src=&quot;pannellum.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
  pannellum.viewer(&#039;panorama&#039;, {
    type: &#039;equirectangular&#039;,
    panorama: &#039;サンライズ出雲シングルのパノラマ写真.jpg&#039;,
    autoLoad: true
  });
&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre></div><br />　できたコマンドラインツールで自動生成して作成したアルバムがこれ．<br /><br /><a href="http://www.ujp.jp/images/theta/20260206SunriseIzumo/20260206SunriseIzumo.html" rel="external">サンライズ出雲乗車記録2026.02.06</a><br /><br />　今回，自分用ツールなのだけど，これでデジカメの写真，スマホの写真，360度写真，動画を全部一緒にしてサイトで表示できるアルバムを作るPython3スクリプトを作ってみた．久しぶりにこういうのを作ったのでちょっと楽しかった．</div>]]>
       </content>
    </entry>
    <entry>
        <title>Brave Browserでページ全体のスクリーンショット</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=11075" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=11075</id>
        <published>2026-02-13T19:10:11+09:00</published>
        <updated>2026-02-13T19:43:42+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　見ているSUUMOのページを丸ごと保存したいと考えた．　ブラウザでページをPDFにして印刷すれば良いのだけど，スタイルシートの影響か，リクルート社が意図的にそうしているかわから...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　見ているSUUMOのページを丸ごと保存したいと考えた．<br /><br />　ブラウザでページをPDFにして印刷すれば良いのだけど，スタイルシートの影響か，リクルート社が意図的にそうしているかわからないけど，レイアウトが崩れてぐちゃぐちゃになる．<br />　調べるとBrave Browserのデベロッパーツールでスクリーンキャプチャー機能があるというので試してみた．<br /><br /><b><span style="font-size: x-large;">デベロッパーツール（検証）を使う</span></b><br /><br />・保存したいページで F12 キー（Macは Cmd+Option+I）を押して検証メニューを開く。<br />・Ctrl+Shift+P（Macは Cmd+Shift+P）を押してコマンドメニューを開く。<br />・screenshot と入力し、「Capture full site screenshot」を選択すると、ページ全体がPNGで保存されます。 <br /><br />　簡単だけど使用頻度から考えると手順を覚えるのが無理．．．でも何も入れないのが良いのかも．<br /><br /><b><span style="font-size: x-large;">拡張機能GoFullPageを使う</span></b><br /><br />　Chromeの拡張機能でGoFullPageをインストール．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14843&GoFullPage1.jpg" align="center" alt="" /></center><br /><br />　保存の際にA4ページサイズとかにするとページ替えの時に空白ができる．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14842&GoFullPage2.jpg" align="center" alt="" /></center><br />　１枚のイメージにするには，image formatをfull imageにすればよかった．<br /><br />　これで定期的にキャプチャーして，<a href="http://www.ujp.jp/modules/d3blog/details.php?bid=5487" rel="external">画像でdiff</a>にすれば，画像の変化した場所がわかるようになる．<br /><br />　早速画像でdiffを取ってみた．<br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14844&GoFullPage3.jpg" align="center" alt="" /></center><br />　今回はサイトで「並び順」を変更しただけの変化を可視化してみたのだけど，新着分のある場所だけが赤色で示されている．また，サイト下部にある広告・他の地域の物件などの動的に変更される部分も赤色になってる．検証は成功かな．</div>]]>
       </content>
    </entry>
    <entry>
        <title>Some of the requested messages no longer exist</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=10983" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=10983</id>
        <published>2025-11-28T10:34:59+09:00</published>
        <updated>2025-11-28T10:34:59+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　うちのメールサーバはIMAP4で運輸しているけど，パソコンのメーラーでたまに稀にSome of the requested messages no longer existというエラーダイアログがdていることがわかった．　これはIMAPサー...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　うちのメールサーバはIMAP4で運輸しているけど，パソコンのメーラーでたまに稀にSome of the requested messages no longer existというエラーダイアログがdていることがわかった．<br />　これはIMAPサーバへのクライアント同時接続が多すぎるという問題．<br /><br />　最近アカウント数や同時接続端末が増えたから，この接続数がオーバーするようだ．<br /><br />　取り急ぎ対応するには，Thunderbirdを使っていたら[アカウント設定]→[サーバー設定]→[詳細]と選ぶと次のダイアログが表示．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14712&ThunderbirdConnection.jpg" align="center" alt="" /></center><br />　[サーバーへの最大同時接続数]を減らすことでサーバへの同時接続数を減らすことができる．これはアカウント毎に行う必要がある．<br />　これによって同時に接続する数が減るので，メール取り込み動作が遅くなるかもしれない．<br /><br />　サーバ側のメモリやCPUが余裕があるなら，サーバ側の設定を変更する手もある．<br />　私のメールサーバはDovcotでIMAP4を構成しているので，次の場所に設定ファイルがある<br /><br /><div class="xoopsCode"><pre><code>/Library/Server/Mail/Config/dovecot/conf.d/10-master.conf</code></pre></div><br /><div class="xoopsCode"><pre><code>service imap {
  # Most of the memory goes to mmap()ing files. You may need to increase this
  # limit if you have huge mailboxes.
  #vsz_limit = $default_vsz_limit

  # Max. number of IMAP processes (connections)
  process_limit = 200🈁

  client_limit = 5
  service_count = 0
}

service pop3 {
  # Max. number of POP3 processes (connections)
  process_limit = 200🈁

  client_limit = 5
  service_count = 0
}</code></pre></div></div>]]>
       </content>
    </entry>
    <entry>
        <title>写真に写る人間の顔をモザイクにするPython3 OpenCV  mediapipe 横顔対応</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=10911" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=10911</id>
        <published>2025-10-15T12:25:02+09:00</published>
        <updated>2025-10-15T12:30:06+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　写真の会写真にモザイクを入れるPythonスクリプトを作ってみたけど，ライブラリmediapipeを使うとより良いというので使ってみた．引用：MediaPipeの顔検出モデルの特徴　MediaPipeの顔検出は...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　<a href="http://www.ujp.jp/modules/d3blog/details.php?bid=10910" rel="external">写真の会写真にモザイクを入れるPythonスクリプトを作ってみた</a>けど，ライブラリmediapipeを使うとより良いというので使ってみた．<br /><br />引用：<div class="xoopsQuote"><blockquote>MediaPipeの顔検出モデルの特徴<br /><br />　MediaPipeの顔検出は主に BlazeFace モデルをベースにしており、以下のような特性があります：<br /><br />・高速かつ軽量：モバイルGPUでもリアルタイム処理が可能<br />・6つのランドマーク検出：両目、鼻先、口、耳の位置を推定<br />・複数顔対応：1枚の画像に複数の顔があっても検出可能<br />・部分的な顔にも対応：顔の一部が隠れていても、ランドマークが十分に見えていれば検出可能</blockquote></div><br />　まずは環境構築から．<br />　顔認識をより正確にするためのmediapipeライブラリは，Pythonの環境を選ぶ模様．現時点での条件を確認．<br /><br />引用：<div class="xoopsQuote"><blockquote>mediapipeが公式に対応しているPythonのバージョンは、3.9、3.10、3.11、そして一部環境で3.12です。それ以外（3.13以降）は現時点では未対応です<br /></blockquote></div>　最初にOSのバージョンを確認．<br /><div class="xoopsCode"><pre><code>% sw_vers🆑

ProductName:		macOS
ProductVersion:		15.3.1
BuildVersion:		24D70
%</code></pre></div>　Pythonのバージョン．<br /><div class="xoopsCode"><pre><code>% python3 --version🆑
Python 3.14.0
%</code></pre></div>　Python 3.10.11を仮想環境で利用する．<br /><br />　環境変数を設定．<br /><div class="xoopsCode"><pre><code>export PYENV_ROOT=&quot;$HOME/.pyenv&quot;
export PATH=&quot;$PYENV_ROOT/bin:$PATH&quot;
eval &quot;$(pyenv init --path)&quot;
eval &quot;$(pyenv init -)&quot;</code></pre></div>　インストール．<br /><div class="xoopsCode"><pre><code>brew install pyenv
pyenv install 3.10.11</code></pre></div>　pyenvでバージョンを確認．<br /><div class="xoopsCode"><pre><code>% pyenv versions🆑

  system
* 3.10.11 (set by /Users/server/.pyenv/version)
  3.10.12
%</code></pre></div>　グローバルに設定．<br /><div class="xoopsCode"><pre><code>% pyenv global 3.10.11🆑
$</code></pre></div>　バージョンを確認．<br /><div class="xoopsCode"><pre><code>% python3 --version🆑
Python 3.10.11</code></pre></div><br /><div class="xoopsCode"><pre><code>pip install --upgrade pip setuptools wheel🆑
pip install mediapipe opencv-contrib-python🆑</code></pre></div>　インストール確認．<br /><div class="xoopsCode"><pre><code>(mp_env) % pip list|grep mediapipe🆑
mediapipe             0.10.21🈁
(mp_env) % pip list|grep opencv🆑
opencv-contrib-python 4.11.0.86🈁
(mp_env) %</code></pre></div>　face_detection_yunet_2023mar.onnxファイルがあるか確認し，無かったらダウンロードする．<br /><div class="xoopsCode"><pre><code>$ ls /Users/ujpadmin/bin/mp_env/lib/python3.10/site-packages/cv2/data/face_detection_yunet_20
23mar.onnx🆑

ls: cannot access &#039;/Users/ujpadmin/bin/mp_env/lib/python3.10/site-packages/cv2/data/face_dete
ction_yunet_2023mar.onnx&#039;: No such file or directory
(mp_env) %</code></pre></div>　無かったのでダウンロード．<br /><div class="xoopsCode"><pre><code>$ curl -L -o face_detection_yunet_2023mar.onnx https://github.com/opencv/opencv_zoo/raw/main/mo
dels/face_detection_yunet/face_detection_yunet_2023mar.onnx
$ mv face_detection_yunet_2023mar.onnx /Users/ujpadmin/bin/mp_env/lib/python3.10/site-packages/cv2/data/.</code></pre></div>　※ユーザujpadminの下に作成した仮想環境mp_envの配下にコピー．<br /><br />　環境を作成したら，以下のスクリプトを保存．<br /><div class="xoopsCode"><pre><code>% cat face_maskerStrongMozaic2.py
import cv2
import os
import sys

def apply_mosaic(image, x, y, w, h, k=30):
    face_roi = image[y:y+h, x:x+w]
    small = cv2.resize(face_roi, (max(1, w//k), max(1, h//k)))
    mosaic = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)
    image[y:y+h, x:x+w] = mosaic
    return image

def process_image(image_path, model_path):
    image = cv2.imread(image_path)
    if image is None:
        print(f&quot;Failed to load: {image_path}&quot;)
        return

    detector = cv2.FaceDetectorYN.create(model_path, &quot;&quot;, (image.shape[1], image.shape[0]))
    detector.setInputSize((image.shape[1], image.shape[0]))
    detector.setScoreThreshold(0.3)

    success, faces = detector.detect(image)
    if success and faces is not None:
        for face in faces:
            x, y, w, h = map(int, face[:4])
            image = apply_mosaic(image, x, y, w, h, k=30)

    # 保存先は元画像と同じディレクトリ
    dir_name = os.path.dirname(image_path)
    base_name = os.path.basename(image_path)
    output_path = os.path.join(dir_name, &quot;masked_&quot; + base_name)
    cv2.imwrite(output_path, image)
    print(f&quot;Saved: {output_path}&quot;)

def main(input_dir):
    if not os.path.isdir(input_dir):
        print(f&quot;Error: {input_dir} is not a directory.&quot;)
        return

    # モデルパスを構築
    cv2_data_dir = os.path.join(os.path.dirname(cv2.__file__), &quot;data&quot;)
    model_path = os.path.join(cv2_data_dir, &quot;face_detection_yunet_2023mar.onnx&quot;)

    # 対象画像ファイルを処理
    for filename in os.listdir(input_dir):
        if filename.lower().endswith((&quot;.jpg&quot;, &quot;.jpeg&quot;, &quot;.png&quot;, &quot;.bmp&quot;)):
            image_path = os.path.join(input_dir, filename)
            process_image(image_path, model_path)

if __name__ == &quot;__main__&quot;:
    if len(sys.argv) &lt; 2:
        print(&quot;Usage: python face_maskerStrongMozaic2.py &lt;image_directory&gt;&quot;)
    else:
        main(sys.argv[1])</code></pre></div>　k=30の部分の数字を変更すると，モザイクのサイズが変更できる．<br />　実行してみる．<br /><div class="xoopsCode"><pre><code>(mp_env) server@apollo20250119 bin % python3 face_maskerStrongMozaic2.py /Users/ujpadmin/Documents
(mp_env) server@apollo20250119 bin %</code></pre></div><br />　モザイクを付与された画像はこちら．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14542&face_maskerStrongMozaic2_1.jpg" align="center" alt="" /></center><br />　おお．前回より圧倒的に正確．少し誤検知はあるけど，未検知は無い．<br /><br />　mediapipeを使ってないバージョンはこれ．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14541&face_mosaic.jpg" align="center" alt="" /></center><br /><br />　次に横を向いている人ばかりの写真．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14543&face_maskerStrongMozaic2_2.jpg" align="center" alt="" /></center><br />　完璧だな．</div>]]>
       </content>
    </entry>
</feed>