Loading...
写真を90年代風に加工できるツールが登場!
写真加工JavaScript生成AI

写真を90年代風に加工できるツールが登場!

AIで生成した画像を90年代風にしてみた

2025-08-1548分で読める

📸 90年代の空気を現代に再現

AIで生成された高解像度の画像を、そのままSNSに投稿しても十分に映えます。ですが、最近じわじわ人気を集めているのは「90年代風」加工。フィルム写真や初期デジカメ特有の、眠い黒や黄ばみ、ノイズ、そして日付スタンプが加わると、なぜか“エモい”仕上がりに早変わりします。

今回は、ブラウザだけで完結する90年代風加工ツールを実装してみました。React + Canvas API を使って、インストール不要で誰でも試せるようにしています。

今回はこの画像を 加工前の画像

こういう感じにしました。 加工後の画像


🛠️ 実装のポイント

  • 彩度を抑えて黄ばみを追加 → プリント写真の退色感を再現
  • 黒つぶれしない影 → 眠いシャドウで90年代のデジカメ感
  • グリーンかぶり → 安価なスキャナっぽい雰囲気
  • フィルム粒子 → 粗めのグレインをRGB分離で追加
  • 日付スタンプ → 右下にオレンジ系フォントで配置

👨‍💻 技術ワンポイント解説

1. 彩度調整 (Saturation)

const satKeep = cfg.satBase - 0.3 * intensity;
const avgBlend = 1 - satKeep;
r = r * satKeep + avg * avgBlend;
g = g * satKeep + avg * avgBlend;
b = b * satKeep + avg * avgBlend;

👉 彩度を落として平均色に寄せることで「眠たい色味」を再現しています。

2. ハイライト/シャドウ別の色補正 (Split Toning)

const luma = 0.2126*r + 0.7152*g + 0.0722*b;
const shadow = Math.max(0, 1 - luma / 128);
const highlight = Math.max(0, (luma - 128) / 127);

r += cfg.highlight.r * highlight * intensity;
g += cfg.highlight.g * highlight * intensity;
b += cfg.highlight.b * highlight * intensity;

👉 明るい部分は黄色っぽく、暗い部分は緑や青を混ぜて「フィルム写真の雰囲気」を出しています。

3. フィルム粒子 (Film Grain)

const amp = 40 * filmGrain;
const n = 128 + (Math.random()*2 - 1) * amp;
gd[i] = gd[i+1] = gd[i+2] = n;

👉 ノイズを生成し、RGBに分離してズラすことで「ザラついたフィルム感」を演出。

4. 色収差 (Chromatic Aberration)

const idxR = (y*width + Math.min(width-1, x+1))*4;
const idxB = (y*width + Math.max(0, x-1))*4;
out[idx] = src[idxR];
out[idx+2] = src[idxB+2];

👉 赤を右、青を左に1pxずらすことで「安いレンズや古いスキャナのにじみ」を再現。

5. 日付スタンプ

ctx.font = `${fontSize}px monospace`;
ctx.fillStyle = gradText;
ctx.fillText(stamp, width - pad, height - pad);

👉 オレンジ〜黄色のグラデーション+ドロップシャドウで、まさに90年代デジカメ風の雰囲気を作り出しています。


👀 実際のビフォー・アフター

  • before: 現代的でシャープなAI生成写真
  • after: 黄ばんだプリント感、眠い黒、粗い粒子、そして日付スタンプ!

渋谷の交差点を写した写真も、加工後は「2001年のセンター街スナップ」に見えてしまうほど。SNSにアップすれば「懐かしい!」「プリクラ感ある!」と盛り上がること間違いなしです。

before/after画像


💻 コード全文

以下が実装コードです。React (Next.js) + Canvas API で、完全にブラウザ内で処理しています。画像はサーバーへ送信されないため安心して利用できます。

// Photo90sEditor.tsx
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { Upload, Download, SlidersHorizontal, Image as ImageIcon, Palette, CalendarDays } from "lucide-react";

type EffectOptions = {
  intensity: number; // 0..1 overall intensity
  noise: number; // 0..1 noise strength
};

type TonePresetKey = "blue" | "warm" | "green";
type TonePreset = {
  name: string;
  highlight: { r: number; g: number; b: number };
  shadow: { r: number; g: number; b: number };
  halation: string; // rgba
  satBase: number; // base saturation keep
  contrastGain: number; // contrast gain per intensity
  halationScale?: number; // amplify halation screen blend
  shadowLift?: number; // lift shadows amount baseline
};

const PRESETS: Record<TonePresetKey, TonePreset> = {
  warm: {
    name: "Warm 90s",
    highlight: { r: 14, g: 6, b: -4 },
    shadow: { r: -2, g: 6, b: 4 },
    halation: "rgba(255, 160, 80, 0.40)",
    satBase: 0.65,
    contrastGain: 0.06,
    halationScale: 1.35,
    shadowLift: 8,
  },
  green: {
    name: "Green 90s",
    highlight: { r: -2, g: 12, b: 4 },
    shadow: { r: -6, g: 8, b: 6 },
    halation: "rgba(80, 200, 140, 0.35)",
    satBase: 0.7,
    contrastGain: 0.1,
    halationScale: 1.0,
    shadowLift: 5,
  },
  blue: {
    name: "Blue 90s",
    highlight: { r: -2, g: 6, b: 14 },
    shadow: { r: -8, g: 2, b: 12 },
    halation: "rgba(60, 130, 255, 0.35)",
    satBase: 0.68,
    contrastGain: 0.12,
    halationScale: 1.0,
    shadowLift: 3,
  },
};

function formatYYMMDD(d = new Date()) {
  const yy = String(d.getFullYear() % 100).padStart(2, "0");
  const mm = String(d.getMonth() + 1).padStart(2, "0");
  const dd = String(d.getDate()).padStart(2, "0");
  return `${yy}.${mm}.${dd}`;
}

export default function Photo90sEditor() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const [imageURL, setImageURL] = useState<string | null>(null);
  const [processing, setProcessing] = useState(false);
  const [options, setOptions] = useState<EffectOptions>({ intensity: 0.5, noise: 0.25 });
  const [isDragging, setIsDragging] = useState(false);
  const [preset, setPreset] = useState<TonePresetKey>("blue");
  const inputRef = useRef<HTMLInputElement | null>(null);
  const [showDate, setShowDate] = useState(true);
  const [greenBlend, setGreenBlend] = useState(0.2);
  const [filmGrain, setFilmGrain] = useState(0.35);

  useEffect(() => {
    return () => {
      if (imageURL) URL.revokeObjectURL(imageURL);
    };
  }, [imageURL]);

  const handleFile = useCallback((file: File) => {
    if (!file || !file.type.startsWith("image")) return;
    const url = URL.createObjectURL(file);
    setImageURL((prev) => {
      if (prev) URL.revokeObjectURL(prev);
      return url;
    });
  }, []);

  const onChangeFile = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
    (e) => {
      const f = e.target.files?.[0];
      if (f) handleFile(f);
    },
    [handleFile]
  );

  const onDrop: React.DragEventHandler<HTMLDivElement> = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);
    const f = e.dataTransfer.files?.[0];
    if (f) handleFile(f);
  };

  const onDragOver: React.DragEventHandler<HTMLDivElement> = (e) => {
    e.preventDefault();
    e.stopPropagation();
  };

  const onDragEnter: React.DragEventHandler<HTMLDivElement> = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(true);
  };

  const onDragLeave: React.DragEventHandler<HTMLDivElement> = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);
  };

  const apply90s = useCallback(async () => {
    if (!imageURL || !canvasRef.current) return;
    setProcessing(true);
    try {
      const img = await new Promise<HTMLImageElement>((resolve, reject) => {
        const i = new Image();
        i.onload = () => resolve(i);
        i.onerror = reject;
        i.src = imageURL;
      });

      const maxDim = 2000; // keep memory reasonable for export
      let { width, height } = img;
      const scale = Math.min(1, maxDim / Math.max(width, height));
      width = Math.round(width * scale);
      height = Math.round(height * scale);

      const canvas = canvasRef.current;
      const ctx = canvas.getContext("2d");
      if (!ctx) return;
      canvas.width = width;
      canvas.height = height;

      // Fill white if the image has transparency
      ctx.fillStyle = "#fff";
      ctx.fillRect(0, 0, width, height);

      ctx.drawImage(img, 0, 0, width, height);

      const imageData = ctx.getImageData(0, 0, width, height);
      const data = imageData.data;

      const intensity = Math.max(0, Math.min(1.5, options.intensity));
      const noiseAmt = Math.max(0, Math.min(1, options.noise));

      // Pixel process: lower saturation, split-toning, mild contrast, noise
      const cfg = PRESETS[preset];
      const satKeep = cfg.satBase - 0.3 * intensity; // base → lower when intensity increases
      const avgBlend = 1 - satKeep; // to pull toward grayscale

      // Contrast factor (simple curve)
      const contrast = 1 + cfg.contrastGain * Math.min(1, intensity); // preset-based gain
      const offset = 128 * (1 - contrast);

      // Subtle S-curve helper
      const sCurve = (v: number) => {
        const t = (v - 128) / 128;
        const curved = t + 0.25 * intensity * (t - t * t * t); // gentle S-curve
        return curved * 128 + 128;
      };

      for (let i = 0; i < data.length; i += 4) {
        let r = data[i];
        let g = data[i + 1];
        let b = data[i + 2];

        const avg = (r + g + b) / 3;
        r = r * satKeep + avg * avgBlend;
        g = g * satKeep + avg * avgBlend;
        b = b * satKeep + avg * avgBlend;

        // Split toning based on preset
        const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
        const shadow = Math.max(0, 1 - luma / 128); // 0..1
        const highlight = Math.max(0, (luma - 128) / 127); // 0..1
        r += cfg.highlight.r * highlight * intensity;
        g += cfg.highlight.g * highlight * intensity;
        b += cfg.highlight.b * highlight * intensity;
        r += cfg.shadow.r * shadow * intensity;
        g += cfg.shadow.g * shadow * intensity;
        b += cfg.shadow.b * shadow * intensity;

        // Optional green cast blend (cheap scanner vibe)
        if (greenBlend > 0) {
          const gb = Math.min(1, Math.max(0, greenBlend));
          const gcfg = PRESETS.green;
          r += (gcfg.highlight.r * highlight + gcfg.shadow.r * shadow) * intensity * gb * 0.7;
          g += (gcfg.highlight.g * highlight + gcfg.shadow.g * shadow) * intensity * gb * 0.7;
          b += (gcfg.highlight.b * highlight + gcfg.shadow.b * shadow) * intensity * gb * 0.7;
        }

        // mild contrast
        r = r * contrast + offset;
        g = g * contrast + offset;
        b = b * contrast + offset;

        // gentle S-curve on each channel
        r = sCurve(r);
        g = sCurve(g);
        b = sCurve(b);

        // lift shadows (make blacks softer)
        const lift = (cfg.shadowLift ?? 4) * Math.min(1.2, intensity);
        const shadowW = shadow;
        r += lift * shadowW;
        g += lift * shadowW;
        b += lift * shadowW;

        // noise per channel
        const n = (Math.random() * 2 - 1) * 40 * noiseAmt; // +/- up to ~40
        r += n;
        g += n * 0.9;
        b += n * 1.1;

        data[i] = r < 0 ? 0 : r > 255 ? 255 : r;
        data[i + 1] = g < 0 ? 0 : g > 255 ? 255 : g;
        data[i + 2] = b < 0 ? 0 : b > 255 ? 255 : b;
      }

      ctx.putImageData(imageData, 0, 0);

      // Subtle chromatic aberration (shift R right, B left by 1px)
      if (intensity > 0.2) {
        const srcData = ctx.getImageData(0, 0, width, height);
        const src = srcData.data;
        const out = new Uint8ClampedArray(src); // copy
        const shift = 1; // px
        for (let y = 0; y < height; y++) {
          for (let x = 0; x < width; x++) {
            const idx = (y * width + x) * 4;
            const idxR = (y * width + Math.min(width - 1, x + shift)) * 4;
            const idxB = (y * width + Math.max(0, x - shift)) * 4;
            out[idx] = src[idxR]; // R
            // G stays
            out[idx + 2] = src[idxB + 2]; // B
          }
        }
        ctx.putImageData(new ImageData(out, width, height), 0, 0);
      }

      // Vignette overlay (multiply)
      const grad = ctx.createRadialGradient(
        width / 2,
        height / 2,
        Math.min(width, height) * 0.2,
        width / 2,
        height / 2,
        Math.max(width, height) * 0.75
      );
      grad.addColorStop(0, "rgba(0,0,0,0)");
      grad.addColorStop(1, `rgba(0,0,0,${0.35 * intensity})`);
      ctx.save();
      ctx.globalCompositeOperation = "multiply";
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, width, height);
      ctx.restore();

      // Subtle scanline effect (every few rows slightly dark)
      if (intensity > 0.4) {
        ctx.save();
        ctx.globalAlpha = 0.06 * (intensity - 0.4);
        ctx.fillStyle = "#000";
        for (let y = 0; y < height; y += 3) {
          ctx.fillRect(0, y, width, 1);
        }
        ctx.restore();
      }

      // Film grain overlay with RGB split
      if (filmGrain > 0.01) {
        const g = document.createElement("canvas");
        g.width = width;
        g.height = height;
        const gctx = g.getContext("2d");
        if (gctx) {
          const gImg = gctx.createImageData(width, height);
          const gd = gImg.data;
          const amp = 40 * filmGrain; // amplitude of grain
          for (let i = 0; i < gd.length; i += 4) {
            const n = 128 + (Math.random() * 2 - 1) * amp; // base gray
            gd[i] = gd[i + 1] = gd[i + 2] = n;
            gd[i + 3] = 255;
          }
          gctx.putImageData(gImg, 0, 0);

          const tint = (color: string, dx: number, dy: number, alpha: number) => {
            const t = document.createElement("canvas");
            t.width = width;
            t.height = height;
            const tctx = t.getContext("2d");
            if (!tctx) return;
            tctx.drawImage(g, 0, 0);
            tctx.globalCompositeOperation = "source-in";
            tctx.fillStyle = color;
            tctx.fillRect(0, 0, width, height);
            ctx.save();
            ctx.globalCompositeOperation = "overlay" as GlobalCompositeOperation;
            ctx.globalAlpha = alpha;
            ctx.drawImage(t, dx, dy);
            ctx.restore();
          };

          const baseAlpha = 0.18 + 0.18 * Math.min(1, intensity);
          tint("rgba(255,0,0,1)", 1, 0, baseAlpha * 0.9);
          tint("rgba(0,255,0,1)", -1, 0, baseAlpha);
          tint("rgba(0,0,255,1)", 0, 1, baseAlpha * 0.85);
        }
      }

      // Bloom/Halation: blur + preset tint, then screen blend
      if (intensity > 0.2) {
        const tmp = document.createElement("canvas");
        tmp.width = width;
        tmp.height = height;
        const tctx = tmp.getContext("2d");
        if (tctx) {
          tctx.filter = `blur(${Math.round(6 + 10 * intensity)}px)`;
          tctx.drawImage(canvas, 0, 0);
          tctx.filter = "none";
          tctx.globalCompositeOperation = "multiply";
          tctx.fillStyle = cfg.halation;
          tctx.fillRect(0, 0, width, height);

          ctx.save();
          ctx.globalCompositeOperation = "screen";
          const hscale = cfg.halationScale ?? 1.0;
          ctx.globalAlpha = (0.14 + 0.12 * intensity) * hscale;
          ctx.drawImage(tmp, 0, 0);
          ctx.restore();

          // Extra green halation blend if requested
          if (greenBlend > 0.01) {
            tctx.globalCompositeOperation = "multiply";
            tctx.fillStyle = PRESETS.green.halation;
            tctx.fillRect(0, 0, width, height);
            ctx.save();
            ctx.globalCompositeOperation = "screen";
            ctx.globalAlpha = (0.08 + 0.08 * intensity) * Math.min(1, greenBlend);
            ctx.drawImage(tmp, 0, 0);
            ctx.restore();
          }
        }
      }

      // Date stamp (bottom-right) with slight gloss
      if (showDate) {
        const stamp = formatYYMMDD();
        ctx.save();
        const pad = Math.max(8, Math.round(Math.min(width, height) * 0.015));
        const fontSize = Math.max(10, Math.round(Math.min(width, height) * 0.028));
        ctx.font = `${fontSize}px monospace`;
        ctx.textBaseline = "bottom";
        ctx.textAlign = "right";

        // Glow highlight
        ctx.save();
        ctx.globalCompositeOperation = "screen";
        ctx.filter = `blur(${Math.max(0.6, 1.0 * intensity)}px)`;
        ctx.globalAlpha = 0.35;
        ctx.fillStyle = "#ffffff";
        ctx.fillText(stamp, width - pad, height - pad - Math.round(fontSize * 0.08));
        ctx.restore();

        // Gradient fill for glossy feel
        const gradText = ctx.createLinearGradient(0, height - pad - fontSize, 0, height - pad);
        gradText.addColorStop(0, "#ffd8a6");
        gradText.addColorStop(0.5, "#ffb347");
        gradText.addColorStop(1, "#ff9f1a");
        ctx.fillStyle = gradText;

        // Shadow for readability
        ctx.shadowColor = "rgba(0,0,0,0.55)";
        ctx.shadowBlur = 0;
        ctx.shadowOffsetX = 1;
        ctx.shadowOffsetY = 1;
        ctx.fillText(stamp, width - pad, height - pad);
        ctx.restore();
      }
    } finally {
      setProcessing(false);
    }
  }, [imageURL, options.intensity, options.noise, preset, showDate, greenBlend, filmGrain]);

  useEffect(() => {
    if (imageURL) apply90s();
  }, [imageURL, apply90s]);

  // Auto-apply on option/preset changes (debounced)
  useEffect(() => {
    if (!imageURL) return;
    const id = setTimeout(() => apply90s(), 180);
    return () => clearTimeout(id);
  }, [options, preset, imageURL, showDate, greenBlend, filmGrain, apply90s]);

  const onDownload = useCallback(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const url = canvas.toDataURL("image/jpeg", 0.92);
    const a = document.createElement("a");
    a.href = url;
    a.download = `photo90s_${Date.now()}.jpg`;
    a.click();
  }, []);

  return (
    <>
      <section className="modern-card p-4 sm:p-6">
        <div className="flex flex-col gap-4">
          <div className="flex flex-col md:flex-row md:items-center gap-3">
            <div className="flex flex-wrap items-center gap-3">
            <label className="modern-button cursor-pointer inline-flex items-center gap-2 w-full sm:w-auto justify-center">
                <Upload className="h-4 w-4" />
                画像を選択
                <input
                  type="file"
                  accept="image/*"
                  className="hidden"
                  onChange={onChangeFile}
                  ref={inputRef}
                />
              </label>
              <button
                className="modern-button inline-flex items-center gap-2 disabled:opacity-60 w-full sm:w-auto justify-center"
                onClick={onDownload}
                disabled={!imageURL || processing}
              >
                <Download className="h-4 w-4" />
                ダウンロード
              </button>
            </div>

            <div className="md:ml-auto">
              <div className="flex flex-col md:flex-row md:flex-wrap md:items-center gap-3 md:gap-4 text-sm text-muted-foreground">
                <div className="flex items-center gap-2 min-w-0">
                  <Palette className="h-4 w-4" />
                  <div className="inline-flex rounded-md overflow-hidden border border-muted-foreground/20 max-w-full overflow-x-auto whitespace-nowrap">
                    {(["blue","warm","green"] as TonePresetKey[]).map((k) => (
                      <button
                        key={k}
                        onClick={() => setPreset(k)}
                        className={`px-3 py-1.5 text-xs ${preset === k ? "bg-primary text-white" : "bg-background hover:bg-muted"}`}
                        aria-pressed={preset === k}
                      >
                        {PRESETS[k].name}
                      </button>
                    ))}
                  </div>
                </div>
                <div className="flex items-center gap-2 min-w-[220px]">
                  <SlidersHorizontal className="h-4 w-4" />
                  <span className="whitespace-nowrap">効果</span>
                  <input
                    aria-label="効果の強さ"
                    type="range"
                    min={0}
                    max={1.5}
                    step={0.01}
                    value={options.intensity}
                    onChange={(e) => setOptions((o) => ({ ...o, intensity: Number(e.target.value) }))}
                    className="w-32"
                  />
                  <span className="tabular-nums w-12 text-right">{Math.round(options.intensity * 100)}%</span>
                </div>
                <div className="flex items-center gap-2 min-w-[220px]">
                  <ImageIcon className="h-4 w-4" />
                  <span className="whitespace-nowrap">ノイズ</span>
                  <input
                    aria-label="ノイズの強さ"
                    type="range"
                    min={0}
                    max={1}
                    step={0.01}
                    value={options.noise}
                    onChange={(e) => setOptions((o) => ({ ...o, noise: Number(e.target.value) }))}
                    className="w-32"
                  />
                  <span className="tabular-nums w-10 text-right">{Math.round(options.noise * 100)}%</span>
                </div>
                <div className="flex items-center gap-2 min-w-[200px]">
                  <span className="whitespace-nowrap">緑かぶり</span>
                  <input
                    aria-label="緑かぶり"
                    type="range"
                    min={0}
                    max={1}
                    step={0.01}
                    value={greenBlend}
                    onChange={(e) => setGreenBlend(Number(e.target.value))}
                    className="w-32"
                  />
                  <span className="tabular-nums w-10 text-right">{Math.round(greenBlend * 100)}%</span>
                </div>
                <div className="flex items-center gap-2 min-w-[220px]">
                  <span className="whitespace-nowrap">粒子</span>
                  <input
                    aria-label="フィルム粒子"
                    type="range"
                    min={0}
                    max={1}
                    step={0.01}
                    value={filmGrain}
                    onChange={(e) => setFilmGrain(Number(e.target.value))}
                    className="w-32"
                  />
                  <span className="tabular-nums w-10 text-right">{Math.round(filmGrain * 100)}%</span>
                </div>
                <div className="flex items-center gap-2">
                  <CalendarDays className="h-4 w-4" />
                  <label className="inline-flex items-center gap-2 cursor-pointer select-none">
                    <input
                      type="checkbox"
                      className="accent-primary"
                      checked={showDate}
                      onChange={(e) => setShowDate(e.target.checked)}
                    />
                    <span>日付スタンプ</span>
                  </label>
                </div>
              </div>
            </div>
          </div>

          {/* Drop area / Preview */}
          {!imageURL ? (
            <div
              onDrop={onDrop}
              onDragOver={onDragOver}
              onDragEnter={onDragEnter}
              onDragLeave={onDragLeave}
              onClick={() => inputRef.current?.click()}
              className={`h-64 grid place-items-center rounded-lg border-2 border-dashed transition-colors ${
                isDragging ? "border-primary/60 bg-primary/5" : "border-muted-foreground/30"
              }`}
              role="button"
              aria-label="ここに画像をドラッグ&ドロップ、またはクリックして選択"
              tabIndex={0}
            >
              <div className="flex flex-col items-center gap-2 text-muted-foreground text-sm">
                <ImageIcon className="h-8 w-8" />
                <p className="text-center leading-relaxed">ここをクリックしてアップロード<br className="hidden sm:block" />またはドラッグ&ドロップ</p>
              </div>
            </div>
          ) : (
            <div className="w-full overflow-auto rounded-lg bg-background">
              <canvas ref={canvasRef} className="max-w-full h-auto block mx-auto" />
            </div>
          )}
        </div>
      </section>

      <section className="text-xs text-muted-foreground">
        <ul className="list-disc pl-5 space-y-1">
          <li>処理はすべてブラウザ内で完結し、画像はサーバーへ送信されません。</li>
          <li>大きな画像はメモリ節約のため最大2000pxにリサイズして処理します。</li>
          <li>保存形式はJPEGです。透明部分がある場合は白で塗りつぶされます。</li>
        </ul>
      </section>
    </>
  );
}


🚀 まとめ

  • 「写ルンです」や「初期デジカメ」風の質感をブラウザだけで再現可能
  • AI生成画像 × レトロ加工 でSNSバズを狙える
  • 技術的には Canvas API のピクセル操作で完結

レトロブームとAI画像の掛け算で、次のトレンドは「平成レトロ風AIフォト」かもしれません。気になった方はぜひコードを参考に、あなたの写真やAI画像を90年代風に加工してみてください!

こちらからお楽しみいただけます!

PR
この記事が役に立ったら:

developer note 記事

Sunoで演歌を作ってみた話 〜AIが奏でる昭和の哀愁〜
Suno / music / 生成AI

最近AI音楽生成サービス「Suno」を使って遊んでいるのですが、思い付きで演歌を作れるか実験しました。

2025-09-21

Sunoで演歌を作ってみた話 〜AIが奏でる昭和の哀愁〜

写真を90年代風に加工できるツールが登場!
写真加工 / JavaScript / 生成AI

AIで生成した画像を90年代風にしてみた

2025-08-15

写真を90年代風に加工できるツールが登場!

WindowsにLatentSyncで大苦戦!AIリップシンクをローカルPCで動かそうとしてエラーの地獄を見た全記録
AI / LatentSync / wsl

「ローカルPCで手軽にAIリップシンクを楽しみたい!」そんな甘い夢を見て、巷で話題の「LatentSync」をインストールしようとしたのが全ての始まりでした。まさか、これが数々のエラーという名の強敵と戦い、黒い画面(ターミナル)という名のダンジョンを彷徨う、壮大な冒険になるとは…この記事は、その血と汗と涙の全記録です。

2025-08-15

WindowsにLatentSyncで大苦戦!AIリップシンクをローカルPCで動かそうとしてエラーの地獄を見た全記録

2025年、AIが「ヤバい」ことになっている件について
AI / 最新技術 / トレンド

もはやSFではない。音楽作成、新薬開発、未知の物理法則の発見まで、最新AIの驚くべき成果を完全解説。

2025-08-15

2025年、AIが「ヤバい」ことになっている件について

構造化XMLタグ完全ガイド - AIプロンプトを劇的に改善する技術
プロンプト / XML

AI時代の新常識「何を言うか」より「どう伝えるか」が決定的な差を生む。構造化XMLタグは、あなたのプロンプトを次のレベルに押し上げる強力な武器です。

2025-08-13

構造化XMLタグ完全ガイド - AIプロンプトを劇的に改善する技術

GPT-5プロンプトガイド: 次世代AIモデルを最大限活用する方法
OpenAI / GPT-5

OpenAIが発表したGPT-5は、エージェント的タスク実行、コーディング、推論能力、そして操作性において大幅な向上を実現したフラグシップモデルです。本記事では、GPT-5の潜在能力を最大限引き出すためのプロンプト技術について、実用的な観点から解説します。

2025-08-13

GPT-5プロンプトガイド: 次世代AIモデルを最大限活用する方法

SunoでフューチャーファンクをDIY!完全解説ガイド
生成AI / Suno / 楽曲

こんにちは!今回は、AI音楽生成ツール「Suno」を使って、ノスタルジックな80年代風フューチャーファンクトラックを作成した体験記をお届けします。プロンプトエンジニアリングのコツから、フューチャーファンクの魅力まで、詳しく解説していきます。

2025-08-12

SunoでフューチャーファンクをDIY!完全解説ガイド

【やってみた】Claude Desktop × Browser MCPでXポスト自動化!AI操作の可能性に驚きと感動!
AI / 自動化 / Claude / mcp

こんにちは!今回は、ずっと気になっていた「AIにX(旧Twitter)への投稿を自動化させる」というチャレンジをしてみました。

2025-08-12

【やってみた】Claude Desktop × Browser MCPでXポスト自動化!AI操作の可能性に驚きと感動!

OpenAI GPT OSS:新しいオープンソースモデルファミリー - 詳細要約
GPT-5 / AI / 創造 / 生産性 / 未来

OpenAIが初のオープンソースモデルファミリー「GPT OSS」をリリースしました。これは推論、エージェント型タスク、多様な開発用途に設計された待望のオープンウェイトモデルです。

2025-08-10

OpenAI GPT OSS:新しいオープンソースモデルファミリー - 詳細要約

GPT-5超速報:爆速創造時代の始まり
GPT-5 / AI / 創造 / 生産性 / 未来

ヤバいAI、GPT-5がキタ!あなたの「ひらめき💡」を「カタチ✨」にする最強の相棒が爆誕。仕事も遊びも爆速になる未来は、もうここにある。速さはAIがくれる。その先にある「信頼」と「物語」を創るのは、いつだって人間だ。

2025-08-09

GPT-5超速報:爆速創造時代の始まり

Gemini Code Assist、2025年夏アップデートで「エージェントモード」とIDE機能強化を発表!開発がさらに加速
Google / Gemini / Code Assist / エージェントモード / IDE

GoogleのAIコーディングアシスタントであるGemini Code Assistは、個人の開発者から企業まで、すべての方のコーディングをさらに高速化する画期的なアップデートを2025年7月に発表しました。今回の目玉は、AIがコードベース全体を理解し、複雑なタスクを実行する「エージェントモード」の登場、そして開発環境(IDE)の使いやすさを向上させる様々な機能強化です。

2025-08-08

Gemini Code Assist、2025年夏アップデートで「エージェントモード」とIDE機能強化を発表!開発がさらに加速

Browser MCP徹底解説:AI(Claude)がX(旧Twitter)に自動投稿する仕組みと手順
Claude / Browser MCP / X

Browser MCPを活用してAIがWebブラウザを操作し、X(旧Twitter)へ自動的に投稿を行うための詳細な設定方法と具体的な利用例を徹底的に解説します。

2025-08-01

Browser MCP徹底解説:AI(Claude)がX(旧Twitter)に自動投稿する仕組みと手順

Gemini APIで画像を解析し、Next.jsアプリに組み込む方法【2024年版】
Next.js / Gemini API / Vercel AI SDK

Google Gemini APIとVercel AI SDKを使って、画像から情報を抽出するアプリを構築しよう。

2025-07-27

Gemini APIで画像を解析し、Next.jsアプリに組み込む方法【2024年版】

機械学習を用いてドラゴンクエスト12の発売日を予測
Python

ちょっとしたプログラミングの遊びをしてみたくて、機械学習を使ってドラゴンクエスト12の発売日を予測してみることにしました。

2024-05-14

機械学習を用いてドラゴンクエスト12の発売日を予測

ESP32とDHT11を使って温度と湿度を測定し、AWS Lambda経由でX(Twitter)に投稿する
Electronics

Freenove ESP32-WROOMボードとDHT11温湿度センサーを使用して、環境の温度と湿度を測定し、そのデータをAWS Lambdaを介してX(Twitter)に自動投稿してみました。

2024-04-05

ESP32とDHT11を使って温度と湿度を測定し、AWS Lambda経由でX(Twitter)に投稿する

年に一度で忘れがちのiOS Distribution Certificate証明書の更新メモ
iOS

毎年、現在ご利用のiOS Distribution証明書の有効期限まであと30日になりました。以降、この証明書は無効となります。新しい証明書を生成するには、サインインして「Certificates, Identifiers &amp; Profiles」(証明書、ID、プロファイル)にアクセスしてください。とAppleからご案内が来るのですが、年に1度しか対応しないのですぐ忘れてしまうためメモいたしました

2024-01-05

年に一度で忘れがちのiOS Distribution Certificate証明書の更新メモ

Xcode15で作成したアプリにAdMobを設定したい時のメモ
iOS

Xcode15でAdMobを導入した時のことを備忘録としてメモいたしました。

2023-10-04

Xcode15で作成したアプリにAdMobを設定したい時のメモ

機械学習を用いて衆議院の解散日を予測
Python

ニュースで衆議院の解散はいつ?みたいなことを取り上げていたので、機械学習を使って衆議院の解散の日を予測してみることにしました。

2023-09-18

機械学習を用いて衆議院の解散日を予測