의약품 반품 보상 프로그램 만들기 — 단계별 강의 (2편)
1편에서 만든 "두뇌"(파서·대조·보상)는 바코드 문자열을 입력으로 받았다. 그런데 그 문자열은 어디서 오나? 이미지(사진) 속 GS1 DataMatrix를 디코드해서 얻는다. 2편은 바로 그 부분이다.
2편을 끝내면: 이미지 파일을 넣으면 → 그 안의 DataMatrix를 (여러 개라도) 읽어 문자열 + 위치 좌표로 돌려주고, 큰 사진 속 작은 코드나 사용자가 고른 영역도 처리하는 클래스를 갖게 된다. 여전히 콘솔에서 테스트한다. (화면 UI는 3편)
이번 편은 요청하신 대로 설명을 더 자세히 넣었다. 개념이 처음이면 📖과 💡 박스를 천천히 읽자.
들어가기 전에 — 큰 그림
[사진/이미지 파일]
│ (2편에서 만드는 부분)
▼
┌─────────────────────────┐
│ BarcodeServiceCpp │ ← ZXingCpp(디코드) + ImageSharp(이미지 처리)
│ "이미지 → 코드 문자열 + 위치" │
└─────────────────────────┘
│ "(01)08806443096813(17)280315(10)6248607(21)..."
▼
┌─────────────────────────┐
│ Gs1Parser (1편) │ ← 문자열 → GTIN/유통기한/로트
└─────────────────────────┘
▼
┌─────────────────────────┐
│ Repository + Compensation │ ← 대조 + 보상 (1편)
└─────────────────────────┘
즉 2편의 산출물 BarcodeServiceCpp는 1편의 Gs1Parser 앞에 붙는 "눈" 역할이다.
Lesson 5 — 바코드 디코딩 기초
🎯 목표
NuGet 패키지를 설치하고, 이미지 파일 하나에서 DataMatrix를 읽어 문자열로 출력한다. 테스트용 코드 이미지도 직접 만든다.
📖 개념 1: NuGet 패키지란?
NuGet은 .NET의 "라이브러리 앱스토어"다. 남이 만든 기능 묶음(패키지)을 프로젝트에 한 줄로 추가한다. 우리가 쓸 패키지:
패키지 역할 비고
| ZXingCpp | 바코드 디코드(zxing-cpp의 .NET 래퍼) | 네이티브 라이브러리 포함, 다중 판독이 기본 |
| SixLabors.ImageSharp | 이미지 로드·자르기·확대·흑백 처리 | 2.1.x = 무료(Apache-2.0) |
| ZXing.Net | 바코드 생성(테스트 이미지 만들기용) | 디코드는 ZXingCpp가 더 강함 |
💡 왜 디코드는 ZXingCpp, 생성은 ZXing.Net? 디코드(읽기)는 정확도가 생명이라 더 강한 zxing-cpp(C++ 엔진)를 쓴다. 생성(쓰기)은 테스트 이미지를 만드는 용도라 순수 .NET인 ZXing.Net이면 충분하다. 역할에 맞게 도구를 나눈 것이다.
📖 개념 2: DataMatrix가 뭔가
DataMatrix는 정사각형 안에 작은 흑백 점(모듈)으로 데이터를 담는 2D 바코드다.
- 왼쪽·아래 변의 실선 L자(finder pattern)로 스캐너가 위치·방향을 찾는다.
- 위·오른쪽 변의 점선(timing pattern)으로 가로·세로 칸 수를 센다.
- 오류정정(Reed-Solomon) 덕분에 일부(최대 ~30%)가 손상돼도 복원된다. (단, 인쇄가 흐려 데이터 자체가 뭉개지면 한계가 있다 — 7편쯤 실제로 겪는다.)
- 의약품은 보통 GS1 DataMatrix: 맨 앞에 FNC1 표시가 있고, GTIN·유통기한·로트·일련번호를 AI로 담는다(1편 파서가 처리하는 그것).
DataMatrix 구조(개념도)
┌ ─ ─ ─ ─ ┐ ← timing(점선): 칸 수
█ ░ █ ░ █ ░ 데이터(흑/백 모듈)
█ █ ░ █ ░ █
█ ░ ░ █ █ ░
█ █ █ █ █ █ ← finder(실선 L): 위치/방향
└ 실 선 변 ┘
📖 개념 3: 이미지를 디코더에 넘기는 흐름
ZXingCpp는 이미지를 ImageView 라는 형태로 받는다. 이건 "픽셀 바이트 배열 + 가로·세로 크기 + 픽셀 형식(RGB 등)"을 묶은 얇은 포장지다. 그래서 흐름은:
이미지 파일 ──(ImageSharp로 로드)──▶ Rgb24 이미지
──(픽셀을 byte[]로 복사)──▶ rgb 바이트 배열
──(ImageView로 포장)──▶ ZXingCpp에 전달 ──▶ 결과 목록
💡 왜 RGB 바이트 배열이 필요한가? 디코더는 "픽셀 색이 죽 늘어선 숫자 배열"만 이해한다. JPG/PNG는 압축된 파일이라 그대로는 못 읽는다. 그래서 ImageSharp가 압축을 풀어 Rgb24(픽셀당 R,G,B 3바이트) 이미지로 만들고, 그 픽셀들을 byte[]로 복사해 넘긴다.
💻 코드 (5-1) 패키지 설치 & 프로젝트 설정
1편의 콘솔 프로젝트에 패키지를 추가한다. VS2022: 도구 → NuGet 패키지 관리자 → 솔루션용 NuGet 관리 → 검색해 설치. 또는 .csproj를 직접 편집:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>PharmaReturn</RootNamespace>
<!-- ZXingCpp 네이티브 라이브러리는 64비트다. 64비트로 고정. -->
<Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ZXingCpp" Version="0.5.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.9" />
<PackageReference Include="ZXing.Net" Version="0.16.10" />
</ItemGroup>
<ItemGroup>
<None Include="data\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
💡 <PlatformTarget>x64</PlatformTarget> 가 왜 중요? ZXingCpp는 안에 C++로 만든 네이티브 .dll을 품고 있는데 64비트다. 프로젝트가 32비트(AnyCPU+Prefer32bit)면 그 dll을 못 불러 DllNotFoundException이 난다. 그래서 64비트로 못 박는다.
💻 코드 (5-2) 테스트용 코드 이미지 만들기
카메라가 없어도 테스트하려면, 우리가 코드 이미지를 만들어 그걸 다시 읽으면 된다(닫힌 루프). TestImageMaker.cs 추가:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using ZXing;
using ZXing.Common;
using ZXing.Datamatrix;
namespace PharmaReturn;
/// <summary>테스트용 GS1 DataMatrix PNG를 만든다(생성=ZXing.Net).</summary>
public static class TestImageMaker
{
public static void Generate(string content, string outPath, int codePx = 240)
{
var hints = new Dictionary<EncodeHintType, object>
{
{ EncodeHintType.GS1_FORMAT, true }, // 맨 앞에 FNC1(=GS1 표시) 추가
{ EncodeHintType.MARGIN, 1 }, // 코드 둘레 여백(quiet zone)
};
// 1) 내용 → 흑백 격자(BitMatrix)로 인코딩
BitMatrix matrix = new DataMatrixWriter()
.encode(content, BarcodeFormat.DATA_MATRIX, codePx, codePx, hints);
// 2) 격자를 PNG로 그리기 (true=검정, false=흰색)
using var img = new Image<L8>(matrix.Width, matrix.Height);
for (int y = 0; y < matrix.Height; y++)
for (int x = 0; x < matrix.Width; x++)
img[x, y] = new L8(matrix[x, y] ? (byte)0 : (byte)255);
img.Save(outPath);
}
}
🔍 한 줄씩:
- DataMatrixWriter().encode(...): 내용을 흑백 격자(BitMatrix)로 만든다. GS1_FORMAT=true면 GS1 규칙대로 FNC1을 앞에 붙인다.
- Image<L8>: 8비트 회색(L=Luminance) 이미지. 0=검정, 255=흰색.
- 격자의 각 칸을 픽셀로 칠해 PNG로 저장.
💻 코드 (5-3) 가장 단순한 디코더
BarcodeServiceCpp.cs 추가(이번 Lesson 버전 — 다음 Lesson에서 키운다):
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using ZXingCpp;
using SsImage = SixLabors.ImageSharp.Image; // ← 이름 충돌 회피(아래 💡)
namespace PharmaReturn;
public static class BarcodeServiceCpp
{
/// <summary>이미지 한 장에서 DataMatrix 문자열을 모두 읽는다.</summary>
public static List<string> DecodeAll(string imagePath)
{
// 1) 이미지 로드 → RGB 픽셀 바이트로 복사
using var image = SsImage.Load<Rgb24>(imagePath);
var rgb = new byte[image.Width * image.Height * 3];
image.CopyPixelDataTo(rgb);
// 2) ZXingCpp가 이해하는 ImageView로 포장
var view = new ImageView(rgb, image.Width, image.Height, ImageFormat.RGB);
// 3) 디코더 설정: DataMatrix만, 반전/회전도 시도
var reader = new BarcodeReader
{
Formats = BarcodeFormat.DataMatrix, // 포맷 한정 → 빠르고 정확
TryInvert = true, // 흰 바탕 검은 코드의 반전도 시도
TryRotate = true, // 돌아간 코드도 시도
};
// 4) From()은 찾은 모든 바코드 목록을 돌려준다(다중 판독이 기본)
var results = new List<string>();
foreach (var b in reader.From(view))
if (b.IsValid && !string.IsNullOrEmpty(b.Text))
results.Add(b.Text);
return results;
}
}
💡 using SsImage = SixLabors.ImageSharp.Image; 이게 뭐죠? ZXingCpp에도 Image라는 타입이 있고 SixLabors.ImageSharp에도 Image가 있다. 둘 다 using 하면 컴파일러가 "어느 Image?"라며 모호 참조 오류를 낸다(실제로 만들다 보면 만나는 흔한 에러다). 그래서 ImageSharp의 Image에 SsImage라는 별명을 붙여 명확히 가리킨다. Rgb24는 ImageSharp에만, ImageView·BarcodeReader는 ZXingCpp에만 있어 충돌이 없다.
✅ 직접 해보기
Program.cs를 잠깐 이렇게:
using PharmaReturn;
string path = Path.Combine(AppContext.BaseDirectory, "test.png");
// 1) 테스트 코드 만들기 (괄호 없는 원시형으로 생성)
TestImageMaker.Generate("010880644309681317280315106248607", path);
// 2) 다시 읽기
foreach (var code in BarcodeServiceCpp.DecodeAll(path))
Console.WriteLine("읽음: " + code);
읽음: ...08806443096813...280315...6248607... 처럼 나오면 성공. (생성 옵션에 따라 GS 구분자가 보이지 않게 섞일 수 있는데, 1편 파서가 처리하니 걱정 말자.)
Lesson 6 — 위치 좌표 + 다중 판독
🎯 목표
바코드의 **문자열뿐 아니라 위치(네 꼭짓점)**도 함께 얻는다. 이건 나중에 화면에서 파란 박스를 그릴 재료다.
📖 개념 1: 왜 위치가 필요한가
3편 화면에서 "이 박스를 읽었어요"를 사람이 보려면, 코드가 이미지의 어디에 있는지 알아야 한다. 디코더는 각 코드의 네 꼭짓점 좌표(좌상·우상·우하·좌하)를 함께 알려준다.
(TopLeft)───────(TopRight)
│ DataMatrix │
(BottomLeft)────(BottomRight)
이 좌표는 이미지 픽셀 좌표다. 즉 "원본 이미지에서 왼쪽 위가 (0,0), 오른쪽으로 X, 아래로 Y"인 좌표계. (화면에 그릴 땐 확대/이동을 반영해 변환해야 하는데, 그건 3편에서.)
📖 개념 2: 결과를 담을 그릇 DecodedCode
문자열 + 꼭짓점 4개 + 유효 여부를 한 묶음으로 만든다. 1편에서 배운 record가 딱이다.
public sealed record DecodedCode(string Text, System.Drawing.Point[] Corners, bool Valid);
💡 System.Drawing.Point는 "정수 좌표 한 점(X,Y)"을 담는 표준 타입이다. 크로스플랫폼 콘솔(net8.0)에서도 쓸 수 있다(System.Drawing.Primitives에 포함). 매번 길게 쓰기 싫어서 파일 위에서 using DrawPoint = System.Drawing.Point; 별명을 준다.
📖 개념 3: "좌표 변환 함수"를 미리 끼워두는 설계
다음 Lesson(타일)에서, 잘라낸 조각 안의 좌표를 원본 이미지 좌표로 되돌려야 한다. 그래서 디코딩 핵심부를 만들 때, "꼭짓점을 어떻게 변환할지"를 함수로 받아두면 전체/조각 모두 같은 코드로 처리할 수 있다. 이게 Func<int,int,DrawPoint> transform다.
- 전체 이미지일 때: (x, y) => 그대로 (x, y) — 변환 없음.
- 조각(타일)일 때: (x, y) => (타일위치 + x/확대배율, ...) — 원본 좌표로 환산.
💡 처음 보면 "함수를 인자로 넘긴다"가 낯설 수 있다. 요리로 비유하면, 디코딩 함수는 "재료를 그릇에 담는 일"만 하고, **담기 직전에 적용할 양념(좌표 변환)**을 호출자가 골라 건네는 것이다. 덕분에 같은 디코딩 함수를 전체/조각에 재사용한다.
💻 코드
BarcodeServiceCpp.cs를 아래로 키운다(전체 이미지 다중 판독 + 위치).
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using ZXingCpp;
using SsImage = SixLabors.ImageSharp.Image;
using DrawPoint = System.Drawing.Point;
namespace PharmaReturn;
/// <summary>판독 결과 1건: 원문 + 4꼭짓점(원본 이미지 픽셀 좌표).</summary>
public sealed record DecodedCode(string Text, DrawPoint[] Corners, bool Valid);
public static class BarcodeServiceCpp
{
/// <summary>전체 이미지에서 다중 판독(위치 포함).</summary>
public static List<DecodedCode> DecodeDetailed(string imagePath)
{
using var image = SsImage.Load<Rgb24>(imagePath);
// 전체 이미지: 좌표 변환 없음 → 그대로
return DecodeView(image, (x, y) => new DrawPoint(x, y));
}
/// <summary>문자열만 필요할 때(하위 호환).</summary>
public static List<string> DecodeAll(string imagePath)
=> DecodeDetailed(imagePath).Select(c => c.Text).ToList();
/// <summary>
/// 한 장의 (이미 준비된) 이미지를 디코드한다.
/// transform: 디코더가 준 조각-내부 좌표(cx,cy)를 원본 좌표로 바꾸는 함수.
/// </summary>
private static List<DecodedCode> DecodeView(Image<Rgb24> img, Func<int, int, DrawPoint> transform)
{
var rgb = new byte[img.Width * img.Height * 3];
img.CopyPixelDataTo(rgb);
var view = new ImageView(rgb, img.Width, img.Height, ImageFormat.RGB);
var reader = new BarcodeReader
{
Formats = BarcodeFormat.DataMatrix,
TryInvert = true,
TryRotate = true,
};
var list = new List<DecodedCode>();
foreach (var b in reader.From(view))
{
if (!b.IsValid || string.IsNullOrEmpty(b.Text)) continue;
// Position: 네 꼭짓점(PointI: X, Y).
// ※ 설치된 ZXingCpp 버전에 따라 멤버명이 다를 수 있다(아래 ⚠️).
var p = b.Position;
var corners = new[]
{
transform(p.TopLeft.X, p.TopLeft.Y),
transform(p.TopRight.X, p.TopRight.Y),
transform(p.BottomRight.X, p.BottomRight.Y),
transform(p.BottomLeft.X, p.BottomLeft.Y),
};
list.Add(new DecodedCode(b.Text, corners, true));
}
return list;
}
}
🔍 핵심:
- DecodeDetailed는 DecodeView에 "변환 안 함" 함수를 넘긴다(전체 이미지니까).
- b.Position에서 네 꼭짓점을 꺼내 transform을 거쳐 Corners에 담는다.
- 결과는 DecodedCode 목록.
⚠️ 버전 주의(중요): b.Position.TopLeft.X 부분은 ZXingCpp 버전에 따라 멤버 이름이 다를 수 있다(예: Position이 배열/인덱서이거나 TopLeft가 소문자거나). 빌드에서 이 줄에 에러가 나면, 설치된 버전의 Barcode/Position 정의를 보고 그 이름에 맞춰 바꾸면 된다. 나머지 구조는 그대로 둔다.
✅ 직접 해보기
using PharmaReturn;
string path = Path.Combine(AppContext.BaseDirectory, "test.png");
TestImageMaker.Generate("010880644309681317280315106248607", path);
foreach (var c in BarcodeServiceCpp.DecodeDetailed(path))
{
Console.WriteLine($"문자열: {c.Text}");
Console.WriteLine($"좌상 꼭짓점: ({c.Corners[0].X},{c.Corners[0].Y})");
}
문자열과 좌상 꼭짓점 좌표가 같이 나오면 성공. (좌표는 코드가 그려진 위치 픽셀이다.)
Lesson 7 — 타일 판독(대량·작은 코드) + 영역 판독
🎯 목표
큰 사진 한 장에 작은 코드가 여러 개여도 최대한 다 읽는다. 또 사용자가 고른 특정 영역만 다시 읽는다.
📖 개념 1: 왜 전체 디코드로는 부족한가
스마트폰 사진은 크다(예: 1152×2560). 그 안에 코드가 작게 여러 개면, 디코더가 전체에서 한두 개만 찾고 만다. 코드가 화면 대비 너무 작아 "안 보이는" 셈이다.
해결책 = 타일 분할. 이미지를 작은 사각형(타일)으로 쪼개 각 타일을 디코드하면, 그 타일 안에선 코드가 상대적으로 커져 잘 읽힌다. 거기에 타일을 확대(upscale) 까지 하면 더 좋아진다.
큰 사진을 격자로 쪼갬(겹치게) 각 타일을 확대해서 디코드
┌────┬────┬────┐ ┌──────────┐
│ ▥ │ ▥ │ ▥ │ ──▶ 타일1 ──▶ │ ▥▥▥ │ ──▶ 읽힘!
├────┼────┼────┤ └──────────┘
│ ▥ │ ▥ │ ▥ │
└────┴────┴────┘
📖 개념 2: 겹침(overlap)이 왜 필요한가
코드가 타일 경계에 걸치면 잘려서 못 읽는다. 그래서 타일을 50% 정도 겹치게 이동시키면, 경계에 걸린 코드도 옆 타일에선 온전히 들어온다.
겹침 없음: 경계의 코드가 반토막 ✗
|‾‾‾‾|‾‾‾‾|
| ▥▮|▮▥ | ← 코드가 두 타일에 쪼개짐
겹침 50%: 다음 타일이 절반 겹쳐 온전히 포함 ✓
|‾‾‾‾|
|‾‾‾‾| ← 이 타일 안에 코드 전체가 들어옴
- step = tile × (1 - overlap) 만큼만 이동한다. overlap=0.5면 타일 크기의 절반씩 이동.
📖 개념 3: 좌표 되돌리기(이게 transform의 진짜 쓸모)
타일을 잘라 3배 확대해서 디코드하면, 디코더가 주는 꼭짓점은 "확대된 조각 안" 좌표다. 이걸 원본 좌표로 되돌려야 화면에 제 위치에 그린다.
- 조각은 원본의 (ox, oy)에서 잘렸고, up배 확대됐다.
- 그러니 조각 내부 좌표 (cx, cy) → 원본 좌표 = (ox + cx/up, oy + cy/up).
이게 (cx, cy) => new DrawPoint(ox + cx / up, oy + cy / up) 한 줄의 의미다. Lesson 6에서 "변환 함수를 받아두는" 설계를 한 이유가 여기서 빛난다.
📖 개념 4: 중복 제거(dedup)
타일이 겹치니 같은 코드가 여러 타일에서 잡힌다. 그래서 문자열을 키로 딕셔너리에 담아(TryAdd) 같은 내용은 한 번만 남긴다. (의약품은 일련번호(21)가 낱개마다 달라 문자열이 전부 달라서, 서로 다른 박스가 합쳐질 걱정은 없다.)
💻 코드 (최종형)
BarcodeServiceCpp.cs를 최종 버전으로 키운다. (Lesson 6 버전에 타일/영역/확대 처리를 더한 것)
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; // Crop, Resize, Grayscale, Contrast
using ZXingCpp;
using SsImage = SixLabors.ImageSharp.Image;
using DrawPoint = System.Drawing.Point;
using DrawRect = System.Drawing.Rectangle;
namespace PharmaReturn;
public sealed record DecodedCode(string Text, DrawPoint[] Corners, bool Valid);
public static class BarcodeServiceCpp
{
/// <summary>전체 + 타일 다중 패스로 가능한 모든 코드를 위치와 함께 회수.</summary>
public static List<DecodedCode> DecodeDetailed(string imagePath)
{
using var image = SsImage.Load<Rgb24>(imagePath);
var byText = new Dictionary<string, DecodedCode>();
// 1) 전체 이미지 한 번
foreach (var c in DecodeView(image, (x, y) => new DrawPoint(x, y)))
byText.TryAdd(c.Text, c);
// 2) 타일 패스: (타일 크기, 확대배율) 두 조합으로 작은 코드 회수
foreach (var (tile, up) in new[] { (512, 2), (320, 3) })
TilePass(image, tile, overlap: 0.5, upscale: up, byText);
return byText.Values.ToList();
}
/// <summary>지정 영역만 확대해 재판독(꼭짓점은 원본 좌표로 보정).</summary>
public static List<DecodedCode> DecodeRegion(string imagePath, DrawRect region)
{
using var image = SsImage.Load<Rgb24>(imagePath);
// 영역이 이미지 밖으로 나가지 않게 교집합으로 자른다
var r = DrawRect.Intersect(region, new DrawRect(0, 0, image.Width, image.Height));
if (r.Width <= 1 || r.Height <= 1) return new();
const int up = 2;
using var crop = image.Clone(c => c.Crop(new Rectangle(r.X, r.Y, r.Width, r.Height))
.Resize(r.Width * up, r.Height * up).Grayscale());
var byText = new Dictionary<string, DecodedCode>();
foreach (var c in DecodeView(crop, (cx, cy) => new DrawPoint(r.X + cx / up, r.Y + cy / up)))
byText.TryAdd(c.Text, c);
return byText.Values.ToList();
}
public static List<string> DecodeAll(string imagePath)
=> DecodeDetailed(imagePath).Select(c => c.Text).ToList();
// ── 내부 ───────────────────────────────────────────────
private static void TilePass(Image<Rgb24> image, int tile, double overlap, int upscale,
Dictionary<string, DecodedCode> byText)
{
int step = Math.Max(1, (int)(tile * (1 - overlap))); // 겹침만큼 덜 이동
for (int y = 0; y < image.Height; y += step)
for (int x = 0; x < image.Width; x += step)
{
int w = Math.Min(tile, image.Width - x); // 가장자리에서 잘림 방지
int h = Math.Min(tile, image.Height - y);
if (w < 40 || h < 40) continue; // 너무 작은 조각은 스킵
int ox = x, oy = y, up = upscale;
// 조각을 잘라 → 확대 → 흑백 → 대비 강화
using var crop = image.Clone(c => c.Crop(new Rectangle(ox, oy, w, h))
.Resize(w * up, h * up).Grayscale().Contrast(1.2f));
// 조각 내부 좌표 → 원본 좌표로 되돌리는 변환을 넘긴다
foreach (var c in DecodeView(crop, (cx, cy) => new DrawPoint(ox + cx / up, oy + cy / up)))
byText.TryAdd(c.Text, c); // 중복(같은 문자열)은 자동 무시
}
}
private static List<DecodedCode> DecodeView(Image<Rgb24> img, Func<int, int, DrawPoint> transform)
{
var rgb = new byte[img.Width * img.Height * 3];
img.CopyPixelDataTo(rgb);
var view = new ImageView(rgb, img.Width, img.Height, ImageFormat.RGB);
var reader = new BarcodeReader
{
Formats = BarcodeFormat.DataMatrix,
TryInvert = true,
TryRotate = true,
};
var list = new List<DecodedCode>();
foreach (var b in reader.From(view))
{
if (!b.IsValid || string.IsNullOrEmpty(b.Text)) continue;
var p = b.Position;
var corners = new[]
{
transform(p.TopLeft.X, p.TopLeft.Y),
transform(p.TopRight.X, p.TopRight.Y),
transform(p.BottomRight.X, p.BottomRight.Y),
transform(p.BottomLeft.X, p.BottomLeft.Y),
};
list.Add(new DecodedCode(b.Text, corners, true));
}
return list;
}
}
🔍 새로 들어온 것 설명:
- image.Clone(c => c.Crop(...).Resize(...).Grayscale().Contrast(...)): ImageSharp의 처리 체인. 원본을 건드리지 않고 복제본에 자르기→확대→흑백→대비 강화를 순서대로 적용한다. (using System.Drawing.Processing; 필요)
- 흑백·대비를 왜? 디코더는 명암으로 모듈을 구분한다. 색 정보를 빼고 대비를 키우면 인쇄가 옅은 코드도 잘 잡힌다.
- DecodeRegion: 사용자가 고른 사각형(region)만 잘라 2배 확대해 디코드. 3편에서 "영역 판독" 버튼에 연결된다.
- DrawRect.Intersect: 사용자가 이미지 경계 밖까지 드래그해도 안전하게 이미지 안으로 잘라준다.
📖 개념 5(중요): 그래도 안 되는 코드가 있다 — 한계와 운영
타일·확대·대비까지 다 해도, 인쇄 자체가 흐려 데이터가 손상된 코드는 못 읽는다(오류정정 한도 밖). 이건 알고리즘이 아니라 물리적 한계다. 운영에서는:
- 더 정면·근접 촬영 또는 줄(트레이) 단위로 나눠 촬영 → 정상 인쇄분은 거의 다 잡힘.
- 그래도 안 되면 그 박스의 인쇄문자(사람이 읽는 글자)를 OCR로 보완 → 3편에서 만든다.
- 인쇄가 흐린 로트는 입고 검수 단계에서 재라벨/반려.
✅ 직접 해보기
- 여러 코드를 한 장에 만들어 다중 판독 확인:
using PharmaReturn;
string dir = AppContext.BaseDirectory;
// 코드 3장을 각각 만든 뒤, 직접 한 장에 합치긴 번거로우니
// 여기서는 '한 장에 1개' 이미지를 여러 개 만들어 각각 디코드해 본다.
string[] codes =
{
"010880644309681317280315106248607",
"010880644309632517280414106240606",
"010880644304152317290420101190605",
};
for (int i = 0; i < codes.Length; i++)
{
string p = Path.Combine(dir, $"t{i}.png");
TestImageMaker.Generate(codes[i], p);
var found = BarcodeServiceCpp.DecodeDetailed(p);
Console.WriteLine($"t{i}.png → {found.Count}개: {string.Join(" / ", found.Select(c => c.Text))}");
}
각 파일에서 1개씩 읽히면 OK.
- (선택) 실제 스마트폰으로 박스 사진을 찍어 프로젝트에 넣고 DecodeDetailed("내사진.jpg")로 몇 개 잡히는지 보자. 정면·또렷할수록 많이 잡힌다. 전체 1패스보다 타일 패스 덕분에 더 많이 잡히는 걸 체감할 수 있다.
2편 마무리
지금까지:
- TestImageMaker.cs — 테스트용 코드 이미지 생성(ZXing.Net)
- BarcodeServiceCpp.cs — 이미지 → 코드 문자열 + 위치, 다중·타일·영역 판독(ZXingCpp + ImageSharp)
이제 1편 + 2편을 합치면 "이미지 → 코드 문자열 → 파싱 → 대조 → 보상"의 끝에서 끝까지가 콘솔에서 돈다. 한번 직접 이어 붙여 실행해 보자:
using PharmaReturn;
var repo = DeliveryRepository.LoadCsv(Path.Combine(AppContext.BaseDirectory, "data", "deliveries.csv"));
var svc = new CompensationService(repo, today: new DateOnly(2026, 6, 16));
foreach (var c in BarcodeServiceCpp.DecodeDetailed("내사진.jpg")) // ① 이미지 → 코드
{
var f = Gs1Parser.Parse(c.Text); // ② 파싱
var r = svc.Evaluate(new ReturnRequest(f.Gtin!, f.Lot, f.Expiry, 1, ReturnReason.Expiry)); // ③ 보상
Console.WriteLine($"{f.Gtin} {f.Lot} → [{r.Status}] {r.Message}");
}
핵심 복습
- 디코더는 이미지 픽셀 배열(ImageView) 을 먹는다 → ImageSharp로 변환해 넘긴다.
- 다중 판독은 reader.From()이 목록을 주는 것으로 기본 지원.
- 타일 분할 + 확대 + 겹침으로 큰 사진의 작은 코드 회수율을 끌어올린다. 조각 좌표는 transform으로 원본 좌표로 되돌린다.
- 물리적으로 흐린 코드는 한계 → 촬영 개선 또는 OCR 보완(3편).
'개발(IT) > C#(VisualStudio)' 카테고리의 다른 글
| 의약품 반품 보상 프로그램 만들기 — 단계별 강의 (1편) (0) | 2026.06.29 |
|---|---|
| [c#] MS-SQL 연동 Helper (0) | 2023.09.23 |
| [c#] 오라클(Oracle) 연동 Helper (0) | 2023.09.23 |
| 자동 업그레이드 다운로드 (Auto Checker) (0) | 2023.09.14 |
| Visual Studio 2022 설치 순서 (1) | 2023.08.23 |