2024년의 끝을 앞두고 사라지다 - 중앙선 북영천역(+화본역)

 

 일상생활 중에 언제나 여행계획을 짜보지만 날씨나 다른 일정등으로 연말까지 미루고 미루다 촉박하게 일정을 정하고 여행을 다녀오는 패턴이 매년 이어지고 있었는데 올해 또한 그렇게 되었습니다.

 이번 포스팅에서는 2024년을 끝으로 영업을 마치게 되는 중앙선 북영천역을 다녀오게 되었습니다.

 

 

지난 11월말 안동에서 무궁화호를 타고 북영천역에 도착하였을 때였습니다.

 

 

선로 옆에 차후 하행선이 될 선로쪽에만 플랫폼을 만들어 둔 모습입니다.

 

 

플랫폼은 3량짜리 무궁화호가 설 만큼의 길이만큼 만들어져 있었습니다.

 

 

그래도 간이 대기 장소가 마련되어 나름 역의 구실을 하고 있었습니다.

 

 

북영천역은 동해-동대구 구간을 운행하는 무궁화호만 정차합니다.

 

 

제가 타고 온 동대구행 무궁화호 열차는 영천삼각선을 통해 바로 하양역으로 가기에 영천역을 가지 않습니다.

 

 

그래서 북영천역은 동해(구 강릉) - 동대구 구간을 운행하는 무궁화호 승객을 위해 만들어진 역입니다.

 

 

이 역은 왕복 2회(편도 4회)의 열차만 정차합니다. 동해-동대구 구간을 운행하는 무궁화호가 전부인 것이지요.

 

 

역 내부는 총 3개의 선로가 놓여져있습니다.

 

 

열차 운행 시간을 보면 평소에 이 역을 이용하기 어려운 시간대의 열차가 대부분입니다.

 

 

그래도 영천에 방문하고자 하는 승객을 위해 열차는 이 역에 정차해왔습니다.

 

 

이 곳에서 영천을 가는 열차도 이 곳을 통과하지면 이 역에 정차하지 않습니다.

 

 

왜냐하면, 이 역보다 영천역에서 더 많은 열차들이 정차를 하고

 

 

승객 입장에서 조금이나마 다양한 행선지가 있는 영천역을 더 선호할 것입니다.

 

 

그러기에 북영천역은 영천역을 경유하지 못하는 동해-동대구 구간 운행 열차의 여객취급만 합니다.

 

 

내년에는 동해-동대구 무궁화호 승객을 위해 운영되던 북영천역이 여객 취급을 중단합니다.

 

 

코레일의 공식 입장은 북영천-영천 이원화된 역 운영을 영천역으로 일원화 하는 것이 목적이라 합니다.

 

 

제 생각엔 코레일의 입장에서 북영천역 운영이 계륵같이 느껴졌으리라고 봅니다.

 

 

영천에 방문하고자 하는 고객들에게 북영천이라는 선택지를 지금껏 유지해 왔지만 실제 승객이 많지 않았고

 

 

중앙선 전구간의 복선전철화가 완료되어 지금보다 영천을 경유하는 열차편이 더 많아질테니

 

 

코레일의 입장에서는 북영천역의 여객취급을 계속 할만한 매리트가 없을만합니다.

 

 

북영천역에서 하차한 승객들이 모두 역을 빠져나가면

 

 

영천역에 있는 직원이 CCTV를 통해 모든 승객이 빠져나간 역 출입문을 폐쇄합니다.

 

 

이렇게 거대한 폴사인이 없었다면 사람들은 북영천의 존재를 알 수 있었을까요?

 

 2024년 12월 14일. 이번에는 북영천역에서 열차를 탑승하기 위해 다시 방문하였습니다.

 

내일(2024년 12월 15일)을 끝으로 북영천역은 여객취급을 더이상 하지 않게 됩니다.

 

 

이제 영천역에서도 KTX를 타고 서울과 부산을 갈 수 있게 되었습니다.

 

 

결국 북영천역은 KTX는 커녕 ITX의 여객도 취급하지 못하고 더이상 승객을 받지 않게 됩니다.

 

 

열차가 정차하기 30분 전부터 출입문을 열어둡니다.

 

 

내일 오전을 끝으로 북영천역에 열차는 더이상 정차하지 않으며 한동안 임시버스가 무료로 운행됩니다.

 

 

비록 북영천역을 이용하는승객은 많지 않았지만 이 곳을 종종 쓰던 고객들은 어떤 기억을 남겼을까요?

 

 

역 입구에서 타는곳까지는 생각보다 많이 걸어야 했습니다.

 

 

여객취급을 중단하는 북영천역은 이후에도 분기 구간에서 신호장으로서의 역할을 계속 수행합니다.

 

 

신녕역 또한 이번을 끝으로 여객취급을 중단하게 됩니다.

 

 

물론 지금까지 이 곳을 들렀던 무궁화호 열차는 계속 정상운행합니다.

 

 

앞으로 영천에서 동해까지 가기 위해서는 영천역에서 하루 1회 운행하는 무궁화호를 타는 선택지만 남게됩니다.

 

 

역 주변을 둘러보다 기관차 한 대가 지나갑니다.

 

 

기관차가 멈칫하며 정지하더니 맞은편에서 열차가 나타납니다.

 

 

이 열차를 타고 저는 동대구역으로 이동할 것입니다.

 

 

그렇게 열차가 들어오고

 

 

열차는 요란한 소리를 내며 가던 길을 멈추어섭니다.

 

아래 사진은 같은날 오전 화본역을 둘러보다 몇장 찍어본것들입니다.

 

 

 

300x250

중앙선 이설전 마지막 풍경들 - 의성역~우보역 구간(2024.12.01)

 

 중앙선 전구간 이설이 완료되기 약 3주 정도 남은 시점에 곧 사라지게 될 풍경들을 사진에 남겨보고자 한 번 더 다녀와보았습니다. 이번에는 의성역~우보역 구간을 돌아다녀 보았습니다.

 

이화건널목

 

 

시간표를 확인하지 않고 오는 바람에 2시간 정도 기다리게 되었습니다. 남는 시간동안 동네를 돌아다녀봅니다.

 

 

 

군위가 귀산 박씨 집성촌인듯 보입니다.

 

 

낮선 외지인을 반기는 강아지

 

 

 

 

우보역

 

 

철길을 둘러보던 도중 길가 한복판에 우보역을 발견하였습니다.

 

 

역내에 상주하는 직원이 있는지 출입문이 막혀있지는 않았습니다.

 

 

우보역 시비

 

 

플랫폼은 영업하던 시절 그대로 유지되는듯 보입니다.

 

 

승강장으로 진입하는 건널목은 철거되어 있습니다.

 

 

역 구내를 둘러보고 바로 옆에 있는 건널목으로 이동합니다.

금천건널목

 

 

철길이 도로 바로 옆에 붙어있어 구도가 잘 나올거같습니다.

 

 

 

 

대리건널목

 

 

 

탑리역에서 의성역 방면으로 가면 처음으로 볼 수 있는 대리건널목

 

 

마을길 바로 옆을 따라가는 철길

 

 

 

만천건널목

 

 

 

300x250

중앙선 이설전 마지막 풍경들 - 북영천역~갑현역 구간(2024.11.22)

 

 다음달(2024년 12월 21일) 중앙선의 모든 구간 이설이 완공되어 KTX가 서울 청량리역에서부터 부산 부전역까지 달릴 예정입니다. 즉, 현재 마지막으로 남은 기존선 구간(안동 - 북영천) 또한 다음달부터는 열차가 더이상 달리지 않을 예정입니다.

 차후 이설되어 볼 수 없게될 풍경들을 사진으로 남기고자 날씨가 화창하던날 마지막으로 남은 중앙선 기존구간을 다녀왔습니다.

 

 첫 번째로 방문한 곳은 북영천역에서 출발한 열차가 처음으로 건널목을 통과하는 호당4건널목입니다.

 

호당4건널목

 

 

 

운좋게 도착하자마자 열차가 지나가는 것을 사진으로 남기는군요.

 

 

 

호당3건널목

 

호당2건널목

 

 

 

 

삼부건널목

 

 

 

 

 

화산역

 

 

 

 

 

화산역 광장에서 바라본 입구길. 역 바로 옆에 붉은 철문의 구 역세권 집이 인상적이었습니다.

 

 

역 내부를 보니 꽤 오래전 폐쇄된 흔적들을 볼 수 있었습니다.

 

 

유성1건널목

 

 

 

유성1건널목에서 바라본 화산역 구내

 

 

화남건널목

 

완전3건널목

 

 

주변에 건널목밖에 없는 이 곳에 새마을운동 기념 공원이 있었습니다.

 

 

왕복 4차선 도로가 이 건널목에서만 2차선으로 줄어듭니다. 아마도 선로이설 후 추가 공사를 하겠죠

 

 

공원에서 쉬다가 기차 구경하기는 딱 좋은 곳이었습니다.

 

 

하지만 내년부터는 이 곳에서 더이상 열차를 볼 수 없게 됩니다.

 

 

 내년이면 이 곳도 4차선으로 확장되어 지금의 흔적은 볼 수 없을 것으로 보입니다.

 

신덕2건널목

 

 

한적한 시골 입구에 위치한 신덕2건널목입니다

 

 

마을 주변을 돌아다니던 도중 요란한 소리가 울려퍼지고

 

 

신녕역을 출발한 열차가 들어옵니다.

 

 

열차가 떠난 직후 모습

 

용천건널목

 

 

 

가일1건널목

 

 

 

왠지 침목으로 만든것으로 추정됩니다.

 

 

석촌2건널목

 

 

건널목 인근 회사에서 키우는 강아지가 일을 열심히 하는군요.

 

 

300x250

Flask 라이브러리로 이미지를 업로드하고 볼 수 있는 서버 구축하기

공대생의 팁 2024. 10. 26. 01:19



 ※ 이 프로젝트는 ChatGPT로 생성된 코드를 사용하였습니다.

 

 프로그래밍을 생업으로 일하시는 분들이라면 한 번 즈음은 자신이 사용해보지 못했던 프로그래밍 언어를 사용해야 할 수 밖에 없는 경험이 있으셨을 것입니다. 저 또한 Python을 위주로 개발을 하다보니 간혹 C/C++를 사용할 일이 있을 때엔 버벅거리면서 코딩을 간신히 해내곤 합니다.

 

 그런 와중에 프로그램 시연을 해야 하는 상황이 되었는데 막상 준비하려 하니 1주일이라는 시간을 갖고는 GUI 프로그램을 뚝딱 만드는건 사실상 불가능한 상황이었습니다. 그러한 고민을 하던 상황에서 혹시나 Chat GPT에게 이러한 고민을 털어놨더니 Web프로그래밍으로 짧은 시간 내에 시각화를 할 수 있는 프로젝트를 만들 수 있다는 답변을 하는 것이었습니다!

 

 지금까지 Web프로그래밍 관련 지식은 20년전 개인 홈페이지 제작을 위해 제로보드를 다루었던 경험밖에 없는 저에게 직접 Web프로그래밍을 하는 것은 불가능했습니다. 하지만 우리는 언제나 그랬듯이 방법을 찾아내왔지요?

 

 이번 포스팅에서는 Web프로그래밍 경험이 없는 사람의 입장에서 ChatGPT에게 다음과 같은 질문을 하였고 아래와 같은 질문을 통해 매우 훌륭한 Web프로그램을 완성하였습니다.

 

Python을 사용해서 다음과 같은 프로그램을 만들어줘.
 - 이미지를 웹사이트에 업로드 하고, 이를 볼 수 있는 웹사이트 제작
 - 이미지를 볼 때 마우스의 휠로 이미지의 축소/확대 기능 적용
 - 마우스로 드래그하여 확대된 이미지를 이동하며 볼 수 있도록함
 - 이미지 뷰어의 아래쪽에는 Thumbnail 방식으로 업로드된 이미지를 클릭하여 볼 수 있도록 함
 - 클립보드에 복사된 이미지를 웹페이지에 붙여넣기를 하는 방법으로 이미지를 업로드
 - 업로드된 이미지는 OpenCV를 사용하여 grayscale 이미지를 생성하고, 원본 및 변환된 이미지를 볼 수 있도록 함

 

 

 프롬프트의 전반적인 구조는 위와 같이 하여 질문을 하였고 몇몇 부분은 추가로 프롬프트를 작성하였으며, 부족한 부분은 직접 다듬었을 뿐인데 다행히도 원하는 대로 동작하는 웹사이트가 완성되었습니다!

 

아래는 ChatGPT가 Flask를 사용하여 만들어낸 프로젝트입니다. 아래는 프로젝트의 파일 구조입니다. app.py가 html을 제어하여 웹페이지를 동작시키는 것으로 이해하면 얼추 맞을 겁니다.

 

 

templates/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Drag and Drop Image Upload</title>
    <style>
        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: flex-start;
            height: 100vh;
            margin: 0;
            background-color: #f5f5f5;
            overflow: hidden;
            /* Hide scrollbars */
        }
        #drop-area {
            border: 2px dashed #ccc;
            padding: 80px;
            width: 100%;
            height: 80px;
            /* Increased height */
            box-sizing: border-box;
            /* Include padding and border in the element's total width and height */
            text-align: center;
            background-color: #fff;
            position: fixed;
            top: 0;
            left: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            /* Center the text */
        }
 
        #drop-area p {
            margin: 0;
            font-size: 24px;
            font-weight: bold;
            /* Bold text */
        }
 
        #loading {
            display: none;
            /* Hide loading initially */
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: rgba(128, 128, 128, 0.7);
            /* Gray with 70% opacity */
            color: white;
            padding: 40px;
            /* Increased padding */
            border-radius: 10px;
            /* Increased border-radius */
            text-align: center;
            z-index: 1000;
            /* Ensure loading screen overlaps all content */
            font-size: 32px;
            /* Larger font size for loading text */
            font-weight: bold;
            /* Bold text for loading */
        }
 
        #gallery {
            display: flex;
            justify-content: center;
            align-items: center;
            width: 100%;
            height: calc(100vh - 280px);
            /* Full height minus drop area height, thumbnails, and border */
            margin-top: 95px;
            /* Adjusted to match drop-area height and border */
            overflow: hidden;
            position: relative;
            /* Keep positioned relative to viewport */
            border-bottom: 1px solid black;
            /* Add bottom border for separation */
        }
 
        #thumbnails {
            display: flex;
            justify-content: center;
            align-items: center;
            width: 100%;
            height: 100px;
            margin: 5px;
            overflow-x: auto;
            background-color: #f5f5f5;
            position: fixed;
            bottom: 0;
            left: 0;
        }
 
        #thumbnails img {
            height: 80px;
            margin: 10px;
            cursor: pointer;
            transition: transform 0.25s ease;
        }
 
        #thumbnails img:hover {
            transform: scale(1.1);
        }
 
        img {
            max-width: 100%;
            max-height: 100%;
            transition: transform 0.25s ease;
            /* Smooth transition for transform */
            cursor: grab;
            /* Cursor shows grab icon */
        }
 
        img:active {
            cursor: grabbing;
            /* Cursor shows grabbing icon */
        }
    </style>
</head>
<body>
    <div id="loading">Loading...</div>
    <h2>Drag and Drop Image Upload</h2>
    <div id="drop-area">
        <p>Drag & drop image files here<br>Or paste image from clipboard</p>
    </div>
    <div id="gallery"></div>
    <div id="thumbnails"></div>
    <script>
        let dropArea = document.getElementById('drop-area');
        let gallery = document.getElementById('gallery');
        let thumbnails = document.getElementById('thumbnails');
        let loading = document.getElementById('loading');
        let currentImage;
        let scale = 1;
        let translateX = 0;
        let translateY = 0;
 
        dropArea.addEventListener('dragover', (event=> {
            event.preventDefault();
            dropArea.style.borderColor = 'green';
        });
 
        dropArea.addEventListener('dragleave', () => {
            dropArea.style.borderColor = '#ccc';
        });
 
        dropArea.addEventListener('drop', (event=> {
            event.preventDefault();
            dropArea.style.borderColor = '#ccc';
 
            thumbnails.innerHTML = ''// Clear existing thumbnails on each drop
            let files = event.dataTransfer.files;
            handleFiles(files);
        });
 
        function handleFiles(files) {
            ([...files]).forEach(uploadFile);
        }
 
        async function uploadFile(file) {
            // Show loading screen before starting upload
            loading.style.display = 'block';
            let url = '/upload';
            let formData = new FormData();
            formData.append('file', file);
 
            try {
                const response = await fetch(url, {
                    method: 'POST',
                    body: formData
                });
 
                const data = await response.json();
                // Hide loading screen after upload is successful
                displayImage(data.images[0]);
                loading.style.display = 'none';
 
                // Add the uploaded image to the thumbnails
                data.images.forEach(imgUrl => addThumbnail(imgUrl))
            } catch (error) {
                console.error('Upload failed');
                loading.style.display = 'none';
            }
        }
 
        function displayImage(imageUrl) {
            // Clear previous image
            gallery.innerHTML = '';
 
            // Create new image
            const img = document.createElement('img');
            img.src = imageUrl;
            gallery.appendChild(img);
 
            // Update current image reference
            currentImage = img;
 
            // Reset scale and position
            scale = 1;
            translateX = 0;
            translateY = 0;
            img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
 
            // Add drag-to-pan functionality
            let isDragging = false;
            let startX, startY;
 
            img.addEventListener('mousedown', (event=> {
                event.preventDefault();
                isDragging = true;
                startX = event.clientX - translateX;
                startY = event.clientY - translateY;
                img.style.cursor = 'grabbing';
            });
 
            document.addEventListener('mousemove', (event=> {
                if (isDragging) {
                    event.preventDefault();
                    translateX = event.clientX - startX;
                    translateY = event.clientY - startY;
 
                    // Restrict movement within the boundaries
                    const rect = img.getBoundingClientRect();
                    const galleryRect = gallery.getBoundingClientRect();
 
                    // Calculate overflows
                    const overflowX = Math.max(0, rect.width * scale - galleryRect.width);
                    const overflowY = Math.max(0, rect.height * scale - galleryRect.height);
 
                    // Clamp translateX and translateY within allowed bounds
                    translateX = Math.min(overflowX / 2, Math.max(-overflowX / 2, translateX));
                    translateY = Math.min(overflowY / 2, Math.max(-overflowY / 2, translateY));
                    img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
                }
            });
 
            document.addEventListener('mouseup', () => {
                if (isDragging) {
                    isDragging = false;
                    img.style.cursor = 'grab';
                }
            });
        }
 
        function addThumbnail(imageUrl) {
            const thumb = document.createElement('img');
            thumb.src = imageUrl;
            thumb.addEventListener('click', () => {
                displayImage(imageUrl);
            });
            thumbnails.appendChild(thumb);
        }
 
        // Zoom in/out handlers
        gallery.addEventListener('wheel', (event=> {
            if (currentImage) {
                event.preventDefault();
                if (event.deltaY > 0) {
                    // Scroll down - zoom out
                    scale *= 0.9;
                } else {
                    // Scroll up - zoom in
                    scale *= 1.1;
                }
 
                scale = Math.min(Math.max(.125, scale), 4); // Restrict scale
 
                // Restrict movement within the boundaries
                const rect = currentImage.getBoundingClientRect();
                const galleryRect = gallery.getBoundingClientRect();
 
                // Calculate overflows
                const overflowX = Math.max(0, rect.width * scale - galleryRect.width);
                const overflowY = Math.max(0, rect.height * scale - galleryRect.height);
 
                // Clamp translateX and translateY within allowed bounds
                translateX = Math.min(overflowX / 2, Math.max(-overflowX / 2, translateX));
                translateY = Math.min(overflowY / 2, Math.max(-overflowY / 2, translateY));
                currentImage.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
            }
        });
 
        // Add click event to hide loading screen in case of manual reset
        loading.addEventListener('click', () => {
            loading.style.display = 'none';
        });
 
        // Handle image paste from clipboard
        window.addEventListener('paste', (event=> {
            const items = event.clipboardData.items;
            for (const item of items) {
                if (item.type.startsWith('image/')) {
                    const file = item.getAsFile();
                    thumbnails.innerHTML = ''// Clear existing thumbnails on paste
                    handleFiles([file]);
                }
            }
        });
    </script>
</body>
</html>
cs

 

 

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from flask import Flask, request, url_for, render_template, send_from_directory, jsonify
from time import time
import os
import cv2
 
app = Flask(__name__)
 
UPLOAD_FOLDER = r'D:\server\static\uploads'
app.config['UPLOAD_FOLDER'= UPLOAD_FOLDER
app.config['MAX_CONTENT_PATH'= 16 * 1024 * 1024
 
# Ensure the upload directory exists
if not os.path.exists(app.config['UPLOAD_FOLDER']):
    os.makedirs(app.config['UPLOAD_FOLDER'])
 
@app.route('/')
def index():
    return render_template('index.html')
 
@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return 'No file part'
 
    file = request.files['file']
    if file.filename == '':
        return 'No selected file'
    if file:
        image_paths = []
        input_image_path = 'input_image.jpg'
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], input_image_path)
        file.save(filepath)
        image_paths.append(url_for('uploaded_file', filename=input_image_path) + '?' + str(int(time())))
        
        img = cv2.imread(filepath)
        gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        gray_filepath = os.path.join(app.config['UPLOAD_FOLDER'], 'gray_' + os.path.basename(filepath))
        cv2.imwrite(gray_filepath, gray_img)
        image_paths.append(url_for('uploaded_file', filename='gray_' + os.path.basename(filepath)) + '?' + str(int(time())))
 
        # Return the URL to access the uploaded file
        return jsonify(images=image_paths)
    return 'File not uploaded correctly'
 
@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
 
if __name__ == '__main__':
    # 모든 외부 접속 허용시 host='0,0,0,0' 설정
    app.run(debug=True, host='0,0,0,0', port=5000)
cs

 

 

- 결과

 

 
 

 

 
 

 저는 지금까지 제 블로그에는 제가 직접 작성한 소스코드를 업로드 해왔었습니다. 그러했던 제가 최근에는 ChatGPT에게 원하는 시스템의 전반적인 내용을 작성 시키고, 저는 그 코드에서 제가 원하는 기능들을 덮붙이는 식으로 일을 수행하고 있습니다. AI를 연구하는 입장에서 어느덧 AI와 함께 협업을 하며 일을 하는 세상이 되었다는 것이 참으로 놀라운 세상이네요. 앞으로도 ChatGPT를 사용해 업무 효율을 높일수 있는 기회들이 이번처럼 많아진다면 앞으로는 지금보다 업무 생산성이 많이 높아질 것이라 기대를 해봅니다.

300x250

중앙선에 남은 마지막 아담한 간이역 - 화본역(2024.07.27)

 

 1939년 4월 처음으로 개통한 중앙선은 1942년 4월  청량리-경주 전구간이 개통되었을 당시 많은 승객들이 각 지역을 잇는 구불구불한 철로를 따라 서있던 간이역들을 통해 열차를 이용해왔었습니다. 중앙선 개통 이라 86년의 세월동안 많은 것이 변하였는데 특히 청량리에서 부전까지 비둘기호가 반나절 넘게 달려야 완주하던 중앙선 철길은 직선화되면서 소요시간이 점점 줄어들었고 이제는 무궁화호를 타고 무려 6시간 남짓이면 될 정도로 소요시간이 무려 절반이나 줄었습니다. 심지어 전철화까지 완료되어 KTX-이음이 달리게 되면 시간을 이보다 더 줄어들 예정이니 말 그대로 격세지감이라는 어르신들의 말의 의미가 이런 경우에 쓰임을 깨닫습니다.

 

 2005년 청량리-덕소 구간 복선화 및 직선화를 시작으로 양평, 원주, 제천, 단양, 영주, 안동, 의성까지 차례차례 진행되었고 어느덧 이러한 변화는 의성-북영천 구간을 제외한 전 구간이 완공되었습니다. 이러한 변화를 아는지 모르는지 내년부로 더이상 열차가 다니지 않게될 화본역은 오늘도 무궁화호를 타고 찾아온 승객들을 맞이하고 있었습니다.

 

 

아화역에서 무궁화호를 타고 화본역에 도착하였습니다.

 

 

전역과 다음역인 두 역은 여객 영업을 하지 않고 있습니다.
내년 선로가 이설되면 두 역은 승객 없이 쓸쓸히 마지막 열차를 보내겠지요. 

 

 

퇴역한 새마을호 객차가 역 인근에서 카페로 사용되고 있는 듯 합니다.

 

 

화본역에도 기관차가 다니던 시절 사용되었던 급수탑이 있습니다.

 

 

역 구내는 올해 마지막 영업하는 역 치고는 상당히 잘 관리되고 있습니다.

 

 

옛날 양식의 역명판도 그대로 재현해 두었군요.

 

 

이전에는 이 곳에서 강릉역에도 갈 수 있었지요?

 

 

어느덧 열차 문이 닫히고

 

 

열차는 청량리역을 향히 달려갑니다.

 

 

역사안으로 들어가봅니다.

 

 

역의 유명세 덕에 현대 양식이 아닌 옛모습으로 리모델링된 역사의 모습입니다.

 

 

승차권이 없는 방문객의 경우 기념권 성격의 입장권을 구매후 역무원의 안내에 따라 승강장에 입장할 수 있습니다.

 

 

열차는 하루에 총 6회 정차하네요.

 

 

그러고보니 저는 화본역 방문으로 군위는 처음 방문해보네요.

 

 

역광장은 상당히 넓습니다.

 

 

치즈냥 한 마리가 더운날 휴식을 취하고 있습니다.

 

 

군위군이 대구광역시에 편입됨에 따라 경상북도라고 적혀있어야 할 부분을 가렸네요.

 

 

무더웠던 2024년 7월말이었어서 열차카페에서 오미자에이드 한잔 샀습니다.

 

 

멀리서 바라본 화본역 역명판

 

 

삼국유사 군위를 형상화한 듯 한 캐릭터들일까요?

 

 

인근에 화본역에 대한 정보를 설명하는 비석이 보입니다.

 

 

한적한 시골 간이역 치고는 광장이 나름 넓습니다.

 

 

진입로에서 바라본 화본역

 

 

역세권에 무려 식당도 있습니다!

 

 

다음 열차가 들어오기 전 역앞 마을 구경을 잠시 하다가

 

 

아화역으로 돌아가기 위해 다시 승강장으로 들어옵니다.

 

 

요 역명판도 철도청 시절에 쓰던 것으로 보이는데?

 

 

역 주변 구경에 정신이 팔려있던 찰나에 벌써 열차가 들어옵니다.

 

 

새로운 철로로 이설되면 이 구간에서 디젤기관차를 볼 기회기 많지는 않겠죠?

 


잠시 짬을 내어 찾아온 간이역 여행을 마치고 다시 일상으로 돌아가봅니다!

 

300x250

MMCV 라이브러리로 Custom AI모델을 만들어보자!(1) - Backbone 추가하기

프로그래밍 팁 2024. 8. 31. 23:39

 

 Pytorch는 Meta AI(구 페이스북 AI연구소)에서  만든 딥러닝 라이브러리로, 오늘날 AI 관련 논문등에서 많이 사용되고 있으며 Github를 통해 오픈소스로 공개되는 AI모델 다수가 Pytorch를 사용하고 있습니다. 과거 Google의 TensorFlow가 산업용 AI분야에 주로 사용되고 있다고 알려져 있으나 대학원에서 Pytorch를 접했던 연구원들이 산업 현장에서 Pytorch 사용을 이어가게 되면서 산업용 AI분야에서도 Pytorch의 점유율이 높아져가고 있습니다.

 

 TensorFlow에 비해 Pytorch가 갖는 강점으로 모델 설계가 직관적이고 수정이 쉽다는 점입니다. 다만, Pytorch로 자신이 원하는 모델을 만들 수 있다 하더라도 지금까지 공개된 수많은 모델들을 일일히 공부하고 이를 이해하고 설계를 하는 과정은 개발시간이 소요되며, 개발자별 소스코드의 구조에 차이가 있을 경우 모델 설계를 처음부터 하는 것이 나을 정도로 복잡한 과정이 필요할 수 있습니다. 심지어 같은 구조의 AI모델임에도 소스코드 구조가 달라지게 되면 각각의 모델들이 파편화되어 모델 구조 관리에 더 큰 어려움이 생기게 되기 마련입니다.

 

 OpenMMLab에서 공개한 오픈소스 라이브러리인 MMCV는 자주 사용되는 기능들을 단일화하고, 기본에 공개된 AI모델들을 MMCV 라이브러리로 설계하여 모델들의 구조를 단일화하여 개발자 입장에서 간단하게 소스코드 일부 수정만으로 AI모델을 쉽게 변경할 수 있어 개발이 쉽습니다. 

 

 이번 포스팅에서는 MMCV라이브러리에 자신이 직접 모델을 만들어 적용하는 방법을 소개시켜드리고자 합니다.

 

 본  포스팅에서는 MMSegmentation을 기준으로 설명드리도록 하겠습니다. MMSegmentation 라이브러리에서 FCN모델은 Backbone으로 ResNet과 HRNet이 기본으로 적용되어 있는데, 저는 여기에 VGG 백본을 추가해보고자 합니다.

 VGG는 2014년 공개된 AI분야 입장에서 보았을 땐 고전 CNN 구조의 모델입니다. 지금 시점에서 보았을 땐 단순한 구조로서 CNN, max pooling, Relu, softmax 등으로 Layer가 구성되어 있어, AI에 입문하시는 분들께서 자신이 직접 AI모델을 만드는 실습자료로 훌륭한 모델 중 하나입니다.

 

 MMCV 라이브러리에서 VGG 모델이 기본으로 제공되고 있어, MMSegmentation에서는 VGG모델을 상속하는 방법으로 Backbone 모델을 추가해보겠습니다. 아래와 같은 경로에 소스코드를 추가해줍니다.

 

 

mmseg/models/backbones/FCNVGG.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import warnings
import torch.nn as nn
from typing import List, Optional, Sequence, Tuple, Union
from mmcv.cnn Import VGG
from mmseg.registry import MODELS
 
@MODELS.register_module()
class FCNVGG(VGG):
    def __init__(self,
                depth: int,
                with_bn: bool = False,
                num_classes: int = -1,
                num_stages: int = 5,
                dilations: Sequence[int= (11111),
                out_indices: Sequence[int= (01234),
                frozen_stages: int = -1,
                bn_eval: bool = True,
                bn_frozen: bool = False,
                ceil_mode: bool = False,
                with_label_pool: bool = True,
                pretrained = None,
                init_cfg = None):
                
        super().__init__(
            depth,
            with_bn,
            num_classes,
            dilations,
            out_indies,
            frozem_stages,
            bn_eval,
            bn_frozen,
            ceil_mode,
            with_last_pool)
            
        assert not (init_cfg and pretrained), \
            'init_cfg and pretrained cannot be specified at the same time'
        if init_cfg is not None:
            self.init_cfg = init_cfg
        elif isinstance(pretrained, str):
            warnings.warn('DeprecationWarning: pretrained is deprecated, '
                            'please use "init_cfg" instead')
            self.init_cfg = dict(type='Pretrained', checkpoint=pretrained)
        elif pretrained is None:
            self.init_cfg = [
                dict(type='Kaiming', layer='Conv2d'),
                dict(type='Constant', val=1, layer='BatchNorm2d'),
                dict(type='Normal', std=0.01, layer='Linear'),
            ]
        else:
            raise TypeError('pretrained must e a str or None')
        
    def init_weights(self, pretrained=None):
        super().init_weights(pretrained)
        
    def forward(self, x): # should return a tuple
        result = super().forward(x)
        return result
cs

 

추가한 Backbone 모델이 mmsegmentation 라이브러리 import시 불러오도록 설정합니다.


mmseg/models/backbones/__init__.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Copyright (c) OpenMMLab. All rights reserved.
from .beit import BEiT
from .bisenetv1 import BiSeNetV1
from .bisenetv2 import BiSeNetV2
from .cgnet import CGNet
from .ddrnet import DDRNet
from .erfnet import ERFNet
from .fast_scnn import FastSCNN
from .hrnet import HRNet
from .icnet import ICNet
from .mae import MAE
from .mit import MixVisionTransformer
from .mobilenet_v2 import MobileNetV2
from .mobilenet_v3 import MobileNetV3
from .mscan import MSCAN
from .pidnet import PIDNet
from .resnest import ResNeSt
from .resnet import ResNet, ResNetV1c, ResNetV1d
from .resnext import ResNeXt
from .stdc import STDCContextPathNet, STDCNet
from .swin import SwinTransformer
from .timm_backbone import TIMMBackbone
from .twins import PCPVT, SVT
from .unet import UNet
from .vit import VisionTransformer
from .vpd import VPD
 
__all__ = [
    'ResNet''ResNetV1c''ResNetV1d''ResNeXt''HRNet''FastSCNN',
    'ResNeSt''MobileNetV2''UNet''CGNet''MobileNetV3',
    'VisionTransformer''SwinTransformer''MixVisionTransformer',
    'BiSeNetV1''BiSeNetV2''ICNet''TIMMBackbone''ERFNet''PCPVT',
    'SVT''STDCNet''STDCContextPathNet''BEiT''MAE''PIDNet''MSCAN',
    'DDRNet''VPD''FCNVGG'
]
cs

 

끝으로, VGG를 Backbone으로 하는 FCN모델을 Config로 구성해줍니다.

fcn_vgg16.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
_base_ = [
    'configs/_base_/datasets/cityscapes.py',
    'configs/_base_/default_runtime.py',
    'configs/_base_/schedules/schedule_40k.py'
]
# model settings
norm_cfg = dict(type='SyncBN', requires_grad=True)
data_preprocessor = dict(
    type='SegDataPreProcessor',
    mean=[123.675116.28103.53],
    std=[58.39557.1257.375],
    bgr_to_rgb=True,
    pad_val=0,
    size=(512,512),
    seg_pad_val=255)
model = dict(
    type='EncoderDecoder',
    data_preprocessor=data_preprocessor,
    backbone=dict(
        type='FCNVGG',
        depth=16,
        with_last_pool=False,
        ceil_mode=True,
        init_cfg=dict(
            type='Pretrained',
            checkpoint='open-mmlab://vgg16_caffe'),
    decode_head=dict(
        type='FCNHead',
        in_channels=512,    # VGG모델의 출력 채널수와 일치시킬것
        in_index=4,         # VGG모델의 출력 Layer중 선택(5개의 Layer중 5번째)
        channels=512,
        num_convs=2,
        concat_input=True,
        dropout_ratio=0.1,
        num_classes=80,
        norm_cfg=norm_cfg,
        align_corners=False,
        loss_decode=dict(
            type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0)),
    auxiliary_head=dict(
        type='FCNHead',
        in_channels=512,    # VGG모델의 출력 채널수와 일치시킬것
        in_index=3,         # VGG모델의 출력 Layer중 선택(5개의 Layer중 4번째)
        channels=256,
        num_convs=1,
        concat_input=False,
        dropout_ratio=0.1,
        num_classes=80,
        norm_cfg=norm_cfg,
        align_corners=False,
        loss_decode=dict(
            type='CrossEntropyLoss', use_sigmoid=False, loss_weight=0.4)),
    # model training and testing settings
    train_cfg = dict()
    test_cfg = dict(mode='whole'))
cs

 

 

 위와 같이 설정해주신 다음 아래의 명령어를 실행하시면 VGG Backbone이 적용된 AI모델이 학습되는 것을 확인하실 수 있습니다.

 

$ python tools/train.py fcn_vgg16.py

 

 

300x250

뒤바뀐 운명 - 중앙선 건천역과 아화역(2024.07.27)

 

지난 2021년 즈음 중앙선 경주 구간이 이설되기 전 폐역 예정이던 역들을 방문했었습니다. 어떤 역은 이설 전부터 여객업무가 중단되어 열차조차 지나가지 않게 될 운명을 기다리던 역도 있었고, 이설 직전까지 승객을 맞이하다 사라진 역들도 있었지요. 그런데 놀랍게도 폐역만을 기다리던 역이 새롭게 이설된 선로 위에서 다시 부활한 역도 있었습니다.

2024년 어느 무더웠던 여름, 지난 2021년 12월 27일까지 여객영업을 하다 사라진 건천역과 바로 다음날 다시 승객들을 맞이하게 된 아화역을 방문하였습니다.

먼저 현재는 폐역이된 건천역부터 방문합니다. 이전에 건천역이 아직까지 여객영업을 하던 순간을 찍었던 당시의 흔적도 같이 보셨으면 합니다.

 

2021.11.23 - 코앞 아파트가 역세권인 간이역 - 중앙선 건천역 [2021.11.21]

 

 

3년전 겨울에 방문했던 건천역을 다시 와보았습니다.

 

으레 그렇듯 기차가 더이상 멈추지 않는 간이역 앞은 주차장이 되어있습니다.

 

 

더이상 방문객을 받아주지 않는 폐역이 된 건천역

 

 

허름한 건물에 붙어있는 역명판이 이곳이 역이었음을 알리고 있습니다.

 

건천역 초역세권이었던 아파트는 푸르른 나무로 가려지고

 

그 앞에는 나무의자 하나만 덩그러니

 

지난 겨울날엔 볼 수 없었던 푸르름을 느껴봅니다.

 

이 골목으로는 더이상 승객들이 오고가지는 않겠지만 저처럼 건천역을 찾아오는 이가 또 있을까요?

 폐역이 된 건천역을 뒤로하고 바로 옆동네에 있는 아화역을 향해갑니다. 중앙선 철길이 이설되기 전 방문하였던 아화역의 모습을 아래의 링크를 통해 확인하실 수 있습니다.

2021.11.25-잊혀진 간이역, 다시 부활할것인가 - 중앙선 아화역[2021.11.21]

 

 

아화리 중심지에서 조금 걸어가면 이설전까지 기차가 멈추지는 않았으나 지나가던 구 아화역이 있습니다.

 

골목 사이에 숨어있는 간이역을 찾아가는 것도 간이역 여행의 재미 중 하나일까요?
3년전 그때나 지금이나 한결같은 모습의 구 아화역

 

새로 이설된 아화역의 위치를 알려준다면 더 좋았을거같아 보이지만 이젠 이 역의 존재를 아는 이도 많지 않을 것 같네요.
비록 기차는 더이상 이 곳을 지나지 않지만 외부의 출입을 차단하고 있습니다.

 

울창해진 풀숲 사이에 승강장의 자취가 희미해져벼렸군요.
좀 더 시간이 지나 방문한다면 어떤 모습까요?
전에 방문했을때와는 달리 창문 일부분이 꼐져있네요. 이래서 폐역이 되면 관리 차원에서 유리창을 모두 갑판으로 교체해버리는 듯 합니다.
아화역 인근에 위치했던 건널목. 이젠 감촉같이 사라졌습니다.
수북히 쌓여있는 자갈길을 통해 이 곳에 철길이 있었음을 알 수 있을 뿐입니다.



3년전 이 곳에서 RDC 무궁화호가 달리던 모습이 떠오릅니다.

 

 건널목이 있던 자리에서 마을 방향으로 쭈욱 걸어가 새로 이설되어 영업중인 아화역에서 열차를 탑니다.

 

아화리 바로 옆에 있는 심곡리에 지어진 신 아화역
간이역때와는 달리 주차장까지 완되어 있습니다. 이용하는 승객들이 있었는지 차들이 제법 있습니다.
화려하게 부활한 아화역에서 청량리행 열차를 타러 갑니다.
아화역이 영업을 재개하던 당시엔 동대구↔포항 무궁화호만 정차했었으나 정차하는 열차가 증가하여 청량리행 열차로 서울까지 한 번에 갈 수 있게 되었습니다.

 

청량리를 출발한 KTX가 이 곳을 달릴때 즈음이면 디젤기관차를 보기 더 어려워질겁니다.
청량리에서 부전까지 한때 8시간 넘게 걸리던 열차도 선로의 지속적인 개량으로 이젠 6시간대까지 줄어들었습니다.
열차를 타고 올해 말 중앙선 선로 이설 계획으로 폐역이 예정된 화본역으로 갑니다.

 

 바쁜 일상속에서도 그 때 그 모습 그대로 있는 간이역들을 보면서 먼 미래엔 치열했던 내 모습도 추억이 되겠지 하는 상상을 하며 사라져가는 일상속 풍경들을 오늘도 한 장씩 남겨봅니다.

300x250

게임큐브 컨트롤러 복각판 정품 사용 후기

흔치않은일상 2024. 6. 30. 23:55


 지난번 Aliexpress를 통해 구매한 짝퉁 게임큐브 컨트롤러를 PC에 연결하여 사용하였던 후기를 올린 적이 있었는데, 구매한지 불과 일주일만에 아날로그 스틱에서 쏠림 현상이 발생하였습니다. 처음에는 불편함을 감수하고 아날로그 스틱의 쏠림에 적응하여 계속 사용해보려 하였지만 이젠 C 아날로그 스틱 마저 아래쪽으로 쏠리는 상황이 되어 스매시 브라더스를 플레이하는 것 조차 불가능한 상황이 되어버렸습니다.

 배송비까지 포함해 단돈 만원 내외의 가격이라는 유혹에 해외직구로 덥석 사버린 이후로도 몇 번 더 시도해보기 위해 다른 색상으로 두 세개 정도 더 사서 게임을 플레이 해보았지만 이상하게도 PC 에 연결하였을 때엔 정상적으로 동작하는 컨트롤러가 Wii에 연결해서 플레이하였을 때엔 캐릭터들이 멋대로 아래 방향으로 이동하는 괴이한 현상을 겪게 되었습니다. 심지어 Aliexpress에서 분해 드라이버 및 부품을 구매하여 자가수리를 시도해보았지만 단순히 스틱만 바꾸는 것만으로는 현상이 나아지지 않았고, 기판 납땜으로 부품을 바꾸는 방법을 시도하기엔 납과 인두가 없는데다가 숙련되지 않은 실력으로 인해 자칫하면 컨트롤러가 자체가 정크가 될 우려에 자가수리를 포기하게 되었습니다.

 

 몇달전 일본에 방문하였을 때 현지에서 게임큐브 컨트롤러를 구해보려 하였지만, 발매된지 20년도 넘은 게임큐브 컨트롤러 중고가 4만엔 정도 가격에 판매되고 있어 차라리 Aliexpress에서 짝퉁을 사는 것이 훨신 나을것이라고 생각한 것이 큰 착각이었습니다. 싼게 비지떡이라는 옛말을 몸소 경험할 수 있었던 순간이었습니다.

 혹시나 국내에서 게임큐브 컨트롤러 정품을 구할 수 있을까 싶어 용산 매장을 둘러보았는데 중고는 6만원, 닌텐도 스위치에서 발매된 스매시 브라더스 Ultimate에서 플레이를 위해 다시 생산된 복각판 신품은 8만 2천원에 팔고 있었습니다. 스매시 브라더스 Ultimate 발매 이후 나온지 6년이 지난 지금 시점에서 이 가격에 사기엔 너무 비싸다는 생각이 들었습니다. 그래서 혹시나 인터넷에서 구할 수 있는지 열심히 찾아보던 도중 지마켓에서 해외 배송비 포함 52500에 판매되는 복각판 게임큐브 컨트롤러가 판매중인 것을 보고 망설임 없이 바로 구매하였습니다.

 

https://link.gmarket.co.kr/4LZ9JKzQpB

 

G마켓-닌텐도 스위치 게임큐브 컨트롤러

52,500원

item.gmarket.co.kr

 

 요새 직구가 유행하면서 예전에 비해 직구 난이도가 확실히 낮아졌다는 이야기를 들었는데 이번에 구매를 해보니 배송기간이 길다는 단점을 뺀다면 국내 구매랑 크게 차이 없이 편리하게 구할 수 있었습니다. 비록 발매된지 6년이 지났지만, 해외 구매 기념 삼아 복각판 게임큐브 컨트롤러 오픈 리뷰를 진행해보겠습니다.

 

 

 현지에서 판매하는 패키지 그대로 발송되었습니다. Ailexpress에서 판매되고 있는 복제품은 포장 없이 컨트롤러만 딸랑 오는 것과는 큰 차이가 있네요.

 

 

 닌텐도 스위치에 게임큐브 컨트롤러를 연결하기 위해서는 별도로 판매중인 어댑터를 사용해야 합니다. 정품은 아직도 10만원대에 판매되는 것으로 보이는데 알리에서 판매중인 복제품을 사용할 수 있을까요?

 

 

 게임큐브 컨트롤러가 지원되는 하드웨어 목록. 게임큐브와 Wii의 경우 하드웨어 자체에 포트가 존재하여 바로 꽃아서 사용할 수 있습니다. Wii U와 Switch의 경우 앞에서 말씀드린 대로 별도의 어댑터를 추가 구매해야 합니다.

 

 

위쪽 측면에서 바라본 패키지의 모습입니다.

 

 

이번에는 내용물을 직접 확인해보겠습니다.

 

 

비록 게임큐브는 Wii가 발매된지 1년 후인 2007년부로 생산이 중단되었지만, 스매시 브라더스의 인기 덕분에 게임큐브 조작을 위해 만들어진 게임큐브 컨트롤러는 2024년 현재까지 무려 23년을 현역으로 활동하고 있습니다. 어찌보면 격세지감이 느껴지는 순간입니다.

 

 

게임큐브컨트롤러의 뒷면. 닌텐도 로고가 선명하게 각인되어 있습니다.

 

 

Aliexpress에서 구매하였던 복제품과의 비교. 복제품의 경우 닌텐도 로고가 없습니다.

 

 

 정면 비교샷. 복제품과 외형상 차이는 없습니다.

 

 

성능 테스트를 위해 Wii를 킨 다음

 

 

Wii의 상단 덮개를 열어 게임큐브 컨트롤러를 연결해줍니다.

 

 

게임큐브 컨트롤러가 오늘날까지도 현역으로 뛸 수 있게 해준 스매시 브라더스를 구동해보겠습니다.

 

 

확실히 스매시브라더스 시리즈는 게임큐브 컨트롤러를 극한으로 사용하고 있음을 체감할 수 있었습니다. Wii 리모콘으로도 조작환경을 제공하지만 C스틱으로만 할 수 있는 스매시 기능들을 사용할 수 있는게 굉장히 편했습니다. 

 무엇보다도 조작감이 복제품과는 차원이 달랐습니다. 복제품의 아날로그 컨트롤러는 살짝 밀어보면 고정이 살짝 느슨한 느낌이라면 정품 컨트롤러는 아날로그 스틱은 그러한 느낌이 없습니다. 즉, 정품 컨트롤러는 미세한 방향도 제어가 잘 되는 느낌이라 보시면 됩니다.

 또 한가지 다른 점으로  버튼을 누를때의 느낌입니다. 복제품의 경우 A버튼을 누를때마다 나는 똑딱 소리가 너무나 당연하게 생각했던 제가 정품 컨트롤러를 사용할 때 버튼을 누를때의 소리가 작은데다 누를때의 느낌이 확인히 달랐습니다. 복제품보다 푹신하게 눌리는 버튼의 느낌이 있습니다.

 

 

확실히 게임 플레이는 복제품과는 확실히 다릅니다. 아날로그 컨트롤러가 잘 먹혀 지금껏 알리에서 샀던 복제품과 달리 후회는 없습니다.

 

 

혹시 정품 게임큐브 컨틀롤러 구매를 망설이시는 분이라면 저는 주저 없이 구매를 적극 추천드리고 싶습니다. 정품만의 가치는 말할것도 없고 무엇보다 Aliexpress에서 불량품을 맞이하는 것을 원치 않는 분이라면 꼭 정품을 구매하셔서 쾌적한 게임플레이를 하시기를 강력히 추천드립니다!

300x250

Windows Powershell에서 python 실행시 환경변수 설정 방법

공대생의 팁 2024. 5. 28. 19:38


Windows 운영체제에서 딥러닝 라이브러리를 학습할 때 2024년 5월 현재 상황에서는 멀티 GPU 학습이 지원되지 않아 단 하나의 GPU에서만 학습을 수행할 수밖에 없습니다. 그나마 Windows 11 이후 WSL을 통해 Linux 환경에서 학습을 수행할 수 있게 되면서 Windows 운영체제에서 멀티 GPU 학습을 할 수 있는 방법이 생겼습니다.


 그러나, Linux 환경에서 단지 한 줄 터미널에 입력하여 실행할 수 있는 명령어들이 Windows에서는 그대로 사용할 수 없어 안타까운 상황들이 아직까지도 종종 있습니다. 예를 들어, 여러대의 GPU가 설치된 PC에서 특정 GPU만 사용하여 학습을 수행할 때 Linux 환경에서는 Terminal에서 아래와 같이 명령어를 입력하면 됩니다.

 

CUDA_VISIBLE_DEVICES=1 python main.py


 그렇다면, Windows에서 위와 같은 기능으로 학습을 수행하려면 어떻게 해야할까요?  Windows에서 특정 GPU에서 학습을 수행하고자 할 때 여러가지 방법이 있는데 첫 번째는 실행하고자 하는 Python 소스코드를 직접 수정하는 것입니다.

 

import os
os.environ['CUDA_DEVICE_ORDER'] = 'PCI_BUS_ID'
os.environ['CUDA_VISIBLE_DEVICES'] = '1'


소스코드에서 직접 설정해서 수행할 수 있어 특정 소스코드를 명시적으로 실행할 수 있으나, 이 경우 소스코드를 직접 수정해야 하는 번거로움이 있습니다.


 다른 방법으로는 학습하고자 하는 명령어를 여러 번 사용하는 경우 Shellscript를 작성하여 필요할 때 마다 Powershell에서 해당 Shell을 수행하는 방법이 있습니다.
예를 들어 아래와 같이 Shell을 작성합니다.


exec_train.ps1

$env:CUDA_VISIBLE_DEVICES = "1"
python main.py

 

 생성된 ShsellScript를 Powershell에서 다음과 같이 실행합니다.

> ./exec_train.ps1

 

위에서 설명드린 방법대로 수행하신다면 Windows 환경에서 멀티GPU를 개별로 다룰 수 있습니다.

300x250

게임큐브 컨트롤러를 PC에 연결해 Dolphin 에뮬레이터에서 사용해보기

흔치않은일상 2024. 4. 7. 11:17


 얼마전 Aliexpress에서 게임큐브 컨트롤러와 PC에 연결하여 사용할 수 있게 해주는 어댑터를 구매했습니다. 본래 일본에 방문했을 때 게임샵에서 구매하려 했습니다만, 발매된지 20년이 지난 게임큐브임에도 중고 컨트롤러 가격이 3천엔이며, 심지어 닌텐도 스위치에 연결하여 사용할 수 있는 컨트롤러 어댑터도 정품은 우리나라 돈으로 10만원이 넘는 가격이어서 감히 살 엄두가 나지 않았는데, Ailexpress에서 개당 5천원에 판매하고 있어 망설임 없이 구매하였습니다.

 이번에 구매한 GameCube Controllers Adapter는 품명 FY-L-1208 인 제품으로, Aliexpress에서 개당 7천원에 구매하였습니다. 정품 가격을 생각하면 이 가격에 구매한 것이 살짝 믿어지지 않아 혹시나 싼게 비지떡인 상황이 발생하지 않을까 걱정이 되기도 했습니다.

 

 

둘다 닌텐도 정품은 아니지만 품질은 생각보다 정품과 비슷했습니다. 흡사 외관상 정품로고만 없는 정품같아 보이기도 합니다. 싼 맛에 Aliexpress에서 구매한다지만 이정도면 가성비가 상당히 좋아보입니다.

 지금 당장은 닌텐도 스위치를 구매하지 않은 상황이어서 PC에 돌핀 에뮬레이터를 설치하고 한 번 플레이 해보고자 컨트롤러 어댑터를 PC모드로 한 다음 PC에 연결하였습니다.

 

 

컴퓨터 설정 메뉴에서 장치를 확인하였을 때 기타 디바이스로 'WUP-028'로 나타나 정상적으로 설치가 된 것으로 보였습니다.

 

 게임을 플레이하기에 앞서 구매한 게임큐브 컨트롤러가 정상적으로 동작을 확인하기 위해 관련 설정에서 '장치 및 프린터'를 클릭합니다.

 

 

장치 및 프린터에서 'WUP-0228' 장치를 확인 후 우클릭 하여 '게임 컨트롤러 설정'을 클릭해주시면

 

 

아래와 같이 게임 컨트롤러 설정창이 나타납니다. 

 

 

 4개의 컨트롤러는 각각 1P에서 부터 4P까지 나타내는 것으로, 자신이 연결한 컨트롤러를 클릭해주시면 컨트롤러가 정상적으로 동작하는지 확인하실 수 있습니다.

 

 

 위의 방법을 통해 Aliexpress에서 구매한 게임큐브 컨트롤러 및 어댑터가 정상적으로 동작하는 것을 확인하였습니다. 그런데 이상하게도 돌핀 에뮬레이터에서는 게임큐브 컨트롤러를 인식하지 못하고 있는 것이었습니다.

 

 

 확인해보니 Dolphin 에뮬레이터에서 게임큐브 컨트롤러 어댑터를 통해 조작이 가능함을 확인할 수 있었는데, Windows 환경에서는 USB 설정을 변경해서 사용할 수 있다고 합니다. 이번에 제가 구매하였던 어댑터같은 경우, 어댑터 모드를 PC모드와 Wii U(닌텐도 스위치)모드를 스위치로 설정할 수 있도록 구성되어 있는데, Dolphin 에뮬레이터에서 사용하기 위해서는 PC모드가 아닌 Wii U(닌텐도 스위치) 모드로 설정해서 연결한 다음, USB 드라이버를 변경해주면 된다고 합니다.

 

 앞에서 진행하였을 때 설치하였던 'WUP-028' 장치를 우클릭하여 '장치 제거'를 해주신 후 어댑터의 스위치를 Wii U(닌텐도 스위치) 모드로 변경후 다시 연결해주시면

 

 

 위의 그림처럼 지정되지 않은 드라이버로 나타나는 것을 확인하실 수 있습니다. 드라이버의 설정을 변경하기 위해 아래의 Zadig을 다운로드하여 실행해줍니다.

https://zadig.akeo.ie/

 

Zadig - USB driver installation made easy

 Zadig USB driver installation made easy Zadig is a Windows application that installs generic USB drivers, such as WinUSB, libusb-win32/libusb0.sys or libusbK, to help you access USB devices. It can be especially useful for cases where: you want to access

zadig.akeo.ie

 

 Zadig를 다운로드 후 실행하면 아래와 같은 화면이 나오는 것을 확인하실 수 있습니다.

 

 

 Option → List All Device를 선택하여 PC에 연결된 USB 기기들의 리스트를 모두 확인합니다.

 

 

 List에서 게임큐브 컨트롤러 어댑터 기기명인 'WUP-028'을 선택하신 다음, USB ID가 057E 0037인지 확인해줍니다. 만약 목록에 'WUP-028'이 나타나지 않을 경우, USB를 다른 포트에 연결해봅니다.

 

 

 확인하셨다면, WinUSB를 선택한 다음 Replace Driver를 선택합니다.

 

 

 아래와 같이 Warning - System Driver 경고문이 나타나면 '예(Y)'를 클릭해줍니다.

 

 

 아래와 같은 화면이 나오고 약 5분 정도 기다려주시면

 

 

 아래와 같이 설치가 완료된 것을 확인하실 수 있습니다.

 

 

 다시 Dolphin 에뮬레이터를 켠 다음 컨트롤러 설정에서 포트를 'Wii U용 게임큐브 어댑터'로 설정 후 '설정' 버튼을 클릭하시면 '어댑터가 감지되었습니다'가 표시됨을 확인하실 수 있습니다.

 

 

 

 

 

참고자료: https://ko.dolphin-emu.org/docs/guides/how-use-official-gc-controller-adapter-wii-u

 

돌핀 에뮬레이터 - How to use the Official GameCube Controller Adapter for Wii U in Dolphin

Official GameCube Controller Adapter for Wii U As of 4.0-4599, Dolphin has built in support for Nintendo's GameCube controller adapters for the Wii U and Switch, the only official USB GameCube adapters available. With Dolphin's implementation, the GameCube

ko.dolphin-emu.org

 

300x250

[mmcv] AssertionError: only one of size and size_divisor should be valid

공대생의 팁 2024. 3. 31. 00:22

 

 MMSegmentation으로 딥러닝 모델을 실행하는 과정에서 종종 아래와 같은 에러를 접하곤 합니다.

 

    assert (size is not None) ^ (size_divisor is not None),
AssertionError: only one of size and size_divisor should be valid

 

MMSegmentation github 사이트에서 관련 이슈로 등록된 글을 통해 확인해본 결과, data_preprocessor에서 size 혹은 size_divisor가 설정되어 있지 않아 발생하는 오류라고 합니다. 아래와 같이 data_preprocessor에 size를 추가 설정해주니 소스코드가 정상적으로 동작하는 것을 확인하였습니다.

 

crop_size = (512, 512)
data_preprocessor = dict(
    # ....
    size=crop_size)

 

 

참고자료: https://github.com/open-mmlab/mmsegmentation/issues/3425

 

AssertionError: only one of size and size_divisor should be valid · Issue #3425 · open-mmlab/mmsegmentation

I am trying to use mmsegmentation for segmentation of pathology images on my own new dataset. I have 512x512 patches of images and annotations. I am representing four classes of my new dataset as g...

github.com

 

300x250

LabelMe로 Coco 데이터셋 변환후 MMDetection에서 학습이 안될 때 해결 방법

공대생의 팁 2024. 3. 26. 20:54

 

 요새 Dataset Lebeling 작업을 수행하는 과정에서 다양한 방식의 Label 구조 형식들을 접해보고 있습니다. Bounding Box 방식과 같이 단지 마우스 2번의 클릭으로 끝나는 경우가 있는가 하면, Polygon 방식과 같이 사진 내에 있는 모든 Object의 테두리에 점을 하나하나 공들여서 표시해보기도 합니다. Labeling 작업에 왕도는 없지만, 좀 더 데이터를 다루는 것에 초점을 맞추어 데이터의 속성을 이해하여 좀 더 적합한 모델을 찾는데 도움이 되기도 합니다.

 

 2024년 현재도 좋은 AI 모델이 소개되고는 있지만 정제되지 않은 데이터로는 좋은 성능의 AI 모델을 구현할 수 없습니다. 비록 데이터셋 제작에 많은 시간이 소모되더라도 결국은 데이터셋의 품질이 AI모델의 성능을 좌우하는 데엔 그 누구도 이견이 없을 것입니다.

 

 평소와 다름없이 LabelMe로 Labeling한 JSON 파일들을 Coco Dataset으로 변환한 다음, MMDetection으로 모델을 학습시키려 하는 과정에서 다음과 같은 에러를 접하게 되었습니다.

 

ValueError: need at least one array to concatenate

 

해당 오류의 원인을 찾아보니 MMDetection에서 원본 coco 데이터셋의 category를 기준으로 설정되어 있어서 우리들이 직접 만든 custom coco dataset의 class명이 원본 coco 데이터셋의 class명과 다를 경우 위와 같은 오류가 나타나는 것이었습니다. 이 문제는 LabelMe로 만든 coco 데이터셋의 class명을 원본 coco 데이터셋과 일치시키면 바로 해결됩니다.

 

 비록 학습시에는 원본 coco 데이터셋의 class명이 출력되시만, 모델 Deploy 수행시 class명을 변경해서 사용할 수 있기 때문에, 학습이 진행되는 동안 출력되는 class명이 다르게 되는 불편함을 감수하실 수 있다면 아래의 소스코드를 다운로드 받은 다음 LabelMe로 작업한 JSON 파일을 변환하여 사용해보시기 바랍니다. 

   If you get an error like the one above when training mmdetecton on a coco dataset that you converted to LabelMe, download labelme2coco.py below and convert it to that file, and you should be able to train mmdetection cleanly and without errors.

 

labelme2coco.py
0.01MB

 

 

위 소스코드를 다운로드 받으신 다음 LabelMe에서 사용하던 대로 명령어를 입력합니다.

 

python labelme2coco.py [변환할 이미지 및 JSON 폴더] [coco 데이터셋 변환 폴더명] --labels labels.txt

 

 

  아래는 LabelMe에서 제공하는 예제입니다.

 

python labelme2coco.py data_annotated data_dataset_coco --labels labels.txt
300x250