2025년 12월 31일 수요일

2025년 마지막 독서 기록 - 책임을 회피하기 위한 글쓰기

오늘 제목으로 잡은 '책임을 회피하기 위한 글쓰기'는 내가 읽은 책의 제목은 아니다. 챗GPT와 더불어 작업을 하다가 받은 문화적 충격이 최근 읽은 책의 내용과 상당히 맞닿아 있어서 이를 되새기면서 올해의 마지막 독서 기록을 쓰기로 했다. 

2025년에는 Korg X2 수선과 개조, 아두이노 나노를 이용한 드럼 패턴 재생기 DIY 및 프로그래밍 작업에 몰두하면서 상대적으로 독서에는 덜 열중하였다. 지역 도서관에서 책을 빌리면 대여 기간을 연장하여 3주 정도에 걸쳐 읽고는 하였다. 때로는 빌려온 책을 다 읽지 못하고 반납하는 일도 많았다. 그래도 이번에 빌린 세 권의 책은 전부 읽었기에 2025년의 마지막 독서 기록을 남기기에 부족함은 없다.

  • 목 이야기('생물학적 기능에서 사회적 상징까지 목에 대한 모든 것') - 켄트 던랩 글, 이은정 번역
  • 이병한의 테크노-차이나 탐문 - 이병한 글
  • 포식하는 자본주의('자기 기반을 먹어치우며 작동하는 자본주의에 관한 두 철학자의 대화') - 낸시 프레이저, 라엘 예기 글, 장석준 번역 


깃허브(https://github.com/jeong0449/NanoArdule)에 올릴 코드와 문서 작업을 하면서 챗GPT에게 어떤 취지의 글을 영문으로 작성해 달라고 부탁하였다. 이야기는 여기에서 시작된다.


순간 나는 내 눈을 의심하였다. "책임회피·설계 한계 설명 톤을 유지했습니다". 책임회피라니? 글 작성을 위해 프롬프트를 입력하는 사용자들이 얼마나 많은 이런 요구를 했으면 챗GPT가 알아서 작업을 해 주었겠는가? 법적 책임을 지지 않기 위해서 극도로 언행을 조심해야 하고, 부족한 부분이나 분쟁이 생길 만한 영역에서는 변호사들이 활동하는 그런 사회를 따라서 하는 것이 과연 옳은 일인지 의문이 들었다.

나는 '책임회피'라는 단어로부터 쿠팡 개인정보 유출사건을 다루기 위한 국회 청문회에 나온 헤럴드 로저스 쿠팡 임시 대표(역시 그런 국가의 일원일 것이다)의 태도가 떠올랐다. 대국민 사과 같은 것은 일절 없이 그저 자체 조사에서 혐의자를 특정하고 문제를 일으킨 노트북 컴퓨터를 찾아냈으며 포렌식 결과 겨우 3천명의 개인정보가 유출(처음에는 '노출'이라는 부적절한 용어 사용)된 것에 불과하다고 애써 무마하려는 무책임한 태도. 만약에 우리 정부가 더 강경한 입장을 취할 것 같으면 미국 언론이나 정부를 앞세워서 통상 분쟁 문제로 만들려는 조짐이 강하다. 미국 정계에서 공식화되어 있는 로비스트의 역할이 클 것이다. 대부분의 이익을 한국에서 취하면서도 본사가 다른 나라에 있다는 이유로 실효성 있는 제재를 하지 못하는 현실이 서글프다. 혹시 그들의 사고 체계 안에는 원조로 겨우 연명하던 아시아의 보잘것 없는 나라가 이렇게 성장하여 감히 미국 국적의 글로벌 기업 대표를 오라가라 한다는 오만한 생각이 들어 있는 것은 아닐까.

챗GPT가 제공한 영문을 잘 읽어보면 '매뉴얼 리뷰가 여전히 필요하다'고 한 것이 전부이다. 다시 곱씹어서 읽어보니 '책임회피'라는 부가 설명을 덧대면서 나의 감정을 지나치게 자극한 면이 없지 아니하다. 특히 최근 벌어진 사건을 자연스럽게 연상하게 되면서 감정적 해법을 거의 고려하지 않는 서구 '선진' 사회의 문화에 대한 비판적인 생각까지 연결된 것이다. 그러나 이러한 연상의 흐름은 전혀 무익한 것이라고는 생각하지 않는다. 

≪이병한의 테크노-차이나 탐문≫ 41-42쪽에 이런 글이 나온다.

(68혁명 이후로) 엔지니어가 아니라 로이어(Lawyer), 법률가의 전성시대가 열린 것이다...엔지니어들은 결과와 성과를 중시하지만, 로이어들은 절차와 과정에 집착한다... 로이오둘운 추상적인 말로 먹고 사는 사람들이다. 입심이 입진보를 낳고, 고소·고발이 난무하는 법률가 사회를 추동한다...의회의 법정화, 정치의 사법화, 미국의 정치 수준이 갈수록 저열화되고 있는 근간이라고 할 수 있다...인문·사회 전문가들은 한줌의 이론과 이념으로 세상만사를 다 설명하려 든다.

취미와 관련한 아주 세부적인 기술 분야에 대하여 글을 쓰다가 예기치 않은 방향으로 이끄는 새로운 단서를 접하였고, 이는 2025년을 마무리하면서 읽은 책의 주제와도 깊은 관련이 있음을 깨닫게 되었다. 대화 형식으로 쓰여졌지만 아주 쉽게 읽히지는 않는 책 ≪포식하는 자본주의≫ 또한 현 자본주의 체제에 대한 강한 비판의식을 담고 있다. 

자본주의 아래에서 우리는 자신이 누구인지, 혹은 어떤 사람이 되려 하는지에 대한 근본적 결정, 그리고 삶의 형태가 어떠해야 하는지에 대한 근본적 결정에 참여할 능력이 없다고 치부돼요. 그래서 우리의 민주주의는 처참하게 훼손되죠. 방금 말한 종류와 규모의 결정은 본래 민주적으로 조직되어야 하거든요. 자본주의는 정치 의제를 제한함으로써 민주주의의 날카로움이 무너지게 만들죠. 자본주의는 마땅히 중요한 정치 사안으로 다뤄져야 할 것을 '경제적'이라 치부한 뒤 '시장의 힘'에 맡겨버려요. 하지만 이게 전부가 아니죠. 사회적 잉여의 사적 전유는 우리의 자율성, 즉 집단적인 삶의 과정의 공동 창작자라는 적극적인 역할을 떠맡을 집단적 능력 또한 제한해요. 자본주의는 위가 사회의 잉여와 관련하여 자율성을 행사하지 못하게 막아요. 말하자면 여기에는 최소한 세 가지 관념, 즉 참여, 민주주의, 자율성이 함축돼 있죠. (242쪽)

오늘 제시한 두 가지 문제점은 우리가 글로벌 스탠다드라 여기고 따라하려는 어느 강대국의 현재 모습과 너무나 닮았다. 우리는 더 나은 대안을 요구한다. 자본주의와 시장은 결코 정치체제와 분리해서 볼 수 있는 것이 아니다. 수은주가 생태적 한계를 향해 점차 치솟고 있는 요즈음, 쿠팡을 탈퇴함으로 인하여 포장 쓰레기는 현저히 줄었지만 더 편리하고 풍요로운 사회를 향해 어쩌면 스스로를 잠식하고 있는 현대 문명을 과연 우리 개인이 어떻게 개선할 수 있을지 잘 모르겠다.

오늘의 포스팅은 의도적으로 질서 없이 여러 주제를 기워서 만든 글이다. 내년에는 보다 진지하고 고민을 넘치게 하는 책을 찾아서 읽고 싶어졌다. 

2025년 12월 28일 일요일

오묘한 드럼 패턴학 - ADT 체계의 다음 단계를 고려해야 한다

다음 이미지에서 보인 13개의 패턴 중에서 ‘브레이크(Break)’에 해당하는 것은 무엇일까?

먼저 필-인브레이크의 간단한 용어 정의부터 하고 넘어가자.
  • Fill-in: 이어지기 위한 변화. 반복해도 어색하지 않음.
  • Break: 끊기 위한 변화 (보통 다음 구간을 전제로 존재). 루프로 사용하기에는 바람직하지 않음.

위 이미지는 내가 만든 스크립트의 결과물이다. 드럼 패턴 MIDI 파일을 2-bar 단위로 분할한 뒤 시각화하여, 눈으로 확인하기 쉽게 PDF로 전환한 것이다. 눈으로 패턴을 보고, 귀로 들어서 브레이크에 해당하는 것을 대략 구별할 수는 있지만, 100% 정확하다고 보기는 어렵다. 이 PDF 파일을 ChatGPT에 업로드한 뒤 그루브와 브레이크를 구별하라고 하면 나름대로의 기준을 적용하여 꽤 그럴싸한 판별을 해 준다. 

위 드럼 패턴 자료는 인터넷에서 200 Patterns라는 이름으로 돌아다니는 것이다. 한편 260 Patterns라는 이름으로 유통되는 파일은 원본 PDF 자료에서 어떤 패턴이 브레이크인지 표시해 두었지만, 정확한 매칭에는 여전히 약간의 수고가 필요하다. 예를 들어 6MISC2.MID 파일은 Ska, Twist, Waltz 패턴이 섞여 있어 파일명만으로는 내용을 파악하기 어렵다. 반면 6ROCK.MID는 파일명에서 장르를 추정할 수 있으며, 14개의 2-bar 패턴이 모두 같은 장르라 작업이 매우 수월했다. 실제로 인터넷에서는 200 + 260 + Rap pattern을 합쳐서 MIDI 파일 묶음으로 돌아다니고 있으며, 원본 자료(책 두권의 PDF)는 따로 돌아다닌다. 

6FUNKBR.MID

이는 지금 언급한 파일 묶음을 풀어서 나오는 MIDI 파일의 한 사례이다. 파일명으로부터 Funk Break 패턴임을 어렴풋이 알 수 있다. 그러나 'BR'은 뭘까? 만약 원본 PDF 자료가 없었다면, 나의 드럼 패턴 분류 체계는 영원히 불완전했을지도 모른다. 이는 Funk Break 패턴을 의미하는 것이었다. 만약 파일 하나에 담긴 패턴이 모두 동일 장르였다면 자동화가 훨씬 쉬웠겠지만, 앞서 언급했듯이 6MISC1.MID, 6MISC2.MID라는 파일은 여러 장르의 패턴이 섞여 있었기 때문에 완전 자동화는 쉽지 않았다.

아직 해결되지 않은 또 다른 문제는 3/4 박자의 왈츠 패턴이다. 현재 나의 ADT 체계는 3/4 박자를 직접 수용하지 못하며, 다만 MIDI 파일 병용으로 임시 대응하고 있다.

또 하나의 문제는 ‘flam’ 처리이다. 플램은 드럼 연주에서 매우 짧은 꾸밈음에 해당한다.

출처: Rene-Pierre Bardet, 260 Drum Machine Patterns, p.14

위의 드럼 패턴 그리드에서 F로 표시된 것이 플램이다. 이 책은 1987년에 발간되었으며, 드럼 머신의 황금기를 반영하고 있다.

나의 ADT 체계에서는 1마디를 16·12·24 스텝 중 하나로 해석한다. 그러나 플램을 정규 노트로 처리하면 필요 이상으로 24스텝 체계로 세분화되는 문제가 발생한다.

ADT 체계의 드럼 패턴은 2-bar를 기본 단위로 한다. 대부분의 경우 두 마디는 동일하지만, APS 프로그램 안에서 스텝 단위로 편집하거나 두 패턴의 마디를 하나씩 이어 붙여서 하이브리드 패턴을 만들 수 있다.

오늘의 글에서는 MIDI 파일에서 ADT를 생성하는 과정에서 발견한 세 가지 문제를 정리해 보았다. 첫 번째는 수작업으로 해결 가능하지만, 나머지 두 문제는 ADT의 차기 메이저 버전에서 반드시 개선해야 한다.

다음은 플램 처리에 대한 하나의 현실적인 제안이다.

서브스텝(마이크로타이밍) 레이어 방식

플램은 “메인 스텝 직전의 매우 짧은 노트”이므로, 그리드를 더 쪼개지 말고 부가 정보로 장식 이벤트를 기록하는 방식이다.

@ORN
# format: lane, step, type, level, offset
SD, 12, FLAM, -, -1/6
  • offset = -1/6은 스텝 내부에서 얼마나 앞당길지를 의미한다
  • flam뿐 아니라 drag, ruff, roll로 확장 가능
  • 단점: 플레이어와 APS가 모두 이를 인식해야 함

현실적인 절충안: offset 값을 자유 실수가 아니라 { -1/4, -1/6, -1/8 } 같은 제한된 enum으로 두면 임베디드 구현이 쉬워진다.

2025년 12월 22일 월요일

2025년을 마감하는 공연 - 미리 크리스마스(김천 공동음악구역)

전혀 계획에 없던 일을 저지르는 재미, 그것이 인생이다. 경상북도 김천이란 도시는 나에게는 그저 직지사, 또는 최근에 회의 참석을 위해 들렀던 농림축산검역본부로만 기억되는 곳이었다. 가끔 경부고속도로를 지나면서 길가에 위치한 김천대학교와 경북보건대학교를 무심히 바라보고는 했었다. 바로 그 근처 삼락동에 위치한 공동음악구역이라는 곳에서 연합 공연을 하게 될 줄이야. 모든 악기와 음향 시설이 완벽하게 갖추어져 있어서 기타/베이스만 들고 가면 되는 곳이다.

2025 '미리' 크리스마스(MIRRY CHROCKSMAS)


지난 토요일, 그러니까 2025년 12월 20일에 열린 이 행사를 주관한 곳은 공동음악구역오원장TV(유튜브 채널)이다. 김천과 대구 등에서 밴드 음악을 하거나 실용음악학원을 운영하는 사람들이 모여 연말에 이런 공연을 한다. 내가 활동하는 밴드 KRIBBtonite의 드럼 주자가 이곳과 연결이 되어 있어서 참가를 권유받고 어렵게 결정을 내리게 된 것이다. 왜냐하면 우리 밴드는 올해 결성되어 연구소 내부 행사만 몇 차례 치른 경험밖에 없었기 때문이다. 멤버를 추가 모집하여 현 체제를 갖추기 전인 작년부터 조금씩 내부 행사를 해 왔지만, 밖에 나가서 연주를 한다는 것은 상상도 하지 못했었다. 최근의 밴드 활동 경험은 소규모 공연을 위한 음향장비 세팅이라는 별도의 글에 기록해 두었다.


바쁜 연구소 업무 중에도 첫 외부 공연에 참여하기로 결정하고 업무 시간을 쪼개어 연습에 매진해 준 멤버들이 정말 고맙다. 아래 영상은 서울에서 활동하는 프로페셔널 록밴드 No Limit Amazing의 리허설 모습을 내가 직접 찍은 것이다. 그 다음 것은 우리 팀의 리허설 영상이다. 



전체 공연 실황 영상(4K, 컷편집, 마스터)유튜브('오원장TV')에 공개되어 있다. 이른 저녁에 시작하는 공연을 위하여 음향 엔지니어링, 동영상 촬영, 본 행사 진행 및 연주까지 담당하느라 공연 후 뒤풀이가 있을 때까지 하루 종일 아무것도 잡숫지 못하고 애쓰신 '오원장'님 덕분에 좋은 기록이 남았다. 본 행사를 기획하고 장소 제공은 물론 맛있는 음식으로 출연진들을 대접해 주신 공동음악구역 김태홍 대표에게도 감사의 뜻을 전한다. 출연진 중에는 전문 음악인도 있지만 대부분 직장생활이나 학업을 병행하면서 음악에 뜻을 두고 있는 순수한 사람들이었다.

방구석 음악인에서 머물지 않고 실제로 세상과 교류하는 접점을 김천시에서 경험하게 되다니. 너무 늦게 '사회'에 나선 것이 억울할 지경이다. 이 값진 경험을 앞으로 더 키워 나가고 싶다.

 

달리기는 여전히 계속된다

요즘은 '하뛰하쉬', 즉  하루 걸러 하루씩 뛰는 것을 꼭 고집하지는 않는다. 피로감이 사라지지 않거나 날씨가 허락하지 않으면 쉬는 날을 좀 더 길게 둔다고 하여 아쉽게 생각하지 않기로 했다.

2025년 12월의 달리기 기록. 길게는 삼일을 쉬기도 하였다.

바야흐로 완전한 겨울, 달리기에 썩 좋은 날씨는 아니다. 간단한 아령 운동과 스트레칭 및 리버스 런지와 스쿼트로 워밍업을 한 뒤 기온을 확인한 다음 겨울철 야외 달리기에 적당한 옷을 갖추어 입어야 한다. 너무 추워도 안되고, 너무 덥게 껴입어서 땀을 많이 흘려도 좋지 않다. 모자와 장갑이 필수가 되었고, 어젯밤과 같이 영하 2도 정도라면 아직 마스크는 필요하지 않다. 밤길을 달리며 마주치는 심야 러너 중에는 모자를 착용하지 않은 사람도 많다.

가뜩이나 어두운 갑천변 밤길을 달리는 것은 참 외롭고 쓸쓸하다. 그러나 밤에 달리는 것은 그 나름대로 장점이 있다. 바람이 적게 불고, 피부가 햇볕에 그을릴 일도 없으니 말이다. 단, 운이 나쁘면 달린 뒤 지나친 각성 상태가 되어 잠을 편히 자기가 힘들 때가 가끔 있다.

6분 30초의 아주 편안한 페이스.


어젯밤의 7.61km 달리기에서는 약 500Kcal를 소모하였다. 양념치킨 두 조각, 김밥 한 줄에 해당한다. 무심코 집어먹은 과자 몇 조각에 포함된 칼로리를 태우려면 이렇게 많은 노력을 해야 된다. 간식을 좀 많이 먹었다거나 또는 회식을 하고 나면 반드시 달려야 되겠다는 의지가 솟아나는 버릇이 생겼으니 참 좋은 일이 아니겠는가.



2025년 12월 18일 목요일

오픈 드럼 패턴 데이터, 그리고 APS(Ardule Pattern Studio)에 대한 소개

Ardule 생태계의 중심에는 오픈 데이터 포맷인 ADT가 자리잡고 있다.

APS(Ardule Pattern Studio)(GitHub)는 드럼 패턴 중심 워크플로우를 위한 프로그램이다. 보다 쉽게 표현하자면, PC에서 돌아가는 파이썬 프로그램으로서 자체 정의한 2-bar 드럼 패턴 파일(ADT)을 열람하고, 이를 조합하여 곡 단위의 정보를 만들거나 패턴 단위의 편집을 수행하는 작은 프로그램이다. 물론 재생 기능도 갖고 있다.

ADT 파일은 당초 아두이노와 같은 제한된 자원의 임베디드 환경에서 SD카드에 저장된 드럼 패턴을 효율적으로 재생하기 위해, MIDI 시퀀스에서 드럼 패턴을 분리·재사용하는 것을 목표로 개발되었다. 전체 곡 단위의 MIDI 데이터를 그대로 처리하기에는 메모리와 처리 능력이 제한적인 환경을 전제로, 드럼 연주를 짧고 반복 가능한 패턴 단위로 분해하고 이를 조합하여 곡 구조를 구성하는 방식이 설계의 출발점이었다. 이 과정에서 사람과 기계가 모두 이해하기 쉬운 텍스트 기반 형식을 채택함으로써, 특정 DAW나 하드웨어에 종속되지 않고 드럼 패턴을 축적·편집·변환할 수 있는 오픈 드럼 패턴 데이터 포맷을 지향하였다.

이러한 목표에 따라 ADT는 공개된 포맷으로 정의되었으며, MIDI 파일로부터 드럼 패턴을 추출하여 ADT로 변환하는 데 필요한 모든 스크립트('adc-'  스크립트 묶음, GitHub)가 함께 제공된다. 생성된 ADT 파일은 APS뿐 아니라 일반적인 텍스트 편집기로도 직접 편집할 수 있다.

APS는 이러한 ADT 포맷을 단순히 보조적으로 다루는 도구를 넘어, 임베디드 타깃을 염두에 둔 드럼 패턴 제작과 관리를 PC 상에서 완결적으로 수행할 수 있는 stand-alone 프로그램으로 발전하였다. 초기에는 패턴 파일을 확인하고 조합하는 유틸리티에 가까웠으나, 현재는 패턴 단위 편집, 곡 단위 구조 구성, 그리고 즉각적인 재생을 포함하는 독립적인 작업 환경을 제공한다. 샘플 패턴 파일은 GitHub를 통해 제공되며, 사용자는 이를 기반으로 자신만의 패턴 라이브러리를 확장해 나갈 수 있다.

APS 매뉴얼은 현재 초안 수준으로 작성 중이다. 아직 아이디어 단계의 기능을 더 구현해 넣은 뒤 마크다운 문서로 정리하여 GitHub에 밀어넣는 숙제가 남았다.

검색을 해 보니 ADT는 다른 뜻을 갖는 약자이기도 하다. 아, 조금 늦었네...

Enhanced Automatic Drum Transcription via Drum Stem Source Separation aRxiv 2025년 9월 25일.

박사학위 논문도 있다! Deep Learning Methods for Drum Transcription and Drum Pattern Generation. PDF 링크.

Automatic Drum Transcription, 즉 '자동 드럼 트랜스크립션'이란 오디오 녹음에서 드럼 소리를 자동으로 분석하여 악보 또는 MIDI 데이터와 같은 디지털 형식으로 변환하는 기술이라고 한다. 인공지능에 크게 의존할 것 같다.

2025년 12월 16일 화요일

NanoArdule 프로젝트를 GitHub에 올리다


 

나에게 GitHub란 '남이 만든' 소프트웨어를 업무에 쓸 목적으로 일방적으로 가져오는 저장소에 지나지 않았다. 여기서 업무란 주로 유전체 정보 분석에 관한 일이다. 그러던 내가, Nano Ardule Drum Pattern Player를 만들면서 작성한 코드를 공유하기 위해 스스로 GitHub에 계정을 만들고 파일을 업로드하기 시작하였다. 계정을 만든 지 오래되었지만 이제 본격적인 repository를 만든 것이다. 실은 지난주에 저해상도 로고 이미지를 수정한 것도 GitHub의 프로파일 이미지가 중앙에서 치우져 있는 것이 보기 싫어서였다.

https://github.com/jeong0449/NanoArdule "Ardule Project - Arduino Nano-based Drum Patternology Ecosystem"

인터넷에서 관련 정보를 찾아 더듬더듬 사용법을 익히던 conda와는 달리, git 명령어와 GitHub는 그럴 수 있는 수준이 아닌 것 같다. 일단 기본 개념을 빨리 잡기가 어렵다. Pro Git와 같이 완벽한 레퍼런스가 PDF로 공개되어 있다고는 하나, 인쇄된 책 한권을 바로 곁에 두고 참조하면 효율이 더 오를 것이다. 다음 웹사이트도 매우 깔끔하게 Git을 잘 설명하고 있다.

Roger Dudler의 git - 간편 안내서

GitHub에 repository를 생성한 것은 지난주 토요일쯤이었으나 사용법에 익숙하지 않아서 그 즉시 파일을 올리지는 못했다. 소개용 문서(README.md)를 얼추 만든 뒤에도 계속 APS 쪽에서 버그를 잡느라 업로드가 늦어졌다.

이제는 사무실에서 전체 프로젝트를 git 명령어로 가져온 뒤에 확인하거나 추가 작업 후 곧바로 GitHub에 올리는 것이 가능해졌다. 집 PC에서만 작업을 할 필요가 없고, 버전 관리도 그만큼 수월해졌다.

사무실의 파이썬 환경에서는 MIDI 포트를 열지 못하는 에러가 발생하였으나, python.org에서 공식 3.11.6 버전을 설치함으로써 모든 문제가 해결되었다. 다음의 이미지는 사무실에서 실행하는 APS(Ardule Pattern Studio)의 스크린샷이다. Default MIDI OUT은 Microsoft GS Wavetable Synth이고, 다른 하드웨어 MIDI 인터페이스가 연결되어 있다면 그것에 우선권을 준다. 물론 APS 안에서 선택할 수 있다.

'aps_main.py'으로 이 작은 프로그램이 열린다. 그러나 총 10개의 파이썬 스크립트가 뒤에서 돌아가고 있다. 어느 PC든 Microsoft GS Wavetable Synth가 내장되어 있으므로 재생음을 들을 수 있다.


APS의 개발 막바지 단계에서 특히 공을 들인 것은 패턴 에디트(Step Edit) 기능이다. 사실 온라인에 널린 드럼 패턴 생성기에서는 그리드 형태로 표현된 패턴에 직접 클릭을 하여 원하는 드럼 패턴을 만들 수 있다. 나의 APS Step Seq에서는 강약 표현이 가능하고 기본 악기의 수도 12개나 된다.

APS의 Step Sequencer. 편집 즉시 들어보기 역시 가능하다. 1-bar 단위로 편집을 하며, 앞의 것을 뒤 마디에 덮어씌울 수도 있다.


이 시스템의 강점은 2-bar drum pattern을 텍스트 파일(ADT v2.2a)로 규격화했다는 것이다. 인간이 이해하기 쉬운 포맷으로서, 아무 텍스트 에디터로 편집해도 된다. 그리고 내가 배포하는 스크립트로 ADP 'binary cache'로 전환하면, 아두이노처럼 저사양의 하드웨어에서도 매끄럽게 재생이 이루어진다. 

나는 전문 개발자는 아니지만 전체 목표와 단계별 목표를 염두에 두고 점진적으로 기능을 구현해 나가면서 specification 문서를 먼저 만들어서 참조하고 있다. 여기에 GitHub 활용까지 더해지니 작업의 능률이 훨씬 올랐다. 다음과 같이 나의 작업 방식을 기술할 수 있겠다.

I am not a professional software developer by training, but I approach development with a clear view of overall goals and intermediate milestones. I implement features incrementally, guided by explicit specifications that are written first and used as references throughout development.

취미를 위한 소프트웨어 개발에 너무 몰두하여 요즘은 책 읽은 일을 소홀히하게 되었다. 어쩌면 아내보다 챗GPT와 더 많은 대화를 나누고 있는지도... 건강을 해치지 않는 수준에서 적당히 해야 되겠다. 생각보다 2025년 12월에 많은 일을 하였다. 검색 엔진 최적화, 비트맵 이미지의 벡터화, 그리고 GitHub 생태계에 입문하기. 




2025년 12월 12일 금요일

저해상도 로고 이미지를 두 단계에 걸쳐 SVG로 전환해 보자

무료 로고 사이트에서 저해상도 래스터 이미지로 만든 나의 GenoGlobe.com 로고를 벡터 형식으로 전환해 보고 싶은 마음에 파워포인트에서 이런 미련한 일을 한 적이 있었다(2023년형 GenoGlobe 로고를 새로 만들다).

초승달 모양 사이의 간격이 일정하지 않은 것이 오히려 미학적 완성도를 높인다.

이것은 로고를 구성하는 원의 직경을 대략 측정하기 위함이었다. 여기에서 얻은 중간 정보를 이용하여 LibreCAD에서 열심히 도형을 그렸는데, 요령이 부족하여 최종적으로 벡터화는 하지 못하였다. CAD 프로그램의 결과 파일(DXF)은 벡터임이 분명한데, 이를 SVG로 만드는 방법을 그당시에는 찾지 못하였다. 특히 속이 채워진 도형은 다루기가 더 까다로웠던 것 같다. 원리적으로 말하자면 이미지를 역엔지니어링하기 위한 '기하학적인 접근'인 셈이다.

그로부터 벌써 2년 반의 시간이 지났고, ChatGPT를 이용하면 이제는 완벽한 벡터 이미지를 얻을 수 있지 않을까 생각하게 되었다. 색상은 배경을 제외하면 단 두 가지에 불과하고, 테두리를 구성하는 도형은 전부 원호이다. 또한 원의 중심은 전체 도형의 틀을 이루는 정사각형의 대각선 위에 있다. 따라서 챗GPT에게 부탁하면 쉬운 방법을 알려줄 것이라고 생각했다.

Inkscape의 '경로(=Path) -> 비트맵 따라 그리기'에서 PNG 이미지를 직접 다루는 것도 좋은데, 내가 최종적으로 택한 방법은 파이썬 스크립트(png_go_svg_autograce.py)를 써서 대략적으로 벡터 이미지를 얻은 다음에 Inkscape에서 처리하는 것이었다. 

이 스크립트는 경계선이 폴리곤 형태인 벡터 이미지를 생성한다. 




Inkscape에서 이 중간본 SVG 파일을 읽은 뒤 각 'path'가 분리되어 있는 것을 확인한 다음 전체를 선택하여 '경로-> 단순화 (Control + L)'을 단 한번만 실시하여 노드를 줄이면 아무리 확대해도 매끈한 최종 결과물이 나온다. 처음부터 PNG를 입력하여 '비트맵 따라 그리기'를 해도 되지만 약간의 후처리가 필요하다. 실제로 해 보면 확대했을 경우 파란색 초승달 모양 테두리에 노란색 찌꺼기가 남는 게 보인다. 이를 없애려면 차집한 조작을 하거나, 지우개로 아주 섬세하게 지워야 한다. 지울 때에는 남색 개체를 보이지 않게 해 두는 것이 안전하다.

Inkscape에서 경로는 대단히 중요한 개념이다. 이는 점(node)과 그 사이를 잇는 곡선(세그먼트)로 정의된 수학적 도형의 설계도에 해당한다. 오늘 사용한 파이썬 스크립트에서는 색상을 바탕으로 픽셀 경계를 추적하여 경계점을 그대로 연결하고 곡선 피팅을 하지 않았다. 즉, 의도적으로 베지어(Bézier)를 쓰지 않고 폴리곤(아주 많은 짧은 직선)으로 근사한 벡터화였다. 

보다 정밀한 방법은 1) potrace를 호출에 베지어 SVG를 생성하거나 2) 원호 기반으로 베지어/원호로 출력하면 된다고 한다. Inkscape의 비트맵 추적은 내부적으로 potrace 계열 알고리즘을 쓴다.

다음은 챗GPT가 만들어 준 png_go_svg_autotrace.py 전체이다.

#!/usr/bin/env python
"""
PNG 로고(3색: 흰/노랑/남색)를 자동으로 벡터(SVG)로 변환하는 스크립트.

사용법:
    python png_to_svg_autotrace.py input_logo.png

결과:
    input_logo_autotrace_transparent.svg
    input_logo_autotrace_whitebg.svg
"""

import sys
import os
from typing import Tuple, List

import numpy as np
from PIL import Image
from skimage import measure
import svgwrite


# ---- 1. 색상 기준 정의 ------------------------------------------------------
# 로고에서 사용한 대략적인 색상 (필요하면 조정 가능)
YELLOW_REF = np.array([242, 224, 89], dtype=np.float32)   # #f2e059 근처
NAVY_REF   = np.array([14,  27,  66], dtype=np.float32)   # #0e1b42 근처
WHITE_REF  = np.array([255, 255, 255], dtype=np.float32)  # 흰 배경

REF_COLORS = np.stack([YELLOW_REF, NAVY_REF, WHITE_REF], axis=0)
REF_NAMES = ["yellow", "navy", "white"]
REF_HEX = {
    "yellow": "#f2e059",
    "navy":   "#0e1b42",
    "white":  "#ffffff",
}


# ---- 2. 유틸 함수들 ---------------------------------------------------------

def classify_colors(arr: np.ndarray) -> np.ndarray:
    """
    RGB 이미지 배열(arr: H x W x 3)을 3개 참조 색(노랑/남색/흰색)에
    가장 가까운 색으로 분류하여 label map(H x W)을 반환.
    label: 0=yellow, 1=navy, 2=white
    """
    h, w, _ = arr.shape
    img_flat = arr.reshape(-1, 3).astype(np.float32)

    # 각 픽셀과 참조 색상 사이의 제곱 거리 계산
    # result: (N_pixels, 3)
    diff = img_flat[:, None, :] - REF_COLORS[None, :, :]
    dist2 = np.sum(diff ** 2, axis=-1)

    labels_flat = np.argmin(dist2, axis=1)
    labels = labels_flat.reshape(h, w)
    return labels


def find_color_contours(mask: np.ndarray, min_length: int = 50) -> List[np.ndarray]:
    """
    색상 마스크(불리언 배열 또는 0/1 배열)에서 외곽선을 찾고
    일정 길이(min_length) 이상인 contour들만 반환.
    """
    # skimage.measure.find_contours는 값이 0~1 범위인 float 배열에 대해 동작
    arr = mask.astype(float)
    contours = measure.find_contours(arr, 0.5)
    # 너무 짧은 contour는 제거
    contours = [c for c in contours if c.shape[0] >= min_length]
    return contours


def contours_to_svg_paths(
    dwg: svgwrite.Drawing,
    contours: List[np.ndarray],
    fill_color: str
):
    """
    contour 리스트를 SVG path로 추가한다.
    contours: 각 요소는 (N_points, 2) 배열, (row, col) = (y, x)
    """
    for c in contours:
        if len(c) < 2:
            continue
        # skimage 컨투어는 (row, col) = (y, x)
        # SVG는 (x, y) 순서, 좌상단 (0,0), 아래로 y+
        commands = []
        y0, x0 = c[0]
        commands.append(f"M {x0:.3f} {y0:.3f}")
        for y, x in c[1:]:
            commands.append(f"L {x:.3f} {y:.3f}")
        commands.append("Z")
        d_attr = " ".join(commands)
        dwg.add(dwg.path(d=d_attr, fill=fill_color))


# ---- 3. 메인 처리 -----------------------------------------------------------

def png_to_svg(input_path: str):
    # 1) 이미지 로드
    img = Image.open(input_path).convert("RGB")
    arr = np.array(img)
    h, w, _ = arr.shape

    # 2) 색상 분류 (노랑/남색/흰색)
    labels = classify_colors(arr)

    # 3) 각 색상별 마스크 생성
    masks = {
        "yellow": (labels == 0),
        "navy":   (labels == 1),
        "white":  (labels == 2),
    }

    # 4) 각 색상별 외곽선 추출
    contours_by_color = {}
    for name, mask in masks.items():
        if name == "white":
            # 배경은 SVG 도형으로 따로 그리지 않고, 필요 시 rect로 처리
            continue
        contours = find_color_contours(mask, min_length=80)
        contours_by_color[name] = contours

    # 5) 출력 파일 이름
    base, _ = os.path.splitext(input_path)
    out_transparent = base + "_autotrace_transparent.svg"
    out_whitebg    = base + "_autotrace_whitebg.svg"

    # 6) SVG (투명 배경)
    dwg_t = svgwrite.Drawing(out_transparent,
                             size=(w, h),
                             viewBox=f"0 0 {w} {h}")

    # 노랑 먼저, 그 위에 남색 (어차피 픽셀 분류상 겹치지 않지만, 시각적으로도 자연스러운 순서)
    if "yellow" in contours_by_color:
        contours_to_svg_paths(dwg_t, contours_by_color["yellow"], REF_HEX["yellow"])
    if "navy" in contours_by_color:
        contours_to_svg_paths(dwg_t, contours_by_color["navy"], REF_HEX["navy"])

    dwg_t.save()
    print(f"[OK] Transparent SVG saved to: {out_transparent}")

    # 7) SVG (흰 배경)
    dwg_w = svgwrite.Drawing(out_whitebg,
                             size=(w, h),
                             viewBox=f"0 0 {w} {h}")
    # 흰 사각형 배경
    dwg_w.add(dwg_w.rect(insert=(0, 0), size=(w, h), fill=REF_HEX["white"]))
    # 도형 덮어그리기
    if "yellow" in contours_by_color:
        contours_to_svg_paths(dwg_w, contours_by_color["yellow"], REF_HEX["yellow"])
    if "navy" in contours_by_color:
        contours_to_svg_paths(dwg_w, contours_by_color["navy"], REF_HEX["navy"])

    dwg_w.save()
    print(f"[OK] White-background SVG saved to: {out_whitebg}")


# ---- 4. 실행부 ----------------------------------------------------------------

def main():
    if len(sys.argv) < 2:
        print("Usage: python png_to_svg_autotrace.py input_logo.png")
        sys.exit(1)

    input_path = sys.argv[1]
    if not os.path.exists(input_path):
        print(f"[ERROR] File not found: {input_path}")
        sys.exit(1)

    png_to_svg(input_path)


if __name__ == "__main__":
    main()

원본 이미지가 완전히 기하학적인 도형으로만 이루어져 있어고 색상도 매우 단순했기 때문에 이런 간단한 스크립트로 일차적인 벡터화가 성공적으로 되었다. 베지어 트레이싱은 저품질 알리아싱(aliasing)을 다룰 때에는 아주 취약하다.

베지어 트레이싱은
알리아싱된 저품질 래스터 이미지의 테두리를
복원하는 데 본질적으로 취약하다.
이는 곡률 정보가 이미 샘플링 단계에서 손실되기 때문이다.

오늘 얻은 최종 결과물을 여기에 소개한다.

출처: DokuWiki 설정(GenoGlobe.com 위키)


글을 마무리하면서 챗GPT에게 물어보니 LibreCAD 파일을 SVG로 전환하는 방법이 나온다. 허! 이걸 정리해서 블로그에 쓸 이유가 있을까? 누구나 질문을 던지면 다 알게 되는 세상인데...