ADOCインターナショナル

2025-09-30

ChatGPTとともに進めたAIツール開発 ―コード生成と修正支援で開発効率を大幅向上

はじめに

AIに関する技術やプロダクトの調査や検証、ソリューション開発などに携わっているXYです。
今回は生成AIを用いた開発の様子についてご紹介したいと思います。

近年、生成AIを中心として急速な普及やRAGの活性化により、ソフトウェア開発の在り方が変わりつつあります。そんな私自身も現在、物体検出や画像認識AIを活用した業務効率化やデータ解析ツールの内製開発に取り組んでおり、その一環としてChatGPTをツール開発アシスタントとして本格導入しました。
今回のブログでは、私がAI組込型ツールを開発する過程で、ChatGPTをどのように活用し、どれほど開発効率が向上したのかを実体験ベースでご紹介します。特に「コードの自動生成」「リファクタリング支援」「ライブラリ選定の提案」など、ChatGPTが開発のパートナーとしていかに有用だったかを中心にお伝えします。

プロジェクトの概要:業務支援のための物体検出及び画像認識AIツールの開発

今回開発中なのは、属人化されるような特定業務をAIで支援するためのAI搭載ツールです。
例えば、画像やテキストを対象とした自動解析、レポート生成、あるいは定型的な処理の自動化など、日常業務の一部をAIに置き換え、さらにセキュリティ上安全なローカルの環境で扱えることを目的としています。
そのAI支援ツールの開発要件の定義〜実装までを少人数で効率よく進める必要があり、「設計しながら手を動かす」アジャイル的な開発プロセスが求められました。こうした場面では、実装の手戻りや試行錯誤が非常に多く発生します。

そこで私は、ChatGPTをコーディング支援ツールとして活用することで、開発サイクルの高速化に挑戦しました。

ChatGPTの活用ポイント

1. コード生成の効率化

最も効果を感じたのはコードのたたき台を生成するスピード感です。たとえば、以下のようなケースで役立ちました。

* FlaskによるAPIサーバの雛形作成
* データ処理パイプラインの実装
* OpenCVやYOLOなどのAIライブラリを活用した画像処理フローの作成
* フロントエンドのひな形作成

これまで手動で数時間かかっていた構成の検討が、数分でたたき台が完成し、そこから実装を洗練させていく形でスムーズに作業を進められました。

実際にChatGPTから得たフロントエンドコードの一例
    
import React, { useState, useEffect } from "react";
import { Stage, Layer, Line, Circle, Image as KonvaImage } from "react-konva";
import { Button, message, Select } from "antd";
import axios from "axios";
import useImage from "use-image";
import LabelManager from "./LabelManager";

const AnnotationCanvas = ({ selectedImage, imageName, projectName }) => {
    const [shapes, setShapes] = useState([]);
    const [selectedShape, setSelectedShape] = useState(null);
    const [selectedVertexIndex, setSelectedVertexIndex] = useState(null);
    const [isDrawingMode, setIsDrawingMode] = useState(false);
    const [newShapePoints, setNewShapePoints] = useState([]);
    const [isAddingVertexMode, setIsAddingVertexMode] = useState(false);
    const [isMovingVertex, setIsMovingVertex] = useState(false);
    const [labels, setLabels] = useState([]);
    const [selectedLabel, setSelectedLabel] = useState(null);
    const [imageSize, setImageSize] = useState({ width: 500, height: 500 });
    const [image] = useImage(selectedImage, 'anonymous');


    useEffect(() => {
        fetchLabels();
        if (image) {
            setImageSize({ width: image.width, height: image.height });

            setShapes([]);

            if (imageName) {
                loadAnnotation(imageName.split(".")[0]);
            }
        }
    }, [image, projectName]);

    const fetchLabels = async () => {
        if (!projectName) return;
        try {
            const response = await axios.get(`/api/labels?project=${projectName}`);
            const labelsObj = response.data.names || {};
            const labelsArray = Object.entries(labelsObj).map(([id, label]) => ({
                id: Number(id),
                label,
            }));
            setLabels(labelsArray);
        } catch (error) {
            message.error("ラベル情報の取得に失敗しました");
        }
    };    
     
    const handleLabelChange = (labelId) => {
        if (!selectedShape) return;
    
        setShapes((prevShapes) =>
            prevShapes.map((shape) =>
                shape.id === selectedShape.id ? { ...shape, label_id: labelId } : shape
            )
        );
    
        setSelectedShape((prev) => ({ ...prev, label_id: labelId }));
        setSelectedLabel(labelId);  // 修正: 0 のラベル ID も正しく反映
    };
    
    const handleDragMoveShape = (e, shapeId) => {
        const node = e.target;
        const dx = node.x();
        const dy = node.y();
    
        setShapes((prevShapes) =>
            prevShapes.map((shape) => {
                if (shape.id === shapeId) {
                    // 全頂点の座標を適切に移動
                    const updatedPoints = shape.points.map((val, index) =>
                        index % 2 === 0 ? val + dx : val + dy
                    );
    
                    return { ...shape, points: updatedPoints };
                }
                return shape;
            })
        );
    
        // 位置をリセット (Konva が次のドラッグでズレないように)
        node.position({ x: 0, y: 0 });
    
        // 選択中の形状も更新
        if (selectedShape && selectedShape.id === shapeId) {
            setSelectedShape((prev) => ({ ...prev, points: prev.points.map((val, index) =>
                index % 2 === 0 ? val + dx : val + dy
            ) }));
        }
    };    

    const handleDeleteShape = () => {
        if (!selectedShape) return;
        setShapes((prevShapes) =>
            prevShapes.filter((shape) => shape.id !== selectedShape.id)
        );
        setSelectedShape(null);
    };  

    const handleRemovePoint = () => {
        if (!selectedShape || selectedVertexIndex === null) return;
    
        const updatedPoints = selectedShape.points.filter(
            (_, index) =>
                index !== selectedVertexIndex * 2 && index !== selectedVertexIndex * 2 + 1
        );
    
        const updatedShape = { ...selectedShape, points: updatedPoints };
        setShapes((prevShapes) =>
            prevShapes.map((shape) =>
                shape.id === selectedShape.id ? updatedShape : shape
            )
        );
        setSelectedShape(updatedShape);
        setSelectedVertexIndex(null);
    };

    const handleAddVertex = (e) => {
        if (!isAddingVertexMode || !selectedShape) return;
    
        const stage = e.target.getStage();
        const pointerPosition = stage.getPointerPosition();
        const { x, y } = pointerPosition;
    
        const updatedPoints = [...selectedShape.points, x, y];
    
        const updatedShape = { ...selectedShape, points: updatedPoints };
        setShapes((prevShapes) =>
            prevShapes.map((shape) =>
                shape.id === selectedShape.id ? updatedShape : shape
            )
        );
        setSelectedShape(updatedShape);
        setIsAddingVertexMode(false);
    };

    const handleCanvasClick = (e) => {
        if (isAddingVertexMode) {
            handleAddVertex(e);
            return;
        }

        if (!isDrawingMode) return;

        const stage = e.target.getStage();
        const pointerPosition = stage.getPointerPosition();
        setNewShapePoints((prev) => [...prev, pointerPosition.x, pointerPosition.y]);
    };

    const handleFinishDrawing = () => {
        if (newShapePoints.length < 6) {
            message.warning("少なくとも3点必要です");
            return;
        }
    
        const newShape = {
            id: shapes.length + 1,
            points: newShapePoints,
            label_id: null,
        };
    
        setShapes((prevShapes) => [...prevShapes, newShape]);
        setNewShapePoints([]);
        setIsDrawingMode(false);
    };

    const handleVertexDragMove = (e, vertexIndex) => {
        if (!selectedShape) return;
    
        const { x, y } = e.target.position();
        const updatedPoints = [...selectedShape.points];
        updatedPoints[vertexIndex * 2] = x;
        updatedPoints[vertexIndex * 2 + 1] = y;
    
        setShapes((prevShapes) =>
            prevShapes.map((shape) =>
                shape.id === selectedShape.id ? { ...shape, points: updatedPoints } : shape
            )
        );
    
        setSelectedShape((prev) => ({ ...prev, points: updatedPoints }));
    };    

    const handleVertexDragEnd = (vertexIndex) => {
        setIsMovingVertex(false);
    };

    const handleSaveAnnotation = async () => {
        if (!selectedImage || !imageName || !projectName) {
            message.warning("プロジェクトまたは画像が選択されていません");
            return;
        }

        if (shapes.some(shape => shape.label_id === null)) {
            message.warning("ラベルが割り当てられていないアノテーションがあるため保存できません");
            return
        }
    
        const normalizedShapes = shapes.map((shape) => ({
            ...shape,
            label_id: shape.label_id,  // 修正
            points: shape.points.map((v, i) =>
                i % 2 === 0 ? v / imageSize.width : v / imageSize.height
            ),
        }));
    
        try {
            await axios.post("/api/save_annotation", {
                project: projectName,
                image_name: imageName.split(".")[0],
                shapes: normalizedShapes,
            });
    
            message.success("アノテーションを保存しました");
            setSelectedShape(null); // 修正: 保存後に選択解除
        } catch (error) {
            message.error("アノテーションの保存に失敗しました");
        }
    };    

    const loadAnnotation = async (imageName) => {
        if (!projectName) return;
    
        try {
            const response = await axios.get(`/api/load_annotation?project=${projectName}&image_name=${imageName}`);
            const loadedShapes = response.data.map((shape, index) => ({
                id: index + 1, // 修正: 一意のIDを付与
                label_id: shape.label_id ?? 0, // 修正
                points: shape.points.map((v, i) =>
                    i % 2 === 0 ? v * image.width : v * image.height
                ),
            }));
    
            setShapes(loadedShapes);
        } catch (error) {
            console.error(error)
            message.error("アノテーションの読み込みに失敗しました");
        }
    };
    
    
    return (
        <div>
            <LabelManager projectName={projectName} onLabelsUpdated={setLabels} shapes={shapes} />
            <Button onClick={() => setIsDrawingMode(!isDrawingMode)}>
                {isDrawingMode ? "描画キャンセル" : "描画開始"}
            </Button>
            {isDrawingMode && newShapePoints.length > 0 && (
                <Button onClick={handleFinishDrawing} style={{ marginLeft: 8 }}>
                    描画確定
                </Button>
            )}

            <Button
                danger
                onClick={handleDeleteShape}
                disabled={!selectedShape}
                style={{ marginLeft: 8 }}
            >
                アノテーション削除
            </Button>

            <Button
                onClick={() => setIsAddingVertexMode(!isAddingVertexMode)}
                disabled={!selectedShape}
                style={{ marginLeft: 8 }}
            >
                {isAddingVertexMode ? "頂点追加終了" : "頂点追加開始"}
            </Button>

            <Button
                danger
                onClick={handleRemovePoint}
                disabled={selectedVertexIndex === null}
                style={{ marginLeft: 8 }}
            >
                頂点削除
            </Button>

            <Button onClick={handleSaveAnnotation} style={{ marginLeft: 8 }}>
                アノテーション保存
            </Button>

            {selectedShape && (
                <div style={{ marginTop: "10px" }}>
                    <span>ラベルを選択: </span>
                    <Select
                        value={selectedLabel}
                        onChange={handleLabelChange}
                        style={{ width: 200 }}
                    >
                        {labels.map((label) => (
                            <Select.Option key={label.id} value={label.id}>
                                {label.label}
                            </Select.Option>
                        ))}
                    </Select>
                </div>
            )}

            <Stage
                width={imageSize.width}
                height={imageSize.height}
                onClick={handleCanvasClick}
                style={{ border: "1px solid #ccc", marginTop: 10, overflow: "scroll", height: "1000px" }}
            >
                <Layer>
                    {image && (
                        <KonvaImage
                            image={image}
                            width={imageSize.width}
                            height={imageSize.height}
                        />
                    )}
                    {shapes.map((shape) => (
                        <Line
                            key={shape.id}
                            points={shape.points}
                            stroke="red"
                            closed
                            draggable
                            onClick={() => {
                                setSelectedShape(shape);
                                setSelectedLabel(shape.label_id);
                            }}
                            onDragEnd={(e) => handleDragMoveShape(e, shape.id)}
                        />
                    ))}

                    {newShapePoints.length > 0 && (
                        <Line
                            points={newShapePoints}
                            stroke="blue"
                            closed={false}
                            lineJoin="round"
                        />
                    )}

                    {selectedShape &&
                        selectedShape.points.map((_, index) => {
                            if (index % 2 === 0) {
                                const vertexIndex = index / 2;
                                return (
                                    <Circle
                                        key={`${selectedShape.id}-pt-${vertexIndex}`}
                                        x={selectedShape.points[index]}
                                        y={selectedShape.points[index + 1]}
                                        radius={5}
                                        fill="blue"
                                        draggable
                                        onDragMove={(e) => handleVertexDragMove(e, vertexIndex)}
                                        onDragEnd={() => handleVertexDragEnd(vertexIndex)}
                                        onClick={() => setSelectedVertexIndex(vertexIndex)}
                                    />
                                );
                            }
                            return null;
                        })}
                </Layer>
            </Stage>
        </div>
    );
};

export default AnnotationCanvas;

    
フロントエンド側の表示:
物体画像認識AIツール作成のために利用した画像ファイル(出展:国土交通省のものに当社が加工)


出典:国土交通省ウェブサイト
(上記画像は、本研究・開発のため当社によって出典元画像は加工されています。)

もちろん、一発で正常に動くとは限りませんが、「開発の出発点としてのスピード感」は非常に高いと感じました。

2. エラー解決やリファクタリング支援

コードがうまく動かない、あるいは「もっとスマートに書き直したい」と感じた際にも、ChatGPTが非常に頼りになりました。具体的には

* エラー原因の解釈と修正提案
* Pythonicな書き方への改善
* メモリ使用量を抑える最適化

など、技術ブログを調べるよりも対話形式で即座に原因にアプローチできる点が大きな強みです。

特に、私のように「AIツールの検証とプロトタイピングを繰り返すフェーズ」では、実装と検証のサイクルを短くすることが品質と速度の両立につながります。

3. ライブラリの選定と設計の方向づけ

たとえば、「画像の特定領域だけを検出して切り出す処理を実装したい」という要件があった際、ChatGPTに相談すると、以下のような提案が即座に返ってきます。

* YOLOなどの物体検出モデルをベースにする案
* セグメンテーションでマスクされた画像を生成する案
* `cv2.findContours()` を使った処理との比較

このように技術選定の観点から複数案を提示してくれるため、「この設計で進めてよいか?」という段階での判断がしやすくなりました。

開発の結果と所感

今回の開発を通して、ChatGPTを導入したことで以下のような成果が得られました。

項目 従来開発 ChatGPT活用後
コード初期作成時間 3〜4時間 約30分
エラー調査時間 1〜2時間 約10分〜30分
検証サイクル 数日に1回 毎日反復可能
学習コスト 高い 必要最低限

特に「手が止まる」時間が激減したことが大きな変化です。ChatGPTはあくまで補助的な存在ですが、開発者の思考スピードについてくる“ペアプログラマ”のような存在として位置付けると非常に効果的です。

おわりに

この記事で紹介している物体検出及び画像認識AIツールは、AIモデル学習や利活用する際に、時間的手間や技術的に苦労する部分を補助するような役割を担っています。
さらに、動作環境は完全ローカルなため、クラウド上にデータをアップロードや保存することなくセキュアに利用することが可能です。

次回は、さらなる開発効率を求めるためClaudeを利用したコード開発について紹介したいと思います。

ブログトップへ